# Procedural PCB Background

Animated, fully procedural printed-circuit-board pattern intended as a screen-filling, 4K-friendly background. Generates a grid-based mesh of traces with pads, bus groups, and edge pins; sends light "signals" travelling along the traces; reacts to the cursor with localized heat that boosts signal density, signal speed, and emits radial pulses on nearby pads.

- **Protocol:** Rive Luau `Layout<T>`. The host artboard provides the size; the script fills it.
- **File:** `scripts/pcb-bg/pcb_mk2.1.luau`
- **Source name:** `PCB_V4_FAST_4K` (in-script type identifier)
- **Type-checked:** Yes — `--!strict` declared at top of file.

---

## Quick start

1. **Add a Layout in the Rive editor** with `L = R = T = B = 0` so the layout fills its parent.
2. **Attach this script as a Node child** of that layout. The script implements `measure` / `resize`, so the runtime feeds it the Layout's pixel size each frame.
3. The Layout receives **pointer events**. The script handles `pointerDown` / `pointerMove` / `pointerUp` / `pointerExit` automatically — make sure the Layout is hit-test-enabled on the artboard.
4. **Adjust inputs in the right-panel inspector.** Property names encode their valid ranges (`<name>_min<lo>_max<hi>`).

The very first frame builds the board (`buildBoard`) using `seed` and the current size; subsequent frames just animate. Calling `update` (which the runtime triggers when any input changes) rebuilds the board, so toggling a knob in the editor regenerates the layout in place.

---

## Property naming convention

Every numeric input is suffixed with its range:

```
<name>_min<lo>_max<hi>
```

Booleans use `_min0_max1`. Colors use `_rgba0_255`. The script clamps every input to its declared range internally, so you can't break it by feeding out-of-range values, but the inspector ranges documented here reflect what the script actually treats as meaningful.

---

## Inputs by group

### Board geometry

| Property | Range | Default | What it does |
|---|---|---|---|
| `cellPx_min12_max64` | 12–64 px | 24 | Target grid cell size in pixels. Smaller = denser, more traces, more cost. The script picks `cols / rows` so that each cell is approximately this size; actual `cellW` / `cellH` are recomputed from the layout dimensions. |
| `densityPct_min5_max95` | 5–95 % | 48 | Target trace coverage. Internally drives `desiredTraceCount = cellCount × density × 0.135` (clamped by `maxTraces`). |
| `maxTraces_min50_max3000` | 50–3000 | 1300 | Hard cap on total trace count. At 4K you'll typically hit this before the density target. |
| `maxBuildAttempts_min500_max9000` | 500–9000 | 6500 | Random-walk attempts the generator gets to lay traces. Higher = more complete board, slower one-time build. The cost is amortized per `clearance²` to avoid runaway loops when clearance > 0. |
| `seed_min1_max999999` | 1–999999 | 1337 | RNG seed. Same seed + same dimensions = identical board. The seed is mixed with `width × 73 + height × 151 + cols × 241 + rows × 307` so different sizes still randomise. |

### Routing (how traces walk the grid)

| Property | Range | Default | What it does |
|---|---|---|---|
| `routeMinCells_min2_max12` | 2–12 cells | 3 | Minimum length of a successful trace, measured in grid steps. |
| `routeMaxCells_min6_max40` | 6–40 cells | 18 | Maximum length per trace. Long traces look more "organic" but compete more for free cells. |
| `turnChancePct_min5_max95` | 5–95 % | 42 | Probability the random walker picks a new direction at each step. Low values give long parallel runs; high values give twisty paths. |
| `edgeStartPct_min0_max100` | 0–100 % | 30 | Probability that a new trace starts from the canvas edge (vs. a random interior cell). Higher = more "I/O-looking" outward fan. |
| `trackClearanceCells_min0_max2` | 0–2 cells | 0 | Minimum gap between adjacent traces. `0` = traces can sit on neighbouring cells. `1` or `2` give cleaner separation but reduce achievable density quickly. |
| `padKeepoutCells_min0_max3` | 0–3 cells | 1 | Radius of empty cells reserved around every pad (trace endpoints, edge pins, bus pins). |
| `cornerChamferPct_min0_max45` | 0–45 % | 32 | How much the right-angle corners of each trace get chamfered, expressed as a percentage of the cell. `0` = pure 90° elbows; ~32 % matches typical PCB CAD rendering. |

### Buses & edge pins

| Property | Range | Default | What it does |
|---|---|---|---|
| `busGroupsPer1000Cells_min0_max80` | 0–80 / 1000 cells | 4 | Density of straight parallel "bus" groups. The script multiplies by `cellCount / 1000` so denser canvases get more buses without recomputing. |
| `busLinesMin_min2_max8` | 2–8 lines | 3 | Lower bound for lines in a single bus group. |
| `busLinesMax_min2_max10` | 2–10 lines | 6 | Upper bound. The actual count per group is `randInt(busLinesMin, busLinesMax)`. |
| `busLengthCells_min3_max28` | 3–28 cells | 14 | Length of each bus line in grid cells. |
| `edgePinsPer1000Cells_min0_max80` | 0–80 / 1000 cells | 4 | Density of short "edge pin" stubs that come in from the canvas border. |

### Strokes & dot sizes

| Property | Range | Default | What it does |
|---|---|---|---|
| `traceThicknessPx_min1_max8` | 1–8 px | 2.1 | Stroke thickness for the static trace path. |
| `signalThicknessPx_min1_max12` | 1–12 px | 3.8 | Stroke thickness for travelling signal segments. The "hot" (cursor-influenced) variant draws ~1.4× thicker. |
| `padRadiusPx_min3_max18` | 3–18 px | 7.4 | Outer ring radius of every pad. Hole drawn at 76 %, dot at 27 %. |
| `signalDotRadiusPx_min2_max14` | 2–14 px | 4.4 | Radius of the bright dot that follows each signal's head. Hot signals scale up to ~1.8× this radius. |

### Signals (animated dots travelling along traces)

| Property | Range | Default | What it does |
|---|---|---|---|
| `baseSignalCount_min0_max200` | 0–200 | 46 | Minimum number of active signals in steady state. |
| `extraSignalCount_min0_max240` | 0–240 | 96 | Additional signals that fade in proportional to cursor heat. Total target = `base + extra × heat`, capped by `maxSignals`. |
| `maxSignals_min0_max260` | 0–260 | 150 | Hard cap on simultaneous signals (defensive — prevents overdraw at huge heat). |
| `signalSpeedPxS_min50_max2400` | 50–2400 px/s | 920 | Base travel speed along each trace. Local cursor influence multiplies by up to ~2.85× when a signal head is at the cursor. |
| `signalTrailCells_min1_max8` | 1–8 cells | 3.2 | Length of the visible trail behind each signal head, expressed in cells (so it scales with `cellPx`). The "hot" signals get a 2.3× longer trail. |

### Cursor / mouse interaction

| Property | Range | Default | What it does |
|---|---|---|---|
| `mouseRadiusPx_min40_max600` | 40–600 px | 220 | Radius of the cursor heat field. Inside this disc the influence smoothsteps from 1 (at cursor) to 0 (at the edge). |
| `cursorPulseRate_min0_max40` | 0–40 / s | 10 | How many radial pulses per second emit from pads near the cursor while the cursor is active and the heat field overlaps a pad. |
| `maxPulses_min0_max140` | 0–140 | 80 | Cap on simultaneous pulses. Pulses fade and remove themselves over time; this just limits the worst case. |

### Background / colors

| Property | Type | Default | What it does |
|---|---|---|---|
| `drawBackground_min0_max1` | bool | `true` | If true, fills the layout with `backgroundColor`. Disable to composite the PCB on top of an existing background. |
| `backgroundColor_rgba0_255` | Color | `(18, 18, 20, 255)` | Fill color when background is enabled. |
| `traceColor_rgba0_255` | Color | `(28, 206, 218, 178)` | Trace stroke colour. |
| `padColor_rgba0_255` | Color | `(48, 235, 246, 235)` | Pad ring + dot colour (the centre hole always uses `backgroundColor`). |
| `signalColor_rgba0_255` | Color | `(168, 255, 255, 255)` | Cool / non-hot signal stroke + head colour. |
| `signalGlowColor_rgba0_255` | Color | `(62, 244, 255, 90)` | Feathered halo behind signals (drawn underneath the bright signal stroke). Hot signals draw a brighter, broader halo at ~`(130, 255, 255, 130)` baked into the script. |

---

## Behaviour at runtime

### Build phase (one-shot per resize/seed/density change)
`init` → `applyPaints` → `buildBoard`. `buildBoard` clears all paths, computes grid dimensions from the layout size, scatters edge pins, lays bus groups, then runs random walks until either `desiredTraceCount` traces are placed or `maxBuildAttempts` is exhausted. Pads are added at every trace start/end. Trace polylines are chamfered before being committed to `tracePath`.

### Per-frame loop
`advance` accumulates wall-clock time and drives a fixed-step (60 Hz) simulation, capped at 3 sub-steps per frame so the simulation never runs away on a slow draw.

Each fixed step:

1. **Cursor heat** smoothly attacks toward 1 (when `mouseActive`) at ~10/s and decays toward 0 at ~3.4/s once the pointer leaves.
2. **Signal target count** = `base + extra × heat`, capped by `maxSignals`. Spawn rate is `16 + 70 × heat` per second.
3. **Each signal** advances along its trace at `signalSpeedPxS × (1 + localBoost × 1.85)`, where `localBoost` is the smoothed cursor influence at the head's current position. When a signal reaches the trace end it emits a pulse on that pad.
4. **Local pulse rate** (cursor heat × `cursorPulseRate_min0_max40`) generates extra pulses on the pad nearest the cursor.
5. **Pulses** are radial rings that grow with eased-out timing, fade out via `(1 − t)²` alpha, and self-remove.

### Pointer handling

| Event | Effect |
|---|---|
| `pointerDown` / `pointerMove` (inside canvas) | Sets target cursor position. Sets `mouseActive = true`, marks the layout hit. |
| `pointerUp` (inside canvas) | Updates target and releases (`mouseActive = false`). |
| `pointerExit`, or any pointer event whose position is within `HOVER_EDGE_RELEASE_PX` (1 px) of the layout edge | Immediately releases hover and zeros heat. This avoids the "stuck cursor" effect when the pointer leaves the canvas. |

Cursor smoothing uses an exponential-decay follower at ~16/s, so the cursor visual lags slightly behind the pointer for a softer interaction feel.

---

## Performance / tuning

- **Cell density is the dominant cost.** Halving `cellPx_min12_max64` quadruples the cell count, which roughly quadruples build cost and (worst-case) trace count.
- **`maxTraces_min50_max3000` and `maxBuildAttempts_min500_max9000` are independent caps.** Use `maxTraces` to bound steady-state cost; use `maxBuildAttempts` to bound the one-time build cost when traces are hard to place (e.g., high `trackClearanceCells`).
- **The grid is bucketed (`bucketSizeCells = 8`).** Cursor-aware trace selection (`chooseTraceIndex`) and pad selection (`choosePadNearMouse`) only scan buckets near the cursor, so cursor interaction stays cheap even at 4K.
- **All static geometry lives in `tracePath` / `padHolePath` / `padRingPath` / `padDotPath`.** They're rebuilt only on `update`/`resize`/init. Per-frame work goes into `signalPath` / `hotSignalPath` / `signalHeadPath` (rebuilt every frame in `rebuildDynamicPaths`) and the pulse list (each pulse owns its own path/paint).
- **`advance` early-returns** when no signals, no pulses, no cursor heat, and base/extra signal counts are zero — so an inert background costs effectively nothing per frame.
- **For 4K layouts** keep `cellPx ≥ 24`, `densityPct ≤ 60`, `maxTraces ≤ 1500` as a starting envelope; from there raise the visual budget while watching frame time.

---

## Visual recipes

A few starting points for common looks. All values are inputs in the inspector; everything else stays at default.

| Look | `cellPx` | `densityPct` | `turnChancePct` | `edgeStartPct` | `cornerChamferPct` | `baseSignalCount` |
|---|---|---|---|---|---|---|
| **Clean PCB grid** | 28 | 38 | 22 | 50 | 35 | 30 |
| **Dense data board** | 18 | 75 | 60 | 20 | 25 | 80 |
| **Sparse hero background** | 38 | 22 | 32 | 10 | 40 | 18 |
| **Bus-heavy spine** | 24 | 30 | 18 | 30 | 30 | (reduce; raise `busGroupsPer1000Cells` to 14 and `busLengthCells` to 22) |

To visualise the heat field, temporarily set `mouseRadiusPx_min40_max600` to ~500 and `cursorPulseRate_min0_max40` to 28 — pulses will trail the cursor like a comet.

---

## Boolean & enumeration semantics

`drawBackground_min0_max1` is the only boolean, and it follows the project convention of declaring booleans with `_min0_max1` so the editor's slider behaviour is consistent with the numeric inputs (treats `0` as `false`, anything `≥ 0.5` as `true`). It's safe to bind to a state-machine boolean directly.

There are no enums; every numeric is a continuous range.

---

## Internal state (read-only)

These fields exist on `self` but are **runtime-managed** — don't bind them to ViewModel properties:

`width`, `height`, `cols`, `rows`, `stride`, `cellW`, `cellH`, `cellPx`, `trackClearanceCells`, `padKeepoutCells`, `cornerChamferPx`, `bucketSizeCells`, `rng`, `accumulator`, `spawnBudget`, `localPulseBudget`, `mouseActive`, `mouseSeen`, `mouseHeat`, `mouseX`, `mouseY`, `mouseTargetX`, `mouseTargetY`, `occupied`, `traceBuckets`, `padBuckets`, `traces`, `pads`, `signals`, `pulses`, plus the eight `Path` and ten `Paint` objects.

The `init` / `resize` / `update` / `advance` / `draw` lifecycle handles all of these — there is no need to reach into them from the editor or from another script.

---

## File layout (where to look in the source)

| Section | Lines | What's there |
|---|---|---|
| Type declarations + constants | 1–160 | `GridPoint`, `TraceSegment`, `Trace`, `Pad`, `Signal`, `Pulse`, `PCB4K`. |
| Math helpers | 162–321 | `clamp`, `mix`, `smoothStep`, `rand01`, distance helpers, `addRect`, `addCircle`. |
| Grid bookkeeping | 323–500 | Cell occupancy, bucket assignment, polyline cleanup + chamfering. |
| Trace generation | 561–911 | `appendTraceFromGridPoints`, `tryWalkRoute`, `tryAddStraightCells`, `addBusGroups`, `addEdgePins`. |
| Cursor / mouse | 1055–1163 | `cursorInfluenceAt`, `chooseTraceIndex`, `choosePadNearMouse`. |
| Signals & pulses | 1165–1360 | `tracePointAt`, `addTraceWindow`, `addPulse`, `spawnSignal`, `rebuildDynamicPaths`, `updatePulses`. |
| Simulation loop | 1362–1441 | `simulate` — the per-fixed-step driver. |
| Lifecycle | 1443–1678 | `buildBoard`, `init`, `measure`, `resize`, `update`, `advance`, `draw`, pointer handlers. |
| Layout returner + defaults | 1680–1855 | The table returned at the end of the script — this is the source of truth for default values. |
