# Flow sources

# Flow sources

Package `@ferret/flow` (no physics, no Babylon), service `flow`.

## The field

`FlowField.at(position, time, medium)` returns `{ velocity, updraft?, turbulence? }` for `air` or `water`: the sum of the level's sources, then the shelters reduce it. Every source is a pure function of position and time, so every peer computes the same field with no replication, and the render can sample it without physics.

```json
"physics": {
  "capabilities": ["media"],
  "flow": [
    { "name": "breeze", "kind": "uniform", "velocity": [6, 0, 2], "profile": { "power": 0.14, "refHeight": 10 }, "gust": { "amplitude": 0.3, "period": 7 } },
    { "name": "twister", "kind": "vortex", "centre": [0, 0, 0], "core": 40, "circulation": 9000, "inflow": 0.3, "updraft": 25,
      "height": 600, "reach": 400, "path": [[0, 0, 0], [800, 0, 200]], "speed": 12, "level": { "curve": [[0, 0], [20, 1], [120, 1], [160, 0]] } },
    { "name": "mill-race", "kind": "channel", "medium": "water", "points": [[0, 0, 0], [0, 0, 40]], "velocity": 2.5, "width": 1.6 }
  ]
}
```

| Kind | Parameters |
|---|---|
| `uniform` | `velocity`, optional power-law `profile` by height, optional `gust` (the deterministic gust of `physics-media`) |
| `front` | `origin`, horizontal `direction`, `speed`, `width`, `before` and `after` velocities: a plane that travels; behind it the wind changes across the width |
| `vortex` | `centre`, `core`, `circulation` Γ: solid rotation inside the core, Γ/2πr outside, counter-clockwise from above; `inflow` (ratio of radial inflow to swirl), `updraft` in the core, `height` |
| `jet` | `origin`, `direction`, `velocity`, `halfAngle`, `decay` length: a fan, a vent, a propeller wash |
| `shelter` | `centre`, `radius`, `strength` 0..1: reduces the flow of the other sources in the lee and reports turbulence |
| `channel` | `points`, `velocity`, `width`: a flow along a polyline with a parabolic cross profile (a river, a race) |
| `curve` | `origin`, `cell`, `size`, `data`: a baked grid of velocities (3 numbers per cell), trilinear inside, nothing outside |

Every source also takes `medium` (`air` default, or `water`), `on`, `level` (a number or a curve `[[t, v], ...]`, a storm at noon), `reach` (a sphere outside which it adds nothing and is not evaluated) and `path` + `speed` + `start` (it moves along points).

## Who reads it

`physics-media` adds the field's air flow to the wind of its settings, and the field's **water** flow to every liquid medium (density from 100 kg/m³), so drag, lift and buoyancy of bodies follow it, in the air and in a mill race or a whirlpool; the old `wind` and `gust` settings keep working unchanged. Vessels (`Vessel.windAt`: sails, control surfaces, hull drag in air), the character (`CharacterMovement` is pushed by the wind by its area and mass, at most `maxWindAccel` m/s², 0 turns it off) and a liquid surface (`waterCurrent(field)` as its `current`) read it too. The render comes with stage A9. `FlowField.maxSources` is the budget of a console tier.

## Media zones in the field

The own `flow` and `gust` of a medium (a canyon, a current in a tank) are published into the field as a `uniform` source named `media:<medium>`, limited to the medium's zone, bounds and surface (air for a light medium, water for a liquid). So vessels, the character and the render see them. `physics-media` skips its own publications when it reads the field, so nothing is counted twice, and `list` / `edit.physics.flow.list` show level data only.

## Commands

`flow.at { position, medium?, time? }`, `flow.set { name, ... }`, `flow.on`, `flow.off`, `flow.move { name, path, speed }`; editor: `edit.physics.flow.set`, `.remove`, `.list`. `FlowService.load(list)` reads a level's `flow` list.

## Over the net (A8)

The field is a pure function of position and time, so only changes travel. `netFlow(field, net, { authority, authorityPeer, now, delay })`: the authority's `add/set/remove` carry an effect time and every machine applies them at that time (call `step()` every fixed step). A machine that is not the authority cannot change the weather, and messages from any other peer are ignored.

## For the inspector and the picture (A9)

Commands: `flow.grid {centre, half, step}` (arrows), `flow.reach` (each source's centre now, reach and vortex core), `flow.funnels`. Render numbers, pure functions of the field and the time (the same sky on every peer): `patchWind` (a wind uniform per patch of grass or trees), `advectClouds`, `plumeLean` (smoke and steam), `dropVelocity` (rain and snow slant), `funnels`, and `turbulenceNoise(seed, p, t)`, a smooth seeded noise the picture may add to ragged a gale; the physics never reads it. `flowArrows` in `physics-fx` draws the grid as lines.

## Flow boxes: flow around bodies (A13)

`@ferret/flow-solver`. The field above is a sum of analytic sources; bodies read it and do not change it. A **flow box** is a bounded slice where the flow is *solved*, so a body in it changes the flow and the flow pushes the body.

- **Solver** (`FlowGrid`): incompressible flow in two dimensions on a staggered grid. Advection by MacCormack with a limiter (a coarse grid keeps its eddies), explicit viscosity, optional vorticity confinement, and a pressure projection by conjugate gradient with a modified incomplete Cholesky preconditioner, warm-started from the last step, at most 40 iterations (a fixed cost per tier). It depends only on its data, never on the clock, so two peers with the same history have the same field (tested bit for bit).
- **Box** (`FlowBox`): `plane: 'xz'` is a horizontal slice at height `origin[1]` (the streets of a town, a river from above), `'xy'` a vertical one (a roof, a wing). Each step it samples the analytic field of the level (everything but itself) on its edges: a side the wind blows into is an inflow, a side it leaves by is open, a side with no normal wind is a wall. It publishes itself into `@ferret/flow` as a `solved` source: inside the box the solver's velocity replaces the analytic sum (blended over `margin` cells), the component off the slice stays the analytic one. Media, vessels, the character, rotors, sails and the picture read the wake without any change. `medium: 'water'` does the same for the water readers and `waterCurrent`.
- **Bodies**: `circle`, `box`, `polygon` in the plane, `static` (voxelised once) or moving (`setBody`, each step: the cells they cover take the body's velocity, so a swung door pushes the air). `force(id)` is the pressure and wall shear on the surface times the body's `depth`.
- **In a level** (`flowBoxes(fieldOf)` capability, block `physics.flowBoxes`): bodies with a `node` follow that rigid body. `forces: 'solved'` applies the force to it and switches the media drag of a body in an *air* box off (`exempt: ['media']`) so nothing is counted twice; the force never changes the body's velocity by more than the difference to the flow that arrives at it, so a body lighter than the air it moves is not thrown. `forces: 'coefficients'` (default) keeps the media drag and uses the box for the field only. Water boxes use `coefficients`.
- **Cost**: `maxCells` (default 40000): a box over it coarsens its cell by 1.25 until it fits and sets `degraded`. A box can `sleep` (the analytic field stays), `every: n` steps it every n-th physics step with the time gathered. Measured on the development machine: 0.8–0.9 µs per cell per step up to 16k cells (64²: 3.3 ms, 128²: 14.7 ms), 4 µs per cell at 256² (65k cells, cache limit). Console tiers scale these by their measured CPU ratio; the D7 numbers are set from them.
- **Tested**: divergence under tolerance every step with a body in the way; Bernoulli in a contraction within 5 %; a cylinder at Re 200 sheds at St 0.17–0.23 and has Cd within 20 % of 1.4; a plate at an angle gets drag and lift to the right side; a swung plate pushes the air and the air pushes back; a tower's lee has less than half the wind; a pier makes a wake in water; deterministic.
- **Commands**: `flowbox.state`, `flowbox.slice` (the inspector view: points with velocity, speed, pressure, solid), `flowbox.at`, `flowbox.wake`, `flowbox.sleep`, `edit.physics.flowBoxes.set/remove`. Words: `(pressure-at x y z)`, `(dynamic-pressure x y z)` (½ρv²); `flow-at` already reads the box.
- **Not built**: `3d` boxes (refused with that message), compressible flow, a pressure-driven roof that comes off (the force is there; the strength link is a scene), the render of the slice in the inspector panel (the command is there).
