<!-- PLAYA · part 8 -->
nction(){ sun.intensity = parseFloat(this.value); });
document.getElementById('sunAz').addEventListener('input', function(){ sunAz = parseFloat(this.value); placeSun(); });
document.getElementById('ambI').addEventListener('input', function(){ hemi.intensity = parseFloat(this.value); });
document.getElementById('bgpick').addEventListener('input', function(){ scene.background = new THREE.Color(this.value); scene.fog.color.set(this.value); });
document.getElementById('lnpick').addEventListener('input', function(){ gridColor = new THREE.Color(this.value).getHex(); buildGrid(); });
/* transform inputs */
document.querySelectorAll('[data-xf]').forEach(function(inp){
  inp.addEventListener('change', function(){
    var k = inp.getAttribute('data-xf').split('.'), v = parseFloat(inp.value);
    if(!isFinite(v)){ say('Type a number.', true); refresh(); return; }
    var ok = edit('Set ' + k.join(' ').toUpperCase(), function(o){
      if(k[0] === 'p') o.position[k[1]] = v;
      else if(k[0] === 'r') o.rotation[k[1]] = v * Math.PI / 180;
      else { if(Math.abs(v) < 0.01){ say('Scale cannot be 0.', true); return false; } o.scale[k[1]] = v; applyMinimum(o); }
      if(wouldOverlap(o)){ say('Blocked — would overlap.', true); return false; }
    });
    if(ok) say(label(selected) + ' ' + k.join('.').toUpperCase() + ' = ' + v + '.');
  });
  inp.addEventListener('keydown', function(e){ if(e.key === 'Enter') inp.blur(); });
});
document.getElementById('chex').addEventListener('keydown', function(e){ if(e.key === 'Enter'){ ACT.applyhex(); this.blur(); } });
document.getElementById('clight').addEventListener('input', function(){ colorState.l = parseFloat(this.value) / 100; showWheel(); });
document.getElementById('clight').addEventListener('change', function(){ if(selected) setColor(hslHex(colorState.h, colorState.s, colorState.l)); });

/* ---------- COLOR WHEEL ---------- */
var wheel = document.getElementById('wheel'), wmk = document.getElementById('wheelmk');
function showWheel(){
  var hex = hslHex(colorState.h, colorState.s, colorState.l);
  document.getElementById('cprev').style.background = hex;
  document.getElementById('chex').value = hex;
  var a = colorState.h * Math.PI / 180;
  wmk.style.left = (50 + Math.cos(a) * colorState.s * 50) + '%';
  wmk.style.top = (50 + Math.sin(a) * colorState.s * 50) + '%';
  wmk.style.background = hex;
}
function wheelPick(e){
  var r = wheel.getBoundingClientRect();
  var dx = e.clientX - (r.left + r.width/2), dy = e.clientY - (r.top + r.height/2);
  var ang = Math.atan2(dy, dx) * 180 / Math.PI; if(ang < 0) ang += 360;
  colorState.h = ang; colorState.s = Math.min(1, Math.sqrt(dx*dx + dy*dy) / (r.width/2));
  showWheel();
}
var wheelDown = false;
wheel.addEventListener('pointerdown', function(e){ wheelDown = true; wheel.setPointerCapture(e.pointerId); wheelPick(e); });
wheel.addEventListener('pointermove', function(e){ if(wheelDown) wheelPick(e); });
wheel.addEventListener('pointerup', function(e){ if(!wheelDown) return; wheelDown = false; wheelPick(e); if(selected) setColor(hslHex(colorState.h, colorState.s, colorState.l)); else say('Color ' + hslHex(colorState.h, colorState.s, colorState.l) + ' ready — select a block, then tap Apply.'); });
wheel.addEventListener('pointercancel', function(){ wheelDown = false; });

/* ---------- VIEWPORT POINTER: orbit / pan / zoom / tap / drag-to-transform ---------- */
var rc = new THREE.Raycaster(), ndc = new THREE.Vector2();
var pointers = {}, gesture = null;
function rayAt(x, y){ var r = canvas.getBoundingClientRect(); ndc.set(((x - r.left) / r.width) * 2 - 1, -((y - r.top) / r.height) * 2 + 1); rc.setFromCamera(ndc, camera); return rc; }
function hitBlock(x, y){
  var vis = blocks.filter(function(b){ return b.visible; });
  var hits = rayAt(x, y).intersectObjects(vis, true);
  if(!hits.length) return null;
  var o = hits[0].object; while(o.parent && !o.userData.creatorBlock) o = o.parent;
  return o.userData.creatorBlock ? o : null;
}
function groundAt(x, y, h){ var pt = new THREE.Vector3(); return rayAt(x, y).ray.intersectPlane(new THREE.Plane(new THREE.Vector3(0,1,0), -(h || 0)), pt) ? pt : null; }
canvas.addEventListener('contextmenu', function(e){ e.preventDefault(); });
canvas.addEventListener('pointerdown', function(e){
  canvas.setPointerCapture(e.pointerId);
  pointers[e.pointerId] = {x: e.clientX, y: e.clientY};
  var ids = Object.keys(pointers);
  if(ids.length === 2){ /* pinch / two-finger pan */
    if(gesture && gesture.kind === 'xf' && gesture.moved) commitXf(gesture);
    var a = pointers[ids[0]], b = pointers[ids[1]];
    gesture = {kind: 'pinch', d: Math.hypot(a.x-b.x, a.y-b.y), mx: (a.x+b.x)/2, my: (a.y+b.y)/2, dist: orbit.dist};
    return;
  }
  var hit = (tool === 'move' || tool === 'rotate' || tool === 'scale') ? hitBlock(e.clientX, e.clientY) : null;
  if(hit && e.button === 0){
    if(selected !== hit) select(hit);
    var gp = groundAt(e.clientX, e.clientY, hit.position.y);
    gesture =