<!-- RUBY-BACKUP · part 77 -->
--------------------------------------------------------------
   Codes:  ropedeform        spawn the demo pair near you
           ropedeformreset    square again, ropes released
           ropedeformclear    remove the demo
   Touch:  grab a gold ring, pull. Two fingers = opposite corners.
   ============================================================================ */
(function(){
'use strict';
if(typeof window==='undefined') return;
var T = window.THREE;
if(!T) return;

/* ---------------- 2D linkage math (exact, closed-form) ---------------- */
function vdist(a,b){ var dx=a[0]-b[0], dy=a[1]-b[1]; return Math.sqrt(dx*dx+dy*dy); }
/* circle-circle intersection; branch chosen by continuity with prev */
function circleInt(p0,r0,p1,r1,prev){
  var dx=p1[0]-p0[0], dy=p1[1]-p0[1], d=Math.sqrt(dx*dx+dy*dy);
  var maxD=r0+r1-1e-6;
  if(d>maxD || d<Math.abs(r0-r1) || d<1e-9) return null;
  var a=(r0*r0-r1*r1+d*d)/(2*d);
  var h=Math.sqrt(Math.max(0,r0*r0-a*a));
  var mx=p0[0]+a*dx/d, my=p0[1]+a*dy/d;
  var ox=-h*dy/d, oy=h*dx/d;
  var c1=[mx+ox,my+oy], c2=[mx-ox,my-oy];
  if(!prev) return c1;
  var d1=(c1[0]-prev[0])*(c1[0]-prev[0])+(c1[1]-prev[1])*(c1[1]-prev[1]);
  var d2=(c2[0]-prev[0])*(c2[0]-prev[0])+(c2[1]-prev[1])*(c2[1]-prev[1]);
  return d1<=d2 ? c1 : c2;
}
var OPP={A:'C',C:'A',B:'D',D:'B'};
var ADJ={A:['B','D'],B:['A','C'],C:['B','D'],D:['A','C']};
/* order around the loop: A -> B -> C -> D -> A (A top-right, B top-left, C bottom-left, D bottom-right) */

/* ---------------- text sprites ---------------- */
function textSprite(text, scale){
  try{
    var c=document.createElement('canvas'); c.width=512; c.height=96;
    var g=c.getContext('2d');
    g.fillStyle='rgba(5,3,1,0.82)'; g.fillRect(0,0,512,96);
    g.strokeStyle='#d8a920'; g.lineWidth=3; g.strokeRect(3,3,506,90);
    g.fillStyle='#ffd700'; g.font='bold 34px Georgia,serif';
    g.textAlign='center'; g.textBaseline='middle';
    g.fillText(text,256,50);
    var tex=new T.CanvasTexture(c);
    var m=new T.SpriteMaterial({map:tex, transparent:true, depthTest:false});
    var s=new T.Sprite(m);
    var sc=scale||1; s.scale.set(3.4*sc,0.64*sc,1);
    return s;
  }catch(e){ return null; }
}

/* ---------------- the deformable frame ---------------- */
var EDGE_MAT=null, JOINT_MAT=null, HANDLE_MAT=null, ROPE_MAT=null, STEEL_MAT=null, GOLD_MAT=null;
function mats(){
  if(EDGE_MAT) return;
  EDGE_MAT=new T.MeshStandardMaterial({color:0xb8860b, metalness:0.85, roughness:0.3});
  JOINT_MAT=new T.MeshStandardMaterial({color:0x8a0f0f, metalness:0.4, roughness:0.5});
  HANDLE_MAT=new T.MeshStandardMaterial({color:0xffd700, metalness:0.9, roughness:0.25, emissive:0x664d00, emissiveIntensity:0.7});
  ROPE_MAT=new T.MeshStandardMaterial({color:0xd8a920, metalness:0.6, roughness:0.4});
  STEEL_MAT=new T.MeshStandardMaterial({color:0x555560, metalness:0.8, roughness:0.35});
  GOLD_MAT=new T.MeshStandardMaterial({color:0xffd700, metalness:0.95, roughness:0.2});
}

function DeformFrame(L, anchored, labelText){
  mats();
  this.L=L; this.anchored=!!anchored;
  var h=L/2;
  this.rest={A:[h,h],B:[-h,h],C:[-h,-h],D:[h,-h]};
  this.pos={A:[h,h],B:[-h,h],C:[-h,-h],D:[h,-h]};
  this.driven={};          /* corner -> true while grabbed */
  this.grabs={};           /* touchId -> corner */
  this.diamondTold=false;
  this.group=new T.Group();
  this.group.userData.deformFrame=this; /* rigging hook */
  this.corners=['A','B','C','D'];
  var self=this;
  /* edges: unit cylinders, re-aimed every update */
  this.edgeMesh={};
  var pairs=[['A','B'],['B','C'],['C','D'],['D','A']];
  pairs.forEach(function(pr){
    var geo=new T.CylinderGeometry(0.055,0.055,1,10);
    var m=new T.Mesh(geo,EDGE_MAT);
    self.group.add(m);
    self.edgeMesh[pr[0]+pr[1]]=m;
  });
  /* joints */
  this.jointMesh={};
  this.corners.forEach(function(k){
    var m=new T.Mesh(new T.SphereGeometry(0.095,14,12),JOINT_MAT);
    self.group.add(m); self.jointMesh[k]=m;
  });
  /* handles: gold ring + fat invisible hit sphere; rope cylinder corner->handle */
  this.handleMesh={}; this.hitMesh={}; this.ropeMesh={}; this.ropeVec={};
  this.corners.forEach(function(k){
    var hg=new T.Group();
    var ring=new T.Mesh(new T.TorusGeometry(0.15,0.045,10,20),HANDLE_MAT);
    hg.add(ring);
    var hit=new T.Mesh(new T.SphereGeometry(0.34,8,6),
      new T.MeshBasicMaterial({visible:false}));
    hit.userData.deformHandle={frame:self, corner:k};
    hg.add(hit);
    self.group.add(hg);
    self.handleMesh[k]=hg; self.hitMesh[k]=hit;
    var rope=new T.Mesh(new T.CylinderGeometry(0.022,0.022,1,8),ROPE_MAT);
    self.group.add(rope); self.ropeMesh[k]=rope;
    self.ropeVec[k]=self.restOut(k, 0.62);
  });
  /* label */
  var lab=textSprite(labelText, 0.85);
  if(lab){ lab.position.set(0, h+0.85, 0); this.group.add(lab); }
  this.updateMeshes();
}
DeformFrame.prototype.restOut=function(k,len){
  var p=this.rest[k];
  var d=Math.sqrt(p[0]*p[0]+p[1]*p[1])||1;
  return [p[0]/d*len, p[1]/d