Fix input, inspector, and responsive UI issues

#1
Files changed (1) hide show
  1. app.py +155 -43
app.py CHANGED
@@ -318,16 +318,13 @@ INDEX_HTML = r"""<!doctype html>
318
  /* ─────────────── Workspace ─────────────── */
319
  .workspace {
320
  display: grid;
321
- grid-template-columns: 240px 1fr 280px;
322
  gap: 1px;
323
  background: var(--border);
324
  min-height: 0;
325
  overflow: hidden;
326
  }
327
- @media (max-width: 1100px) { .workspace { grid-template-columns: 220px 1fr 240px; } }
328
- @media (max-width: 900px) {
329
- .workspace { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
330
- }
331
 
332
  .col {
333
  display: flex; flex-direction: column;
@@ -376,6 +373,9 @@ INDEX_HTML = r"""<!doctype html>
376
  min-height: 0;
377
  }
378
  .panel-body.tight { padding: 8px; }
 
 
 
379
 
380
  /* ─────────────── Drop zone ─────────────── */
381
  .drop {
@@ -390,11 +390,12 @@ INDEX_HTML = r"""<!doctype html>
390
  transition: border-color 150ms ease, background 150ms ease;
391
  }
392
  .drop.dragover { border-color: var(--accent); background: var(--accent-soft); }
393
- .drop.has-image { border-style: solid; border-color: var(--border); cursor: default; }
 
 
394
  .drop input[type="file"] {
395
- position: absolute; inset: 0; opacity: 0; cursor: pointer;
396
  }
397
- .drop.has-image input[type="file"] { pointer-events: none; }
398
  .drop-empty {
399
  display: flex; flex-direction: column; align-items: center; gap: 8px;
400
  color: var(--muted); text-align: center; padding: 16px;
@@ -410,9 +411,29 @@ INDEX_HTML = r"""<!doctype html>
410
  .drop-empty strong { color: var(--text); font-weight: 600; font-size: 12px; }
411
  .drop-empty span { font-size: 11px; color: var(--muted); }
412
  .drop-preview {
413
- width: 100%; height: 100%; object-fit: contain;
 
414
  background: #0a0d10; display: block;
 
415
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  .drop-meta {
417
  margin-top: 10px;
418
  display: grid; grid-template-columns: auto 1fr; gap: 4px 10px;
@@ -613,6 +634,7 @@ INDEX_HTML = r"""<!doctype html>
613
  }
614
  .section.collapsed .chev { transform: rotate(-90deg); }
615
  .section.collapsed .section-body { display: none; }
 
616
  .section-body { padding: 10px 12px; }
617
 
618
  /* Scene tree */
@@ -682,17 +704,52 @@ INDEX_HTML = r"""<!doctype html>
682
  border: 1px solid var(--border);
683
  border-radius: var(--radius-sm);
684
  overflow: hidden;
685
- cursor: pointer;
686
  background: var(--surface-2);
687
  transition: border-color 120ms ease, transform 120ms ease;
688
  }
689
  .ex-thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
 
 
690
  .ex-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
691
- .ex-thumb .ex-label {
692
- position: absolute; inset: auto 0 0 0;
693
- padding: 14px 6px 4px;
694
- background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
695
- color: #fff; font-size: 10px; font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  }
697
 
698
  /* ─────────────── Status bar ─────────────── */
@@ -786,7 +843,7 @@ INDEX_HTML = r"""<!doctype html>
786
  <main class="workspace">
787
  <!-- LEFT: Input -->
788
  <section class="col">
789
- <div class="panel">
790
  <div class="panel-head">
791
  <div class="bar"></div>
792
  <h3>Input</h3>
@@ -794,7 +851,7 @@ INDEX_HTML = r"""<!doctype html>
794
  </div>
795
  <div class="panel-body">
796
  <label class="drop" id="drop">
797
- <input type="file" id="file" accept="image/*" />
798
  <div class="drop-empty" id="dropEmpty">
799
  <div class="icon">
800
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -807,6 +864,14 @@ INDEX_HTML = r"""<!doctype html>
807
  <span>JPG · PNG · WebP</span>
808
  </div>
809
  <img class="drop-preview hidden" id="preview" alt="" />
 
 
 
 
 
 
 
 
810
  </label>
811
  <dl class="drop-meta">
812
  <dt>FILE</dt><dd class="mono" id="inputFileName">—</dd>
@@ -815,6 +880,16 @@ INDEX_HTML = r"""<!doctype html>
815
  </dl>
816
  </div>
817
  </div>
 
 
 
 
 
 
 
 
 
 
818
  </section>
819
 
820
  <!-- CENTER: Viewport -->
@@ -951,21 +1026,10 @@ INDEX_HTML = r"""<!doctype html>
951
  <h4>Pipeline log</h4>
952
  <span class="chev">▾</span>
953
  </div>
954
- <div class="log mono" id="log">
955
  <div class="log-empty">ready · awaiting input</div>
956
  </div>
957
  </div>
958
-
959
- <div class="section">
960
- <div class="section-head" data-toggle>
961
- <h4>Examples</h4>
962
- <span class="meta mono" id="examplesCount">—</span>
963
- <span class="chev">▾</span>
964
- </div>
965
- <div class="section-body">
966
- <div class="examples-grid" id="gallery"></div>
967
- </div>
968
- </div>
969
  </aside>
970
  </main>
971
 
@@ -1028,11 +1092,12 @@ INDEX_HTML = r"""<!doctype html>
1028
 
1029
  function logLine(stage, kind, msg) {
1030
  const time = new Date().toLocaleTimeString("en-US", { hour12: false });
 
1031
  const line = document.createElement("div");
1032
  line.className = "log-line";
1033
  line.innerHTML = `
1034
  <span class="log-time">${time}</span>
1035
- <span class="log-stage ${kind}">${stage}</span>
1036
  <span class="log-msg">${msg}</span>
1037
  `;
1038
  // Remove empty placeholder
@@ -1215,12 +1280,25 @@ INDEX_HTML = r"""<!doctype html>
1215
  return s.toFixed(2) + "s";
1216
  }
1217
 
 
 
 
 
 
 
 
 
 
 
 
 
1218
  fileInput.addEventListener("change", (e) => {
1219
  const f = e.target.files && e.target.files[0];
1220
  if (f) {
1221
  pendingFile = f;
1222
  showPreview(f);
1223
  setReady(true);
 
1224
  }
1225
  });
1226
 
@@ -1230,7 +1308,17 @@ INDEX_HTML = r"""<!doctype html>
1230
  ["dragleave", "drop"].forEach(ev =>
1231
  drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove("dragover"); })
1232
  );
1233
- drop.addEventListener("drop", (e) => {
 
 
 
 
 
 
 
 
 
 
1234
  const f = e.dataTransfer.files && e.dataTransfer.files[0];
1235
  if (f && f.type.startsWith("image/")) {
1236
  pendingFile = f;
@@ -1241,8 +1329,20 @@ INDEX_HTML = r"""<!doctype html>
1241
 
1242
  // Collapsible sections
1243
  document.querySelectorAll(".section-head[data-toggle]").forEach((head) => {
 
 
 
 
 
 
 
1244
  head.addEventListener("click", () => {
1245
- head.parentElement.classList.toggle("collapsed");
 
 
 
 
 
1246
  });
1247
  });
1248
 
@@ -1397,28 +1497,40 @@ INDEX_HTML = r"""<!doctype html>
1397
  { label: "Animate", path: "/__examples/animate_room.jpg" },
1398
  { label: "My bedroom", path: "/__examples/my_bedroom.JPG" },
1399
  { label: "Cave", path: "/__examples/cave_ai.jpg" },
 
 
 
 
 
 
 
 
1400
  ];
1401
  gallery.innerHTML = EXAMPLES.map((ex) => `
1402
- <div class="ex-thumb" data-path="${ex.path}" title="${ex.label}">
1403
- <img loading="lazy" src="${ex.path}" alt="${ex.label}" />
1404
- <div class="ex-label">${ex.label}</div>
1405
  </div>
1406
  `).join("");
1407
  examplesCount.textContent = `${EXAMPLES.length}`;
1408
 
 
 
 
 
 
 
 
 
 
 
 
 
1409
  gallery.addEventListener("click", async (e) => {
1410
  const t = e.target.closest(".ex-thumb");
1411
  if (!t) return;
1412
  const path = t.dataset.path;
1413
  try {
1414
- const resp = await fetch(path);
1415
- const blob = await resp.blob();
1416
- const f = new File([blob], path.split("/").pop(), { type: blob.type });
1417
- pendingFile = f;
1418
- showPreview(f);
1419
- setReady(true);
1420
- logLine("input", "info", `loaded example: ${path.split("/").pop()}`);
1421
- toastMsg("Example loaded");
1422
  } catch (err) {
1423
  toastMsg("Failed to load example", true);
1424
  logLine("input", "err", err.message || String(err));
 
318
  /* ─────────────── Workspace ─────────────── */
319
  .workspace {
320
  display: grid;
321
+ grid-template-columns: 300px 1fr 280px;
322
  gap: 1px;
323
  background: var(--border);
324
  min-height: 0;
325
  overflow: hidden;
326
  }
327
+ @media (max-width: 1100px) { .workspace { grid-template-columns: 280px 1fr 240px; } }
 
 
 
328
 
329
  .col {
330
  display: flex; flex-direction: column;
 
373
  min-height: 0;
374
  }
375
  .panel-body.tight { padding: 8px; }
376
+ .input-panel { flex: 0 0 auto; }
377
+ .examples-panel { flex: 1 1 auto; }
378
+ .examples-panel .panel-body { overflow: auto; }
379
 
380
  /* ─────────────── Drop zone ─────────────── */
381
  .drop {
 
390
  transition: border-color 150ms ease, background 150ms ease;
391
  }
392
  .drop.dragover { border-color: var(--accent); background: var(--accent-soft); }
393
+ .drop.has-image { border-style: solid; border-color: var(--border); cursor: pointer; }
394
+ .drop.has-image:hover,
395
+ .drop.has-image:focus-within { border-color: var(--accent); }
396
  .drop input[type="file"] {
397
+ position: absolute; inset: 0; z-index: 3; opacity: 0; cursor: pointer;
398
  }
 
399
  .drop-empty {
400
  display: flex; flex-direction: column; align-items: center; gap: 8px;
401
  color: var(--muted); text-align: center; padding: 16px;
 
411
  .drop-empty strong { color: var(--text); font-weight: 600; font-size: 12px; }
412
  .drop-empty span { font-size: 11px; color: var(--muted); }
413
  .drop-preview {
414
+ width: 100%; height: 100%; object-fit: cover;
415
+ object-position: center center;
416
  background: #0a0d10; display: block;
417
+ transition: opacity 150ms ease;
418
  }
419
+ .drop.has-image:hover .drop-preview,
420
+ .drop.has-image:focus-within .drop-preview { opacity: 0.78; }
421
+ .drop-replace {
422
+ position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 2;
423
+ min-height: 30px;
424
+ display: flex; align-items: center; justify-content: center; gap: 6px;
425
+ padding: 0 10px;
426
+ border: 1px solid rgba(255, 255, 255, 0.18);
427
+ border-radius: var(--radius-sm);
428
+ background: rgba(10, 13, 16, 0.84);
429
+ color: var(--text);
430
+ font-size: 11px; font-weight: 600;
431
+ pointer-events: none;
432
+ opacity: 0; transform: translateY(4px);
433
+ transition: opacity 150ms ease, transform 150ms ease;
434
+ }
435
+ .drop.has-image .drop-replace { opacity: 1; transform: translateY(0); }
436
+ .drop-replace svg { width: 13px; height: 13px; color: var(--accent); }
437
  .drop-meta {
438
  margin-top: 10px;
439
  display: grid; grid-template-columns: auto 1fr; gap: 4px 10px;
 
634
  }
635
  .section.collapsed .chev { transform: rotate(-90deg); }
636
  .section.collapsed .section-body { display: none; }
637
+ .section:last-child.collapsed { flex: 0 0 auto; min-height: auto; }
638
  .section-body { padding: 10px 12px; }
639
 
640
  /* Scene tree */
 
704
  border: 1px solid var(--border);
705
  border-radius: var(--radius-sm);
706
  overflow: hidden;
707
+ cursor: grab;
708
  background: var(--surface-2);
709
  transition: border-color 120ms ease, transform 120ms ease;
710
  }
711
  .ex-thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
712
+ .ex-thumb:active { cursor: grabbing; }
713
+ .ex-thumb.dragging { border-color: var(--accent); opacity: 0.55; }
714
  .ex-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
715
+
716
+ @media (max-width: 900px) {
717
+ body { overflow: auto; }
718
+ .app {
719
+ height: auto;
720
+ min-height: 100vh;
721
+ grid-template-rows: auto auto 24px;
722
+ }
723
+ .workspace {
724
+ display: flex;
725
+ flex-direction: column;
726
+ overflow: visible;
727
+ }
728
+ .col { overflow: visible; }
729
+ .input-panel, .examples-panel { flex: none; }
730
+ .examples-panel .panel-body { max-height: 360px; }
731
+ .examples-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
732
+ .viewport { height: max(420px, 60vh); }
733
+ .inspector, .section:last-child { flex: none; }
734
+ .log { max-height: 240px; }
735
+ }
736
+ @media (max-width: 640px) {
737
+ .toolbar {
738
+ grid-template-columns: minmax(0, 1fr) auto;
739
+ grid-template-rows: 34px 28px;
740
+ gap: 0 8px;
741
+ padding: 4px 8px;
742
+ }
743
+ .tb-brand { grid-column: 1; grid-row: 1; border-right: 0; }
744
+ .tb-scene { grid-column: 1 / -1; grid-row: 2; }
745
+ .toolbar > .tb-actions:nth-of-type(3) { grid-column: 2; grid-row: 1; }
746
+ .toolbar > .tb-actions:last-child { display: none; }
747
+ .tb-shortcut { display: none; }
748
+ .examples-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
749
+ .statusbar { gap: 12px; overflow-x: auto; }
750
+ }
751
+ @media (max-width: 420px) {
752
+ .examples-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
753
  }
754
 
755
  /* ─────────────── Status bar ─────────────── */
 
843
  <main class="workspace">
844
  <!-- LEFT: Input -->
845
  <section class="col">
846
+ <div class="panel input-panel">
847
  <div class="panel-head">
848
  <div class="bar"></div>
849
  <h3>Input</h3>
 
851
  </div>
852
  <div class="panel-body">
853
  <label class="drop" id="drop">
854
+ <input type="file" id="file" accept="image/*" aria-label="Upload or replace input image" title="Upload or replace input image" />
855
  <div class="drop-empty" id="dropEmpty">
856
  <div class="icon">
857
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 
864
  <span>JPG · PNG · WebP</span>
865
  </div>
866
  <img class="drop-preview hidden" id="preview" alt="" />
867
+ <span class="drop-replace" aria-hidden="true">
868
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
869
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
870
+ <polyline points="17 8 12 3 7 8"/>
871
+ <line x1="12" y1="3" x2="12" y2="15"/>
872
+ </svg>
873
+ <span>Replace image</span>
874
+ </span>
875
  </label>
876
  <dl class="drop-meta">
877
  <dt>FILE</dt><dd class="mono" id="inputFileName">—</dd>
 
880
  </dl>
881
  </div>
882
  </div>
883
+ <div class="panel examples-panel">
884
+ <div class="panel-head">
885
+ <div class="bar"></div>
886
+ <h3>Example images</h3>
887
+ <span class="meta mono" id="examplesCount">—</span>
888
+ </div>
889
+ <div class="panel-body tight">
890
+ <div class="examples-grid" id="gallery"></div>
891
+ </div>
892
+ </div>
893
  </section>
894
 
895
  <!-- CENTER: Viewport -->
 
1026
  <h4>Pipeline log</h4>
1027
  <span class="chev">▾</span>
1028
  </div>
1029
+ <div class="section-body log mono" id="log">
1030
  <div class="log-empty">ready · awaiting input</div>
1031
  </div>
1032
  </div>
 
 
 
 
 
 
 
 
 
 
 
1033
  </aside>
1034
  </main>
1035
 
 
1092
 
1093
  function logLine(stage, kind, msg) {
1094
  const time = new Date().toLocaleTimeString("en-US", { hour12: false });
1095
+ const stageLabel = stage === "reconstruct" ? "recon." : stage;
1096
  const line = document.createElement("div");
1097
  line.className = "log-line";
1098
  line.innerHTML = `
1099
  <span class="log-time">${time}</span>
1100
+ <span class="log-stage ${kind}">${stageLabel}</span>
1101
  <span class="log-msg">${msg}</span>
1102
  `;
1103
  // Remove empty placeholder
 
1280
  return s.toFixed(2) + "s";
1281
  }
1282
 
1283
+ async function loadExample(path) {
1284
+ const resp = await fetch(path);
1285
+ if (!resp.ok) throw new Error(`Example request failed (${resp.status})`);
1286
+ const blob = await resp.blob();
1287
+ const f = new File([blob], path.split("/").pop(), { type: blob.type });
1288
+ pendingFile = f;
1289
+ showPreview(f);
1290
+ setReady(true);
1291
+ logLine("input", "info", `loaded example: ${f.name}`);
1292
+ toastMsg("Example loaded");
1293
+ }
1294
+
1295
  fileInput.addEventListener("change", (e) => {
1296
  const f = e.target.files && e.target.files[0];
1297
  if (f) {
1298
  pendingFile = f;
1299
  showPreview(f);
1300
  setReady(true);
1301
+ e.target.value = "";
1302
  }
1303
  });
1304
 
 
1308
  ["dragleave", "drop"].forEach(ev =>
1309
  drop.addEventListener(ev, (e) => { e.preventDefault(); drop.classList.remove("dragover"); })
1310
  );
1311
+ drop.addEventListener("drop", async (e) => {
1312
+ const examplePath = e.dataTransfer.getData("application/x-infinisplat-example");
1313
+ if (examplePath) {
1314
+ try {
1315
+ await loadExample(examplePath);
1316
+ } catch (err) {
1317
+ toastMsg("Failed to load example", true);
1318
+ logLine("input", "err", err.message || String(err));
1319
+ }
1320
+ return;
1321
+ }
1322
  const f = e.dataTransfer.files && e.dataTransfer.files[0];
1323
  if (f && f.type.startsWith("image/")) {
1324
  pendingFile = f;
 
1329
 
1330
  // Collapsible sections
1331
  document.querySelectorAll(".section-head[data-toggle]").forEach((head) => {
1332
+ head.setAttribute("role", "button");
1333
+ head.setAttribute("tabindex", "0");
1334
+ head.setAttribute("aria-expanded", "true");
1335
+ const toggleSection = () => {
1336
+ const collapsed = head.parentElement.classList.toggle("collapsed");
1337
+ head.setAttribute("aria-expanded", String(!collapsed));
1338
+ };
1339
  head.addEventListener("click", () => {
1340
+ toggleSection();
1341
+ });
1342
+ head.addEventListener("keydown", (e) => {
1343
+ if (e.key !== "Enter" && e.key !== " ") return;
1344
+ e.preventDefault();
1345
+ toggleSection();
1346
  });
1347
  });
1348
 
 
1497
  { label: "Animate", path: "/__examples/animate_room.jpg" },
1498
  { label: "My bedroom", path: "/__examples/my_bedroom.JPG" },
1499
  { label: "Cave", path: "/__examples/cave_ai.jpg" },
1500
+ { label: "Old living", path: "/__examples/old_livingroom.png" },
1501
+ { label: "Courtyard", path: "/__examples/eth3d_courtyard.png" },
1502
+ { label: "Ghibli real", path: "/__examples/ghibli_realroom.jpg" },
1503
+ { label: "Flower room", path: "/__examples/flower_room.jpg" },
1504
+ { label: "Beggar home", path: "/__examples/beggar_home.jpg" },
1505
+ { label: "ScanNet++", path: "/__examples/scannetpp_fe94fc30cf.JPG" },
1506
+ { label: "Masi room", path: "/__examples/pexels-masi.jpg" },
1507
+ { label: "Maksim room", path: "/__examples/maksim-shutov-unsplash.jpg" },
1508
  ];
1509
  gallery.innerHTML = EXAMPLES.map((ex) => `
1510
+ <div class="ex-thumb" data-path="${ex.path}" title="${ex.label}" draggable="true">
1511
+ <img loading="lazy" src="${ex.path}" alt="${ex.label}" draggable="false" />
 
1512
  </div>
1513
  `).join("");
1514
  examplesCount.textContent = `${EXAMPLES.length}`;
1515
 
1516
+ gallery.addEventListener("dragstart", (e) => {
1517
+ const t = e.target.closest(".ex-thumb");
1518
+ if (!t || !e.dataTransfer) return;
1519
+ e.dataTransfer.effectAllowed = "copy";
1520
+ e.dataTransfer.setData("application/x-infinisplat-example", t.dataset.path);
1521
+ e.dataTransfer.setData("text/uri-list", new URL(t.dataset.path, window.location.href).href);
1522
+ t.classList.add("dragging");
1523
+ });
1524
+ gallery.addEventListener("dragend", (e) => {
1525
+ e.target.closest(".ex-thumb")?.classList.remove("dragging");
1526
+ });
1527
+
1528
  gallery.addEventListener("click", async (e) => {
1529
  const t = e.target.closest(".ex-thumb");
1530
  if (!t) return;
1531
  const path = t.dataset.path;
1532
  try {
1533
+ await loadExample(path);
 
 
 
 
 
 
 
1534
  } catch (err) {
1535
  toastMsg("Failed to load example", true);
1536
  logLine("input", "err", err.message || String(err));