<!-- RUBY-BACKUP · part 42 -->
========= */
function buildSpring(){
  try{
    if(typeof scene==='undefined') return;
    mats();
    var base=forwardPoint(7,0);
    var g=new T.Group();
    var gy=base.y, sy=gy+1.2;
    var k=6.0, c=1.4;
    /* anchor post */
    var post=new T.Mesh(new T.BoxGeometry(0.35,2.2,0.35),mats().red);
    post.position.set(-2.2,sy-0.2,0); g.add(post);
    /* rail */
    var rail=new T.Mesh(new T.BoxGeometry(5.4,0.12,0.5),mats().steel);
    rail.position.set(0.3,gy+0.32,0); g.add(rail);
    /* spring helix: built at unit length, scaled on X — coils stretch true */
    var coils=9, rad=0.3, tubeR=0.055, segs=coils*16;
    var pts=[];
    for(var i=0;i<=segs;i++){
      var t=i/segs, an=t*coils*Math.PI*2;
      pts.push(new T.Vector3(t,Math.cos(an)*rad,Math.sin(an)*rad));
    }
    var curve=new T.CatmullRomCurve3(pts);
    var springMesh=new T.Mesh(new T.TubeGeometry(curve,segs,tubeR,8,false),mats().gold);
    g.add(springMesh);
    var M=regMachine('spring',g);
    M.k=k; M.c=c; M.restLen=2.0; M.x=0; M.v=0; M.animating=false;
    M.ax=-1.9; /* anchor x */
    /* load block */
    M.load=new T.Mesh(new T.BoxGeometry(0.75,0.75,0.75),mats().crate);
    g.add(M.load);
    var lhit=hitSphere(0.7); M.load.add(lhit);
    var lbl=textSprite('SPRING — F = −kx',0.85); lbl.position.set(0.3,sy+1.35,0); g.add(lbl);
    var formula=textSprite('k = '+k.toFixed(1)+' · x = 0.00 · F = 0.00',0.65);
    formula.position.set(0.3,sy+0.82,0); g.add(formula); M.formula=formula;
    function layout(){
      var loadX=M.ax+M.restLen+M.x;
      M.load.position.set(loadX,gy+0.32+0.375,0);
      /* stretch the helix from anchor to load */
      var len=Math.max(0.2,M.restLen+M.x);
      springMesh.position.set(M.ax,sy,0);
      springMesh.scale.set(len,1,1);
      try{
        g.remove(M.formula);
        M.formula=textSprite('k = '+k.toFixed(1)+' · x = '+M.x.toFixed(2)+' · F = '+(k*Math.abs(M.x)).toFixed(2),0.62);
        M.formula.position.set(0.3,sy+0.82,0); g.add(M.formula);
      }catch(e){}
    }
    M.layout=layout;
    function physics(){
      /* semi-implicit Euler, substeps — stable and honest */
      var dt=1/120;
      for(var s=0;s<4;s++){
        var F=-M.k*M.x-M.c*M.v;
        M.v+=F*dt; M.x+=M.v*dt;
      }
      layout();
      if(Math.abs(M.x)>0.004||Math.abs(M.v)>0.004){
        requestAnimationFrame(physics);
      }else{ M.x=0; M.v=0; M.animating=false; layout(); }
    }
    /* drag the load along X */
    var hx0=0, hx00=0, dragging=false;
    regHit(lhit,{
      onDown:function(x,y){
        try{
          setNDC(x,y); raycaster.setFromCamera(ndc,camera);
          var plane=new T.Plane(new T.Vector3(0,0,1),0);
          /* horizontal plane at load height */
          plane=new T.Plane(new T.Vector3(0,1,0),-(gy+0.7));
          var pt=new T.Vector3();
          if(raycaster.ray.intersectPlane(plane,pt)){ hx0=pt.x; hx00=M.x; dragging=true; M.animating=false; }
        }catch(e){}
      },
      onMove:function(x,y){
        if(!dragging) return;
        try{
          setNDC(x,y); raycaster.setFromCamera(ndc,camera);
          var plane=new T.Plane(new T.Vector3(0,1,0),-(gy+0.7));
          var pt=new T.Vector3();
          if(!raycaster.ray.intersectPlane(plane,pt)){
            M.x=Math.max(-1.2,Math.min(2.5,hx00+(pt.x-hx0)));
            M.v=0; layout();
          }
        }catch(e){}
      },
      onUp:function(){
        if(dragging){ dragging=false; M.animating=true; physics();
          status('released — Hooke takes it home. F = −kx.'); }
      }
    });
    g.position.copy(base);
    scene.add(g);
    layout();
    riseIn(g,base.y,2.5,0.6);
    status('spring — drag the block to stretch it. let go. only the spring may stretch.');
  }catch(e){ status('spring failed: '+e.message); }
}

/* ============================================================================
   CODEX — the whole system on one gold panel.
   Every function, its antonym, its reason, its formula.
   `codex` opens it.
   ============================================================================ */
var CODEX=[
 ['grab','setdown','pick up and move blocks','MOVE = drag'],
 ['pulley','unpulley','a wheel to redirect your pull','REDIRECT 1:1'],
 ['rig','detach / unrig','rope it and pull it','PULL 1 = MOVE 1'],
 ['anchor','unanchor','pin it — pulls reshape it','PIN: pulls reshape'],
 ['lift','flatten','raise the ground itself','EARTH UP = RING UP'],
 ['diamond','square','pull corners — edges rigid, joints flex','EDGES RIGID, JOINTS FLEX'],
 ['compound','uncompound','chain sheaves for power','PULL n = LIFT 1'],
 ['counterweight','unweight','balance the load, a finger moves it','m1 = m2 → BALANCED'],
 ['winch','unwinch','crank the drum, wind the rope','ΔROPE = Δθ × r'],
 ['gearlink','ungear','turn the gear, the rope travels','ΔROPE = Δθ × r_axle'],
 ['lever','unlever','the long end moves the world','EFFORT × b = LOAD × a'],
 ['spring','unspring','the only elastic thing — Hooke','F = −kx'],
 ['delete','undo','rem