<!-- PLAYA · part 5 -->
r.q); b.scale.fromArray(r.s); b.visible = r.vis !== false;
  meshesOf(b).forEach(function(m, i){ var d = r.mats[i]; if(!d) return; var mt = m.material;
    mt.color.setHex(d.c); mt.roughness = d.r; mt.metalness = d.m; mt.opacity = d.o; mt.transparent = d.o < 1; mt.depthWrite = d.o >= 1;
    mt.emissive.setHex(d.e); mt.emissiveIntensity = d.ei; mt.wireframe = !!d.w; mt.flatShading = !!d.f; mt.needsUpdate = true; });
  return b;
}
function readLib(){ var s = localStorage.getItem(LIB_KEY); return s ? JSON.parse(s) : []; }
function writeLib(a){ localStorage.setItem(LIB_KEY, JSON.stringify(a)); }

/* ---------- TOOLS ---------- */
var tool = 'select';
var isolated = null;   /* list of blocks hidden by Isolate */
var colorState = {h: 40, s: 0.55, l: 0.5};
function label(b){ return b ? shapeName(b.userData.blockType) + ' #' + b.userData.id : 'nothing'; }
function hslHex(h, s, l){ return '#' + new THREE.Color().setHSL(h/360, s, l).getHexString().toUpperCase(); }

var ACT = {
  tool: function(t){
    tool = t;
    var msg = {select:'Select — tap a block.', move:'Move — drag a block across the ground.', rotate:'Rotate — drag a block left / right.',
      scale:'Scale — drag a block up / down.', create:'Create — tap the ground to place a ' + shapeName(selectedShape) + '.', delete:'Delete — tap a block to remove it.'}[t];
    say(msg); refresh();
  },
  spawn: function(t){ selectedShape = t; spawn(t, orbit.target.x, orbit.target.z, false); },
  duplicate: function(){
    var o = need(); if(!o) return;
    var c = cloneBlock(o);
    var b = boxOf(o), w = b.max.x - b.min.x;
    if(!placeFree(c, o.position.x + w + 1, o.position.z)){ say('Cannot duplicate — no free space (no overlap).', true); return; }
    c.position.y = o.position.y;
    attach(c); pushHist({label: 'Duplicate ' + label(o), kind: 'add', obj: c}); select(c);
    say('Duplicated → ' + label(c) + '.');
  },
  layer: function(){
    var o = need(); if(!o) return;
    var c = cloneBlock(o), b = boxOf(o);
    c.position.copy(o.position); attach(c); c.updateMatrixWorld(true);
    var cb = boxOf(c); c.position.y += (b.max.y - cb.min.y);
    if(wouldOverlap(c)){ detach(c); say('Cannot layer — something is above it (no overlap).', true); return; }
    pushHist({label: 'Layer on ' + label(o), kind: 'add', obj: c}); select(c);
    say('Layered ' + label(c) + ' on top.');
  },
  delete: function(){
    var o = need(); if(!o) return;
    detach(o); pushHist({label: 'Delete ' + label(o), kind: 'remove', obj: o});
    say('Deleted ' + label(o) + '. Undo brings it back.'); refresh();
  },
  undo: function(){
    var e = undoStack.pop(); if(!e){ say('Nothing to undo.'); return; }
    applyEntry(e, -1); redoStack.push(e); if(selected) selBox.setFromObject(selected);
    say('Undone: ' + e.label); refresh();
  },
  redo: function(){
    var e = redoStack.pop(); if(!e){ say('Nothing to redo.'); return; }
    applyEntry(e, 1); undoStack.push(e); say('Redone: ' + e.label); refresh();
  },
  flip: function(){ if(edit('Flip', function(o){ o.scale.x *= -1; })) say('Flipped ' + label(selected) + ' (mirror X).'); },
  stretch: function(){ if(edit('Stretch', function(o){ o.scale.y *= 1.25; dropToGround(o); if(wouldOverlap(o)){ say('Stretch blocked — would overlap.', true); return false; } })) say('Stretched ' + label(selected) + ' taller.'); },
  inflate: function(){ if(edit('Inflate', function(o){ o.scale.multiplyScalar(1.15); dropToGround(o); if(wouldOverlap(o)){ say('Inflate blocked — would overlap.', true); return false; } })) say('Inflated ' + label(selected) + '.'); },
  flatten: function(){ if(edit('Flatten', function(o){ o.scale.y *= 0.8; applyMinimum(o); dropToGround(o); })) say('Flattened ' + label(selected) + '.'); },
  pinch: function(){ if(edit('Pinch', function(o){ o.scale.x *= 0.85; o.scale.z *= 0.85; applyMinimum(o); })) say('Pinched ' + label(selected) + '.'); },
  grow: function(f){ f = parseFloat(f); if(edit(f > 1 ? 'Bigger' : 'Smaller', function(o){ o.scale.multiplyScalar(f); applyMinimum(o); dropToGround(o); if(f > 1 && wouldOverlap(o)){ say('Blocked — would overlap.', true); return false; } })) say('Scale ' + selected.scale.x.toFixed(2) + '.'); },
  bend: function(k){ k = parseFloat(k) * 0.5; if(edit('Bend ' + (k > 0 ? '+' : '−'), function(o){ o.userData.bend += k; bendGeometry(o, k); })) say('Bent ' + label(selected) + ' (bend ' + selected.userData.bend.toFixed(1) + ').'); },
  unbend: function(){ if(edit('Straighten', function(o){ o.userData.bend = 0; rebuildGeometry(o); })) say('Straightened ' + label(selected) + '.'); },
  res: function(r){ r = parseInt(r, 10); if(edit('Resolution ' + r + '×', function(o){ o.userData.res = r; rebuildGeometry(o); })) say('Mesh resolution ' + ({1:'Low',2:'Medium',4:'High'})[r] + ' — ' + polyCount(selected) + ' triangles.'); },
  snap: function(){ if(edit('Snap', function(o){
      o.position.x = Math.round(o.position.x); o.position.z = Math.round(o.position.z);
      var st = Math.PI / 12; o.rotatio