<!-- RUBY-BACKUP · part 79 -->
e){} }

function clearDeformDemo(){
  _risers=[]; _riseDone=null;
  if(demo&&demo.group&&demo.group.parent) demo.group.parent.remove(demo.group);
  demo=null; activeTouches={};
}
function resetDeformDemo(){
  if(!demo){ status('no rope lab — type ropedeform first'); return; }
  demo.anchored.reset(); demo.free.reset();
  /* free frame back to spawn spot */
  if(demo.freeHome) demo.free.group.position.copy(demo.freeHome);
  status('square again — ropes slack, joints straight');
}

/* ---------------- rise-in: nothing teleports ---------------- */
/* pieces ARRIVE from below ground, staggered. linear rise, honest physics,
   no fading tricks. stake, then frames, then title. */
var _risers=[], _riseDone=null;
function risePieces(pieces, onDone){
  var now=performance.now()/1000, depth=2.5, dur=0.55;
  pieces.forEach(function(pc){
    if(!pc.obj) return;
    var toY=pc.obj.position.y;
    pc.obj.position.y=toY-depth;
    _risers.push({o:pc.obj, fromY:toY-depth, toY:toY,
      t0:now+(pc.delay||0), dur:dur, done:false});
  });
  _riseDone=onDone||null;
  requestAnimationFrame(_riseStep);
}
function _riseStep(){
  if(!_risers.length){ if(_riseDone){ var f=_riseDone; _riseDone=null; f(); } return; }
  var now=performance.now()/1000, allDone=true;
  for(var i=0;i<_risers.length;i++){
    var r=_risers[i];
    if(r.done) continue;
    var t=(now-r.t0)/r.dur;
    if(t<0){ allDone=false; continue; }
    if(t>=1){ r.o.position.y=r.toY; r.done=true; continue; }
    allDone=false;
    r.o.position.y=r.fromY+(r.toY-r.fromY)*t;
  }
  if(!allDone) requestAnimationFrame(_riseStep);
  else{ _risers=[]; if(_riseDone){ var f2=_riseDone; _riseDone=null; f2(); } }
}

function spawnDeformDemo(){
  try{
    clearDeformDemo();
    mats();
    var L=1.7;
    /* where: ahead of the player */
    var px=(typeof mePos!=='undefined')?mePos.x:0;
    var pz=(typeof mePos!=='undefined')?mePos.z:0;
    var cam=(typeof camera!=='undefined')?camera:null;
    var fwd=new T.Vector3(0,0,1);
    if(cam){ cam.getWorldDirection(fwd); fwd.y=0; if(fwd.lengthSq()<1e-6) fwd.set(0,0,1); fwd.normalize(); }
    var perp=new T.Vector3(-fwd.z,0,fwd.x);
    var cxW=px*0.13+fwd.x*2.6, czW=pz*0.13+fwd.z*2.6;
    var gy=groundYAt(cxW/0.13, czW/0.13);
    var group=new T.Group();

    /* ---- ANCHORED station (left) ---- */
    var aFrame=new DeformFrame(L,true,'ANCHORED — pull rings, SHAPE changes');
    var ax=cxW+perp.x*1.9, az=czW+perp.z*1.9;
    var agy=groundYAt(ax/0.13,az/0.13);
    var aCenterY=agy+L/2+1.15;
    aFrame.group.position.set(ax,aCenterY,az);
    aFrame.group.rotation.y=Math.atan2(px*0.13-ax, pz*0.13-az);
    group.add(aFrame.group);
    /* stake: post from ground to frame bottom, ring at frame center */
    var stake=buildAnchorStake();
    var postLen=(aCenterY-L/2)-agy+0.4;
    stake.userData.post.scale.y=Math.max(0.3,postLen);
    stake.userData.post.position.set(ax,agy+postLen/2-0.2,az);
    stake.userData.tip.position.set(ax,agy-0.15,az);
    stake.userData.ring.position.set(ax,aCenterY,az);
    /* chain: thick short line post-top -> frame center */
    var chainGeo=new T.BufferGeometry().setFromPoints(
      [new T.Vector3(ax,agy+postLen-0.2,az), new T.Vector3(ax,aCenterY,az)]);
    var chain=new T.Line(chainGeo,new T.LineBasicMaterial({color:0xffd700}));
    stake.add(chain);
    group.add(stake);

    /* ---- FREE station (right) ---- */
    var fFrame=new DeformFrame(L,false,'FREE — pull the ring, the BLOCK moves');
    var fx=cxW-perp.x*1.9, fz=czW-perp.z*1.9;
    var fgy=groundYAt(fx/0.13,fz/0.13);
    var fCenterY=fgy+L/2+1.15;
    fFrame.group.position.set(fx,fCenterY,fz);
    fFrame.group.rotation.y=Math.atan2(px*0.13-fx, pz*0.13-fz);
    group.add(fFrame.group);
    var freeHome=fFrame.group.position.clone();

    /* title */
    var title=textSprite('ROPE vs BLOCK',1.1);
    if(title){ title.position.set(cxW,Math.max(aCenterY,fCenterY)+L/2+1.3,czW); group.add(title); }

    if(typeof scene!=='undefined') scene.add(group);
    demo={group:group, anchored:aFrame, free:fFrame, freeHome:freeHome, entering:true};
    /* RISE-IN: stake first, then anchored frame, free frame, title.
       each rises from below ground — the lab gets BUILT, not blinked in. */
    risePieces([
      {obj:stake, delay:0},
      {obj:aFrame.group, delay:0.35},
      {obj:fFrame.group, delay:0.7},
      {obj:title, delay:1.05}
    ], function(){ if(demo) demo.entering=false; });
    status('rope lab rising — watch it build...');
  }catch(e){ status('rope lab failed: '+e.message); }
}

/* ---------------- touch: grab the gold rings ---------------- */
function touchNDC(t){
  ndc.x=(t.clientX/window.innerWidth)*2-1;
  ndc.y=-(t.clientY/window.innerHeight)*2+1;
}
function framePlaneWorld(frame){
  var n=new T.Vector3(0,0,1).applyQuaternion(frame.group.getWorldQuaternion(new T.Quaternion()));
  var c=frame.group.getWorldPosition(new T.Vector3());
  planeTmp.setFromNormalAndCoplanarPoint(n,c);
  return planeTmp;
}
function pla