<!-- RUBY-BACKUP · part 377 -->
('her ' + pieces.length + ' pieces, laid out — study them');
    },
    undefined,
    function(err){ codeStatus('could not load her to dissect'); }
  );
}

/* INVESTIGATE HER DIMENSIONS */

/* FIX HER PARTS — repair and reshape PROVOLTA piece by piece */
var PROVOLTA_MODEL = null;
var PROVOLTA_ORIGINALS = {};
function getProvoltaModel(cb){
  if(PROVOLTA_MODEL){ cb(PROVOLTA_MODEL); return; }
  if(typeof THREE==='undefined' || typeof THREE.GLTFLoader==='undefined'){ codeStatus('GLTFLoader not ready'); return; }
  var loader = new THREE.GLTFLoader();
  loader.load('/ruby/warrior.glb?v=1', function(glb){
    PROVOLTA_MODEL = glb.scene;
    /* save originals */
    PROVOLTA_MODEL.traverse(function(obj){
      if(obj.isMesh){
        var n = (obj.name || 'unnamed').toLowerCase();
        if(!PROVOLTA_ORIGINALS[n]){
          PROVOLTA_ORIGINALS[n] = {
            color: obj.material.color.clone(),
            metalness: obj.material.metalness,
            roughness: obj.material.roughness,
            scale: obj.scale.clone()
          };
        }
      }
    });
    cb(PROVOLTA_MODEL);
  }, undefined, function(err){ codeStatus('could not load her to fix'); });
}
function fixProvoltaPart(partName, prop, val){
  getProvoltaModel(function(model){
    var found = 0;
    model.traverse(function(obj){
      if(obj.isMesh){
        var n = (obj.name || '').toLowerCase();
        if(n.indexOf(partName.toLowerCase()) >= 0){
          found++;
          if(prop==='color'){
            var hex = val.replace('#','');
            obj.material.color.set('#' + hex);
            codeStatus(n + ' → color #' + hex);
          } else if(prop==='metal'){
            obj.material.metalness = Math.max(0, Math.min(1, parseFloat(val)));
            obj.material.needsUpdate = true;
            codeStatus(n + ' → metal ' + val);
          } else if(prop==='rough'){
            obj.material.roughness = Math.max(0, Math.min(1, parseFloat(val)));
            obj.material.needsUpdate = true;
            codeStatus(n + ' → rough ' + val);
          } else if(prop==='scale'){
            var s = parseFloat(val);
            obj.scale.set(s, s, s);
            codeStatus(n + ' → scale ' + val);
          } else if(prop==='glow'){
            obj.material.emissive = obj.material.emissive || new THREE.Color(0x000000);
            var hex2 = val.replace('#','');
            obj.material.emissive.set('#' + hex2);
            obj.material.emissiveIntensity = 1.0;
            obj.material.needsUpdate = true;
            codeStatus(n + ' → glow #' + hex2);
          }
        }
      }
    });
    if(!found) codeStatus('no part matching "' + partName + '"');
  });
}
function resetProvoltaPart(partName){
  getProvoltaModel(function(model){
    var found = 0;
    model.traverse(function(obj){
      if(obj.isMesh){
        var n = (obj.name || 'unnamed').toLowerCase();
        if(n.indexOf(partName.toLowerCase()) >= 0 && PROVOLTA_ORIGINALS[n]){
          var o = PROVOLTA_ORIGINALS[n];
          obj.material.color.copy(o.color);
          obj.material.metalness = o.metalness;
          obj.material.roughness = o.roughness;
          obj.scale.copy(o.scale);
          obj.material.needsUpdate = true;
          found++;
        }
      }
    });
    codeStatus(found ? partName + ' restored' : 'no part matching "' + partName + '"');
  });
}
function measureProvolta(){
  if(typeof scene==='undefined') return;
  if(typeof THREE==='undefined' || typeof THREE.GLTFLoader==='undefined'){
    codeStatus('GLTFLoader not ready');
    return;
  }
  codeStatus('measuring her...');
  var loader = new THREE.GLTFLoader();
  loader.load('/ruby/warrior.glb?v=1',
    function(glb){
      /* overall */
      var box = new THREE.Box3().setFromObject(glb.scene);
      var size = box.getSize(new THREE.Vector3());
      var lines = [];
      lines.push('PROVOLTA — full measure:');
      lines.push('height ' + size.y.toFixed(2) + ' · width ' + size.x.toFixed(2) + ' · depth ' + size.z.toFixed(2));
      lines.push('---');
      /* per piece */
      var pieces = [];
      glb.scene.traverse(function(obj){
        if(obj.isMesh) pieces.push(obj);
      });
      /* group by name */
      var byName = {};
      pieces.forEach(function(p){
        var n = p.name || 'unnamed';
        if(!byName[n]) byName[n] = [];
        byName[n].push(p);
      });
      Object.keys(byName).sort().forEach(function(name){
        var gb = new THREE.Box3();
        byName[name].forEach(function(m){ gb.expandByObject(m); });
        var s = gb.getSize(new THREE.Vector3());
        lines.push(name + ': ' + s.x.toFixed(2) + ' x ' + s.y.toFixed(2) + ' x ' + s.z.toFixed(2));
      });
      lines.push('---');
      lines.push(pieces.length + ' meshes · ' + Object.keys(byName).length + ' parts');
      /* show it */
      codeStatus(lines.join('\n'));
      /* also log to console for study */
      try{ console.log(lines.join('\n')); }catch(e){}
    },
    undefined,
    f