Documentation
    Preparing search index...

    Class ForceFieldSystem

    Applies §27 force fields to every dynamic rigid body of every tracked world, once per fixed step (§39 step 5, §26, §27).

    See the module header for the design: why this is a system rather than a step inside PhysicsWorld, why the cross-package field contract is structural, and why the units are a required argument.

    Implements

    Index
    priority: number

    Execution order key (§39); default PRIORITY_FORCES.

    • get fields(): readonly ForceFieldEntry[]

      The registered fields, in registration order — which is the order they are summed in, and therefore part of the determinism contract (§33: floating point addition is not associative).

      Returns readonly ForceFieldEntry[]

    • Registers field, sampled in units, at the end of the summation order.

      The same field object may be registered twice — with different units, or with the same ones — and is then sampled twice per body per step. Nothing deduplicates: a field is a pure function (§33), two registrations of one function are two terms of the sum, and silently collapsing them would make 2 × drag unspellable.

      Parameters

      Returns ForceField

      field, so a construction call can be inlined

    • §39 lifecycle: forgets every world and every field.

      The worlds are not disposed — a world outlives the systems that read it (PhysicsSystem says the same), and this one never owned any of them.

      Returns void

    • Samples every registered field at every dynamic body of every tracked world and queues the total through §26 (§39 step 5).

      for each tracked world, in tracking order:
        world.forEachActiveBody: every dynamic, awake body, in registration order (§33)
          point    = the visit's world-space centre of mass  (§25)
          velocity = body.linearVelocity, copied             (start-of-step value)
          total    = Σ over fields, in registration order:
                       sample(point, velocity, simulationTime) × (units === "acceleration" ? mass : 1)
          if total ≠ 0: body.applyForce(total)
      

      §26 splits applyForce from applyForceAtPoint, and this is the former: the force acts at the centre of mass and induces no torque. Sampling at the point the force acts at is the only choice that keeps a field's answer and its effect describing the same place — for an off-centre or compound body (§24) the transform origin can be anywhere, including outside the shape. PhysicsWorld.forEachActiveBody reads it from the solver, and is also where the "dynamic and awake" filter and its §22/§32 arguments live.

      RigidBody.linearVelocity is refreshed from the solver in the previous step's publish pass, so a velocity-dependent field (drag, wind) sees the state the step begins in — the same rule the particle path states, and what makes a drag field's closed form checkable. The copy costs one vector write per body and makes §27's "do not mutate velocity" advisory rather than load-bearing: a badly written field corrupts this system's scratch instead of the component's mirror of solver state.

      Sleeping and non-dynamic bodies are skipped unless an entry opts in

      By forEachActiveBody, which states the argument: a force on a static or kinematic body does nothing, and RigidBody.applyForce does not implicitly wake a sleeper (WP-5.2). A persistent field (gravity, wind) that visited every sleeper and then called wake() would defeat §32. The opt-in is per-entry ForceFieldAddOptions.wakesSleepingBodies: only those fields walk forEachSleepingDynamicBody, and only a non-zero contribution calls RigidBody.wake(). Two entries that disagree do not share a visit — the flag is not a system-wide OR.

      Nothing is allocated after construction except the batched SoA scratch, which grows to the next power of two that covers world.size the first time a ForceField.sampleAll field meets that many registrations and is then reused. A system with no fields, or with no tracked world, does no solver call at all: the field list is tested before the worlds are walked, so registering the system and using it later costs one array-length comparison per step.

      When at least one registered field offers sampleAll, that world's active bodies are gathered into stride-3 SoA buffers (binary64, so the packed values equal centerOfMass / linearVelocity bit-for-bit) and each such field is invoked once per step instead of once per body. A field without the fast path is still sampled with ForceField.sample, in its own place in the registration order, into the same newton accumulator — one custom field neither reorders the sum nor disables batching for its neighbours. Per-body summation order is unchanged: field 0 then field 1 then field 2, which is why the force-fields golden does not move.

      Parameters

      Returns void

    • Stops visiting world's bodies. Returns whether it was tracked, so a teardown path may call it unconditionally. The world itself is untouched: forces already queued on its bodies are still drained by its next step, because they are §26 commands like any other.

      Parameters

      Returns boolean