<!-- RUBY-BACKUP · part 570 -->
var k=0;k<pts.length;k++) pv.push(pts[k].x, pts[k].y, pts[k].z);
    var pg = new THREE.BufferGeometry();
    pg.setAttribute('position', new THREE.Float32BufferAttribute(pv, 3));
    var pm = new THREE.PointsMaterial({color:0xffe27a, size:0.35, transparent:true, opacity:0,
      blending:THREE.AdditiveBlending, depthWrite:false});
    var dust = new THREE.Points(pg, pm);
    var group = new THREE.Group();
    group.add(lines); group.add(dust);
    if(window.scene) window.scene.add(group);
    webSeq++;
    var web = {group:group, mats:[lm, pm], name:'web'+webSeq,
               dist:latchDist, conns:conns, pts:pts.length, pairs:pairs};
    webs.push(web);
    /* threads fade in — nothing pops */
    var t0 = Date.now();
    (function fadeIn(){
      var t = Math.min(1, (Date.now()-t0)/450);
      lm.opacity = 0.85*t; pm.opacity = 0.95*t;
      if(t < 1) requestAnimationFrame(fadeIn);
    })();
    return web;
  }

  /* ---------- dissolve (the antonym) ---------- */
  window.unspider = function(){
    if(!webs.length){ status('no webs'); return 'no webs'; }
    var n = webs.length;
    webs.forEach(function(w){
      var t0 = Date.now();
      (function fadeOut(){
        var t = Math.min(1, (Date.now()-t0)/500);
        w.mats[0].opacity = 0.85*(1-t);
        w.mats[1].opacity = 0.95*(1-t);
        if(t < 1) requestAnimationFrame(fadeOut);
        else{
          try{
            w.group.traverse(function(o){
              if(o.geometry) o.geometry.dispose();
              if(o.material) o.material.dispose();
            });
            if(window.scene) window.scene.remove(w.group);
          }catch(e){}
        }
      })();
    });
    webs = [];
    var msg = n+' web'+(n>1?'s':'')+' dissolved';
    status(msg);
    return msg;
  };
  /* exposure for future systems (phoenix erase, snapshots) */
  window.__clearWebs = function(){ try{ window.unspider(); }catch(e){} };
  window.__webCount = function(){ return webs.length; };

  window.listWebs = function(){
    if(!webs.length){ status('no webs spun'); return 'no webs'; }
    var msg = webs.map(function(w){
      return w.name+': '+w.conns+' threads, '+w.pts+' points @ '+w.dist+'u';
    }).join(' | ');
    status(msg);
    return msg;
  };

  window.spiderDist = function(n){
    n = parseFloat(n);
    if(isNaN(n) || n <= 0){ status('spider <distance> — e.g. spider 5'); return latchDist; }
    latchDist = Math.min(60, Math.max(0.5, n));
    return latchDist;
  };

  /* ==========================================================================
     WARP — the web is the map, the pulley is the vehicle.
     The King: "You use the pulleys to warp the new found connection after
     the spider has woven the whole."
     Every thread is a pulley line. Tap one (or warp <n>) and a gold pulley
     wheel carries you along it to the far point.
     ========================================================================== */
  function riderPos(){
    try{
      if(typeof mePos !== 'undefined' && mePos) return new THREE.Vector3(mePos.x, mePos.y, mePos.z);
    }catch(e){}
    try{ if(window.scene && camera) return camera.position.clone(); }catch(e){}
    return new THREE.Vector3();
  }

  function segHit(p, a, b){
    var ab = b.clone().sub(a);
    var len2 = ab.lengthSq();
    var t = len2 > 0 ? Math.max(0, Math.min(1, p.clone().sub(a).dot(ab)/len2)) : 0;
    var c = a.clone().add(ab.multiplyScalar(t));
    return {d:p.distanceTo(c), t:t};
  }

  function nearestThread(point, maxD){
    var best = null;
    for(var w=0;w<webs.length;w++){
      var prs = webs[w].pairs;
      for(var i=0;i<prs.length;i++){
        var h = segHit(point, prs[i][0], prs[i][1]);
        if(h.d <= maxD && (!best || h.d < best.d))
          best = {web:webs[w], idx:i, a:prs[i][0], b:prs[i][1], d:h.d};
      }
    }
    return best;
  }

  function rideThread(a, b){
    var me = riderPos();
    var start = (a.distanceToSquared(me) <= b.distanceToSquared(me)) ? a.clone() : b.clone();
    var end = start.equals(a) ? b.clone() : a.clone();
    /* the pulley: gold wheel + hub + hanging car */
    var wheel = new THREE.Group();
    var gold = new THREE.MeshStandardMaterial({color:0xd8a920, metalness:0.85,
      roughness:0.25, emissive:0x2a1e00});
    var tire = new THREE.Mesh(new THREE.TorusGeometry(0.5, 0.15, 10, 22), gold);
    var hub = new THREE.Mesh(new THREE.CylinderGeometry(0.12, 0.12, 0.34, 10), gold);
    hub.rotation.x = Math.PI/2;
    var carGeo = new THREE.BufferGeometry().setFromPoints(
      [new THREE.Vector3(0,0,0), new THREE.Vector3(0,-1.1,0)]);
    var carLine = new THREE.Line(carGeo, new THREE.LineBasicMaterial({color:0xffd700}));
    var car = new THREE.Mesh(new THREE.OctahedronGeometry(0.32), gold);
    car.position.y = -1.35;
    wheel.add(tire); wheel.add(hub); wheel.add(carLine); wheel.add(car);
    wheel.position.copy(start);
    wheel.lookAt(end);
    if(window.scene) window.scene.add(wheel);
    status('warping — riding the thread');