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.