<!-- PLAYA · part 3 -->
n false; }
  pushHist({label: label, kind: 'state', obj: obj, before: before, after: snap(obj)});
  refresh();
  return true;
}
function applyEntry(e, dir){ /* dir: -1 undo, +1 redo */
  if(e.kind === 'state') restore(e.obj, dir < 0 ? e.before : e.after);
  else if(e.kind === 'add'){ if(dir < 0) detach(e.obj); else attach(e.obj); }
  else if(e.kind === 'remove'){ if(dir < 0) attach(e.obj); else detach(e.obj); }
  else if(e.kind === 'multi'){ var list = dir < 0 ? e.list.slice().reverse() : e.list; list.forEach(function(x){ applyEntry(x, dir); }); }
}
function attach(obj){ if(blocks.indexOf(obj) < 0){ scene.add(obj); blocks.push(obj); } }
function detach(obj){ var i = blocks.indexOf(obj); if(i > -1) blocks.splice(i, 1); scene.remove(obj); if(selected === obj) select(null); }

/* ---------- SELECTION ---------- */
var selected = null;
var selBox = new THREE.BoxHelper(undefined, 0xffd700);
selBox.visible = false; scene.add(selBox);
function select(obj){ selected = obj; if(obj){ selBox.setFromObject(obj); } selBox.visible = !!obj && obj.visible; refresh(); }
function need(){ if(!selected){ say('Select a block first — tap one in the view, or pick it in Manage Layers.', true); return null; } return selected; }

/* ---------- SPAWN ---------- */
var selectedShape = 'cube';
function placeFree(b, x, z){
  /* spiral out from (x,z) until nothing overlaps (saga-solid spawnBlock idea) */
  var steps = [[0,0]];
  for(var ring=1; ring<=6; ring++) for(var k=0; k<8; k++){ var a = k/8*Math.PI*2; steps.push([Math.cos(a)*ring*6, Math.sin(a)*ring*6]); }
  for(var i=0;i<steps.length;i++){
    b.position.set(Math.round(x + steps[i][0]), 0, Math.round(z + steps[i][1]));
    dropToGround(b);
    if(!wouldOverlap(b)) return true;
  }
  return false;
}
function spawn(type, x, z, exact){
  var b = makeBlock(type, 1);
  if(exact){
    b.position.set(Math.round(x), 0, Math.round(z)); dropToGround(b);
    if(wouldOverlap(b)){ say('Blocked — a block is already there (no overlap).', true); return null; }
  } else if(!placeFree(b, x, z)){ say('Cannot place — no free space nearby (no overlap).', true); return null; }
  attach(b);
  pushHist({label: 'Add ' + shapeName(type), kind: 'add', obj: b});
  select(b);
  say('Placed ' + shapeName(type) + ' #' + b.userData.id + '.');
  return b;
}
function cloneBlock(src){
  var c = makeBlock(src.userData.blockType, src.userData.res);
  var sn = snap(src); sn.vis = true; restore(c, sn);
  return c;
}

/* ---------- SHAPE OPS ---------- */
function rebuildGeometry(obj){
  var parts = shapeParts(obj.userData.blockType, obj.userData.res);
  meshesOf(obj).forEach(function(m, i){ m.geometry = parts[i][0]; });
  if(obj.userData.bend) bendGeometry(obj, obj.userData.bend);
}
function bendGeometry(obj, k){
  /* x += k * h^2 in block space (h = height above the block's base) — a real vertex bend */
  var ms = meshesOf(obj);
  var minY = Infinity, maxY = -Infinity;
  ms.forEach(function(m){ m.geometry.computeBoundingBox(); var bb = m.geometry.boundingBox.clone().applyMatrix4(m.matrix); minY = Math.min(minY, bb.min.y); maxY = Math.max(maxY, bb.max.y); });
  var H = Math.max(0.001, maxY - minY);
  var v = new THREE.Vector3(), inv = new THREE.Matrix4();
  ms.forEach(function(m){
    var g = m.geometry.clone(); m.updateMatrix(); inv.copy(m.matrix).invert();
    var pos = g.attributes.position;
    for(var i=0;i<pos.count;i++){
      v.fromBufferAttribute(pos, i).applyMatrix4(m.matrix);
      var h = (v.y - minY) / H; v.x += k * h * h * H * 0.35;
      v.applyMatrix4(inv); pos.setXYZ(i, v.x, v.y, v.z);
    }
    pos.needsUpdate = true; g.computeVertexNormals(); g.computeBoundingBox(); g.computeBoundingSphere();
    m.geometry = g;
  });
}
function eachMat(obj, fn){ meshesOf(obj).forEach(function(m){ fn(m.material, m); m.material.needsUpdate = true; }); }
function setColor(hex){
  edit('Color ' + hex, function(o){ eachMat(o, function(mt){ mt.color.set(hex); if(mt.emissiveIntensity > 0 && o.userData.glow) mt.emissive.set(hex); }); });
  say('Color ' + hex + ' on ' + label(selected) + '.');
}

/* ---------- MATERIAL BRUSHES ---------- */
var BRUSHES = [
  ['Gold',     {color:0xd4a017, roughness:0.3, metalness:0.9}, 'linear-gradient(135deg,#ffe58a,#b8860b)'],
  ['Metal',    {color:0xc0c4cc, roughness:0.25, metalness:0.95}, 'linear-gradient(135deg,#f2f4f8,#6b7280)'],
  ['Marble',   {color:0xf2efe9, roughness:0.25, metalness:0.0}, 'linear-gradient(135deg,#ffffff,#cfc8bd)'],
  ['Concrete', {color:0x8e8e88, roughness:0.95, metalness:0.0}, 'linear-gradient(135deg,#a8a8a2,#6b6b66)'],
  ['Glass',    {color:0xa8d8ff, roughness:0.05, metalness:0.1, opacity:0.35}, 'linear-gradient(135deg,rgba(200,235,255,.9),rgba(80,140,200,.6))'],
  ['Wood',     {color:0x8b5a2b, roughness:0.8, metalness:0.0}, 'linear-gradient(135deg,#b07a45,#5a3615)'],
  ['Rock',     {color:0x6b625a, roughness:1.0, metalness:0.0}, 'linear-gradient(135deg,#8a8178,#4a433d)'],
  ['Fabric',   {color:0x7a2e3a, roughness:1.0, metalness:0.0}, '