diff --git a/.gitignore b/.gitignore index 568c247..a507c0e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.pyc __pycache__/ .genraw/ +pipeline/.genraw/ +pipeline/_normalized/ saves/ node_modules/ .env diff --git a/E-progress.md b/E-progress.md new file mode 100644 index 0000000..8e397f3 --- /dev/null +++ b/E-progress.md @@ -0,0 +1,150 @@ +# LANE E — ASSETS · progress (PROCITY-E) + +*2026-07-14. Owner: PROCITY-E. Scope: `pipeline/*`, `web/assets/*`, `web/assets/manifest.json`. +Reviewed against `docs/LANES/LANE_E_ASSETS.md` acceptance. Nothing in other lanes' files was touched.* + +## Status at a glance + +| Step | Deliverable | State | +|---|---|---| +| 1 Audit | `pipeline/AUDIT.md` (+ `glb_stat.py`, `_audit_stats.json`) | ✅ done | +| 2 Normalize | `pipeline/normalize.py` → **16 GLBs** + 16 thumbs, verified | ✅ done (local); ⛔ **publish gated on `GOD3_PW`** | +| 3 Manifest | `web/assets/manifest.json` + `validate_manifest.py` (+ `build_manifest.py`) | ✅ done, validates green | +| 4 Facade skins | `gen_skins.py` **`--local`** backend + 23-skin gap pack | ✅ **DONE — 23 skins generated on-device, free, harvested + mapped** | +| 5 Hero props | `MESHGOD_BATCH.md` + `meshgod_batch.json` (9 solids) | ✅ spec'd; ⛔ **not run (John's go, ~$2.70)** | +| — | `pipeline/README.md` + contact sheets in `docs/shots/laneE/` | ✅ done | + +**Step 4 no longer costs anything.** The 23 facades were generated **locally on the M3 Ultra** +via MODELBEAST `flux_local` (FLUX.2-klein-4B on MPS) — free, ~5.7s each, weights already cached. +No OpenRouter key, no ~9¢ spend. See "Step 4 (done, local)" below. + +**Two things still need John (both optional — the game runs asset-free):** (1) the depot password +`GOD3_PW` to publish the 16 GLBs live; (2) a go/no-go on the paid **hero-prop** step (5 ≈ $2.70). + +## What landed + +### Step 1 — Audit +- `pipeline/glb_stat.py`: dependency-free GLB parser (tris, bbox, texture px, Draco) — runs on + ultra's py3.9. Full library stats → `pipeline/_audit_stats.json`. +- `pipeline/AUDIT.md`: the library is a raw dump — almost nothing obeys the GLB law (only + `record_crate_plastic_01` was native-metres; scale/orientation arbitrary; tris 12→960k; textures + to 4096; several **branded/trademarked** assets quarantined). Candidate→PROCITY-id table with + status, coverage-vs-registry table, QUARANTINE list, depot cross-reference. +- Toolchain reality documented (Blender present on ultra but off PATH; no coreutils/timeout). + +### Step 2 — Normalize & publish +- `pipeline/normalize.py`: headless Blender, extends `MESHGOD/finish_glb.py` (solidify/decimate/ + bright/base-origin) + adds **height-normalize, outlier cleanup, texture→WebP≤1024, per-asset + up/yaw, 256px thumbnail**. Driven by `pipeline/batch.json`. +- **16 assets** normalized to house law (metres, +Y up, base origin, ≤5k tris, WebP ≤1024, no + Draco). Every one **eyeballed** — Y-up confirmed by re-importing exports through `render_glb.py`. + Footprints from Blender-space `_normalize_results.json`. Thumbs in `web/assets/thumbs/`. + - fittings (9): record_crate, wire_shelf, clothes_rack, bookshelf, cube_shelf_wide, counter, + coffee_table, work_table, box_crate + - furniture (7): bench, bench_modern, bench_wood, longbench, park_bench, streetlight, food_cart +- `pipeline/publish.py`: mirrors the meshgod→3GOD contract exactly (cookie `hmac(GOD3_PW,"3god-ok")`, + `POST /api/upload` + `/api/thumb`). Reads the secret from env — never contains/logs it. `--dry-run` + verified all 16 ready. **Not published yet: `GOD3_PW` isn't set here.** Run: `export GOD3_PW=… && + python3 pipeline/publish.py` (idempotent, reversible). + +### Step 3 — Manifest +- `web/assets/manifest.json` (v1): the contract Lanes B/C/D read. All **69 skins** catalogued + (25 facade / 10 sky / 10 ground / 8 wallpaper / 6 floor / 7 surface / 3 awning) + 16 fittings/ + furniture with **footprints, heights, thumbs**. `conventions` block documents the GLB law, + blank-signboard rule, corner-lot policy, fallbacks, and depot load path. +- Every registry shop type has **≥2 facades** (verified in the validator). Facades tagged + `signboard: blank|baked` — 5 djsim/market/grimy skins carry baked sign text and must not get a + name overlay; the other 20 are clean overlay targets. +- `pipeline/validate_manifest.py`: Lane F gate — parses JSON, checks every skin exists, every GLB + is local-or-on-depot, thumbs exist, footprints/heights sane, ≥2 facades/type. **Green** (16 + local-only warnings until publish; `--depot` enforces live). +- `pipeline/build_manifest.py`: reproducible generator (curated type-maps + derived skin/fitting + lists) so the manifest regenerates when assets change. + +### Step 4 — Facade skin expansion (DONE, local) +- `pipeline/gen_skins.py`: ported from `thriftgod/gen_assets.py` — **same STYLE bible + blank- + signboard template** so new skins match. Resumable `.genraw/`, `--dry-run`/`--only`/`--harvest`. +- **`--local` backend (the one I used):** MODELBEAST `flux_local` → FLUX.2-klein-4B on the M3 + Ultra's MPS GPU. Apache/**ungated**, weights already cached (15 GB), **free**, ~5.7 s/img, one + GPU job at a time. Direct subprocess to `~/Documents/MODELBEAST/server/operators/flux_local/run.py` + (`--outdir`/`--params` JSON) — no server, no cloud, no key. Cloud OpenRouter path kept as fallback. +- **All 23 generated → harvested → mapped.** 0 failures, 2:11 total. Eyeballed every one + (contact sheet `docs/shots/laneE/facades_gen_local.png`): every shopfront has a **genuinely + blank signboard** (Lane B name-overlay target), style indistinguishable from the 25 anchors. + The pack: toy (2), book (2), video (2), milkbar (3), wide dept anchors (2), stall/market (2), + warehouse (2), arcade portal (1), residential house fronts (4), corner-lot `-side` walls (3). +- **Coverage after mapping** (`build_manifest.py` `FACADE_TYPES`, mirrors registry.js + additive): + record 3 · opshop 3 · toy 5 · book 5 · video 4 · pawn 7 · milkbar 5 · dept 4 · **stall 4** + — every registry type ≥3. **`stall` went 2→4**, retiring the Lane A single-facade gap. +- **Corner-lot `-side` skins now exist** (brick/render/weatherboard): tagged `face:"side"` with + empty `types` in the manifest so no shop-front pool ever picks a windowless wall; Lane B maps + them to the exposed side face (`conventions.corner_lots` + new `conventions.facade_face`). +- `pipeline/SKIN_PROMPTS.md`: identical pack kept for the free manual flow (fallback if MODELBEAST + is ever down). + +### Step 5 — Bespoke hero props +- `pipeline/meshgod_batch.json` + `MESHGOD_BATCH.md`: 9 solids (glass case, fridge, till, listening + booth, arcade cabinet, magazine rack, milkshake mixer, novelty record, bus shelter) in the + thriftgod_hero pattern. Documents the **MeshGod gotcha** (thin wires/flat discs → primitives), + reuse-first (cash-register/glass-case likely already on depot), and the primitive fallbacks. + **Not run** — gated on John (~$2.70). Nothing blocks: all have in-engine fallbacks. + +## Adversarial review & fixes (self-run, 4-dimension × verify) + +Ran a 19-agent adversarial review over all deliverables (correctness / contract-fit / doc-accuracy / +house-law), each finding independently verified. **15 confirmed** (1 blocker, 5 major, 9 minor). +All fixed and re-verified: + +- **BLOCKER — off-origin exports.** `normalize.py` joined meshes but never flattened parent node + hierarchies, so assets under a parent empty shipped off-origin: `park_bench` was **~1 km off-origin, + floating 30 m**; `food_cart` 15 m off; `clothes_rack` sank 0.19 m. My earlier spot-check missed it + (the clean assets have no hierarchy). **Fixed:** `parent_clear(KEEP_TRANSFORM)` + drop non-mesh + nodes before join. Re-verified node-aware: all 16 now `center=[0, h/2, 0]`, **minY=0.0**. +- **Major — tri budget counted faces, not triangles** (latent for quad FBX/OBJ). Fixed: triangulate + in `clean_mesh` so the ≤5k gate + reported counts are true triangles. +- **Major — fitting-id namespace.** Added `fittingAliases` (registry semantic id → physical id) to + the manifest. (Lane C's `glb.js` already maps its kinds to my physical ids, so it worked — this + makes it explicit + future-proof.) +- **Major — facade map vs registry.js.** Aligned my facade→type map to be a consistent superset of + Lane A's `registry.js`; flagged that registry's `stall` pool has only 1 facade (Lane A gap). +- **Major — facing -Z not enforced.** Corrected the docs: symmetric props are orientation-agnostic; + no auto-facing (per-asset `yaw` available). Removed the false "enforced" claim. +- **Major — licence provenance.** Strengthened the publish-set caveat (record per-asset provenance + before pushing to the shared CDN) — a reason publish stays gated on John. +- **Minors:** hardened `glb_stat.py` (node-aware world bounds + WebP size sniff — the exact blind + spots that hid the blocker); `validate_manifest.py` liveness now uses a Range GET not HEAD; + Akai-MPC trademark flag; corrected audit counts (59 parsed `.glb`, rejects 8, food_cart tris). + +## Acceptance check (LANE_E) + +- ✅ `AUDIT.md` complete. +- ✅ ≥15 normalized GLBs (**16**) with thumbnails — ⛔ *live on 3GOD pending publish (needs cred)*. +- ✅ `manifest.json` validates green; fittings/furniture carry footprints for B/C to place untouched. +- ✅ Every registry shop type has ≥2 facades — now **≥3** (48 facades total). New skins + **generated** (not just planned) and eyeball-matched to the anchor style + (`facades_style_anchor.png` = 25 originals; `facades_gen_local.png` = 23 new local skins; + `fittings_normalized.png` = the 16 GLBs — all in `docs/shots/laneE/`). +- ✅ One-page `pipeline/README.md` (re-run each step, costs, where creds live). + +## Notes for the other lanes + +- **Lane B / C:** read `web/assets/manifest.json`. `fittings`/`furniture` → `depot:` via + `core/loaders.js` (placeholder box on miss — the game runs asset-free). Footprints are `[w,d]` + metres on the ground; heights in metres; all base-origin, +Y up, facing −Z. +- **Lane B:** overlay shop names only on facades with `signboard:"blank"`. Wide `dept-*` anchors + (when generated) want a wider UV plane. `awning`/`ground`/`sky` skins are yours; `wall`/`interior` + are Lane C's. +- **Lane F:** wire `python3 pipeline/validate_manifest.py` into the gate (add `--depot` post-publish). + +## Open questions for Fable/John + +1. **Publish now?** I have 16 verified GLBs staged in `pipeline/_normalized/` ready to push to the + shared depot. It's an outward-facing write to shared infra + needs `GOD3_PW`, so I left it for + your go rather than assume it. +2. ~~Spend the ~9¢ on facades?~~ **Resolved — done for free on-device** (MODELBEAST flux_local). + Only the **~$2.70 hero-prop** MeshGod batch (Step 5) still wants a go/no-go; all props have + in-engine primitive fallbacks, so it's a polish upgrade, not a blocker. *(Note: image→GLB is + also local on this box via `sf3d`/`trellis_mac`, but those weights are HF-gated — a setup + step before that path is free too.)* +3. A few library assets were near-misses (garage_shelves, radiogram, market_stall — see AUDIT + rejects). Worth a manual Blender re-origin pass later, or leave to primitives? I left them out. diff --git a/docs/shots/laneE/.gitkeep b/docs/shots/laneE/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/shots/laneE/facades_gen_local.png b/docs/shots/laneE/facades_gen_local.png new file mode 100644 index 0000000..6150f02 Binary files /dev/null and b/docs/shots/laneE/facades_gen_local.png differ diff --git a/docs/shots/laneE/facades_style_anchor.png b/docs/shots/laneE/facades_style_anchor.png new file mode 100644 index 0000000..abb2ba2 Binary files /dev/null and b/docs/shots/laneE/facades_style_anchor.png differ diff --git a/docs/shots/laneE/fittings_normalized.png b/docs/shots/laneE/fittings_normalized.png new file mode 100644 index 0000000..5bc3a49 Binary files /dev/null and b/docs/shots/laneE/fittings_normalized.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md new file mode 100644 index 0000000..037c8c8 --- /dev/null +++ b/pipeline/AUDIT.md @@ -0,0 +1,135 @@ +# PROCITY Asset Audit — Lane E, Step 1 + +*Generated 2026-07-14 from a live inspection of `johnking@100.91.239.7:~/Documents/3D=models/` +(the sorted house library) + the 3GOD depot `GET /api/list`. Tri counts / bounding boxes from +`pipeline/glb_stat.py` (dependency-free GLB parser); orientation & final scale verified by +rendering every candidate through Blender and looking at the thumbnails (never "should look right").* + +## TL;DR + +- The library is a **raw dump**: almost nothing obeys the house GLB law. Scale is arbitrary + (only `record_crate_plastic_01` was native-metres), up-axis varies, tri counts span **12 → 960k**, + textures reach **4096px**, and several assets carry **branded/trademarked designs** (quarantined). +- `pipeline/normalize.py` fixes scale/orientation/tris/textures uniformly. **16 assets** normalized + clean, verified upright & metre-scaled, WebP ≤1024, ≤5k tris, +Y up, base origin — ready to publish. +- Gaps with **no usable source** (→ `MESHGOD_BATCH.md` or in-engine primitives): glass display + case, fridge, till/register, spinner rack, magazine rack, market stall, escalator, VHS shelving. +- Depot already holds a few reusable house props (`street-bench`, `council-bin`, `milk-crate-stack`) + and 138 assets total (mostly TRELLIS `*-views-3d-*` / `image-3d-*` MeshGod outputs). +- **Skin caveat:** 5 of the 25 facades carry **baked-in sign text** (`djsim-milkbar` "MILK BAR", + `djsim-video`, `djsim-pub`, `market`, `grimy`) — they break the blank-signboard convention, so + the manifest tags them `signboard:"baked"` and Lane B must not overlay a name on them. The other + 20 are genuinely blank. + +## Toolchain reality (differs from the lane brief) + +| thing | brief said | actual | +|---|---|---| +| Blender on ultra | "runs on ultra against Blender" | present but **not on PATH** → `/Applications/Blender.app/Contents/MacOS/Blender` (v5.0.1). Renders 4 angles in ~0.9s. | +| coreutils / timeout | none | confirmed — used ssh `ConnectTimeout`, no `timeout`/`setsid` | +| python on ultra | 3.9 | 3.9.6 — `glb_stat.py`/`normalize.py` written to that floor | +| OpenRouter key | `thriftgod/.env` | absent; house convention `~/Documents/dealgod/lore/keys.env` (auto-read by gen scripts) | +| depot upload cred | — | `GOD3_PW` env, **not set here** → publish is gated on John (see `publish.py`) | + +## Normalized set (16 — ready; `manifest.json` → `fittings` / `furniture`) + +house law enforced on every one: metres · +Y up · origin at base (minY=0, centred X/Z — verified +node-aware) · ≤5k tris · WebP ≤1024 · no Draco. *Facing is not auto-corrected* — symmetric props are +orientation-agnostic; the few directional ones (counter, food_cart) keep their native front and +Lane B/C rotate when placing (per-asset `yaw` is available in `batch.json`). + +| PROCITY id | source GLB | src tris → out | footprint (m) | height (m) | notes | +|---|---|---|---|---|---| +| **fit** record_crate | shop-fittings/record_crate_plastic_01 | 18320→5000 | 0.46 × 0.63 | 0.41 | ★ native-metres, textured red milk crate; serves record bin too | +| **fit** wire_shelf | shop-fittings/shelf_metal_wire_01 | 330→330 | 1.17 × 0.44 | 1.80 | ★ clean upright wire shelving | +| **fit** clothes_rack | shop-fittings/clothes_rack_01 | 3152→3152 | 1.34 × 0.43 | 1.50 | ★ rolling clothes rack (opshop) | +| **fit** bookshelf | shop-fittings/bookshelves | 45128→5000 | 0.20 × 1.02 | 2.00 | tall bookshelf w/ books (book/video/opshop) | +| **fit** cube_shelf_wide | shop-fittings/shelf_cube_bookshelf_01 | 17734→4999 | 0.75 × 1.24 | 1.10 | stocked cube shelving (toy/book) | +| **fit** counter | shop-fittings/balcao | 276→276 | 1.06 × 4.02 | 1.10 | long bar counter — Lane C may want to scale to lot | +| **fit** coffee_table | shop-fittings/coffeetable | 495→495 | 0.67 × 1.49 | 0.45 | low display/side table | +| **fit** work_table | shop-fittings/computertable | 1556→1556 | 0.44 × 1.04 | 0.75 | work/trestle table w/ lower shelf | +| **fit** box_crate | shop-fittings/warehouse_box | 78→78 | 1.35 × 1.32 | 0.60 | open bulk bin / bric-a-brac box | +| **street** bench | street-furniture/bench | 1942→1942 | 0.44 × 1.21 | 0.85 | slatted seat | +| **street** bench_modern | street-furniture/eb_hipster_bench | 640→640 | 1.68 × 0.47 | 0.45 | low modern bench | +| **street** bench_wood | street-furniture/woodbench | 11728→3999 | 1.34 × 0.59 | 0.85 | timber park bench | +| **street** longbench | street-furniture/longbench | 1172→1172 | 5.86 × 1.08 | 0.85 | long feature bench (arcade/market) | +| **street** park_bench | street-furniture/park_bench_01 | 9884→5000 | 1.92 × 1.62 | 0.85 | classic slatted park bench | +| **street** streetlight | street-furniture/streetlight | 6592→4999 | 1.64 × 1.82 | 5.00 | ornate lamppost | +| **street** food_cart | street-furniture/food_cart_bakso_01 | 30316→5000 | 3.00 × 2.07 | 2.00 | textured food cart (~3MB — heavy, 28 tex; optimize later) | + +## Rejected during normalize (needs-manual-fix — NOT in manifest) + +| source | issue (verified by render) | disposition | +|---|---|---| +| shop-fittings/cube_bookcase (`cube_shelf`) | node-scale distortion → 6.3 × 8.4 m footprint | dropped; `cube_shelf_wide` covers the need | +| shop-fittings/garage_shelves_metal_v2 | imports lying flat (green pallet); baked node transform, up-fix didn't help | needs manual re-origin in Blender; `wire_shelf` covers shelving | +| shop-fittings/table, saloon_table, table_02, desk_01, kitchen_table | degenerate/flat or node-distorted (`table` → 55×55 m; `desk` → 17×17 m) | rejected; `work_table`/`coffee_table` cover tables | +| shop-fittings/radiogram_furniture_v2_l1 | Blender "Please select a file" on import (bad GLB) | needs re-export | +| street-furniture/market_stall_01_CHECK | import error (`_CHECK` = sorter-flagged) | → MeshGod/primitive (see `MESHGOD_BATCH.md`) | + +## QUARANTINE — licence unclear / branded (🔴 do NOT publish or ship) + +The house rule is 🟢 web-ok only; anything branded or of unknown provenance is quarantined until +cleared. These are **excluded from the manifest**: + +| asset | reason | +|---|---| +| `furniture/armchair-vitra-eames`, `ines-chair-calligaris` | protected designer marques (Vitra/Eames®, Calligaris®) | +| `shop-fittings/ikea-cupboard` | IKEA® product design (+259k tris) | +| `props-scenes/mk2_1210_turntable_-_old` | Technics SL-1210® industrial design (trademark) | +| `props-scenes/tuborg_fest_scene` | Tuborg® brand scene | +| `furniture/uploads_files_*`, `props-scenes/uploads_files_*` (Couch William, Harland Office Chair, nv toufa, Backpack) | CGTrader/free3d bulk dumps, licence not recorded | +| `furniture/sofa-c`, `hug-armchair`, `dragonchest`, `kitty-bed-01`, `bean_bag_chair` | provenance unrecorded — verify before use | + +> **Blanket caveat (applies to the 16-asset publish set):** the *generic* fittings we normalized +> (benches, shelves, counter, crate…) carry **no explicit licence/provenance record** — I cross- +> checked all 16 source filenames and none is a recognizable trademark, and they were pre-sorted +> into the house library by John for exactly this use, so they're treated as usable per the lane +> brief. But "unclear ⇒ quarantine" is the house rule: **before `publish.py` pushes to the shared +> CDN, record a one-line source/licence per asset**, and for any CC-BY source carry attribution. +> If provenance genuinely can't be established for one, pull it from the publish set. This is a +> reason the publish step is deliberately gated on John, not automated. + +## Coverage vs the registry fittings needs + +| shop type | fittings need | covered by | gap → | +|---|---|---|---| +| record | bins, crates, counter, listening corner | record_crate, counter, wire_shelf | **listening booth** → MeshGod | +| opshop | clothes racks, bric-a-brac shelving, book wall | clothes_rack, wire_shelf, bookshelf, box_crate | — | +| toy | cube shelves, display tables, **glass case** | cube_shelf_wide, coffee_table | **glass case** → MeshGod | +| book | bookshelves, **spinner racks**, armchair | bookshelf, cube_shelf_wide | spinner rack, armchair → MeshGod/primitive | +| video | aisle shelves, **returns slot**, poster wall | bookshelf, wire_shelf | returns slot, poster wall → primitive | +| pawn | **glass cabinets**, wall hooks, barred window | counter | glass cabinet → MeshGod; pegboard = `tex-pegboard` skin | +| milkbar | counter, **fridge**, magazine rack | counter | **fridge**, magazine rack → MeshGod/primitive | +| dept | mixed sections, **escalator prop** | (all above) | escalator → primitive | +| stall | trestle tables, crates | work_table, box_crate, record_crate | **market stall** → MeshGod/primitive | +| street | bench/streetlight/bin/tree | bench×4, longbench, park_bench, streetlight, food_cart | bin (`council-bin` on depot), tree → billboard (Lane B) | + +Gaps with no clean source are shortlisted in `MESHGOD_BATCH.md` (budget-gated) — everything has an +in-engine primitive fallback, so none of these block the game. + +## Depot cross-reference (`GET /api/list`, 138 assets) + +- **90 named + 48 TRELLIS-hash.** Reusable house props already live (verify visually + licence + before wiring): `street-bench.glb`, `council-bin.glb`, `milk-crate-stack.glb`, `cashbot.glb`. + ⚠️ `akai-mpc-live-iii-*` is a **trademarked product design** — same quarantine class as Technics/ + IKEA; do not treat as clean web-ok reuse without a licence check. +- **No `procity_*` assets yet** — this lane is the first to publish that namespace. +- Our 16 normalized GLBs are staged in `pipeline/_normalized/` and upload via `pipeline/publish.py` + once `GOD3_PW` is provided (idempotent, reversible). + +## Full library inventory (dispositions) + +`shop-fittings/` (33): normalized 9 · rejected 8 (7 node-distorted/degenerate + radiogram bad-GLB) · +quarantine 1 (ikea) · unused/dup 15 (heavier or duplicate tables/shelves — `shelf_storage_rack_01` +alone is 960k tris). +`street-furniture/` (9): normalized 7 · error 1 (market_stall) · `building_shell_01` reserved for +Lane B (38k-tri building shell — not a fitting). +`furniture/` (17): mostly domestic (beds/sofas/chairs) — out of scope for shop fittings; 6 quarantined. +`props-scenes/` (10): `dj_roomv01` + turntable = record-shop dressing (turntable quarantined: Technics); +rest branded/scene dumps. + +Raw stats for every **parsable `.glb`** (59 of the ~69 library entries; the rest are `.fbx`/`.obj` +or subdir sources not counted): `pipeline/_audit_stats.json`. `glb_stat.py` reports **world-space** +dims (node-transform-aware) and WebP/PNG/JPEG texture sizes. Normalizer output: +`pipeline/_normalize_results.json`. diff --git a/pipeline/MESHGOD_BATCH.md b/pipeline/MESHGOD_BATCH.md new file mode 100644 index 0000000..313dfe5 --- /dev/null +++ b/pipeline/MESHGOD_BATCH.md @@ -0,0 +1,60 @@ +# PROCITY bespoke hero props — MeshGod batch (Step 5) + +> **Budget-gated. Do NOT run without John's go.** ≈ 9 solids × ~30¢ ≈ **$2.70**. Everything +> here has an in-engine primitive fallback, so the game never blocks on it. The batch spec is +> `pipeline/meshgod_batch.json` (djsim_home / thriftgod_hero pattern). + +## What this fills + +The audit coverage table (`AUDIT.md`) found the sorted library has **no usable source** for a +handful of shop-defining props. These are them — the objects that make a shop-type read instantly: + +| prop | shop type | why not from the library | +|---|---|---| +| listening-booth | record | nothing like it in the dump | +| cash-register | all | none (till is universal) | +| glass-case | toy / pawn / milkbar | audit found no glass display case | +| drinks-fridge | milkbar | none | +| magazine-rack | milkbar / pawn | none | +| arcade-cabinet | arcade / dept | none | +| milkshake-mixer | milkbar | none | +| novelty-record | record (roof ornament) | the "ooh" landmark object | +| bus-shelter | street | none (Lane B street dressing) | + +## Run it (only after go-ahead) + +```bash +# 1. FIRST check for reuse — save money. cash-register & glass-case very likely already exist +# from thriftgod_hero.json (its street-bench/council-bin/milk-crate-stack/cashbot are already +# live on the depot): +curl -s https://digalot.fyi/3god/api/list | grep -iE 'register|glass|display|fridge|arcade' + +# 2. generate the ones not already available: +cd ~/Documents/MESHGOD # (or meshgod) +python -m meshgod.batch manifest ~/Documents/procity/pipeline/meshgod_batch.json --out assets3d/procity + +# 3. finish + publish through the SAME house pipeline as the library assets: +# meshgod.batch already runs finish_glb (metres/base-origin/decimate). Then normalize any +# stragglers with pipeline/normalize.py and publish with pipeline/publish.py. +``` + +## The MeshGod gotcha (why some gaps are NOT here) + +Thin wires and flat discs reconstruct badly in image→3D. These stay **in-engine primitives** +(listed under `_primitives_instead` in the JSON) — Lane B/C build them from boxes/cylinders: + +- **spinner-rack** (thin wire) → extruded-cylinder primitive +- **band-poster-easel** (thin A-frame) → box + canvas poster +- **vhs-rewinder** (tiny box) → box primitive +- **returns-slot** → a cut in the shopfront wall plane +- **escalator-prop** → extruded ramp primitive +- **pegboard-wall** → a plane skinned with `tex-pegboard.jpg` (already in `web/assets/gen/`) + +The `novelty-record` is deliberately specced as a **thick chunky disc on a base** (not a flat +disc) so it survives reconstruction. + +## After generation + +- Each solid → `procity_fit__01.glb` / `procity_street__01.glb`, thumbnail rendered, + add to `web/assets/manifest.json` (re-run `build_manifest.py` once the results land), publish. +- Re-run `validate_manifest.py --depot` to confirm they're live and the manifest stays green. diff --git a/pipeline/README.md b/pipeline/README.md new file mode 100644 index 0000000..dfe67cf --- /dev/null +++ b/pipeline/README.md @@ -0,0 +1,91 @@ +# PROCITY pipeline (Lane E) — how to re-run everything + +The asset pipeline: audit ultra's library → normalize the good bits to house GLB law → publish +to the 3GOD depot → ship `web/assets/manifest.json` (the contract Lanes B/C/D read) → fill facade ++ prop gaps. **Everything is optional at runtime** — the game runs asset-free; this lane makes it +gorgeous, it doesn't make it work. + +## Where things live + +| what | where | +|---|---| +| ultra (library + Blender) | `ssh johnking@100.91.239.7` — key auth, **no** coreutils/timeout, py3.9, Blender at `/Applications/Blender.app/Contents/MacOS/Blender` (not on PATH) | +| model library | ultra `~/Documents/3D=models/{shop-fittings,street-furniture,furniture,props-scenes}` | +| 3GOD depot | `https://digalot.fyi/3god` — `GET /a/`, `GET /api/list`, `POST /api/upload?name=` | +| OpenRouter key (skins) | env `OPENROUTER_KEY` or `~/Documents/dealgod/lore/keys.env` (never committed) | +| depot password (publish) | env `GOD3_PW` (never committed; ask John) — cookie = `hmac(GOD3_PW,"3god-ok")` | +| Ollama (free LLM) | `http://100.89.131.57:11434` (qwen3) — for names/lore/cataloguing, unused so far | +| MODELBEAST (free local gen) | this box, `~/Documents/MODELBEAST`. `flux_local` = FLUX.2-klein-4B text→image on MPS (**used for the facade skins** — ungated, weights cached). `sf3d`/`trellis_mac` = image→GLB, local but HF-gated (setup needed) | + +## The steps (each is idempotent / resumable) + +### 1. Audit — `AUDIT.md` *(free)* +```bash +scp pipeline/glb_stat.py johnking@100.91.239.7:/tmp/ +ssh johnking@100.91.239.7 'cd ~/Documents/3D=models && python3 /tmp/glb_stat.py --json shop-fittings street-furniture furniture props-scenes' > pipeline/_audit_stats.json +``` +`glb_stat.py` is a dependency-free GLB parser (tris, bbox, texture sizes, Draco flag). Findings + +licensing triage are written up in `AUDIT.md`. + +### 2. Normalize — `normalize.py` → 16 GLBs + thumbs *(free, runs on ultra)* +```bash +scp pipeline/normalize.py pipeline/batch.json johnking@100.91.239.7:/tmp/ +BL=/Applications/Blender.app/Contents/MacOS/Blender +ssh johnking@100.91.239.7 "\$BL --background --python /tmp/normalize.py -- /tmp/batch.json /tmp/procity_norm" +scp johnking@100.91.239.7:/tmp/procity_norm/_results.json pipeline/_normalize_results.json +scp 'johnking@100.91.239.7:/tmp/procity_norm/thumbs/*.png' web/assets/thumbs/ +scp 'johnking@100.91.239.7:/tmp/procity_norm/*.glb' pipeline/_normalized/ +``` +Enforces the house GLB law (metres, +Y up, base origin, ≤5k tris, WebP ≤1024, no Draco), renders a +256px thumbnail each. Extends `MESHGOD/scripts/finish_glb.py` (solidify/decimate/bright/base-origin) ++ adds texture-webp, height-normalize, outlier cleanup, per-asset up/yaw. Edit `batch.json` to add +assets; **always eyeball the thumbnails** — node-transform-distorted sources (see AUDIT rejects) +look fine in-frame but have wrong footprints. + +### 3. Manifest — `build_manifest.py` + `validate_manifest.py` *(free)* +```bash +python3 pipeline/build_manifest.py # → web/assets/manifest.json +python3 pipeline/validate_manifest.py # Lane F gate: 0 errors = green +python3 pipeline/validate_manifest.py --depot # stricter: require GLBs live on the depot +``` +`build_manifest.py` derives skins from `web/assets/gen/` + fittings from `_normalize_results.json`; +the curated facade→shop-type maps live at the top of that file (edit there when adding skins). + +### 4. Publish — `publish.py` → 3GOD depot *(the one outward-facing step)* +```bash +python3 pipeline/publish.py --dry-run # free: list what would upload +export GOD3_PW=... # ask John +python3 pipeline/publish.py # POST each GLB + thumb to the depot (idempotent) +``` + +### 5. Facade skin gaps — `gen_skins.py --local` *(DONE, free, on-device)* +The 23-skin gap pack is **already generated + harvested + mapped**. To re-run or extend: +```bash +python3 pipeline/gen_skins.py --local --dry-run # free: list the pack, no GPU +python3 pipeline/gen_skins.py --local # MODELBEAST flux2-klein-4b on MPS → .genraw/ (~5.7s/img) +python3 pipeline/gen_skins.py --harvest # → web/assets/gen/facade-*.jpg, then re-run build_manifest.py +``` +`--local` drives `~/Documents/MODELBEAST/server/operators/flux_local/run.py` directly (no server, +no key, weights cached). It runs one GPU job at a time. Add skins by editing `PROMPTS` in +`gen_skins.py` and their shop-type map in `build_manifest.py` (`FACADE_TYPES`). **Always eyeball +`.genraw/` and delete rejects before `--harvest`.** Cloud fallback (drop `--local`): OpenRouter +`google/gemini-3.1-flash-image`, ~$0.004/img, needs `OPENROUTER_KEY`. Manual flow: `SKIN_PROMPTS.md`. + +### 6. Bespoke hero props — `MESHGOD_BATCH.md` + `meshgod_batch.json` *(≈$2.70, GATED)* +Glass case, fridge, till, listening booth, etc. — the gaps no library asset fills. **Do not run +without John's go**; check the DealGod cache / depot for reuse first (cash-register + glass-case +likely already exist). Everything has a primitive fallback. + +## Cost summary + +| step | cost | +|---|---| +| audit, normalize, manifest, validate, publish | **free** (compute + bandwidth) | +| facade skin gap-fill (23 imgs) | **free** — done on-device (MODELBEAST flux_local); cloud fallback ~9¢ | +| MeshGod hero props (9 solids) | **~$2.70** — gated on John (local `sf3d`/`trellis_mac` is free but HF-gated) | + +## Artifacts (not committed — regenerated from the steps) + +`pipeline/_normalized/` (staging GLBs, live on depot), `pipeline/.genraw/` (raw skin gen). +Committed: the scripts, `*.md`, `batch.json`, `meshgod_batch.json`, `_audit_stats.json`, +`_normalize_results.json`, `web/assets/manifest.json`, `web/assets/thumbs/`, `docs/shots/laneE/`. diff --git a/pipeline/SKIN_PROMPTS.md b/pipeline/SKIN_PROMPTS.md new file mode 100644 index 0000000..a0346a8 --- /dev/null +++ b/pipeline/SKIN_PROMPTS.md @@ -0,0 +1,93 @@ +# PROCITY facade skins — manual prompt pack (Step 4, route 2) + +> Free route: paste these into the house image flow (Gemini / Flow) by hand. Route 1 +> (`pipeline/gen_skins.py`) generates the *identical* set automatically for ~9¢ if the +> OpenRouter key is available. Either way, review, then save into `web/assets/gen/` under the +> **exact filenames** below and re-run `python3 pipeline/build_manifest.py` to catalogue them. + +## LOCK — prepend to every prompt (the style + POLY bible) + +``` +1990s Australian op-shop aesthetic, slightly faded and worn, warm fluorescent lighting, +kitsch but loving, no text unless specified, no watermarks. +``` + +**Golden rules (why these match the existing 25):** +- **Blank signboard** above the entrance — the game overlays the shop name (never bake text in). +- **Flat front elevation, edge to edge** — it's a texture on a plane, not a hero shot. No angle, + no sky margin, no ground margin (except the wide/house variants which include a sliver). +- **Straight-on, overcast daylight.** Landscape framing (~16:9) for shopfronts. +- Downsize to ≤1024px before saving (`sips -Z 1024 in.png --out out.jpg`). + +--- + +## Shop-type facades (registry gaps) → `facade-.jpg` + +Template: *"Straight-on photograph of a small single-storey Australian suburban shop facade, +**{desc}**, blank empty signboard above the entrance, glass shop window and a door, no text or +lettering anywhere, overcast daylight, flat front elevation filling the entire frame edge to edge, +nothing else visible."* + +| save as | {desc} | +|---|---| +| `facade-toy-brights.jpg` | cheerful toy shop, primary-colour painted render, big low display window | +| `facade-toy-lolly.jpg` | old-fashioned lolly and toy shop, pastel candy-stripe painted front | +| `facade-book-barn.jpg` | a crammed secondhand book barn, tall dusty windows stacked with books, dark timber | +| `facade-book-scholarly.jpg` | a genteel antiquarian bookshop, dark green paint, gold-line window, brass fittings | +| `facade-video-neon.jpg` | a 1990s video rental store, glossy red and blue plastic signage band, poster light-boxes (all blank) | +| `facade-video-suburban.jpg` | a suburban strip video shop, cream brick, glass double doors, returns chute | +| `facade-milkbar-corner.jpg` | a classic corner milk bar, cream and green wall tiles, wide wrap-around window, ice-cream awning | +| `facade-milkbar-fibro.jpg` | a little fibro milk bar with a fly-screen door, a bench out front, faded soft-drink colours | +| `facade-milkbar-deco.jpg` | a streamline-deco milk bar, curved rendered corner, chrome trim, pastel mint paint | +| `facade-arcade-entry.jpg` | a covered pedestrian shopping arcade entrance, tiled archway, terrazzo floor, patterned ceiling | +| `facade-warehouse-roller.jpg` | an industrial discount warehouse shop, red brick with a big corrugated steel roller door, blank banner | +| `facade-warehouse-tin.jpg` | a corrugated-tin clearance shed shopfront, roller door half up, concrete apron | +| `facade-market-fruit.jpg` | an open fruit-and-veg shopfront, empty produce crates and astro-turf steps, striped awning | +| `facade-market-edge.jpg` | a row of humble market-square edge shopfronts under one shared posted verandah, mixed paint colours | + +## Wide department-store anchors → `facade-.jpg` (Lane B maps onto a WIDE plane) + +Template: *"Straight-on photograph of a **WIDE two-storey** Australian department-store anchor +building facade, **{desc}**, a long blank signboard band across the top, rows of ground-floor +display windows and a central entrance, no text or lettering anywhere, overcast daylight, flat +front elevation filling the entire frame edge to edge, nothing else visible."* + +| save as | {desc} | +|---|---| +| `facade-dept-anchor.jpg` | mid-century mustard-render and glass, cantilevered awning, grand central doors | +| `facade-dept-emporium.jpg` | a federation-era brick emporium, arched upper windows, ornate parapet | + +## Corner-lot second faces → `facade--side.jpg` (the `-side` convention) + +> **Corner-lot decision:** in v1 Lane B reuses the *front* facade skin on the side face (documented +> in `manifest.json` → `conventions.corner_lots`). These blank `-side` walls are the upgrade: a +> windowless side elevation so corner buildings don't show a duplicated shopfront down the lane. + +Template: *"Straight-on photograph of the **plain windowless side wall** of an Australian shop +building, **{desc}**, no shopfront, no door, a faded blank rectangular ghost-sign area, no text or +lettering anywhere, overcast daylight, flat elevation filling the entire frame edge to edge."* + +| save as | {desc} | +|---|---| +| `facade-brick-side.jpg` | painted red brick, a downpipe, one small high window | +| `facade-weatherboard-side.jpg` | cream weatherboard boards, a rain downpipe | +| `facade-render-side.jpg` | weathered pastel painted render with hairline cracks | + +## Residential fronts → `facade-.jpg` (lots with `use:'house'`) + +Template: *"Straight-on photograph of a modest Australian suburban house front, **{desc}**, front +path and a bit of fence, no text anywhere, overcast daylight, flat front elevation filling the +entire frame edge to edge, nothing else visible."* + +| save as | {desc} | +|---|---| +| `facade-res-terrace.jpg` | a single-fronted Victorian terrace, iron-lace verandah, dark brick, tiny front garden | +| `facade-res-weatherboard.jpg` | a modest weatherboard cottage, bull-nose verandah, picket fence | +| `facade-res-fibro.jpg` | a 1950s cream fibro house, hip roof, small steel-framed windows | +| `facade-res-brickveneer.jpg` | a 1970s brown brick-veneer house, aluminium sliding windows, carport edge | + +--- + +**23 skins total.** After harvesting, `build_manifest.py` auto-catalogues any `facade-*.jpg` +(new shop-type ones get their `types` from the map in `build_manifest.py` — add the new keys +there so they attach to the right shop types; unmapped ones default to `["general"]`). diff --git a/pipeline/_audit_stats.json b/pipeline/_audit_stats.json new file mode 100644 index 0000000..b09cf27 --- /dev/null +++ b/pipeline/_audit_stats.json @@ -0,0 +1,1491 @@ +[ + { + "file": "balcao.glb", + "tris": 276, + "dims_m": [ + 2.728, + 2.827, + 10.324 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "bookshelves.glb", + "tris": 45128, + "dims_m": [ + 53.279, + 540.0, + 275.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "c251.glb", + "tris": 8498, + "dims_m": [ + 1803.207, + 213.057, + 2161.42 + ], + "nodes": 47, + "meshes": 47, + "materials": 3, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "cabinet_bookcase.glb", + "tris": 73408, + "dims_m": [ + 6.504, + 4.278, + 9.913 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 2, + "tex_sizes": [ + [ + 2048, + 2048 + ], + [ + 1, + 1 + ] + ], + "max_tex": 2048, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "clothes_rack_01.glb", + "tris": 3152, + "dims_m": [ + 111.866, + 125.098, + 35.754 + ], + "nodes": 7, + "meshes": 5, + "materials": 5, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "coffee_table_l3.glb", + "tris": 2112, + "dims_m": [ + 102.0, + 102.0, + 36.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 2, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "coffeetable.glb", + "tris": 495, + "dims_m": [ + 1.208, + 0.809, + 2.687 + ], + "nodes": 1, + "meshes": 1, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "computertable.glb", + "tris": 1556, + "dims_m": [ + 0.688, + 1.173, + 1.633 + ], + "nodes": 15, + "meshes": 15, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "crate_bytyrosmith.glb", + "tris": 12, + "dims_m": [ + 2.0, + 2.0, + 2.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 320, + 320 + ] + ], + "max_tex": 320, + "draco": false, + "exts": [] + }, + { + "file": "cube_bookcase.glb", + "tris": 16960, + "dims_m": [ + 9.882, + 2.186, + 13.148 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 2048, + 2048 + ] + ], + "max_tex": 2048, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "desk_01.glb", + "tris": 94, + "dims_m": [ + 35.468, + 1.543, + 35.468 + ], + "nodes": 3, + "meshes": 2, + "materials": 4, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "garage_shelves_metal_v2.glb", + "tris": 86208, + "dims_m": [ + 91.5, + 30.5, + 220.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "ikea-cupboard.glb", + "tris": 259248, + "dims_m": [ + 4.198, + 1.698, + 4.198 + ], + "nodes": 26, + "meshes": 26, + "materials": 5, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "kitchen_table.glb", + "tris": 33728, + "dims_m": [ + 101.972, + 212.972, + 75.946 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "modernshelves.glb", + "tris": 70988, + "dims_m": [ + 295.027, + 83.321, + 162.759 + ], + "nodes": 1, + "meshes": 1, + "materials": 6, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "prop_sewing_machine_01.glb", + "tris": 4432, + "dims_m": [ + 0.645, + 0.323, + 0.308 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "record_crate_plastic_01.glb", + "tris": 18320, + "dims_m": [ + 0.298, + 0.264, + 0.408 + ], + "nodes": 5, + "meshes": 1, + "materials": 1, + "images": 3, + "tex_sizes": [ + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ] + ], + "max_tex": 4096, + "draco": false, + "exts": [] + }, + { + "file": "saloon_table.glb", + "tris": 902, + "dims_m": [ + 16, + 1.87, + 16 + ], + "nodes": 3, + "meshes": 3, + "materials": 3, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "shelf_cube_bookshelf_01.glb", + "tris": 17734, + "dims_m": [ + 1.48, + 2.143, + 2.416 + ], + "nodes": 119, + "meshes": 119, + "materials": 11, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "shelf_metal_wire_01.glb", + "tris": 330, + "dims_m": [ + 1.04, + 1.605, + 0.39 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "shelf_storage_rack_01.glb", + "tris": 960092, + "dims_m": [ + 103.998, + 235.35, + 255.102 + ], + "nodes": 15, + "meshes": 14, + "materials": 3, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "shelving.glb", + "tris": 108, + "dims_m": [ + 0.796, + 2.0, + 0.801 + ], + "nodes": 9, + "meshes": 9, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "table.glb", + "tris": 1154, + "dims_m": [ + 200, + 2.698, + 200 + ], + "nodes": 2, + "meshes": 2, + "materials": 3, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "table_01.glb", + "tris": 84, + "dims_m": [ + 1.125, + 0.816, + 2.0 + ], + "nodes": 2, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "table_02.glb", + "tris": 7578, + "dims_m": [ + 14.817, + 15.376, + 69.095 + ], + "nodes": 4, + "meshes": 4, + "materials": 4, + "images": 6, + "tex_sizes": [ + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ], + [ + 4096, + 2064 + ], + [ + 4096, + 2064 + ], + [ + 4096, + 2064 + ] + ], + "max_tex": 4096, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "table_2.glb", + "tris": 876, + "dims_m": [ + 1.949, + 0.783, + 0.979 + ], + "nodes": 6, + "meshes": 6, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "table_8_places.glb", + "tris": 49494, + "dims_m": [ + 1499.065, + 900.815, + 1352.731 + ], + "nodes": 1, + "meshes": 1, + "materials": 4, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "table_model.glb", + "tris": 21994, + "dims_m": [ + 109.967, + 9.721, + 109.967 + ], + "nodes": 16, + "meshes": 16, + "materials": 9, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "table_platform_01.glb", + "tris": 318676, + "dims_m": [ + 6.039, + 3.295, + 5.145 + ], + "nodes": 43, + "meshes": 43, + "materials": 13, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_clearcoat", + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "wardrobe.glb", + "tris": 76972, + "dims_m": [ + 9.999, + 10.987, + 16.496 + ], + "nodes": 1, + "meshes": 1, + "materials": 0, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "wardrobe_01_CHECK.glb", + "tris": 522, + "dims_m": [ + 103.061, + 4.982, + 46.011 + ], + "nodes": 5, + "meshes": 4, + "materials": 8, + "images": 4, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "warehouse_box.glb", + "tris": 78, + "dims_m": [ + 19.58, + 8.692, + 19.178 + ], + "nodes": 8, + "meshes": 8, + "materials": 3, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "wood_table.glb", + "tris": 177070, + "dims_m": [ + 40.042, + 14.879, + 40.042 + ], + "nodes": 10, + "meshes": 10, + "materials": 4, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "bench.glb", + "tris": 1942, + "dims_m": [ + 1159.351, + 2257.575, + 3225.519 + ], + "nodes": 1, + "meshes": 1, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "building_shell_01.glb", + "tris": 38583, + "dims_m": [ + 400.0, + 294.118, + 1606.937 + ], + "nodes": 45, + "meshes": 44, + "materials": 9, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "eb_hipster_bench.glb", + "tris": 640, + "dims_m": [ + 211.061, + 56.453, + 58.55 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "food_cart_bakso_01.glb", + "tris": 30316, + "dims_m": [ + 1145.052, + 800.15, + 791.091 + ], + "nodes": 101, + "meshes": 87, + "materials": 37, + "images": 28, + "tex_sizes": [ + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "FB_ngon_encoding" + ] + }, + { + "file": "longbench.glb", + "tris": 1172, + "dims_m": [ + 19.508, + 2.828, + 3.592 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 500, + 275 + ] + ], + "max_tex": 500, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "market_stall_01_CHECK.glb", + "tris": 1426, + "dims_m": [ + 159.637, + 111.522, + 69.139 + ], + "nodes": 2, + "meshes": 1, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "KHR_materials_ior", + "FB_ngon_encoding" + ] + }, + { + "file": "park_bench_01.glb", + "tris": 9884, + "dims_m": [ + 21.186, + 9.361, + 17.811 + ], + "nodes": 20, + "meshes": 18, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_volume", + "FB_ngon_encoding" + ] + }, + { + "file": "streetlight.glb", + "tris": 6592, + "dims_m": [ + 127.923, + 390.886, + 142.36 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "woodbench.glb", + "tris": 11728, + "dims_m": [ + 260.729, + 165.953, + 114.766 + ], + "nodes": 1, + "meshes": 1, + "materials": 0, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "bean_bag_chair.glb", + "tris": 4672, + "dims_m": [ + 84.513, + 83.826, + 68.942 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "bed.glb", + "tris": 1140, + "dims_m": [ + 5.657, + 1.519, + 3.402 + ], + "nodes": 7, + "meshes": 7, + "materials": 7, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "bed_king_01.glb", + "tris": 42864, + "dims_m": [ + 174.911, + 225.77, + 118.944 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "bed_modern_01.glb", + "tris": 276396, + "dims_m": [ + 5634.643, + 1309.334, + 5741.765 + ], + "nodes": 1, + "meshes": 1, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "blenderclock_blend.glb", + "tris": 10296, + "dims_m": [ + 2.334, + 2.758, + 1.158 + ], + "nodes": 13, + "meshes": 13, + "materials": 6, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [] + }, + { + "file": "circular_throw_rug.glb", + "tris": 80000, + "dims_m": [ + 172.296, + 177.992, + 3.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "dragonchest.glb", + "tris": 970, + "dims_m": [ + 52.179, + 14.149, + 53.217 + ], + "nodes": 8, + "meshes": 8, + "materials": 5, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_emissive_strength" + ] + }, + { + "file": "dresser.glb", + "tris": 5118, + "dims_m": [ + 215.239, + 205.798, + 400.0 + ], + "nodes": 1, + "meshes": 1, + "materials": 2, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "eb_ceiling_light.glb", + "tris": 328, + "dims_m": [ + 38.163, + 15.45, + 38.163 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_ior" + ] + }, + { + "file": "kitty-bed-01.glb", + "tris": 150385, + "dims_m": [ + 793.865, + 285.621, + 672.963 + ], + "nodes": 36, + "meshes": 36, + "materials": 4, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular" + ] + }, + { + "file": "living_room_set_01.glb", + "tris": 1080, + "dims_m": [ + 10.0, + 2.6, + 5.249 + ], + "nodes": 7, + "meshes": 6, + "materials": 5, + "images": 0, + "tex_sizes": [], + "max_tex": 0, + "draco": false, + "exts": [ + "KHR_materials_specular", + "KHR_materials_ior" + ] + }, + { + "file": "radiogram_furniture_v2_l1.glb", + "tris": 29500, + "dims_m": [ + 4.891, + 5.942, + 7.803 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 2048, + 2048 + ] + ], + "max_tex": 2048, + "draco": false, + "exts": [ + "KHR_materials_transmission" + ] + }, + { + "file": "tv_cabinet_01.glb", + "tris": 1536, + "dims_m": [ + 153.0, + 71.0, + 58.554 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 1024, + 1024 + ] + ], + "max_tex": 1024, + "draco": false, + "exts": [ + "KHR_materials_transmission", + "KHR_materials_specular" + ] + }, + { + "file": "dj_roomv01.glb", + "tris": 19346, + "dims_m": [ + 4.404, + 1.422, + 1.999 + ], + "nodes": 33, + "meshes": 14, + "materials": 7, + "images": 26, + "tex_sizes": [ + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 2048, + 2048 + ], + [ + 2048, + 2048 + ], + [ + 2048, + 2048 + ], + [ + 2048, + 2048 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ], + [ + 1024, + 1024 + ] + ], + "max_tex": 2048, + "draco": false, + "exts": [] + }, + { + "file": "mk2_1210_turntable_-_old.glb", + "tris": 5724, + "dims_m": [ + 64.0, + 22.566, + 49.19 + ], + "nodes": 65, + "meshes": 35, + "materials": 9, + "images": 4, + "tex_sizes": [ + [ + 128, + 128 + ], + [ + 128, + 128 + ], + [ + 2048, + 2048 + ], + [ + 2048, + 2048 + ] + ], + "max_tex": 2048, + "draco": false, + "exts": [] + }, + { + "file": "tuborg_fest_scene.glb", + "tris": 175310, + "dims_m": [ + 1482.63, + 510.303, + 503.26 + ], + "nodes": 20, + "meshes": 8, + "materials": 1, + "images": 1, + "tex_sizes": [ + [ + 4096, + 4096 + ] + ], + "max_tex": 4096, + "draco": false, + "exts": [] + }, + { + "file": "uploads_files_7115497_Backpack.glb", + "tris": 37680, + "dims_m": [ + 0.929, + 1.117, + 0.998 + ], + "nodes": 1, + "meshes": 1, + "materials": 1, + "images": 3, + "tex_sizes": [ + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ], + [ + 4096, + 4096 + ] + ], + "max_tex": 4096, + "draco": false, + "exts": [] + } +] diff --git a/pipeline/_normalize_results.json b/pipeline/_normalize_results.json new file mode 100644 index 0000000..a22f18e --- /dev/null +++ b/pipeline/_normalize_results.json @@ -0,0 +1,294 @@ +{ + "results": [ + { + "src": "shop-fittings/record_crate_plastic_01.glb", + "file": "procity_fit_record_crate_01.glb", + "out": "/tmp/procity_norm/procity_fit_record_crate_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_record_crate_01.png", + "tri_before": 18320, + "tri_after": 5000, + "size_m": [ + 0.464, + 0.41, + 0.635 + ], + "footprint": [ + 0.46, + 0.63 + ] + }, + { + "src": "shop-fittings/shelf_metal_wire_01.glb", + "file": "procity_fit_wire_shelf_01.glb", + "out": "/tmp/procity_norm/procity_fit_wire_shelf_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_wire_shelf_01.png", + "tri_before": 330, + "tri_after": 330, + "size_m": [ + 1.167, + 1.8, + 0.437 + ], + "footprint": [ + 1.17, + 0.44 + ] + }, + { + "src": "shop-fittings/clothes_rack_01.glb", + "file": "procity_fit_clothes_rack_01.glb", + "out": "/tmp/procity_norm/procity_fit_clothes_rack_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_clothes_rack_01.png", + "tri_before": 3152, + "tri_after": 3152, + "size_m": [ + 1.341, + 1.5, + 0.429 + ], + "footprint": [ + 1.34, + 0.43 + ] + }, + { + "src": "shop-fittings/bookshelves.glb", + "file": "procity_fit_bookshelf_01.glb", + "out": "/tmp/procity_norm/procity_fit_bookshelf_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_bookshelf_01.png", + "tri_before": 45128, + "tri_after": 5000, + "size_m": [ + 0.197, + 2.0, + 1.019 + ], + "footprint": [ + 0.2, + 1.02 + ] + }, + { + "src": "shop-fittings/cube_bookcase.glb", + "file": "procity_fit_cube_shelf_01.glb", + "out": "/tmp/procity_norm/procity_fit_cube_shelf_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_cube_shelf_01.png", + "tri_before": 16960, + "tri_after": 5000, + "size_m": [ + 6.328, + 1.4, + 8.42 + ], + "footprint": [ + 6.33, + 8.42 + ] + }, + { + "src": "shop-fittings/shelf_cube_bookshelf_01.glb", + "file": "procity_fit_cube_shelf_wide_01.glb", + "out": "/tmp/procity_norm/procity_fit_cube_shelf_wide_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_cube_shelf_wide_01.png", + "tri_before": 17734, + "tri_after": 4999, + "size_m": [ + 0.751, + 1.1, + 1.236 + ], + "footprint": [ + 0.75, + 1.24 + ] + }, + { + "src": "shop-fittings/balcao.glb", + "file": "procity_fit_counter_01.glb", + "out": "/tmp/procity_norm/procity_fit_counter_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_counter_01.png", + "tri_before": 276, + "tri_after": 276, + "size_m": [ + 1.062, + 1.1, + 4.018 + ], + "footprint": [ + 1.06, + 4.02 + ] + }, + { + "src": "shop-fittings/coffeetable.glb", + "file": "procity_fit_coffee_table_01.glb", + "out": "/tmp/procity_norm/procity_fit_coffee_table_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_coffee_table_01.png", + "tri_before": 495, + "tri_after": 495, + "size_m": [ + 0.671, + 0.45, + 1.494 + ], + "footprint": [ + 0.67, + 1.49 + ] + }, + { + "src": "shop-fittings/computertable.glb", + "file": "procity_fit_work_table_01.glb", + "out": "/tmp/procity_norm/procity_fit_work_table_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_work_table_01.png", + "tri_before": 1556, + "tri_after": 1556, + "size_m": [ + 0.44, + 0.75, + 1.044 + ], + "footprint": [ + 0.44, + 1.04 + ] + }, + { + "src": "shop-fittings/warehouse_box.glb", + "file": "procity_fit_box_crate_01.glb", + "out": "/tmp/procity_norm/procity_fit_box_crate_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_fit_box_crate_01.png", + "tri_before": 78, + "tri_after": 78, + "size_m": [ + 1.352, + 0.6, + 1.324 + ], + "footprint": [ + 1.35, + 1.32 + ] + }, + { + "src": "street-furniture/bench.glb", + "file": "procity_street_bench_01.glb", + "out": "/tmp/procity_norm/procity_street_bench_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_bench_01.png", + "tri_before": 1942, + "tri_after": 1942, + "size_m": [ + 0.437, + 0.85, + 1.214 + ], + "footprint": [ + 0.44, + 1.21 + ] + }, + { + "src": "street-furniture/eb_hipster_bench.glb", + "file": "procity_street_bench_modern_01.glb", + "out": "/tmp/procity_norm/procity_street_bench_modern_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_bench_modern_01.png", + "tri_before": 640, + "tri_after": 640, + "size_m": [ + 1.682, + 0.45, + 0.467 + ], + "footprint": [ + 1.68, + 0.47 + ] + }, + { + "src": "street-furniture/woodbench.glb", + "file": "procity_street_bench_wood_01.glb", + "out": "/tmp/procity_norm/procity_street_bench_wood_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_bench_wood_01.png", + "tri_before": 11728, + "tri_after": 3999, + "size_m": [ + 1.335, + 0.85, + 0.588 + ], + "footprint": [ + 1.34, + 0.59 + ] + }, + { + "src": "street-furniture/longbench.glb", + "file": "procity_street_longbench_01.glb", + "out": "/tmp/procity_norm/procity_street_longbench_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_longbench_01.png", + "tri_before": 1172, + "tri_after": 1172, + "size_m": [ + 5.864, + 0.85, + 1.08 + ], + "footprint": [ + 5.86, + 1.08 + ] + }, + { + "src": "street-furniture/park_bench_01.glb", + "file": "procity_street_park_bench_01.glb", + "out": "/tmp/procity_norm/procity_street_park_bench_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_park_bench_01.png", + "tri_before": 9884, + "tri_after": 5000, + "size_m": [ + 1.921, + 0.85, + 1.614 + ], + "footprint": [ + 1.92, + 1.61 + ] + }, + { + "src": "street-furniture/streetlight.glb", + "file": "procity_street_streetlight_01.glb", + "out": "/tmp/procity_norm/procity_street_streetlight_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_streetlight_01.png", + "tri_before": 6592, + "tri_after": 4999, + "size_m": [ + 1.636, + 5.0, + 1.821 + ], + "footprint": [ + 1.64, + 1.82 + ] + }, + { + "src": "street-furniture/food_cart_bakso_01.glb", + "file": "procity_street_food_cart_01.glb", + "out": "/tmp/procity_norm/procity_street_food_cart_01.glb", + "thumb": "/tmp/procity_norm/thumbs/procity_street_food_cart_01.png", + "tri_before": 30220, + "tri_after": 5000, + "size_m": [ + 3.003, + 2.0, + 2.075 + ], + "footprint": [ + 3.0, + 2.07 + ] + } + ], + "errors": [] +} \ No newline at end of file diff --git a/pipeline/batch.json b/pipeline/batch.json new file mode 100644 index 0000000..8d713da --- /dev/null +++ b/pipeline/batch.json @@ -0,0 +1,20 @@ +[ + {"src": "shop-fittings/record_crate_plastic_01.glb", "id": "record_crate", "category": "fit", "height_m": 0.41, "tris": 5000}, + {"src": "shop-fittings/shelf_metal_wire_01.glb", "id": "wire_shelf", "category": "fit", "height_m": 1.8, "tris": 5000}, + {"src": "shop-fittings/clothes_rack_01.glb", "id": "clothes_rack", "category": "fit", "height_m": 1.5, "tris": 5000}, + {"src": "shop-fittings/bookshelves.glb", "id": "bookshelf", "category": "fit", "height_m": 2.0, "tris": 5000}, + {"src": "shop-fittings/cube_bookcase.glb", "id": "cube_shelf", "category": "fit", "height_m": 1.4, "tris": 5000}, + {"src": "shop-fittings/shelf_cube_bookshelf_01.glb", "id": "cube_shelf_wide", "category": "fit", "height_m": 1.1, "tris": 5000}, + {"src": "shop-fittings/balcao.glb", "id": "counter", "category": "fit", "height_m": 1.1, "tris": 3000}, + {"src": "shop-fittings/coffeetable.glb", "id": "coffee_table", "category": "fit", "height_m": 0.45, "tris": 3000}, + {"src": "shop-fittings/computertable.glb", "id": "work_table", "category": "fit", "height_m": 0.75, "tris": 3000}, + {"src": "shop-fittings/warehouse_box.glb", "id": "box_crate", "category": "fit", "height_m": 0.6, "tris": 2000}, + + {"src": "street-furniture/bench.glb", "id": "bench", "category": "street", "height_m": 0.85, "tris": 3000}, + {"src": "street-furniture/eb_hipster_bench.glb", "id": "bench_modern", "category": "street", "height_m": 0.45, "tris": 3000}, + {"src": "street-furniture/woodbench.glb", "id": "bench_wood", "category": "street", "height_m": 0.85, "tris": 4000}, + {"src": "street-furniture/longbench.glb", "id": "longbench", "category": "street", "height_m": 0.85, "tris": 3000}, + {"src": "street-furniture/park_bench_01.glb", "id": "park_bench", "category": "street", "height_m": 0.85, "tris": 5000}, + {"src": "street-furniture/streetlight.glb", "id": "streetlight", "category": "street", "height_m": 5.0, "tris": 5000}, + {"src": "street-furniture/food_cart_bakso_01.glb", "id": "food_cart", "category": "street", "height_m": 2.0, "tris": 5000} +] diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py new file mode 100644 index 0000000..303ff4c --- /dev/null +++ b/pipeline/build_manifest.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +"""Generate web/assets/manifest.json — the single contract Lanes B/C/D read. + +Mechanical parts (skin listing, fitting footprints) are derived from the filesystem + +the normalizer's _normalize_results.json; the curated parts (which facade suits which +shop type) are the maps below, seeded from CITY_SPEC's registry table. Re-run any time +skins or normalized GLBs change: python3 pipeline/build_manifest.py + +Everything here is OPTIONAL at runtime — the game runs asset-free (CITY_SPEC house law). +The manifest just tells the other lanes what nicer assets exist and how big they are. +""" +import json, os, glob + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +GEN = os.path.join(ROOT, "web", "assets", "gen") +RESULTS = os.path.join(ROOT, "pipeline", "_normalize_results.json") +OUT = os.path.join(ROOT, "web", "assets", "manifest.json") +DEPOT = "https://digalot.fyi/3god" + +# ── curated: facade skin → shop types it suits ───────────────────────────────────────── +# MIRRORS Lane A's web/js/core/registry.js SHOP_TYPES[*].facades (the authoritative render-time +# pool) so there is ONE consistent mapping, plus additive-only coverage: cashtheists→pawn (a pawn +# facade registry leaves unreferenced), corrugated→stall (registry's stall pool has only `market`; +# this is stall's 2nd skin — flagged for Lane A), djsim-pub→general. +FACADE_TYPES = { + "timber-teal": ["record"], "arcade-tile": ["record"], "djsim-record": ["record"], + "weatherboard": ["opshop"], "fibro-blue": ["opshop"], "djsim-opshop": ["opshop"], + "stucco-pink": ["toy"], "deco-pastel": ["toy"], "boutique": ["toy"], + "federation": ["book"], "sandstone": ["book"], "redbrick": ["book"], + "stripmall": ["video"], "djsim-video": ["video"], + "besser": ["pawn"], "grimy": ["pawn"], "djsim-pawn": ["pawn"], + "corrugated": ["pawn", "stall"], "cashtheists": ["pawn"], + "djsim-milkbar": ["milkbar"], "brickveneer": ["milkbar"], + "terrazzo": ["dept"], "djsim-dept": ["dept"], + "market": ["stall"], + "djsim-pub": ["general"], + # ── Step-4 gap-fill fronts, generated on-device (MODELBEAST flux2-klein-4b, free) ────── + # docs/shots/laneE/facades_gen_local.png. Additive to registry.js: each fattens a thin + # pool so no type leans on one skin (video 2→4, milkbar 2→5, dept 2→4, stall 1→3). + "toy-brights": ["toy"], "toy-lolly": ["toy"], + "book-barn": ["book"], "book-scholarly": ["book"], + "video-neon": ["video"], "video-suburban": ["video"], + "milkbar-corner": ["milkbar"], "milkbar-fibro": ["milkbar"], "milkbar-deco": ["milkbar"], + "dept-anchor": ["dept"], "dept-emporium": ["dept"], + "market-fruit": ["stall"], "market-edge": ["stall"], + "warehouse-roller": ["pawn", "general"], "warehouse-tin": ["pawn", "general"], + "arcade-entry": ["general"], # arcade portal, not a shop — Lane B places by key + # residential fronts for use:'house' lots (incl. the milkbar corner-shop's street) + "res-terrace": ["house"], "res-weatherboard": ["house"], + "res-fibro": ["house"], "res-brickveneer": ["house"], +} +# facades that already carry BAKED sign text (mostly the 90sDJsim Flux ones) — Lane B should NOT +# overlay a shop name on these; the blank-signboard ones are the clean overlay targets. Verified +# by eye against docs/shots/laneE/facades_style_anchor.png. +BAKED_SIGN = {"djsim-milkbar", "djsim-video", "djsim-pub", "market", "grimy"} +# ── curated: ground skin → where it belongs ───────────────────────────────────────────── +GROUND_USE = { + "asphalt1": "road", "asphalt2": "road", "djsim-road": "road", + "footpath1": "footpath", "footpath2": "footpath", "djsim-footpath": "footpath", + "grass": "yard", "brickpave": "plaza", "gravel": "verge", "reddust": "outback", +} +# ── curated: interior floor / surface split of the tex-* skins ─────────────────────────── +FLOOR_TEX = ["carpet-swirl", "carpet-mustard", "carpet-greygreen", "lino-check", "lino-cork", + "boards-polished"] +SURFACE_TEX = { # interior wall / counter surfaces → biased shop types (hint only) + "wood-counter": ["record", "milkbar", "pawn"], "boutique-wall": ["toy", "opshop"], + "pegboard": ["pawn", "video"], "brick": ["general"], "render": ["general"], + "grimy-wall": ["pawn"], "velvet-curtain": ["toy", "record"], +} +AWNING_TEX = ["awning-red", "awning-green", "awning-blue"] + + +def rel(p): + return "gen/" + os.path.basename(p) + + +def build(): + have = {os.path.splitext(os.path.basename(f))[0] for f in glob.glob(GEN + "/*.jpg")} + + def f(cat, key): + name = f"{cat}-{key}" + return f"gen/{name}.jpg" if name in have else None + + # ---- facades ---- + facade = {} + for key, types in FACADE_TYPES.items(): + file = f("facade", key) + if file: + facade[key] = {"file": file, "types": types, + "signboard": "baked" if key in BAKED_SIGN else "blank"} + # any facade-*.jpg not in the curated map → catalogue it so nothing is orphaned. + # `-side` skins are corner-lot SIDE walls (windowless): face:"side" + empty types so no + # shop-front pool ever selects one — Lane B picks them by the facade--side.jpg name. + for name in sorted(have): + if not name.startswith("facade-") or name[len("facade-"):] in facade: + continue + key = name[len("facade-"):] + is_side = key.endswith("-side") + entry = {"file": "gen/" + name + ".jpg", + "types": [] if is_side else ["general"], + "signboard": "baked" if key in BAKED_SIGN else "blank"} + if is_side: + entry["face"] = "side" + facade[key] = entry + + # ---- skies (array; not type-specific) ---- + sky = [{"id": n[len("sky-"):], "file": "gen/" + n + ".jpg"} + for n in sorted(have) if n.startswith("sky-")] + + # ---- grounds (object keyed by id, with a use tag) ---- + ground = {} + for name in sorted(have): + if name.startswith("ground-"): + key = name[len("ground-"):] + ground[key] = {"file": "gen/" + name + ".jpg", "use": GROUND_USE.get(key, "road")} + + # ---- interior wallpapers (array) ---- + wall = [{"id": n[len("wall-"):], "file": "gen/" + n + ".jpg"} + for n in sorted(have) if n.startswith("wall-")] + + # ---- interior floors + surfaces + street awnings (from tex-*) ---- + floor = [{"id": k, "file": f("tex", k)} for k in FLOOR_TEX if f("tex", k)] + surface = [{"id": k, "file": f("tex", k), "types": t} + for k, t in SURFACE_TEX.items() if f("tex", k)] + awning = [{"id": k, "file": f("tex", k)} for k in AWNING_TEX if f("tex", k)] + + skins = { + "facade": facade, + "sky": sky, + "ground": ground, + "wall": wall, + "interior": {"floor": floor, "surface": surface}, + "awning": awning, + } + + # ---- fittings / furniture from the normalizer results ---- + fittings, furniture = {}, {} + # cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md + DROP = {"procity_fit_cube_shelf_01.glb"} + if os.path.exists(RESULTS): + res = json.load(open(RESULTS))["results"] + for r in res: + if r["file"] in DROP: + continue + fid = r["file"].replace("procity_fit_", "").replace("procity_street_", "").replace("_01.glb", "") + entry = { + "file": r["file"], + "footprint": r["footprint"], # [w, d] metres, on the ground + "height": r["size_m"][1], # metres + "thumb": "thumbs/" + r["file"].replace(".glb", ".png"), + } + (furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry + + # registry.js semantic fitting id → our physical asset id (only where a GLB exists; the rest + # stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings + # resolve to a GLB. Lane C's glb.js already maps its own kinds straight to the physical ids below. + fitting_aliases = { + "record_bins": "record_crate", "crates": "box_crate", "counter": "counter", + "clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf", + "cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table", + "bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table", + } + fitting_aliases = {k: v for k, v in fitting_aliases.items() if v in fittings} + + manifest = { + "version": 1, + "depot": DEPOT, + "conventions": { + "glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco", + "facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic " + "facing-correction (per-asset yaw is available in batch.json) — Lane B/C rotate " + "directional props (counter, food_cart) to taste when placing", + "facade_blank_signboard": "facades tagged signboard:'blank' ship a blank sign panel — " + "Lane B overlays the shop name there. signboard:'baked' facades " + "(some djsim-* Flux skins) already have sign text: use as-is, don't overlay", + "facade_face": "facade entries without a 'face' key are front elevations (shop or " + "house fronts); face:'side' entries are windowless corner-lot side " + "walls with empty types — never select one as a shop front", + "corner_lots": "corner lots expose a side wall: dedicated facade--side.jpg skins " + "now exist for brick/render/weatherboard (face:'side') — Lane B maps " + "these to the exposed side face; other types reuse the front skin", + "fallbacks": "every asset is optional — missing file => primitive/flat-colour, never a crash", + "load": "fittings/furniture load from depot as depot:; skins are local gen/", + }, + "skins": skins, + "fittings": fittings, + "furniture": furniture, + "fittingAliases": fitting_aliases, + } + with open(OUT, "w") as fh: + json.dump(manifest, fh, indent=2) + print(f"wrote {OUT}") + print(f" facades {len(facade)} skies {len(sky)} grounds {len(ground)} " + f"wallpapers {len(wall)} floors {len(floor)} surfaces {len(surface)} awnings {len(awning)}") + print(f" fittings {len(fittings)} furniture {len(furniture)}") + + +if __name__ == "__main__": + build() diff --git a/pipeline/gen_skins.py b/pipeline/gen_skins.py new file mode 100644 index 0000000..cc3f16b --- /dev/null +++ b/pipeline/gen_skins.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +"""PROCITY facade-skin gap-fill — ported from thriftgod/gen_assets.py (same engine, same bible). + +Two backends, one prompt pack. The STYLE prefix and the blank-signboard FACADE template are +lifted verbatim from thriftgod so new skins are indistinguishable from the 25 originals (Lane B +overlays the shop name onto the blank sign). + + --local (DEFAULT CHOICE) MODELBEAST flux_local → FLUX.2-klein-4B on the M3 Ultra's MPS GPU. + FREE, on-device, Apache/ungated (weights already cached). ~7s/img, one job at a time. + (cloud) OpenRouter `google/gemini-3.1-flash-image`, ~$0.004/img. Needs OPENROUTER_KEY. + + python3 pipeline/gen_skins.py --local --dry-run # list the pack, no spend, no GPU + python3 pipeline/gen_skins.py --local # generate all on-device → pipeline/.genraw/ + python3 pipeline/gen_skins.py --local --only res # just the residential family + python3 pipeline/gen_skins.py --harvest # copy approved .genraw/* → web/assets/gen/facade-*.jpg + +Everything here is a Step-4 GAP — the game already ships 25 facades and runs facade-free, so +none of this blocks anyone. `--local` costs only electricity; cloud is the fallback if MODELBEAST +is down. Review `.genraw/` by eye and delete rejects before `--harvest`. +""" +import base64, concurrent.futures, json, os, re, sys, time, urllib.request, shutil + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +RAW = os.path.join(ROOT, "pipeline", ".genraw") +GEN = os.path.join(ROOT, "web", "assets", "gen") +os.makedirs(RAW, exist_ok=True) +MODEL = os.environ.get("TG_ART_MODEL", "google/gemini-3.1-flash-image") + +KEY = os.environ.get("OPENROUTER_KEY") +if not KEY: # house convention — key never committed + kf = os.path.expanduser("~/Documents/dealgod/lore/keys.env") + if os.path.exists(kf): + for line in open(kf): + if line.startswith("OPENROUTER_KEY="): + KEY = line.strip().split("=", 1)[1] + +# ── the style bible (verbatim from thriftgod gen_assets.py:20) ────────────────────────── +STYLE = ("1990s Australian op-shop aesthetic, slightly faded and worn, warm fluorescent " + "lighting, kitsch but loving, no text unless specified, no watermarks. ") +# ── the blank-signboard shop-facade template (verbatim, gen_assets.py:69-73) ──────────── +FACADE = ("Straight-on photograph of a small single-storey Australian suburban shop facade, " + "{v}, blank empty signboard above the entrance, glass shop window and a door, " + "no text or lettering anywhere, overcast daylight, flat front elevation filling " + "the entire frame edge to edge, nothing else visible") +# ── wide anchor + windowless side-wall templates (same discipline, different geometry) ─── +WIDE = ("Straight-on photograph of a WIDE two-storey Australian department-store anchor " + "building facade, {v}, a long blank signboard band across the top, rows of ground-floor " + "display windows and a central entrance, no text or lettering anywhere, overcast daylight, " + "flat front elevation filling the entire frame edge to edge, nothing else visible") +SIDE = ("Straight-on photograph of the plain windowless SIDE wall of an Australian shop building, " + "{v}, no shopfront, no door, a faded blank rectangular ghost-sign area, no text or lettering " + "anywhere, overcast daylight, flat elevation filling the entire frame edge to edge") +HOUSE = ("Straight-on photograph of a modest Australian suburban house front, {v}, front path and " + "a bit of fence, no text anywhere, overcast daylight, flat front elevation filling the " + "entire frame edge to edge, nothing else visible") + +PROMPTS = { + # ── shop-type facades that the registry is thin on ────────────────────────────────── + **{f"facade-{k}": FACADE.format(v=v) for k, v in { + "toy-brights": "cheerful toy shop, primary-colour painted render, big low display window", + "toy-lolly": "old-fashioned lolly and toy shop, pastel candy-stripe painted front", + "book-barn": "a crammed secondhand book barn, tall dusty windows stacked with books, dark timber", + "book-scholarly": "a genteel antiquarian bookshop, dark green paint, gold-line window, brass fittings", + "video-neon": "a 1990s video rental store, glossy red and blue plastic signage band, poster light-boxes (all blank)", + "video-suburban": "a suburban strip video shop, cream brick, glass double doors, returns chute", + "milkbar-corner": "a classic corner milk bar, cream and green wall tiles, wide wrap-around window, ice-cream awning", + "milkbar-fibro": "a little fibro milk bar with a fly-screen door, a bench out front, faded soft-drink colours", + "milkbar-deco": "a streamline-deco milk bar, curved rendered corner, chrome trim, pastel mint paint", + "arcade-entry": "a covered pedestrian shopping arcade entrance, tiled archway, terrazzo floor, patterned ceiling", + "warehouse-roller": "an industrial discount warehouse shop, red brick with a big corrugated steel roller door, blank banner", + "warehouse-tin": "a corrugated-tin clearance shed shopfront, roller door half up, concrete apron", + "market-fruit": "an open fruit-and-veg shopfront, empty produce crates and astro-turf steps, striped awning", + "market-edge": "a row of humble market-square edge shopfronts under one shared posted verandah, mixed paint colours", + }.items()}, + # ── the WIDE dept-store anchor (needs a wide UV plane in Lane B) ───────────────────── + **{f"facade-{k}": WIDE.format(v=v) for k, v in { + "dept-anchor": "mid-century mustard-render and glass, cantilevered awning, grand central doors", + "dept-emporium": "a federation-era brick emporium, arched upper windows, ornate parapet", + }.items()}, + # ── corner-lot second faces: the `-side` convention (see manifest conventions) ────── + **{f"facade-{k}-side": SIDE.format(v=v) for k, v in { + "brick": "painted red brick, a downpipe, one small high window", + "weatherboard": "cream weatherboard boards, a rain downpipe", + "render": "weathered pastel painted render with hairline cracks", + }.items()}, + # ── residential terrace/cottage fronts (lots with use:'house') ────────────────────── + **{f"facade-{k}": HOUSE.format(v=v) for k, v in { + "res-terrace": "a single-fronted Victorian terrace, iron-lace verandah, dark brick, tiny front garden", + "res-weatherboard": "a modest weatherboard cottage, bull-nose verandah, picket fence", + "res-fibro": "a 1950s cream fibro house, hip roof, small steel-framed windows", + "res-brickveneer": "a 1970s brown brick-veneer house, aluminium sliding windows, carport edge", + }.items()}, +} + + +def gen(slug, prompt): + body = json.dumps({"model": MODEL, "modalities": ["image", "text"], + "messages": [{"role": "user", "content": STYLE + prompt}]}).encode() + req = urllib.request.Request("https://openrouter.ai/api/v1/chat/completions", data=body, + headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=120) as r: + d = json.load(r) + msg = d["choices"][0]["message"] + imgs = msg.get("images") or [] + url = imgs[0]["image_url"]["url"] if imgs else "" + m = re.match(r"data:image/(\w+);base64,(.+)", url, re.S) + if not m: + raise RuntimeError(f"no image in response: {str(msg)[:120]}") + fn = os.path.join(RAW, f"{slug}.{m.group(1)}") + with open(fn, "wb") as f: + f.write(base64.b64decode(m.group(2))) + return fn + + +# ── LOCAL backend: MODELBEAST flux_local (mflux / FLUX.2-klein-4B, MPS) — FREE, ~7s/img, ungated ── +import subprocess, shutil as _sh +MB = os.environ.get("MB_HOME", os.path.expanduser("~/Documents/MODELBEAST")) +FLUX_PY = os.path.join(MB, "venvs/mflux/bin/python") +FLUX_RUN = os.path.join(MB, "server/operators/flux_local/run.py") + + +def local_available(): + return os.path.exists(FLUX_PY) and os.path.exists(FLUX_RUN) + + +def gen_local(slug, prompt, w=1024, h=576, steps=4, seed=7): + outdir = os.path.join(RAW, "_job_" + slug) + os.makedirs(outdir, exist_ok=True) + params = json.dumps({"prompt": STYLE + prompt, "model": "flux2-klein-4b", + "steps": steps, "width": w, "height": h, "seed": seed, + "quantize": "8", "guidance": 3.5}) + r = subprocess.run([FLUX_PY, FLUX_RUN, "--outdir", outdir, "--params", params], + capture_output=True, text=True, timeout=300) + pngs = [p for p in os.listdir(outdir) if p.endswith(".png")] + if not pngs: + raise RuntimeError((r.stderr or r.stdout or "no png")[-160:]) + fn = os.path.join(RAW, f"{slug}.png") + _sh.move(os.path.join(outdir, pngs[0]), fn) + _sh.rmtree(outdir, ignore_errors=True) + return fn + + +def have(slug): + return any(os.path.exists(os.path.join(RAW, f"{slug}.{e}")) for e in ("png", "jpeg", "jpg", "webp")) + + +LOCAL = False # set by --local: use MODELBEAST flux_local instead of paid OpenRouter + + +def gen_one(item): + slug, prompt = item + for attempt in (1, 2): + try: + fn = gen_local(slug, prompt) if LOCAL else gen(slug, prompt) + return slug, os.path.getsize(fn) // 1024, None + except Exception as e: + if attempt == 2: + return slug, 0, str(e)[:90] + time.sleep(3) + + +def harvest(): + """Copy reviewed .genraw/*.{png,jpeg} into web/assets/gen/ as *.jpg. Manual review first — + only files you keep in .genraw/ get harvested. Uses sips if available for real JPEG.""" + n = 0 + for f in sorted(os.listdir(RAW)): + slug, ext = os.path.splitext(f) + if ext.lower() not in (".png", ".jpeg", ".jpg", ".webp"): + continue + dst = os.path.join(GEN, f"{slug}.jpg") + src = os.path.join(RAW, f) + if ext.lower() in (".jpg", ".jpeg"): + shutil.copyfile(src, dst) + else: + if os.system(f"sips -s format jpeg {src!r} --out {dst!r} >/dev/null 2>&1") != 0: + shutil.copyfile(src, dst) # fallback: copy as-is (rename) + n += 1 + print(f" harvested {slug}.jpg") + print(f"harvested {n} → {GEN} (re-run build_manifest.py to catalogue them)") + + +if __name__ == "__main__": + if "--harvest" in sys.argv: + harvest(); sys.exit(0) + LOCAL = "--local" in sys.argv # MODELBEAST flux_local: on-device, free, ungated + only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else "" + # local = one GPU job at a time (MPS won't share cleanly); cloud fans out + default_workers = 1 if LOCAL else 4 + workers = int(sys.argv[sys.argv.index("--workers") + 1]) if "--workers" in sys.argv else default_workers + todo = {s: p for s, p in PROMPTS.items() + if (not only or only in s) and not have(s)} + backend = "MODELBEAST flux2-klein-4b, local·free" if LOCAL else MODEL + cost = 0.0 if LOCAL else len(todo) * 0.004 + print(f"{len(todo)} skins to generate ({backend}) ≈ ${cost:.2f}" + (f" filter:*{only}*" if only else "")) + if "--dry-run" in sys.argv: + for s in sorted(todo): + print(f" {s}") + sys.exit(0) + if LOCAL: + if not local_available(): + print(f"ERROR: MODELBEAST flux_local missing ({FLUX_RUN}).\n" + "Set MB_HOME or check ~/Documents/MODELBEAST/server/operators/flux_local/.") + sys.exit(2) + elif not KEY: + print("ERROR: OPENROUTER_KEY not set and no ~/Documents/dealgod/lore/keys.env.\n" + "Use --local (MODELBEAST, free) or route 2: pipeline/SKIN_PROMPTS.md.") + sys.exit(2) + fails = 0 + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex: + for i, (slug, kb, err) in enumerate(ex.map(gen_one, todo.items()), 1): + if err: + fails += 1; print(f"[{i}/{len(todo)}] {slug} FAILED: {err}") + else: + print(f"[{i}/{len(todo)}] {slug} ({kb}KB)") + print(f"done, {fails} failures. Review .genraw/, delete rejects, then --harvest.") diff --git a/pipeline/glb_stat.py b/pipeline/glb_stat.py new file mode 100644 index 0000000..99e892a --- /dev/null +++ b/pipeline/glb_stat.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +"""Dependency-free GLB inspector for the PROCITY asset audit. + +Parses .glb binary containers (no Blender, no pygltflib) and reports, per file: +tri count, bounding box in metres (scale sanity), node/mesh/material/image counts, +and embedded texture pixel dimensions. Also flags likely house-GLB-law violations. + +Usage: + python3 glb_stat.py FILE.glb [FILE2.glb ...] # human table + python3 glb_stat.py --json DIR # JSON, recurse *.glb in DIR + python3 glb_stat.py --json FILE.glb # JSON for one file + +House GLB law (CITY_SPEC / LANE_E): metres, +Y up, origin at base, facing -Z, +props <=5k tris, textures <=1024, no Draco. +""" +import json, struct, sys, os, glob + +COMPONENT_SIZE = {5120: 1, 5121: 1, 5122: 2, 5123: 2, 5125: 4, 5126: 4} +TYPE_COUNT = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, + "MAT2": 4, "MAT3": 9, "MAT4": 16} + + +def _read_glb(path): + with open(path, "rb") as f: + data = f.read() + if data[:4] != b"glTF": + # maybe a .gltf JSON, or non-glb; try JSON parse + try: + return json.loads(data.decode("utf-8")), b"" + except Exception: + raise ValueError("not a GLB/glTF") + ver, length = struct.unpack(" mn[i] else 0.0 for i in range(3)] + # texture pixel sizes from embedded images (PNG/JPEG header sniff) + tex_sizes = [] + images = gltf.get("images", []) + bufviews = gltf.get("bufferViews", []) + _, binc = _read_glb(path) + for im in images: + wh = None + if "bufferView" in im and binc: + bv = bufviews[im["bufferView"]] + start = bv.get("byteOffset", 0) + blob = binc[start:start + bv.get("byteLength", 0)] + wh = _img_size(blob) + if wh: + tex_sizes.append(wh) + exts = gltf.get("extensionsUsed", []) or [] + draco = any("draco" in e.lower() for e in exts) + return { + "file": os.path.basename(path), + "tris": tris, + "dims_m": dims, # x,y,z extent in file units (hopefully metres) + "nodes": len(gltf.get("nodes", [])), + "meshes": len(meshes), + "materials": len(gltf.get("materials", [])), + "images": len(images), + "tex_sizes": tex_sizes, + "max_tex": max((max(w, h) for w, h in tex_sizes), default=0), + "draco": draco, + "exts": exts, + } + + +def _img_size(blob): + if blob[:8] == b"\x89PNG\r\n\x1a\n": + w, h = struct.unpack(">II", blob[16:24]) + return (w, h) + if blob[:2] == b"\xff\xd8": # JPEG: scan SOF markers + i = 2 + while i < len(blob) - 9: + if blob[i] != 0xFF: + i += 1 + continue + marker = blob[i + 1] + if marker in (0xC0, 0xC1, 0xC2, 0xC3): + h, w = struct.unpack(">HH", blob[i + 5:i + 9]) + return (w, h) + seglen = struct.unpack(">H", blob[i + 2:i + 4])[0] + i += 2 + seglen + if blob[:4] == b"RIFF" and blob[8:12] == b"WEBP": # WebP (normalize.py exports these) + fmt = blob[12:16] + if fmt == b"VP8 ": # lossy: 16-bit dims at offset 26 (14-bit each) + w = struct.unpack("> 6) + 1 + return (w, h) + if fmt == b"VP8X": # extended: 24-bit dims at offset 24 + w = (blob[24] | blob[25] << 8 | blob[26] << 16) + 1 + h = (blob[27] | blob[28] << 8 | blob[29] << 16) + 1 + return (w, h) + return None + + +def main(): + args = sys.argv[1:] + as_json = "--json" in args + args = [a for a in args if a != "--json"] + files = [] + for a in args: + if os.path.isdir(a): + files += sorted(glob.glob(os.path.join(a, "**", "*.glb"), recursive=True)) + else: + files.append(a) + rows = [] + for f in files: + try: + rows.append(stat(f)) + except Exception as e: + rows.append({"file": os.path.basename(f), "error": str(e)}) + if as_json: + print(json.dumps(rows, indent=2)) + return + hdr = f"{'file':38} {'tris':>7} {'dims (m) x,y,z':>22} {'mtl':>3} {'img':>3} {'maxtex':>6} draco" + print(hdr) + print("-" * len(hdr)) + for r in rows: + if "error" in r: + print(f"{r['file']:38} ERROR: {r['error']}") + continue + d = r["dims_m"] + dstr = f"{d[0]:.2f},{d[1]:.2f},{d[2]:.2f}" + print(f"{r['file']:38} {r['tris']:>7} {dstr:>22} {r['materials']:>3} " + f"{r['images']:>3} {r['max_tex']:>6} {'YES' if r['draco'] else '-'}") + + +if __name__ == "__main__": + main() diff --git a/pipeline/meshgod_batch.json b/pipeline/meshgod_batch.json new file mode 100644 index 0000000..f14e206 --- /dev/null +++ b/pipeline/meshgod_batch.json @@ -0,0 +1,23 @@ +{ + "_comment": "PROCITY hero props — the gaps the sorted library CAN'T fill (see AUDIT.md coverage table). Text->3D via MeshGod. height_m = real-world height (1 unit = 1m). Run ONLY with John's go (≈30c/solid ≈ $2.70): python -m meshgod.batch manifest pipeline/meshgod_batch.json --out assets3d/procity . BEFORE generating, check the DealGod canon-3D cache / 3GOD /api/list for reuse (cash-register + glass-case likely already exist from thriftgod_hero.json). MeshGod gotcha: thin wires / flat discs reconstruct badly — those stay in-engine primitives and are NOT in this list.", + "assets": [ + {"name": "listening-booth", "prompt": "a record shop listening station booth, angled timber panel with a shelf and hanging headphones, single stool", "height_m": 1.6, "tris": 6000, "for": "record"}, + {"name": "cash-register", "prompt": "an ornate antique brass cash register with round number keys and a bell", "height_m": 0.35, "tris": 6000, "for": "all", "reuse": "thriftgod_hero:vintage-cash-register — check depot first"}, + {"name": "glass-case", "prompt": "a retail glass display case counter with a wooden base and glass top shelves", "height_m": 1.0, "tris": 6000, "for": "toy/pawn/milkbar", "reuse": "thriftgod_hero:glass-display-case — check depot first"}, + {"name": "drinks-fridge", "prompt": "a round-shouldered vintage upright drinks fridge with a glass door and faded stickers", "height_m": 1.6, "tris": 5000, "for": "milkbar"}, + {"name": "magazine-rack", "prompt": "a slanted timber and wire magazine display rack with tiered shelves", "height_m": 1.2, "tris": 4000, "for": "milkbar/pawn"}, + {"name": "arcade-cabinet", "prompt": "a 1990s upright arcade machine cabinet shell, boxy body, sloped control panel, dark screen bezel", "height_m": 1.7, "tris": 6000, "for": "arcade/dept"}, + {"name": "milkshake-mixer", "prompt": "a chrome twin-spindle milkshake mixer machine on a diner counter, solid body", "height_m": 0.5, "tris": 4000, "for": "milkbar"}, + {"name": "novelty-record", "prompt": "an oversized thick vinyl record standing on edge as a roadside novelty ornament, solid chunky disc on a base", "height_m": 2.0, "tris": 4000, "for": "record roof-ornament"}, + {"name": "bus-shelter", "prompt": "an Australian suburban bus stop shelter, steel frame with scratched perspex panels and a bench seat", "height_m": 2.4, "tris": 8000, "for": "street"} + ], + "_primitives_instead": { + "_comment": "MeshGod gotcha assets — thin/flat geometry reconstructs badly. Lane B/C build these as in-engine primitives, NOT via MeshGod:", + "spinner-rack": "thin wire book/card spinner — extruded-cylinder primitive", + "band-poster-easel": "thin A-frame sidewalk sign — box primitive + canvas poster", + "vhs-rewinder": "tiny box on the counter — box primitive", + "returns-slot": "wall slot for the video returns — cut in the wall plane", + "escalator-prop": "dept-store escalator — extruded ramp primitive", + "pegboard-wall": "pawn/video wall — plane with tex-pegboard.jpg skin (already in gen/)" + } +} diff --git a/pipeline/normalize.py b/pipeline/normalize.py new file mode 100644 index 0000000..da0e97c --- /dev/null +++ b/pipeline/normalize.py @@ -0,0 +1,294 @@ +"""PROCITY GLB normalizer — enforce the house GLB law on library assets. + +Runs headless on ultra (Blender at /Applications/Blender.app/Contents/MacOS/Blender): + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python normalize.py -- BATCH.json OUT_DIR [THUMB_DIR] + +Processes every entry in BATCH.json in ONE Blender session (clean scene between +assets — no per-asset relaunch). For each asset it: + + * imports (Blender applies the glTF Y-up convention → correct world orientation) + * joins to a single mesh, applies transforms + * optional up-axis correction + yaw so the asset stands up and faces -Z + * base-origin: centre in X/Y, sit on the floor (min Z = 0) [house law] + * height-normalises to a real-world metre height [house law] + * decimates to a triangle budget (props <=5k) [house law] + * optional self-illuminate (TRELLIS/dark-bake assets) (finish_glb.py:88) + * downsizes every texture to <=1024px and exports them as WebP [house law] + * exports GLB, +Y up, embedded textures, NO Draco [house law] + * renders a 256px hero thumbnail (workbench, textured) [step 2] + +Output names: procity___01.glb (e.g. procity_fit_record_crate_01.glb) + +The heavy finish logic (solidify/decimate/bright/base-origin) is lifted verbatim +from MESHGOD/scripts/finish_glb.py — this is the same law, extended for the +PROCITY batch + texture-webp + thumbnail, per the lane rule "extend, don't fork". + +BATCH.json schema (list of): + { "src": "shop-fittings/balcao.glb", # relative to --root (default ~/Documents/3D=models) + "id": "counter", "category": "fit", # → procity_fit_counter_01.glb + "height_m": 1.1, # target height (0 = keep source scale) + "tris": 3000, # triangle budget + "up": "none", # none|x+90|x-90|y+90|y-90 (stand a lying asset up) + "yaw": 0, # degrees about up-axis, so front faces -Z + "solidify": false, # cap open bottoms (TRELLIS scans only) + "bright": false } # self-illuminate dark-baked albedo +""" +import bpy, sys, os, math, json +from mathutils import Vector + +ARGV = sys.argv[sys.argv.index("--") + 1:] +BATCH = ARGV[0] +OUT_DIR = ARGV[1] if len(ARGV) > 1 else "/tmp/procity_norm" +THUMB_DIR = ARGV[2] if len(ARGV) > 2 else os.path.join(OUT_DIR, "thumbs") +ROOT = os.environ.get("PROCITY_MODELS_ROOT", os.path.expanduser("~/Documents/3D=models")) +os.makedirs(OUT_DIR, exist_ok=True) +os.makedirs(THUMB_DIR, exist_ok=True) + +UP_ROT = { # extra rotation to stand a mis-authored asset upright + "none": (0, 0, 0), "x+90": (math.radians(90), 0, 0), "x-90": (math.radians(-90), 0, 0), + "y+90": (0, math.radians(90), 0), "y-90": (0, math.radians(-90), 0), + "z+90": (0, 0, math.radians(90)), "z-90": (0, 0, math.radians(-90)), +} + + +def wipe(): + for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.images): + for d in list(blk): + if d.users == 0: + blk.remove(d) + + +def bounds(obj): + bb = [obj.matrix_world @ Vector(c) for c in obj.bound_box] + mn = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb))) + mx = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb))) + return mn, mx + + +def import_join(src): + ext = os.path.splitext(src)[1].lower() + if ext in (".glb", ".gltf"): + bpy.ops.import_scene.gltf(filepath=src) + elif ext == ".fbx": + bpy.ops.import_scene.fbx(filepath=src) + elif ext == ".obj": + bpy.ops.wm.obj_import(filepath=src) + else: + raise ValueError("unsupported: " + ext) + meshes = [o for o in bpy.data.objects if o.type == 'MESH'] + if not meshes: + raise ValueError("no meshes") + # BAKE ancestor transforms into each mesh, then drop non-mesh nodes. glТF imports often park + # the mesh under a parent empty carrying unit-conversion scale / pivot offset — joining + + # transform_apply alone leaves that parent node in the export (asset ships off-origin & scaled, + # violating "origin at base"). parent_clear KEEP_TRANSFORM folds matrix_world into each basis. + bpy.ops.object.select_all(action='DESELECT') + for m in meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = meshes[0] + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + for o in list(bpy.data.objects): + if o.type != 'MESH': # empties, armatures, cameras, lights + bpy.data.objects.remove(o, do_unlink=True) + meshes = [o for o in bpy.data.objects if o.type == 'MESH'] + bpy.ops.object.select_all(action='DESELECT') + for m in meshes: + m.select_set(True) + bpy.context.view_layer.objects.active = meshes[0] + if len(meshes) > 1: + bpy.ops.object.join() + obj = bpy.context.view_layer.objects.active + obj.modifiers.clear() # drop armature/etc modifiers (static props) + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + return obj + + +def clean_mesh(obj): + """Drop stray/loose geometry that inflates the bbox (dumped assets are full of it): + weld doubles, delete loose verts/edges, then clip statistical outlier verts + (|coord-median| > 6*MAD on any axis) — the stray points that make a table read 55m.""" + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(threshold=0.0001) + bpy.ops.mesh.delete_loose(use_verts=True, use_edges=True, use_faces=False) + bpy.ops.mesh.quads_convert_to_tris() # so len(polygons)==triangle count: the ≤5k budget + bpy.ops.object.mode_set(mode='OBJECT') # gate + reported tri counts stay honest (glTF triangulates on export anyway) + vs = obj.data.vertices + n = len(vs) + if n < 20: + return 0 + outliers = [] + for axis in range(3): + vals = sorted(v.co[axis] for v in vs) + med = vals[n // 2] + mad = sorted(abs(x - med) for x in vals)[n // 2] or 1e-6 + lo, hi = med - 6 * mad * 1.4826, med + 6 * mad * 1.4826 + for v in vs: + if v.co[axis] < lo or v.co[axis] > hi: + outliers.append(v.index) + outliers = set(outliers) + if outliers and len(outliers) < n * 0.03: # only if they are a tiny minority + for v in vs: + v.select = v.index in outliers + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.delete(type='VERT') + bpy.ops.object.mode_set(mode='OBJECT') + return len(outliers) + return 0 + + +def resize_textures(max_px=1024): + for img in bpy.data.images: + if img.size[0] > max_px or img.size[1] > max_px: + w, h = img.size + s = max_px / max(w, h) + img.scale(max(1, int(w * s)), max(1, int(h * s))) + + +def normalize_one(spec): + wipe() + src = os.path.join(ROOT, spec["src"]) if not os.path.isabs(spec["src"]) else spec["src"] + obj = import_join(src) + clipped = clean_mesh(obj) # welds, drops loose geo, TRIANGULATES + tri_before = len(obj.data.polygons) # true source triangle count (post-triangulate) + + # stand upright (if mis-authored), then face -Z + rx, ry, rz = UP_ROT.get(spec.get("up", "none"), (0, 0, 0)) + obj.rotation_euler = (rx, ry, rz) + if spec.get("yaw"): + obj.rotation_euler[2] += math.radians(spec["yaw"]) + bpy.ops.object.transform_apply(rotation=True) + + # solidify: cap open bottoms → watertight, white sole (finish_glb.py:54) + if spec.get("solidify"): + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.remove_doubles(threshold=0.0002) + n0 = len(obj.data.polygons) + bpy.ops.mesh.fill_holes(sides=0) + bpy.ops.mesh.normals_make_consistent(inside=False) + bpy.ops.object.mode_set(mode='OBJECT') + n1 = len(obj.data.polygons) + if n1 > n0: + white = bpy.data.materials.new("sole_white"); white.use_nodes = True + b = white.node_tree.nodes.get("Principled BSDF") + if b: + b.inputs["Base Color"].default_value = (1, 1, 1, 1) + obj.data.materials.append(white) + widx = len(obj.data.materials) - 1 + for i in range(n0, n1): + obj.data.polygons[i].material_index = widx + + # decimate to budget (finish_glb.py:80) + tris = len(obj.data.polygons) + budget = spec.get("tris", 5000) + if tris > budget: + m = obj.modifiers.new('decimate', 'DECIMATE') + m.ratio = max(0.01, budget / tris) + bpy.ops.object.modifier_apply(modifier=m.name) + tri_after = len(obj.data.polygons) + + # self-illuminate dark bakes (finish_glb.py:88) + if spec.get("bright"): + for m in bpy.data.materials: + if not m.use_nodes: + continue + b = m.node_tree.nodes.get("Principled BSDF") + if not b: + continue + bc = b.inputs.get("Base Color"); ec = b.inputs.get("Emission Color") + es = b.inputs.get("Emission Strength") + if ec is None: + continue + if bc and bc.is_linked: + m.node_tree.links.new(bc.links[0].from_socket, ec) + elif bc: + ec.default_value = bc.default_value + if es is not None: + es.default_value = 0.32 + + # base-origin + height-normalise (finish_glb.py:109) + mn, mx = bounds(obj) + h = spec.get("height_m", 0) + if h and h > 0: + cur = (mx.z - mn.z) or 1.0 + obj.scale *= (h / cur) + bpy.ops.object.transform_apply(scale=True) + mn, mx = bounds(obj) + base = Vector(((mn.x + mx.x) / 2, (mn.y + mx.y) / 2, mn.z)) + obj.location -= base + bpy.ops.object.transform_apply(location=True) + mn, mx = bounds(obj) + + resize_textures(1024) + + name = f"procity_{spec['category']}_{spec['id']}_01.glb" + out = os.path.join(OUT_DIR, name) + bpy.ops.export_scene.gltf( + filepath=out, export_format='GLB', export_yup=True, export_apply=True, + export_image_format='WEBP', export_image_quality=85, + ) + thumb = render_thumb(name.replace(".glb", ".png")) + return { + "src": spec["src"], "file": name, "out": out, "thumb": thumb, + "tri_before": tri_before, "tri_after": tri_after, + "size_m": [round(mx.x - mn.x, 3), round(mx.z - mn.z, 3), round(mx.y - mn.y, 3)], + "footprint": [round(mx.x - mn.x, 2), round(mx.y - mn.y, 2)], + } + + +def render_thumb(png_name): + """One 3/4 hero shot, 256px, textured workbench (render_glb.py lighting).""" + scn = bpy.context.scene + obj = bpy.context.view_layer.objects.active + mn, mx = bounds(obj) + ctr = (mn + mx) / 2 + size = max((mx - mn).x, (mx - mn).y, (mx - mn).z) or 1.0 + for o in list(bpy.data.objects): + if o.type in ('CAMERA', 'LIGHT'): + bpy.data.objects.remove(o, do_unlink=True) + w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw") + scn.world = w; w.use_nodes = True + w.node_tree.nodes["Background"].inputs[1].default_value = 1.05 + bpy.ops.object.light_add(type='AREA', location=(ctr.x + size, ctr.y - size, mx.z + size)) + bpy.context.object.data.energy = 800 * size; bpy.context.object.data.size = 6 + bpy.ops.object.camera_add(); cam = bpy.context.object; scn.camera = cam + r = size * 1.9 + cam.location = Vector((ctr.x + r * 0.8, ctr.y - r, ctr.z + size * 0.6)) + d = ctr - cam.location + cam.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler() + scn.render.engine = 'BLENDER_WORKBENCH' + scn.display.shading.light = 'STUDIO' + scn.display.shading.color_type = 'TEXTURE' + scn.display.shading.show_cavity = True + scn.view_settings.view_transform = 'Standard' + scn.render.film_transparent = True + scn.render.resolution_x = scn.render.resolution_y = 256 + p = os.path.join(THUMB_DIR, png_name) + scn.render.filepath = p + bpy.ops.render.render(write_still=True) + return p + + +def main(): + specs = json.load(open(BATCH)) + results, errors = [], [] + for spec in specs: + try: + r = normalize_one(spec) + results.append(r) + print(f"OK {r['file']:34} tris {r['tri_before']}→{r['tri_after']:>5} " + f"size {r['size_m']} footprint {r['footprint']}") + except Exception as e: + errors.append({"src": spec.get("src"), "id": spec.get("id"), "error": str(e)}) + print(f"ERR {spec.get('src')}: {e}") + json.dump({"results": results, "errors": errors}, + open(os.path.join(OUT_DIR, "_results.json"), "w"), indent=2) + print(f"\nNORMALIZE_DONE ok={len(results)} err={len(errors)} → {OUT_DIR}/_results.json") + + +main() diff --git a/pipeline/publish.py b/pipeline/publish.py new file mode 100644 index 0000000..6c258e3 --- /dev/null +++ b/pipeline/publish.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Publish the normalized PROCITY GLBs + thumbnails to the shared 3GOD depot. + +Mirrors the house push contract (meshgod/server.py:_push_3god) exactly: + * auth cookie: 3g = hmac_sha256(GOD3_PW, "3god-ok").hexdigest() + * GLB POST {depot}/api/upload?name= body=glb model/gltf-binary + * thumb POST {depot}/api/thumb?name= body=png image/png + +Credentials come from the environment — this script never contains or logs the secret: + export GOD3_PW=... # the shared depot password (ask John) + export GOD3_DEPOT=https://digalot.fyi/3god # optional; this is the default + python3 pipeline/publish.py # publish every GLB in _normalized/ + python3 pipeline/publish.py --dry-run # list what WOULD upload, no network + +This is the one outward-facing step in the lane; run it deliberately. Uploads are +idempotent (same name overwrites) and reversible via the depot's own delete. +""" +import os, sys, glob, hmac, hashlib, json, urllib.request, urllib.parse + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +NORM = os.path.join(ROOT, "pipeline", "_normalized") +THUMBS = os.path.join(ROOT, "web", "assets", "thumbs") +DEPOT = os.environ.get("GOD3_DEPOT", "https://digalot.fyi/3god").rstrip("/") +PW = os.environ.get("GOD3_PW", "") +DRY = "--dry-run" in sys.argv +# assets intentionally excluded from publish (see AUDIT.md) +SKIP = {"procity_fit_cube_shelf_01.glb"} + + +def cookie(): + tok = hmac.new(PW.encode(), b"3god-ok", hashlib.sha256).hexdigest() + return f"3g={tok}" + + +def post(path, body, ctype): + req = urllib.request.Request(DEPOT + path, data=body, method="POST", + headers={"Content-Type": ctype, "Cookie": cookie()}) + with urllib.request.urlopen(req, timeout=120) as r: + return r.status, r.read().decode("utf-8", "replace")[:200] + + +def main(): + glbs = sorted(g for g in glob.glob(NORM + "/*.glb") if os.path.basename(g) not in SKIP) + if not glbs: + print(f"no GLBs in {NORM} — run normalize.py first"); sys.exit(1) + print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}") + if not DRY and not PW: + print("ERROR: GOD3_PW not set in environment. Ask John for the depot password, then:\n" + " export GOD3_PW=... && python3 pipeline/publish.py") + sys.exit(2) + published = [] + for g in glbs: + name = os.path.basename(g) + thumb = os.path.join(THUMBS, name.replace(".glb", ".png")) + size = os.path.getsize(g) + if DRY: + print(f" would upload {name:38} {size//1024:>5}KB thumb={os.path.exists(thumb)}") + continue + try: + st, msg = post("/api/upload?name=" + urllib.parse.quote(name), + open(g, "rb").read(), "model/gltf-binary") + tstat = "" + if os.path.exists(thumb): + ts, _ = post("/api/thumb?name=" + urllib.parse.quote(name.replace(".glb", ".png")), + open(thumb, "rb").read(), "image/png") + tstat = f" thumb:{ts}" + ok = st in (200, 201) + print(f" {'OK ' if ok else 'ERR'} {name:38} glb:{st}{tstat}") + if ok: + published.append(name) + except Exception as e: + print(f" ERR {name:38} {e}") + if not DRY: + json.dump(published, open(os.path.join(ROOT, "pipeline", "_published.json"), "w"), indent=2) + print(f"\npublished {len(published)}/{len(glbs)} → depot. verify: curl -s {DEPOT}/api/list") + + +if __name__ == "__main__": + main() diff --git a/pipeline/validate_manifest.py b/pipeline/validate_manifest.py new file mode 100644 index 0000000..6cff25e --- /dev/null +++ b/pipeline/validate_manifest.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Validate web/assets/manifest.json — runs in Lane F's integration gate. + +Checks: JSON parses; every referenced skin exists locally; every fitting/furniture GLB +exists locally (pipeline/_normalized) OR HEADs 200 on the depot; every thumb exists; +footprints & heights are sane; every registry shop type has >=2 facades. + + python3 pipeline/validate_manifest.py # local + soft depot check + python3 pipeline/validate_manifest.py --depot # require GLBs live on the depot (post-publish) + +Exit 0 = green, 1 = fail. Plain stdlib (no deps), so it runs anywhere. +""" +import json, os, sys, urllib.request, urllib.error + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ASSETS = os.path.join(ROOT, "web", "assets") +NORM = os.path.join(ROOT, "pipeline", "_normalized") +MANIFEST = os.path.join(ASSETS, "manifest.json") +REGISTRY_TYPES = ["record", "opshop", "toy", "book", "video", "pawn", "milkbar", "dept", "stall"] +STRICT_DEPOT = "--depot" in sys.argv + +errors, warnings = [], [] + + +def err(m): errors.append(m) +def warn(m): warnings.append(m) + + +def head_ok(url): + # The depot contract documents GET only, not HEAD — probe with a 1-byte Range GET so we don't + # download the whole GLB and don't depend on HEAD being supported by the CDN/cache. + try: + req = urllib.request.Request(url, headers={"Range": "bytes=0-0"}) + with urllib.request.urlopen(req, timeout=12) as r: + return r.status in (200, 206) + except urllib.error.HTTPError as e: + if e.code == 416: # range not satisfiable but the file exists + return True + return False + except Exception: + return False + + +def check_skin(file): + p = os.path.join(ASSETS, file) + if not os.path.isfile(p): + err(f"skin missing: {file}") + + +def check_glb(entry, depot): + file = entry["file"] + local = os.path.join(NORM, file) + on_disk = os.path.isfile(local) + live = head_ok(f"{depot}/a/{file}") + if STRICT_DEPOT and not live: + err(f"GLB not live on depot: {file}") + elif not on_disk and not live: + err(f"GLB missing (not local, not on depot): {file}") + elif not live: + warn(f"GLB not yet published to depot (local only): {file}") + # thumb + thumb = entry.get("thumb") + if thumb and not os.path.isfile(os.path.join(ASSETS, thumb)): + err(f"thumb missing: {thumb}") + # footprint / height sanity + fp = entry.get("footprint") + if not (isinstance(fp, list) and len(fp) == 2 and all(0 < x < 12 for x in fp)): + err(f"insane footprint for {file}: {fp}") + h = entry.get("height") + if not (isinstance(h, (int, float)) and 0 < h < 8): + err(f"insane height for {file}: {h}") + + +def main(): + try: + m = json.load(open(MANIFEST)) + except Exception as e: + print(f"FAIL: manifest does not parse: {e}") + sys.exit(1) + + depot = m.get("depot", "https://digalot.fyi/3god") + sk = m.get("skins", {}) + + # facades + type coverage + facade = sk.get("facade", {}) + for k, v in facade.items(): + check_skin(v["file"]) + for t in REGISTRY_TYPES: + n = sum(1 for v in facade.values() if t in v.get("types", [])) + if n < 2: + err(f"shop type '{t}' has only {n} facade(s) (need >=2)") + + # other skin groups + for s in sk.get("sky", []): + check_skin(s["file"]) + for g in sk.get("ground", {}).values(): + check_skin(g["file"]) + for w in sk.get("wall", []): + check_skin(w["file"]) + interior = sk.get("interior", {}) + for grp in ("floor", "surface"): + for it in interior.get(grp, []): + check_skin(it["file"]) + for a in sk.get("awning", []): + check_skin(a["file"]) + + # fittings + furniture GLBs + n_glb = 0 + for grp in ("fittings", "furniture"): + for entry in m.get(grp, {}).values(): + check_glb(entry, depot) + n_glb += 1 + + print(f"manifest v{m.get('version')} — facades {len(facade)}, " + f"skins {sum(len(v) if isinstance(v, list) else (len(v) if isinstance(v, dict) else 0) for v in sk.values())} groups, " + f"GLBs {n_glb}") + for w in warnings: + print(f" WARN {w}") + if errors: + for e in errors: + print(f" ERR {e}") + print(f"\nFAIL — {len(errors)} error(s), {len(warnings)} warning(s)") + sys.exit(1) + print(f"\nOK — 0 errors, {len(warnings)} warning(s)") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/web/assets/gen/facade-arcade-entry.jpg b/web/assets/gen/facade-arcade-entry.jpg new file mode 100644 index 0000000..a57768c Binary files /dev/null and b/web/assets/gen/facade-arcade-entry.jpg differ diff --git a/web/assets/gen/facade-book-barn.jpg b/web/assets/gen/facade-book-barn.jpg new file mode 100644 index 0000000..2d3e8ff Binary files /dev/null and b/web/assets/gen/facade-book-barn.jpg differ diff --git a/web/assets/gen/facade-book-scholarly.jpg b/web/assets/gen/facade-book-scholarly.jpg new file mode 100644 index 0000000..74aeeac Binary files /dev/null and b/web/assets/gen/facade-book-scholarly.jpg differ diff --git a/web/assets/gen/facade-brick-side.jpg b/web/assets/gen/facade-brick-side.jpg new file mode 100644 index 0000000..00ffa63 Binary files /dev/null and b/web/assets/gen/facade-brick-side.jpg differ diff --git a/web/assets/gen/facade-dept-anchor.jpg b/web/assets/gen/facade-dept-anchor.jpg new file mode 100644 index 0000000..970650d Binary files /dev/null and b/web/assets/gen/facade-dept-anchor.jpg differ diff --git a/web/assets/gen/facade-dept-emporium.jpg b/web/assets/gen/facade-dept-emporium.jpg new file mode 100644 index 0000000..0b8dc1f Binary files /dev/null and b/web/assets/gen/facade-dept-emporium.jpg differ diff --git a/web/assets/gen/facade-market-edge.jpg b/web/assets/gen/facade-market-edge.jpg new file mode 100644 index 0000000..16ef987 Binary files /dev/null and b/web/assets/gen/facade-market-edge.jpg differ diff --git a/web/assets/gen/facade-market-fruit.jpg b/web/assets/gen/facade-market-fruit.jpg new file mode 100644 index 0000000..af33411 Binary files /dev/null and b/web/assets/gen/facade-market-fruit.jpg differ diff --git a/web/assets/gen/facade-milkbar-corner.jpg b/web/assets/gen/facade-milkbar-corner.jpg new file mode 100644 index 0000000..6e1a699 Binary files /dev/null and b/web/assets/gen/facade-milkbar-corner.jpg differ diff --git a/web/assets/gen/facade-milkbar-deco.jpg b/web/assets/gen/facade-milkbar-deco.jpg new file mode 100644 index 0000000..8fa45a1 Binary files /dev/null and b/web/assets/gen/facade-milkbar-deco.jpg differ diff --git a/web/assets/gen/facade-milkbar-fibro.jpg b/web/assets/gen/facade-milkbar-fibro.jpg new file mode 100644 index 0000000..e6b040a Binary files /dev/null and b/web/assets/gen/facade-milkbar-fibro.jpg differ diff --git a/web/assets/gen/facade-render-side.jpg b/web/assets/gen/facade-render-side.jpg new file mode 100644 index 0000000..dba9a5b Binary files /dev/null and b/web/assets/gen/facade-render-side.jpg differ diff --git a/web/assets/gen/facade-res-brickveneer.jpg b/web/assets/gen/facade-res-brickveneer.jpg new file mode 100644 index 0000000..cffe218 Binary files /dev/null and b/web/assets/gen/facade-res-brickveneer.jpg differ diff --git a/web/assets/gen/facade-res-fibro.jpg b/web/assets/gen/facade-res-fibro.jpg new file mode 100644 index 0000000..9eac3ef Binary files /dev/null and b/web/assets/gen/facade-res-fibro.jpg differ diff --git a/web/assets/gen/facade-res-terrace.jpg b/web/assets/gen/facade-res-terrace.jpg new file mode 100644 index 0000000..4bf9c3f Binary files /dev/null and b/web/assets/gen/facade-res-terrace.jpg differ diff --git a/web/assets/gen/facade-res-weatherboard.jpg b/web/assets/gen/facade-res-weatherboard.jpg new file mode 100644 index 0000000..8c93552 Binary files /dev/null and b/web/assets/gen/facade-res-weatherboard.jpg differ diff --git a/web/assets/gen/facade-toy-brights.jpg b/web/assets/gen/facade-toy-brights.jpg new file mode 100644 index 0000000..eac61e3 Binary files /dev/null and b/web/assets/gen/facade-toy-brights.jpg differ diff --git a/web/assets/gen/facade-toy-lolly.jpg b/web/assets/gen/facade-toy-lolly.jpg new file mode 100644 index 0000000..f0c90f2 Binary files /dev/null and b/web/assets/gen/facade-toy-lolly.jpg differ diff --git a/web/assets/gen/facade-video-neon.jpg b/web/assets/gen/facade-video-neon.jpg new file mode 100644 index 0000000..6f496f7 Binary files /dev/null and b/web/assets/gen/facade-video-neon.jpg differ diff --git a/web/assets/gen/facade-video-suburban.jpg b/web/assets/gen/facade-video-suburban.jpg new file mode 100644 index 0000000..a534407 Binary files /dev/null and b/web/assets/gen/facade-video-suburban.jpg differ diff --git a/web/assets/gen/facade-warehouse-roller.jpg b/web/assets/gen/facade-warehouse-roller.jpg new file mode 100644 index 0000000..dd760cd Binary files /dev/null and b/web/assets/gen/facade-warehouse-roller.jpg differ diff --git a/web/assets/gen/facade-warehouse-tin.jpg b/web/assets/gen/facade-warehouse-tin.jpg new file mode 100644 index 0000000..a920bd7 Binary files /dev/null and b/web/assets/gen/facade-warehouse-tin.jpg differ diff --git a/web/assets/gen/facade-weatherboard-side.jpg b/web/assets/gen/facade-weatherboard-side.jpg new file mode 100644 index 0000000..8f2c10e Binary files /dev/null and b/web/assets/gen/facade-weatherboard-side.jpg differ diff --git a/web/assets/manifest.json b/web/assets/manifest.json new file mode 100644 index 0000000..313776e --- /dev/null +++ b/web/assets/manifest.json @@ -0,0 +1,730 @@ +{ + "version": 1, + "depot": "https://digalot.fyi/3god", + "conventions": { + "glb_law": "metres, +Y up, origin at base (minY=0, centred X/Z), <=5k tris, WebP <=1024, no Draco", + "facing": "symmetric props are orientation-agnostic; normalize.py applies no automatic facing-correction (per-asset yaw is available in batch.json) \u2014 Lane B/C rotate directional props (counter, food_cart) to taste when placing", + "facade_blank_signboard": "facades tagged signboard:'blank' ship a blank sign panel \u2014 Lane B overlays the shop name there. signboard:'baked' facades (some djsim-* Flux skins) already have sign text: use as-is, don't overlay", + "facade_face": "facade entries without a 'face' key are front elevations (shop or house fronts); face:'side' entries are windowless corner-lot side walls with empty types \u2014 never select one as a shop front", + "corner_lots": "corner lots expose a side wall: dedicated facade--side.jpg skins now exist for brick/render/weatherboard (face:'side') \u2014 Lane B maps these to the exposed side face; other types reuse the front skin", + "fallbacks": "every asset is optional \u2014 missing file => primitive/flat-colour, never a crash", + "load": "fittings/furniture load from depot as depot:; skins are local gen/" + }, + "skins": { + "facade": { + "timber-teal": { + "file": "gen/facade-timber-teal.jpg", + "types": [ + "record" + ], + "signboard": "blank" + }, + "arcade-tile": { + "file": "gen/facade-arcade-tile.jpg", + "types": [ + "record" + ], + "signboard": "blank" + }, + "djsim-record": { + "file": "gen/facade-djsim-record.jpg", + "types": [ + "record" + ], + "signboard": "blank" + }, + "weatherboard": { + "file": "gen/facade-weatherboard.jpg", + "types": [ + "opshop" + ], + "signboard": "blank" + }, + "fibro-blue": { + "file": "gen/facade-fibro-blue.jpg", + "types": [ + "opshop" + ], + "signboard": "blank" + }, + "djsim-opshop": { + "file": "gen/facade-djsim-opshop.jpg", + "types": [ + "opshop" + ], + "signboard": "blank" + }, + "stucco-pink": { + "file": "gen/facade-stucco-pink.jpg", + "types": [ + "toy" + ], + "signboard": "blank" + }, + "deco-pastel": { + "file": "gen/facade-deco-pastel.jpg", + "types": [ + "toy" + ], + "signboard": "blank" + }, + "boutique": { + "file": "gen/facade-boutique.jpg", + "types": [ + "toy" + ], + "signboard": "blank" + }, + "federation": { + "file": "gen/facade-federation.jpg", + "types": [ + "book" + ], + "signboard": "blank" + }, + "sandstone": { + "file": "gen/facade-sandstone.jpg", + "types": [ + "book" + ], + "signboard": "blank" + }, + "redbrick": { + "file": "gen/facade-redbrick.jpg", + "types": [ + "book" + ], + "signboard": "blank" + }, + "stripmall": { + "file": "gen/facade-stripmall.jpg", + "types": [ + "video" + ], + "signboard": "blank" + }, + "djsim-video": { + "file": "gen/facade-djsim-video.jpg", + "types": [ + "video" + ], + "signboard": "baked" + }, + "besser": { + "file": "gen/facade-besser.jpg", + "types": [ + "pawn" + ], + "signboard": "blank" + }, + "grimy": { + "file": "gen/facade-grimy.jpg", + "types": [ + "pawn" + ], + "signboard": "baked" + }, + "djsim-pawn": { + "file": "gen/facade-djsim-pawn.jpg", + "types": [ + "pawn" + ], + "signboard": "blank" + }, + "corrugated": { + "file": "gen/facade-corrugated.jpg", + "types": [ + "pawn", + "stall" + ], + "signboard": "blank" + }, + "cashtheists": { + "file": "gen/facade-cashtheists.jpg", + "types": [ + "pawn" + ], + "signboard": "blank" + }, + "djsim-milkbar": { + "file": "gen/facade-djsim-milkbar.jpg", + "types": [ + "milkbar" + ], + "signboard": "baked" + }, + "brickveneer": { + "file": "gen/facade-brickveneer.jpg", + "types": [ + "milkbar" + ], + "signboard": "blank" + }, + "terrazzo": { + "file": "gen/facade-terrazzo.jpg", + "types": [ + "dept" + ], + "signboard": "blank" + }, + "djsim-dept": { + "file": "gen/facade-djsim-dept.jpg", + "types": [ + "dept" + ], + "signboard": "blank" + }, + "market": { + "file": "gen/facade-market.jpg", + "types": [ + "stall" + ], + "signboard": "baked" + }, + "djsim-pub": { + "file": "gen/facade-djsim-pub.jpg", + "types": [ + "general" + ], + "signboard": "baked" + }, + "toy-brights": { + "file": "gen/facade-toy-brights.jpg", + "types": [ + "toy" + ], + "signboard": "blank" + }, + "toy-lolly": { + "file": "gen/facade-toy-lolly.jpg", + "types": [ + "toy" + ], + "signboard": "blank" + }, + "book-barn": { + "file": "gen/facade-book-barn.jpg", + "types": [ + "book" + ], + "signboard": "blank" + }, + "book-scholarly": { + "file": "gen/facade-book-scholarly.jpg", + "types": [ + "book" + ], + "signboard": "blank" + }, + "video-neon": { + "file": "gen/facade-video-neon.jpg", + "types": [ + "video" + ], + "signboard": "blank" + }, + "video-suburban": { + "file": "gen/facade-video-suburban.jpg", + "types": [ + "video" + ], + "signboard": "blank" + }, + "milkbar-corner": { + "file": "gen/facade-milkbar-corner.jpg", + "types": [ + "milkbar" + ], + "signboard": "blank" + }, + "milkbar-fibro": { + "file": "gen/facade-milkbar-fibro.jpg", + "types": [ + "milkbar" + ], + "signboard": "blank" + }, + "milkbar-deco": { + "file": "gen/facade-milkbar-deco.jpg", + "types": [ + "milkbar" + ], + "signboard": "blank" + }, + "dept-anchor": { + "file": "gen/facade-dept-anchor.jpg", + "types": [ + "dept" + ], + "signboard": "blank" + }, + "dept-emporium": { + "file": "gen/facade-dept-emporium.jpg", + "types": [ + "dept" + ], + "signboard": "blank" + }, + "market-fruit": { + "file": "gen/facade-market-fruit.jpg", + "types": [ + "stall" + ], + "signboard": "blank" + }, + "market-edge": { + "file": "gen/facade-market-edge.jpg", + "types": [ + "stall" + ], + "signboard": "blank" + }, + "warehouse-roller": { + "file": "gen/facade-warehouse-roller.jpg", + "types": [ + "pawn", + "general" + ], + "signboard": "blank" + }, + "warehouse-tin": { + "file": "gen/facade-warehouse-tin.jpg", + "types": [ + "pawn", + "general" + ], + "signboard": "blank" + }, + "arcade-entry": { + "file": "gen/facade-arcade-entry.jpg", + "types": [ + "general" + ], + "signboard": "blank" + }, + "res-terrace": { + "file": "gen/facade-res-terrace.jpg", + "types": [ + "house" + ], + "signboard": "blank" + }, + "res-weatherboard": { + "file": "gen/facade-res-weatherboard.jpg", + "types": [ + "house" + ], + "signboard": "blank" + }, + "res-fibro": { + "file": "gen/facade-res-fibro.jpg", + "types": [ + "house" + ], + "signboard": "blank" + }, + "res-brickveneer": { + "file": "gen/facade-res-brickveneer.jpg", + "types": [ + "house" + ], + "signboard": "blank" + }, + "brick-side": { + "file": "gen/facade-brick-side.jpg", + "types": [], + "signboard": "blank", + "face": "side" + }, + "render-side": { + "file": "gen/facade-render-side.jpg", + "types": [], + "signboard": "blank", + "face": "side" + }, + "weatherboard-side": { + "file": "gen/facade-weatherboard-side.jpg", + "types": [], + "signboard": "blank", + "face": "side" + } + }, + "sky": [ + { + "id": "cold-front", + "file": "gen/sky-cold-front.jpg" + }, + { + "id": "endless-blue", + "file": "gen/sky-endless-blue.jpg" + }, + { + "id": "golden-arvo", + "file": "gen/sky-golden-arvo.jpg" + }, + { + "id": "grey-drizzle", + "file": "gen/sky-grey-drizzle.jpg" + }, + { + "id": "heat-haze", + "file": "gen/sky-heat-haze.jpg" + }, + { + "id": "high-cirrus", + "file": "gen/sky-high-cirrus.jpg" + }, + { + "id": "monsoon", + "file": "gen/sky-monsoon.jpg" + }, + { + "id": "outback-dusk", + "file": "gen/sky-outback-dusk.jpg" + }, + { + "id": "spring-puffy", + "file": "gen/sky-spring-puffy.jpg" + }, + { + "id": "summer-storm", + "file": "gen/sky-summer-storm.jpg" + } + ], + "ground": { + "asphalt1": { + "file": "gen/ground-asphalt1.jpg", + "use": "road" + }, + "asphalt2": { + "file": "gen/ground-asphalt2.jpg", + "use": "road" + }, + "brickpave": { + "file": "gen/ground-brickpave.jpg", + "use": "plaza" + }, + "djsim-footpath": { + "file": "gen/ground-djsim-footpath.jpg", + "use": "footpath" + }, + "djsim-road": { + "file": "gen/ground-djsim-road.jpg", + "use": "road" + }, + "footpath1": { + "file": "gen/ground-footpath1.jpg", + "use": "footpath" + }, + "footpath2": { + "file": "gen/ground-footpath2.jpg", + "use": "footpath" + }, + "grass": { + "file": "gen/ground-grass.jpg", + "use": "yard" + }, + "gravel": { + "file": "gen/ground-gravel.jpg", + "use": "verge" + }, + "reddust": { + "file": "gen/ground-reddust.jpg", + "use": "outback" + } + }, + "wall": [ + { + "id": "damask-mauve", + "file": "gen/wall-damask-mauve.jpg" + }, + { + "id": "diamond-green", + "file": "gen/wall-diamond-green.jpg" + }, + { + "id": "floral-cream", + "file": "gen/wall-floral-cream.jpg" + }, + { + "id": "floral-gold", + "file": "gen/wall-floral-gold.jpg" + }, + { + "id": "geo-orange", + "file": "gen/wall-geo-orange.jpg" + }, + { + "id": "stripe-sage", + "file": "gen/wall-stripe-sage.jpg" + }, + { + "id": "trellis-blue", + "file": "gen/wall-trellis-blue.jpg" + }, + { + "id": "woodchip-white", + "file": "gen/wall-woodchip-white.jpg" + } + ], + "interior": { + "floor": [ + { + "id": "carpet-swirl", + "file": "gen/tex-carpet-swirl.jpg" + }, + { + "id": "carpet-mustard", + "file": "gen/tex-carpet-mustard.jpg" + }, + { + "id": "carpet-greygreen", + "file": "gen/tex-carpet-greygreen.jpg" + }, + { + "id": "lino-check", + "file": "gen/tex-lino-check.jpg" + }, + { + "id": "lino-cork", + "file": "gen/tex-lino-cork.jpg" + }, + { + "id": "boards-polished", + "file": "gen/tex-boards-polished.jpg" + } + ], + "surface": [ + { + "id": "wood-counter", + "file": "gen/tex-wood-counter.jpg", + "types": [ + "record", + "milkbar", + "pawn" + ] + }, + { + "id": "boutique-wall", + "file": "gen/tex-boutique-wall.jpg", + "types": [ + "toy", + "opshop" + ] + }, + { + "id": "pegboard", + "file": "gen/tex-pegboard.jpg", + "types": [ + "pawn", + "video" + ] + }, + { + "id": "brick", + "file": "gen/tex-brick.jpg", + "types": [ + "general" + ] + }, + { + "id": "render", + "file": "gen/tex-render.jpg", + "types": [ + "general" + ] + }, + { + "id": "grimy-wall", + "file": "gen/tex-grimy-wall.jpg", + "types": [ + "pawn" + ] + }, + { + "id": "velvet-curtain", + "file": "gen/tex-velvet-curtain.jpg", + "types": [ + "toy", + "record" + ] + } + ] + }, + "awning": [ + { + "id": "awning-red", + "file": "gen/tex-awning-red.jpg" + }, + { + "id": "awning-green", + "file": "gen/tex-awning-green.jpg" + }, + { + "id": "awning-blue", + "file": "gen/tex-awning-blue.jpg" + } + ] + }, + "fittings": { + "record_crate": { + "file": "procity_fit_record_crate_01.glb", + "footprint": [ + 0.46, + 0.63 + ], + "height": 0.41, + "thumb": "thumbs/procity_fit_record_crate_01.png" + }, + "wire_shelf": { + "file": "procity_fit_wire_shelf_01.glb", + "footprint": [ + 1.17, + 0.44 + ], + "height": 1.8, + "thumb": "thumbs/procity_fit_wire_shelf_01.png" + }, + "clothes_rack": { + "file": "procity_fit_clothes_rack_01.glb", + "footprint": [ + 1.34, + 0.43 + ], + "height": 1.5, + "thumb": "thumbs/procity_fit_clothes_rack_01.png" + }, + "bookshelf": { + "file": "procity_fit_bookshelf_01.glb", + "footprint": [ + 0.2, + 1.02 + ], + "height": 2.0, + "thumb": "thumbs/procity_fit_bookshelf_01.png" + }, + "cube_shelf_wide": { + "file": "procity_fit_cube_shelf_wide_01.glb", + "footprint": [ + 0.75, + 1.24 + ], + "height": 1.1, + "thumb": "thumbs/procity_fit_cube_shelf_wide_01.png" + }, + "counter": { + "file": "procity_fit_counter_01.glb", + "footprint": [ + 1.06, + 4.02 + ], + "height": 1.1, + "thumb": "thumbs/procity_fit_counter_01.png" + }, + "coffee_table": { + "file": "procity_fit_coffee_table_01.glb", + "footprint": [ + 0.67, + 1.49 + ], + "height": 0.45, + "thumb": "thumbs/procity_fit_coffee_table_01.png" + }, + "work_table": { + "file": "procity_fit_work_table_01.glb", + "footprint": [ + 0.44, + 1.04 + ], + "height": 0.75, + "thumb": "thumbs/procity_fit_work_table_01.png" + }, + "box_crate": { + "file": "procity_fit_box_crate_01.glb", + "footprint": [ + 1.35, + 1.32 + ], + "height": 0.6, + "thumb": "thumbs/procity_fit_box_crate_01.png" + } + }, + "furniture": { + "bench": { + "file": "procity_street_bench_01.glb", + "footprint": [ + 0.44, + 1.21 + ], + "height": 0.85, + "thumb": "thumbs/procity_street_bench_01.png" + }, + "bench_modern": { + "file": "procity_street_bench_modern_01.glb", + "footprint": [ + 1.68, + 0.47 + ], + "height": 0.45, + "thumb": "thumbs/procity_street_bench_modern_01.png" + }, + "bench_wood": { + "file": "procity_street_bench_wood_01.glb", + "footprint": [ + 1.34, + 0.59 + ], + "height": 0.85, + "thumb": "thumbs/procity_street_bench_wood_01.png" + }, + "longbench": { + "file": "procity_street_longbench_01.glb", + "footprint": [ + 5.86, + 1.08 + ], + "height": 0.85, + "thumb": "thumbs/procity_street_longbench_01.png" + }, + "park_bench": { + "file": "procity_street_park_bench_01.glb", + "footprint": [ + 1.92, + 1.61 + ], + "height": 0.85, + "thumb": "thumbs/procity_street_park_bench_01.png" + }, + "streetlight": { + "file": "procity_street_streetlight_01.glb", + "footprint": [ + 1.64, + 1.82 + ], + "height": 5.0, + "thumb": "thumbs/procity_street_streetlight_01.png" + }, + "food_cart": { + "file": "procity_street_food_cart_01.glb", + "footprint": [ + 3.0, + 2.07 + ], + "height": 2.0, + "thumb": "thumbs/procity_street_food_cart_01.png" + } + }, + "fittingAliases": { + "record_bins": "record_crate", + "crates": "box_crate", + "counter": "counter", + "clothes_racks": "clothes_rack", + "bricabrac_shelving": "wire_shelf", + "book_wall": "bookshelf", + "cube_shelves": "cube_shelf_wide", + "display_tables": "coffee_table", + "bookshelves": "bookshelf", + "vhs_shelving": "wire_shelf", + "trestle_tables": "work_table" + } +} \ No newline at end of file diff --git a/web/assets/thumbs/procity_fit_bookshelf_01.png b/web/assets/thumbs/procity_fit_bookshelf_01.png new file mode 100644 index 0000000..79f0aa3 Binary files /dev/null and b/web/assets/thumbs/procity_fit_bookshelf_01.png differ diff --git a/web/assets/thumbs/procity_fit_box_crate_01.png b/web/assets/thumbs/procity_fit_box_crate_01.png new file mode 100644 index 0000000..94c61b0 Binary files /dev/null and b/web/assets/thumbs/procity_fit_box_crate_01.png differ diff --git a/web/assets/thumbs/procity_fit_clothes_rack_01.png b/web/assets/thumbs/procity_fit_clothes_rack_01.png new file mode 100644 index 0000000..8d7b400 Binary files /dev/null and b/web/assets/thumbs/procity_fit_clothes_rack_01.png differ diff --git a/web/assets/thumbs/procity_fit_coffee_table_01.png b/web/assets/thumbs/procity_fit_coffee_table_01.png new file mode 100644 index 0000000..24c47f0 Binary files /dev/null and b/web/assets/thumbs/procity_fit_coffee_table_01.png differ diff --git a/web/assets/thumbs/procity_fit_counter_01.png b/web/assets/thumbs/procity_fit_counter_01.png new file mode 100644 index 0000000..2ebe974 Binary files /dev/null and b/web/assets/thumbs/procity_fit_counter_01.png differ diff --git a/web/assets/thumbs/procity_fit_cube_shelf_wide_01.png b/web/assets/thumbs/procity_fit_cube_shelf_wide_01.png new file mode 100644 index 0000000..f070732 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cube_shelf_wide_01.png differ diff --git a/web/assets/thumbs/procity_fit_record_crate_01.png b/web/assets/thumbs/procity_fit_record_crate_01.png new file mode 100644 index 0000000..f30d12e Binary files /dev/null and b/web/assets/thumbs/procity_fit_record_crate_01.png differ diff --git a/web/assets/thumbs/procity_fit_wire_shelf_01.png b/web/assets/thumbs/procity_fit_wire_shelf_01.png new file mode 100644 index 0000000..0c76068 Binary files /dev/null and b/web/assets/thumbs/procity_fit_wire_shelf_01.png differ diff --git a/web/assets/thumbs/procity_fit_work_table_01.png b/web/assets/thumbs/procity_fit_work_table_01.png new file mode 100644 index 0000000..b2c5632 Binary files /dev/null and b/web/assets/thumbs/procity_fit_work_table_01.png differ diff --git a/web/assets/thumbs/procity_street_bench_01.png b/web/assets/thumbs/procity_street_bench_01.png new file mode 100644 index 0000000..627b6ed Binary files /dev/null and b/web/assets/thumbs/procity_street_bench_01.png differ diff --git a/web/assets/thumbs/procity_street_bench_modern_01.png b/web/assets/thumbs/procity_street_bench_modern_01.png new file mode 100644 index 0000000..c371bd7 Binary files /dev/null and b/web/assets/thumbs/procity_street_bench_modern_01.png differ diff --git a/web/assets/thumbs/procity_street_bench_wood_01.png b/web/assets/thumbs/procity_street_bench_wood_01.png new file mode 100644 index 0000000..26b1edc Binary files /dev/null and b/web/assets/thumbs/procity_street_bench_wood_01.png differ diff --git a/web/assets/thumbs/procity_street_food_cart_01.png b/web/assets/thumbs/procity_street_food_cart_01.png new file mode 100644 index 0000000..8b5e97c Binary files /dev/null and b/web/assets/thumbs/procity_street_food_cart_01.png differ diff --git a/web/assets/thumbs/procity_street_longbench_01.png b/web/assets/thumbs/procity_street_longbench_01.png new file mode 100644 index 0000000..0074e2a Binary files /dev/null and b/web/assets/thumbs/procity_street_longbench_01.png differ diff --git a/web/assets/thumbs/procity_street_park_bench_01.png b/web/assets/thumbs/procity_street_park_bench_01.png new file mode 100644 index 0000000..74f148d Binary files /dev/null and b/web/assets/thumbs/procity_street_park_bench_01.png differ diff --git a/web/assets/thumbs/procity_street_streetlight_01.png b/web/assets/thumbs/procity_street_streetlight_01.png new file mode 100644 index 0000000..990d5a5 Binary files /dev/null and b/web/assets/thumbs/procity_street_streetlight_01.png differ