<!-- SOLID CITY · part 2042 -->
C.unionAABB(ts.map(function (it) { return it.box; })); selBox.min.set(u.x0, u.y0, u.z0); selBox.max.set(u.x1, u.y1, u.z1);
  selHelp.material.color.setHex(ts[0].cr.local ? 0xffd700 : 0x9a8f7a);
}
function pivotOf(datas) { var u = C.unionAABB(boxesOf(datas)); return { x: (u.x0 + u.x1) / 2, y: u.y0, z: (u.z0 + u.z1) / 2, yc: (u.y0 + u.y1) / 2 }; }
function keepBase(datas, y0) { var u = C.unionAABB(boxesOf(datas)), dy = y0 - u.y0; datas.forEach(function (d) { d.p[1] += dy; }); }
/* apply fn(datas, pivot) to the selection (validated, undoable) or to the ghost */
function edit(label, fn, keep) {
  if (!F.sel) {
    if (!F.ghostRel) return say('Pick a shape first.', true);
    var g = clone(F.ghostRel), pv = pivotOf(g); fn(g, pv); if (keep !== false) keepBase(g, pv.y);
    var bad = g.map(C.pieceAABB).filter(function (b) { var dx = b.x1 - b.x0, dy = b.y1 - b.y0, dz = b.z1 - b.z0; return Math.min(dx, dy, dz) < L.MIN_DIM || Math.max(dx, dy, dz) > L.MAX_DIM; });
    if (bad.length) return say('Can\u2019t — pieces stay between 0.13 m and ' + L.MAX_DIM + ' m.', true);
    F.ghostRel = g; setGhostKeep(); say(label + ' (preview).'); return;
  }
  var ts = targets(); if (!ts[0].cr.local) return say('Published builds are locked — ☰ → PICK UP to edit your own.', true);
  var datas = ts.map(function (it) { return clone(it.d); }), p = pivotOf(datas);
  fn(datas, p); if (keep !== false) keepBase(datas, p.y);
  var r = check(datas, ts[0].cr, ts); if (!r.ok) { buzz(); return say(label + ' blocked — ' + r.why, true); }
  pushUndo(); ts.forEach(function (it, i) { it.d = C.sanitizePiece(datas[i]); refreshItem(it); });
  changed(); say(label + '.');
}
function rotY(deg) { var q = C.qyaw(deg * Math.PI / 180); return function (ds, pv) { ds.forEach(function (d) { var v = C.qrot(q, d.p[0] - pv.x, 0, d.p[2] - pv.z); d.p[0] = pv.x + v[0]; d.p[2] = pv.z + v[2]; d.q = C.qmul(q, d.q); }); }; }
function tip(deg) { var h = deg * Math.PI / 360, q = [Math.sin(h), 0, 0, Math.cos(h)]; return function (ds, pv) { ds.forEach(function (d) { var v = C.qrot(q, d.p[0] - pv.x, d.p[1] - pv.yc, d.p[2] - pv.z); d.p = [pv.x + v[0], pv.yc + v[1], pv.z + v[2]]; d.q = C.qmul(q, d.q); }); }; }
function scaleBy(f) { return function (ds, pv) { ds.forEach(function (d) { d.p = [pv.x + (d.p[0] - pv.x) * f, pv.y + (d.p[1] - pv.y) * f, pv.z + (d.p[2] - pv.z) * f]; d.s = d.s.map(function (v) { return v * f; }); }); }; }
function each(fn) { return function (ds) { ds.forEach(fn); }; }
function nudge(dir) { // camera-relative, snapped to the nearest world axis, one grid step
  var g = F.grid, yaw = S.camYaw, fx = -Math.sin(yaw), fz = -Math.cos(yaw), v;
  if (dir === 'up' || dir === 'down') v = [0, dir === 'up' ? g : -g, 0];
  else {
    var x = 0, z = 0; if (dir === 'fwd') { x = fx; z = fz; } else if (dir === 'back') { x = -fx; z = -fz; } else if (dir === 'right') { x = -fz; z = fx; } else { x = fz; z = -fx; }
    v = Math.abs(x) > Math.abs(z) ? [Math.sign(x) * g, 0, 0] : [0, 0, Math.sign(z) * g];
  }
  if (!F.sel) {
    if (v[1]) { F.lift = Math.round((F.lift + v[1]) * 1000) / 1000; if (F.lift < -20) F.lift = -20; } else { F.gOff.x += v[0]; F.gOff.z += v[2]; }
    F.lastKey = ''; return;
  }
  edit('Moved', function (ds) { ds.forEach(function (d) { d.p = [d.p[0] + v[0], d.p[1] + v[1], d.p[2] + v[2]]; }); }, false);
}
function setColor(hex) {
  F.color = hex; var swatch = $('fg-csw'); if (swatch) swatch.style.background = '#' + ('00000' + hex.toString(16)).slice(-6);
  edit('Colour', each(function (d) { d.mats.forEach(function (m) { m.c = hex; if (m.ei > 0) m.e = hex; }); }), false);
}
function setFinish(f) { F.finish = f; edit({ matte: 'Matte', glow: 'Glow', glass: 'Glass' }[f], each(function (d) { applyFinish(d, f); }), false); }
var CLAY = {
  bendp: ['Bend +', each(function (d) { d.bend = clamp(d.bend + 0.5, -3, 3); })],
  bendm: ['Bend \u2212', each(function (d) { d.bend = clamp(d.bend - 0.5, -3, 3); })],
  unbend: ['Straighten', each(function (d) { d.bend = 0; })],
  stretch: ['Stretch', each(function (d) { d.s[1] *= 1.25; })],
  flatten: ['Flatten', each(function (d) { d.s[1] *= 0.8; })],
  pinch: ['Pinch', each(function (d) { d.s[0] *= 0.85; d.s[2] *= 0.85; })],
  inflate: ['Inflate', scaleBy(1.15)],
  flip: ['Flip', each(function (d) { d.s[0] *= -1; })],
  tip: ['Tip 90\u00b0', tip(90)],
  hollow: ['Hollow', function (ds) { var w = ds[0].mats[0].w ? 0 : 1; ds.forEach(function (d) { d.mats.forEach(function (m) { m.w = w; }); }); }],
  sharp: ['Sharp / smooth', function (ds) { var f = ds[0].mats[0].f ? 0 : 1; ds.forEach(function (d) { d.mats.forEach(function (m) { m.f = f; }); }); }],
  upright: ['Upright', each(function (d) { var q = d.q, yaw = Math.atan2(2 * (q[3] * q[1] + q[0] * q[2]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])); d.q = C.qyaw(Math.round(yaw / (Math.PI / 12)) * Math.PI / 12); })]
};
function duplicate() {
  if (!F.sel) return say('Select a piece (or ☰ → WHOLE) to copy it.', true);
  var ts = targets(), ds = ts.map(functio