#!/usr/bin/env python3 """LANE DOLL — stage keyed garment cutouts onto the 704x1408 paper-doll canvas. Inputs are ALREADY keyed (transparent-bg RGBA, from MODELBEAST bg_remove_local), so unlike doll_composite.install we skip key_white and just crop-scale-place via the SAME SLOT_DEF anchors (measured off base.png). Output: web/world/media/doll/_i_.png — the server's item_art() resolver (server.py:177) wears it for any fashion item whose _art_slug(name)==itemkey. stage stage the curated flowrinse map (default ~/Documents/flowrinse_keyed) one stage a single cutout (gen_wardrobe.py calls this) The keyer isn't perfect — run `doll_composite.py fit` after to eyeball at doll scale (NOTES has the cut list). Curated, not dumped: each flowrinse file maps to a matched EXISTING item slug where one fits (auto-wires, no culture.json change) or a NEW itemkey (LANE DOLL D2 adds the item). """ import sys, os, json sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PIL import Image from doll_composite import place, CANVAS, SLOT_DEF # reuse the measured anchors + compositor HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DOLL_DIR = os.path.join(HERE, 'web', 'world', 'media', 'doll') LAYOUT = os.path.join(HERE, 'web', 'world', 'doll_layout.json') # filename stem -> (slot, itemkey, wiring). wiring: 'match' = itemkey is an EXISTING culture.json # item slug (art auto-wires); 'new' = LANE DOLL D2 adds an item named to slug back to itemkey. FLOWRINSE = { # --- MATCHES (itemkey == an existing item's slug; art auto-wires, no culture.json change) --- 'beanie_in_maroon_mustard_bands': ('hat', 'coogi_knit_beanie', 'match'), 'bucket_hat_neon_colour_blocked_p': ('hat', 'cross_colours_bucket_hat', 'match'), 'bucket_hat_with_surf_print': ('hat', 'mambo_surf_bucket_hat', 'match'), 'black_ribbed_tank_top_graphic': ('top', 'stussy_world_tour_womens_tank', 'match'), 'grey_hoodie_with_c_patch': ('top', 'champion_reverse_weave_hoodie', 'match'), 'silk_shirt_leopard_print': ('top', 'leopard_print_silk_shirt', 'match'), 'windbreaker_with_chevron_panels': ('top', 'womens_windbreaker', 'match'), 'white_cotton_tee_illustration': ('top', 'kmart_plain_white_tee', 'match'), 'baggy_black_cargo_pants_eight_ball': ('bottom', 'stussy_8_ball_cargo_pant', 'match'), 'camouflage_cargo_shorts_laid_flat': ('bottom', 'army_disposal_camo_cargo_shorts','match'), 'denim_shorts_on_white_background': ('bottom', 'kmart_acid_wash_denim_shorts', 'match'), 'knit_skirt_multicolour_purple_te': ('bottom', 'coogi_womens_knit_skirt', 'match'), 'silk_skirt_with_medallion_motif': ('bottom', 'versace_baroque_womens_silk_skirt','match'), 'track_pants_with_popper_studs': ('bottom', 'adidas_track_pants', 'match'), 'white_canvas_court_shoes': ('shoes', 'dunlop_volley_sneakers', 'match'), 'op_shop_tennis_shoe_cream_canvas': ('shoes', 'kmart_canvas_plimsolls', 'match'), 'worn_leather_work_boot_alone': ('shoes', 'combat_boots', 'match'), 'dad_sneakers_with_neon_accents': ('shoes', 'disruptor_ii_sneakers', 'match'), # --- NEW (LANE DOLL D2 adds the item, named to slug back to itemkey) --- 'black_cap_gold_baroque_print': ('hat', 'versace_baroque_cap', 'new'), 'chocolate_brown_corduroy_cap': ('hat', 'corduroy_cap', 'new'), 'felt_hat_on_grey_background': ('hat', 'felt_hat', 'new'), 'chunky_knit_sweater_multicolour': ('top', 'coogi_knit_jumper', 'new'), 'oversized_white_tee_rave_splatter': ('top', 'rave_splatter_tee', 'new'), 'surf_shirt_printed_cartoon_art': ('top', 'mambo_loud_shirt', 'new'), 'navy_athletic_shorts_laid_flat': ('bottom', 'athletic_shorts', 'new'), 'gumboot_isolated_on_grey_background': ('shoes', 'gumboots', 'new'), } # bags use the separate ST.bag system (bag_ overlays), not fashion items — deferred, see NOTES. BAGS = ['1990s_australian_bum_bag_maroon', 'navy_canvas_rucksack_early_1990s'] # non-garment / needs-eyeball — out of scope this round, listed in NOTES. OUT = ['dive_bar_interior_dj_booth', 'paper_doll_clothing_item_dress_u'] # tiny per-stem canvas-px nudges from eyeballing at doll scale (filled after the first fit render). # the two single-boot cutouts lie flat → place() scales them short and they sit high; drop to the feet. NUDGE = { 'worn_leather_work_boot_alone': {'y': 95}, 'gumboot_isolated_on_grey_background': {'y': 45}, } def stage(cutout_path, slot, itemkey, nudge=None): """Crop-scale-place an already-keyed RGBA cutout to _i_.png. Returns the stem.""" if slot not in SLOT_DEF: raise SystemExit(f'unknown slot {slot!r} (want {list(SLOT_DEF)})') im = Image.open(cutout_path).convert('RGBA') n = {'x': 0, 'y': 0, 's': 1.0, **(nudge or {})} stem = f'{slot}_i_{itemkey}' os.makedirs(DOLL_DIR, exist_ok=True) # BAKE the (nudged) placement into the 704x1408 PNG — matches the shipped overlays, which are pre-placed # with a {0,0,1} layout entry (432/433 are). The layout stays neutral so the DOM preview and the PIL # `fit` compositor render identically (contract §4: "DOM and PIL match"); a baked nudge + a live layout # nudge would double. place(im, slot, n['x'], n['y'], n['s']).save(os.path.join(DOLL_DIR, stem + '.png')) lay = json.load(open(LAYOUT)) if os.path.exists(LAYOUT) else {} lay[stem] = {'x': 0, 'y': 0, 's': 1.0} json.dump(lay, open(LAYOUT, 'w'), indent=2, sort_keys=True) return stem def cmd_stage(flowdir): staged, missing = [], [] for fn, (slot, key, wiring) in sorted(FLOWRINSE.items()): src = os.path.join(flowdir, fn + '.png') if not os.path.exists(src): missing.append(fn); continue stem = stage(src, slot, key, NUDGE.get(fn)) staged.append((stem, wiring)) print(f' staged {stem:44} [{wiring}]') print(f'\n{len(staged)} staged, {len(missing)} missing.') print(f' matches (auto-wire): {sum(1 for _,w in staged if w=="match")}') print(f' new items needed: {sum(1 for _,w in staged if w=="new")} -> D2 apply_fashion.py') print(f' deferred (bags): {BAGS}') print(f' out of scope: {OUT}') if missing: print(f' MISSING inputs: {missing}') if __name__ == '__main__': c = sys.argv[1] if len(sys.argv) > 1 else 'stage' if c == 'one': # one print(stage(sys.argv[2], sys.argv[3], sys.argv[4])) else: # stage [flowrinse_dir] cmd_stage(sys.argv[2] if len(sys.argv) > 2 else os.path.expanduser('~/Documents/flowrinse_keyed'))