# Locus — agent brief

**Read this before changing the four-bar / 10-bar simulator.**
It is the context hand-off for another Grok (or any agent) that will extend the app.

Public copies:

- In-app: [`/docs/AGENTS.md`](/docs/AGENTS.md)
- Math note (PDF): [`/docs/locus-model-and-dynamics.pdf`](/docs/locus-model-and-dynamics.pdf)
- Nested project instructions (this sandbox): `src/lib/mechanism/AGENTS.md`

Paste this into a new Grok session:

```
Read https://raw.githubusercontent.com/rayjyotir5/rayjyotir5.github.io/main/locus-simulator/AGENTS.md
You are extending Locus, a planar four-bar + Roberts 10-bar simulator.
Obey that brief. Do not regress kinematics continuity, reduced EL, or the split joint.
```

---

## What it is

**Locus** is an interactive planar mechanism lab:

1. A **four-bar** \(O_2A\,AB\,BO_4\,O_4O_2\) with coupler point \(P=(p,q)\) in the \(AB\) frame.
2. Assembly by **projective geometry**: radical axis of the two joint circles, then the common chord.
3. Live **coupler curve** (sextic).
4. The two **Roberts–Chebyshev cognates**, drawn as a solid **10-bar** that moves in tandem.
5. Optional **split** of the triple joint at \(P\) into \(P_{12},P_{13},P_{23}\) (parameter \(\varepsilon\)).
6. **Reduced 1-DOF Euler–Lagrange** dynamics on \(\theta_2\): two torsion springs, viscous revolutes, one constant world force, no gravity.

Stack: Vite + React + Zustand (persist) + canvas 2D. Entry: `src/routes/index.tsx`.

---

## Hard invariants (do not regress)

1. **One DOF.** Configuration is \(\theta_2\) plus a **sheet hint** \((B,P)\). Never introduce an independent \(\theta_3\) integrator. Cognates are a **function of the live four-bar pose**, not a second mechanism.
2. **Projective assembly.** \(B\) comes from `intersectCirclesProjective` (radical axis + line/circle). Do not switch to a cosine-law branch sign (`±1` assembly) as the live solver.
3. **Velocity continuity of \(P\).** Through a lock, fold through the sheet using predicted \(P+V_P\). Do **not** reverse \(P\) at a change-point on a fully rotating crank.
4. **Limited cranks stay limited.** If `analyze().crankFullyRotates` is false, sweep only the circuit; bounce at the ends; **never** force \(\theta_2\in[0,2\pi)\) so links disappear.
5. **Kinematic mode must not chatter.** After dynamics is switched off, keep the current \(B,P\) hint. Do not `seedHintFrom` every frame. Multiply `dTheta` by `direction`. Cap predicted \(PV\). Clear \(V,PV\) on a circuit bounce.
6. **Dynamics is reduced EL, not penalty/DAE.**
   \[
   M(\theta)\ddot\theta + \tfrac12 M'(\theta)\dot\theta^2 + c\,D(\theta)\dot\theta
   = Q_{\mathrm{spr}}(\theta)+Q_F(\theta).
   \]
   Loop-close first, then finite-difference Jacobians. Do not add gravity unless asked. \(c=0\) must nearly conserve \(T+V_{\mathrm{spr}}+V_F\).
7. **\(O_6\) is not a ground pin.** It is a Cayley point, floating pin of \(C_2O_6\) and \(C_3O_6\). At \(\varepsilon=0\) it is geometrically stationary so \(Q_F\) on \(O_6\) is \(\approx 0\). At \(\varepsilon>0\) it **moves**.
8. **Split \(\varepsilon\) is kinematic, not cosmetic.** \(P_{12},P_{13},P_{23}\) are rigid in the \(AB\) frame. Cayley parallelograms and similarities use those pins. Inertia includes the three small-triangle bars. \(\varepsilon=0\) must recover coincident-\(P\) Roberts.

---

## File map

All mechanism code lives in `src/lib/mechanism/`. UI in `src/components/simulator/`.

| File | Role |
| --- | --- |
| `types.ts` | `Dimensions`, `Pose`, `BranchHint`, `Circuit`, `Analysis` |
| `projective.ts` | \(\mathbb{R}^2\) ops, homogeneous points, circle conic, **radical axis**, line/circle intersect, coupler point \((p,q)\) |
| `kinematics.ts` | `solvePose`, Grashof `analyze`, circuits, locks, `traceCouplerCached`, `branchAfter` |
| `advance.ts` | `advanceConfiguration` — continuous crank step, lock visit, bounce, predicted-\(P\) scoring |
| `cognates.ts` | Cayley construction, `splitTripleJoint`, `cognatesFromPose(pose, eps)` |
| `dynamics.ts` | bodies, \(M,M',Q,D\), springs, force, `evaluateReduced`, `stepLagrange` (RK2) |
| `store.ts` | Zustand + persist. `step(dt)` is kinematic **or** Lagrange |
| `presets.ts` | crank-rocker, Watt, Chebyshev, Hoecken, lemniscate, drag-link, parallelogram, rhombus, … |
| `MechanismCanvas.tsx` | rAF loop, drawing, drag \(P/A/O_4\), pan/zoom |
| `ControlPanel.tsx` | dims, motion, dynamics, construction, math readout, \(\theta_2(t)\) plot |
| `ThetaPlot.tsx` | unwrapped \(\theta_2\) vs time while dynamics runs |

---

## Kinematics

**Dimensions** `{ d, l2, l3, l4, p, q }`. Ground \(O_2=(0,0)\), \(O_4=(d,0)\). Crank \(A=\ell_2(\cos\theta_2,\sin\theta_2)\).

**Pose.** Circles: centre \(A\) radius \(\ell_3\), centre \(O_4\) radius \(\ell_4\). Radical axis \(\mathcal L=C_A-C_4\). Intersect \(\mathcal L\) with the crank circle → two points \(B,B^\star\). Pick with the hint (nearest \(B\), else predicted \(P\)). Coupler point \(P=A+p\,\hat t+q\,\hat n\).

**Sheets.** Two assemblies per \(\theta_2\) when the circles meet. `BranchHint` carries last \(B,P\) and optional velocities \(V,PV\). `solvePose(dims, theta2, hint)`.

**Circuits.** `analyze(dims)` → Grashof class, `crankFullyRotates`, `circuits[]` as unwrapped \([\mathrm{start},\mathrm{end}]\). Locks: \(|A-O_4|\in\{\lvert\ell_3-\ell_4\rvert,\ell_3+\ell_4\}\).

**Advance (kinematic play):**

- `dTheta = direction * speed * dt`
- If a lock is crossed, **visit** the exact lock angle then continue (fold-through).
- If the trial \(\theta\) leaves a limited circuit, **reflect** \(\theta\) and flip `direction`; **drop** \(V,PV\).
- Score candidate \(B\)s by predicted \(P\). Cap \(\lVert PV\rVert\).

**Coupler curve** is traced independently (`traceCouplerCached`) and is not the dynamics trajectory.

---

## Cognates and the 10-bar

From a live assembled pose (complex/plane vectors):

\[
A_2=O_2+(Q_{12}-A),\quad
B_3=O_4+(Q_{13}-B)
\]
\[
C_2=A_2+(Q_{12}-A_2)\frac{Q_{12}-A}{B-A},\quad
C_3=Q_{13}+(B_3-Q_{13})\frac{Q_{13}-A}{B-A}
\]
\[
O_6=C_2+(C_3-Q_{23})
\]

At \(\varepsilon=0\), \(Q_{12}=Q_{13}=Q_{23}=P\) and \(O_6\) is stationary.

**Split \(\varepsilon>0\).** Equilateral of circumradius \(\varepsilon\), **body-fixed in the \(AB\) frame**, centred on \(P\):

- \(P_{12}\) original ∩ left
- \(P_{13}\) original ∩ right
- \(P_{23}\) left ∩ right

`store.splitEps` (0…0.35, default 0.07) is passed into `cognatesFromPose` **and** `evaluateReduced`. Changing \(\varepsilon\) changes \(M(\theta)\) and moves \(O_6\).

Nine moving rods plus, when split, three small-triangle bars and \(A\!-\!P_{12}\), \(B\!-\!P_{13}\). Coupler triangles are not triple-counted as extra mass.

**\(O_6\) is not grounded.** Do not draw \(O_2O_6\) / \(O_4O_6\) as ground. Damping at \(O_6\) is the relative rate of \(C_2O_6\) vs \(C_3O_6\).

---

## Dynamics

Reduced coordinate \(q=\theta_2\). `snapshotAt` loop-closes the four-bar then builds cognates at \(\varepsilon\). Jacobians by finite difference \(h=2.5\cdot10^{-3}\) on the **same sheet**.

**Inertia.** Each rod: \(m=\mu L\), \(I=mL^2/12\), COM at midpoint.

\[
M(\theta)=\sum_i\big(m_i\lVert J_{v,i}\rVert^2+I_i J_{\omega,i}^2\big)
\]

\(M'\) is centred-difference, clamped \(\lvert M'\rvert\le 6M\).

**Springs.** Up to two, joints \(O_2,A,O_4,O_6\). Neutral \(\varphi_j^0\) independent.

\[
Q_{\mathrm{spr}}=-\sum_j k_j(\varphi_j-\varphi_j^0)\,\varphi_j'
\]

**Damping.** Rayleigh \(D=\sum_k(\partial\psi_k/\partial\theta)^2\) over revolutes (ground pins, floating pins, split pins, cognate pins at \(P\)). Torque \(-c D\dot\theta\).

**Force.** Constant world \(F=F(\cos\alpha,\sin\alpha)\) on a vertex \(A,B,P,P_{12},P_{13},P_{23},A_2,C_2,B_3,C_3,O_6\).

\[
Q_F=F\cdot\partial r_\star/\partial\theta,\qquad V_F=-F\cdot r_\star
\]

**Integrator.** RK2 / Heun, 10 substeps per frame. After each trial, `reflectOnCircuit` then `driveNearest` (walk \(\theta\) in ≤0.05 rad steps with **position** hints only — no \(V\) — so dynamics cannot flip the sheet).

**Energy.** \(E=T+V_1+V_2+V_F\). With \(c=0\) relative drift should stay \(\sim10^{-3}\) over a few seconds. On-screen \(\dot W=c D\dot\theta^2\).

**UI contract.** `setDynamics(true)` captures spring rests at the current pose and **pauses**. **Release** applies `springDelta` to \(\theta_2\) (clamped to the circuit) and plays. Switching dynamics **off** must restore smooth kinematic sweep (invariant 5).

Time scale reuses `speed`. Trace `{t, θ}` is unwrapped for the plot.

---

## Store (what to call)

`useMechanism` in `store.ts`. Important fields: `dims`, `theta2`, `branchB/V/P/PV`, `playing`, `speed`, `direction`, `dynamics`, `omega`, `springs[2]`, `springDelta`, `massDensity`, `jointDamping`, `force`, `splitEps`, `energy`, `thetaTrace`.

Kinematic step uses `advanceConfiguration`. Dynamic step uses `stepLagrange(..., force, splitEps)`.

Persist **does not** save `dynamics`, `omega`, traces, or hints. Rehydrate forces `dynamics=false`.

---

## UI / canvas

- Drag **P** → set `p,q` in the \(AB\) frame.
- Drag **A** → `setTheta`.
- Drag **O₄** → set `d`.
- Wheel zoom, double-click fit, space pause.
- Header **Notes** → math PDF. Header **Agents** → this file.

Do not restyle the chrome unless asked. Tokens live in `src/styles.css` (`--color-crank`, `--color-locus`, …).

---

## How to extend (patterns that fit)

| Ask | Where | Note |
| --- | --- | --- |
| New preset | `presets.ts` | Keep `analyze` happy; limited cranks must have a circuit |
| Extra spring / joint | `SpringJoint` + `jointAngle` | Must be a function of the pose |
| Extra force point | `ForceVertex` + `vertexAt` | Jacobian is finite-diff of that point |
| Gravity | add \(V_g=\sum m_i g y_i\) to `evaluateReduced` | Same \(Q\) pattern as the constant force |
| 2-DOF / break Roberts | **new** reduced coordinates | Do not fake it by letting bar lengths vary and calling it rigid |
| Export / plot | `thetaTrace` already; add series the same way | Unwrap with `shortestAngle` |
| Different integrator | `stepLagrange` only | Keep loop-close + sheet hints |

---

## Verification (run before claiming done)

```bash
npm run typecheck
```

Headless checks that have mattered:

- Watt / Chebyshev: limited circuit, no disappearing links, \(P\) continuous through locks (kinematic).
- Crank-rocker: 0 sheet flips over a full turn after `setDynamics(false)`.
- Cognates at \(\varepsilon=0\): \(\lVert\Delta O_6\rVert=0\) over a crank cycle; cognate bar lengths constant.
- Split \(\varepsilon=0.07\): small triangle equilateral of side \(\varepsilon\sqrt{3}\); \(O_6\) moves \(O(\varepsilon)\); `links===12`.
- Dynamics \(c=0\): \(E\) drift ≲ 0.1% over ~300 steps.
- Dynamics \(c>0\): energy decays; \(\theta_2(t)\) plot is a damped oscillation toward rest.

---

## What this is not

- Not a DAE / Newton–Euler multiplier solver (constraint forces are not exposed).
- Not a 3-D mechanism, not spatial 4-bar.
- Not MuJoCo. Masses are planar rods.
- Cognates are overconstrained 1-DOF **because of Roberts**. A large \(\varepsilon\) is an “almost-Roberts” map: cognate rocker lengths can vary slightly; masses use instantaneous \(L\). If you need a strictly rigid non-Roberts 10-bar, freeze lengths at a reference pose and solve the extra loops — that is a new kinematics module.

---

## Prompt for the next agent

```
You are working in the Locus sandbox (Vite, src/lib/mechanism).
Read public/docs/AGENTS.md and src/lib/mechanism/AGENTS.md.
Math: public/docs/locus-model-and-dynamics.pdf

Task: <one sentence>

Keep: projective assembly, continuous sheet, circuit-limited sweep,
reduced EL on θ2, O6 ungrounded, split ε in Cayley + inertia.
Verify with typecheck and a small numeric script (O6, M, energy).
```
