# eCVT — engineering model specification

The eCVT 3D model engine (issues #1187/#1195). This file is the single source of truth
for the geometry and kinematics. `gear-math.mjs` implements it; `verify.mjs` proves it;
`index.html` renders it. Nothing in the implementation may contradict this spec —
change the spec first.

All angles are radians CCW about +z (right-hand rule) unless a degree sign is written.
All lengths are **millimetres**; the scene applies one global scale at the root.

## 1. Architecture (unchanged from v1)

- **Ring gear = crank input.** Crank spindle, crank arms, ring gear and torque-motor
  drum are one rigid body (`inputBody`, angle θr).
- **Sun gear = control input.** Sun gear + speed-motor rotor are one rigid body
  (`sunBody`, angle θs).
- **Carrier = output.** Carrier cage, planet pins, output hub and chainring are one
  rigid body (`carrierBody`, angle θc). Each planet is a separate rigid body on its pin.
- **Torque motor** stator is fixed to the housing; its pinion (`pinionBody`, angle θq)
  meshes with the drum on the input body.

## 2. Gear macro-geometry

| symbol | value | meaning |
|---|---|---|
| α | 20° | pressure angle, all meshes |
| m | 1.25 mm | module, all meshes |
| Zs | 27 | sun teeth |
| Zp | 18 | planet teeth (×3) |
| Zr | 63 | ring internal teeth |
| Zd | 48 | drum external teeth (on input body) |
| Zq | 18 | torque-motor pinion teeth |
| N | 3 | planet count |
| b | 10 mm | gear face width |
| δ | 0.04·m | tooth thinning per gear (backlash allowance) |

Why these counts (v1 used 42/18/12):

- **Same teaching arithmetic**: Zr/(Zs+Zr) = 63/90 = **0.70** and Zs/(Zs+Zr) = 27/90 =
  **0.30** — identical Willis weights to v1's 42/60 and 18/60.
- **No undercut**: minimum external tooth count at α=20°, x=0 is 2/sin²α ≈ 17.1.
  v1's 12-tooth planet is undercut (physically invalid at standard proportions);
  Zp=18, Zs=27, Zq=18 all clear it. Zd=48 trivially clears it.
- **Geometric closure**: Zr = Zs + 2·Zp (27 + 36 = 63) so the standard center distance
  a = m(Zs+Zp)/2 = **28.125 mm** is simultaneously exact for sun–planet and planet–ring.
- **Assembly with equally spaced planets**: (Zs+Zr)/N = 90/3 = 30 ∈ ℤ.

Derived radii (all in mm; r = pitch, rb = base, ra = tip, rf = root):

| gear | r = mZ/2 | rb = r·cosα | ra | rf |
|---|---|---|---|---|
| sun (ext) | 16.875 | 15.857 | r + m = 18.125 | r − 1.25m = 15.3125 |
| planet (ext) | 11.25 | 10.572 | 12.50 | 9.6875 |
| ring (int) | 39.375 | 37.000 | r − **0.85m** = 38.3125 | r + 1.25m = 40.9375 |
| drum (ext) | 30.0 | 28.191 | 31.25 | 28.4375 |
| pinion (ext) | 11.25 | 10.572 | 12.50 | 9.6875 |

**The ring addendum is stubbed to 0.85·m.** At the full 1.0·m depth the ring tip's
contact point falls 0.43 mm beyond the planet's base-tangent point on the line of
action — theoretical tip-root interference (masked at runtime only by backlash).
0.85·m gives a +0.32 mm tangent margin and εα = 1.72. Note ring tip (38.3125) >
ring base (37.000): the internal flank is fully involute. External gears with
Z ≤ 41 (threshold 2.5/(1−cos α) = 41.45) have rf < rb; the sub-base flank is a
radial segment plus root fillet (see §3).

Drum–pinion center distance: a_dq = m(Zd+Zq)/2 = **41.25 mm**, pinion center at
station angle **90°** (straight up) from the main axis, fixed to the housing.

## 3. Tooth profile construction

Involute of the base circle, parameterised by roll angle t ≥ 0:
`P(t) = rb·(cos t + t·sin t, sin t − t·cos t)` — radius at t is `R(t) = rb·√(1+t²)`,
so `t(R) = √((R/rb)² − 1)`. Polar angle of the involute point measured from its
base-circle start is `inv(θR) = tan θR − θR` where `cos θR = rb/R`.

**External gear** with tooth thinned by δ: circular tooth thickness at pitch is
`s = πm/2 − δ`. Half-thickness *angle* of the tooth at radius R (measured from the
tooth centerline):

```
ψ_ext(R) = s/(2r) + inv(α) − inv(θR),   cos θR = rb/R
```

Flank runs from max(rf, rb) to ra. Where rf < rb, extend with a radial segment from
the involute start down to the root fillet. Root fillet radius ρf = 0.25·m, built by
**exact tangency**: the fillet-arc center is solved numerically so the arc is tangent to
both the true flank curve and the root circle, and the flank is cut at the tangent point
(a radial-line approximation of the flank is NOT acceptable — it leaves a 0.5mm phantom
flap on the ring, found the hard way). The fillet must never be touched by the mating
tip — proved in §6.
Tip corners are left sharp. Every polygon vertex lies exactly on the analytic boundary;
polygon EDGES may deviate only by chord sagitta on the fillet arcs, which is sampled
uniformly around the fillet center (≈3 µm at 8 segments) and bounded at 30 µm by a §6
edge-midpoint check, with a 0.1% polygon-vs-analytic area cross-check against
material-losing regressions.

**Internal (ring) gear**: teeth point inward; tip circle is *smaller* than pitch
(ra = r − m), root *larger* (rf = r + 1.25m). Flanks are involutes of the same base
circle. With the ring's tooth thinned by δ (space widened), the half-thickness angle:

```
ψ_int(R) = s/(2r) − inv(α) + inv(θR),   s = πm/2 − δ
```

(sign flip vs. external: an internal tooth gets *thinner* as R decreases toward its tip.)

**Tooth-center convention:** every gear's local profile places a **tooth center on the
local +x axis** (angle 0). This includes the ring. All phase formulas in §5 assume it.

Profiles are sampled densely (≥ 16 points per involute flank) into a closed 2D polygon
per gear, used both for extrusion (page) and collision proof (harness).

## 4. Kinematics (algebraic, drift-free)

Only the two inputs are ever integrated:

```
θr += ωr·dt        (crank/ring, from crank rpm slider)
θs += ωs·dt        (sun, from sun rpm slider)
```

Everything else is derived **algebraically every frame** — no incremental state:

```
θc  = (Zs·θs + Zr·θr) / (Zs + Zr)                    (Willis)
θp_j = θc − (Zs/Zp)·(θs − θc) + φp_j                  (absolute planet spin)
θq  = −(Zd/Zq)·θr + φq                                (pinion, external mesh sign)
```

Planet j is a child of `carrierBody` at fixed local position
`a·(cos ψ0_j, sin ψ0_j)`; its local rotation is `θp_j − θc`. Its world station angle
is ψ_j = ψ0_j + θc automatically.

Consequences (must hold exactly, checked in §6):
- Direct drive θs = θr = θ ⇒ θc = θ and θp_j − θc = φp_j (planets do not spin on pins).
- Neutral Zs·θs + Zr·θr = 0 ⇒ θc = 0.
- ωc = 0.70·ωr + 0.30·ωs.

## 5. Mesh phasing

Notation: `frac(x) = x − floor(x) ∈ [0, 1)` (floor-based — valid for negative
arguments, which occur whenever a slider goes negative). Invariant tests must
compare `|frac(M) − ½|`, which maps both the +½ and −½ representations to zero.

For a mesh contact along direction ψ, define the *pitch phase* of each gear
as its fractional tooth pitch offset at the contact direction:

```
u_s  = frac( Zs·(ψ − θs) / 2π )         sun, contact direction ψ
u_p1 = frac( Zp·(ψ + π − θp) / 2π )     planet, sun-side contact (direction ψ+π from planet)
u_p2 = frac( Zp·(ψ − θp) / 2π )         planet, ring-side contact (direction ψ)
u_r  = frac( Zr·(ψ − θr) / 2π )         ring, contact direction ψ
```

u = 0 means a tooth center points exactly along the contact direction; u = ½ means a
space center does.

**Invariants** (differentiate using Willis with ψ = ψ0 + θc; both are constants of the
motion, which is why phasing is set once at assembly and can never drift):

```
M1 = u_s + u_p1  ≡ ½  (mod 1)     sun tooth ↔ planet space   (external mesh)
M2 = u_p2 − u_r  ≡ ½  (mod 1)     planet tooth ↔ ring space  (internal mesh)
```

**Station quantisation.** Since u_p1 − u_p2 ≡ Zp/2 ≡ 0 (mod 1) for even Zp, M1 and M2
together force `frac((Zs+Zr)·ψ0_j / 2π) = 0`: planet stations must sit on multiples of
2π/(Zs+Zr) = **4°**. Equal 120° spacing is compatible (90/3 = 30 stations apart).
v1's 90° base station is *not* on the 4° grid for these tooth counts; we use:

```
ψ0_j ∈ { 92°, 212°, 332° }
```

**Assembly phases** (with all profiles tooth-centered per §3, and θs=θr=0 at t=0):

```
u_s(j)  = frac( Zs·ψ0_j / 2π )              = 0.9  for all three stations
φp_j    = ψ0_j + π + (2π/Zp)·(u_s(j) − ½)   = ψ0_j + 188°
φq      = 3π/2 − (2π/Zq)·(½ − frac(Zd/4))   = 260°   (station 90°, Zd·(90°)/360° = 12 ∈ ℤ)
```

M2 for the ring then holds automatically because frac((Zs+Zr)·ψ0_j/2π) = 0 — this is
exactly the classic planetary assembly condition, recovered from the invariants.

## 6. Verification requirements (`verify.mjs` — must exit 0)

Structural:
1. Zr = Zs + 2Zp; (Zs+Zr) mod N = 0; every ψ0_j on the 4° grid; 120° apart.
2. No undercut: Z ≥ 2/sin²α for all external gears (Zq, Zp, Zs, Zd).
3. Tip-tooth thickness s_a = 2·ra·ψ_ext(ra) ≥ 0.2·m for every external gear
   (teeth must not run to a point); ring tip thickness similarly ≥ 0.2·m via ψ_int.
4. Radial clearances: adjacent planet tips
   (2a·sin(π/N) > 2·ra_planet + 1 mm); planet tip vs sun root and ring root
   (standard 0.25m bottom clearance); ring tip vs planet root ((1.25 −
   intAddendum)·m); pinion tip vs drum root clearance.
5. Involute-interference (analytic): margins measured **along the line of action
   against the base-tangent points** (t-coordinates), never as contact radius vs
   rb — radius is symmetric about the tangent point and cannot see a crossing.
   Contact must stay inside both members' tangent points on every mesh.
6. Contact ratio ε_α computed for all three mesh types; require ε_α ≥ 1.2.
6b. Solid-model envelopes: every part's annulus band × z-range (from LAYOUT in
   gear-math.mjs, the same table the renderer builds from) — shaft-in-bore
   clearances ≥ 0.2 mm at every rotating interface; planets clear the spider-arm
   openings angularly; and a 41-step explode sweep proving no two
   differently-moving visible parts ever overlap in both z and radius
   (meshing gear pairs excepted — they are proved in 2D; housing/stators are
   hidden above the fade fraction and only checked below it).
6c. Wrap invariance: shifting θs and/or θr by ±2π·180 leaves every derived body
   angle unchanged mod 2π (long-runtime float-precision guard; the page wraps
   with exactly this modulus and MUST NOT wrap by bare 2π, which shifts the
   carrier by 108° and the planets by 270°).

Kinematic:
7. Willis identity, direct-drive lock (planet relative spin ≡ 0), neutral, and
   pinion ratio — exact to 1e-12 over random inputs.
8. Phase invariants M1, M2 ≡ ½ (mod 1) to 1e-9 for all planets over ≥ 100 random
   (θs, θr) poses. Same for the drum–pinion mesh invariant.

Geometric collision proof (the "no interpenetration" guarantee):
9. Material sense: external gear material is {r ≤ boundary(φ)}; the RING's
   material is {r ≥ boundary(φ)} (the sampled curve is its inner boundary and
   the gear body is the annulus outside it, up to the rim) — containment
   predicates must respect this, not treat the ring curve as an outer boundary.
   For each mesh pair (sun×planet_j all j, planet_j×ring all j, drum×pinion),
   sweep the 2-DOF input space: θr fixed at 5 values over a drum pitch × θs
   swept over ≥ 2 sun–planet mesh cycles (and a pure-θr sweep for drum×pinion);
   at each pose, sample both boundaries densely in the mesh window **facing the
   actual contact side** (for the internal mesh both windows face away from the
   ring center) and assert no sampled boundary point of either gear lies inside
   the other's material.
10. Report minimum Euclidean clearance at representative poses (expected ≈
    backlash-level, δ·cosα ≈ 0.047 mm, strictly > 0), per mesh pair.

## 7. Rendering / solid-body rules

- One `THREE.Group` per rigid body; exactly one writer to each body's rotation per
  frame; bodies parented as §1 (planets under carrier). No mesh may belong to two bodies.
- Gears are prism extrusions of the §3 polygons (no bevel on teeth — v1 note: bevel
  creates a barber-pole illusion). Face width b, gear z-planes:
  sun/planets/ring share one plane; drum and pinion share a second plane behind it;
  chainring in front. Axial stack must have ≥ 1 mm clearance between neighbouring planes.
- Index marks and painted arrows are children of their body (they rotate with it).
- Housing: translucent static shell + bearings; **cutaway toggle** via
  `renderer.localClippingEnabled` clipping plane at y = 0 on housing+motor materials only.
- Scene scale: 1 three.js unit = 12 mm, applied on the root group only.
- Camera, lights, orbit controls, white background — same idiom as v1.
- All part envelopes (z-stations, radii, bores) come from the normative LAYOUT
  table in gear-math.mjs — the renderer and the §6b envelope checks consume the
  same values, so the drawing and the proof cannot drift apart.
- **Explode** (LAYOUT.explode): z-only displacement per sub-assembly, vectors
  proved crossing-free by §6b. Planets translate with the cage plus their own
  local z offset, passing through the open spider sectors (arms are 60° from
  stations). The right crank arm detaches forward (+z) — dragging it back with
  the spindle would sweep it through the speed motor and chainring. Housing and
  motor stators hide above `fadeAt`.

## 8. Page scope ("just the 3D model")

No article, no tour. Single full-height stage with:
- crank rpm slider (−120…150, default 80), sun rpm slider (−300…300, default 80),
- presets: Low (−60), Direct (=crank), High (+220), Neutral (−(Zr/Zs)·crank),
- play/pause, explode slider, housing + cutaway toggles,
- live readout: crank/sun/chainring rpm + ratio, planet spin rpm, and the
  0.70/0.30 equation line,
- **runtime self-audit badge** (grafted from the v3 experiment, #1193): after
  first paint the page runs `selfAudit()` — an in-page subset of this spec's §6
  suite (structural config, kinematic identities, phase invariants, wrap
  invariance, analytic collision probe) built on the SAME collisionKit code the
  Node harness uses — and displays pass/fail. verify.mjs asserts audit parity.
Footer links back to /pages/ecvt (v1) as "the explainer".
