Lane E round 3 (landed by Fable after review): 5 hero props gen'd local + published, local-depot mode, manifest 14 fittings + 8 furniture
- gen_props.py: flux_local -> trellis_mac -> normalize pipeline; 5/8 props kept (arcade-cabinet, listening-booth, drinks-fridge, milkshake-mixer, novelty-record) - magazine-rack -> primitive (thin-wire gotcha); glass-case + bus-shelter -> fal.ai fallback - cash-register reused from depot; all 22 GLBs live, validate --depot 0/0 - loaders.js ?localdepot=1 + stage_local_depot.py for network-free GLB validation - qa.sh --strict GREEN 4/4 at commit time Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
53f472d0d7
commit
bd40ec7e7a
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,3 +7,5 @@ pipeline/_normalized/
|
|||||||
saves/
|
saves/
|
||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
|
pipeline/.genprops/
|
||||||
|
web/assets/models/
|
||||||
|
|||||||
@ -1,7 +1,31 @@
|
|||||||
# LANE E — ASSETS · progress (PROCITY-E)
|
# LANE E — ASSETS · progress (PROCITY-E)
|
||||||
|
|
||||||
*2026-07-14. Owner: PROCITY-E. Scope: `pipeline/*`, `web/assets/*`, `web/assets/manifest.json`.
|
*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.*
|
Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in
|
||||||
|
other lanes' files was touched.*
|
||||||
|
|
||||||
|
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
|
||||||
|
|
||||||
|
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`
|
||||||
|
image→GLB (1024_cascade) → `normalize.py` → thumbs → manifest. **5 generated + kept**
|
||||||
|
(listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer, novelty_record — contact sheet
|
||||||
|
`docs/shots/laneE/props_gen_local.png`), **cash_register reused** from the depot
|
||||||
|
(`vintage-cash-register.glb`). 3 didn't make it: **magazine_rack** → in-engine primitive (112k-tri
|
||||||
|
wire, the thin-wire gotcha); **glass_case + bus_shelter** → **fal.ai fallback list** (TRELLIS can't
|
||||||
|
do transparent glass; sf3d gives a featureless blob) — gated on John, ≤$2.70, both have primitive
|
||||||
|
fallbacks. Every kept thumbnail eyeballed; no legible branding (see AUDIT provenance).
|
||||||
|
- **Local-depot mode** (`core/loaders.js` `?localdepot=1` + `pipeline/stage_local_depot.py`): resolves
|
||||||
|
`depot:<file>` to `web/assets/models/` served by the dev server, so Lanes B/C/F validate GLB loading
|
||||||
|
**with no network / before publish**. Verified live in-browser (loads a `depot:` GLB, CDN path intact).
|
||||||
|
- **Manifest**: 14 fittings + 8 furniture (5 hero + cash_register added); `validate_manifest.py`
|
||||||
|
green (0 err). Provenance recorded in `AUDIT.md` (props are 100% synthetic/local — cleanest in the set).
|
||||||
|
- **Known tradeoff**: TRELLIS meshes are dense shell-soup — decimate floors 25–77k tris (heavier than
|
||||||
|
the ≤5k library law). Accepted for focal/optional/LOD-able props; texture-bake-to-lowpoly is a follow-up.
|
||||||
|
- **Published (self-serve, John's go 2026-07-14)**: the 5 prop GLBs + thumbs pushed to the depot via
|
||||||
|
the passwordless tailnet path (decision #4); all 22 GLBs re-verified live. **`qa.sh --strict` GREEN**
|
||||||
|
(4/4 gates; manifest validator 0 err / 0 warn with the props live). Provenance was recorded first.
|
||||||
|
- **Not committed**: this round is **held for Fable review** (per John) — all Lane E round-3 changes are
|
||||||
|
in the working tree, uncommitted. GLB binaries stay git-ignored/staged; the depot has them.
|
||||||
|
|
||||||
## Status at a glance
|
## Status at a glance
|
||||||
|
|
||||||
@ -11,7 +35,8 @@ Reviewed against `docs/LANES/LANE_E_ASSETS.md` acceptance. Nothing in other lane
|
|||||||
| 2 Normalize | `pipeline/normalize.py` → **16 GLBs** + 16 thumbs, verified | ✅ done (local); ⛔ **publish gated on `GOD3_PW`** |
|
| 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 |
|
| 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** |
|
| 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)** |
|
| 5 Hero props | round-3 `gen_props.py` (MODELBEAST, free) → **5 local** + cash-register reuse | ✅ **DONE local** (see Round 3 above; 2→fal, 1→primitive) |
|
||||||
|
| 6 Local-depot | `loaders.js ?localdepot=1` + `stage_local_depot.py` | ✅ **DONE, verified in-browser** |
|
||||||
| — | `pipeline/README.md` + contact sheets in `docs/shots/laneE/` | ✅ done |
|
| — | `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**
|
**Step 4 no longer costs anything.** The 23 facades were generated **locally on the M3 Ultra**
|
||||||
|
|||||||
BIN
docs/shots/laneE/props_gen_local.png
Normal file
BIN
docs/shots/laneE/props_gen_local.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@ -90,6 +90,31 @@ cleared. These are **excluded from the manifest**:
|
|||||||
> If provenance genuinely can't be established for one, pull it from the publish set. This is a
|
> 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.
|
> reason the publish step is deliberately gated on John, not automated.
|
||||||
|
|
||||||
|
## Provenance — round-3 hero props (for the depot publish sign-off)
|
||||||
|
|
||||||
|
Per the round-3 instruction ("record the per-asset provenance list in AUDIT.md first, then hand
|
||||||
|
John the two-liner"). The hero props have the **cleanest provenance in the whole set** — they are
|
||||||
|
100% generated on-device, from no third-party source:
|
||||||
|
|
||||||
|
| asset (`procity_fit/street_<id>_01.glb`) | origin | licence zone |
|
||||||
|
|---|---|---|
|
||||||
|
| **5 generated** — listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer (fit) · novelty_record (street) | **fully synthetic, local**: concept image = MODELBEAST `flux_local` (FLUX.2-klein-4B, Apache-2.0 model) → mesh = `trellis_mac` (TRELLIS.2, an original reconstruction of that concept image). No scanned/third-party geometry, no copyrighted source asset. | 🟢 original work (CC0-equivalent) |
|
||||||
|
| cash_register (`vintage-cash-register.glb`) | **reuse** of John's own `thriftgod_hero` asset, already live on 3GOD (round-3 reuse check) — not regenerated. | 🟢 same as existing depot set |
|
||||||
|
| magazine_rack | **not shipped** — TRELLIS wire rack floored ~112k tris (thin-wire gotcha) → in-engine primitive. | n/a |
|
||||||
|
| glass_case, bus_shelter | **not shipped** — TRELLIS failed on the transparent glass → fal.ai fallback list (John's go, ≤$2.70) or primitive. | n/a |
|
||||||
|
|
||||||
|
- **Prompts were generic period objects with explicit `no text, no brands, no people`.** Every kept
|
||||||
|
prop was eyeballed for accidental baked branding (contact sheet `docs/shots/laneE/props_gen_local.png`).
|
||||||
|
The only textual marks are the **drinks-fridge's weathered faux-stickers** — verified at full concept
|
||||||
|
resolution to be **garbled, illegible pseudo-lettering** (no real trademark; e.g. a faux barcode +
|
||||||
|
nonsense glyphs), which reads as period flavour and blurs out at mesh-texture resolution. 🟢 cleared.
|
||||||
|
- Tri/size note: TRELLIS meshes are dense multi-shell scans; `normalize.py`'s COLLAPSE decimate floors
|
||||||
|
well above the ≤5k library budget (**25–77k tris, ~0.3–3.5 MB/prop**; neither COLLAPSE nor meshopt
|
||||||
|
can reduce shell-soup without a voxel-remesh that destroys the baked UVs). Accepted for **focal,
|
||||||
|
optional, LOD-able** props (texture-intact) — flagged for a later texture-bake-to-lowpoly / gltfpack pass.
|
||||||
|
- These GLBs stage locally (`web/assets/models/`, git-ignored, served by `?localdepot=1`) and await
|
||||||
|
John's depot publish; they are **not** committed binaries (same policy as the library `_normalized` set).
|
||||||
|
|
||||||
## Coverage vs the registry fittings needs
|
## Coverage vs the registry fittings needs
|
||||||
|
|
||||||
| shop type | fittings need | covered by | gap → |
|
| shop type | fittings need | covered by | gap → |
|
||||||
|
|||||||
@ -71,10 +71,29 @@ no key, weights cached). It runs one GPU job at a time. Add skins by editing `PR
|
|||||||
`.genraw/` and delete rejects before `--harvest`.** Cloud fallback (drop `--local`): OpenRouter
|
`.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`.
|
`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)*
|
### 6. Bespoke hero props — `gen_props.py` (MODELBEAST) *(round-3: DONE, free, on-device)*
|
||||||
Glass case, fridge, till, listening booth, etc. — the gaps no library asset fills. **Do not run
|
The `meshgod_batch.json` gap list now goes through MODELBEAST instead of fal.ai — free, local:
|
||||||
without John's go**; check the DealGod cache / depot for reuse first (cash-register + glass-case
|
```bash
|
||||||
likely already exist). Everything has a primitive fallback.
|
python3 pipeline/gen_props.py --dry-run # list props + what's done
|
||||||
|
python3 pipeline/gen_props.py --concepts # flux_local concept images → .genprops/*.png (eyeball!)
|
||||||
|
python3 pipeline/gen_props.py --mesh --only glass-case # trellis_mac 1024_cascade image→GLB (~5-8 min)
|
||||||
|
python3 pipeline/gen_props.py --mesh # all props with a concept, no glb
|
||||||
|
# then normalize (uses local Blender), stage, catalogue:
|
||||||
|
BL=/Applications/Blender.app/Contents/MacOS/Blender
|
||||||
|
"$BL" --background --python pipeline/normalize.py -- pipeline/batch_props.json pipeline/_normalized web/assets/thumbs
|
||||||
|
cp pipeline/_normalized/_results.json pipeline/_props_results.json && python3 pipeline/build_manifest.py
|
||||||
|
```
|
||||||
|
**5 shipped** (arcade_cabinet, listening_booth, drinks_fridge, milkshake_mixer, novelty_record) + cash-register
|
||||||
|
reused from depot. `magazine-rack` → primitive (thin-wire gotcha); `glass-case`/`bus-shelter` → fal.ai
|
||||||
|
fallback (TRELLIS can't do transparent glass; `sf3d --draft` gives a featureless blob). TRELLIS meshes run
|
||||||
|
heavy (25–77k tris) — see AUDIT provenance. **Eyeball every thumbnail**; ≤2 local attempts then fal (John's go).
|
||||||
|
|
||||||
|
### 7. Local-depot mode — `loaders.js ?localdepot=1` + `stage_local_depot.py` *(round-3: DONE)*
|
||||||
|
Lets Lanes B/C/F validate GLB loading with **no network / before publish**:
|
||||||
|
```bash
|
||||||
|
python3 pipeline/stage_local_depot.py # mirror _normalized/*.glb → web/assets/models/ (git-ignored)
|
||||||
|
# then load the game with ?localdepot=1 — depot:<file> resolves to assets/models/<file> instead of the CDN
|
||||||
|
```
|
||||||
|
|
||||||
## Cost summary
|
## Cost summary
|
||||||
|
|
||||||
|
|||||||
90
pipeline/_props_results.json
Normal file
90
pipeline/_props_results.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/listening-booth.glb",
|
||||||
|
"file": "procity_fit_listening_booth_01.glb",
|
||||||
|
"out": "pipeline/_normalized/procity_fit_listening_booth_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_fit_listening_booth_01.png",
|
||||||
|
"tri_before": 146946,
|
||||||
|
"tri_after": 53578,
|
||||||
|
"size_m": [
|
||||||
|
0.879,
|
||||||
|
1.6,
|
||||||
|
0.71
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
0.88,
|
||||||
|
0.71
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/drinks-fridge.glb",
|
||||||
|
"file": "procity_fit_drinks_fridge_01.glb",
|
||||||
|
"out": "pipeline/_normalized/procity_fit_drinks_fridge_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_fit_drinks_fridge_01.png",
|
||||||
|
"tri_before": 191640,
|
||||||
|
"tri_after": 77412,
|
||||||
|
"size_m": [
|
||||||
|
0.849,
|
||||||
|
1.6,
|
||||||
|
0.962
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
0.85,
|
||||||
|
0.96
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/arcade-cabinet.glb",
|
||||||
|
"file": "procity_fit_arcade_cabinet_01.glb",
|
||||||
|
"out": "pipeline/_normalized/procity_fit_arcade_cabinet_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_fit_arcade_cabinet_01.png",
|
||||||
|
"tri_before": 196336,
|
||||||
|
"tri_after": 25143,
|
||||||
|
"size_m": [
|
||||||
|
0.75,
|
||||||
|
1.7,
|
||||||
|
0.853
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
0.75,
|
||||||
|
0.85
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/milkshake-mixer.glb",
|
||||||
|
"file": "procity_fit_milkshake_mixer_01.glb",
|
||||||
|
"out": "pipeline/_normalized/procity_fit_milkshake_mixer_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_fit_milkshake_mixer_01.png",
|
||||||
|
"tri_before": 185106,
|
||||||
|
"tri_after": 73498,
|
||||||
|
"size_m": [
|
||||||
|
0.381,
|
||||||
|
0.5,
|
||||||
|
0.271
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
0.38,
|
||||||
|
0.27
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/novelty-record.glb",
|
||||||
|
"file": "procity_street_novelty_record_01.glb",
|
||||||
|
"out": "pipeline/_normalized/procity_street_novelty_record_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_street_novelty_record_01.png",
|
||||||
|
"tri_before": 193253,
|
||||||
|
"tri_after": 26506,
|
||||||
|
"size_m": [
|
||||||
|
1.909,
|
||||||
|
2.0,
|
||||||
|
0.934
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
1.91,
|
||||||
|
0.93
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
@ -1,10 +1,14 @@
|
|||||||
[
|
[
|
||||||
|
"procity_fit_arcade_cabinet_01.glb",
|
||||||
"procity_fit_bookshelf_01.glb",
|
"procity_fit_bookshelf_01.glb",
|
||||||
"procity_fit_box_crate_01.glb",
|
"procity_fit_box_crate_01.glb",
|
||||||
"procity_fit_clothes_rack_01.glb",
|
"procity_fit_clothes_rack_01.glb",
|
||||||
"procity_fit_coffee_table_01.glb",
|
"procity_fit_coffee_table_01.glb",
|
||||||
"procity_fit_counter_01.glb",
|
"procity_fit_counter_01.glb",
|
||||||
"procity_fit_cube_shelf_wide_01.glb",
|
"procity_fit_cube_shelf_wide_01.glb",
|
||||||
|
"procity_fit_drinks_fridge_01.glb",
|
||||||
|
"procity_fit_listening_booth_01.glb",
|
||||||
|
"procity_fit_milkshake_mixer_01.glb",
|
||||||
"procity_fit_record_crate_01.glb",
|
"procity_fit_record_crate_01.glb",
|
||||||
"procity_fit_wire_shelf_01.glb",
|
"procity_fit_wire_shelf_01.glb",
|
||||||
"procity_fit_work_table_01.glb",
|
"procity_fit_work_table_01.glb",
|
||||||
@ -13,6 +17,8 @@
|
|||||||
"procity_street_bench_wood_01.glb",
|
"procity_street_bench_wood_01.glb",
|
||||||
"procity_street_food_cart_01.glb",
|
"procity_street_food_cart_01.glb",
|
||||||
"procity_street_longbench_01.glb",
|
"procity_street_longbench_01.glb",
|
||||||
|
"procity_street_novelty_record_01.glb",
|
||||||
"procity_street_park_bench_01.glb",
|
"procity_street_park_bench_01.glb",
|
||||||
"procity_street_streetlight_01.glb"
|
"procity_street_streetlight_01.glb",
|
||||||
|
"vintage-cash-register.glb"
|
||||||
]
|
]
|
||||||
10
pipeline/batch_props.json
Normal file
10
pipeline/batch_props.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/listening-booth.glb", "id": "listening_booth", "category": "fit", "height_m": 1.6, "tris": 6000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/glass-case.glb", "id": "glass_case", "category": "fit", "height_m": 1.0, "tris": 6000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/drinks-fridge.glb", "id": "drinks_fridge", "category": "fit", "height_m": 1.6, "tris": 5000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/magazine-rack.glb", "id": "magazine_rack", "category": "fit", "height_m": 1.2, "tris": 4000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/arcade-cabinet.glb", "id": "arcade_cabinet", "category": "fit", "height_m": 1.7, "tris": 6000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/milkshake-mixer.glb", "id": "milkshake_mixer", "category": "fit", "height_m": 0.5, "tris": 4000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/novelty-record.glb", "id": "novelty_record", "category": "street", "height_m": 2.0, "tris": 4000},
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/bus-shelter.glb", "id": "bus_shelter", "category": "street", "height_m": 2.4, "tris": 8000}
|
||||||
|
]
|
||||||
@ -13,10 +13,19 @@ import json, os, glob
|
|||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
GEN = os.path.join(ROOT, "web", "assets", "gen")
|
GEN = os.path.join(ROOT, "web", "assets", "gen")
|
||||||
RESULTS = os.path.join(ROOT, "pipeline", "_normalize_results.json")
|
RESULTS = os.path.join(ROOT, "pipeline", "_normalize_results.json") # library GLBs
|
||||||
|
PROPS_RESULTS = os.path.join(ROOT, "pipeline", "_props_results.json") # round-3 hero props
|
||||||
OUT = os.path.join(ROOT, "web", "assets", "manifest.json")
|
OUT = os.path.join(ROOT, "web", "assets", "manifest.json")
|
||||||
DEPOT = "https://digalot.fyi/3god"
|
DEPOT = "https://digalot.fyi/3god"
|
||||||
|
|
||||||
|
# ── depot-reuse: already-published GLBs we point at without a local normalize. The round-3 reuse
|
||||||
|
# check found vintage-cash-register.glb live on 3GOD (from thriftgod_hero) — reuse it for the
|
||||||
|
# universal till instead of re-generating. Manual footprint/height (no local file to measure);
|
||||||
|
# no local thumb, so it isn't staged for ?localdepot (falls back to primitive there). ──
|
||||||
|
DEPOT_REUSE = {
|
||||||
|
"cash_register": {"file": "vintage-cash-register.glb", "footprint": [0.35, 0.28], "height": 0.35},
|
||||||
|
}
|
||||||
|
|
||||||
# ── curated: facade skin → shop types it suits ─────────────────────────────────────────
|
# ── curated: facade skin → shop types it suits ─────────────────────────────────────────
|
||||||
# MIRRORS Lane A's web/js/core/registry.js SHOP_TYPES[*].facades (the authoritative render-time
|
# 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
|
# pool) so there is ONE consistent mapping, plus additive-only coverage: cashtheists→pawn (a pawn
|
||||||
@ -133,13 +142,14 @@ def build():
|
|||||||
"awning": awning,
|
"awning": awning,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- fittings / furniture from the normalizer results ----
|
# ---- fittings / furniture from the normalizer results (library + round-3 hero props) ----
|
||||||
fittings, furniture = {}, {}
|
fittings, furniture = {}, {}
|
||||||
# cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md
|
# cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md
|
||||||
DROP = {"procity_fit_cube_shelf_01.glb"}
|
DROP = {"procity_fit_cube_shelf_01.glb"}
|
||||||
if os.path.exists(RESULTS):
|
for results_file in (RESULTS, PROPS_RESULTS):
|
||||||
res = json.load(open(RESULTS))["results"]
|
if not os.path.exists(results_file):
|
||||||
for r in res:
|
continue
|
||||||
|
for r in json.load(open(results_file)).get("results", []):
|
||||||
if r["file"] in DROP:
|
if r["file"] in DROP:
|
||||||
continue
|
continue
|
||||||
fid = r["file"].replace("procity_fit_", "").replace("procity_street_", "").replace("_01.glb", "")
|
fid = r["file"].replace("procity_fit_", "").replace("procity_street_", "").replace("_01.glb", "")
|
||||||
@ -150,6 +160,9 @@ def build():
|
|||||||
"thumb": "thumbs/" + r["file"].replace(".glb", ".png"),
|
"thumb": "thumbs/" + r["file"].replace(".glb", ".png"),
|
||||||
}
|
}
|
||||||
(furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry
|
(furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry
|
||||||
|
# depot-reuse assets (no local normalize) — e.g. the reused vintage cash register
|
||||||
|
for fid, meta in DEPOT_REUSE.items():
|
||||||
|
fittings.setdefault(fid, dict(meta))
|
||||||
|
|
||||||
# registry.js semantic fitting id → our physical asset id (only where a GLB exists; the rest
|
# 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
|
# stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings
|
||||||
|
|||||||
147
pipeline/gen_props.py
Normal file
147
pipeline/gen_props.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""PROCITY hero props — free, on-device (round-3 Fable path: MODELBEAST, not fal.ai).
|
||||||
|
|
||||||
|
Two local stages, both on the M3 Ultra GPU, zero cloud cost:
|
||||||
|
1. concept image flux_local (FLUX.2-klein-4B) text->object photo ~6s
|
||||||
|
2. mesh trellis_mac (TRELLIS.2-4B) image->GLB+PBR ~3-5 min (1024_cascade)
|
||||||
|
then pipeline/normalize.py (house GLB law) + thumbnail + manifest.
|
||||||
|
|
||||||
|
Prop list + prompts come from pipeline/meshgod_batch.json (the same gap list that was going to
|
||||||
|
fal.ai). cash-register is skipped — vintage-cash-register.glb already lives on the 3GOD depot.
|
||||||
|
|
||||||
|
python3 pipeline/gen_props.py --dry-run # list props + what's done, no GPU
|
||||||
|
python3 pipeline/gen_props.py --concepts # stage 1 for all -> .genprops/<name>.png
|
||||||
|
python3 pipeline/gen_props.py --concepts --only glass-case
|
||||||
|
python3 pipeline/gen_props.py --mesh --only glass-case # stage 2 for one (concept must exist)
|
||||||
|
python3 pipeline/gen_props.py --mesh # stage 2 for every prop with a concept, no glb
|
||||||
|
python3 pipeline/gen_props.py --draft --only glass-case # sf3d seconds-fast draft (iterate concept)
|
||||||
|
|
||||||
|
Resumable: skips a stage whose output already exists in .genprops/. Eyeball .genprops/*.png (concepts)
|
||||||
|
and the rendered GLBs before normalizing. Everything here is OPTIONAL at runtime — every prop has an
|
||||||
|
in-engine primitive fallback (see MESHGOD_BATCH.md _primitives_instead), so nothing blocks on it.
|
||||||
|
"""
|
||||||
|
import json, os, sys, subprocess, shutil, time
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
RAW = os.path.join(ROOT, "pipeline", ".genprops")
|
||||||
|
BATCH = os.path.join(ROOT, "pipeline", "meshgod_batch.json")
|
||||||
|
os.makedirs(RAW, exist_ok=True)
|
||||||
|
|
||||||
|
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")
|
||||||
|
TRELLIS_PY = os.path.join(MB, "vendor/trellis-mac/.venv/bin/python")
|
||||||
|
TRELLIS_RUN = os.path.join(MB, "server/operators/trellis_mac/run.py")
|
||||||
|
SF3D_PY = os.path.join(MB, "venvs/sf3d/bin/python")
|
||||||
|
SF3D_RUN = os.path.join(MB, "server/operators/sf3d/run.py")
|
||||||
|
|
||||||
|
# Clean product-shot framing so TRELLIS/sf3d (which segment the subject) get a whole, isolated
|
||||||
|
# object on a plain backdrop — NOT the faded op-shop facade look. Period-appropriate but neutral.
|
||||||
|
CONCEPT = ("Clean studio product photograph of {v}. Single object, centred, entire object in "
|
||||||
|
"frame, three-quarter view, plain seamless pale-grey backdrop, soft even lighting, "
|
||||||
|
"sharp focus, no text, no people, no other objects, 1990s Australian period-correct.")
|
||||||
|
|
||||||
|
|
||||||
|
def props():
|
||||||
|
data = json.load(open(BATCH))
|
||||||
|
out = []
|
||||||
|
for a in data["assets"]:
|
||||||
|
# cash-register: reuse the depot's vintage-cash-register.glb (round-3 reuse check)
|
||||||
|
if a["name"] == "cash-register":
|
||||||
|
continue
|
||||||
|
out.append(a)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def concept_path(name):
|
||||||
|
return os.path.join(RAW, f"{name}.png")
|
||||||
|
|
||||||
|
|
||||||
|
def glb_path(name):
|
||||||
|
return os.path.join(RAW, f"{name}.glb")
|
||||||
|
|
||||||
|
|
||||||
|
def gen_concept(a, seed=3):
|
||||||
|
name, prompt = a["name"], a["prompt"]
|
||||||
|
outdir = os.path.join(RAW, "_c_" + name)
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
params = json.dumps({"prompt": CONCEPT.format(v=prompt), "model": "flux2-klein-4b",
|
||||||
|
"steps": 4, "width": 1024, "height": 1024, "seed": seed})
|
||||||
|
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")[-200:])
|
||||||
|
shutil.move(os.path.join(outdir, pngs[0]), concept_path(name))
|
||||||
|
shutil.rmtree(outdir, ignore_errors=True)
|
||||||
|
return concept_path(name)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_mesh(a, draft=False):
|
||||||
|
"""concept image -> GLB. TRELLIS 1024_cascade by default; sf3d for a fast draft."""
|
||||||
|
name = a["name"]
|
||||||
|
cpath = concept_path(name)
|
||||||
|
if not os.path.exists(cpath):
|
||||||
|
raise RuntimeError(f"no concept image for {name} — run --concepts first")
|
||||||
|
outdir = os.path.join(RAW, "_m_" + name)
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
if draft:
|
||||||
|
params = json.dumps({"device": "mps", "texture_resolution": 1024, "foreground_ratio": 0.85})
|
||||||
|
cmd = [SF3D_PY, SF3D_RUN, "--input", cpath, "--outdir", outdir, "--params", params]
|
||||||
|
tmo = 600
|
||||||
|
else:
|
||||||
|
params = json.dumps({"pipeline_type": "1024_cascade", "texture_size": 1024, "seed": 0})
|
||||||
|
cmd = [TRELLIS_PY, TRELLIS_RUN, "--input", cpath, "--outdir", outdir, "--params", params]
|
||||||
|
tmo = 1200
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=tmo)
|
||||||
|
glbs = []
|
||||||
|
for dp, _, fs in os.walk(outdir):
|
||||||
|
glbs += [os.path.join(dp, f) for f in fs if f.endswith(".glb")]
|
||||||
|
if not glbs:
|
||||||
|
raise RuntimeError((r.stderr or r.stdout or "no glb")[-240:])
|
||||||
|
dst = glb_path(name)
|
||||||
|
shutil.move(sorted(glbs)[0], dst)
|
||||||
|
shutil.rmtree(outdir, ignore_errors=True)
|
||||||
|
return dst
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
|
||||||
|
draft = "--draft" in sys.argv
|
||||||
|
todo = [a for a in props() if not only or only in a["name"]]
|
||||||
|
|
||||||
|
if "--dry-run" in sys.argv:
|
||||||
|
print(f"{len(todo)} props (cash-register reused from depot):")
|
||||||
|
for a in todo:
|
||||||
|
c = "concept✓" if os.path.exists(concept_path(a["name"])) else "concept·"
|
||||||
|
g = "glb✓" if os.path.exists(glb_path(a["name"])) else "glb·"
|
||||||
|
print(f" {a['name']:16s} {c} {g} h={a['height_m']}m for={a['for']}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
do_concepts = "--concepts" in sys.argv or (not "--mesh" in sys.argv and not draft)
|
||||||
|
do_mesh = "--mesh" in sys.argv or draft
|
||||||
|
|
||||||
|
if do_concepts:
|
||||||
|
for a in todo:
|
||||||
|
if os.path.exists(concept_path(a["name"])):
|
||||||
|
print(f"[concept] {a['name']} — exists, skip"); continue
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
gen_concept(a)
|
||||||
|
print(f"[concept] {a['name']} — {round(time.time()-t0,1)}s")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[concept] {a['name']} FAILED: {e}")
|
||||||
|
|
||||||
|
if do_mesh:
|
||||||
|
tool = "sf3d-draft" if draft else "trellis 1024_cascade"
|
||||||
|
for a in todo:
|
||||||
|
if not draft and os.path.exists(glb_path(a["name"])):
|
||||||
|
print(f"[mesh] {a['name']} — glb exists, skip"); continue
|
||||||
|
t0 = time.time()
|
||||||
|
try:
|
||||||
|
dst = gen_mesh(a, draft=draft)
|
||||||
|
kb = os.path.getsize(dst) // 1024
|
||||||
|
print(f"[mesh:{tool}] {a['name']} — {round(time.time()-t0,1)}s ({kb}KB)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[mesh:{tool}] {a['name']} FAILED: {e}")
|
||||||
|
print("done. Eyeball .genprops/*.png (concepts) + rendered GLBs before normalize.")
|
||||||
@ -1,5 +1,13 @@
|
|||||||
{
|
{
|
||||||
"_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.",
|
"_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.",
|
||||||
|
"_round3_outcome": {
|
||||||
|
"_comment": "Round 3 (Fable): these went through MODELBEAST (flux_local concept -> trellis_mac GLB) instead of fal.ai — free, on-device. Pipeline: pipeline/gen_props.py. Outcome below; see pipeline/gen_props.py + docs/shots/laneE/props_gen_local.png.",
|
||||||
|
"generated_local": ["listening-booth", "drinks-fridge", "arcade-cabinet", "milkshake-mixer", "novelty-record"],
|
||||||
|
"reused_from_depot": {"cash-register": "vintage-cash-register.glb (thriftgod_hero, already live on 3GOD)"},
|
||||||
|
"to_primitive": {"magazine-rack": "TRELLIS wire rack = shell-soup, floors ~112k tris (COLLAPSE + meshopt both fail on wire) — the classic thin-wire gotcha; use an in-engine wire primitive instead"},
|
||||||
|
"fal_fallback": {"_comment": "TRELLIS failed on these transparent-glass subjects (no GLB); sf3d gives a featureless blob. Local 2-attempt budget spent -> fal.ai, gated on John (<=$2.70). Both have in-engine primitive fallbacks so nothing blocks.", "glass-case": "transparent glass top defeats TRELLIS segmentation", "bus-shelter": "thin steel frame + perspex panels"},
|
||||||
|
"note_tris": "TRELLIS meshes are dense multi-shell scans; normalize.py decimate floors 25-77k tris (>library <=5k law). Accepted for focal/optional/LOD-able props (texture-intact); a texture-bake-to-lowpoly / gltfpack pass is a follow-up."
|
||||||
|
},
|
||||||
"assets": [
|
"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": "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": "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"},
|
||||||
|
|||||||
@ -183,13 +183,23 @@ def normalize_one(spec):
|
|||||||
for i in range(n0, n1):
|
for i in range(n0, n1):
|
||||||
obj.data.polygons[i].material_index = widx
|
obj.data.polygons[i].material_index = widx
|
||||||
|
|
||||||
# decimate to budget (finish_glb.py:80)
|
# decimate to budget (finish_glb.py:80). TRELLIS meshes are dense multi-shell scans where a
|
||||||
tris = len(obj.data.polygons)
|
# single COLLAPSE pass stalls far above target (arcade: 196k→25k at ratio 0.03); iterate —
|
||||||
|
# each pass re-targets the budget from the now-smaller count — until within 10% or a pass
|
||||||
|
# stops making progress (collapse limit). Library assets already under budget skip the loop.
|
||||||
budget = spec.get("tris", 5000)
|
budget = spec.get("tris", 5000)
|
||||||
if tris > budget:
|
tris = len(obj.data.polygons)
|
||||||
|
passes = 0
|
||||||
|
while tris > budget * 1.1 and passes < 5:
|
||||||
m = obj.modifiers.new('decimate', 'DECIMATE')
|
m = obj.modifiers.new('decimate', 'DECIMATE')
|
||||||
m.ratio = max(0.01, budget / tris)
|
m.ratio = max(0.02, budget / tris)
|
||||||
bpy.ops.object.modifier_apply(modifier=m.name)
|
bpy.ops.object.modifier_apply(modifier=m.name)
|
||||||
|
new = len(obj.data.polygons)
|
||||||
|
passes += 1
|
||||||
|
if new >= tris * 0.97: # barely moved — collapse limit reached, don't spin
|
||||||
|
tris = new
|
||||||
|
break
|
||||||
|
tris = new
|
||||||
tri_after = len(obj.data.polygons)
|
tri_after = len(obj.data.polygons)
|
||||||
|
|
||||||
# self-illuminate dark bakes (finish_glb.py:88)
|
# self-illuminate dark bakes (finish_glb.py:88)
|
||||||
|
|||||||
35
pipeline/stage_local_depot.py
Normal file
35
pipeline/stage_local_depot.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Mirror the normalized GLBs into web/assets/models/ so loaders.js local-depot mode
|
||||||
|
(?localdepot=1) can serve them from the dev server — Lanes B/C/F validate GLB loading with
|
||||||
|
NO network and before/without the 3GOD publish.
|
||||||
|
|
||||||
|
python3 pipeline/stage_local_depot.py # copy pipeline/_normalized/*.glb -> web/assets/models/
|
||||||
|
python3 pipeline/stage_local_depot.py --clean # wipe web/assets/models/ first
|
||||||
|
|
||||||
|
Re-run whenever _normalized changes (new hero props, re-normalized assets). The staged copies are
|
||||||
|
the same files the depot serves as depot:<file>; local mode just points at these instead of the CDN.
|
||||||
|
"""
|
||||||
|
import os, sys, glob, shutil
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
SRC = os.path.join(ROOT, "pipeline", "_normalized")
|
||||||
|
DST = os.path.join(ROOT, "web", "assets", "models")
|
||||||
|
|
||||||
|
if "--clean" in sys.argv and os.path.isdir(DST):
|
||||||
|
shutil.rmtree(DST)
|
||||||
|
os.makedirs(DST, exist_ok=True)
|
||||||
|
|
||||||
|
glbs = sorted(glob.glob(os.path.join(SRC, "*.glb")))
|
||||||
|
if not glbs:
|
||||||
|
print(f"no GLBs in {SRC} — run normalize.py first")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
for src in glbs:
|
||||||
|
dst = os.path.join(DST, os.path.basename(src))
|
||||||
|
# copy only if missing or changed (idempotent, keeps mtimes stable for git)
|
||||||
|
if not os.path.exists(dst) or os.path.getsize(dst) != os.path.getsize(src):
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
|
n += 1
|
||||||
|
print(f"staged {len(glbs)} GLBs -> web/assets/models/ ({n} new/changed)")
|
||||||
|
print("loaders.js ?localdepot=1 now resolves depot:<file> to assets/models/<file>")
|
||||||
@ -647,6 +647,50 @@
|
|||||||
],
|
],
|
||||||
"height": 0.6,
|
"height": 0.6,
|
||||||
"thumb": "thumbs/procity_fit_box_crate_01.png"
|
"thumb": "thumbs/procity_fit_box_crate_01.png"
|
||||||
|
},
|
||||||
|
"listening_booth": {
|
||||||
|
"file": "procity_fit_listening_booth_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
0.88,
|
||||||
|
0.71
|
||||||
|
],
|
||||||
|
"height": 1.6,
|
||||||
|
"thumb": "thumbs/procity_fit_listening_booth_01.png"
|
||||||
|
},
|
||||||
|
"drinks_fridge": {
|
||||||
|
"file": "procity_fit_drinks_fridge_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
0.85,
|
||||||
|
0.96
|
||||||
|
],
|
||||||
|
"height": 1.6,
|
||||||
|
"thumb": "thumbs/procity_fit_drinks_fridge_01.png"
|
||||||
|
},
|
||||||
|
"arcade_cabinet": {
|
||||||
|
"file": "procity_fit_arcade_cabinet_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
0.75,
|
||||||
|
0.85
|
||||||
|
],
|
||||||
|
"height": 1.7,
|
||||||
|
"thumb": "thumbs/procity_fit_arcade_cabinet_01.png"
|
||||||
|
},
|
||||||
|
"milkshake_mixer": {
|
||||||
|
"file": "procity_fit_milkshake_mixer_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
0.38,
|
||||||
|
0.27
|
||||||
|
],
|
||||||
|
"height": 0.5,
|
||||||
|
"thumb": "thumbs/procity_fit_milkshake_mixer_01.png"
|
||||||
|
},
|
||||||
|
"cash_register": {
|
||||||
|
"file": "vintage-cash-register.glb",
|
||||||
|
"footprint": [
|
||||||
|
0.35,
|
||||||
|
0.28
|
||||||
|
],
|
||||||
|
"height": 0.35
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"furniture": {
|
"furniture": {
|
||||||
@ -712,6 +756,15 @@
|
|||||||
],
|
],
|
||||||
"height": 2.0,
|
"height": 2.0,
|
||||||
"thumb": "thumbs/procity_street_food_cart_01.png"
|
"thumb": "thumbs/procity_street_food_cart_01.png"
|
||||||
|
},
|
||||||
|
"novelty_record": {
|
||||||
|
"file": "procity_street_novelty_record_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
1.91,
|
||||||
|
0.93
|
||||||
|
],
|
||||||
|
"height": 2.0,
|
||||||
|
"thumb": "thumbs/procity_street_novelty_record_01.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fittingAliases": {
|
"fittingAliases": {
|
||||||
|
|||||||
BIN
web/assets/thumbs/procity_fit_arcade_cabinet_01.png
Normal file
BIN
web/assets/thumbs/procity_fit_arcade_cabinet_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
web/assets/thumbs/procity_fit_drinks_fridge_01.png
Normal file
BIN
web/assets/thumbs/procity_fit_drinks_fridge_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
web/assets/thumbs/procity_fit_listening_booth_01.png
Normal file
BIN
web/assets/thumbs/procity_fit_listening_booth_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
web/assets/thumbs/procity_fit_milkshake_mixer_01.png
Normal file
BIN
web/assets/thumbs/procity_fit_milkshake_mixer_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
web/assets/thumbs/procity_street_novelty_record_01.png
Normal file
BIN
web/assets/thumbs/procity_street_novelty_record_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@ -6,6 +6,29 @@ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|||||||
|
|
||||||
export const DEPOT = 'https://digalot.fyi/3god'; // shared GLB CDN: GET /a/<file>, /api/list
|
export const DEPOT = 'https://digalot.fyi/3god'; // shared GLB CDN: GET /a/<file>, /api/list
|
||||||
|
|
||||||
|
// Local-depot mode (Lane E round-3): resolve depot:<file> to GLBs served by the dev server
|
||||||
|
// (web/assets/models/) instead of the CDN, so Lanes B/C/F can validate GLB loading with NO
|
||||||
|
// network / before the depot publish. Enable per-load with ?localdepot=1 (or ?localdepot=<base>),
|
||||||
|
// or globally with window.PROCITY_LOCALDEPOT = true | '<base>'. Staged GLBs = the normalized
|
||||||
|
// library set + hero props (mirror of pipeline/_normalized; run pipeline/stage_local_depot.py).
|
||||||
|
export const LOCAL_MODELS = 'assets/models/';
|
||||||
|
function localDepotBase() {
|
||||||
|
try {
|
||||||
|
const q = new URLSearchParams(location.search).get('localdepot');
|
||||||
|
if (q === '1' || q === '' || q === 'true') return LOCAL_MODELS;
|
||||||
|
if (q) return q.endsWith('/') ? q : q + '/';
|
||||||
|
} catch { /* no location (non-browser import) */ }
|
||||||
|
const g = (typeof window !== 'undefined') && window.PROCITY_LOCALDEPOT;
|
||||||
|
if (g === true) return LOCAL_MODELS;
|
||||||
|
if (typeof g === 'string' && g) return g.endsWith('/') ? g : g + '/';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const LOCAL_DEPOT = localDepotBase();
|
||||||
|
// resolve a depot:<file> ref to a concrete URL (local mode → served copy, else the CDN)
|
||||||
|
export function depotURL(file) {
|
||||||
|
return LOCAL_DEPOT ? LOCAL_DEPOT + file : `${DEPOT}/a/${file}`;
|
||||||
|
}
|
||||||
|
|
||||||
const gltfLoader = new GLTFLoader();
|
const gltfLoader = new GLTFLoader();
|
||||||
const texLoader = new THREE.TextureLoader();
|
const texLoader = new THREE.TextureLoader();
|
||||||
const GLB_CACHE = {}; // url → Promise<gltf|null>
|
const GLB_CACHE = {}; // url → Promise<gltf|null>
|
||||||
@ -13,7 +36,7 @@ const TEX_CACHE = {}; // url → THREE.Texture (may still be loading; fallback
|
|||||||
|
|
||||||
// loadGLB('gen/x.glb' | 'depot:file.glb' | full URL) → Promise<gltf|null>, cached, never rejects
|
// loadGLB('gen/x.glb' | 'depot:file.glb' | full URL) → Promise<gltf|null>, cached, never rejects
|
||||||
export function loadGLB(ref) {
|
export function loadGLB(ref) {
|
||||||
const url = ref.startsWith('depot:') ? `${DEPOT}/a/${ref.slice(6)}` : ref;
|
const url = ref.startsWith('depot:') ? depotURL(ref.slice(6)) : ref;
|
||||||
return (GLB_CACHE[url] ||= gltfLoader.loadAsync(url).catch(err => {
|
return (GLB_CACHE[url] ||= gltfLoader.loadAsync(url).catch(err => {
|
||||||
console.warn('[loaders] GLB failed, placeholder stays:', url, err?.message || err);
|
console.warn('[loaders] GLB failed, placeholder stays:', url, err?.message || err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user