The 2 R3 TRELLIS glass rejects + optional magazine-rack, generated on fal.ai Hunyuan3D v2 (John-authorized, $22 balance). pipeline/gen_props_fal.py drives the fal operator using the FAL_KEY from MODELBEAST settings (never hardcoded). Frosted/ opaque flux_local concepts gave the reconstructor a surface (clear glass defeated TRELLIS). glass-case 1 att, bus-shelter 2 att (opaque back wall), magazine-rack 1 att = ~$0.64 total, guardrail held. All baked to <=8k via bake_lowpoly.py (switched to EMIT albedo bake — Hunyuan materials bake near-black under DIFFUSE). Verified loading in vendored GLTFLoader, published, validate --depot 0 err. Manifest 17 fittings + 9 furniture; aliases wire glass_case/glass_cabinets/magazine_rack/fridge/listening_corner. Costs logged in AUDIT R5. Last 2 primitive-only fittings retired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
217 lines
12 KiB
Python
217 lines
12 KiB
Python
#!/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") # 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")
|
|
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 ─────────────────────────────────────────
|
|
# 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-<x>-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 (library + round-3 hero props) ----
|
|
fittings, furniture = {}, {}
|
|
# cube_shelf dropped: horizontal node-scale distortion (6.3x8.4m footprint) — see AUDIT.md
|
|
DROP = {"procity_fit_cube_shelf_01.glb"}
|
|
for results_file in (RESULTS, PROPS_RESULTS):
|
|
if not os.path.exists(results_file):
|
|
continue
|
|
for r in json.load(open(results_file)).get("results", []):
|
|
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
|
|
# 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
|
|
# 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 = {
|
|
# registry 'counter' → the ~2m counter-with-till (counter_till), NOT the 4m balcao 'counter'
|
|
"record_bins": "record_crate", "crates": "box_crate", "counter": "counter_till",
|
|
"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",
|
|
# round-5 fal/MODELBEAST hero props → their registry semantic ids
|
|
"glass_case": "glass_case", "glass_cabinets": "glass_case", "magazine_rack": "magazine_rack",
|
|
"fridge": "drinks_fridge", "listening_corner": "listening_booth",
|
|
}
|
|
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-<x>-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:<file>; skins are local gen/<file>",
|
|
},
|
|
"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()
|