Spaces:
Running
Running
| # Annotated-Photo Fallback: Image Sizing and Annotation Accuracy | |
| Review date: 2026-06-14. Scope: one user report β when the **annotated-photo | |
| fallback** triggers (instead of the 3D cutaway), the uploaded image is "too | |
| large to display," and the annotation placement should be confirmed accurate. | |
| This document is findings + next steps for Codex to implement. It does **not** | |
| authorize any deployment or Hugging Face changes on its own; follow the normal | |
| GitHub β HF sync and verification flow in `AGENTS.md`. All file/line references | |
| are to `index.html` unless noted. | |
| --- | |
| ## TL;DR | |
| 1. **The image-sizing bug is real and confirmed in a browser.** The | |
| annotated-photo image renders at its **intrinsic aspect height at full panel | |
| width**, ignoring the viewport height, so tall photos overflow the viewport | |
| by ~2x. `object-fit: contain` never engages because the box it should fit | |
| inside is itself oversized. | |
| 2. **Root cause:** nested `display: grid; place-items: center` containers | |
| (`.annotated-stage` β `.annotation-frame`) with auto-sized rows. A child's | |
| `height: 100%` resolves against an *auto* (content-sized) grid row, not the | |
| container's pixel height, so it collapses back to the image's natural size. | |
| 3. **The annotation accuracy is mostly broken *as a side effect* of the sizing | |
| bug** β labels are positioned against the oversized, overflowing image box, | |
| so any callout in the lower half of a tall photo lands below the visible | |
| viewport and is clipped. Fixing the sizing fixes most of the misplacement. | |
| A few annotation-accuracy improvements remain that are independent of sizing | |
| (no marker dot at the point, edge-clamp detaches labels, unused `box`). | |
| --- | |
| ## How this was verified (not just code inspection) | |
| I reproduced the exact `buildAnnotatedPhoto` DOM (`index.html:996-1076`) and the | |
| exact CSS rules (`.fallback-stage`/`.annotated-stage`/`.annotation-frame`/ | |
| `.annotation-image`, `index.html:466-511`) in a standalone page, loaded a | |
| **3000 Γ 4000 portrait** test image into a **900 Γ 600** viewport pane, and | |
| measured the rendered geometry in a real browser (Chromium via Playwright): | |
| | Scenario | Image rendered (WΓH) | Fits 600px-tall pane? | | |
| | --- | --- | --- | | |
| | **Current code (baseline)** | **864 Γ 1151** | β overflows (image bottom at 1209px) | | |
| | Fix: pin image absolutely to padded box | 864 Γ 498 | β contained (top 58 β bottom 556) | | |
| | Fix: definite `minmax(0,1fr)` tracks at **both** grid levels | 864 Γ 498 | β contained | | |
| `864 = 900 β 18pxΓ2` (panel width minus horizontal padding); `1151 β 864 Γ | |
| (4000/3000)` β i.e. the image is laid out at panel width and natural aspect, | |
| with **zero** vertical constraint. That is the "too large to display" symptom. | |
| (The repro page was temporary and has been removed; re-create it if you want to | |
| re-measure.) | |
| --- | |
| ## Root cause (precise) | |
| The fallback markup is three nested boxes: | |
| ``` | |
| #viewport (absolute, inset:0) <- definite size, good | |
| ββ .fallback-stage.annotated-stage index.html:1011-1012 | |
| position:absolute; inset:0 (definite height β) | |
| display:grid; place-items:center; padding:58/18/44 index.html:490-495 | |
| ββ .annotation-frame index.html:1013-1014 | |
| width:100%; height:100% index.html:497-503 | |
| display:grid; place-items:center <-- second nested grid | |
| ββ img.annotation-image index.html:1015-1018 | |
| β width:100%; height:100%; object-fit:contain index.html:505-511 | |
| ββ .label-layer (absolute, inset:0) | |
| ``` | |
| Two compounding CSS facts: | |
| - **`place-items: center` sets `align-items: center`, not `stretch`.** Combined | |
| with the default `grid-auto-rows: auto`, the single grid row is **content | |
| sized**, not stretched to the container's definite height. | |
| - **A percentage `height` resolves against the grid *area* (the track), and an | |
| `auto` track is *indefinite*.** So `height: 100%` on a child of a | |
| `place-items: center` grid computes as `auto`. | |
| This happens **twice**: | |
| 1. `.annotation-frame { height: 100% }` inside `.annotated-stage` β | |
| the frame's track is auto β frame height becomes auto. | |
| 2. `img.annotation-image { height: 100% }` inside `.annotation-frame` β | |
| the image's track is auto β image height becomes auto β the `<img>` uses its | |
| intrinsic aspect ratio at the available width (864px) β 1151px tall. | |
| Measured proof that the bug is the **inner** grid too: even after forcing | |
| `.annotation-frame` to a correct definite 498px box, the image still rendered | |
| 1151px, because its `height:100%` resolves against `.annotation-frame`'s *inner | |
| auto row*, not the frame's element box. Both grid levels must be fixed (or the | |
| nested-grid sizing dropped entirely). | |
| > Note: `.source-image` in the side panel (`index.html:384-390`) does **not** | |
| > have this bug because it has a fixed `height: 180px` β a definite height, so | |
| > `object-fit: contain` works there. The viewport fallback wants to fill | |
| > available space rather than a fixed height, which is why it hit the trap. | |
| --- | |
| ## Fix options (both verified to contain the image at 864 Γ 498) | |
| Pick one. **Option A is recommended** β fewest moving parts, no dependence on | |
| fragile nested-grid percentage resolution. | |
| ### Option A (recommended): pin the image to the padded content box | |
| Stop relying on `height: 100%` chaining through two grids. Make the image fill a | |
| single absolutely-positioned box and let `object-fit: contain` do the framing. | |
| ```css | |
| /* .annotated-stage can keep its background; drop the grid centering */ | |
| .annotation-frame { | |
| position: absolute; | |
| inset: 58px 18px 44px; /* same as the old .annotated-stage padding */ | |
| /* remove width/height:100% + display:grid + place-items:center */ | |
| } | |
| .annotation-image { | |
| position: absolute; | |
| inset: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; /* now fits a definite box -> letterboxed, contained */ | |
| } | |
| ``` | |
| `.label-layer` already is `position:absolute; inset:0` (`index.html:466-471`), | |
| so it keeps lining up with the frame. The `58/18/44` padding currently lives on | |
| `.annotated-stage` (`index.html:493`); move that inset onto `.annotation-frame` | |
| (or keep the padding on the stage and set `.annotation-frame { position:absolute; | |
| inset:0 }`). Either way the frame becomes a **definite** box and the image is | |
| contained. | |
| ### Option B: keep the grids but make every track definite | |
| If you prefer to keep `display:grid` centering, the breakage is the auto rows β | |
| give **both** grids definite tracks and let the items stretch: | |
| ```css | |
| .annotated-stage { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr); | |
| grid-template-rows: minmax(0, 1fr); | |
| place-items: stretch; /* not center: stretch the frame to fill */ | |
| padding: 58px 18px 44px; | |
| } | |
| .annotation-frame { | |
| display: grid; | |
| grid-template-columns: minmax(0, 1fr); | |
| grid-template-rows: minmax(0, 1fr); | |
| min-width: 0; | |
| min-height: 0; | |
| width: auto; /* track sizing handles it; 100% no longer needed */ | |
| height: auto; | |
| } | |
| .annotation-image { width: 100%; height: 100%; object-fit: contain; } | |
| ``` | |
| This also measured 864 Γ 498, contained. It is more brittle (relies on readers | |
| understanding the `minmax(0,1fr)` + `min-height:0` idiom), hence Option A is | |
| preferred. | |
| > Whichever option: this is **CSS-only**. No change to `buildAnnotatedPhoto`'s | |
| > JS, the schema, the backend, or `app.py` is required for the sizing fix. | |
| --- | |
| ## Annotation accuracy | |
| ### A) Most misplacement is a *symptom* of the sizing bug β fixing sizing fixes it | |
| `updatePhotoLabels` (`index.html:1031-1062`) positions each callout from: | |
| - `containedImageRect(image)` (`index.html:1096-1118`) β computes the | |
| letterboxed sub-rect of the image from `image.naturalWidth/Height` and the | |
| `<img>` bounding box, then | |
| - `label.left = imageRect.left β layerRect.left + point[0] * imageRect.width` | |
| (and the `point[1]` equivalent for top), clamped to the layer. | |
| This math is **correct** *only when the `<img>` box matches the visible image | |
| area*. With the current sizing bug the `<img>` box is 864 Γ 1151 and overflows | |
| the viewport, so: | |
| - `imageRect.width/height` come from the oversized box, so a `point[1] = 0.8` | |
| callout is placed at ~0.8 Γ 1151 β 920px down β **far below** the 600px | |
| viewport, which is `overflow: hidden` (`.viewport-pane`, `index.html:105`), so | |
| the label is clipped and the user never sees it. | |
| - The clamp uses `layerRect.height` (also 1151), so the "keep on screen" clamp | |
| (`index.html:1044-1047`) clamps to the wrong, oversized bounds. | |
| **Once the image is contained (Option A/B), `containedImageRect` returns the | |
| real letterboxed rect and the existing placement math is accurate.** So the | |
| sizing fix is also the primary annotation-accuracy fix. Re-verify after fixing. | |
| ### B) Independent annotation-accuracy improvements (do after the sizing fix) | |
| **User decision (2026-06-14): in scope β implement sizing fix *and* this | |
| annotation-accuracy polish.** Do items #1β#3 below; #4 (1px border) and #5 | |
| (coercion clamp) are optional robustness items, implement if cheap. | |
| 1. **No marker at the actual point.** The callout box is centered *on* the point | |
| via `.scene-label { transform: translate(-50%, -50%) }` (`index.html:476`), | |
| so the text occludes the component instead of pointing at it, and an | |
| edge-clamped label has nothing tying it back to the real location. Recommend | |
| drawing a small dot/crosshair at the exact `point` (a 1β2px element, not | |
| transformed) and offsetting the text label, optionally with a short leader | |
| line. Numbered prefixes already exist (`(index + 1) + ". "`, | |
| `index.html:1051`) but there's no matching number on the photo, so the number | |
| is currently meaningless spatially. | |
| 2. **Edge clamp silently detaches the label from its point.** `Math.min(width β | |
| 72, Math.max(72, β¦))` (`index.html:1040-1047`) keeps the label on screen but, | |
| with no leader/marker (see #1), a clamped label no longer indicates which | |
| component it describes. The marker dot from #1 is the fix β keep the *dot* at | |
| the true (clamped-to-image) point and only clamp the *text*. | |
| 3. **`annotation.box` is parsed and validated but never drawn.** The schema and | |
| coercion carry an optional normalized `box: [x, y, w, h]` | |
| (`snap2sim/schema.py:197-202`, `snap2sim/model_io.py:325-327`), but | |
| `buildAnnotatedPhoto` ignores it. When present, drawing the bounding box over | |
| the contained image would be a much stronger, more accurate annotation than a | |
| single point. Optional enhancement. | |
| 4. **1px border offset.** `.annotation-image` has a `1px` border | |
| (`index.html:509`) and `box-sizing: border-box` is global (`index.html:38-40`), | |
| so `object-fit: contain` fits the *content* box (inside the border) while | |
| `containedImageRect` measures the *border* box β a 1px placement error. | |
| Negligible; mention only. If you want it exact, account for the border or use | |
| `outline` instead of `border`. | |
| 5. **Coercion rejects (drops) out-of-range points instead of clamping.** | |
| `_unit_number_list` returns `None` if any coordinate is outside `[0,1]` | |
| (`snap2sim/model_io.py:311-315`), so a point like `[1.02, 0.5]` discards the | |
| *entire* annotation and the part may then fall through to "unavailable". The | |
| browser already `clamp01`s at render (`index.html:1120-1122`), so consider | |
| clamping (not rejecting) server-side for resilience. Minor robustness item; | |
| the prompt already asks for `[0,1]` (`snap2sim/prompts.py:31-34`). | |
| The point-coordinate contract itself is consistent end-to-end: normalized | |
| `[0,1]`, **origin top-left**, asserted in the prompt (`snap2sim/prompts.py:31-34`) | |
| and consumed as `point[0]βx*width`, `point[1]βy*height` from the image's | |
| top-left (`index.html:1040-1047`). No change needed there. | |
| --- | |
| ## Security / invariants to preserve | |
| - This is a client-side CSS/JS change to an existing render path; keep the | |
| "no model-authored HTML" rule from `AGENTS.md`/`SECURITY.md`. `label`/`note` | |
| must stay `textContent`, never `innerHTML` (already correct, | |
| `index.html:1051-1058`). A new marker-dot/box element must be built with | |
| `document.createElement` + style, not injected markup. | |
| - Keep using the **local** `currentPreviewUrl` object URL for the photo | |
| (`index.html:1018`, `index.html:1134-1139`); do not round-trip the image back | |
| from the server for display. | |
| - No change to rate limiting, upload caps, or decompression-bomb guards in | |
| `app.py` is required. | |
| --- | |
| ## Suggested implementation order for Codex | |
| 1. **Sizing fix first (Option A).** CSS-only in `index.html`. This is the bug | |
| the user reported and it also corrects most annotation placement. | |
| 2. **Re-verify in a real browser** with both a **tall (portrait)** and a **wide | |
| (landscape)** photo that triggers the annotate fallback: the whole image is | |
| visible and contained, no viewport overflow/scroll, and callouts land on the | |
| correct components within the image (especially lower-half points that were | |
| previously clipped). Check the β€860px stacked layout (`index.html:555-581`). | |
| 3. **Then** the annotation-accuracy items (in scope per the user): marker dot at | |
| the true point + offset/leader label (#1, #2), and render `annotation.box` | |
| when present (#3). Items #4/#5 optional. | |
| 4. Run the standard local checks per `AGENTS.md` (schema/parser + FastAPI | |
| `TestClient` for `/`, `/analyze_image`, `/generate_scene`), then the normal | |
| PR -> GitHub Actions HF sync -> Space verification. | |
| ## Resolved scope | |
| - **Sizing fix + annotation-accuracy polish are both in scope** | |
| (user decision, 2026-06-14): the CSS sizing fix, marker dots at the true | |
| point, offset/leader labels, and rendering `annotation.box` when the model | |
| provides it. | |
| - Final submission note, 2026-06-15: this work shipped before the public | |
| `build-small-hackathon/Snap2Sim` submission. | |