mwg API
    Preparing search index...

    Class XpbdWorld<T>

    A deterministic structure solver: point masses joined by springs, solved with extended position-based dynamics (XPBD), with the force in every link read back so a link can break at a real load rather than at a stretch.

    The method (Macklin, Muller and Chentanaput's XPBD) moves each node freely for a substep, then corrects positions so every link returns to its rest length, with a compliance of rest / stiffness / dt^2: a link behaves as a spring of the stated stiffness whatever the step or iteration count, and the correction's Lagrange multiplier divided by dt^2 is the force it carried. Steps are fixed and nothing is random, so the same structure and the same calls give the same result on every machine, which is what replaying or validating a player's bridge needs.

    It is the engine of Pontifex (a bridge-building game), generalised: the vehicle, the water and the levels stay in the game, and what is here is only nodes, links, forces and breaking. Unlike PhysicsWorld it has no rigid bodies or collisions: nodes and links only.

    import { XpbdWorld } from '@datamoc/mwg/physics/xpbd';

    const world = new XpbdWorld({ gravity: 520 });
    const wall = world.addNode({ x: 0, y: 0, fixed: true });
    const weight = world.addNode({ x: 40, y: 0, mass: 1 });
    const rope = world.addLink({ a: wall, b: weight, stiffness: 2e4, strength: 900 });
    world.breaking = true;
    for (let i = 0; i < 120; i++) world.step();
    console.log(world.links[rope].broken, world.nodes[weight].y > 0);

    A bridge is a truss: beams are links, a cable is a link with tensionOnly: true (it pulls when stretched and goes slack when pushed), and a link with a strength breaks at that load. A bare chain of planks has no vertical stiffness and only sags like a rope, so the deck needs triangles.

    import { XpbdWorld } from '@datamoc/mwg/physics/xpbd';

    //a Warren truss between two abutments, with a cart of `cartMass` at midspan
    function loadBridge(cartMass: number): { sag: number; broken: number; peakLoad: number } {
    const world = new XpbdWorld({ gravity: 520 });
    const spacing = 50;
    const bottom: number[] = [];
    for (let i = 0; i <= 6; i++) bottom.push(world.addNode({ x: i * spacing, y: 0, fixed: i === 0 || i === 6 }));
    const top: number[] = [];
    for (let i = 0; i < 6; i++) top.push(world.addNode({ x: i * spacing + spacing / 2, y: -45 }));

    const beam = (a: number, b: number): void => {
    world.addLink({ a, b, stiffness: 6e4, strength: 16000 });
    };
    for (let i = 0; i < 6; i++) beam(bottom[i], bottom[i + 1]); //the deck
    for (let i = 0; i < 5; i++) beam(top[i], top[i + 1]); //the upper chord
    for (let i = 0; i < 6; i++) {
    beam(bottom[i], top[i]); //the diagonals
    beam(top[i], bottom[i + 1]);
    }

    //the cart rests on the deck and the chord; without a `strength` its links cannot break
    const cart = world.addNode({ x: 150, y: -6, mass: cartMass });
    world.addLink({ a: cart, b: bottom[3], stiffness: 6e4 });
    world.addLink({ a: cart, b: top[2], stiffness: 6e4 });

    world.breaking = true;
    for (let i = 0; i < 360; i++) world.step();
    return {
    sag: Math.max(...bottom.map((node) => world.nodes[node].y)),
    broken: world.links.filter((link) => link.broken).length,
    peakLoad: Math.max(...world.links.map((link) => link.ratio)), //1 is the breaking point
    };
    }

    console.log(loadBridge(2)); //holds, a little sag
    console.log(loadBridge(20)); //too heavy: beams break

    Type Parameters

    • T = undefined
    Index
    breaking: boolean = false

    links break only while this is true, so a structure can settle under its own weight first

    damping: number
    gravity: number
    ground: XpbdGround | null = null

    nodes are held on or above this; null for none. Water, a gorge or a cliff face is the game's own business

    groundFriction: number
    links: XpbdLink<T>[] = []
    nodes: XpbdNode[] = []
    onBreak: Signal<{ link: number; x: number; y: number }> = ...

    fires once for a link that passed its strength, which has already stopped carrying load

    peakRatio: number = 0

    the highest smoothed load ratio across the links at the last step, for a strain gauge

    • an external force on a node for the next step only (a wheel's weight, wind)

      Parameters

      • node: number
      • fx: number
      • fy: number

      Returns void

    • breaks a link now, as if it had passed its strength: it stops carrying load and onBreak fires. A broken link is left alone

      Parameters

      • index: number

      Returns void

    • a 32-bit FNV-1a hash of snapshot()'s exact bits: equal for equal states, whatever the machine

      Returns string

    • indices of the unbroken links with an endpoint or a crossing inside the box (a ship's hull, a blast area)

      Parameters

      • left: number
      • top: number
      • right: number
      • bottom: number

      Returns number[]

    • indices of the unbroken links whose segment passes within radius of a point, nearest first

      Parameters

      • x: number
      • y: number
      • radius: number

      Returns number[]

    • Dynamic solver state and queued forces, for comparing runs with the same configuration. Game payloads and the ground callback are not captured.

      Returns { forces: number[][]; links: number[][]; nodes: number[][] }