Spaces:
Sleeping
Sleeping
File size: 13,941 Bytes
5e65d06 7006f4b 5e65d06 7006f4b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | # 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.
|