<!-- RUBY-BACKUP · part 139 -->
Line);
        downInfo = null;
        return;
      }
      if(awaitingAnchor){
        if(grp){ awaitingAnchor=false; selectConnected(grp); }
        else codeStatus('tap the anchor stone');
        downInfo = null;
        return;
      }
      var pe = grp ? portalAt(grp) : null;
      if(pe){
        var purl = location.origin + '/ruby/?watch=' + encodeURIComponent(pe.watch);
        var pw2 = window.open(purl, '_blank');
        if(!pw2) location.href = purl;
        downInfo = null;
        return;
      }
      if(WALK){
        /* point at anchor stones: tap one and the cursor locks in — then build together */
        ndc.set((e.clientX/window.innerWidth)*2-1, -(e.clientY/window.innerHeight)*2+1);
        rayc.setFromCamera(ndc, camera);
        var whits = rayc.intersectObjects(stones.children, true);
        var wgrp = whits.length ? stoneGroupOf(whits[0].object) : null;
        var wlock = (wgrp && lockedAnchors.indexOf(wgrp)>=0) ? wgrp : (FLYM ? aimedAnchor : null);
        if(wlock){
          cursor.x = wlock.position.x; cursor.z = wlock.position.z; saveCursor(); paintCursor();
          codeStatus('locked in — punch a code, it builds at your point');
        }else{
          /* tap a fellow traveler and glide over — no long walk */
          var nearCid = null, nearD = 1.4;
          for(var rcid in remoteAvatars){
            var rp = remoteAvatars[rcid].grp.position;
            _a1.set(rp.x, rp.y+0.5, rp.z);
            var dp = rayc.ray.distanceToPoint(_a1);
            if(dp < nearD){ nearD = dp; nearCid = rcid; }
          }
          if(nearCid){
            var tp = remoteAvatars[nearCid].grp.position;
            glide = {x:tp.x, z:tp.z};
            codeStatus('gliding over');
          } else codeStatus('point at a locked anchor');
        }
        downInfo = null; ghost.visible = false; return;
      }
      /* BUILD MODE: tap a stone to build upon it */
      if(typeof BUILD_MODE!=='undefined' && BUILD_MODE){
        var bh = [];
        try{
          var _bt3 = [];
          scene.traverse(function(o){ if(o.isMesh && !o.isSprite) _bt3.push(o); });
          bh = rayc.intersectObjects(_bt3, false);
        }catch(e){ bh = []; }
        for(var bi=0; bi<bh.length; bi++){
          var bo = bh[bi].object;
          if(bo && bo.position && typeof placeStone==='function'){
            var nx = bo.position.x, ny = bo.position.y+2, nz = bo.position.z;
            placeStone(nx, ny, nz, 'b');
            // If a camera marker was here, its view is obsolete until unused
            var bkey = Math.round(nx)+','+Math.round(nz);
            ALL_INVISIBLE.forEach(function(st){
              if(st && Math.round(st.position.x)==Math.round(nx) && Math.round(st.position.z)==Math.round(nz)){
                BLOCKED_VIEWS[bkey] = true;
              }
            });
            codeStatus('built upon stone');
            break;
          }
        }
        downInfo = null; return;
      }
      /* the world ties in: every touch routes through the Mainframe */
      if(mode==='delete'){
        if(grp) Mainframe.shrink(grp);
      }else{
        /* anchors first: tap an interconnection point to raise a shape anchored there */
        var ah = rayc.intersectObjects(anchorGroup.children, false);
        if(ah.length){
          var np = ah[0].object.position;
          Mainframe.place(np.x, np.z);
        }else if(grp){
          Mainframe.grow(grp);
        }else{
          var cell = groundCell(e.clientX, e.clientY);
          if(cell) Mainframe.place(cell.x, cell.z);
        }
      }
    }
    downInfo = null;
  }
  ghost.visible = false;
}
canvas.addEventListener('pointerup', endPointer);
canvas.addEventListener('pointercancel', endPointer);
canvas.addEventListener('wheel', function(e){
  e.preventDefault();
  camGoal.r = Math.max(6, Math.min(70, camGoal.r * (1 + Math.sign(e.deltaY)*0.12)));
}, {passive:false});

/* ---- shapes: one button opens the icons, he chooses ---- */
var SHAPE_ICONS = {
  cube: '<svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  pyramid: '<svg viewBox="0 0 24 24"><path d="M12 5 L19 19 L5 19 Z" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  sphere: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  cyl: '<svg viewBox="0 0 24 24"><rect x="6" y="8" width="12" height="9" fill="none" stroke="#ffd700" stroke-width="2"/><ellipse cx="12" cy="8" rx="6" ry="2.5" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  torus: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="none" stroke="#ffd700" stroke-width="2"/><circle cx="12" cy="12" r="2.5" fill="none" stroke="#ffd700" stroke-width="2"/></svg>',
  cone: '<svg viewBox="0 0 24 24"><path d="M12 5 L18 18 L6 18 Z" fill="none" stroke="#ffd700" stroke-width="2"/><ellipse cx="12" cy="18" rx="6" ry="2" fill="none" stroke="#ffd700" stroke