<!doctype html>
<html><head><meta charset="utf-8"><title>step2glb — thumb playground</title>
<style>
  :root { color-scheme: dark; }
  * { box-sizing: border-box; }
  body { margin: 0; background: #0d1117; color: #c9d1d9; font: 13px/1.5 system-ui, -apple-system, sans-serif; min-height: 100vh; }
  header { padding: 14px 24px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 14px; }
  h1 { margin: 0; font-size: 16px; color: #e6edf3; font-weight: 600; }
  .pill { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 10px; letter-spacing: 0.04em; background: rgba(0,184,176,0.15); color: #00e6dc; border: 1px solid rgba(0,184,176,0.3); font-family: ui-monospace, monospace; }
  .meta { color: #7d8590; font-size: 11px; font-family: ui-monospace, monospace; margin-left: auto; }

  .drop {
    margin: 24px;
    padding: 60px 24px;
    border: 2px dashed #30363d;
    border-radius: 8px;
    background: #161b22;
    text-align: center;
    color: #8b949e;
    transition: border-color 0.15s, background 0.15s;
    cursor: pointer;
  }
  .drop.drag { border-color: #00b8b0; background: rgba(0,184,176,0.05); }
  .drop strong { color: #e6edf3; font-size: 16px; display: block; margin-bottom: 8px; }
  .drop input { display: none; }
  .drop .hint { font-size: 11px; color: #7d8590; margin-top: 12px; }

  .status { margin: 16px 24px; padding: 12px 16px; background: #161b22; border: 1px solid #30363d; border-radius: 6px; font-family: ui-monospace, monospace; font-size: 12px; display: none; }
  .status.show { display: block; }
  .status.err { border-color: rgba(248,81,73,0.4); color: #f85149; }
  .status.ok { color: #3fb950; }

  main { padding: 0 24px 24px; }
  section.chip { padding: 12px 0; }
  h2 { margin: 0 0 10px; font-size: 14px; color: #e6b450; font-family: ui-monospace, monospace; }
  /* Cells size to the chosen PNG dimensions so picking sm / md / lg
     actually shows the tile at its real rendered size — that's the
     point of the toggle. The grid wraps so lg lays out 1-2 per row,
     md 3-4, sm 6-8. */
  .row { display: grid; gap: 10px; }
  .row.sm { grid-template-columns: repeat(auto-fit, minmax(160px, max-content)); }
  .row.md { grid-template-columns: repeat(auto-fit, minmax(320px, max-content)); }
  .row.lg { grid-template-columns: repeat(auto-fit, minmax(600px, max-content)); }
  .cell { background: #161b22; border: 1px solid #30363d; border-radius: 4px; display: flex; flex-direction: column; cursor: pointer; transition: border-color 0.1s; }
  .cell:hover { border-color: #00b8b0; }
  .cell .label { padding: 4px 8px; font-size: 11px; color: #79c0ff; font-family: ui-monospace, monospace; border-bottom: 1px solid #30363d; background: #0d1117; display: flex; justify-content: space-between; align-items: center; }
  .cell .label .dim { color: #7d8590; font-size: 10px; }
  .cell .frame { padding: 6px; background: #000; display: flex; align-items: center; justify-content: center; }
  /* Render the PNG at its natural pixel dimensions; CSS doesn't shrink
     a 160×120 thumb up to 320×240 so the user actually sees the size
     difference. */
  .cell img { display: block; width: auto; height: auto; }
  .empty { color: #7d8590; font-style: italic; font-size: 11px; padding: 60px 0; }

  .sizes-toggle { margin: 8px 0 14px; display: inline-flex; gap: 4px; }
  .sizes-toggle button { background: #161b22; border: 1px solid #30363d; color: #8b949e; padding: 4px 10px; font-size: 11px; font-family: ui-monospace, monospace; border-radius: 4px; cursor: pointer; }
  .sizes-toggle button.active { background: rgba(0,184,176,0.15); border-color: rgba(0,184,176,0.3); color: #00e6dc; }
</style></head><body>
<header>
  <h1>step2glb thumb playground</h1>
  <span class="pill">/thumbnail-batch</span>
  <span class="meta" id="meta">drop a .step file to render 6 orientations × 3 sizes in one shot</span>
</header>

<div class="drop" id="drop">
  <strong>Drop a .step / .stp file here</strong>
  <span>or click to choose</span>
  <div class="hint">renders all 6 face-up orientations × 3 sizes (sm / md / lg) in a single service call</div>
  <input type="file" id="file" accept=".step,.stp">
</div>

<div class="status" id="status"></div>

<main id="main"></main>

<script>
const drop = document.getElementById('drop');
const file = document.getElementById('file');
const status = document.getElementById('status');
const main = document.getElementById('main');
const meta = document.getElementById('meta');

let selectedSize = 'md';

drop.addEventListener('click', () => file.click());
file.addEventListener('change', (e) => { if (e.target.files[0]) handleFile(e.target.files[0]); });
drop.addEventListener('dragover', (e) => { e.preventDefault(); drop.classList.add('drag'); });
drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
drop.addEventListener('drop', (e) => {
  e.preventDefault(); drop.classList.remove('drag');
  if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
});

async function handleFile(f) {
  setStatus(`rendering ${f.name} (${(f.size/1024).toFixed(1)} KB)…`, '');
  main.innerHTML = '';
  try {
    const buf = await f.arrayBuffer();
    const t0 = performance.now();
    const r = await fetch('render', { method: 'POST', body: buf });
    const json = await r.json();
    const wallMs = Math.round(performance.now() - t0);
    if (!json.ok) {
      setStatus(`error: ${json.error || 'render failed'}`, 'err');
      return;
    }
    render(f.name, json, wallMs);
    setStatus(
      `${json.thumbnailCount} thumbnails · service ${json.totalDurationMs} ms (load ${json.loadDurationMs} ms) · wall ${wallMs} ms`,
      'ok'
    );
  } catch (e) {
    setStatus(`error: ${e.message}`, 'err');
  }
}

function setStatus(text, kind) {
  status.textContent = text;
  status.className = 'status show' + (kind ? ' ' + kind : '');
}

function render(filename, json, wallMs) {
  const stem = filename.replace(/\.(step|stp)$/i, '');
  const orients = ['asIs', 'zDown', 'yUp', 'yDown', 'xUp', 'xDown'];
  const sizes = ['sm', 'md', 'lg'];

  // Group thumbnails by size for size-toggled view
  const byOrientSize = {};
  for (const t of json.thumbnails) {
    byOrientSize[t.upAxis + '/' + t.size] = t;
  }

  main.innerHTML = '';

  // Size toggles
  const sizesToggle = document.createElement('div');
  sizesToggle.className = 'sizes-toggle';
  sizesToggle.innerHTML = sizes.map(s =>
    `<button data-size="${s}" class="${s === selectedSize ? 'active' : ''}">${s}</button>`
  ).join('');
  sizesToggle.addEventListener('click', (e) => {
    if (e.target.tagName === 'BUTTON') {
      selectedSize = e.target.dataset.size;
      render(filename, json, wallMs);
    }
  });
  main.appendChild(sizesToggle);

  // Chip section: all 6 orientations at the chosen size
  const sec = document.createElement('section');
  sec.className = 'chip';
  const h2 = document.createElement('h2');
  h2.textContent = `${stem} — ${selectedSize}`;
  sec.appendChild(h2);
  const row = document.createElement('div');
  row.className = 'row ' + selectedSize;
  for (const orient of orients) {
    const t = byOrientSize[orient + '/' + selectedSize];
    const cell = document.createElement('div');
    cell.className = 'cell';
    const label = document.createElement('div');
    label.className = 'label';
    label.innerHTML = `${orient}<span class="dim">${t ? t.width + '×' + t.height : '—'}</span>`;
    cell.appendChild(label);
    const frame = document.createElement('div');
    frame.className = 'frame';
    if (t) {
      const img = document.createElement('img');
      img.src = 'data:image/png;base64,' + t.dataB64;
      img.alt = `${stem} ${orient}`;
      img.title = `${stem}-3d-iso${suffix(orient)}${sizeSuffix(t.size)}.png — click to download`;
      img.onclick = () => download(`${stem}-3d-iso${suffix(orient)}${sizeSuffix(t.size)}.png`, t.dataB64);
      frame.appendChild(img);
    } else {
      frame.innerHTML = '<span class="empty">missing</span>';
    }
    cell.appendChild(frame);
    row.appendChild(cell);
  }
  sec.appendChild(row);
  main.appendChild(sec);

  // bbox info
  if (json.bboxMm) {
    const bb = json.bboxMm;
    const dx = (bb[0][1] - bb[0][0]).toFixed(2);
    const dy = (bb[1][1] - bb[1][0]).toFixed(2);
    const dz = (bb[2][1] - bb[2][0]).toFixed(2);
    // 6 orientations × 3 sizes = 18 PNGs returned, 6 shown at a time
    meta.textContent = `bbox ${dx} × ${dy} × ${dz} mm — pose=${json.pose} — 6 orientations × 3 sizes (showing ${selectedSize})`;
  }

  if (json.renderWarnings && json.renderWarnings.length > 0) {
    const warn = document.createElement('div');
    warn.className = 'status show err';
    warn.style.margin = '12px 0';
    warn.innerHTML = '<strong>render warnings:</strong><br>' + json.renderWarnings.map(w => `• ${w}`).join('<br>');
    main.appendChild(warn);
  }
}

function suffix(orient) {
  return { asIs:'', zDown:'-zdown', yUp:'-yup', yDown:'-ydown', xUp:'-xup', xDown:'-xdown' }[orient] || '';
}
function sizeSuffix(size) {
  return { md:'', sm:'-sm', lg:'-lg' }[size] || '';
}

function download(filename, b64) {
  const a = document.createElement('a');
  a.href = 'data:image/png;base64,' + b64;
  a.download = filename;
  a.click();
}
</script>
</body></html>