<!-- RUBY-BACKUP · part 294 -->
dPosition(worldPos);
    target.updateWorldMatrix(true, false);
    var localPos = target.worldToLocal(worldPos.clone());
    // remove from current parent
    if(_grabbed.parent) _grabbed.parent.remove(_grabbed);
    // add as child
    target.add(_grabbed);
    _grabbed.position.copy(localPos);
    // clear the gold glow
    if(_grabbed.material && _grabbed.material.emissive && _grabbed.userData._oldEmissive!==undefined){
      _grabbed.material.emissive.setHex(_grabbed.userData._oldEmissive);
    }
    var name = _grabbed.name || 'thing';
    _grabbed = null;
    INSTALL_MODE = false;
    var h = document.getElementById('installhint');
    if(h) h.remove();
    showGrabHint();
    codeStatus('installed');
    // undo support
    var installed = target.children[target.children.length-1];
    var targetRef = target;
    pushCommand({
      name: 'install',
      execute: function(){ targetRef.add(installed); },
      undo: function(){ targetRef.remove(installed); if(typeof scene!=='undefined') scene.add(installed); }
    });
  }catch(e){ codeStatus('install failed'); }
}
// tap to install when in install mode
if(typeof window!=='undefined'){
  window.addEventListener('click', function(e){
    if(!INSTALL_MODE || !_grabbed) return;
    try{
      if(typeof raycaster!=='undefined' && typeof camera!=='undefined' && typeof scene!=='undefined'){
        var mouse = new THREE.Vector2();
        mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        var hits = raycaster.intersectObjects(scene.children, true);
        for(var i=0; i<hits.length; i++){
          var o = hits[i].object;
          if(o===_grabbed) continue;
          if(o.name && (o.name.indexOf('street')===0 || o.name==='plaza' || o.name.indexOf('ground')===0)) continue;
          var target = o;
          while(target.parent && target.parent!==scene && !target.userData.isPrimitive){
            target = target.parent;
          }
          if(target!==_grabbed){
            doInstall(target);
            break;
          }
        }
      }
    }catch(err){}
  });
}
function showGrabPanel(){
  hideGrabPanel();
  var p = document.createElement('div');
  p.id = 'grabpanel';
  p.style.cssText = 'position:fixed;bottom:120px;right:14px;z-index:10001;display:flex;flex-direction:column;gap:8px;';
  var btns = [
    ['⟲', 'twist left', function(){ if(_grabbed) _grabbed.rotation.y += 0.26; }],
    ['⟳', 'twist right', function(){ if(_grabbed) _grabbed.rotation.y -= 0.26; }],
    ['⤴', 'tilt up', function(){ if(_grabbed) _grabbed.rotation.x += 0.26; }],
    ['⤵', 'tilt down', function(){ if(_grabbed) _grabbed.rotation.x -= 0.26; }],
    ['⬆', 'lift', function(){ if(_grabbed) _grabbed.position.y += 0.5; }],
    ['⬇', 'lower', function(){ if(_grabbed) _grabbed.position.y -= 0.5; }],
    ['＋', 'bigger', function(){ if(_grabbed) _grabbed.scale.multiplyScalar(1.1); }],
    ['－', 'smaller', function(){ if(_grabbed) _grabbed.scale.multiplyScalar(0.9); }],
    ['✓', 'drop', function(){ dropGrabbed(); hideGrabPanel(); showGrabHint(); codeStatus('dropped'); }],
    ['⬒', 'install into', function(){ startInstall(); }]
  ];
  btns.forEach(function(b){
    var btn = document.createElement('button');
    btn.textContent = b[0];
    btn.title = b[1];
    btn.style.cssText = 'width:48px;height:48px;background:#d8a920;color:#111;border:none;border-radius:50%;font-size:20px;font-weight:bold;';
    btn.onclick = b[2];
    p.appendChild(btn);
  });
  document.body.appendChild(p);
  _grabPanel = p;
}
function hideGrabPanel(){
  var p = document.getElementById('grabpanel');
  if(p) p.remove();
  _grabPanel = null;
}
// tap to pick up when in grab mode
if(typeof window!=='undefined'){
  window.addEventListener('click', function(e){
    if(!GRAB_MODE || _grabbed) return;
    // raycast to find tapped object
    try{
      if(typeof raycaster!=='undefined' && typeof camera!=='undefined' && typeof scene!=='undefined'){
        var mouse = new THREE.Vector2();
        mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        var hits = raycaster.intersectObjects(scene.children, true);
        for(var i=0; i<hits.length; i++){
          var o = hits[i].object;
          // skip ground, streets, UI
          if(o.name && (o.name.indexOf('street')===0 || o.name==='plaza' || o.name.indexOf('ground')===0)) continue;
          // walk up to find a group
          var target = o;
          while(target.parent && target.parent!==scene && !target.userData.isPrimitive){
            target = target.parent;
          }
          pickUp(target);
          break;
        }
      }
    }catch(err){}
  });
}
/* STREETS — creative, winding, alive */
/* PERMANENT STREETS — load at start, never removed */
function ensurePermanentStreets(){
  try{
    