kwabs22 Claude Opus 5.5 commited on
Commit
363fb28
Β·
1 Parent(s): 64b0f23

List both sides of every papercraft joint; archive QA round 5

Browse files

QA round 5 read the top cap's empty glues_to as a missing joint. Tabs sit on
one side only and the top cap's are on band 7, so the joint was there but the
report listed only outgoing tabs. Each piece now has glues_to and
receives_from, with labels resolved to piece names (a split band "band 2"
-> 2a, 2b; "band 1a start" -> band 1a). Tab labels are kept in the data even
when too short to print. Apple 8x16: 22 pieces, none unconnected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Files changed (4) hide show
  1. SPEC.md +3 -1
  2. app.py +1 -2
  3. feedback/vli-qa-feedback-2026-10-30.md +24 -0
  4. vli/papercraft.py +23 -5
SPEC.md CHANGED
@@ -143,7 +143,9 @@ that would curl past ~120Β° on paper overlaps itself, so it is split into parts
143
  Every joint has glue tabs on one side only, labelled with the piece they meet (band top
144
  edge β†’ next band or top cap, seam end β†’ own start or next part, bottom cap rim β†’ band 1).
145
  Caps under 4 mm are dropped and the band closes to a point; tabs under 2 mm are dropped.
146
- Cut solid lines, fold dashed lines.
 
 
147
 
148
  Output is A4 pages (190 Γ— 277 mm inside 10 mm margins). Each piece is turned to its
149
  narrowest orientation and shelf-packed; strips longer than a page are split like curled
 
143
  Every joint has glue tabs on one side only, labelled with the piece they meet (band top
144
  edge β†’ next band or top cap, seam end β†’ own start or next part, bottom cap rim β†’ band 1).
145
  Caps under 4 mm are dropped and the band closes to a point; tabs under 2 mm are dropped.
146
+ Cut solid lines, fold dashed lines. The report lists, per piece, `glues_to` (the tabs it
147
+ carries) and `receives_from` (pieces whose tabs land on it), so a tabless piece such as
148
+ the top cap still shows its joint.
149
 
150
  Output is A4 pages (190 Γ— 277 mm inside 10 mm margins). Each piece is turned to its
151
  narrowest orientation and shelf-packed; strips longer than a page are split like curled
app.py CHANGED
@@ -283,8 +283,7 @@ def papercraft_rings(ledger_json: str, item_id: str, rings: str = "8", samples:
283
  "model_height_cm": round((profile[-1]["y"] - profile[0]["y"]) * scale / 10, 1),
284
  "real_height_cm": round(real_cm, 1), "max_height_cm_for_a4": round(a4_cm, 1),
285
  "fits_a4": bool(h is None or h <= a4_cm), "pages": page_report,
286
- "pieces": [{"name": pc["name"], "glues_to": sorted({t["label"] for t in pc["tabs"] if t["label"]})}
287
- for pc in pieces]}
288
  return json.dumps(report, indent=1), files
289
  except Exception as e:
290
  _fail(e)
 
283
  "model_height_cm": round((profile[-1]["y"] - profile[0]["y"]) * scale / 10, 1),
284
  "real_height_cm": round(real_cm, 1), "max_height_cm_for_a4": round(a4_cm, 1),
285
  "fits_a4": bool(h is None or h <= a4_cm), "pages": page_report,
286
+ "pieces": [{"name": name, **j} for name, j in papercraft.joins(pieces).items()]}
 
287
  return json.dumps(report, indent=1), files
288
  except Exception as e:
289
  _fail(e)
feedback/vli-qa-feedback-2026-10-30.md CHANGED
@@ -158,3 +158,27 @@ Ladder shape note: for this cat the descent ends at 1,000 faces β€” the 250 requ
158
  **Still open (from the apple demo, unchanged by this push β€” confirmed by re-render):** grey SimReady objects (average the already-downloaded 256px thumbnail into `base_color`), dark no-sun scenes (`sun: null` β†’ near-black render; suggest default daylight or a "what light?" question), cross-tier hint in questions (`q3 "which table?"` shows `alternatives: []` while 101 Objaverse tables sit one reject away), thumbnails as URLs so variants can be chosen by look (the 9 `food_produce_apple_*` variants are the real path to "a green apple"). Plus the minor `use`-back-swap check and the UI visual click-test.
159
 
160
  > Note (agent, 2026-10-30): the four apple-demo items above were addressed in 98ca16a, pushed after this round was written.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  **Still open (from the apple demo, unchanged by this push β€” confirmed by re-render):** grey SimReady objects (average the already-downloaded 256px thumbnail into `base_color`), dark no-sun scenes (`sun: null` β†’ near-black render; suggest default daylight or a "what light?" question), cross-tier hint in questions (`q3 "which table?"` shows `alternatives: []` while 101 Objaverse tables sit one reject away), thumbnails as URLs so variants can be chosen by look (the 9 `food_produce_apple_*` variants are the real path to "a green apple"). Plus the minor `use`-back-swap check and the UI visual click-test.
159
 
160
  > Note (agent, 2026-10-30): the four apple-demo items above were addressed in 98ca16a, pushed after this round was written.
161
+
162
+ ---
163
+
164
+ ## 8. Verification round 5 (2026-10-30, after the apple-demo push) β€” all four pass, and papercraft's first live run
165
+
166
+ **"the green apple on the table", re-run:**
167
+
168
+ | Item | Live result |
169
+ |---|---|
170
+ | Default daylight + shadows | βœ“ `sun: {id: null, elevation 50.2Β°, azimuth 68.8Β°, shadows: true, daylight: true}` β€” blue sky, visible crate/table shadows, readable picture with no glowing verb |
171
+ | `"what light?"` question | βœ“ `q_light`, implied, `default_chosen: "daylight"`, alternatives `["rephrase: '<noun> shone' or '<noun> glowed'"]` |
172
+ | Thumbnail colours | βœ“ render: crate unmistakably green (`#326d3e`), table `#d6d4d5`, apple `#b27045` on the tabletop. Bonus: colour is now a per-object `color` field **in the ledger** β€” a chat model reads the colour without rendering |
173
+ | Thumbnail URLs | βœ“ `huggingface.co/datasets/.../resolve/...` links replace Space paths |
174
+ | `on_reject` | βœ“ `q4 "which table?" β†’ {tier: "objaverse", candidates: 101, offered: 9}`; `q0 "which green?" β†’ {tier: "primitive"}` (correct: green has 0 Objaverse candidates) |
175
+
176
+ Note: apple `b01` is a reddish-brown (#b27045) β€” "a green apple" still means walking the `b01–j01` variants via `use:`/reroll and reading each `color` (one call per variant). V2 nicety: colour chips on alternative labels.
177
+
178
+ **`papercraft_rings` first live run (apple, rings 8Γ—16):** βœ“ works end-to-end.
179
+ - 6 A4 pages (210Γ—297 mm), 22 pieces: 7 bands split where curl would exceed ~120Β° (`band 1a–1e, 2a–2b, 3a–3b, 4a–4c, 5a–5c, 6a–6b, 7a–7c`) + bottom cap + top cap.
180
+ - Glue graph complete downwards: each part names its neighbours and the next band, last parts loop to `band … start`, bottom cap β†’ band 1.
181
+ - Sizing: real height 6.6 cm β†’ blank `height_cm` gives the largest A4-fitting model (16.3 cm, `fits_a4: true`); pass 6.6 for true scale.
182
+ - **Bug: `top cap` has `glues_to: []`** β€” its glue edge to band 7 is missing from the assembly graph (bottom cap correctly glues to band 1). Small fix in the glue-path builder; an assembler would otherwise hold an unlabeled lid.
183
+
184
+ > Note (agent): not a missing edge β€” tabs sit on one side of each joint, and the top cap's are on band 7's top edge. The report listed only outgoing tabs; it now also lists `receives_from`, so the top cap shows band 7a–7c.
vli/papercraft.py CHANGED
@@ -73,21 +73,19 @@ def _tab(p, q, inside, label):
73
  """A trapezoid glue tab on edge p-q, on the side away from `inside`.
74
 
75
  Edges under MIN_TAB_MM get no tab (too small to glue), and only edges of
76
- LABEL_TAB_MM or more carry the label, so converging edges don't pile up text.
77
  """
78
  e = q - p
79
  L = np.linalg.norm(e)
80
  if L < MIN_TAB_MM:
81
  return None
82
- if L < LABEL_TAB_MM:
83
- label = ""
84
  n = np.array([-e[1], e[0]]) / L
85
  if np.dot(n, inside - p) > 0:
86
  n = -n
87
  w, inset = min(TAB_MM, 0.4 * L), min(TAB_MM, 0.25 * L)
88
  u = e / L
89
  return {"poly": [p, q, q + n * w - u * inset, p + n * w + u * inset], "label": label,
90
- "at": (p + q) / 2 + n * w / 2}
91
 
92
 
93
  def _chunks(B, max_turn_deg=120, max_len=np.inf):
@@ -232,7 +230,7 @@ def svg(placed, title):
232
  for pc, off in placed:
233
  for t in pc["tabs"]:
234
  out.append(f'<polygon points="{pts(t["poly"], off)}" fill="#eee" stroke="#000" stroke-width="0.2"/>')
235
- if not t["label"]:
236
  continue
237
  a = xy(t["at"], off)
238
  out.append(f'<text x="{a[0]:.1f}" y="{a[1]:.1f}" font-size="2" text-anchor="middle" '
@@ -249,5 +247,25 @@ def svg(placed, title):
249
  return "\n".join(out), (round(width), round(height))
250
 
251
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  def _esc(s):
253
  return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
 
73
  """A trapezoid glue tab on edge p-q, on the side away from `inside`.
74
 
75
  Edges under MIN_TAB_MM get no tab (too small to glue), and only edges of
76
+ LABEL_TAB_MM or more print the label, so converging edges don't pile up text.
77
  """
78
  e = q - p
79
  L = np.linalg.norm(e)
80
  if L < MIN_TAB_MM:
81
  return None
 
 
82
  n = np.array([-e[1], e[0]]) / L
83
  if np.dot(n, inside - p) > 0:
84
  n = -n
85
  w, inset = min(TAB_MM, 0.4 * L), min(TAB_MM, 0.25 * L)
86
  u = e / L
87
  return {"poly": [p, q, q + n * w - u * inset, p + n * w + u * inset], "label": label,
88
+ "show": bool(L >= LABEL_TAB_MM), "at": (p + q) / 2 + n * w / 2}
89
 
90
 
91
  def _chunks(B, max_turn_deg=120, max_len=np.inf):
 
230
  for pc, off in placed:
231
  for t in pc["tabs"]:
232
  out.append(f'<polygon points="{pts(t["poly"], off)}" fill="#eee" stroke="#000" stroke-width="0.2"/>')
233
+ if not t["show"]:
234
  continue
235
  a = xy(t["at"], off)
236
  out.append(f'<text x="{a[0]:.1f}" y="{a[1]:.1f}" font-size="2" text-anchor="middle" '
 
247
  return "\n".join(out), (round(width), round(height))
248
 
249
 
250
+ def joins(pieces):
251
+ """Both sides of every joint. Tabs sit on one side only (the last band carries the
252
+ tabs for the top cap), so a piece lists the tabs it carries and the pieces whose
253
+ tabs land on it. A label names a piece, a split band ("band 2" -> 2a, 2b) or a
254
+ seam start ("band 1a start")."""
255
+ names = [pc["name"] for pc in pieces]
256
+
257
+ def targets(label):
258
+ base = label[:-len(" start")] if label.endswith(" start") else label
259
+ return [n for n in names if n == base or (n != base and n.rstrip("abcdefghijklmnopqrstuvwxyz") == base)]
260
+
261
+ out = {n: {"glues_to": set(), "receives_from": set()} for n in names}
262
+ for pc in pieces:
263
+ for t in pc["tabs"]:
264
+ for n in targets(t["label"]):
265
+ out[pc["name"]]["glues_to"].add(n)
266
+ out[n]["receives_from"].add(pc["name"])
267
+ return {n: {k: sorted(v) for k, v in j.items()} for n, j in out.items()}
268
+
269
+
270
  def _esc(s):
271
  return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")