Spaces:
Running on Zero
Running on Zero
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 filesQA 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>
- SPEC.md +3 -1
- app.py +1 -2
- feedback/vli-qa-feedback-2026-10-30.md +24 -0
- 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":
|
| 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
|
| 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["
|
| 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("&", "&").replace("<", "<").replace(">", ">")
|
|
|
|
| 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("&", "&").replace("<", "<").replace(">", ">")
|