moonphys

    A deterministic 2D rigid body physics engine in pure MoonBit: SAT collision detection, sequential impulse solver with warm starting, Coulomb friction and restitution, revolute/distance/prismatic/angle-limit/motor joints, sleeping, ray casts and region queries, snapshot JSON with a state digest, plus SVG rendering and a WebAssembly playground.

    physics
    rigid-body
    collision
    simulation
    game
    Download zip
    Author
    Version
    0.2.2
    License
    Apache-2.0
    Last updated
    8 days ago
    Downloads
    60

    No dependencies

    #moonphys

    CI mooncakes license

    纯 MoonBit 实现的确定性 2D 刚体物理引擎 — 用 SAT 做碰撞检测,用顺序冲量法求解接触,支持摩擦、恢复系数、关节、睡眠,并能把每一帧渲染成 SVG。

    一个 2D 物理引擎是游戏引擎、物理教学和机器人仿真里最基础的一块拼图,MoonBit 生态此前没有:mooncakes 上的 chipmunk、box2d、verlet、aabb、collision 等关键词全部为空。moonphys 填的是这个空位,而不是又一个业务工具。

    let world = @moonphys.World::new()

    // 静态地面
    ignore(
    world.add_body(
    @moonphys.Body::fixed(@moonphys.Shape::box(10.0, 0.1)).at(0.0, -0.1),
    ),
    )

    // 一个自由下落的方块
    ignore(world.add_body(@moonphys.Body::box(0.5, 0.5).at(0.0, 4.0)))

    // 固定步长推进 2 秒
    for _ in 0..<120 {
    world.step(1.0 / 60.0)
    }

    println(world.body(1).position().y()) // 0.5:稳稳落在有厚度的地面上

    #特性

    模块内容
    数学基座Vec2、Transform、向量运算符重载、Show 实现
    形状圆、任意凸多边形(逆时针),自动计算质心与转动惯量(平行轴定理)
    宽相增量 sort-and-sweep:复用上一帧的排序数组,只对近序数组做插入排序(每步近乎 O(n));pair 缓冲区与持久接触桶都跨帧复用,tie 按 body 下标打破
    窄相SAT + reference/incident edge 裁剪(圆/圆、多边形/圆、多边形/多边形),返回多点接触
    求解器顺序冲量法 + 持久接触(Arbiter 按 feature id 迁移冲量做暖启动)+ Baumgarte 位置偏置
    摩擦库仑锥,切向冲量钳制在 ±μ·Pn
    恢复系数只在法向相对速度超过阈值时生效,避免静止抖动
    关节DistanceJoint(定长/软弹簧)、RevolveJoint(铰链)、PrismaticJoint(滑轨+行程限位+电动)、AngleLimitJoint(角限位)、MotorJoint(角马达),统一用有效质量矩阵求解
    限位硬限位按纯速度约束求解(不反弹),残余穿出由积分后的位置投影消除
    睡眠线/角速度低于阈值持续 0.5s 后休眠,接触对的动态体互相唤醒
    确定性无随机、无哈希迭代、固定步长 + 稳定排序 → 两次运行逐位一致(有测试)
    渲染render 包把世界快照输出为 SVG,带网格与配色,可逐帧拼成动画
    交互演示playground/ 编译成 wasm-gc,导出 24 个纯数值 pg_* 函数,浏览器里拖拽刚体、点击施力

    #安装

    moon add weopqrst/moonphys

    在需要使用的包的 moon.pkg 中:

    import { "weopqrst/moonphys" @moonphys, }

    #五分钟上手

    // main.mbt
    fn main {
    let world = @moonphys.World::builder()
    .ground(20.0) // 静态地面,上表面 y = 0
    .ball(0.4, 0.0, 6.0) // 半径 0.4,位于 (0, 6)
    .box(0.5, 0.5, 1.5, 8.0) // 半宽 0.5、半高 0.5
    .body(@moonphys.Body::ball(0.3).at(-2.0, 9.0).with_restitution(0.8))
    .build()
    for _ in 0..<240 {
    world.step(1.0 / 60.0)
    }
    println(world.snapshot_json()) // 直接喂给前端/回放
    }

    moon run examples/bounce # 20 行:弹球落地,逐步打印位置与速度 moon run examples/pyramid # 金字塔随动能衰减整体入睡 moon run examples/ragdoll # 四段布娃娃,铰链连成的链条

    World::builder() 只是语法糖,链式写完的场景和逐个 add_body 得到的世界逐位相同(builder_test.mbt 用状态摘要对拍)。需要关节时仍用 add_body 拿回下标再连:

    let world = @moonphys.World::new()
    let a = world.add_body(@moonphys.Body::box(0.2, 0.05).at(0.0, 5.0))
    let b = world.add_body(@moonphys.Body::box(0.2, 0.05).at(0.4, 5.0))
    world.add_revolve_joint(a, b, @moonphys.Vec2::new(0.2, 5.0))

    #演示

    仓库自带一个把物理结果渲染成动画的生成器,输出不依赖任何外部资源的单文件 HTML(每帧一个内联 SVG,浏览器只负责回放):

    moon run cmd/main --target js > docs/index.html

    生成的页面约 2.9 MB,包含三个场景,各 240 帧、4 秒循环(docs/index.html 已按 UTF-8 提交;Windows PowerShell 5.1 的 > 会写成 UTF-16,改用 | Out-File -Encoding utf8 或 git bash):

    1. Pyramid stack — 6 层 21 个方块的金字塔,左上方斜面滚球(摩擦),三颗弹球砸下来。
    2. Five link chain · double pendulum — 5 节链条与双摆,铰链约束在重力下的混沌运动。
    3. Linear actuator · angular motor · soft rope — 电动滑轨推箱、角马达带动的旋转臂与软弹簧绳、重力压在角限位上的跷跷板。

    在线预览:https://mihujiang.github.io/moonphys/。docs/ 由 .github/workflows/pages.yml 在每次变动后自动部署(fork 后首次需要在仓库 Settings → Pages 把 Source 选成 GitHub Actions,工作流自己建不了站点)。

    #可交互 playground(wasm-gc)

    docs/playground/ 是可以直接上手玩的版本:真正的 moonphys 编译成 wasm-gc 后在浏览器里跑,canvas 逐帧渲染,鼠标和滑块直接改物理状态。

    在线打开:https://mihujiang.github.io/moonphys/playground/。本地跑需要一个 HTTP 服务器(file: 协议下浏览器不允许取 .wasm):

    python -m http.server 8123 # 仓库根目录 # 浏览器打开 http://127.0.0.1:8123/docs/playground/

    操作:

    输入效果
    按住刚体拖动用临界阻尼弹簧把刚体拉向光标(pg_grab_*),拖动中照样参与碰撞
    单击空白处半径 3 m 的衰减冲量(pg_blast)
    滚轮缩放视图
    空格 / R / 数字键暂停继续 / 重置当前场景 / 切换到第 n 个场景
    底部滑块重力大小、速度迭代次数

    内置 6 个场景:五层金字塔、九层塔 + 双球、200 圆密集堆、四节链 + 距离关节单摆、马达/滑轨/角限位同场、多形状落斜面。HUD 显示 FPS、刚体数、接触数与已入睡数(睡眠中的刚体半透明绘制)。

    引擎侧只有 playground/engine.mbt:moon.pkg 声明 pkgtype(kind: "foreign_library"),再给函数加 #export_name("pg_..."),ABI 全用 Int32 / Double,字符串和内存视图一律不过边界——所以前端不需要任何胶水代码,WebAssembly.instantiateStreaming 之后直接调用导出函数。

    改完引擎后重新生成产物(docs/playground/engine.wasm 已提交,Pages 才能免工具链直接服务):

    powershell -File tools/build-playground.ps1 # 或 bash tools/build-playground.sh node tools/check-playground.mjs # 校验 24 个导出 + 6 场景各 180 步

    页面自带 ?selftest=1 开关:加载 wasm 后跑完 6 个场景各 180 步、各来一次冲击,再走一遍完整帧循环,把 FPS/刚体/接触/入睡数写进 document.title;无头浏览器用 --dump-dom 就能抓这行结果,例如 selftest ok scenes=6 bodies=253 contacts=24 asleep=15 hud=10/16/24/15。

    #API 速览

    // 世界
    let world = @moonphys.World::new() // 重力默认 (0, -9.81)
    world.set_gravity(@moonphys.Vec2::new(0.0, -3.0)) // 低重力
    world.set_velocity_iterations(10) // 迭代次数,越大越硬
    world.set_sleep_enabled(true)
    world.step(1.0 / 60.0) // 固定步长
    world.run(120, 1.0 / 60.0) // 等价于循环 step
    world.time() // 累计仿真时间
    world.arbiter_count() // 当前接触对
    world.snapshot() // 渲染/测试用的不可变快照
    world.snapshot_json() // 同上,序列化成 JSON
    world.state_hash() // 状态指纹,用于确定性回归

    // 链式建场景(等价于逐个 add_body)
    @moonphys.World::builder()
    .gravity(0.0, -9.81).iterations(10).sleep_enabled(true)
    .ground(20.0).ball(0.4, 0.0, 4.0).box(0.5, 0.5, 1.0, 6.0)
    .body(some_preconfigured_body)
    .build()

    // 刚体
    let body = @moonphys.Body::box(0.5, 0.5) // 动态方块
    .at(0.0, 3.0) // 质心位置
    .with_rotation(0.3) // 弧度
    .with_velocity(1.0, 0.0)
    .with_angular_velocity(-0.5)
    .with_friction(0.6)
    .with_restitution(0.2) // 弹性
    .with_density(1.0) // 质量 = 密度 × 面积
    .with_damping(0.05, 0.05) // 线/角阻尼
    .with_allow_sleep(true)
    .with_user_data(3) // 渲染配色用的自定义标记
    ignore(world.add_body(body))

    // 形状
    @moonphys.Shape::box(0.5, 0.5) // 半宽半高
    @moonphys.Shape::circle(0.4)
    @moonphys.Shape::convex(vertices) // 逆时针凸多边形

    // 力与冲量
    body.apply_force(@moonphys.Vec2::new(0.0, 10.0)) // 本步有效
    body.apply_impulse(@moonphys.Vec2::new(2.0, 0.0), body.position())
    body.point_velocity(body.position())

    // 查询
    body.mass(); body.inertia(); body.aabb(); body.contains_point(p)
    body.local_to_world(p); body.world_to_local(p)

    // 关节
    world.add_distance_joint(index_a, index_b) // 保持当前中心距
    world.add_revolve_joint(index_a, index_b, @moonphys.Vec2::new(0.0, 5.0)) // 世界锚点
    world.add_revolve_joint_local(a, b, local_a, local_b) // 各自的局部锚点
    world.add_prismatic_joint(a, b, anchor, axis) // 滑轨,可 .with_limits/.with_motor
    world.add_angle_limit_joint_at(a, b, anchor, -0.6, 0.6) // 绕锚点的角限位
    world.add_motor_joint(a, b, 2.0, 200.0) // 目标角速度, 最大力矩
    world.add_soft_distance_joint(a, b, 4.0, 1.0) // 频率 Hz, 阻尼比

    #实现要点

    碰撞检测:多边形用 SAT 求最大分离轴;分离轴落在 A 上时以 A 为参考面,否则交换 A/B(relative_tol = 0.98、absolute_tol = 0.001 做偏置,避免两帧之间反复翻转)。确定参考面后找入射面,把入射线段裁剪到参考面的侧平面,保留 separation < 0 的点,最多两点。

    求解顺序(World::step):

    1. 同步 AABB → 宽相配对(复用上一帧的排序数组与 pair 缓冲区)
    2. 窄相生成接触点;命中已有 Arbiter 则按 feature id 迁移旧冲量(暖启动),否则新建
    3. 积分重力、外力、阻尼,清除累积力
    4. 每个 Arbiter / 关节计算有效质量与偏置
    5. 迭代 velocity_iterations 次求解速度约束
    6. 积分位置 → 更新睡眠状态 → 同步 AABB

    为什么是顺序冲量法:它只需要存储每条约束的累计冲量,迭代几次就能让堆叠稳定,代码量比 LCP/投影高斯-赛德尔求解器小一个量级,适合"看得懂、改得动"的库。10 次迭代下 6 层方块塔在 4 秒内不塌、不沉陷(有测试盯着)。

    确定性:不引入随机数;宽相排序的所有 tie 都按下标打破;接触点顺序由裁剪顺序决定;关节按加入顺序求解。因此同样的构造 + 同样的步长 = 逐位相同的轨迹。

    World::state_hash() 把每个 body 的位置、旋转、线速度、角速度按原始 bit 折进一个 FNV-1a 摘要,world_test.mbt 把某个固定场景 300 步后的摘要钉成常量——moon test 与 moon test --target js 跑的是同一条断言,任一后端出现 1 ulp 的差异都会让测试失败。CI 里还有一条更直接的对拍:同一组 bench 场景在 wasm-gc 与 js 上各跑 600 步,比对摘要行。

    #性能

    moon run bench --target js # 或 --target wasm-gc

    三个场景各跑 600 步固定步长(dt = 1/60),输出每步毫秒数、body 数、接触数与状态摘要:

    场景bodies接触数js ms/stepwasm-gc ms/step
    stack:10×10 方块塔1011430.400.49
    crowd:500 个相切圆密集堆5018831.131.21
    chain:20 节刚性链22170.070.09

    (Windows 11 / Intel Core Ultra 9 275HX / Node 24;同机复跑 stack 在 0.39–0.45 之间波动,关注量级即可。两个 target 的 hash= 行完全相同,说明轨迹逐位一致。)

    做过的两次改动:

    1. 宽相:原先每步重建数组并整表插入排序,改为跨帧复用排序结果 + 近序插入排序,并把持久接触的查找从「每条 pair 扫全部 arbiter」改成按低位 body 下标分桶。方块塔从 0.62 → 0.50 ms/step,摘要不变(说明只是变快,没有改变轨迹)。
    2. 唤醒策略:原先「邻居醒着就唤醒我」会让堆里的第一个入睡者每步被邻居惊醒,整堆永远睡不下去;改成邻居必须真的在移动(速度超过睡眠阈值)才唤醒后,静置的堆会整体入睡、不再参与求解,方块塔因此降到 0.40 ms/step。

    #测试

    moon test # 64 项 moon test --target js # 同一套断言在 JS 后端上再跑一遍 node tools/check-playground.mjs # playground 的 wasm:24 个导出 + 6 场景各 180 步 powershell -File tools/acceptance.ps1 # 模拟验收:克隆仓库 + 从 mooncakes 装包,各跑一遍

    覆盖:向量/变换数学、质量属性、AABB、圆与多边形的 7 类碰撞情形(含法向翻转)、自由落体解析解对照、落地静止、堆叠稳定性、恢复系数、睡眠、整堆入睡与落地唤醒、宽相剪枝、增量宽相与暴力枚举逐步对拍、宽相能接纳后加入的 body、冲量、射线与 AABB/点查询、17° 斜面静摩擦不起滑、动量守恒、关节长度与锚点误差、滑轨只沿轴运动与行程限位、角马达达到目标角速度、角限位夹住重力负载、软关节收敛、20 节链条不发散、builder 与手写场景逐位一致、快照 JSON 覆盖每个 body、状态摘要可复现且钉死常量、演示场景数值健全性、SVG 输出结构。

    #移植来源与许可

    moonphys 是原创的 MoonBit 实现,算法与工程结构参考了以下 MIT 许可的公开实现,未包含其任何源码:

    • Box2D-Lite — Erin Catto,https://github.com/erincatto/box2d-lite (MIT):Arbiter/顺序冲量、裁剪式多边碰撞的经典教学实现
    • Box2D — Erin Catto,https://github.com/erincatto/box2d (MIT):宽相、睡眠、关节的工业级参考
    • Planck.js — Ali Shakiba,https://github.com/shakiba/planck.js (MIT):JavaScript 移植版,本文的 API 风格参考

    本项目以 Apache-2.0 发布,见 LICENSE。

    Aabb

    pub struct Aabb {
    min : Vec2
    max : Vec2
    }

    Axis-aligned bounding box.

    Aabb::center

    fn Aabb::center(self : Aabb) -> Vec2

    Centre point of the box.

    Aabb::contains_point

    fn Aabb::contains_point(self : Aabb, p : Vec2) -> Bool

    Whether p lies inside the box.

    Aabb::expanded

    fn Aabb::expanded(self : Aabb, margin : Double) -> Aabb

    Grow the box by margin on every side.

    Aabb::height

    fn Aabb::height(self : Aabb) -> Double

    Height along y.

    Aabb::max

    fn Aabb::max(self : Aabb) -> Vec2

    Upper corner.

    Aabb::min

    fn Aabb::min(self : Aabb) -> Vec2

    Lower corner.

    Aabb::new

    fn Aabb::new(min : Vec2, max : Vec2) -> Aabb

    Build a box from its lower and upper corners.

    Aabb::overlaps

    fn Aabb::overlaps(self : Aabb, other : Aabb) -> Bool

    Whether the two boxes intersect; touching counts.

    Aabb::union

    fn Aabb::union(self : Aabb, other : Aabb) -> Aabb

    Smallest box covering both inputs.

    Aabb::width

    fn Aabb::width(self : Aabb) -> Double

    Width along x.

    AngleLimitJoint

    pub struct AngleLimitJoint {
    body_a : Int
    body_b : Int
    lower : Double
    upper : Double
    impulse : Double
    mass : Double
    active : Int
    has_pivot : Bool
    local_pivot_b : Vec2
    }

    Keeps the relative rotation of two bodies inside [lower, upper].

    AngleLimitJoint::at

    fn AngleLimitJoint::at(world : World, body_a : Int, body_b : Int, anchor : Vec2, lower : Double, upper : Double) -> AngleLimitJoint

    Angle stop that treats anchor as the pivot: the positional pass rotates the bodies about that point, so a revolute joint sharing the same anchor is not disturbed by the correction.

    AngleLimitJoint::correct_positions

    fn AngleLimitJoint::correct_positions(self : AngleLimitJoint, bodies : Array[Body]) -> Unit

    Rotate the bodies back inside the allowed range without touching velocities.

    AngleLimitJoint::new

    fn AngleLimitJoint::new(body_a : Int, body_b : Int, lower : Double, upper : Double) -> AngleLimitJoint

    Keep the relative rotation inside the lower and upper bounds, in radians.

    AngleLimitJoint::pre_step

    fn AngleLimitJoint::pre_step(self : AngleLimitJoint, bodies : Array[Body]) -> Unit

    Prepare the angular mass, the stop bias and the projection bias.

    AngleLimitJoint::solve_velocity

    fn AngleLimitJoint::solve_velocity(self : AngleLimitJoint, bodies : Array[Body]) -> Unit

    Apply one iteration of the angular limit.

    Arbiter

    pub struct Arbiter {
    body_a : Int
    body_b : Int
    contacts : Array[ContactPoint]
    normal_impulses : Array[Double]
    tangent_impulses : Array[Double]
    mass_normal : Array[Double]
    mass_tangent : Array[Double]
    bias : Array[Double]
    restitution_bias : Array[Double]
    friction : Double
    restitution : Double
    }

    Persistent contact constraints between two bodies.

    Impulses are accumulated and clamped, and are carried over to the next frame by matching contact feature ids, which is what makes stacks stable.

    Arbiter::body_a

    fn Arbiter::body_a(self : Arbiter) -> Int

    Index of the first body: the lower index of the pair.

    Arbiter::body_b

    fn Arbiter::body_b(self : Arbiter) -> Int

    Index of the second body: the higher index of the pair.

    Arbiter::contacts

    fn Arbiter::contacts(self : Arbiter) -> Array[ContactPoint]

    Contact points of this pair, each carrying its warm-started impulse.

    Arbiter::is_touching

    fn Arbiter::is_touching(self : Arbiter) -> Bool

    Whether the last update produced at least one contact point.

    Arbiter::new

    fn Arbiter::new(body_a : Int, body_b : Int, contacts : Array[ContactPoint], friction : Double, restitution : Double) -> Arbiter

    Create an arbiter for a body pair from freshly detected contact points.

    Arbiter::pre_step

    fn Arbiter::pre_step(self : Arbiter, bodies : Array[Body], inv_dt : Double) -> Unit

    Compute effective masses and bias terms, then warm start.

    Arbiter::solve_velocity

    fn Arbiter::solve_velocity(self : Arbiter, bodies : Array[Body]) -> Unit

    Run one sequential impulse iteration.

    Arbiter::total_normal_impulse

    fn Arbiter::total_normal_impulse(self : Arbiter) -> Double

    Total normal impulse, useful to detect resting stability in tests.

    Arbiter::update

    fn Arbiter::update(self : Arbiter, contacts : Array[ContactPoint], friction : Double, restitution : Double) -> Unit

    Refresh the manifold, keeping impulses of surviving points.

    Body

    pub struct Body {
    body_type : BodyType
    shape : Shape
    position : Vec2
    rotation : Double
    velocity : Vec2
    angular_velocity : Double
    force : Vec2
    torque : Double
    mass : Double
    inv_mass : Double
    inertia : Double
    inv_inertia : Double
    friction : Double
    restitution : Double
    linear_damping : Double
    angular_damping : Double
    allow_sleep : Bool
    awake : Bool
    sleep_time : Double
    user_data : Int
    world_aabb : Aabb
    }

    A rigid body: a shape plus its motion state and material parameters.

    For polygons the vertices are re-centered on construction, so position always denotes the center of mass. Circles are centered by definition.

    Body::aabb

    fn Body::aabb(self : Body) -> Aabb

    World-space bounding box, synced at the end of every step.

    Body::angular_velocity

    fn Body::angular_velocity(self : Body) -> Double

    Angular velocity in radians per second.

    Body::apply_angular_impulse

    fn Body::apply_angular_impulse(self : Body, impulse : Double) -> Unit

    Change the angular velocity by impulse / inertia right away.

    Body::apply_force

    fn Body::apply_force(self : Body, force : Vec2) -> Unit

    Accumulate a force for the next step; it is cleared after integration.

    Body::apply_impulse

    fn Body::apply_impulse(self : Body, impulse : Vec2, point : Vec2) -> Unit

    Apply an impulse at a world-space point.

    Body::apply_torque

    fn Body::apply_torque(self : Body, torque : Double) -> Unit

    Accumulate a torque for the next step.

    Body::at

    fn Body::at(self : Body, x : Double, y : Double) -> Body

    Builder: move the body so its center of mass sits at (x, y).

    Body::awake

    fn Body::awake(self : Body) -> Bool

    Whether the body is currently simulated, that is not sleeping.

    Body::ball

    fn Body::ball(radius : Double) -> Body

    A dynamic circle.

    Body::box

    fn Body::box(half_width : Double, half_height : Double) -> Body

    A dynamic box centered at the current position.

    Body::clear_forces

    fn Body::clear_forces(self : Body) -> Unit

    Drop the accumulated force and torque.

    Body::contains_point

    fn Body::contains_point(self : Body, point : Vec2) -> Bool

    Whether a world-space point is inside the body.

    Body::dynamic

    fn Body::dynamic(shape : Shape) -> Body

    A dynamic body, moved by the solver.

    Body::fixed

    fn Body::fixed(shape : Shape) -> Body

    An immovable body, e.g. the ground.

    Body::friction

    fn Body::friction(self : Body) -> Double

    Coulomb friction coefficient.

    Body::inertia

    fn Body::inertia(self : Body) -> Double

    Rotational inertia; zero means infinite inertia (static).

    Body::is_dynamic

    fn Body::is_dynamic(self : Body) -> Bool

    Whether the body is moved by forces, gravity and collisions.

    Body::is_static

    fn Body::is_static(self : Body) -> Bool

    Whether the body has infinite mass and never moves.

    Body::kinetic_energy

    fn Body::kinetic_energy(self : Body) -> Double

    Translational plus rotational kinetic energy.

    Body::local_to_world

    fn Body::local_to_world(self : Body, point : Vec2) -> Vec2

    Local point to world point.

    Body::mass

    fn Body::mass(self : Body) -> Double

    Mass in kilograms; zero means infinite mass (static).

    Body::new

    fn Body::new(shape : Shape, body_type : BodyType) -> Body

    Create a body of body_type with the given shape and default material.

    Body::point_velocity

    fn Body::point_velocity(self : Body, point : Vec2) -> Vec2

    Velocity of a world-space point on the body.

    Body::position

    fn Body::position(self : Body) -> Vec2

    Centre of mass position in world space.

    Body::restitution

    fn Body::restitution(self : Body) -> Double

    Restitution, 0 is inelastic, 1 is perfectly elastic.

    Body::rotation

    fn Body::rotation(self : Body) -> Double

    Rotation in radians.

    Body::set_density

    fn Body::set_density(self : Body, density : Double) -> Unit

    Recompute mass and inertia for density.

    Body::shape

    fn Body::shape(self : Body) -> Shape

    The collision shape.

    Body::sleep

    fn Body::sleep(self : Body) -> Unit

    Put the body to sleep until something touches it.

    Body::sync_aabb

    fn Body::sync_aabb(self : Body) -> Unit

    Cached world AABB, refreshed by the world each step.

    Body::transform

    fn Body::transform(self : Body) -> Transform

    Position and rotation packed into a Transform.

    Body::user_data

    fn Body::user_data(self : Body) -> Int

    Free integer tag, handy for render colors and test bookkeeping.

    Body::velocity

    fn Body::velocity(self : Body) -> Vec2

    Linear velocity of the centre of mass.

    Body::wake

    fn Body::wake(self : Body) -> Unit

    Wake the body so it is simulated again.

    Body::with_allow_sleep

    fn Body::with_allow_sleep(self : Body, allow : Bool) -> Body

    Builder: opt in or out of automatic sleeping.

    Body::with_angular_velocity

    fn Body::with_angular_velocity(self : Body, w : Double) -> Body

    Builder: set the initial angular velocity.

    Body::with_damping

    fn Body::with_damping(self : Body, linear : Double, angular : Double) -> Body

    Builder: set linear and angular damping per second.

    Body::with_density

    fn Body::with_density(self : Body, density : Double) -> Body

    Builder: set the density used to derive mass and inertia.

    Body::with_friction

    fn Body::with_friction(self : Body, friction : Double) -> Body

    Builder: set the friction coefficient.

    Body::with_restitution

    fn Body::with_restitution(self : Body, restitution : Double) -> Body

    Builder: set the restitution.

    Body::with_rotation

    fn Body::with_rotation(self : Body, angle : Double) -> Body

    Builder: set the initial rotation in radians.

    Body::with_user_data

    fn Body::with_user_data(self : Body, data : Int) -> Body

    Builder: attach an integer tag, handy for game code.

    Body::with_velocity

    fn Body::with_velocity(self : Body, vx : Double, vy : Double) -> Body

    Builder: set the initial linear velocity.

    Body::world_to_local

    fn Body::world_to_local(self : Body, point : Vec2) -> Vec2

    World point to local point.

    BodySnapshot

    pub struct BodySnapshot {
    position : Vec2
    rotation : Double
    shape : Shape
    asleep : Bool
    user_data : Int
    }

    Immutable view of a body, used for rendering and for regression tests.

    BodySnapshot::asleep

    fn BodySnapshot::asleep(self : BodySnapshot) -> Bool

    Whether the body was asleep.

    BodySnapshot::position

    fn BodySnapshot::position(self : BodySnapshot) -> Vec2

    Centre of mass position when the snapshot was taken.

    BodySnapshot::rotation

    fn BodySnapshot::rotation(self : BodySnapshot) -> Double

    Rotation when the snapshot was taken.

    BodySnapshot::shape

    fn BodySnapshot::shape(self : BodySnapshot) -> Shape

    Shape of the body.

    BodySnapshot::user_data

    fn BodySnapshot::user_data(self : BodySnapshot) -> Int

    User tag of the body.

    BodyType

    pub enum BodyType {
    Static
    Dynamic
    }

    Static bodies never move; dynamic bodies are integrated and solved.

    ContactPoint

    pub struct ContactPoint {
    position : Vec2
    normal : Vec2
    separation : Double
    feature_id : Int
    }

    One contact point of a manifold, in world space.

    normal always points from the first shape toward the second one, and separation is negative when the shapes overlap.

    ContactPoint::feature_id

    fn ContactPoint::feature_id(self : ContactPoint) -> Int

    Stable id of the contacting features, used to migrate warm-start impulses.

    ContactPoint::normal

    fn ContactPoint::normal(self : ContactPoint) -> Vec2

    Contact normal, pointing from body A towards body B.

    ContactPoint::position

    fn ContactPoint::position(self : ContactPoint) -> Vec2

    World-space contact position.

    ContactPoint::separation

    fn ContactPoint::separation(self : ContactPoint) -> Double

    Signed gap between the shapes; negative while they overlap.

    DistanceJoint

    pub struct DistanceJoint {
    body_a : Int
    body_b : Int
    local_anchor_a : Vec2
    local_anchor_b : Vec2
    length : Double
    impulse : Double
    mass : Double
    bias : Double
    gamma : Double
    frequency : Double
    damping_ratio : Double
    }

    A rigid rod that keeps two anchor points at a fixed distance.

    DistanceJoint::anchors

    fn DistanceJoint::anchors(self : DistanceJoint, bodies : Array[Body]) -> (Vec2, Vec2)

    World-space anchor points of the joint.

    DistanceJoint::between

    fn DistanceJoint::between(world : World, body_a : Int, body_b : Int) -> DistanceJoint

    Join the centers of two bodies at their current distance.

    DistanceJoint::length

    fn DistanceJoint::length(self : DistanceJoint) -> Double

    Target distance between the two anchors.

    DistanceJoint::new

    fn DistanceJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2, length : Double) -> DistanceJoint

    Pin the distance between two world anchors to length.

    DistanceJoint::pre_step

    fn DistanceJoint::pre_step(self : DistanceJoint, bodies : Array[Body], inv_dt : Double) -> Unit

    Compute the effective mass and the velocity bias for this step.

    DistanceJoint::solve_velocity

    fn DistanceJoint::solve_velocity(self : DistanceJoint, bodies : Array[Body]) -> Unit

    Apply one iteration of the distance constraint.

    DistanceJoint::with_softness

    fn DistanceJoint::with_softness(self : DistanceJoint, frequency : Double, damping_ratio : Double) -> DistanceJoint

    Turn the rigid rod into a spring.

    frequency is in Hz and damping_ratio of 1 is critically damped. With softness the joint trades exactness for stability to avoid the jitter a hard constraint shows on long chains.

    Joint

    pub enum Joint {
    Distance(DistanceJoint)
    Revolve(RevolveJoint)
    Prismatic(PrismaticJoint)
    AngleLimit(AngleLimitJoint)
    Motor(MotorJoint)
    }

    Type-erased joint stored in a world.

    Joint::correct_positions

    fn Joint::correct_positions(self : Joint, bodies : Array[Body]) -> Unit

    Post-integration positional pass. Hard stops are solved as pure velocity constraints, so whatever overshoot is left is projected out here.

    Joint::pre_step

    fn Joint::pre_step(self : Joint, bodies : Array[Body], inv_dt : Double) -> Unit

    Let the joint prepare its effective mass and bias for this step.

    Joint::solve_velocity

    fn Joint::solve_velocity(self : Joint, bodies : Array[Body]) -> Unit

    Let the joint apply one velocity iteration.

    MassData

    pub struct MassData {
    mass : Double
    inertia : Double
    center : Vec2
    }

    Mass properties of a shape with unit density scaled by density.

    MassData::center

    fn MassData::center(self : MassData) -> Vec2

    Centre of mass in local space.

    MassData::inertia

    fn MassData::inertia(self : MassData) -> Double

    Rotational inertia about the centre of mass.

    MassData::mass

    fn MassData::mass(self : MassData) -> Double

    Total mass.

    Mat22

    pub struct Mat22 {
    c1 : Vec2
    c2 : Vec2
    }

    A 2x2 matrix stored as two column vectors.

    Mat22::abs

    fn Mat22::abs(self : Mat22) -> Mat22

    Absolute value of every entry, used to build the friction matrix.

    Mat22::from_angle

    fn Mat22::from_angle(angle : Double) -> Mat22

    Rotation matrix for angle radians.

    Mat22::identity

    fn Mat22::identity() -> Mat22

    Identity matrix.

    Mat22::mul_vec

    fn Mat22::mul_vec(self : Mat22, v : Vec2) -> Vec2

    Apply the matrix to a vector.

    Mat22::transpose

    fn Mat22::transpose(self : Mat22) -> Mat22

    Transpose of the matrix.

    MotorJoint

    pub struct MotorJoint {
    body_a : Int
    body_b : Int
    target_speed : Double
    max_torque : Double
    impulse : Double
    mass : Double
    cap : Double
    }

    Angular motor: drives the relative angular velocity towards target_speed with a bounded torque.

    MotorJoint::new

    fn MotorJoint::new(body_a : Int, body_b : Int, target_speed : Double, max_torque : Double) -> MotorJoint

    Drive the relative angular velocity towards target_speed with at most max_torque.

    MotorJoint::pre_step

    fn MotorJoint::pre_step(self : MotorJoint, bodies : Array[Body], inv_dt : Double) -> Unit

    Prepare the angular mass and clamp the motor torque.

    MotorJoint::solve_velocity

    fn MotorJoint::solve_velocity(self : MotorJoint, bodies : Array[Body]) -> Unit

    Apply one iteration of the motor constraint.

    PrismaticJoint

    pub struct PrismaticJoint {
    body_a : Int
    body_b : Int
    local_anchor_a : Vec2
    local_anchor_b : Vec2
    local_axis_a : Vec2
    has_limits : Bool
    lower : Double
    upper : Double
    motor_speed : Double
    max_motor_force : Double
    impulse : Vec2
    motor_impulse : Double
    limit_impulse : Double
    k11 : Double
    k12 : Double
    k22 : Double
    axis_mass : Double
    motor_cap : Double
    bias : Vec2
    active : Int
    }

    A slider: the two anchors are constrained to stay on one axis, relative rotation is locked, and translation along the axis can be limited and driven by a motor.

    The axis is stored in body A's frame, so a slider attached to a static body is a plain world-space rail.

    PrismaticJoint::at

    fn PrismaticJoint::at(world : World, body_a : Int, body_b : Int, anchor : Vec2, axis : Vec2) -> PrismaticJoint

    Slider whose anchors are both at anchor and whose rail is axis in world space at construction time.

    PrismaticJoint::correct_positions

    fn PrismaticJoint::correct_positions(self : PrismaticJoint, bodies : Array[Body]) -> Unit

    Push the slider back inside its travel range without touching velocities.

    PrismaticJoint::new

    fn PrismaticJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2, local_axis_a : Vec2) -> PrismaticJoint

    Lock the bodies to a rail through anchor along axis.

    PrismaticJoint::pre_step

    fn PrismaticJoint::pre_step(self : PrismaticJoint, bodies : Array[Body], inv_dt : Double) -> Unit

    Compute effective mass, soft stop bias and motor bias.

    PrismaticJoint::solve_velocity

    fn PrismaticJoint::solve_velocity(self : PrismaticJoint, bodies : Array[Body]) -> Unit

    Apply one iteration of the rail, limit and motor constraints.

    PrismaticJoint::with_limits

    fn PrismaticJoint::with_limits(self : PrismaticJoint, lower : Double, upper : Double) -> PrismaticJoint

    Restrict the travel along the rail to [lower, upper], in world units.

    PrismaticJoint::with_motor

    fn PrismaticJoint::with_motor(self : PrismaticJoint, speed : Double, max_force : Double) -> PrismaticJoint

    Drive the slider along its rail at speed (units/second) with at most max_force of force.

    RayHit

    pub struct RayHit {
    body_index : Int
    point : Vec2
    normal : Vec2
    distance : Double
    }

    Result of a ray cast against the world.

    RayHit::body_index

    fn RayHit::body_index(self : RayHit) -> Int

    Index of the body that was hit.

    RayHit::distance

    fn RayHit::distance(self : RayHit) -> Double

    Distance from the ray origin to the hit point.

    RayHit::normal

    fn RayHit::normal(self : RayHit) -> Vec2

    Outward surface normal at the hit point.

    RayHit::point

    fn RayHit::point(self : RayHit) -> Vec2

    World-space point where the ray entered the shape.

    RevolveJoint

    pub struct RevolveJoint {
    body_a : Int
    body_b : Int
    local_anchor_a : Vec2
    local_anchor_b : Vec2
    impulse : Vec2
    k11 : Double
    k12 : Double
    k22 : Double
    bias : Vec2
    }

    A hinge: two anchor points are kept coincident, both bodies may rotate.

    RevolveJoint::at

    fn RevolveJoint::at(world : World, body_a : Int, body_b : Int, anchor : Vec2) -> RevolveJoint

    Revolve joint whose anchors are both at the given world point.

    RevolveJoint::new

    fn RevolveJoint::new(body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2) -> RevolveJoint

    Pin two local anchors to the same world point, leaving rotation free.

    RevolveJoint::pre_step

    fn RevolveJoint::pre_step(self : RevolveJoint, bodies : Array[Body], inv_dt : Double) -> Unit

    Compute the 2x2 effective mass and the position bias.

    RevolveJoint::solve_velocity

    fn RevolveJoint::solve_velocity(self : RevolveJoint, bodies : Array[Body]) -> Unit

    Apply one iteration of the hinge constraint.

    Shape

    pub enum Shape {
    Circle(Double)
    Polygon(Array[Vec2])
    }

    Collision geometry in local space.

    Polygon vertices must form a convex counter-clockwise hull; use Shape::convex to build one safely.

    Shape::aabb

    fn Shape::aabb(self : Shape, transform : Transform) -> Aabb

    World-space AABB of the shape placed by transform.

    Shape::area

    fn Shape::area(self : Shape) -> Double

    Area of the shape (polygon area, or circle area).

    Shape::box

    fn Shape::box(half_width : Double, half_height : Double) -> Shape

    Axis-aligned box centered at the local origin.

    Shape::circle

    fn Shape::circle(radius : Double) -> Shape

    A circle of the given radius.

    Shape::contains_local_point

    fn Shape::contains_local_point(self : Shape, point : Vec2) -> Bool

    Whether a local-space point lies inside the shape.

    Shape::convex

    fn Shape::convex(vertices : Array[Vec2]) -> Shape

    Build a convex polygon, forcing counter-clockwise winding.

    Duplicated and collinear vertices are dropped first, because a zero-length edge makes the edge normal undefined and would poison the solver.

    Shape::edge_normal

    fn Shape::edge_normal(self : Shape, i : Int) -> Vec2

    Outward unit normal of polygon edge i.

    Shape::is_circle

    fn Shape::is_circle(self : Shape) -> Bool

    Whether this shape is a circle.

    Shape::mass_data

    fn Shape::mass_data(self : Shape, density : Double) -> MassData

    Compute mass properties for the given density.

    Shape::radius

    fn Shape::radius(self : Shape) -> Double

    Radius of a circle shape, or 0.0 for polygons.

    Shape::vertices

    fn Shape::vertices(self : Shape) -> Array[Vec2]

    Local-space vertices; empty for circles.

    Transform

    pub struct Transform {
    position : Vec2
    rotation : Double
    }

    Rotation of a body: world = rotation * local + position.

    Transform::apply

    fn Transform::apply(self : Transform, point : Vec2) -> Vec2

    Transform a point from local space to world space.

    Transform::apply_inv

    fn Transform::apply_inv(self : Transform, world : Vec2) -> Vec2

    Transform a point from world space to local space.

    Transform::apply_inv_vector

    fn Transform::apply_inv_vector(self : Transform, world : Vec2) -> Vec2

    Rotate a vector from world space to local space.

    Transform::apply_vector

    fn Transform::apply_vector(self : Transform, direction : Vec2) -> Vec2

    Rotate a vector from local space to world space.

    Transform::new

    fn Transform::new(position : Vec2, rotation : Double) -> Transform

    Build a transform from a position and a rotation.

    Vec2

    pub struct Vec2 {
    x : Double
    y : Double
    }

    A 2D vector of Double. Immutable: operations return fresh values, which keeps constraint solving free of aliasing and keeps steps reproducible.
    impl Add for Vec2
    impl Eq for Vec2
    impl Neg for Vec2
    impl Show for Vec2
    impl Sub for Vec2

    Vec2::abs

    fn Vec2::abs(self : Vec2) -> Vec2

    Component-wise absolute value.

    Vec2::add

    fn Vec2::add(self : Vec2, other : Vec2) -> Vec2

    Promote the trait method to a regular method explicitly: the implicit promotion of impl methods is deprecated in newer toolchains.

    Vec2::cross

    fn Vec2::cross(self : Vec2, other : Vec2) -> Double

    Scalar cross product x1 * y2 - y1 * x2.

    Vec2::cross_scalar

    fn Vec2::cross_scalar(self : Vec2, z : Double) -> Vec2

    z x v = (-z * v.y, z * v.x).

    Vec2::dot

    fn Vec2::dot(self : Vec2, other : Vec2) -> Double

    Dot product.

    Vec2::equal

    fn Vec2::equal(self : Vec2, other : Vec2) -> Bool

    Vec2::length

    fn Vec2::length(self : Vec2) -> Double

    Length.

    Vec2::length_sq

    fn Vec2::length_sq(self : Vec2) -> Double

    Squared length.

    Vec2::max

    fn Vec2::max(self : Vec2, other : Vec2) -> Vec2

    Component-wise maximum.

    Vec2::min

    fn Vec2::min(self : Vec2, other : Vec2) -> Vec2

    Component-wise minimum.

    Vec2::neg

    fn Vec2::neg(self : Vec2) -> Vec2

    Vec2::new

    fn Vec2::new(x : Double, y : Double) -> Vec2

    Build a vector from its components.

    Vec2::normalize

    fn Vec2::normalize(self : Vec2) -> Vec2

    Unit vector, or zero when the vector is degenerate.

    Vec2::normalize_or

    fn Vec2::normalize_or(self : Vec2, fallback : Vec2) -> Vec2

    Unit vector, or fallback when degenerate.

    Vec2::not_equal

    fn Vec2::not_equal(x : Vec2, y : Vec2) -> Bool

    Vec2::output

    fn Vec2::output(self : Vec2, logger : &Logger) -> Unit

    Vec2::perp

    fn Vec2::perp(self : Vec2) -> Vec2

    Counter-clockwise perpendicular (-y, x).

    Vec2::rotate

    fn Vec2::rotate(self : Vec2, angle : Double) -> Vec2

    Counter-clockwise rotation by angle radians.

    Vec2::rotate_by

    fn Vec2::rotate_by(self : Vec2, c : Double, s : Double) -> Vec2

    Roration by an angle given precomputed cosine/sine.

    Vec2::rotate_inv

    fn Vec2::rotate_inv(self : Vec2, angle : Double) -> Vec2

    Inverse rotation by angle radians.

    Vec2::scale

    fn Vec2::scale(self : Vec2, s : Double) -> Vec2

    Multiply both components by s.

    Vec2::splat

    fn Vec2::splat(v : Double) -> Vec2

    A vector with both components set to v.

    Vec2::sub

    fn Vec2::sub(self : Vec2, other : Vec2) -> Vec2

    Vec2::to_string

    fn Vec2::to_string(self : Vec2) -> String

    Vec2::x

    fn Vec2::x(self : Vec2) -> Double

    Horizontal component.

    Vec2::y

    fn Vec2::y(self : Vec2) -> Double

    Vertical component.

    Vec2::zero

    fn Vec2::zero() -> Vec2

    The zero vector.

    World

    pub struct World {
    bodies : Array[Body]
    arbiters : Array[Arbiter]
    joints : Array[Joint]
    gravity : Vec2
    velocity_iterations : Int
    time : Double
    sleep_enabled : Bool
    sorted : Array[Int]
    pairs : Array[(Int, Int)]
    arbiter_buckets : Array[Array[Arbiter]]
    }

    The simulation world: bodies, persistent contacts and joints.

    World::add_angle_limit_joint

    fn World::add_angle_limit_joint(self : World, body_a : Int, body_b : Int, lower : Double, upper : Double) -> AngleLimitJoint

    Keep the relative rotation of two bodies between lower and upper.

    World::add_angle_limit_joint_at

    fn World::add_angle_limit_joint_at(self : World, body_a : Int, body_b : Int, anchor : Vec2, lower : Double, upper : Double) -> AngleLimitJoint

    Angle stop whose corrections pivot around anchor, which is what you want when the same anchor also carries a revolute joint.

    World::add_body

    fn World::add_body(self : World, body : Body) -> Int

    Add a body and return its index, which joints and arbiters refer to.

    World::add_distance_joint

    fn World::add_distance_joint(self : World, body_a : Int, body_b : Int) -> Unit

    Connect the centers of two bodies with a rigid distance joint.

    World::add_joint

    fn World::add_joint(self : World, joint : Joint) -> Unit

    Attach a joint; joints are solved after the contacts on every step.

    World::add_motor_joint

    fn World::add_motor_joint(self : World, body_a : Int, body_b : Int, target_speed : Double, max_torque : Double) -> MotorJoint

    Drive the relative angular velocity of two bodies towards target_speed with at most max_torque.

    World::add_prismatic_joint

    fn World::add_prismatic_joint(self : World, body_a : Int, body_b : Int, anchor : Vec2, axis : Vec2) -> PrismaticJoint

    Attach a slider between two bodies. The returned joint is the live object stored in the world, so limits and a motor can be configured in place:

    let slider = world.add_prismatic_joint(rail, cart, anchor, Vec2::new(1.0, 0.0))
    slider.with_limits(-3.0, 3.0)
    slider.with_motor(1.5, 40.0)

    World::add_revolve_joint

    fn World::add_revolve_joint(self : World, body_a : Int, body_b : Int, anchor : Vec2) -> Unit

    Pin two bodies together at a world-space anchor (a hinge).

    World::add_revolve_joint_local

    fn World::add_revolve_joint_local(self : World, body_a : Int, body_b : Int, local_anchor_a : Vec2, local_anchor_b : Vec2) -> Unit

    Pin two bodies together, using one local anchor per body.

    World::add_soft_distance_joint

    fn World::add_soft_distance_joint(self : World, body_a : Int, body_b : Int, frequency : Double, damping_ratio : Double) -> Unit

    A distance joint whose stiffness is finite, for ropes and springs.

    World::arbiter_count

    fn World::arbiter_count(self : World) -> Int

    Number of touching pairs found by the last step.

    World::body

    fn World::body(self : World, index : Int) -> Body

    Access a body by index.

    World::body_count

    fn World::body_count(self : World) -> Int

    Number of bodies.

    World::builder

    fn World::builder() -> WorldBuilder

    Start a builder on an empty world with default gravity.

    World::contact_count

    fn World::contact_count(self : World) -> Int

    Number of contact points found by the last step.

    World::gravity

    fn World::gravity(self : World) -> Vec2

    Current gravity vector.

    World::joint_count

    fn World::joint_count(self : World) -> Int

    Number of joints.

    World::load_state

    fn World::load_state(self : World, bytes : Bytes) -> Bool

    Restore a snapshot produced by save_state.

    Returns false and leaves the world untouched when the buffer is truncated, corrupted, from another format version, or when the world has joints (joint solver state is not part of the format).

    World::new

    fn World::new() -> World

    An empty world with Earth gravity and ten solver iterations.

    World::query_aabb

    fn World::query_aabb(self : World, region : Aabb) -> Array[Int]

    All body indices whose AABB overlaps region, in ascending order.

    World::query_point

    fn World::query_point(self : World, point : Vec2) -> Array[Int]

    Bodies whose shape contains the point, most useful for picking.

    World::ray_cast

    fn World::ray_cast(self : World, origin : Vec2, direction : Vec2, max_distance : Double) -> RayHit?

    Cast a ray through the world and return the closest hit, or None.

    direction is normalized internally, so max_distance is a plain world distance. Static and moving bodies are both considered.

    World::run

    fn World::run(self : World, steps : Int, dt : Double) -> Unit

    Advance by steps fixed-size steps.

    World::save_state

    fn World::save_state(self : World) -> Bytes

    Serialize the full simulation state: bodies (including velocities, forces and sleep timers), persistent contact impulses, and the world clock.

    hash at the tail makes a corrupted or truncated buffer detectable.

    World::set_gravity

    fn World::set_gravity(self : World, gravity : Vec2) -> Unit

    Change gravity; wakes every body.

    World::set_sleep_enabled

    fn World::set_sleep_enabled(self : World, enabled : Bool) -> Unit

    Turn automatic sleeping on or off.

    World::set_velocity_iterations

    fn World::set_velocity_iterations(self : World, iterations : Int) -> Unit

    How many solver iterations each step runs.

    World::snapshot

    fn World::snapshot(self : World) -> Array[BodySnapshot]

    Copy the visual state of every body: shape, pose, sleep flag and tag.

    World::snapshot_json

    fn World::snapshot_json(self : World) -> String

    Serialise the current visual state of the world as JSON.

    The payload is small and allocation friendly (a single string), so it can be used to debug a simulation, to record a replay, or to hand the frame to a front end that draws it:

    { "time": 2.5, "bodies": [ {"i": 0, "shape": "polygon", "v": [[-20, 0], [20, 0], [20, 1], [-20, 1]], "pos": [0, 0.5], "angle": 0, "asleep": true, "tag": 0}, {"i": 1, "shape": "circle", "r": 0.4, "pos": [0, 3], "angle": 0, "asleep": false, "tag": 7} ] }

    Circles carry r, polygons carry their vertices v, both are in local space: apply pos and angle to place them.

    World::state_hash

    fn World::state_hash(self : World) -> UInt64

    Bit-exact digest of the simulated state: every position, rotation and velocity is folded in as raw bits, so two runs that diverge by one ulp produce different digests.

    This is what makes determinism testable: the same scene has to print the same digest on native, js and wasm-gc, because the engine uses no randomness and no hash-ordered iteration.

    World::step

    fn World::step(self : World, dt : Double) -> Unit

    Advance the simulation by dt seconds.

    World::time

    fn World::time(self : World) -> Double

    Simulated time in seconds.

    World::total_kinetic_energy

    fn World::total_kinetic_energy(self : World) -> Double

    Sum of the kinetic energy of all bodies; the solver must never increase it in a purely dissipative scene.

    World::wake_all

    fn World::wake_all(self : World) -> Unit

    Wake every dynamic body.

    World::with_gravity

    fn World::with_gravity(gx : Double, gy : Double) -> World

    An empty world with custom gravity, given per component.

    WorldBuilder

    pub struct WorldBuilder {
    world : World
    last : Int
    }

    Chainable scene setup.

    let world = @moonphys.World::builder()
    .ground(20.0)
    .ball(0.4, 0.0, 4.0)
    .box(0.5, 0.5, 1.0, 6.0)
    .build()

    Every method returns the builder itself, so the whole scene reads as one expression. build() hands back the world; bodies are added in call order, which means indices are known and joints can be attached afterwards with add_revolve_joint and friends.

    WorldBuilder::ball

    fn WorldBuilder::ball(self : WorldBuilder, radius : Double, x : Double, y : Double) -> WorldBuilder

    Add a dynamic circle of radius centred at (x, y).

    WorldBuilder::body

    fn WorldBuilder::body(self : WorldBuilder, body : Body) -> WorldBuilder

    Add a body that was configured ahead of time with the Body::with_* builders, for example one with friction, restitution or a tag.

    WorldBuilder::box

    fn WorldBuilder::box(self : WorldBuilder, half_width : Double, half_height : Double, x : Double, y : Double) -> WorldBuilder

    Add a dynamic box centred at (x, y).

    WorldBuilder::build

    fn WorldBuilder::build(self : WorldBuilder) -> World

    Finish the scene and hand over the world.

    WorldBuilder::gravity

    fn WorldBuilder::gravity(self : WorldBuilder, gx : Double, gy : Double) -> WorldBuilder

    Set gravity, for example (0.0, -9.81) for Earth or (0.0, 0.0) for a top-down scene.

    WorldBuilder::ground

    fn WorldBuilder::ground(self : WorldBuilder, half_width : Double) -> WorldBuilder

    Add a static floor of the given half width whose top surface is y = 0.

    WorldBuilder::iterations

    fn WorldBuilder::iterations(self : WorldBuilder, iterations : Int) -> WorldBuilder

    Set how many solver iterations each step runs; more iterations mean stiffer stacks and more time.

    WorldBuilder::last_index

    fn WorldBuilder::last_index(self : WorldBuilder) -> Int

    Index of the body added last, -1 when nothing has been added yet.

    WorldBuilder::sleep_enabled

    fn WorldBuilder::sleep_enabled(self : WorldBuilder, enabled : Bool) -> WorldBuilder

    Turn automatic sleeping on or off.

    K_EPSILON

    let K_EPSILON : Double

    Numerical tolerance for length comparisons.

    K_PI

    let K_PI : Double

    Pi.

    LINEAR_SLOP

    let LINEAR_SLOP : Double

    Skin width; contacts shallower than this are considered touching.

    clamp

    fn clamp(v : Double, lo : Double, hi : Double) -> Double

    Clamp v into [lo, hi].

    collide

    fn collide(shape_a : Shape, xf_a : Transform, shape_b : Shape, xf_b : Transform) -> Array[ContactPoint]

    Collide two shapes and return 0..2 world-space contact points.

    cross_scalar

    fn cross_scalar(z : Double, v : Vec2) -> Vec2

    z x v = (-z * v.y, z * v.x), scalar on the left.

    ray_cast_shape

    fn ray_cast_shape(shape : Shape, xf : Transform, origin : Vec2, direction : Vec2, max_distance : Double) -> (Double, Vec2)?

    Cast a ray against one shape.

    direction must be unit length (the World wrapper normalizes it). Returns (distance, normal) along the ray, or None when the ray misses. A ray starting inside the shape counts as a miss, which is what makes World::ray_cast usable for line-of-sight checks.