<!-- RUBY-BACKUP · part 21 -->
ode id ---------- */
  function resolve(x, z){
    try{
      var near = window.nearestNode(x, z);
      if(!near) return null;
      if(Math.hypot(near.x - x, near.z - z) > 10) return null;
      return near.id;
    }catch(e){ return null; }
  }

  /* ---------- apply one saved/default mode to a live node ---------- */
  function applyMode(nid, e){
    var m = (e.mode || 'solid').toLowerCase();
    var sns = window.__solidNodes || {};
    try{
      switch(m){
        case 'portal': window.setNodeMode(nid, 'solid'); break; /* pairs linked after */
        case 'switch':
          window.switchNode(nid);
          if(e.on) window.flipSwitch(nid);
          break;
        case 'sensor': window.sensorNode(nid); break;
        case 'spring': window.springNode(nid); break;
        case 'anchor': window.anchorNode(nid); break;
        case 'latch': window.setNodeMode(nid, 'latch'); break;
        case 'conveyor':
          window.conveyorNode(nid);
          if(typeof e.faceDeg === 'number') window.faceNode(nid, e.faceDeg);
          break;
        case 'elevator': window.elevatorNode(nid, e.height || 6); break;
        case 'catapult': window.catapultNode(nid); break;
        case 'blueprint': window.blueprintNode(nid); break;
        case 'welder': window.welderNode(nid); break;
        case 'crane': window.craneNode(nid); break;
        case 'counter':
          window.counterNode(nid);
          if(typeof e.count === 'number' && sns[nid]) sns[nid].count = e.count;
          break;
        case 'timer': window.timerNode(nid, e.secs || 10); break;
        case 'messenger': window.messengerNode(nid, e.words || ''); break;
        case 'music':
          window.musicNode(nid, 'c');
          if(typeof e.freq === 'number' && sns[nid]) sns[nid].note = e.freq;
          break;
        case 'painter': window.painterNode(nid); break;
        case 'hider':
          window.hiderNode(nid);
          if(e.hidden) window.hiderNode(nid, 'hide');
          break;
        case 'battery':
          window.batteryNode(nid);
          if(typeof e.charge === 'number' && sns[nid]) sns[nid].charge = e.charge;
          break;
        case 'generator': window.generatorNode(nid); break;
        default: window.setNodeMode(nid, 'solid');
      }
    }catch(ex){}
  }

  /* ---------- dump live state to a savable object ---------- */
  window.__dumpNodes = function(){
    var out = {v:1, t:Date.now(), nodes:[], portals:[], network:null, spans:[]};
    try{
      var sns = window.__solidNodes || {};
      var st = (typeof window.__nodeSysState === 'function') ? window.__nodeSysState() : {};
      for(var nid in sns){
        var sn = sns[nid];
        if(!sn) continue;
        var e = {x: Math.round(sn.x*10)/10, z: Math.round(sn.z*10)/10, mode: sn.mode || 'solid'};
        if(e.mode === 'switch') e.on = !!(st.switchStates && st.switchStates[nid]);
        if(e.mode === 'counter' && typeof sn.count === 'number') e.count = sn.count;
        if(e.mode === 'battery' && typeof sn.charge === 'number') e.charge = sn.charge;
        if(e.mode === 'timer' && sn.timerTotal) e.secs = Math.max(2, Math.round((sn.timerEnd - Date.now())/1000));
        if(e.mode === 'messenger' && sn.words) e.words = String(sn.words).slice(0, 200);
        if(e.mode === 'music' && typeof sn.note === 'number') e.freq = sn.note;
        if(e.mode === 'elevator' && sn.topY && sn.baseY) e.height = Math.round((sn.topY - sn.baseY)*10)/10;
        if(e.mode === 'conveyor' && typeof sn.face === 'number') e.faceDeg = Math.round(sn.face*180/Math.PI);
        if(e.mode === 'hider') e.hidden = !!sn.hidden;
        out.nodes.push(e);
      }
      /* portal pairs as position pairs */
      var seen = {}, pmap = st.portals || {};
      for(var a in pmap){
        var b = pmap[a];
        if(!b || seen[a] || seen[b]) continue;
        seen[a] = seen[b] = true;
        var sa = sns[a], sb = sns[b];
        if(sa && sb) out.portals.push([
          {x: Math.round(sa.x*10)/10, z: Math.round(sa.z*10)/10},
          {x: Math.round(sb.x*10)/10, z: Math.round(sb.z*10)/10}
        ]);
      }
      /* network */
      if(st.network && st.network.members && st.network.members.length){
        var mem = [];
        for(var i=0;i<st.network.members.length;i++){
          var sm = sns[st.network.members[i]];
          if(sm) mem.push({x: Math.round(sm.x*10)/10, z: Math.round(sm.z*10)/10});
        }
        if(mem.length) out.network = {connected:true, members: mem};
      }
      /* spans */
      out.spans = dumpSpans();
    }catch(e){}
    return out;
  };

  function worldPosOf(o){
    try{
      var v = new THREE.Vector3();
      o.getWorldPosition(v);
      return v;
    }catch(e){ return null; }
  }
  function dumpSpans(){
    var arr = [];
    try{
      var spans = window.__spans || [];
      for(var i=0;i<spans.length;i++){
        var s = spans[i];
        if(!s || !s.aObj || !s.bObj || !s.aObj.parent || !s.bObj.parent) continue;
        var pa = wo