<!-- RUBY-BACKUP · part 44 -->
ctObjects(scene.children,true);
    for(var i=0;i<hits.length;i++){
      var p=hits[i].object, bad=false, firstMesh=null;
      while(p && !p.isScene){
        var ud=p.userData||{};
        if(ud.rigPart||ud.grandPart||ud.rigPulley||ud.deformHandle||ud.deformFrame||ud.liftStake){ bad=true; break; }
        if(!firstMesh && p.isMesh) firstMesh=p;
        p=p.parent;
      }
      if(bad||!firstMesh) continue;
      var mt=firstMesh.material;
      if(mt&&mt.transparent&&mt.opacity<0.6) continue;
      var gg=firstMesh.geometry; if(!gg) continue;
      try{ if(!gg.boundingSphere) gg.computeBoundingSphere(); }catch(e){ continue; }
      if(!gg.boundingSphere) continue;
      var sc=new T.Vector3(); firstMesh.getWorldScale(sc);
      var r=gg.boundingSphere.radius*Math.max(sc.x,sc.y,sc.z);
      if(r<0.4||r>90) continue;
      return {obj:firstMesh, point:hits[i].point.clone()};
    }
  }catch(e){}
  return null;
}
function spanTap(x,y){
  try{
    var t=pickRoad(x,y);
    if(!t){
      if(hitSysPart(x,y)){ status('that is a machine part — tap a plain object, or empty ground to grow a twin'); return true; }
      if(!spanA){ status('tap an object first — then tap empty ground to grow its twin'); return true; }
      var gp=pickGroundPoint(x,y);
      if(!gp){ status('no ground there'); return true; }
      spanCreateTwin(spanA,gp);
      spanA=null; spanArmed=false;
      return true;
    }
    if(!spanA){ spanA=t; status('A set — now tap another object, or tap empty ground to grow a twin'); return true; }
    if(t.obj===spanA.obj){ status('tap a DIFFERENT object for B'); return true; }
    buildSpan(spanA,t);
    spanA=null; spanArmed=false;
    return true;
  }catch(e){ return false; }
}
function spanCreateTwin(A,pointB){
  try{
    mats();
    if(typeof scene==='undefined') return;
    var src=A.obj;
    var twin=src.clone(true);
    (function clean(o){
      try{ var ud=o.userData||{};
        delete ud.rigPart; delete ud.grandPart; delete ud.deformHandle;
        delete ud.rigPulley; delete ud.pulleyRef; delete ud.liftStake;
        delete ud.deformFrame; delete ud.stake; }catch(e){}
      for(var i=0;i<o.children.length;i++) clean(o.children[i]);
    })(twin);
    scene.add(twin);
    twin.updateMatrixWorld(true);
    var bb=new T.Box3().setFromObject(twin);
    if(bb.isEmpty()){ try{ scene.remove(twin); }catch(e){} status('that would not clone'); return; }
    var cx=(bb.max.x+bb.min.x)/2, cy=(bb.max.y+bb.min.y)/2, cz=(bb.max.z+bb.min.z)/2;
    var halfH=Math.max(0.15,(bb.max.y-bb.min.y)/2);
    var off=new T.Vector3(cx-twin.position.x, cy-twin.position.y, cz-twin.position.z);
    var startPos=A.point.clone().sub(off);
    var endPos=new T.Vector3(pointB.x-off.x, pointB.y+halfH-off.y, pointB.z-off.z);
    twin.position.copy(startPos);
    var travelRope=ropeSeg(); scene.add(travelRope);
    try{ travelRope.userData.grandPart=true; }catch(e){}
    status('growing a twin — the rope is the path it travels...');
    var t0=performance.now()/1000, dur=0.9;
    (function step(){
      var t=Math.min(1,(performance.now()/1000-t0)/dur);
      twin.position.lerpVectors(startPos,endPos,t);
      try{
        var tp=twin.position.clone().add(off);
        aimCyl(travelRope,A.point,tp);
      }catch(e){}
      if(t<1){ requestAnimationFrame(step); return; }
      try{ scene.remove(travelRope); }catch(e){}
      try{ twin.updateMatrixWorld(true); }catch(e){}
      buildSpan(A,{obj:twin,point:pointB.clone()});
      status('twin born where the rope led — synced 1:1 from birth. move one, both answer. tap the ring to mirror.');
    })();
  }catch(e){ status('twin failed: '+e.message); }
}
function moveObjBy(obj,wdelta){
  try{
    var p=obj.parent, ld;
    if(p && !p.isScene){
      p.updateWorldMatrix(true,false);
      var e=p.matrixWorld.elements;
      var sx=Math.hypot(e[0],e[1],e[2])||1,
          sy=Math.hypot(e[4],e[5],e[6])||1,
          sz=Math.hypot(e[8],e[9],e[10])||1;
      ld=new T.Vector3(
        (e[0]*wdelta.x+e[1]*wdelta.y+e[2]*wdelta.z)/sx,
        (e[4]*wdelta.x+e[5]*wdelta.y+e[6]*wdelta.z)/sy,
        (e[8]*wdelta.x+e[9]*wdelta.y+e[10]*wdelta.z)/sz
      );
    }else ld=wdelta.clone();
    obj.position.add(ld);
  }catch(e){}
}
function buildSpan(A,B){
  try{
    if(typeof scene==='undefined') return;
    mats();
    var g=new T.Group();
    var M=regMachine('span',g);
    var s={
      aObj:A.obj, bObj:B.obj,
      offA:A.obj.worldToLocal(A.point.clone()),
      offB:B.obj.worldToLocal(B.point.clone()),
      mode:1, lock:false, grow:0,
      lastA:worldPosOf(A.obj), lastB:worldPosOf(B.obj),
      machine:M, label:''
    };
    s.rope=ropeSeg(); g.add(s.rope);
    s.toggle=goldRing(0.2); g.add(s.toggle);
    s.thit=hitSphere(0.6); g.add(s.thit);
    regHit(s.thit,{onTap:function(){
      s.mode*=-1; layoutSpan(s);
      status(s.mode===1
        ? 'SPAN 1:1 — SYNCED: B follows A, same way, same distance'
        : 'SPAN −1:1 — MIRRORED: B opposes A, pulley inversi