<!-- RUBY-BACKUP · part 78 -->
*len];
};
DeformFrame.prototype.local3=function(p2){
  return new T.Vector3(p2[0],p2[1],0);
};
/* clamp a driven corner: reachable + linkage limits vs opposite corner */
DeformFrame.prototype.clampCorner=function(k){
  var L=this.L, P=this.pos, o=OPP[k];
  var t=P[k], r=this.rest[k];
  /* stay near home: max reach 1.35*L from rest */
  var dx=t[0]-r[0], dy=t[1]-r[1], d=Math.sqrt(dx*dx+dy*dy), maxR=1.35*L;
  if(d>maxR){ t=[r[0]+dx/d*maxR, r[1]+dy/d*maxR]; }
  /* linkage: |XY| within [0.35, 2L-0.02] */
  var Y=P[o];
  var dxy=vdist(t,Y), maxD=2*L-0.02, minD=0.35;
  if(dxy>maxD){ var s=maxD/dxy; t=[Y[0]+(t[0]-Y[0])*s, Y[1]+(t[1]-Y[1])*s]; }
  else if(dxy<minD && dxy>1e-9){ var s2=minD/dxy; t=[Y[0]+(t[0]-Y[0])*s2, Y[1]+(t[1]-Y[1])*s2]; }
  P[k]=t;
};
/* solve the linkage: driven corners pinned, adjacents on rigid circles */
DeformFrame.prototype.solve=function(){
  var L=this.L, P=this.pos, D=this.driven;
  var keys=Object.keys(D).filter(function(k){return D[k];});
  if(!keys.length) return;
  var self=this;
  keys.forEach(function(k){ self.clampCorner(k); });
  /* solve each driven corner's opposite pair */
  var done={};
  keys.forEach(function(k){
    var o=OPP[k];
    var tag=k<o ? k+o : o+k;
    if(done[tag]) return; done[tag]=true;
    /* clamp the pair distance (both driven case) */
    var d=vdist(P[k],P[o]), maxD=2*L-0.02;
    if(d>maxD){
      /* pull both back symmetrically toward their midpoint */
      var mx=(P[k][0]+P[o][0])/2, my=(P[k][1]+P[o][1])/2;
      var ux=(P[k][0]-mx)/d, uy=(P[k][1]-my)/d;
      P[k]=[mx+ux*maxD/2, my+uy*maxD/2];
      P[o]=[mx-ux*maxD/2, my-uy*maxD/2];
    }
    var adj=ADJ[k];
    var nB=circleInt(P[k],L,P[o],L,P[adj[0]]);
    var nD=circleInt(P[k],L,P[o],L,P[adj[1]]);
    /* keep branches apart: assign each solved point to nearest previous */
    if(nB&&nD){
      var dBB=vdist(nB,P[adj[0]])+vdist(nD,P[adj[1]]);
      var dBX=vdist(nB,P[adj[1]])+vdist(nD,P[adj[0]]);
      if(dBX<dBB){ var tmp=nB; nB=nD; nD=tmp; }
      P[adj[0]]=nB; P[adj[1]]=nD;
    }
  });
  this.checkDiamond();
};
DeformFrame.prototype.checkDiamond=function(){
  var P=this.pos;
  var longD=vdist(P.A,P.C), shortD=vdist(P.B,P.D);
  var ratio=longD/Math.max(0.001,shortD);
  /* also accept the other diagonal being the long one */
  var r2=vdist(P.B,P.D)/Math.max(0.001,longD);
  var isDiamond=ratio>1.35||r2>1.35;
  if(isDiamond&&!this.diamondTold){
    this.diamondTold=true;
    try{ codeStatus('diamond — edges rigid, joints flexed. that is a block, not a rope.'); }catch(e){}
  }
  if(!isDiamond) this.diamondTold=false;
};
DeformFrame.prototype.reset=function(){
  var h=this.L/2, self=this;
  this.pos={A:[h,h],B:[-h,h],C:[-h,-h],D:[h,-h]};
  this.driven={}; this.grabs={}; this.diamondTold=false;
  this.corners.forEach(function(k){ self.ropeVec[k]=self.restOut(k,0.62); });
  this.updateMeshes();
};
/* re-aim every mesh from 2D state */
var _up=new T.Vector3(0,1,0), _dir=new T.Vector3(), _mid=new T.Vector3(), _p1=new T.Vector3(), _p2=new T.Vector3();
function aimCyl(mesh,a,b){
  _p1.set(a.x,a.y,a.z); _p2.set(b.x,b.y,b.z);
  _dir.subVectors(_p2,_p1);
  var len=_dir.length()||0.0001;
  _mid.addVectors(_p1,_p2).multiplyScalar(0.5);
  mesh.position.copy(_mid);
  mesh.quaternion.setFromUnitVectors(_up,_dir.normalize());
  mesh.scale.set(1,len,1);
}
DeformFrame.prototype.updateMeshes=function(){
  var self=this;
  var P3={};
  this.corners.forEach(function(k){ P3[k]=self.local3(self.pos[k]); });
  /* edges */
  [['A','B'],['B','C'],['C','D'],['D','A']].forEach(function(pr){
    aimCyl(self.edgeMesh[pr[0]+pr[1]], P3[pr[0]], P3[pr[1]]);
  });
  /* joints */
  this.corners.forEach(function(k){ self.jointMesh[k].position.copy(P3[k]); });
  /* handles + ropes */
  this.corners.forEach(function(k){
    var hp=new T.Vector3(P3[k].x+self.ropeVec[k][0], P3[k].y+self.ropeVec[k][1], 0);
    self.handleMesh[k].position.copy(hp);
    self.handleMesh[k].lookAt(self.group.localToWorld(hp.clone()));
    aimCyl(self.ropeMesh[k], P3[k], hp);
  });
};

/* ---------------- anchor stake visual ---------------- */
function buildAnchorStake(){
  var g=new T.Group();
  var post=new T.Mesh(new T.CylinderGeometry(0.09,0.12,1,10),STEEL_MAT);
  g.add(post); g.userData.post=post;
  var tip=new T.Mesh(new T.ConeGeometry(0.14,0.5,10),STEEL_MAT);
  tip.rotation.x=Math.PI; g.add(tip); g.userData.tip=tip;
  var ring=new T.Mesh(new T.TorusGeometry(0.2,0.05,10,20),GOLD_MAT);
  g.add(ring); g.userData.ring=ring;
  return g;
}

/* ---------------- demo state ---------------- */
var demo=null; /* {group, anchored:DeformFrame, free:DeformFrame, stake, title} */
var raycaster=new T.Raycaster();
var ndc=new T.Vector2();
var planeTmp=new T.Plane();
var activeTouches={}; /* touchId -> {frame, corner, ropeVecAtGrab:[x,y], lastLocal:[x,y]} */

function groundYAt(rx,rz){
  try{ if(typeof fortifiedGroundY==='function') return fortifiedGroundY(rx,rz); }catch(e){}
  return 0.3;
}
function status(m){ try{ codeStatus(m); }catch(