diff --git a/E-progress.md b/E-progress.md index b479624..f2094c1 100644 --- a/E-progress.md +++ b/E-progress.md @@ -7,9 +7,302 @@ > lanes need from this one, newest first) and **[`pipeline/AUDIT.md`](pipeline/AUDIT.md)** > (provenance, licence zone, measurements and method, per round). Recorded in R39 rather than > quietly resurrected: a progress file whose last entry is 27 rounds old is a trap for the next -> reader, and the honest fix is a signpost, not a backfill. *(One exception below: the R40 tasking -> explicitly asked for its control-arm transcripts in THIS file, so §40 lives here AND is -> cross-referenced from the notes.)* +> reader, and the honest fix is a signpost, not a backfill. *(Two exceptions below: the R40 tasking +> explicitly asked for its control-arm transcripts in THIS file, and the R41 §41.2 tasking asked for +> its three import tables here, so §40 and §41 live here AND are cross-referenced from the notes.)* + +## Round 41 (§41.2 — THE FITTINGS) · 2026-08-07 + +*Entry here by explicit R41 tasking (as R40 was). Cross-lane hand-off for C in +`docs/LANES/LANE_E_NOTES.md` §41; provenance in `pipeline/AUDIT.md`.* + +**110 assets landed** from three libraries, all interior (ruling 4), all published + sha1-verified, +validator green. The round's one recurring lesson: **every handover number was a triangle count, +and triangles were never the binding constraint — draw calls were.** + +| | source | shipped | rejected | tris | draws | +|---|---|---|---|---|---| +| `~/Documents/3dstore` | 46 | **40** | 6 | 18,420 | 288 → **40** | +| `not-tonight/art_incoming` | 48 | **39** | 3 (+6 `staff*` skipped) | 9,508,361 → 194,837 | 39 | +| ultra `3D=models/dj-gear` | 60 | **31** | 29 not selected | 2,284,094 → 143,340 | 13,813 → **38** | +| **total** | **154** | **110** | — | 11,810,875 → **356,597** | 14,140 → **117** | + +### The finding that shaped the round: draws, not tris + +The tasking said 3dstore "passes the house law AS-IS". On tris, metres, Draco and textures that is +exactly true and I re-verified all four. It is false on the number Lane C spends: **30 of the 46 +files carry 3–16 materials on a single mesh** (one flat colour per record sleeve, +`Mat_Sleeve_`), and glTF gives each material its own primitive, which `GLTFLoader` turns into +its own `Mesh`. A 1,020-tri "Black Tub" cost **16 draw calls**. The interior margin is 162 — ten +tubs would have been the whole room. + +Every one of those materials is a flat `baseColorFactor` with no texture and no UVs, so +`pipeline/merge_prims.py` bakes each primitive's colour into a `COLOR_0` attribute and emits one +white-factor material. glTF multiplies `COLOR_0` by `baseColorFactor` and both are linear, so the +result is **numerically the original colour** — verified by A/B render +(`docs/shots/laneE/r41_3dstore_merge_ab.png`) and by zero triangle drift on all 40. + +The same defect, unmeasured, was in the DJ gear: joined bodies still carried 5–31 materials, so +"one mesh" was still 5–31 draws. `merge_prims.py` over that set took it **530 → 38**. + +### §41.2a — `~/Documents/3dstore` · 40 shipped, 6 rejected + +Re-measured with `glb_stat.py`, not quoted: 45 of 46 are ≤5k tris (median 384), metre-correct, ++Y up, no Draco, no textures. **44 of 45 are base-origin** — `vinylsleeve_v5` is centred +(minY −0.160) and was re-seated. `3dmrp.glb` confirmed at **499,948 tris** + a 4096px texture. + +No Blender pass: the geometry is already correct and every extra tool is a chance to move it. +Spec: `pipeline/r41_3dstore.json` (by name, ruling 3) · stager: `pipeline/r41_stage_3dstore.py`. + +| id | kind | tris | draws | footprint (m) | height (m) | +|---|---|---:|---|---|---:| +| `beam_roof` | arch | 12 | 1 → 1 | 6.5 × 0.32 | 0.36 | +| `column_square` | arch | 12 | 1 → 1 | 0.22 × 0.22 | 2.6 | +| `column_wide` | arch | 12 | 1 → 1 | 0.36 × 0.46 | 2.6 | +| `door_shop` | arch | 144 | 1 → 1 | 1.7 × 0.19 | 2.3 | +| `bin_endoffunk` | bin | 972 | 16 → 1 | 0.36 × 0.34 | 0.34 | +| `chiu_bin` | bin | 996 | 16 → 1 | 0.37 × 0.43 | 0.35 | +| `chiu_bin_half` | bin | 600 | 16 → 1 | 0.24 × 0.43 | 0.35 | +| `chiu_bin_low` | bin | 912 | 16 → 1 | 0.34 × 0.43 | 0.35 | +| `crate_blue` | bin | 936 | 16 → 1 | 0.34 × 0.3 | 0.322 | +| `mos_bin_bottom` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.34 | +| `mos_bin_lower_mid` | bin | 912 | 16 → 1 | 0.34 × 0.36 | 0.34 | +| `mos_bin_top` | bin | 912 | 16 → 1 | 0.34 × 0.2 | 0.34 | +| `mos_bin_upper_mid` | bin | 912 | 16 → 1 | 0.34 × 0.26 | 0.34 | +| `rack_bin_wooden` | bin | 912 | 16 → 1 | 0.34 × 0.4 | 0.338 | +| `tub_black` | bin | 1020 | 16 → 1 | 0.36 × 0.53 | 0.229 | +| `tub_black_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 | +| `tub_white` | bin | 972 | 16 → 1 | 0.34 × 0.53 | 0.229 | +| `tub_white_long` | bin | 1536 | 16 → 1 | 0.53 × 0.36 | 0.229 | +| `counter_station` | counter | 192 | 1 → 1 | 0.88 × 0.5 | 0.87 | +| `counter_station_side` | counter | 192 | 1 → 1 | 0.5 × 0.88 | 0.87 | +| `vinyl_sleeve` | prop | 180 | 4 → 1 | 0.32 × 0.01 | 0.32 | +| `window_booth` | prop | 144 | 1 → 1 | 1.85 × 0.09 | 0.72 | +| `bin_big_4x4` | rack | 288 | 4 → 1 | 0.59 × 1.5 | 1.255 | +| `chiu_rack_long` | rack | 300 | 3 → 1 | 1.88 × 0.43 | 0.795 | +| `chiu_rack_short` | rack | 276 | 3 → 1 | 1.38 × 0.43 | 0.795 | +| `mos_stack` | rack | 204 | 5 → 1 | 0.37 × 0.4 | 1.275 | +| `tubs_3x3` | rack | 168 | 3 → 1 | 0.53 × 1.3 | 0.555 | +| `tubs_3x3_long` | rack | 168 | 3 → 1 | 1.3 × 0.53 | 0.51 | +| `tubs_4x2_vertical` | rack | 240 | 3 → 1 | 0.53 × 1.56 | 0.475 | +| `tubs_4x2_wide` | rack | 240 | 3 → 1 | 1.56 × 0.53 | 0.475 | +| `tubs_shelf_long` | rack | 228 | 3 → 1 | 1.56 × 0.55 | 0.565 | +| `tubs_shelf_wide` | rack | 228 | 3 → 1 | 0.55 × 1.56 | 0.575 | +| `wooden_rack_long` | rack | 384 | 3 → 1 | 0.78 × 1.8 | 0.81 | +| `wooden_rack_wide` | rack | 384 | 3 → 1 | 1.8 × 0.78 | 0.93 | +| `library_trolley` | trolley | 264 | 4 → 1 | 0.41 × 0.88 | 0.74 | +| `aircon_unit` | wall_mount | 12 | 1 → 1 | 1.15 × 0.25 | 0.35 | +| `shelf_board_long` | wall_mount | 12 | 1 → 1 | 4.6 × 0.48 | 0.03 | +| `shelf_board_short` | wall_mount | 12 | 1 → 1 | 0.48 × 1.95 | 0.03 | +| `wall_shelves_big` | wall_mount | 36 | 3 → 1 | 4.65 × 0.05 | 0.76 | +| `wall_shelves_long` | wall_mount | 48 | 4 → 1 | 2.6 × 0.05 | 1.1 | + +**Rejected (6):** + +| source | why | +|---|---| +| `3dmrp.glb` | 499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed. | +| `DOOR.glb` | 0.010 m thick — a 1 cm plane, 12 tris. The solid door is DOOR-front.glb (0.19 m), which is accepted as door_shop. | +| `GREY COLUMN.glb` | 0.010 m thick plane, 12 tris. The solid post is GREY COLUMN-FRONT.glb (0.22 x 0.22 m), accepted as column_square. | +| `Shelf Level.glb` | 4.50 x 0.05 x 0.005 m — a 5 mm trim rail from the booth_room build script, not a placeable fitting. | +| `Shelf Level long.glb` | 2.60 x 0.05 x 0.005 m — same 5 mm trim rail, short version. | +| `floor crate.glb` | 0.34 x 0.34 x 0.002 m — a 2 mm floor decal. Would z-fight Lane C's interior floor; a real crate is crate_blue. | + +### §41.2b — DJ gear · 31 shipped, node contract 4/4 PASS + +**Ruling 5 and the draw budget were in direct conflict, and nobody had measured the second half.** +`gltf-transform optimize` is banned because it deleted `Fader_CROSS` — correct. But the reason it +was reached for is also real: measured on ultra's exports, these models cost **TR-1000 2,145 · +MPC-XL 1,319 · TR-808 1,092 · PMC-08Pro 751 · TTM54i 659 draws**. Against a 162-draw margin, one +PMC-08Pro is 4.6× an entire room. Both "join everything" and "join nothing" ship something +unusable. + +*(Also re-measured: the handover's "median 6,288 tris, 18 already ≤5k" is the stale +`dj-gear/manifest.json`. True median is **51,528 tris**; only **12** of 60 are ≤5k.)* + +`pipeline/rig_join.py` joins **selectively**: scenery merges to one body, each contract node keeps +its own object, name and pivot. Two variants ship — `static` (dressing, 1 draw) and `rigged` +(drivable hero, 2–5 draws). Contract verified by `pipeline/check_contract.py`, which parses the +**shipped GLB's** node list rather than trusting the tool that wrote it. + +| id | mode | tris | draws | source contract | shipped contract | verdict | +|---|---|---|---|---|---|---| +| `deck_1200_rigged` | rigged | 254,048 → 3,821 | 62 → 5 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | PASS | +| `mixer_pmc05pro_rigged` | rigged | 61,872 → 5,000 | 525 → 2 | Fader_CROSS | Fader_CROSS | PASS | +| `mixer_shdj1200_rigged` | rigged | 86,577 → 4,999 | 776 → 2 | Fader_CROSS | Fader_CROSS | PASS | +| `mixer_ttm54i_rigged` | rigged | 75,788 → 5,000 | 659 → 2 | Fader_CROSS | Fader_CROSS | PASS | +| `cart_case` | static | 6,912 → 5,000 | 79 → 1 | — | — | N/A | +| `cart_concorde` | static | 2,853 → 2,853 | 34 → 1 | — | — | N/A | +| `cart_m447` | static | 1,387 → 1,387 | 25 → 1 | — | — | N/A | +| `cart_stanton` | static | 3,290 → 3,290 | 15 → 1 | — | — | N/A | +| `deck_1200` | static | 254,048 → 5,000 | 62 → 1 | Platter_SPIN,Arm_YAW,Fader_PITCH,Btn_STARTSTOP | — | STATIC (contract intentionally dropped) | +| `drum_808` | static | 118,569 → 5,000 | 1092 → 1 | — | — | N/A | +| `drum_tr1000` | static | 119,959 → 4,897 | 2145 → 1 | — | — | N/A | +| `headphones_hd25` | static | 8,495 → 4,999 | 169 → 1 | — | — | N/A | +| `headshell_technics` | static | 2,097 → 2,097 | 30 → 1 | — | — | N/A | +| `mixer_mx2200` | static | 69,127 → 5,000 | 411 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc05pro` | static | 61,872 → 5,000 | 525 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc06pro` | static | 59,633 → 5,000 | 506 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc07pro` | static | 64,752 → 4,999 | 420 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc08pro` | static | 137,885 → 5,000 | 751 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc10` | static | 72,950 → 4,999 | 522 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmc20` | static | 91,127 → 5,000 | 775 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_pmx9000` | static | 130,053 → 5,000 | 807 → 1 | — | — | N/A | +| `mixer_shdj1200` | static | 86,577 → 5,000 | 776 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `mixer_ttm54i` | static | 75,788 → 4,999 | 659 → 1 | Fader_CROSS | — | STATIC (contract intentionally dropped) | +| `sampler_mpc3000` | static | 102,536 → 5,000 | 480 → 1 | — | — | N/A | +| `sampler_s900` | static | 86,536 → 5,000 | 443 → 1 | — | — | N/A | +| `sampler_sp1200` | static | 158,218 → 5,000 | 481 → 1 | — | — | N/A | +| `sl_deck_1` | static | 19,139 → 5,000 | 113 → 1 | — | — | N/A | +| `sl_deck_2` | static | 19,241 → 5,000 | 97 → 1 | — | — | N/A | +| `sl_deck_3` | static | 23,347 → 5,000 | 141 → 1 | — | — | N/A | +| `sl_deck_4` | static | 19,875 → 5,000 | 161 → 1 | — | — | N/A | +| `zip_drive` | static | 9,543 → 5,000 | 72 → 1 | — | — | N/A | + +**`STATIC` = the contract is dropped by design** (a dressing copy is one joined mesh and is not +claimed to be drivable); **`N/A`** = the source carried no contract node (a cartridge has no +crossfader). **FAIL: 0.** Excluded as out-of-period: DIGITAKT-II, DIGITONE-II, POLYEND-DRUMS, +MPC-XL (2020s gear); the rest of the 60 are near-duplicate colourways. + +### §41.2c — pub/venue props · 39 shipped, 3 rejected, 6 `staff*` skipped + +Two handover claims failed re-measurement. **These are not metre-correct**: every source is +unit-normalised to a max dimension of exactly 1.00 m, so a picnic table and an ashtray are the same +size. Aspect *is* preserved, so a real-world height (read off each `*_shot.png` reference render) +fixes all three axes — the height table is `pipeline/r41_pub_batch.json`. And **textures are not +"all fine"**: 37 of 39 have no texture at all (they carry `COLOR_0` vertex colour), while `keg` and +`urinal_trough` carried 2048px maps, resized to 1024. + +**40× decimation was impossible as specified.** COLLAPSE decimation cannot merge across overlapping +shells: `loungeChair` is **94% non-manifold** (376,076 of 399,542 verts) and a ratio-0.01 decimate +moved it 1,319,836 → 1,274,119, i.e. **3%**. `topiary` behaved identically at 93%; `pokie`, at 29%, +decimated normally. So `normalize.py` gained a `remesh` path (voxel remesh → decimate → vertex-colour +transfer), which needed two fixes found by measuring rather than assuming: + +- **colour came back empty** until `bpy.ops.object.datalayout_transfer()` created the destination + layers — a Data Transfer modifier applied without it silently transfers nothing; +- **a voxel remesh needs a volume**, so thin-walled sources (`keg`) returned a wire cage at every + voxel size until a Solidify pass preceded the remesh + (`docs/shots/laneE/r41_shellsoup_solidify.png`). + +| id | src tris | out tris | ratio | non-manifold | footprint (m) | height (m) | +|---|---:|---:|---:|---:|---|---:| +| `ash_urn` | 198,782 | 5,000 | 40× | 43% | 0.32 × 0.33 | 0.75 | +| `ashtray_post` | 198,710 | 5,000 | 40× | 42% | 0.41 × 0.41 | 0.95 | +| `bain_marie` | 184,992 | 4,958 | 37× | 83% | 0.89 × 0.76 | 0.95 | +| `banquette` | 190,350 | 4,999 | 38× | 68% | 1.55 × 1.02 | 0.85 | +| `beer_umbrella` | 192,752 | 4,995 | 39× | 60% | 2.68 × 2.65 | 2.3 | +| `bin_round` | 197,576 | 4,999 | 40× | 40% | 0.53 × 0.51 | 0.75 | +| `bistro_table` | 196,995 | 4,999 | 39× | 38% | 1.18 × 1.18 | 0.92 | +| `broken_basin` | 197,978 | 5,000 | 40× | 53% | 0.59 × 0.49 | 0.6 | +| `cable_snake` | 193,623 | 5,000 | 39× | 64% | 0.45 × 0.47 | 0.2 | +| `can_stack` | 196,005 | 5,000 | 39× | 46% | 0.38 × 0.32 | 0.45 | +| `cdj_deck` | 197,967 | 4,998 | 40× | 33% | 0.63 × 0.42 | 0.15 | +| `champ_bucket` | 183,943 | 4,999 | 37× | 62% | 0.24 × 0.2 | 0.35 | +| `concrete_plinth` | 190,528 | 5,000 | 38× | 53% | 0.76 × 0.74 | 1.2 | +| `crowd_barrier` | 189,524 | 5,000 | 38× | 60% | 1.97 × 0.3 | 1.1 | +| `distro_box` | 193,633 | 5,000 | 39× | 41% | 0.63 × 0.87 | 1.2 | +| `fire_ext` | 198,707 | 5,000 | 40× | 37% | 0.34 × 0.19 | 0.6 | +| `firepit` | 188,041 | 5,000 | 38× | 88% | 0.76 × 0.56 | 0.5 | +| `flight_case` | 195,452 | 5,000 | 39× | 37% | 0.6 × 0.36 | 0.75 | +| `jukebox` | 184,271 | 4,999 | 37× | 75% | 0.97 × 0.41 | 1.52 | +| `keg` | 441,861 | 5,000 | 88× | 23% | 0.4 × 0.4 | 0.6 | +| `lounge_chair` | 1,319,156 | 5,000 | 264× | 94% | 0.69 × 0.69 | 0.8 | +| `marble_sink` | 199,095 | 4,998 | 40× | 21% | 1.15 × 0.56 | 0.85 | +| `old_couch` | 196,277 | 5,000 | 39× | 46% | 1.8 × 0.95 | 0.85 | +| `pallet` | 198,086 | 5,000 | 40× | 30% | 0.79 × 0.73 | 0.2 | +| `picnic_table` | 197,370 | 4,999 | 39× | 42% | 1.38 × 1.41 | 0.75 | +| `pie_warmer` | 196,434 | 5,000 | 39× | 31% | 0.61 × 0.71 | 0.55 | +| `planter` | 239,862 | 4,897 | 49× | 87% | 0.98 × 0.96 | 0.55 | +| `plywood_bar` | 192,907 | 5,000 | 39× | 63% | 1.51 × 0.82 | 1.05 | +| `pokie` | 198,030 | 5,000 | 40× | 29% | 0.75 × 0.78 | 1.7 | +| `raffle_drum` | 193,830 | 5,000 | 39× | 68% | 0.37 × 0.37 | 0.9 | +| `rope_post` | 199,748 | 5,000 | 40× | 27% | 0.32 × 0.32 | 1.0 | +| `sandwich_board` | 198,359 | 5,000 | 40× | 25% | 0.58 × 0.56 | 1.0 | +| `smoke_machine` | 194,423 | 5,000 | 39× | 52% | 0.17 × 0.24 | 0.38 | +| `speaker_stack` | 186,357 | 5,000 | 37× | 59% | 0.52 × 0.52 | 1.8 | +| `strobe_light` | 193,392 | 5,000 | 39× | 57% | 0.22 × 0.24 | 0.4 | +| `sub_bass` | 193,581 | 4,998 | 39× | 53% | 0.87 × 0.63 | 0.7 | +| `topiary` | 415,872 | 4,999 | 83× | 93% | 0.61 × 0.61 | 1.2 | +| `urinal_trough` | 488,808 | 5,000 | 98× | 6% | 0.31 × 0.26 | 0.7 | +| `wheelie_bin` | 195,084 | 5,000 | 39× | 42% | 0.72 × 0.92 | 1.1 | + +**Rejected (3) — eyeballed against each source's `*_shot.png`, per the house rule:** + +| id | src tris | why | +|---|---:|---| +| `balustrade` | 198,229 | glass panel + handrail. The remesh lost the handrail entirely and left a lumpy slab with a detached lump — a transparent thin panel is the worst case for a signed-distance remesh. Eyeballed against balustrade_shot.png; unusable. | +| `cocktail_bar` | 194,298 | back-bar with a lit bottle display. At 5k the bottle shelving smears into a grey L-shaped mass; nothing about it reads as a bar. Rejected rather than shipping mush. | +| `dj_plinth` | 197,159 | the white plinth survives but the controller on top melts into a grey blob. Nothing is lost: cdj_deck normalized cleanly and Lane C can stand it on any counter or the plywood_bar. | + +Plus the **6 `staff*.glb` skipped** as instructed (200k-tri statues, no skin/anim/material — and +PROCITY has a rigged ped fleet, so a photogrammetry human statue would read wrong beside it). + +### Gate, publish, validate + +**The R40 four-surface transmission gate earned its keep on live imports, not a fixture.** Sweeping +the 110 new assets caught **5 real `KHR_materials_transmission` materials** — cartridge dust-covers +(`cc_clear`, `n44_clear`, factor 0.85–0.9) and a mixer meter window (`meter_window`, 0.9). Each +would have made three.js issue *every opaque draw in that room twice*. `strip_transmission.py` +removed all 5 with **BIN chunk byte-identical** (sha1 printed per file). This is the control arm +R40 demonstrated with a synthetic fixture, firing on real cargo. + +Full house-law sweep over all 110 — tris ≤5k, WebP ≤1024, no Draco, base-origin |minY| ≤5 mm, no +transmission, and `_normalized/` byte-identical to the `web/assets/models/` served mirror: +**0 violations.** + +``` +$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/publish.py --files pipeline/_r41_publish_list.txt + published 110/110 this run → _published.json now 169 (was 59). + ✓ _published.json matches the live depot (all 169 recorded assets live). + +$ sha1 verify (fetch each back from the depot, compare bytes) + 110/110 identical, 0 mismatched # → pipeline/_r41_sha1.json + 32.4 MB on the depot + +$ GOD3_DEPOT=http://100.94.195.115:8788 python3 pipeline/validate_manifest.py +manifest v1 — facades 49, skins 92 groups, GLBs 149 (+40 non-manifest GLBs transmission-swept), audio 34 +OK — 0 errors, 0 warning(s) # rc=0 + +$ ... validate_manifest.py --depot # stricter: every GLB must be live on the depot +OK — 0 errors, 0 warning(s) # rc=0 +``` + +`manifest.json` fittings **23 → 133**. Every new entry carries `footprint` [w,d] and `height` in +metres measured **from the shipped GLB** (not from the Blender scene that wrote it — `rig_join`'s +in-scene bounds mis-reported a 0.114 m mixer as 0.629 m, which is why the staging step re-measures +the file), plus `kind`, `tris` and `draws` so Lane C can spend the 162-draw margin deliberately. +DJ entries add `mode`, `control_nodes`, `source_controls` and `drivable`. + +### Tools added / changed (all in `pipeline/`) + +| file | what | +|---|---| +| `merge_prims.py` | **new** — bake `baseColorFactor` → `COLOR_0`, collapse N primitives to 1. The draw-cost fix. | +| `contact_sheet.py` | **new** — labelled multi-GLB sheet in one Blender run, the eyeball tool. | +| `thumbs.py` | **new** — EEVEE 256px thumbs; workbench `TEXTURE` renders vertex-coloured, material-less meshes flat grey, which is 2 of this round's 3 libraries. | +| `rig_join.py` | **new** — the selective join; static + contract-preserving rigged variants. | +| `check_contract.py` | **new** — contract assertion read from the shipped GLB. | +| `normalize.py` | `remesh` / `shell_thickness` / `vertex_colour` options + `nonmanifold_ratio()`; explicit `COLOR_0` export; **orientation bug fixed** — `yaw`/`up` were applied without re-selecting the object, so `transform_apply` silently no-opped (the jukebox came out 0.40 m wide instead of 0.97 m). Now it re-selects and *verifies the dimensions changed*, warning if not. | +| `publish.py` | `--files LIST.txt` — publish an explicit set; `_normalized/` is shared with other lanes and a blanket run would push their half-written staging. | +| `build_manifest.py` | reads the three R41 result files; new entries carry kind/tris/draws/control-nodes. | +| `r41_3dstore.json`, `r41_pub_batch.json`, `r41_djgear_batch.json` | the by-name specs (ruling 3). | + +### Proof shots (`docs/shots/laneE/`) + +`r41_3dstore_40.png` · `r41_3dstore_merge_ab.png` (16-material source vs 1-draw merge, identical) · +`r41_pub_42.png` · `r41_pub_before_after.png` · `r41_pub_rejects.png` (the 3 rejects beside their +sources) · `r41_shellsoup_solidify.png` (the keg cage vs the solidify fix) · `r41_djgear_31.png` · +`r41_djgear_rigged.png` · `r41_djgear_merge_ab.png`. + +### For Lane C (§41.4) + +Everything is in `manifest.json` under `fittings`, keyed by the ids in the tables above. Place on +`footprint`/`height` untouched. Budget notes: **every static asset is 1 draw**; the four `*_rigged` +DJ heroes are 2–5 draws each and are the only ones with drivable `control_nodes`. The `-long`/ +`-wide` 3dstore pairs are the same mesh rotated 90° — pick one, they are not two props. `kind` +tells you what it stands on: `bin`/`rack`/`counter`/`trolley` are floor-standing, `wall_mount` +needs a wall and a height, `arch` is structure, `prop` is a surface dressing. ## Round 40 (§40.1+40.4) · 2026-08-04 — *(entry here by explicit R40 tasking; cross-lane story in `docs/LANES/LANE_E_NOTES.md` §40)* @@ -432,3 +725,286 @@ All fixed and re-verified: 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. + +--- + +# Round 41 (§41.1) — THE MOTION LIBRARY *(Lane E, motion half)* + +**Done, green, and it is the round's critical path — Lane D can start.** 46 clips, 6 grouped GLBs, +**3 498 124 B (3.34 MB)**, zero-draw, plus a new `web/assets/motion_manifest.json`. +Full provenance + the licence lines are in `pipeline/AUDIT.md` under "Round 41 §41.1". + +## 0. Ruling 1 first — the Bandai hazard is closed (paths, exactly) + +| | | +|---|---| +| **was** | `~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` — 3,077 **CC BY-NC 4.0** BVH, 725 MB, unzoned, neutrally named, **nothing in any filename marking them**. One `mirpamo batch` from a shipped build. | +| **now** | `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` | +| **and** | every clip renamed: **3,077 / 3,077** now end `_NC-research.bvh`; `find … ! -name '*_NC-research.bvh'` returns **0** | +| **new** | `~/Documents/MIRPAMO/library/downloads/red/README.md` — the zone law, mirrored from ultra's `MOTIONLIB/red/README.md` | +| **fixed** | `~/Documents/MIRPAMO/library/manifest.json`: `CC-BY-NC-ND 4.0` → **`CC BY-NC 4.0`** (+ `zone:"RED"`, `verified:true`, the renamed path as `name`, and the correction recorded in `notes`) | + +**NOTHING WAS DELETED.** 552 MB of clip data is still on disk byte-for-byte; the instruction +preferred non-destructive and ultra holds the canonical zoned copy anyway +(`~/Documents/MOTIONLIB/red/bandai/`, re-verified live: **3,077 BVH / 552 MB**). + +Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names output +`@.glb`, so **the marker now propagates into any retargeted GLB automatically**. +`downloads/` is gitignored in MIRPAMO, so `red/` inherits that. + +The ledger fix is real, not cosmetic: the bundled `dataset-1/LICENSE` and `dataset-2/LICENSE` both +open *"Attribution-NonCommercial 4.0 International"* — **no ND clause**, as ultra's `SOURCES.md` +already said. Also recorded while I was in there: `downloads/lafan1_ubisoft/` (the other +non-commercial source) is **scaffold only on this box** — 332 KB of 134-byte git-lfs pointers, +**0 BVH**, so it cannot leak; the real payload is ultra's `red/lafan1/`. + +## 1. `mirpamo make smoke` — the tool ground truth, run BEFORE the batch + +**PASSED**, Blender 5.1.2, exit 0 — procedural mesh → autorig → retarget → verify, `[mirpamo] verify OK`. + +## 2. What ships + +`web/models/clips/` (new dir), one fetch per group — ruling 6 is **6 files, not 46**: + +| group file | category | clips | bytes | motion | +|---|---|---:|---:|---:| +| `idles.glb` | idle | 10 | 1 029 496 | 78.3 s | +| `social.glb` | social | 8 | 895 648 | 73.9 s | +| `venue.glb` | venue | 6 | 526 036 | 39.5 s | +| `browse.glb` | browse | 8 | 476 056 | 30.8 s | +| `sitlean.glb` | sitlean | 8 | 358 112 | 27.5 s | +| `locomotion.glb` | locomotion | 6 | 212 776 | 12.1 s | +| **TOTAL ADDED** | | **46** | **3 498 124 B = 3.34 MB** | **262 s** | + +`web/assets/motion_manifest.json` — **16 KB** (new file; I did not touch `manifest.json`, which is +the props half's this round). **Total added bytes: 3 514 636 (3.35 MB).** + +The 8 clips PROCITY already ships cost **4.30 MB**. This is **5.75× the clips for 78% of that**. + +## 3. ZERO-DRAW — proved with `glb_stat.py`, not asserted + +``` +browse.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +idles.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +locomotion.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +sitlean.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +social.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +venue.glb tris=0 meshes=0 materials=0 images=0 nodes=66 draco=False dims=[0,0,0] +``` + +66 nodes = 65 mixamorig joints + `Armature`, i.e. the **byte-shape of the existing `idle.glb`** +(which is 66 nodes / 1 skin / 0 meshes / 0 tris). Confirmed again after loading in three.js: +0 meshes, 0 triangles added to the scene by any group file. + +## 4. THE MANIFEST SCHEMA — `web/assets/motion_manifest.json` (Lane D reads this) + +**One line: load `groups[]` once, then index `clips[]` — the group GLB's +`gltf.animations[]` are NAMED by `clipId`, so `animations.find(a => a.name === clipId)` is the whole +lookup.** + +```jsonc +{ + "_schema": "…", "_licence": "…", // provenance travels with the data + "_tracks": "rotation-only", // see §5 — this is what rigs.js keeps anyway + "_encoding": "rotation outputs are int16 NORMALIZED …; all 65 joints animated incl. fingers", + "_note": "skeleton-only, 0 tris, 66 nodes — same byte-shape as models/peds/idle.glb", + "fps": 30, + "groups": { // ONE ENTRY = ONE FETCH + "idles.glb": { "category":"idle", "desc":"…", "clips":[,…], + "bytes":1029496, "nodes":66, "durationSum":78.3 } + }, + "clips": { // 46 entries + "sit_bench_swing": { + "group": "sitlean.glb", // which file to fetch + "category": "sitlean", + "duration": 2.1333, // seconds, MEASURED from the keyframe times + "loopable": true, // safe to play on repeat + "loopSeamDeg": 0.04, // MEASURED: max per-bone angle, first key vs last key + "source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx", + "mixamo": "Sitting", // the Mixamo catalogue title + "channels": 65, "rotBones": 65, + "pair": "", // OPTIONAL — a matched two-person / open-close partner + "loopDemoted": "…" // OPTIONAL — present iff curated-loopable but seam too wide + } + }, + "totalBytes": 3498124, "clipCount": 46, "groupCount": 6, "dedupSavedBytes": 1341724 +} +``` + +Consumer notes for D: the clips ride the **same `_canon` + `_rotOnly` path as `walk`/`idle`** — 65 +raw tracks in, **64 out** (Hips.quaternion dropped). `loadPedFleet` today takes `r.anims[0]`; a +group file has N, so read them by name. Everything stays behind your gate — I added no loader code +and touched no file under `web/js/`. + +## 5. Where the bytes went — three levers, each measured + +Baseline, straight out of Blender as 46 separate GLBs: **8.20 MB**. + +| lever | → | why it costs nothing real | +|---|---:|---| +| rotation-only tracks | 7.71 MB | `_rotOnly` decodes every translation/scale track and discards it the same frame. Dead bytes in this engine, not fidelity. `--tracks all` restores them | +| accessor dedup by content | 5.98 MB | lossless — **1 341 724 B** of byte-identical keyframe payloads collapsed | +| int16-normalized rotations | **3.34 MB** | glTF-legal; three r175 de-normalises in `GLTFLoader._getArrayFromAccessor` (checked in `web/vendor/`). **Worst angular error across all 46 clips: 0.0034°** | + +**The lever I did NOT pull, for D:** finger channels are **49.7% of the payload** (measured — 3.37 MB +of the pre-quantisation total). `python3 pipeline/clips_pack.py --drop-fingers` takes the whole set +to **1.71 MB**. I kept fidelity; say the word and it is one flag and one minute. + +## 6. Why there is NO retarget — and why that is right + +The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton**. That is the premise of +the house one-canonical-clip-bank law (`rigs.js` `_canon`). Running mixamorig→mixamorig through +MIRPAMO's retargeter would (a) add transfer error for nothing and (b) **emit the ped MESH into the +clip file**, killing the zero-draw property that makes this round affordable. MIRPAMO's retargeter +is for foreign skeletons (its CMU bonemap) and for rigging raw meshes — it was still run (`make +smoke`, green) as the tool check. The Blender-5 slotted-action gotcha is avoided **by construction**: +nothing goes out through FBX at any stage — FBX in, glTF out. + +## 7. The clips, by category (46) — `clipId | Mixamo title | duration | loopable | seam° | source FBX` + +### idles.glb — idle (10) — the clone-army killer +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `idle_breathing` | Breathing Idle | 9.97s | yes | 0.0 | `Breathing_Idle__c9c6d0d5.fbx` | +| `idle_standing` | Standing Idle | 6.03s | yes | 0.0 | `Standing_Idle__c9c831cf.fbx` | +| `idle_look_around` | Looking Around | 6.37s | yes | 0.0 | `Idle_Stand_Looking_Around__c9c6cdf7.fbx` | +| `idle_weight_shift` | Weight Shift | 9.47s | yes | 0.0 | `Shifting_Weight_From_Side_To_Side__c9c908f7.fbx` | +| `idle_cocky_lean` | Being Cocky | 2.93s | no | 0.0 | `Cocky_Lean_Back__c9c90e3c.fbx` | +| `idle_yawn` | Yawn | 8.37s | no | 0.0 | `Big_Yawn_While_Standing__c9c98361.fbx` | +| `idle_happy_1` | Happy Idle | 2.03s | yes | 0.3 | `Happy_Idle_Variation_1__c9ccf37e.fbx` | +| `idle_happy_2` | Happy Idle | 2.97s | yes | 0.0 | `Happy_Idle_Variation_2__c9ccf445.fbx` | +| `idle_old_man` | Old Man Idle | 12.20s | yes | 0.0 | `Old_Man_Standing_Idle__c9c6c004.fbx` | +| `idle_smoking` | Smoking | 17.93s | yes | 0.4 | `Idle_Smoking__c9c603fd.fbx` | +### browse.glb — browse (8) +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `browse_hold_idle` | Holding Idle | 6.03s | yes | 0.1 | `Holding_An_Object_Idle__c9cda11a.fbx` | +| `browse_hold_walk` | Holding Walk | 1.40s | yes | 6.3 | `Holding_An_Object_While_Walking__c9cda1e7.fbx` | +| `browse_hold_turn_l` | Holding Turn Left | 1.23s | no | 172.5 | `Holding_An_Object_While_Turning_Left__c9cda2af.fbx` | +| `browse_hold_turn_r` | Holding Turn Right | 1.33s | no | 147.3 | `Holding_An_Object_While_Turning_Right__c9cda378.fbx` | +| `browse_open_cabinet` | Opening | 4.83s | no | 0.0 | `Opening_Cabinet_Door__c9c6d49e.fbx` | +| `browse_close_cabinet` | Closing | 6.20s | no | 0.0 | `Closing_Cabinet_Door__c9c6d55d.fbx` | +| `browse_carry_box` | Box Idle | 6.33s | yes | 0.0 | `Carrying_A_Box__c9cd8d90.fbx` | +| `browse_pick_up` | Picking Up Object | 3.47s | no | 92.2 | `Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx` | +### sitlean.glb — sitlean (8) +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `sit_bench_swing` | Sitting | 2.13s | yes | 0.0 | `Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx` | +| `sit_hands_thighs` | Sitting Idle | 6.43s | yes | 0.1 | `Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx` | +| `sit_impatient` | Sitting | 7.03s | yes | 0.0 | `Sitting_And_Impatiently_Waiting__c9ccad56.fbx` | +| `sit_fidget_feet` | Sitting | 1.20s | yes | 0.2 | `Sit_With_Fidgeting_Feet__c9c699a5.fbx` | +| `lean_wall_postures` | One Shoulder Lean | 2.03s | yes | 0.0 | `Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx` | +| `lean_wall_leg_up` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx` | +| `lean_wall_shoulders` | Leaning | 3.37s | yes | 0.0 | `Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx` | +| `lean_wall_walkman` | Leaning On A Wall | 1.97s | yes | 0.0 | `Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx` | +### social.glb — social (8) +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `social_conversation` | Talking | 10.30s | yes | 0.3 | `General_Conversation__c9c88301.fbx` | +| `social_chat_water` | Talking At Watercooler | 44.60s | yes | 0.8 | `Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx` | +| `social_greeting` | Standing Greeting | 5.13s | no | 0.0 | `Greeting_While_Standing__c9c7996a.fbx` | +| `social_shake_1` | Shaking Hands 1 | 4.40s | no | 0.0 | `2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx` | +| `social_shake_2` | Shaking Hands 2 | 4.40s | no | 0.1 | `2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx` | +| `social_nod_yes` | Head Nod Yes | 2.63s | no | 0.0 | `Nodding_Head_Yes__c9c90d7b.fbx` | +| `social_shake_no` | Shaking Head No | 1.83s | no | 0.0 | `Shaking_Head_No_Dismissively__c9c90b3c.fbx` | +| `social_wave` | Waving | 0.57s | no | 0.0 | `Waving__c9c5ed32.fbx` | +### locomotion.glb — locomotion (6) +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `walk_shopping_bag` | Walking With Shopping Bag | 1.30s | yes | 0.0 | `Male_Walking_With_Shopping_Bag__c9c64027.fbx` | +| `turn_left_90` | Unarmed Turn Left 90 | 1.53s | no | 84.6 | `Turning_90_Degrees_Left__c9ceef5f.fbx` | +| `turn_right_90` | Unarmed Turn Right 90 | 1.53s | no | 83.2 | `Turning_90_Degrees_Right__c9cef01d.fbx` | +| `turn_walk_180` | Walking Turn 180 | 1.03s | no | 180.0 | `Turning_180_Degrees_While_Walking__c9c9c73f.fbx` | +| `turn_in_place` | Turning | 3.70s | no | 36.7 | `Turning_In_Place__c9c70ef0.fbx` | +| `walk_to_stand` | Stop Walking | 3.03s | no | 55.3 | `Walking_To_Standing_Idle__c9c8d966.fbx` | +### venue.glb — venue (6) +| clip id | Mixamo title | dur | loop | seam° | source FBX | +|---|---|---:|:---:|---:|---| +| `venue_clap_stand` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Standing__c9c60ef2.fbx` | +| `venue_clap_seated` | Clapping | 1.20s | yes | 0.1 | `Clap_While_Seated__c9c60fce.fbx` | +| `venue_cheer` | Cheering | 2.93s | yes | 0.0 | `Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx` | +| `venue_northern_soul` | Northern Soul Dance | 16.10s | yes | 0.4 | `Basic_Northern_Soul_Step__c9cbe56d.fbx` | +| `venue_bartending` | Bartending | 13.60s | yes | 0.0 | `Pouring_And_Serving_Drinks__c9c78799.fbx` | +| `venue_headphones` | Listening To Music | 4.50s | yes | 0.0 | `Male_With_Headphones_Listening_To_Music__c9c63329.fbx` | + +## 8. Curation — three substitutions, because the named seed was wrong for this town + +Every §41.1 seed was checked against `index.json` by name. Three resolved to something the brief +did not intend: + +| §41.1 named | what it actually is in the bank | shipped instead | +|---|---|---| +| the whole `45/90/135/180_Degree_{L,R}_Turn` set | **rifle-aiming.** Every single one is Mixamo "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not want citizens sweeping the street down a barrel | the civilian set: `Turning_90_Degrees_Left/Right` (Mixamo **"Unarmed Turn 90"**), `Turning_180_Degrees_While_Walking`, `Turning_In_Place`, `Walking_To_Standing_Idle` | +| `Examine_Forwards` / `_Backwards` | **static poses, not motion** — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` | +| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `…_With_Shoulders_Against_A_Wall` | + +`2_People_Shaking_Hands_Part_1/2` **does** exist (`…_-_Female` / `…_-_Male`) and **ships as a pair**, +flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close. +Two picks are deliberately for *this* game rather than generic: `Idle_Smoking` and +`Male_Walking_With_Shopping_Bag` (1990s Australian high street), plus `Pouring_And_Serving_Drinks` +for the pub and `Male_With_Headphones_Listening_To_Music` for the record shop. + +## 9. Verification — every claim above is a measurement + +**`pipeline/clips_verify.py` → GREEN, 0 errors, 0 warnings** (new, re-runnable; **F may gate on it** +— it is the "every referenced clip resolves" check). It asserts: file sizes match the manifest · +every clip id resolves to a named animation and nothing extra rides along · every group is zero-draw +and Draco-free · all six carry the identical 66-node skeleton · every channel targets a real node · +every declared duration equals the keyframe times in the bytes · every clip survives `_rotOnly` with +tracks left · quantisation error is bounded. + +**In the repo's own vendored three.js r175**, through `GLTFLoader` + `SkeletonUtils.clone` of a real +`man_casual_01.glb`, all 46 clips: **65 raw tracks → 64 after `_rotOnly`**, durations match to 3 +decimals, every sampled bone world-position finite and moving, 0 meshes / 0 tris added. + +**The R16 trap was checked for on purpose.** R16 found that keeping `Hips.quaternion` on a seated +clip lays the body flat (head folded to hip height). Spine tilt (Hips→Head vs +Y) sampled at 24 +points across every clip: **0 frames over 75°, on all 46.** Worst is 36.5° (`social_chat_water` — +a lean on the watercooler); the seated family sits at 27–28°, a natural seated forward lean. +**Nothing folds.** + +**Loopability is measured.** `loopSeamDeg` = max per-bone angle between first and last rotation key. +28 of 46 loopable. One curated-loopable clip was **auto-demoted by the packer** — `turn_in_place`, +36.7° seam — and carries `loopDemoted` saying so. The one-shots read exactly as expected +(`browse_hold_turn_l` 172.5°, `turn_left_90` 180°): they are turns, they are not supposed to close. + +## 10. Re-running it (all $0, all on-device, ~2 minutes) + +Added to `pipeline/`: `clips_r41.json` (the checked-in selection — **every asset by name, no +wildcard, ruling 3**), `clips_export.py` (Blender), `clips_pack.py` (pure stdlib), `clips_verify.py`. + +```bash +# 1. fetch the 46 named FBX from ultra's GREEN Mixamo bank (BY NAME — never a wildcard) +python3 - <<'EOF' > /tmp/fbx_list.txt +import json;print('\n'.join(c['file'] for g in json.load(open('pipeline/clips_r41.json'))['groups'].values() for c in g['clips'])) +EOF +ssh johnking@100.91.239.7 "cd ~/Documents/MOTIONLIB/green/mixamo_full && tar czf - $(tr '\n' ' ' per-clip skeleton-only GLB (local Blender 5.1.2; FBX in, glTF out — never FBX out) +/Applications/Blender.app/Contents/MacOS/Blender --background --python pipeline/clips_export.py -- \ + --list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb +# 3. merge into the <=6 grouped GLBs + write the manifest +python3 pipeline/clips_pack.py --stage /tmp/clip_glb --out web/models/clips \ + --manifest web/assets/motion_manifest.json # [--drop-fingers] [--no-quantize] [--tracks all] +# 4. gate +python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 0 errors = green +python3 pipeline/glb_stat.py --json web/models/clips # tris must be 0 on all six +``` + +## 11. Handoff + +**Lane D (41.3) — you are unblocked.** `web/assets/motion_manifest.json` is the contract; §4 above is +the schema. `web/models/clips/*.glb` are plain uncompressed GLBs, no Draco, no decoder. Nothing in +`web/js/**` was touched — no loader code, no gate, no default changed, so `?noassets=1` and +`?classic=1` are byte-identical to R40 until you wire it. The per-group `bytes` are in the manifest +so you can pick what boots: `sitlean` + `locomotion` together are **571 KB**; the whole set is +**3.34 MB in 6 fetches**. If that is too much, `--drop-fingers` halves it in one minute. + +**Lane F (41.6):** `python3 pipeline/clips_verify.py` is a ready-made clip-manifest-integrity gate — +exit 0 = green, and it fails loudly on a missing file, a renamed clip, a size drift, a non-zero-draw +group or a skeleton mismatch. `pipeline/glb_stat.py --json web/models/clips` is the zero-draw proof. + +**Files I touched, and only these:** `web/models/clips/{idles,browse,sitlean,social,locomotion,venue}.glb` +(new) · `web/assets/motion_manifest.json` (new) · `pipeline/{clips_r41.json,clips_export.py,clips_pack.py,clips_verify.py}` +(new) · `pipeline/AUDIT.md` · `pipeline/README.md` · `E-progress.md` · `docs/LANES/LANE_E_NOTES.md`. +Outside the repo, for ruling 1 only: `~/Documents/MIRPAMO/library/**`. **No git command was run.** +I did not touch `web/assets/manifest.json` (props half's), `web/js/**`, or `tools/qa.sh`. diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 748f0cc..c2611c8 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,5 +1,80 @@ # LANE E — cross-lane notes +## Round 41 — 110 interior fittings land, and the number that mattered was DRAWS ⟨41.2⟩ + +Shots: `docs/shots/laneE/r41_3dstore_40.png` · `r41_pub_42.png` · `r41_djgear_31.png` · +`r41_pub_rejects.png` · `r41_3dstore_merge_ab.png`. Full tables: `E-progress.md` § Round 41. + +### → LANE C: §41.4 — your stock is in `manifest.json` `fittings` (23 → 133). Place on the numbers untouched. + +**Read this first, it is the whole hand-off:** every asset below is **1 draw** except four DJ heroes +(2–5). That was not free — as handed over, this cargo cost **14,140 draws**; it ships at **117**. +The interior margin is 162, so the naive import would have been 87× over budget on draws while +passing every triangle check. Per-entry `tris` and `draws` are now IN the manifest so you can spend +the margin without guessing. + +Each entry: `file` · `footprint` [w,d] m · `height` m · `thumb` · `kind` · `tris` · `draws` · +`source`. DJ entries add `mode` (`static`|`rigged`), `control_nodes`, `source_controls`, `drivable`. + +**`kind` tells you how to place it:** + +| kind | n | placement | ids | +|---|---:|---|---| +| `bin` | 14 | floor-standing record bins, browsable | `bin_endoffunk`, `chiu_bin`, `chiu_bin_half`, `chiu_bin_low`, `crate_blue`, `mos_bin_bottom`, `mos_bin_lower_mid`, `mos_bin_top`, `mos_bin_upper_mid`, `rack_bin_wooden`, `tub_black`, `tub_black_long`, `tub_white`, `tub_white_long` | +| `rack` | 12 | floor-standing multi-tier racks | `bin_big_4x4`, `chiu_rack_long`, `chiu_rack_short`, `mos_stack`, `tubs_3x3`, `tubs_3x3_long`, `tubs_4x2_vertical`, `tubs_4x2_wide`, `tubs_shelf_long`, `tubs_shelf_wide`, `wooden_rack_long`, `wooden_rack_wide` | +| `counter` | 2 | service counters | `counter_station`, `counter_station_side` | +| `trolley` | 1 | wheeled | `library_trolley` | +| `prop` | 2 | surface dressing | `vinyl_sleeve`, `window_booth` | +| `wall_mount` | 5 | **needs a wall + a mount height — these have no legs** | `aircon_unit`, `shelf_board_long`, `shelf_board_short`, `wall_shelves_big`, `wall_shelves_long` | +| `arch` | 4 | structure (door/columns/beam) | `beam_roof`, `column_square`, `column_wide`, `door_shop` | +| `pub` | 39 | pub/cafe/milkbar/venue, floor-standing | see below | +| `djgear` | 31 | record shop + gig venue hero props | see below | + +**Pub/venue (39):** `ash_urn`, `ashtray_post`, `bain_marie`, `banquette`, `beer_umbrella`, `bin_round`, `bistro_table`, `broken_basin`, `cable_snake`, `can_stack`, `cdj_deck`, `champ_bucket`, `concrete_plinth`, `crowd_barrier`, `distro_box`, `fire_ext`, `firepit`, `flight_case`, `jukebox`, `keg`, `lounge_chair`, `marble_sink`, `old_couch`, `pallet`, `picnic_table`, `pie_warmer`, `planter`, `plywood_bar`, `pokie`, `raffle_drum`, `rope_post`, `sandwich_board`, `smoke_machine`, `speaker_stack`, `strobe_light`, `sub_bass`, `topiary`, `urinal_trough`, `wheelie_bin`. + +**DJ gear — static, 1 draw each (27):** `cart_case`, `cart_concorde`, `cart_m447`, `cart_stanton`, `deck_1200`, `drum_808`, `drum_tr1000`, `headphones_hd25`, `headshell_technics`, `mixer_mx2200`, `mixer_pmc05pro`, `mixer_pmc06pro`, `mixer_pmc07pro`, `mixer_pmc08pro`, `mixer_pmc10`, `mixer_pmc20`, `mixer_pmx9000`, `mixer_shdj1200`, `mixer_ttm54i`, `sampler_mpc3000`, `sampler_s900`, `sampler_sp1200`, `sl_deck_1`, `sl_deck_2`, `sl_deck_3`, `sl_deck_4`, `zip_drive`. + +**DJ gear — rigged heroes (the only drivable ones):** + +| id | draws | control nodes | +|---|---:|---| +| `deck_1200_rigged` | 5 | `Platter_SPIN`, `Arm_YAW`, `Fader_PITCH`, `Btn_STARTSTOP` | +| `mixer_ttm54i_rigged` | 2 | `Fader_CROSS` | +| `mixer_pmc05pro_rigged` | 2 | `Fader_CROSS` | +| `mixer_shdj1200_rigged` | 2 | `Fader_CROSS` | + +Drive them by their **bare node name** (`dj-gear/README.md`: the cap/stem/rib are parented under +it, so moving the node moves the whole control). glTF drops the source's limit constraints, so +**clamp the travel yourself**. Place at most one or two — a static copy is 1 draw and looks the +same standing still. + +**Three traps:** +1. The 3dstore `-long`/`-wide` pairs (`tubs_4x2_wide`/`_vertical`, `tubs_3x3`/`_long`, + `tubs_shelf_long`/`_wide`, `wooden_rack_long`/`_wide`, `chiu_rack_long`/`_short`, + `counter_station`/`_side`) are **the same mesh rotated 90°**. Pick one per placement; shipping + both into one room is paying twice for one prop. +2. `vinyl_sleeve` is a 3 mm record sleeve — its footprint depth is floored to 0.01 m so the + validator accepts it. It is a surface/wall dressing, not something to stand on the floor. +3. `beam_roof` is 6.5 m long and `shelf_board_long` 4.6 m — both are longer than most interiors are + wide. Check the room before placing. + +### → LANE F: §41.6 — the R40 four-surface gate held over 110 new assets, and it caught real cargo + +`validate_manifest.py` is **rc 0, 0 errors, 0 warnings** with `GLBs 149 (+40 non-manifest GLBs +transmission-swept)`, and also rc 0 under `--depot`. Nothing about the gate changed this round; it +was exercised. + +**It fired on live imports, not a fixture.** The sweep caught **5 real +`KHR_materials_transmission` materials** in the DJ gear — three cartridge dust-covers (`cc_clear`, +`n44_clear`, factor 0.85–0.9) and a mixer meter window (`meter_window`, 0.9). Those are *plausible* +glass, which is exactly the R39 failure mode (nobody looks at a material that has a reason to be +transmissive). Each would have made three.js issue every opaque draw in that room twice. +`strip_transmission.py` removed all five with the **BIN chunk byte-identical**. R40 demonstrated +this control with a synthetic fixture; R41 is the same control catching real cargo. + +All 110 are published to the tailnet depot and **sha1-verified byte-identical on fetch-back** +(`pipeline/_r41_sha1.json`); `_published.json` 59 → 169 and matches the live depot. + ## Round 40 — the transmission gate grows eyes on every load path, and the arcade kit is a rule, not an asset ⟨40.1 + 40.4⟩ Shots: `docs/shots/laneE/r40_arcade_posts_before.png` · `r40_arcade_posts_after.png` · @@ -1687,3 +1762,27 @@ gate 3 after your sign-off. - **E2a record_crate: DONE + verified** (loads in vendored GLTFLoader). Re-published. - **E2b counter: in progress** (local, on-device — no fal spend per decision #3). - Not doing: fal.ai spend (glass-case/bus-shelter stay parked), hero-prop bake-to-lowpoly. + +--- + +## Round 41 §41.1 — the motion library (E, motion half) + +- **Ruling 1 closed first.** 3,077 CC BY-NC Bandai-Namco BVH on m3ultra were unzoned and unmarked; + now `~/Documents/MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` with `_NC-research` + on **every** file (3,077/3,077, 0 unmarked) — `mirpamo batch` names output `@.glb`, so + the marker propagates automatically. Nothing deleted (552 MB intact; ultra's `red/bandai/` is the + canonical zoned copy). MIRPAMO's `library/manifest.json` licence corrected `CC-BY-NC-ND 4.0` → + **`CC BY-NC 4.0`** (the bundled LICENSE files say Attribution-NonCommercial, no ND), matching + ultra's `MOTIONLIB/SOURCES.md`. +- **46 clips, 6 grouped GLBs, 3 498 124 B, 0 tris** → `web/models/clips/` + new + `web/assets/motion_manifest.json` (16 KB). 5.75× the clips the game shipped, for 78% of their bytes. +- **Pipeline:** `pipeline/clips_r41.json` (named selection) → `clips_export.py` (Blender) → + `clips_pack.py` (stdlib merge) → `clips_verify.py` (gate, GREEN). See `pipeline/README.md` §10. +- **No retarget:** the bank and the peds are the same mixamorig skeleton. Retargeting would add + error and bake the ped mesh into the clip, ending zero-draw. `mirpamo make smoke` green anyway. +- **Curation note for future rounds:** the `*_Degree_{L,R}_Turn` clips in the Mixamo bank are all + **rifle-aiming**; the civilian equivalents are `Turning_90_Degrees_{Left,Right}` ("Unarmed Turn"). + Bank entries at ~262 KB whose Mixamo title ends "Pose" are **single-frame static poses**, not + motion — check `kb` before picking. +- **Standing lever for D/F:** finger channels are 49.7% of the clip payload; + `clips_pack.py --drop-fingers` takes the set 3.34 MB → 1.71 MB in one minute. diff --git a/docs/shots/laneE/r41_3dstore_40.png b/docs/shots/laneE/r41_3dstore_40.png new file mode 100644 index 0000000..635936f Binary files /dev/null and b/docs/shots/laneE/r41_3dstore_40.png differ diff --git a/docs/shots/laneE/r41_3dstore_merge_ab.png b/docs/shots/laneE/r41_3dstore_merge_ab.png new file mode 100644 index 0000000..243375b Binary files /dev/null and b/docs/shots/laneE/r41_3dstore_merge_ab.png differ diff --git a/docs/shots/laneE/r41_djgear_31.png b/docs/shots/laneE/r41_djgear_31.png new file mode 100644 index 0000000..914e826 Binary files /dev/null and b/docs/shots/laneE/r41_djgear_31.png differ diff --git a/docs/shots/laneE/r41_djgear_merge_ab.png b/docs/shots/laneE/r41_djgear_merge_ab.png new file mode 100644 index 0000000..ac401be Binary files /dev/null and b/docs/shots/laneE/r41_djgear_merge_ab.png differ diff --git a/docs/shots/laneE/r41_djgear_rigged.png b/docs/shots/laneE/r41_djgear_rigged.png new file mode 100644 index 0000000..2bebda2 Binary files /dev/null and b/docs/shots/laneE/r41_djgear_rigged.png differ diff --git a/docs/shots/laneE/r41_pub_42.png b/docs/shots/laneE/r41_pub_42.png new file mode 100644 index 0000000..c290ffe Binary files /dev/null and b/docs/shots/laneE/r41_pub_42.png differ diff --git a/docs/shots/laneE/r41_pub_before_after.png b/docs/shots/laneE/r41_pub_before_after.png new file mode 100644 index 0000000..bcf56df Binary files /dev/null and b/docs/shots/laneE/r41_pub_before_after.png differ diff --git a/docs/shots/laneE/r41_pub_rejects.png b/docs/shots/laneE/r41_pub_rejects.png new file mode 100644 index 0000000..483ea7f Binary files /dev/null and b/docs/shots/laneE/r41_pub_rejects.png differ diff --git a/docs/shots/laneE/r41_shellsoup_solidify.png b/docs/shots/laneE/r41_shellsoup_solidify.png new file mode 100644 index 0000000..467558e Binary files /dev/null and b/docs/shots/laneE/r41_shellsoup_solidify.png differ diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 1c64dd8..bc66c72 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -5,6 +5,36 @@ `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").* +## R41 §41.2 — provenance for the three imported libraries (2026-08-07) + +All three are John's own local libraries; none is a vendor pack and none touches the ruling-3 +deny-list. Every asset entered **by name** through a committed spec, never a wildcard copy. + +| library | path | licence zone | spec | shipped | +|---|---|---|---|---| +| shop fittings | `~/Documents/3dstore/` (this box) | in-house — built in Blender by John (`*.blend` + `build_booth_room_v*.py` sit beside the GLBs) | `pipeline/r41_3dstore.json` | 40 of 46 | +| pub/venue props | `~/Documents/not-tonight/art_incoming/` (this box) | in-house — `trimesh` generator output from John's `not-tonight` project, each with its own `*_shot.png` reference render | `pipeline/r41_pub_batch.json` | 39 of 48 | +| DJ gear | ultra `~/Documents/3D=models/dj-gear/glb/` | in-house — exported 2026-08-01 from John's `~/Documents/opusblender` blends | `pipeline/r41_djgear_batch.json` | 31 of 60 | + +**Trademark note, carried forward from the R4 Akai-MPC flag:** the DJ-gear ids name real hardware +(`drum_808`, `sampler_mpc3000`, `sampler_sp1200`, `sampler_s900`, `mixer_pmc*`, `deck_1200`, +`headphones_hd25`, `cart_m447`). The meshes are John's own reconstructions, not vendor assets, and +the source `README` records that several are already wired into 90sDJsim. Same standing caveat as +R4: **fine in-house; flag before any public/commercial release**, where in-world text should use +generic names. + +**Method (measured, not quoted).** Tri counts, bounding boxes, texture sizes, Draco and +transmission flags from `pipeline/glb_stat.py`; **draw counts** from a direct node→mesh→primitive +walk of each GLB, which is the number `GLTFLoader` turns into `Mesh` objects and the one the +handover notes omitted. Footprints and heights are re-measured **from the shipped GLB** after +export, not from the Blender scene that wrote it — `rig_join.py`'s in-scene bounds mis-reported a +0.114 m mixer as 0.629 m, and Lane C places on these numbers. Every asset was eyeballed on a +contact sheet against its source (`docs/shots/laneE/r41_*.png`); the 9 rejects are listed with +reasons in `E-progress.md` § Round 41. + +**Documented budget exceptions: none.** All 110 land ≤5k tris, WebP ≤1024, no Draco, base-origin. + + ## TL;DR - The library is a **raw dump**: almost nothing obeys the house GLB law. Scale is arbitrary @@ -1100,3 +1130,135 @@ GLBs 36 → 39); **`validate_manifest.py`: 0 errors, 0 warnings.** **Licence unchanged 🟢, $0.** No geometry, no texture and no third-party asset was added or altered — one extension object was deleted from three material blocks. + +## Round 41 §41.1 — THE MOTION LIBRARY (Lane E, motion half) + +*Appended by E-motion. Numbers below are measured with `pipeline/glb_stat.py`, +`pipeline/clips_verify.py` and an in-shell three.js r175 probe — not quoted from the survey.* + +### 0. Ruling 1 — THE BANDAI HAZARD, CLOSED (done first, before any clip work) + +3,077 **CC BY-NC 4.0** (non-commercial, never shippable) Bandai-Namco Research BVH sat on m3ultra in +a neutrally-named, unzoned path with nothing in any filename marking them: +`~/Documents/MIRPAMO/library/downloads/bandai_namco_1/` (725 MB incl. the repo's `src/` figures; +552 MB of clips: dataset-1 = 175 BVH, dataset-2 = 2,902 BVH). + +**Renamed, not deleted** (non-destructive, per the instruction's preference — 552 MB of data still +on disk, byte-for-byte): + +| what | before | after | +|---|---|---| +| the tree | `MIRPAMO/library/downloads/bandai_namco_1/` | `MIRPAMO/library/downloads/red/bandai_namco_1_NC-research/` | +| every clip | `dataset-1_walk_normal_001.bvh` | `dataset-1_walk_normal_001_NC-research.bvh` — **3,077/3,077 renamed, 0 unmarked left** | +| the zone | *(none — MIRPAMO had no zone system)* | new `downloads/red/README.md`, mirroring ultra's `MOTIONLIB/red/README.md` | + +Why per-file and not just the directory: `bin/mirpamo` `cmd_batch` names its output +`@.glb`, so the suffix **propagates into every retargeted GLB automatically** — +a non-commercial clip can no longer reach a shipped build without `_NC-research` in its filename. +`downloads/` is already gitignored in MIRPAMO, so `red/` inherits that and nothing gets committed. + +**Ledger reconciled.** `MIRPAMO/library/manifest.json` said `CC-BY-NC-ND 4.0`. The bundled +`dataset-1/LICENSE` and `dataset-2/LICENSE` both open with *"Attribution-NonCommercial 4.0 +International"* — **CC BY-NC 4.0, no ND clause**, which is what ultra's corrected +`~/Documents/MOTIONLIB/SOURCES.md` says. The entry now carries the correct licence, `"zone": "RED"`, +`"verified": true`, the renamed path as its `name` (so a future `fetch.py` run lands in the marked +path too), and a note recording the correction. The `lafan1_ubisoft` entry — also non-commercial — +got the same zone field plus the measured fact that **this box's copy is scaffold only**: 0 BVH, +332 KB of 134-byte git-lfs pointer files. + +Canonical correctly-zoned copy verified live on ultra: `~/Documents/MOTIONLIB/red/bandai/`, +**3,077 BVH / 552 MB**. Nothing was lost by not deleting; nothing can now leak by accident. + +### 1. Provenance and licence — 🟢 ONE REGIME, MIXAMO ONLY (ruling 2) + +| field | value | +|---|---| +| source | ultra `~/Documents/MOTIONLIB/green/mixamo_full/` — 2,394 anim-only skinless FBX + `index.json` | +| licence | **Mixamo (Adobe)** — free to use inside games/projects, **no attribution requirement** | +| the one rule | **never redistribute or resell as clip packs.** Retargeted per-character clips inside the game are explicitly fine — which is exactly what ships here | +| attribution debt | **none.** R41 adds nothing to the 41.5 credits surface | +| cost | **$0** — local Blender 5.1.2, no cloud, no key | +| deny-list (ruling 3) | untouched. Nothing entered `web/` from `character_kit_modular/exports/bodies/**`, any `anatomy/`, `elsa_coronation_hair_wig_kh3.glb`, `daphne_sexy.glb` or `~/Documents/mocaponline/**`. **No wildcard copy was used at any step** — the 46 source FBX were fetched by explicit name from a checked-in list (`pipeline/clips_r41.json`), each one verified present in `index.json` with a matching Mixamo title before the copy ran | +| CMU / 100STYLE / ACCAD | **not touched.** All attribution-bearing; they wait on 41.5 | + +### 2. What shipped — 46 clips, 6 grouped GLBs, 3 498 124 B + +`web/models/clips/` — **zero-draw, verified, not asserted** (`pipeline/glb_stat.py`): +every group file reports `tris=0 meshes=0 materials=0 images=0 nodes=66 draco=false`. + +| group file | category | clips | bytes | motion | +|---|---|---|---:|---:| +| `idles.glb` | idle | 10 | 1 029 496 | 78.3 s | +| `social.glb` | social | 8 | 895 648 | 73.9 s | +| `venue.glb` | venue | 6 | 526 036 | 39.5 s | +| `browse.glb` | browse | 8 | 476 056 | 30.8 s | +| `sitlean.glb` | sitlean | 8 | 358 112 | 27.5 s | +| `locomotion.glb` | locomotion | 6 | 212 776 | 12.1 s | +| **total** | | **46** | **3 498 124 (3.34 MB)** | **262 s** | + +For scale: the 8 clips PROCITY already ships (`web/models/peds/{idle,walk,sit,look,dance_*}.glb`) +cost **4.30 MB**. R41 adds **5.75× the clips for 78% of that** — and in **6 fetches, not 46** +(ruling 6). `web/assets/motion_manifest.json` is 16 KB. + +### 3. Where the bytes went — three levers, all measured + +Baseline: the 46 clips exported straight out of Blender as separate GLBs = **8.20 MB**. + +| step | result | why it is safe | +|---|---|---:| +| rotation-only tracks | 8.08 → **7.71 MB** | `rigs.js` `_rotOnly` decodes every translation/scale track and throws it away on the same frame. They were dead bytes in this engine, not fidelity | +| accessor dedup by content | 7.71 → **5.98 MB** | lossless. 1 341 724 B of byte-identical keyframe payloads collapsed inside the group files | +| int16-normalized rotations | 5.98 → **3.34 MB** | glTF-legal animation output. three.js r175 de-normalises it in `GLTFLoader._getArrayFromAccessor` (verified in `web/vendor/`). **Worst measured angular error over all 46 clips: 0.0034°** | + +Documented but **not applied**: `--drop-fingers` removes the 40 finger-bone channels, which are +**49.7% of the payload** (measured), taking the set to **1.71 MB**. Fidelity was kept instead — +the lever is one flag away if D's boot budget ever bites. + +### 4. No retarget was run, and that is the correct call + +The Mixamo bank and PROCITY's ped fleet are **the same mixamorig skeleton** — that is the entire +premise of the house "one canonical clip bank" law (`rigs.js` `_canon` folds `mixamorig1/4/…` to +`mixamorig`). Pushing a mixamorig→mixamorig pair through MIRPAMO's retargeter would add +transfer error for nothing **and** would emit the ped MESH into the clip file, destroying the +zero-draw property this round is affordable because of. MIRPAMO's retargeter is for foreign +skeletons (its CMU bonemap) and for rigging raw meshes. + +MIRPAMO was still exercised as the tool ground-truth check the instruction asked for: +**`make smoke` PASSED** (Blender 5.1.2, exit 0, procedural mesh → autorig → retarget → verify). +The documented Blender-5 gotcha was respected by construction: nothing is exported through FBX at +any stage — Mixamo FBX goes **in**, glTF comes **out**, so slotted actions are never flattened. + +### 5. Curation — three substitutions, all because the named clip was wrong for this town + +§41.1's seed list was checked line by line against `index.json`. Three seeds resolved to something +the brief did not intend, and were substituted: + +| seed named in §41.1 | what it actually is in the bank | substituted with | +|---|---|---| +| `45/90/135/180_Degree_{Left,Right}_Turn` (the whole set) | **rifle-aiming**: every one is "Rifle Turn" or "Rifle Crouch Turn". A shopping town does not need a citizen sweeping the street down a barrel | the civilian set — `Turning_90_Degrees_Left/Right` (Mixamo "Unarmed Turn 90"), `Turning_180_Degrees_While_Walking`, `Turning_In_Place`, `Walking_To_Standing_Idle` | +| `Examine_Forwards` / `Examine_Backwards` | **static poses**, not motions — 262 KB single-frame entries titled "Female Crouch Pose" / "Female Action Pose" | `Opening_Cabinet_Door` + `Picking_Up_An_Object_With_One_Hand` (real shop-floor motion) | +| `Legs_Crossed_Leaning_On_Object` | static pose, "Male Standing Pose", 262 KB | `Leaning_Square_Against_A_Wall_With_One_Leg_Up` + `Leaning_Square_With_Shoulders_Against_A_Wall` | + +`2_People_Shaking_Hands_Part_1/2` **does** exist (as `…_-_Female` / `…_-_Male`) and the pair ships +together, flagged `"pair"` in the manifest — as do the two holding-turns and the cabinet open/close. + +### 6. The proof, in the game's own three.js + +`pipeline/clips_verify.py` (new, re-runnable, F may gate on it): **GREEN, 0 errors, 0 warnings** — +sizes match the manifest, every clip id resolves to a named animation, nothing extra rides along, +every group is zero-draw and carries the same 66-node skeleton, every declared duration matches the +keyframe times in the bytes, and every clip survives `_rotOnly` with tracks left. + +Loaded through **the repo's own vendored three.js r175 + GLTFLoader + SkeletonUtils**, bound to a +real `SkeletonUtils.clone` of `man_casual_01.glb`, all 46 clips: 65 raw tracks → **64 after +`_rotOnly`** (Hips.quaternion dropped, exactly as walk/idle behave), durations match to 3 decimals, +every sampled bone position finite and non-degenerate, 0 meshes and 0 triangles added to the scene. + +**The R16 trap was checked for explicitly.** R16 found that keeping `Hips.quaternion` on a seated +clip lays the body flat. Spine tilt (Hips→Head vs +Y) was sampled at 24 points across every clip: +**0 frames over 75° on all 46**, worst clip 36.5° (`social_chat_water`, a lean on the watercooler), +seated family 27–28° (a natural seated forward lean). Nothing folds. + +**Loopability is measured, not asserted.** Every clip carries `loopSeamDeg` = the max per-bone angle +between its first and last rotation key. 28 of 46 are loopable; one curated-loopable clip was +**auto-demoted** by the packer — `turn_in_place`, 36.7° seam — and says so in the manifest. diff --git a/pipeline/README.md b/pipeline/README.md index c848bb7..b9ed59b 100644 --- a/pipeline/README.md +++ b/pipeline/README.md @@ -166,3 +166,44 @@ to D's primitive (round-3 thin-wire lesson — spec allows "primitives when abse `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/`. + +### 10. Motion library — `clips_export.py` → `clips_pack.py` → `clips_verify.py` *(round-41 §41.1: DONE, free, on-device)* + +46 Mixamo clips → **6 grouped skeleton-only GLBs** in `web/models/clips/` (3 498 124 B, 0 tris) + a +**new** `web/assets/motion_manifest.json`. Source is ultra's `~/Documents/MOTIONLIB/green/mixamo_full/` +**only** (Mixamo = GREEN: usable in games, **no attribution**, never redistribute as clip packs). +The selection is checked in at `pipeline/clips_r41.json` — **every asset by name, no wildcard**. + +```bash +python3 - <<'EOF' > /tmp/fbx_list.txt # 1. the 46 names +import json;print('\n'.join(c['file'] for g in json.load(open('pipeline/clips_r41.json'))['groups'].values() for c in g['clips'])) +EOF +mkdir -p /tmp/fbx && ssh johnking@100.91.239.7 \ + "cd ~/Documents/MOTIONLIB/green/mixamo_full && tar czf - $(tr '\n' ' ' per-clip skeleton-only GLB +"$BL" --background --python pipeline/clips_export.py -- --list pipeline/clips_r41.json --fbx /tmp/fbx --out /tmp/clip_glb +python3 pipeline/clips_pack.py --stage /tmp/clip_glb \ # 3. merge to <=6 groups + manifest + --out web/models/clips --manifest web/assets/motion_manifest.json +python3 pipeline/clips_verify.py --stage /tmp/clip_glb # 4. gate: 0 errors = green +``` + +**No retarget is run and none is wanted:** the Mixamo bank and PROCITY's peds are the *same* +mixamorig skeleton (`rigs.js` `_canon` folds `mixamorig1/4/…`). A mixamorig→mixamorig pass through +MIRPAMO would add error **and** bake the ped mesh into the clip file, destroying the zero-draw +property. MIRPAMO (`~/Documents/MIRPAMO`, `make smoke` = its ground truth, green on Blender 5.1.2) +is for foreign skeletons (CMU/BVH) and for rigging raw meshes. **Blender-5 gotcha avoided by +construction: FBX goes in, glTF comes out — nothing is ever exported through FBX.** + +Packer flags (each measured in `AUDIT.md` R41): `--tracks all` restores translation+scale (the +engine's `_rotOnly` discards them, so they ship off by default) · `--no-quantize` keeps float32 +rotations (int16-normalized is ~2× smaller at a worst measured error of **0.0034°**) · +`--drop-fingers` removes the 40 finger channels = **49.7% of the payload**, taking the set to +1.71 MB. Accessor dedup is lossless and always on (saved 1 341 724 B). + +⚠️ **Licence zoning, ruling 1 (closed R41):** MIRPAMO's `library/downloads/` also held 3,077 +**CC BY-NC** (never-shippable) Bandai-Namco BVH, unzoned and unmarked. They now live at +`library/downloads/red/bandai_namco_1_NC-research/` with the `_NC-research` suffix on **every** file +(so `mirpamo batch`'s `@.glb` naming propagates the marker), and MIRPAMO's +`library/manifest.json` licence label is reconciled to ultra's `MOTIONLIB/SOURCES.md`. +**R41 ships one licence regime: Mixamo.** CMU/100STYLE/ACCAD are CC-BY — they wait on 41.5's credits +surface. diff --git a/pipeline/_published.json b/pipeline/_published.json index 46d3a68..e031424 100644 --- a/pipeline/_published.json +++ b/pipeline/_published.json @@ -6,33 +6,143 @@ "dj_phrtt_01.glb", "dj_techno_01.glb", "look.glb", + "procity_fit_aircon_unit_01.glb", "procity_fit_arcade_cabinet_01.glb", + "procity_fit_ash_urn_01.glb", + "procity_fit_ashtray_post_01.glb", + "procity_fit_bain_marie_01.glb", + "procity_fit_banquette_01.glb", "procity_fit_bass_guitar_01.glb", "procity_fit_bass_guitar_01_hi.glb", + "procity_fit_beam_roof_01.glb", + "procity_fit_beer_umbrella_01.glb", + "procity_fit_bin_big_4x4_01.glb", + "procity_fit_bin_endoffunk_01.glb", + "procity_fit_bin_round_01.glb", + "procity_fit_bistro_table_01.glb", "procity_fit_bookshelf_01.glb", "procity_fit_box_crate_01.glb", + "procity_fit_broken_basin_01.glb", + "procity_fit_cable_snake_01.glb", + "procity_fit_can_stack_01.glb", + "procity_fit_cart_case_01.glb", + "procity_fit_cart_concorde_01.glb", + "procity_fit_cart_m447_01.glb", + "procity_fit_cart_stanton_01.glb", + "procity_fit_cdj_deck_01.glb", + "procity_fit_champ_bucket_01.glb", + "procity_fit_chiu_bin_01.glb", + "procity_fit_chiu_bin_half_01.glb", + "procity_fit_chiu_bin_low_01.glb", + "procity_fit_chiu_rack_long_01.glb", + "procity_fit_chiu_rack_short_01.glb", "procity_fit_clothes_rack_01.glb", "procity_fit_coffee_table_01.glb", + "procity_fit_column_square_01.glb", + "procity_fit_column_wide_01.glb", + "procity_fit_concrete_plinth_01.glb", "procity_fit_counter_01.glb", + "procity_fit_counter_station_01.glb", + "procity_fit_counter_station_side_01.glb", "procity_fit_counter_till_01.glb", + "procity_fit_crate_blue_01.glb", "procity_fit_crate_stack_01.glb", + "procity_fit_crowd_barrier_01.glb", "procity_fit_cube_shelf_wide_01.glb", + "procity_fit_deck_1200_01.glb", + "procity_fit_deck_1200_rigged_01.glb", + "procity_fit_distro_box_01.glb", + "procity_fit_door_shop_01.glb", "procity_fit_drinks_fridge_01.glb", + "procity_fit_drum_808_01.glb", "procity_fit_drum_kit_01.glb", "procity_fit_drum_kit_01_hi.glb", + "procity_fit_drum_tr1000_01.glb", "procity_fit_electric_guitar_01.glb", "procity_fit_electric_guitar_01_hi.glb", + "procity_fit_fire_ext_01.glb", + "procity_fit_firepit_01.glb", + "procity_fit_flight_case_01.glb", "procity_fit_glass_case_01.glb", "procity_fit_guitar_amp_01.glb", "procity_fit_guitar_amp_01_hi.glb", + "procity_fit_headphones_hd25_01.glb", + "procity_fit_headshell_technics_01.glb", + "procity_fit_jukebox_01.glb", + "procity_fit_keg_01.glb", + "procity_fit_library_trolley_01.glb", "procity_fit_listening_booth_01.glb", + "procity_fit_lounge_chair_01.glb", "procity_fit_magazine_rack_01.glb", + "procity_fit_marble_sink_01.glb", "procity_fit_mic_stand_01.glb", "procity_fit_mic_stand_01_hi.glb", "procity_fit_milkshake_mixer_01.glb", + "procity_fit_mixer_mx2200_01.glb", + "procity_fit_mixer_pmc05pro_01.glb", + "procity_fit_mixer_pmc05pro_rigged_01.glb", + "procity_fit_mixer_pmc06pro_01.glb", + "procity_fit_mixer_pmc07pro_01.glb", + "procity_fit_mixer_pmc08pro_01.glb", + "procity_fit_mixer_pmc10_01.glb", + "procity_fit_mixer_pmc20_01.glb", + "procity_fit_mixer_pmx9000_01.glb", + "procity_fit_mixer_shdj1200_01.glb", + "procity_fit_mixer_shdj1200_rigged_01.glb", + "procity_fit_mixer_ttm54i_01.glb", + "procity_fit_mixer_ttm54i_rigged_01.glb", + "procity_fit_mos_bin_bottom_01.glb", + "procity_fit_mos_bin_lower_mid_01.glb", + "procity_fit_mos_bin_top_01.glb", + "procity_fit_mos_bin_upper_mid_01.glb", + "procity_fit_mos_stack_01.glb", + "procity_fit_old_couch_01.glb", + "procity_fit_pallet_01.glb", + "procity_fit_picnic_table_01.glb", + "procity_fit_pie_warmer_01.glb", + "procity_fit_planter_01.glb", + "procity_fit_plywood_bar_01.glb", + "procity_fit_pokie_01.glb", + "procity_fit_rack_bin_wooden_01.glb", + "procity_fit_raffle_drum_01.glb", "procity_fit_record_crate_01.glb", + "procity_fit_rope_post_01.glb", + "procity_fit_sampler_mpc3000_01.glb", + "procity_fit_sampler_s900_01.glb", + "procity_fit_sampler_sp1200_01.glb", + "procity_fit_sandwich_board_01.glb", + "procity_fit_shelf_board_long_01.glb", + "procity_fit_shelf_board_short_01.glb", + "procity_fit_sl_deck_1_01.glb", + "procity_fit_sl_deck_2_01.glb", + "procity_fit_sl_deck_3_01.glb", + "procity_fit_sl_deck_4_01.glb", + "procity_fit_smoke_machine_01.glb", + "procity_fit_speaker_stack_01.glb", + "procity_fit_strobe_light_01.glb", + "procity_fit_sub_bass_01.glb", + "procity_fit_topiary_01.glb", + "procity_fit_tub_black_01.glb", + "procity_fit_tub_black_long_01.glb", + "procity_fit_tub_white_01.glb", + "procity_fit_tub_white_long_01.glb", + "procity_fit_tubs_3x3_01.glb", + "procity_fit_tubs_3x3_long_01.glb", + "procity_fit_tubs_4x2_vertical_01.glb", + "procity_fit_tubs_4x2_wide_01.glb", + "procity_fit_tubs_shelf_long_01.glb", + "procity_fit_tubs_shelf_wide_01.glb", + "procity_fit_urinal_trough_01.glb", + "procity_fit_vinyl_sleeve_01.glb", + "procity_fit_wall_shelves_big_01.glb", + "procity_fit_wall_shelves_long_01.glb", + "procity_fit_wheelie_bin_01.glb", + "procity_fit_window_booth_01.glb", "procity_fit_wire_shelf_01.glb", + "procity_fit_wooden_rack_long_01.glb", + "procity_fit_wooden_rack_wide_01.glb", "procity_fit_work_table_01.glb", + "procity_fit_zip_drive_01.glb", "procity_street_aframe_board_01.glb", "procity_street_bench_01.glb", "procity_street_bench_01_hi.glb", diff --git a/pipeline/_r41_3dstore_results.json b/pipeline/_r41_3dstore_results.json new file mode 100644 index 0000000..50bde38 --- /dev/null +++ b/pipeline/_r41_3dstore_results.json @@ -0,0 +1,1113 @@ +{ + "results": [ + { + "src": "Black Tub.glb", + "id": "tub_black", + "kind": "bin", + "file": "procity_fit_tub_black_01.glb", + "tris": 1020, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.36, + 0.53 + ], + "height": 0.229, + "size_m": [ + 0.362, + 0.53, + 0.229 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tub_black_01.png" + }, + { + "src": "Black Tub long.glb", + "id": "tub_black_long", + "kind": "bin", + "file": "procity_fit_tub_black_long_01.glb", + "tris": 1536, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.53, + 0.36 + ], + "height": 0.229, + "size_m": [ + 0.532, + 0.36, + 0.229 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tub_black_long_01.png" + }, + { + "src": "White Tub.glb", + "id": "tub_white", + "kind": "bin", + "file": "procity_fit_tub_white_01.glb", + "tris": 972, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.53 + ], + "height": 0.229, + "size_m": [ + 0.342, + 0.53, + 0.229 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tub_white_01.png" + }, + { + "src": "White Tub long.glb", + "id": "tub_white_long", + "kind": "bin", + "file": "procity_fit_tub_white_long_01.glb", + "tris": 1536, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.53, + 0.36 + ], + "height": 0.229, + "size_m": [ + 0.532, + 0.36, + 0.229 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tub_white_long_01.png" + }, + { + "src": "Blue Crate.glb", + "id": "crate_blue", + "kind": "bin", + "file": "procity_fit_crate_blue_01.glb", + "tris": 936, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.3 + ], + "height": 0.322, + "size_m": [ + 0.344, + 0.3, + 0.322 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_crate_blue_01.png" + }, + { + "src": "Wooden Rack Bin.glb", + "id": "rack_bin_wooden", + "kind": "bin", + "file": "procity_fit_rack_bin_wooden_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.4 + ], + "height": 0.338, + "size_m": [ + 0.34, + 0.4, + 0.338 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_rack_bin_wooden_01.png" + }, + { + "src": "endoffunk1.glb", + "id": "bin_endoffunk", + "kind": "bin", + "file": "procity_fit_bin_endoffunk_01.glb", + "tris": 972, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.36, + 0.34 + ], + "height": 0.34, + "size_m": [ + 0.357, + 0.34, + 0.34 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_bin_endoffunk_01.png" + }, + { + "src": "chiu-long-bin.glb", + "id": "chiu_bin", + "kind": "bin", + "file": "procity_fit_chiu_bin_01.glb", + "tris": 996, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.37, + 0.43 + ], + "height": 0.35, + "size_m": [ + 0.37, + 0.43, + 0.35 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_chiu_bin_01.png" + }, + { + "src": "chiu-long-bin-low.glb", + "id": "chiu_bin_low", + "kind": "bin", + "file": "procity_fit_chiu_bin_low_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.43 + ], + "height": 0.35, + "size_m": [ + 0.34, + 0.43, + 0.35 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_chiu_bin_low_01.png" + }, + { + "src": "chiu-half-bin-sideways.glb", + "id": "chiu_bin_half", + "kind": "bin", + "file": "procity_fit_chiu_bin_half_01.glb", + "tris": 600, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.24, + 0.43 + ], + "height": 0.35, + "size_m": [ + 0.24, + 0.43, + 0.35 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_chiu_bin_half_01.png" + }, + { + "src": "chiu-long.glb", + "id": "chiu_rack_long", + "kind": "rack", + "file": "procity_fit_chiu_rack_long_01.glb", + "tris": 300, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.88, + 0.43 + ], + "height": 0.795, + "size_m": [ + 1.88, + 0.43, + 0.795 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_chiu_rack_long_01.png" + }, + { + "src": "chiu-short.glb", + "id": "chiu_rack_short", + "kind": "rack", + "file": "procity_fit_chiu_rack_short_01.glb", + "tris": 276, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.38, + 0.43 + ], + "height": 0.795, + "size_m": [ + 1.38, + 0.43, + 0.795 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_chiu_rack_short_01.png" + }, + { + "src": "mos-japhop.glb", + "id": "mos_stack", + "kind": "rack", + "file": "procity_fit_mos_stack_01.glb", + "tris": 204, + "draws_before": 5, + "draws_after": 1, + "materials_before": 5, + "footprint": [ + 0.37, + 0.4 + ], + "height": 1.275, + "size_m": [ + 0.37, + 0.4, + 1.275 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_mos_stack_01.png" + }, + { + "src": "mos-japhop-top.glb", + "id": "mos_bin_top", + "kind": "bin", + "file": "procity_fit_mos_bin_top_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.2 + ], + "height": 0.34, + "size_m": [ + 0.34, + 0.2, + 0.34 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_mos_bin_top_01.png" + }, + { + "src": "mos-japhop-upper-mid.glb", + "id": "mos_bin_upper_mid", + "kind": "bin", + "file": "procity_fit_mos_bin_upper_mid_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.26 + ], + "height": 0.34, + "size_m": [ + 0.34, + 0.26, + 0.34 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_mos_bin_upper_mid_01.png" + }, + { + "src": "mos-japhop-lower-mid.glb", + "id": "mos_bin_lower_mid", + "kind": "bin", + "file": "procity_fit_mos_bin_lower_mid_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.36 + ], + "height": 0.34, + "size_m": [ + 0.34, + 0.36, + 0.34 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_mos_bin_lower_mid_01.png" + }, + { + "src": "mos-japhop-bottom.glb", + "id": "mos_bin_bottom", + "kind": "bin", + "file": "procity_fit_mos_bin_bottom_01.glb", + "tris": 912, + "draws_before": 16, + "draws_after": 1, + "materials_before": 16, + "footprint": [ + 0.34, + 0.4 + ], + "height": 0.34, + "size_m": [ + 0.34, + 0.4, + 0.34 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_mos_bin_bottom_01.png" + }, + { + "src": "big-4x4x4-01.glb", + "id": "bin_big_4x4", + "kind": "rack", + "file": "procity_fit_bin_big_4x4_01.glb", + "tris": 288, + "draws_before": 4, + "draws_after": 1, + "materials_before": 4, + "footprint": [ + 0.59, + 1.5 + ], + "height": 1.255, + "size_m": [ + 0.59, + 1.5, + 1.255 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_bin_big_4x4_01.png" + }, + { + "src": "tubs-4x2-wide.glb", + "id": "tubs_4x2_wide", + "kind": "rack", + "file": "procity_fit_tubs_4x2_wide_01.glb", + "tris": 240, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.56, + 0.53 + ], + "height": 0.475, + "size_m": [ + 1.56, + 0.53, + 0.475 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_4x2_wide_01.png" + }, + { + "src": "tubs-4x2-vertical.glb", + "id": "tubs_4x2_vertical", + "kind": "rack", + "file": "procity_fit_tubs_4x2_vertical_01.glb", + "tris": 240, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 0.53, + 1.56 + ], + "height": 0.475, + "size_m": [ + 0.53, + 1.56, + 0.475 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_4x2_vertical_01.png" + }, + { + "src": "white-tub-3x3.glb", + "id": "tubs_3x3", + "kind": "rack", + "file": "procity_fit_tubs_3x3_01.glb", + "tris": 168, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 0.53, + 1.3 + ], + "height": 0.555, + "size_m": [ + 0.53, + 1.3, + 0.555 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_3x3_01.png" + }, + { + "src": "white-tub-3x3-long.glb", + "id": "tubs_3x3_long", + "kind": "rack", + "file": "procity_fit_tubs_3x3_long_01.glb", + "tris": 168, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.3, + 0.53 + ], + "height": 0.51, + "size_m": [ + 1.3, + 0.53, + 0.51 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_3x3_long_01.png" + }, + { + "src": "3-top-4-low-tubs-on-shelf-long.glb", + "id": "tubs_shelf_long", + "kind": "rack", + "file": "procity_fit_tubs_shelf_long_01.glb", + "tris": 228, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.56, + 0.55 + ], + "height": 0.565, + "size_m": [ + 1.56, + 0.55, + 0.565 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_shelf_long_01.png" + }, + { + "src": "3-top-4-low-tubs-on-shelf-wide.glb", + "id": "tubs_shelf_wide", + "kind": "rack", + "file": "procity_fit_tubs_shelf_wide_01.glb", + "tris": 228, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 0.55, + 1.56 + ], + "height": 0.575, + "size_m": [ + 0.55, + 1.56, + 0.575 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_tubs_shelf_wide_01.png" + }, + { + "src": "wooden-rack-type-long.glb", + "id": "wooden_rack_long", + "kind": "rack", + "file": "procity_fit_wooden_rack_long_01.glb", + "tris": 384, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 0.78, + 1.8 + ], + "height": 0.81, + "size_m": [ + 0.78, + 1.8, + 0.81 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_wooden_rack_long_01.png" + }, + { + "src": "wooden-rack-type-wide.glb", + "id": "wooden_rack_wide", + "kind": "rack", + "file": "procity_fit_wooden_rack_wide_01.glb", + "tris": 384, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 1.8, + 0.78 + ], + "height": 0.93, + "size_m": [ + 1.8, + 0.78, + 0.93 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_wooden_rack_wide_01.png" + }, + { + "src": "library trolley.glb", + "id": "library_trolley", + "kind": "trolley", + "file": "procity_fit_library_trolley_01.glb", + "tris": 264, + "draws_before": 4, + "draws_after": 1, + "materials_before": 4, + "footprint": [ + 0.41, + 0.88 + ], + "height": 0.74, + "size_m": [ + 0.41, + 0.88, + 0.74 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_library_trolley_01.png" + }, + { + "src": "STATION 1.glb", + "id": "counter_station", + "kind": "counter", + "file": "procity_fit_counter_station_01.glb", + "tris": 192, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 0.88, + 0.5 + ], + "height": 0.87, + "size_m": [ + 0.88, + 0.5, + 0.87 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_counter_station_01.png" + }, + { + "src": "STATION 2.glb", + "id": "counter_station_side", + "kind": "counter", + "file": "procity_fit_counter_station_side_01.glb", + "tris": 192, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 0.5, + 0.88 + ], + "height": 0.87, + "size_m": [ + 0.5, + 0.88, + 0.87 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_counter_station_side_01.png" + }, + { + "src": "window-booth.glb", + "id": "window_booth", + "kind": "prop", + "file": "procity_fit_window_booth_01.glb", + "tris": 144, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 1.85, + 0.09 + ], + "height": 0.72, + "size_m": [ + 1.85, + 0.09, + 0.72 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_window_booth_01.png" + }, + { + "src": "vinylsleeve_v5.glb", + "id": "vinyl_sleeve", + "kind": "prop", + "file": "procity_fit_vinyl_sleeve_01.glb", + "tris": 180, + "draws_before": 4, + "draws_after": 1, + "materials_before": 4, + "footprint": [ + 0.32, + 0.01 + ], + "height": 0.32, + "size_m": [ + 0.32, + 0.003, + 0.32 + ], + "rebased": true, + "rebase_delta": [ + 0.0, + 0.16, + 0.0 + ], + "thumb": "thumbs/procity_fit_vinyl_sleeve_01.png" + }, + { + "src": "AIR CON.glb", + "id": "aircon_unit", + "kind": "wall_mount", + "file": "procity_fit_aircon_unit_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 1.15, + 0.25 + ], + "height": 0.35, + "size_m": [ + 1.15, + 0.25, + 0.35 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_aircon_unit_01.png" + }, + { + "src": "long guy.glb", + "id": "shelf_board_long", + "kind": "wall_mount", + "file": "procity_fit_shelf_board_long_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 4.6, + 0.48 + ], + "height": 0.03, + "size_m": [ + 4.6, + 0.48, + 0.03 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_shelf_board_long_01.png" + }, + { + "src": "short guy.glb", + "id": "shelf_board_short", + "kind": "wall_mount", + "file": "procity_fit_shelf_board_short_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 0.48, + 1.95 + ], + "height": 0.03, + "size_m": [ + 0.48, + 1.95, + 0.03 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_shelf_board_short_01.png" + }, + { + "src": "wall big.glb", + "id": "wall_shelves_big", + "kind": "wall_mount", + "file": "procity_fit_wall_shelves_big_01.glb", + "tris": 36, + "draws_before": 3, + "draws_after": 1, + "materials_before": 3, + "footprint": [ + 4.65, + 0.05 + ], + "height": 0.76, + "size_m": [ + 4.65, + 0.05, + 0.76 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_wall_shelves_big_01.png" + }, + { + "src": "wall long.glb", + "id": "wall_shelves_long", + "kind": "wall_mount", + "file": "procity_fit_wall_shelves_long_01.glb", + "tris": 48, + "draws_before": 4, + "draws_after": 1, + "materials_before": 4, + "footprint": [ + 2.6, + 0.05 + ], + "height": 1.1, + "size_m": [ + 2.6, + 0.05, + 1.1 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_wall_shelves_long_01.png" + }, + { + "src": "DOOR-front.glb", + "id": "door_shop", + "kind": "arch", + "file": "procity_fit_door_shop_01.glb", + "tris": 144, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 1.7, + 0.19 + ], + "height": 2.3, + "size_m": [ + 1.7, + 0.19, + 2.3 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_door_shop_01.png" + }, + { + "src": "GREY COLUMN-FRONT.glb", + "id": "column_square", + "kind": "arch", + "file": "procity_fit_column_square_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 0.22, + 0.22 + ], + "height": 2.6, + "size_m": [ + 0.22, + 0.22, + 2.6 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_column_square_01.png" + }, + { + "src": "WHITE COLUMN.glb", + "id": "column_wide", + "kind": "arch", + "file": "procity_fit_column_wide_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 0.36, + 0.46 + ], + "height": 2.6, + "size_m": [ + 0.36, + 0.46, + 2.6 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_column_wide_01.png" + }, + { + "src": "ROOF COLUMN.glb", + "id": "beam_roof", + "kind": "arch", + "file": "procity_fit_beam_roof_01.glb", + "tris": 12, + "draws_before": 1, + "draws_after": 1, + "materials_before": 1, + "footprint": [ + 6.5, + 0.32 + ], + "height": 0.36, + "size_m": [ + 6.5, + 0.32, + 0.36 + ], + "rebased": false, + "rebase_delta": [ + 0, + 0, + 0 + ], + "thumb": "thumbs/procity_fit_beam_roof_01.png" + } + ], + "errors": [], + "rejects": [ + { + "src": "3dmrp.glb", + "why": "499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed." + }, + { + "src": "DOOR.glb", + "why": "0.010 m thick \u2014 a 1 cm plane, 12 tris. The solid door is DOOR-front.glb (0.19 m), which is accepted as door_shop." + }, + { + "src": "GREY COLUMN.glb", + "why": "0.010 m thick plane, 12 tris. The solid post is GREY COLUMN-FRONT.glb (0.22 x 0.22 m), accepted as column_square." + }, + { + "src": "Shelf Level.glb", + "why": "4.50 x 0.05 x 0.005 m \u2014 a 5 mm trim rail from the booth_room build script, not a placeable fitting." + }, + { + "src": "Shelf Level long.glb", + "why": "2.60 x 0.05 x 0.005 m \u2014 same 5 mm trim rail, short version." + }, + { + "src": "floor crate.glb", + "why": "0.34 x 0.34 x 0.002 m \u2014 a 2 mm floor decal. Would z-fight Lane C's interior floor; a real crate is crate_blue." + } + ], + "draws_before_total": 288, + "draws_after_total": 40 +} \ No newline at end of file diff --git a/pipeline/_r41_djgear_results.json b/pipeline/_r41_djgear_results.json new file mode 100644 index 0000000..15e360c --- /dev/null +++ b/pipeline/_r41_djgear_results.json @@ -0,0 +1,1033 @@ +{ + "results": [ + { + "src": "TURNTABLE-1200-RIGGED.glb", + "id": "deck_1200", + "kind": "djgear", + "file": "procity_fit_deck_1200_01.glb", + "mode": "static", + "tris_before": 254048, + "tris": 5000, + "draws_before": 62, + "draws": 1, + "contract_source": [ + "Platter_SPIN", + "Arm_YAW", + "Fader_PITCH", + "Btn_STARTSTOP" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Arm_LIFT", + "Arm_YAW", + "Btn_STARTSTOP", + "Fader_PITCH", + "Platter", + "Platter_SPIN" + ], + "source_control_count": 6, + "footprint": [ + 0.45, + 0.51 + ], + "height": 0.118, + "max_tex": 0, + "thumb": "thumbs/procity_fit_deck_1200_01.png" + }, + { + "src": "SL1.glb", + "id": "sl_deck_1", + "kind": "djgear", + "file": "procity_fit_sl_deck_1_01.glb", + "mode": "static", + "tris_before": 19139, + "tris": 5000, + "draws_before": 113, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Knob_MIC_GAIN_MOUNT", + "Knob_MIC_GAIN_ROOT" + ], + "source_control_count": 2, + "footprint": [ + 0.25, + 0.23 + ], + "height": 0.041, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sl_deck_1_01.png" + }, + { + "src": "SL2.glb", + "id": "sl_deck_2", + "kind": "djgear", + "file": "procity_fit_sl_deck_2_01.glb", + "mode": "static", + "tris_before": 19241, + "tris": 5000, + "draws_before": 97, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Knob_CD_PHONO_MOUNT", + "Knob_CD_PHONO_ROOT" + ], + "source_control_count": 2, + "footprint": [ + 0.22, + 0.22 + ], + "height": 0.036, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sl_deck_2_01.png" + }, + { + "src": "SL3.glb", + "id": "sl_deck_3", + "kind": "djgear", + "file": "procity_fit_sl_deck_3_01.glb", + "mode": "static", + "tris_before": 23347, + "tris": 5000, + "draws_before": 141, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.25, + 0.26 + ], + "height": 0.037, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sl_deck_3_01.png" + }, + { + "src": "SL4.glb", + "id": "sl_deck_4", + "kind": "djgear", + "file": "procity_fit_sl_deck_4_01.glb", + "mode": "static", + "tris_before": 19875, + "tris": 5000, + "draws_before": 161, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Fader_SAMPLE_RATE", + "Knob_PHONO_CD_MOUNT", + "Knob_PHONO_CD_ROOT", + "Sw_SAMPLE_RATE_FMT" + ], + "source_control_count": 4, + "footprint": [ + 0.25, + 0.33 + ], + "height": 0.043, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sl_deck_4_01.png" + }, + { + "src": "TTM54i.glb", + "id": "mixer_ttm54i", + "kind": "djgear", + "file": "procity_fit_mixer_ttm54i_01.glb", + "mode": "static", + "tris_before": 75788, + "tris": 4999, + "draws_before": 659, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Btn_EFFECTS_1", + "Btn_EFFECTS_2", + "Btn_HAMSTER_1", + "Btn_HAMSTER_2", + "Btn_XF_HAMSTER", + "Btn_XF_HAMSTER_FMOUNT", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind", + "Fader_BALANCE_2", + "Fader_BALANCE_2_ind", + "Fader_CROSS", + "Fader_CROSS_ind" + ], + "source_control_count": 295, + "footprint": [ + 0.23, + 0.42 + ], + "height": 0.107, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_ttm54i_01.png" + }, + { + "src": "PMC-05PRO.glb", + "id": "mixer_pmc05pro", + "kind": "djgear", + "file": "procity_fit_mixer_pmc05pro_01.glb", + "mode": "static", + "tris_before": 61872, + "tris": 5000, + "draws_before": 525, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_CF_MONITOR", + "Fader_CF_MONITOR_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_MON_LEVEL", + "Fader_MON_LEVEL_ind", + "Fader_PGM1", + "Fader_PGM1_ind", + "Fader_PGM2", + "Fader_PGM2_ind", + "Knob_CF_MODE", + "Knob_CF_MODE_ROOT" + ], + "source_control_count": 271, + "footprint": [ + 0.26, + 0.4 + ], + "height": 0.111, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc05pro_01.png" + }, + { + "src": "PMC-06ProA.glb", + "id": "mixer_pmc06pro", + "kind": "djgear", + "file": "procity_fit_mixer_pmc06pro_01.glb", + "mode": "static", + "tris_before": 59633, + "tris": 5000, + "draws_before": 506, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Btn_CF_REV", + "Fader_CROSS", + "Fader_MON_CF", + "Fader_PGM1_LEVEL", + "Fader_PGM2_LEVEL", + "Knob_CF_MODE", + "Knob_CF_MODE_MOUNT", + "Knob_CF_MODE_ROOT", + "Knob_CF_MODE_k", + "Knob_CF_MODE_k.001", + "Knob_CF_MODE_k.002", + "Knob_CF_MODE_k.003" + ], + "source_control_count": 220, + "footprint": [ + 0.15, + 0.47 + ], + "height": 0.074, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc06pro_01.png" + }, + { + "src": "PMC-07Pro.glb", + "id": "mixer_pmc07pro", + "kind": "djgear", + "file": "procity_fit_mixer_pmc07pro_01.glb", + "mode": "static", + "tris_before": 64752, + "tris": 4999, + "draws_before": 420, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_BAL_1", + "Fader_BAL_2", + "Fader_CF_MON", + "Fader_CROSS", + "Fader_EQ1_HI", + "Fader_EQ1_LO", + "Fader_EQ1_MID", + "Fader_EQ2_HI", + "Fader_EQ2_LO", + "Fader_EQ2_MID", + "Fader_MASTER_LEVEL", + "Fader_MICEQ_HI" + ], + "source_control_count": 159, + "footprint": [ + 0.25, + 0.42 + ], + "height": 0.113, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc07pro_01.png" + }, + { + "src": "PMC-08Pro.glb", + "id": "mixer_pmc08pro", + "kind": "djgear", + "file": "procity_fit_mixer_pmc08pro_01.glb", + "mode": "static", + "tris_before": 137885, + "tris": 5000, + "draws_before": 751, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_BAL_1", + "Fader_BAL_2", + "Fader_CF_REV", + "Fader_CF_REV_FMOUNT", + "Fader_CROSS", + "Fader_EFF_BAL", + "Fader_EFF_LEVEL", + "Fader_IF1_REV", + "Fader_IF1_REV_FMOUNT", + "Fader_IF2_REV", + "Fader_IF2_REV_FMOUNT", + "Fader_ISO1_HI" + ], + "source_control_count": 414, + "footprint": [ + 0.26, + 0.46 + ], + "height": 0.119, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc08pro_01.png" + }, + { + "src": "PMC-10.glb", + "id": "mixer_pmc10", + "kind": "djgear", + "file": "procity_fit_mixer_pmc10_01.glb", + "mode": "static", + "tris_before": 72950, + "tris": 4999, + "draws_before": 522, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_BALANCE", + "Fader_BALANCE_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_EQ_1", + "Fader_EQ_1_ind", + "Fader_EQ_2", + "Fader_EQ_2_ind", + "Fader_EQ_3", + "Fader_EQ_3_ind", + "Fader_EQ_4", + "Fader_EQ_4_ind" + ], + "source_control_count": 74, + "footprint": [ + 0.48, + 0.22 + ], + "height": 0.222, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc10_01.png" + }, + { + "src": "PMC-20.glb", + "id": "mixer_pmc20", + "kind": "djgear", + "file": "procity_fit_mixer_pmc20_01.glb", + "mode": "static", + "tris_before": 91127, + "tris": 5000, + "draws_before": 775, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Btn_EFFECT_1", + "Btn_EFFECT_2", + "Btn_EFFECT_3", + "Btn_MON_EFFECT", + "Btn_MON_PGMA", + "Btn_MON_PGMB", + "Btn_MON_PGMC", + "Btn_POWER", + "Btn_TRANSFORMER_1", + "Btn_TRANSFORMER_2", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind" + ], + "source_control_count": 289, + "footprint": [ + 0.51, + 0.22 + ], + "height": 0.222, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc20_01.png" + }, + { + "src": "SH-DJ1200.glb", + "id": "mixer_shdj1200", + "kind": "djgear", + "file": "procity_fit_mixer_shdj1200_01.glb", + "mode": "static", + "tris_before": 86577, + "tris": 5000, + "draws_before": 776, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_A", + "Fader_A_ind", + "Fader_B", + "Fader_B_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Knob_AUX", + "Knob_AUX_ROOT", + "Knob_AUX_k", + "Knob_AUX_k.001", + "Knob_AUX_k.002", + "Knob_AUX_k.003" + ], + "source_control_count": 433, + "footprint": [ + 0.26, + 0.39 + ], + "height": 0.114, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_shdj1200_01.png" + }, + { + "src": "MX-2200.glb", + "id": "mixer_mx2200", + "kind": "djgear", + "file": "procity_fit_mixer_mx2200_01.glb", + "mode": "static", + "tris_before": 69127, + "tris": 5000, + "draws_before": 411, + "draws": 1, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [], + "contract_verdict": "STATIC (contract intentionally dropped)", + "named_controls": [ + "Fader_CROSS", + "Fader_CUE", + "Fader_MIC", + "Fader_PHONO1", + "Fader_PHONO2", + "Sw_CUE", + "Sw_CUE_ROOT", + "Sw_CUE_base", + "Sw_CUE_tip", + "Sw_DJMIC", + "Sw_DJMIC_ROOT", + "Sw_DJMIC_base" + ], + "source_control_count": 63, + "footprint": [ + 0.3, + 0.31 + ], + "height": 0.103, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_mx2200_01.png" + }, + { + "src": "PMX-9000.glb", + "id": "mixer_pmx9000", + "kind": "djgear", + "file": "procity_fit_mixer_pmx9000_01.glb", + "mode": "static", + "tris_before": 130053, + "tris": 5000, + "draws_before": 807, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Fader_CUE_LEVEL", + "Fader_EQ_15K", + "Fader_EQ_1K", + "Fader_EQ_250", + "Fader_EQ_55", + "Fader_EQ_5K", + "Fader_IN1_LEVEL", + "Fader_IN1_PRESET", + "Fader_IN2_LEVEL", + "Fader_IN2_PRESET", + "Fader_IN3_LEVEL", + "Fader_IN3_PRESET" + ], + "source_control_count": 235, + "footprint": [ + 0.48, + 0.37 + ], + "height": 0.178, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmx9000_01.png" + }, + { + "src": "TR-808.glb", + "id": "drum_808", + "kind": "djgear", + "file": "procity_fit_drum_808_01.glb", + "mode": "static", + "tris_before": 118569, + "tris": 5000, + "draws_before": 1092, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_START_STOP", + "Btn_TAP", + "Knob_AUTO_FILL", + "Knob_AUTO_FILL_MOUNT", + "Knob_AUTO_FILL_ROOT", + "Knob_AUTO_FILL_k", + "Knob_AUTO_FILL_k.001", + "Knob_AUTO_FILL_k.002", + "Knob_AUTO_FILL_k.003", + "Knob_AUTO_FILL_k.004", + "Knob_AUTO_FILL_k.005", + "Knob_AUTO_FILL_k.006" + ], + "source_control_count": 725, + "footprint": [ + 0.53, + 0.47 + ], + "height": 0.153, + "max_tex": 0, + "thumb": "thumbs/procity_fit_drum_808_01.png" + }, + { + "src": "TR-1000.glb", + "id": "drum_tr1000", + "kind": "djgear", + "file": "procity_fit_drum_tr1000_01.glb", + "mode": "static", + "tris_before": 119959, + "tris": 4897, + "draws_before": 2145, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_FILL_IN_TRIG", + "Btn_FILL_IN_TRIG_BMOUNT", + "Btn_INST_BD", + "Btn_INST_BD_BMOUNT", + "Btn_INST_CC", + "Btn_INST_CC_BMOUNT", + "Btn_INST_CH", + "Btn_INST_CH_BMOUNT", + "Btn_INST_HC", + "Btn_INST_HC_BMOUNT", + "Btn_INST_HT", + "Btn_INST_HT_BMOUNT" + ], + "source_control_count": 1957, + "footprint": [ + 0.47, + 0.37 + ], + "height": 0.161, + "max_tex": 0, + "thumb": "thumbs/procity_fit_drum_tr1000_01.png" + }, + { + "src": "SP-1200.glb", + "id": "sampler_sp1200", + "kind": "djgear", + "file": "procity_fit_sampler_sp1200_01.glb", + "mode": "static", + "tris_before": 158218, + "tris": 5000, + "draws_before": 481, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_ABCD", + "Btn_DISK", + "Btn_ENTER", + "Btn_KP_0", + "Btn_KP_1", + "Btn_KP_2", + "Btn_KP_3", + "Btn_KP_4", + "Btn_KP_5", + "Btn_KP_6", + "Btn_KP_7", + "Btn_KP_8" + ], + "source_control_count": 191, + "footprint": [ + 0.44, + 0.62 + ], + "height": 0.191, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sampler_sp1200_01.png" + }, + { + "src": "MPC3000.glb", + "id": "sampler_mpc3000", + "kind": "djgear", + "file": "procity_fit_sampler_mpc3000_01.glb", + "mode": "static", + "tris_before": 102536, + "tris": 5000, + "draws_before": 480, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_16_LEVELS", + "Btn_COUNT_IN", + "Btn_CUR_DOWN", + "Btn_CUR_LEFT", + "Btn_CUR_RIGHT", + "Btn_CUR_UP", + "Btn_ERASE", + "Btn_FN_DISK", + "Btn_FN_EDIT", + "Btn_FN_MAIN", + "Btn_FN_MIDI", + "Btn_FN_MIX" + ], + "source_control_count": 188, + "footprint": [ + 0.47, + 0.59 + ], + "height": 0.119, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sampler_mpc3000_01.png" + }, + { + "src": "S900.glb", + "id": "sampler_s900", + "kind": "djgear", + "file": "procity_fit_sampler_s900_01.glb", + "mode": "static", + "tris_before": 86536, + "tris": 5000, + "draws_before": 443, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_DISK", + "Btn_EDIT_PRGRM", + "Btn_EDIT_SAMPLE", + "Btn_ENT", + "Btn_KP_0", + "Btn_KP_1", + "Btn_KP_2", + "Btn_KP_3", + "Btn_KP_4", + "Btn_KP_5", + "Btn_KP_6", + "Btn_KP_7" + ], + "source_control_count": 175, + "footprint": [ + 0.47, + 0.56 + ], + "height": 0.135, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sampler_s900_01.png" + }, + { + "src": "HD25.glb", + "id": "headphones_hd25", + "kind": "djgear", + "file": "procity_fit_headphones_hd25_01.glb", + "mode": "static", + "tris_before": 8495, + "tris": 4999, + "draws_before": 169, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.23, + 0.16 + ], + "height": 0.302, + "max_tex": 0, + "thumb": "thumbs/procity_fit_headphones_hd25_01.png" + }, + { + "src": "M44-7.glb", + "id": "cart_m447", + "kind": "djgear", + "file": "procity_fit_cart_m447_01.glb", + "mode": "static", + "tris_before": 1387, + "tris": 1387, + "draws_before": 25, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.04, + 0.02 + ], + "height": 0.028, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cart_m447_01.png" + }, + { + "src": "CONCORDE-PRO.glb", + "id": "cart_concorde", + "kind": "djgear", + "file": "procity_fit_cart_concorde_01.glb", + "mode": "static", + "tris_before": 2853, + "tris": 2853, + "draws_before": 34, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.1, + 0.02 + ], + "height": 0.057, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cart_concorde_01.png" + }, + { + "src": "CONCORDE-CASE.glb", + "id": "cart_case", + "kind": "djgear", + "file": "procity_fit_cart_case_01.glb", + "mode": "static", + "tris_before": 6912, + "tris": 5000, + "draws_before": 79, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.22, + 0.18 + ], + "height": 0.208, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cart_case_01.png" + }, + { + "src": "HEADSHELL-TECHNICS.glb", + "id": "headshell_technics", + "kind": "djgear", + "file": "procity_fit_headshell_technics_01.glb", + "mode": "static", + "tris_before": 2097, + "tris": 2097, + "draws_before": 30, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.08, + 0.02 + ], + "height": 0.023, + "max_tex": 0, + "thumb": "thumbs/procity_fit_headshell_technics_01.png" + }, + { + "src": "STANTON-500.glb", + "id": "cart_stanton", + "kind": "djgear", + "file": "procity_fit_cart_stanton_01.glb", + "mode": "static", + "tris_before": 3290, + "tris": 3290, + "draws_before": 15, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [], + "source_control_count": 0, + "footprint": [ + 0.04, + 0.02 + ], + "height": 0.025, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cart_stanton_01.png" + }, + { + "src": "ZIP-100.glb", + "id": "zip_drive", + "kind": "djgear", + "file": "procity_fit_zip_drive_01.glb", + "mode": "static", + "tris_before": 9543, + "tris": 5000, + "draws_before": 72, + "draws": 1, + "contract_source": [], + "contract_shipped": [], + "contract_verdict": "N/A", + "named_controls": [ + "Btn_EJECT" + ], + "source_control_count": 1, + "footprint": [ + 0.14, + 0.23 + ], + "height": 0.039, + "max_tex": 0, + "thumb": "thumbs/procity_fit_zip_drive_01.png" + }, + { + "src": "TURNTABLE-1200-RIGGED.glb", + "id": "deck_1200_rigged", + "kind": "djgear", + "file": "procity_fit_deck_1200_rigged_01.glb", + "mode": "rigged", + "tris_before": 254048, + "tris": 3821, + "draws_before": 62, + "draws": 5, + "contract_source": [ + "Platter_SPIN", + "Arm_YAW", + "Fader_PITCH", + "Btn_STARTSTOP" + ], + "contract_shipped": [ + "Platter_SPIN", + "Arm_YAW", + "Fader_PITCH", + "Btn_STARTSTOP" + ], + "contract_verdict": "PASS", + "named_controls": [ + "Arm_LIFT", + "Arm_YAW", + "Btn_STARTSTOP", + "Fader_PITCH", + "Platter", + "Platter_SPIN" + ], + "source_control_count": 6, + "footprint": [ + 0.51, + 0.56 + ], + "height": 0.11, + "max_tex": 0, + "thumb": "thumbs/procity_fit_deck_1200_rigged_01.png" + }, + { + "src": "TTM54i.glb", + "id": "mixer_ttm54i_rigged", + "kind": "djgear", + "file": "procity_fit_mixer_ttm54i_rigged_01.glb", + "mode": "rigged", + "tris_before": 75788, + "tris": 5000, + "draws_before": 659, + "draws": 2, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [ + "Fader_CROSS" + ], + "contract_verdict": "PASS", + "named_controls": [ + "Btn_EFFECTS_1", + "Btn_EFFECTS_2", + "Btn_HAMSTER_1", + "Btn_HAMSTER_2", + "Btn_XF_HAMSTER", + "Btn_XF_HAMSTER_FMOUNT", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind", + "Fader_BALANCE_2", + "Fader_BALANCE_2_ind", + "Fader_CROSS", + "Fader_CROSS_ind" + ], + "source_control_count": 295, + "footprint": [ + 0.23, + 0.42 + ], + "height": 0.107, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_ttm54i_rigged_01.png" + }, + { + "src": "PMC-05PRO.glb", + "id": "mixer_pmc05pro_rigged", + "kind": "djgear", + "file": "procity_fit_mixer_pmc05pro_rigged_01.glb", + "mode": "rigged", + "tris_before": 61872, + "tris": 5000, + "draws_before": 525, + "draws": 2, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [ + "Fader_CROSS" + ], + "contract_verdict": "PASS", + "named_controls": [ + "Fader_CF_MONITOR", + "Fader_CF_MONITOR_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_MON_LEVEL", + "Fader_MON_LEVEL_ind", + "Fader_PGM1", + "Fader_PGM1_ind", + "Fader_PGM2", + "Fader_PGM2_ind", + "Knob_CF_MODE", + "Knob_CF_MODE_ROOT" + ], + "source_control_count": 271, + "footprint": [ + 0.26, + 0.4 + ], + "height": 0.111, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_pmc05pro_rigged_01.png" + }, + { + "src": "SH-DJ1200.glb", + "id": "mixer_shdj1200_rigged", + "kind": "djgear", + "file": "procity_fit_mixer_shdj1200_rigged_01.glb", + "mode": "rigged", + "tris_before": 86577, + "tris": 4999, + "draws_before": 776, + "draws": 2, + "contract_source": [ + "Fader_CROSS" + ], + "contract_shipped": [ + "Fader_CROSS" + ], + "contract_verdict": "PASS", + "named_controls": [ + "Fader_A", + "Fader_A_ind", + "Fader_B", + "Fader_B_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Knob_AUX", + "Knob_AUX_ROOT", + "Knob_AUX_k", + "Knob_AUX_k.001", + "Knob_AUX_k.002", + "Knob_AUX_k.003" + ], + "source_control_count": 433, + "footprint": [ + 0.26, + 0.39 + ], + "height": 0.114, + "max_tex": 0, + "thumb": "thumbs/procity_fit_mixer_shdj1200_rigged_01.png" + } + ], + "problems": [ + [ + "procity_fit_cart_case_01.glb", + "not base-origin minY=-0.162" + ], + [ + "procity_fit_deck_1200_rigged_01.glb", + "not base-origin minY=0.018" + ] + ] +} \ No newline at end of file diff --git a/pipeline/_r41_pub_results.json b/pipeline/_r41_pub_results.json new file mode 100644 index 0000000..ff3b11a --- /dev/null +++ b/pipeline/_r41_pub_results.json @@ -0,0 +1,731 @@ +{ + "results": [ + { + "src": "ashUrn.glb", + "id": "ash_urn", + "kind": "pub", + "file": "procity_fit_ash_urn_01.glb", + "tris_before": 198782, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.435, + "vertex_colour": true, + "footprint": [ + 0.32, + 0.33 + ], + "height": 0.75, + "max_tex": 0, + "thumb": "thumbs/procity_fit_ash_urn_01.png" + }, + { + "src": "ashtray.glb", + "id": "ashtray_post", + "kind": "pub", + "file": "procity_fit_ashtray_post_01.glb", + "tris_before": 198710, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.425, + "vertex_colour": true, + "footprint": [ + 0.41, + 0.41 + ], + "height": 0.95, + "max_tex": 0, + "thumb": "thumbs/procity_fit_ashtray_post_01.png" + }, + { + "src": "bainMarie.glb", + "id": "bain_marie", + "kind": "pub", + "file": "procity_fit_bain_marie_01.glb", + "tris_before": 184992, + "tris": 4958, + "draws": 1, + "nonmanifold": 0.835, + "vertex_colour": true, + "footprint": [ + 0.89, + 0.76 + ], + "height": 0.95, + "max_tex": 0, + "thumb": "thumbs/procity_fit_bain_marie_01.png" + }, + { + "src": "banquette.glb", + "id": "banquette", + "kind": "pub", + "file": "procity_fit_banquette_01.glb", + "tris_before": 190350, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.686, + "vertex_colour": true, + "footprint": [ + 1.55, + 1.02 + ], + "height": 0.85, + "max_tex": 0, + "thumb": "thumbs/procity_fit_banquette_01.png" + }, + { + "src": "barrier.glb", + "id": "crowd_barrier", + "kind": "pub", + "file": "procity_fit_crowd_barrier_01.glb", + "tris_before": 189524, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.603, + "vertex_colour": true, + "footprint": [ + 1.97, + 0.3 + ], + "height": 1.1, + "max_tex": 0, + "thumb": "thumbs/procity_fit_crowd_barrier_01.png" + }, + { + "src": "beerUmbrella.glb", + "id": "beer_umbrella", + "kind": "pub", + "file": "procity_fit_beer_umbrella_01.glb", + "tris_before": 192752, + "tris": 4995, + "draws": 1, + "nonmanifold": 0.607, + "vertex_colour": true, + "footprint": [ + 2.68, + 2.65 + ], + "height": 2.3, + "max_tex": 0, + "thumb": "thumbs/procity_fit_beer_umbrella_01.png" + }, + { + "src": "binRound.glb", + "id": "bin_round", + "kind": "pub", + "file": "procity_fit_bin_round_01.glb", + "tris_before": 197576, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.4, + "vertex_colour": true, + "footprint": [ + 0.53, + 0.51 + ], + "height": 0.75, + "max_tex": 0, + "thumb": "thumbs/procity_fit_bin_round_01.png" + }, + { + "src": "bistroTable.glb", + "id": "bistro_table", + "kind": "pub", + "file": "procity_fit_bistro_table_01.glb", + "tris_before": 196995, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.382, + "vertex_colour": true, + "footprint": [ + 1.18, + 1.18 + ], + "height": 0.92, + "max_tex": 0, + "thumb": "thumbs/procity_fit_bistro_table_01.png" + }, + { + "src": "brokenBasin.glb", + "id": "broken_basin", + "kind": "pub", + "file": "procity_fit_broken_basin_01.glb", + "tris_before": 197978, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.535, + "vertex_colour": true, + "footprint": [ + 0.59, + 0.49 + ], + "height": 0.6, + "max_tex": 0, + "thumb": "thumbs/procity_fit_broken_basin_01.png" + }, + { + "src": "cableSnake.glb", + "id": "cable_snake", + "kind": "pub", + "file": "procity_fit_cable_snake_01.glb", + "tris_before": 193623, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.641, + "vertex_colour": true, + "footprint": [ + 0.45, + 0.47 + ], + "height": 0.2, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cable_snake_01.png" + }, + { + "src": "canStack.glb", + "id": "can_stack", + "kind": "pub", + "file": "procity_fit_can_stack_01.glb", + "tris_before": 196005, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.464, + "vertex_colour": true, + "footprint": [ + 0.38, + 0.32 + ], + "height": 0.45, + "max_tex": 0, + "thumb": "thumbs/procity_fit_can_stack_01.png" + }, + { + "src": "cdj.glb", + "id": "cdj_deck", + "kind": "pub", + "file": "procity_fit_cdj_deck_01.glb", + "tris_before": 197967, + "tris": 4998, + "draws": 1, + "nonmanifold": 0.335, + "vertex_colour": true, + "footprint": [ + 0.63, + 0.42 + ], + "height": 0.15, + "max_tex": 0, + "thumb": "thumbs/procity_fit_cdj_deck_01.png" + }, + { + "src": "champBucket.glb", + "id": "champ_bucket", + "kind": "pub", + "file": "procity_fit_champ_bucket_01.glb", + "tris_before": 183943, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.625, + "vertex_colour": true, + "footprint": [ + 0.24, + 0.2 + ], + "height": 0.35, + "max_tex": 0, + "thumb": "thumbs/procity_fit_champ_bucket_01.png" + }, + { + "src": "distroBox.glb", + "id": "distro_box", + "kind": "pub", + "file": "procity_fit_distro_box_01.glb", + "tris_before": 193633, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.415, + "vertex_colour": true, + "footprint": [ + 0.63, + 0.87 + ], + "height": 1.2, + "max_tex": 0, + "thumb": "thumbs/procity_fit_distro_box_01.png" + }, + { + "src": "fireExt.glb", + "id": "fire_ext", + "kind": "pub", + "file": "procity_fit_fire_ext_01.glb", + "tris_before": 198707, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.376, + "vertex_colour": true, + "footprint": [ + 0.34, + 0.19 + ], + "height": 0.6, + "max_tex": 0, + "thumb": "thumbs/procity_fit_fire_ext_01.png" + }, + { + "src": "firepit.glb", + "id": "firepit", + "kind": "pub", + "file": "procity_fit_firepit_01.glb", + "tris_before": 188041, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.883, + "vertex_colour": true, + "footprint": [ + 0.76, + 0.56 + ], + "height": 0.5, + "max_tex": 0, + "thumb": "thumbs/procity_fit_firepit_01.png" + }, + { + "src": "flightCase.glb", + "id": "flight_case", + "kind": "pub", + "file": "procity_fit_flight_case_01.glb", + "tris_before": 195452, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.376, + "vertex_colour": true, + "footprint": [ + 0.6, + 0.36 + ], + "height": 0.75, + "max_tex": 0, + "thumb": "thumbs/procity_fit_flight_case_01.png" + }, + { + "src": "jukebox.glb", + "id": "jukebox", + "kind": "pub", + "file": "procity_fit_jukebox_01.glb", + "tris_before": 184271, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.754, + "vertex_colour": true, + "footprint": [ + 0.97, + 0.41 + ], + "height": 1.52, + "max_tex": 0, + "thumb": "thumbs/procity_fit_jukebox_01.png" + }, + { + "src": "keg.glb", + "id": "keg", + "kind": "pub", + "file": "procity_fit_keg_01.glb", + "tris_before": 441861, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.237, + "vertex_colour": true, + "footprint": [ + 0.4, + 0.4 + ], + "height": 0.6, + "max_tex": 1024, + "thumb": "thumbs/procity_fit_keg_01.png" + }, + { + "src": "loungeChair.glb", + "id": "lounge_chair", + "kind": "pub", + "file": "procity_fit_lounge_chair_01.glb", + "tris_before": 1319156, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.941, + "vertex_colour": true, + "footprint": [ + 0.69, + 0.69 + ], + "height": 0.8, + "max_tex": 0, + "thumb": "thumbs/procity_fit_lounge_chair_01.png" + }, + { + "src": "marbleSink.glb", + "id": "marble_sink", + "kind": "pub", + "file": "procity_fit_marble_sink_01.glb", + "tris_before": 199095, + "tris": 4998, + "draws": 1, + "nonmanifold": 0.212, + "vertex_colour": true, + "footprint": [ + 1.15, + 0.56 + ], + "height": 0.85, + "max_tex": 0, + "thumb": "thumbs/procity_fit_marble_sink_01.png" + }, + { + "src": "oldCouch.glb", + "id": "old_couch", + "kind": "pub", + "file": "procity_fit_old_couch_01.glb", + "tris_before": 196277, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.468, + "vertex_colour": true, + "footprint": [ + 1.8, + 0.95 + ], + "height": 0.85, + "max_tex": 0, + "thumb": "thumbs/procity_fit_old_couch_01.png" + }, + { + "src": "pallet.glb", + "id": "pallet", + "kind": "pub", + "file": "procity_fit_pallet_01.glb", + "tris_before": 198086, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.307, + "vertex_colour": true, + "footprint": [ + 0.79, + 0.73 + ], + "height": 0.2, + "max_tex": 0, + "thumb": "thumbs/procity_fit_pallet_01.png" + }, + { + "src": "picnicTable.glb", + "id": "picnic_table", + "kind": "pub", + "file": "procity_fit_picnic_table_01.glb", + "tris_before": 197370, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.425, + "vertex_colour": true, + "footprint": [ + 1.38, + 1.41 + ], + "height": 0.75, + "max_tex": 0, + "thumb": "thumbs/procity_fit_picnic_table_01.png" + }, + { + "src": "pieWarmer.glb", + "id": "pie_warmer", + "kind": "pub", + "file": "procity_fit_pie_warmer_01.glb", + "tris_before": 196434, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.317, + "vertex_colour": true, + "footprint": [ + 0.61, + 0.71 + ], + "height": 0.55, + "max_tex": 0, + "thumb": "thumbs/procity_fit_pie_warmer_01.png" + }, + { + "src": "pillar.glb", + "id": "concrete_plinth", + "kind": "pub", + "file": "procity_fit_concrete_plinth_01.glb", + "tris_before": 190528, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.538, + "vertex_colour": true, + "footprint": [ + 0.76, + 0.74 + ], + "height": 1.2, + "max_tex": 0, + "thumb": "thumbs/procity_fit_concrete_plinth_01.png" + }, + { + "src": "planter.glb", + "id": "planter", + "kind": "pub", + "file": "procity_fit_planter_01.glb", + "tris_before": 239862, + "tris": 4897, + "draws": 1, + "nonmanifold": 0.877, + "vertex_colour": true, + "footprint": [ + 0.98, + 0.96 + ], + "height": 0.55, + "max_tex": 0, + "thumb": "thumbs/procity_fit_planter_01.png" + }, + { + "src": "plywoodBar.glb", + "id": "plywood_bar", + "kind": "pub", + "file": "procity_fit_plywood_bar_01.glb", + "tris_before": 192907, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.631, + "vertex_colour": true, + "footprint": [ + 1.51, + 0.82 + ], + "height": 1.05, + "max_tex": 0, + "thumb": "thumbs/procity_fit_plywood_bar_01.png" + }, + { + "src": "pokie.glb", + "id": "pokie", + "kind": "pub", + "file": "procity_fit_pokie_01.glb", + "tris_before": 198030, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.291, + "vertex_colour": true, + "footprint": [ + 0.75, + 0.78 + ], + "height": 1.7, + "max_tex": 0, + "thumb": "thumbs/procity_fit_pokie_01.png" + }, + { + "src": "raffleDrum.glb", + "id": "raffle_drum", + "kind": "pub", + "file": "procity_fit_raffle_drum_01.glb", + "tris_before": 193830, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.685, + "vertex_colour": true, + "footprint": [ + 0.37, + 0.37 + ], + "height": 0.9, + "max_tex": 0, + "thumb": "thumbs/procity_fit_raffle_drum_01.png" + }, + { + "src": "ropePost.glb", + "id": "rope_post", + "kind": "pub", + "file": "procity_fit_rope_post_01.glb", + "tris_before": 199748, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.276, + "vertex_colour": true, + "footprint": [ + 0.32, + 0.32 + ], + "height": 1.0, + "max_tex": 0, + "thumb": "thumbs/procity_fit_rope_post_01.png" + }, + { + "src": "sandwichBoard.glb", + "id": "sandwich_board", + "kind": "pub", + "file": "procity_fit_sandwich_board_01.glb", + "tris_before": 198359, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.257, + "vertex_colour": true, + "footprint": [ + 0.58, + 0.56 + ], + "height": 1.0, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sandwich_board_01.png" + }, + { + "src": "smokeMachine.glb", + "id": "smoke_machine", + "kind": "pub", + "file": "procity_fit_smoke_machine_01.glb", + "tris_before": 194423, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.528, + "vertex_colour": true, + "footprint": [ + 0.17, + 0.24 + ], + "height": 0.38, + "max_tex": 0, + "thumb": "thumbs/procity_fit_smoke_machine_01.png" + }, + { + "src": "stackF1.glb", + "id": "speaker_stack", + "kind": "pub", + "file": "procity_fit_speaker_stack_01.glb", + "tris_before": 186357, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.599, + "vertex_colour": true, + "footprint": [ + 0.52, + 0.52 + ], + "height": 1.8, + "max_tex": 0, + "thumb": "thumbs/procity_fit_speaker_stack_01.png" + }, + { + "src": "strobe.glb", + "id": "strobe_light", + "kind": "pub", + "file": "procity_fit_strobe_light_01.glb", + "tris_before": 193392, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.572, + "vertex_colour": true, + "footprint": [ + 0.22, + 0.24 + ], + "height": 0.4, + "max_tex": 0, + "thumb": "thumbs/procity_fit_strobe_light_01.png" + }, + { + "src": "subBass.glb", + "id": "sub_bass", + "kind": "pub", + "file": "procity_fit_sub_bass_01.glb", + "tris_before": 193581, + "tris": 4998, + "draws": 1, + "nonmanifold": 0.531, + "vertex_colour": true, + "footprint": [ + 0.87, + 0.63 + ], + "height": 0.7, + "max_tex": 0, + "thumb": "thumbs/procity_fit_sub_bass_01.png" + }, + { + "src": "topiary.glb", + "id": "topiary", + "kind": "pub", + "file": "procity_fit_topiary_01.glb", + "tris_before": 415872, + "tris": 4999, + "draws": 1, + "nonmanifold": 0.937, + "vertex_colour": true, + "footprint": [ + 0.61, + 0.61 + ], + "height": 1.2, + "max_tex": 0, + "thumb": "thumbs/procity_fit_topiary_01.png" + }, + { + "src": "trough.glb", + "id": "urinal_trough", + "kind": "pub", + "file": "procity_fit_urinal_trough_01.glb", + "tris_before": 488808, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.065, + "vertex_colour": true, + "footprint": [ + 0.31, + 0.26 + ], + "height": 0.7, + "max_tex": 1024, + "thumb": "thumbs/procity_fit_urinal_trough_01.png" + }, + { + "src": "wheelieBin.glb", + "id": "wheelie_bin", + "kind": "pub", + "file": "procity_fit_wheelie_bin_01.glb", + "tris_before": 195084, + "tris": 5000, + "draws": 1, + "nonmanifold": 0.429, + "vertex_colour": true, + "footprint": [ + 0.72, + 0.92 + ], + "height": 1.1, + "max_tex": 0, + "thumb": "thumbs/procity_fit_wheelie_bin_01.png" + } + ], + "rejects": [ + { + "id": "balustrade", + "why": "glass panel + handrail. The remesh lost the handrail entirely and left a lumpy slab with a detached lump \u2014 a transparent thin panel is the worst case for a signed-distance remesh. Eyeballed against balustrade_shot.png; unusable.", + "tris_src": 198229 + }, + { + "id": "cocktail_bar", + "why": "back-bar with a lit bottle display. At 5k the bottle shelving smears into a grey L-shaped mass; nothing about it reads as a bar. Rejected rather than shipping mush.", + "tris_src": 194298 + }, + { + "id": "dj_plinth", + "why": "the white plinth survives but the controller on top melts into a grey blob. Nothing is lost: cdj_deck normalized cleanly and Lane C can stand it on any counter or the plywood_bar.", + "tris_src": 197159 + } + ], + "skipped_staff": [ + "staffBartender", + "staffChef", + "staffCleaner", + "staffDj", + "staffGlassie", + "staffSecurity" + ] +} \ No newline at end of file diff --git a/pipeline/_r41_publish_list.txt b/pipeline/_r41_publish_list.txt new file mode 100644 index 0000000..d7783c5 --- /dev/null +++ b/pipeline/_r41_publish_list.txt @@ -0,0 +1,110 @@ +procity_fit_aircon_unit_01.glb +procity_fit_ash_urn_01.glb +procity_fit_ashtray_post_01.glb +procity_fit_bain_marie_01.glb +procity_fit_banquette_01.glb +procity_fit_beam_roof_01.glb +procity_fit_beer_umbrella_01.glb +procity_fit_bin_big_4x4_01.glb +procity_fit_bin_endoffunk_01.glb +procity_fit_bin_round_01.glb +procity_fit_bistro_table_01.glb +procity_fit_broken_basin_01.glb +procity_fit_cable_snake_01.glb +procity_fit_can_stack_01.glb +procity_fit_cart_case_01.glb +procity_fit_cart_concorde_01.glb +procity_fit_cart_m447_01.glb +procity_fit_cart_stanton_01.glb +procity_fit_cdj_deck_01.glb +procity_fit_champ_bucket_01.glb +procity_fit_chiu_bin_01.glb +procity_fit_chiu_bin_half_01.glb +procity_fit_chiu_bin_low_01.glb +procity_fit_chiu_rack_long_01.glb +procity_fit_chiu_rack_short_01.glb +procity_fit_column_square_01.glb +procity_fit_column_wide_01.glb +procity_fit_concrete_plinth_01.glb +procity_fit_counter_station_01.glb +procity_fit_counter_station_side_01.glb +procity_fit_crate_blue_01.glb +procity_fit_crowd_barrier_01.glb +procity_fit_deck_1200_01.glb +procity_fit_deck_1200_rigged_01.glb +procity_fit_distro_box_01.glb +procity_fit_door_shop_01.glb +procity_fit_drum_808_01.glb +procity_fit_drum_tr1000_01.glb +procity_fit_fire_ext_01.glb +procity_fit_firepit_01.glb +procity_fit_flight_case_01.glb +procity_fit_headphones_hd25_01.glb +procity_fit_headshell_technics_01.glb +procity_fit_jukebox_01.glb +procity_fit_keg_01.glb +procity_fit_library_trolley_01.glb +procity_fit_lounge_chair_01.glb +procity_fit_marble_sink_01.glb +procity_fit_mixer_mx2200_01.glb +procity_fit_mixer_pmc05pro_01.glb +procity_fit_mixer_pmc05pro_rigged_01.glb +procity_fit_mixer_pmc06pro_01.glb +procity_fit_mixer_pmc07pro_01.glb +procity_fit_mixer_pmc08pro_01.glb +procity_fit_mixer_pmc10_01.glb +procity_fit_mixer_pmc20_01.glb +procity_fit_mixer_pmx9000_01.glb +procity_fit_mixer_shdj1200_01.glb +procity_fit_mixer_shdj1200_rigged_01.glb +procity_fit_mixer_ttm54i_01.glb +procity_fit_mixer_ttm54i_rigged_01.glb +procity_fit_mos_bin_bottom_01.glb +procity_fit_mos_bin_lower_mid_01.glb +procity_fit_mos_bin_top_01.glb +procity_fit_mos_bin_upper_mid_01.glb +procity_fit_mos_stack_01.glb +procity_fit_old_couch_01.glb +procity_fit_pallet_01.glb +procity_fit_picnic_table_01.glb +procity_fit_pie_warmer_01.glb +procity_fit_planter_01.glb +procity_fit_plywood_bar_01.glb +procity_fit_pokie_01.glb +procity_fit_rack_bin_wooden_01.glb +procity_fit_raffle_drum_01.glb +procity_fit_rope_post_01.glb +procity_fit_sampler_mpc3000_01.glb +procity_fit_sampler_s900_01.glb +procity_fit_sampler_sp1200_01.glb +procity_fit_sandwich_board_01.glb +procity_fit_shelf_board_long_01.glb +procity_fit_shelf_board_short_01.glb +procity_fit_sl_deck_1_01.glb +procity_fit_sl_deck_2_01.glb +procity_fit_sl_deck_3_01.glb +procity_fit_sl_deck_4_01.glb +procity_fit_smoke_machine_01.glb +procity_fit_speaker_stack_01.glb +procity_fit_strobe_light_01.glb +procity_fit_sub_bass_01.glb +procity_fit_topiary_01.glb +procity_fit_tub_black_01.glb +procity_fit_tub_black_long_01.glb +procity_fit_tub_white_01.glb +procity_fit_tub_white_long_01.glb +procity_fit_tubs_3x3_01.glb +procity_fit_tubs_3x3_long_01.glb +procity_fit_tubs_4x2_vertical_01.glb +procity_fit_tubs_4x2_wide_01.glb +procity_fit_tubs_shelf_long_01.glb +procity_fit_tubs_shelf_wide_01.glb +procity_fit_urinal_trough_01.glb +procity_fit_vinyl_sleeve_01.glb +procity_fit_wall_shelves_big_01.glb +procity_fit_wall_shelves_long_01.glb +procity_fit_wheelie_bin_01.glb +procity_fit_window_booth_01.glb +procity_fit_wooden_rack_long_01.glb +procity_fit_wooden_rack_wide_01.glb +procity_fit_zip_drive_01.glb diff --git a/pipeline/_r41_sha1.json b/pipeline/_r41_sha1.json new file mode 100644 index 0000000..b337f0a --- /dev/null +++ b/pipeline/_r41_sha1.json @@ -0,0 +1,112 @@ +{ + "procity_fit_aircon_unit_01.glb": "ce874f7467673f91b81a14ecc60da23411517760", + "procity_fit_ash_urn_01.glb": "90205f27ff38968679384f4d04e18eb4315ede1d", + "procity_fit_ashtray_post_01.glb": "1bddccaf298394c68f5dae535c11532bab834ca9", + "procity_fit_bain_marie_01.glb": "d3482c49cc30dd47e7715e6d9c5340ab579d8f77", + "procity_fit_banquette_01.glb": "878ca932e557bc1a2201b315b8dc52d52a6a72b0", + "procity_fit_beam_roof_01.glb": "b856a9657443d765a0089cb3754464b09589a62f", + "procity_fit_beer_umbrella_01.glb": "c95b6106d5247d931d5c6895f30b135529135d3f", + "procity_fit_bin_big_4x4_01.glb": "7f1bfd7708bf6dfadcc67e31e056283d16c3916a", + "procity_fit_bin_endoffunk_01.glb": "ffac5eef693afb67ae51d29eed87f8279b68375c", + "procity_fit_bin_round_01.glb": "77bec352d4a8a0a90dfc070befcba2cb958b84ed", + "procity_fit_bistro_table_01.glb": "b68368b646c58c72520779e1f1b42206dba40282", + "procity_fit_broken_basin_01.glb": "bea1a0af949b0b847fe8849ec095f16ab241e8d1", + "procity_fit_cable_snake_01.glb": "aa6d2ba9374b141046038e1f35bed3d78fc3fd2d", + "procity_fit_can_stack_01.glb": "613b4edfde03bc23d4b0ded4be229883fd2536f3", + "procity_fit_cart_case_01.glb": "3aabbc174bc20bd81e9bd303b149e7ef4ba9e88a", + "procity_fit_cart_concorde_01.glb": "7d7420387b68045d5b2e1daf708a30af9300c54a", + "procity_fit_cart_m447_01.glb": "196f2fb283bce31d4170e006524e21c9a8c69c82", + "procity_fit_cart_stanton_01.glb": "19dfb2eaeee522215693bd11282eae6763b09f8c", + "procity_fit_cdj_deck_01.glb": "2f99eac26a4020a9906120c44a63d508b31a22fd", + "procity_fit_champ_bucket_01.glb": "e0526054c3bfed01b317ee4c6ce2b54e25cc54c6", + "procity_fit_chiu_bin_01.glb": "5d46826d073a864167fdfc8137527afc53962244", + "procity_fit_chiu_bin_half_01.glb": "ef0f25a0e335962ca82a691b2d54bed78061cfb9", + "procity_fit_chiu_bin_low_01.glb": "0d764172e91d81125a3c099e4deaf4cd44353d7b", + "procity_fit_chiu_rack_long_01.glb": "fc545f1f54475f51ef4bb15ca26857aab7442fe6", + "procity_fit_chiu_rack_short_01.glb": "7a891a7d3fa861dfd3e43784b2b51ea02d57adef", + "procity_fit_column_square_01.glb": "4e6d3e0e268aede54b09843696f8be714f3c0a75", + "procity_fit_column_wide_01.glb": "c62f08c030b76da90e806691944818e8e2d011ae", + "procity_fit_concrete_plinth_01.glb": "6048ad7408377e7ece105c651f8117739cba2c16", + "procity_fit_counter_station_01.glb": "f1bd34cb5212759778340e43db18c1d02c653b55", + "procity_fit_counter_station_side_01.glb": "78a472e3dd3467091449b9b9d02108f06ca36738", + "procity_fit_crate_blue_01.glb": "d0d0315df8b95fe0a455010415075b707c0cd959", + "procity_fit_crowd_barrier_01.glb": "363a0f80ce5239e616c5060634ff1bf8f03197d7", + "procity_fit_deck_1200_01.glb": "5070c5a85838164a8464a6c46a2ab08eb3fee70e", + "procity_fit_deck_1200_rigged_01.glb": "b2175d53dd8413d3aeca748ba6f036dbfb13e638", + "procity_fit_distro_box_01.glb": "69b0d169711702d21329dcb9641b7e395f6b7abf", + "procity_fit_door_shop_01.glb": "9abed4dda41f27252e35a150afcfb35d162e4418", + "procity_fit_drum_808_01.glb": "ee817b3e152b70afea1019917bcee09c9178eb76", + "procity_fit_drum_tr1000_01.glb": "4b6868395265325e0d9d3574e101313ff9823140", + "procity_fit_fire_ext_01.glb": "4471601a7ec882a93f6742afc1389a1e66652d8f", + "procity_fit_firepit_01.glb": "96bfd21bb94b4478f4c3cc18d0d8a99e328b09b2", + "procity_fit_flight_case_01.glb": "744378cd2cde5a3fad6b074b82f1a3fb0a2aa020", + "procity_fit_headphones_hd25_01.glb": "99c4c8e63fe59aac36bf6f9dd8907c7781b8a0d1", + "procity_fit_headshell_technics_01.glb": "1db6e8b04b44e9cb47c61b0bc49867d7f104ca5a", + "procity_fit_jukebox_01.glb": "c6efb9277dac136ce75e36c9c2f9b9085228c545", + "procity_fit_keg_01.glb": "7a05c10cfe335b3f14047d115f7e64b0518ab580", + "procity_fit_library_trolley_01.glb": "892f604c1660daa84bf10c699478c83ca3072473", + "procity_fit_lounge_chair_01.glb": "358e0f0624f0d211ece6afa2af1cad395770aa8b", + "procity_fit_marble_sink_01.glb": "c38245fc60e991499b59772cbb8ec519a8fa5c49", + "procity_fit_mixer_mx2200_01.glb": "32b231ec9fb305bfb1c5b82cc57cfdace32933d6", + "procity_fit_mixer_pmc05pro_01.glb": "f115b9caab36fd7401d76c5971c33ce27466364c", + "procity_fit_mixer_pmc05pro_rigged_01.glb": "2d3fe7173e7ecbfc99b7c4b9d7bd1840c3a2b006", + "procity_fit_mixer_pmc06pro_01.glb": "bb482cf1f4d09b62767f50bf65d9b39d595c6ebe", + "procity_fit_mixer_pmc07pro_01.glb": "a48aa36207fe3c1c124f19ba9f616193f3a79609", + "procity_fit_mixer_pmc08pro_01.glb": "b17d5c026d6c8450115177eaa9009400c1996f29", + "procity_fit_mixer_pmc10_01.glb": "f58e6b5d36cba3001a922ffee0f2b3f44a19e36f", + "procity_fit_mixer_pmc20_01.glb": "610876db83ef03ffe0441284cd7788ae17468836", + "procity_fit_mixer_pmx9000_01.glb": "0fca9258da288390861a10ac06ba613f8865cb44", + "procity_fit_mixer_shdj1200_01.glb": "0ecb07aab2616081e625dba2bc537348a544b837", + "procity_fit_mixer_shdj1200_rigged_01.glb": "daa49b9d6be0dc0b100bfdc5eddce47e49564275", + "procity_fit_mixer_ttm54i_01.glb": "c7f81962ce3702223ddbe3a117df8a1205b0bde5", + "procity_fit_mixer_ttm54i_rigged_01.glb": "181e6ec5f7edefb6b9f71616ce0bdf7eb064fd05", + "procity_fit_mos_bin_bottom_01.glb": "b3d032bc63296359424779091a6ddb64d25dd76c", + "procity_fit_mos_bin_lower_mid_01.glb": "e2714d078e56e8a6f19a956954ad534c20f67ee9", + "procity_fit_mos_bin_top_01.glb": "d92b4cf931d597a453dec68f800e50463ea21ea4", + "procity_fit_mos_bin_upper_mid_01.glb": "68a6533f072145bf28bec5ced388a09bec6e716d", + "procity_fit_mos_stack_01.glb": "64e9baba429910c504df14b94dd3acfeacccbf34", + "procity_fit_old_couch_01.glb": "549663b12ed46f3cec96d942a360d453667bdde9", + "procity_fit_pallet_01.glb": "fa4bd1c7e8ff3a7afee43afbf92983eed8040987", + "procity_fit_picnic_table_01.glb": "cea78b9c302a1df16d96cbe080a69e3f9b378984", + "procity_fit_pie_warmer_01.glb": "1aad547a6115f39c509389046be7a31855b0c710", + "procity_fit_planter_01.glb": "a00293155b8bf533aa0f70030f52f00e5d4bb6a5", + "procity_fit_plywood_bar_01.glb": "bccb7d15a3f61603e6b37a4a8633adacaeb5a611", + "procity_fit_pokie_01.glb": "90c2841ad1935a762929850edbdc4a4a5a9e7da6", + "procity_fit_rack_bin_wooden_01.glb": "c410601f91a32272172ec8e79c541f6373dbcaeb", + "procity_fit_raffle_drum_01.glb": "42c0b1d6a8c6c5c48a2ccc3d246e7504139eadf0", + "procity_fit_rope_post_01.glb": "ba4a0f6abc4058ca4632d4c23b99aa65cd4d99f5", + "procity_fit_sampler_mpc3000_01.glb": "8b3def767602846c9056a76d780db3732a48d845", + "procity_fit_sampler_s900_01.glb": "9714875be9af91f05c3ecf82631bbaf2fadf15f3", + "procity_fit_sampler_sp1200_01.glb": "370c9c3c177b38e194bfe5842e3d61febc36b91e", + "procity_fit_sandwich_board_01.glb": "cdf8467eb7f24ae2352ddfa6b68b17c2d34763b6", + "procity_fit_shelf_board_long_01.glb": "e46b3556a0be0648908e3297eb3ffdf3e1a66fc0", + "procity_fit_shelf_board_short_01.glb": "13826ee78d491d1072aa7a73d4e2f275ac2fe390", + "procity_fit_sl_deck_1_01.glb": "4d1e4e23f75711168d479784579d79c2349700bb", + "procity_fit_sl_deck_2_01.glb": "cd782ad484627ef2ed560c1564e9c409bb8a5d06", + "procity_fit_sl_deck_3_01.glb": "53391d94e52e6a4ddf84096dbbf8f54b6cc92bc3", + "procity_fit_sl_deck_4_01.glb": "52b7a1d28a76b5dda3d6eecb1f70eb92c59a6bc3", + "procity_fit_smoke_machine_01.glb": "ee952a30c9410ae10fc83cc4388b9f09770b6d2e", + "procity_fit_speaker_stack_01.glb": "e08303b63dc8a672d3dd32f6ef4d988a69a3e779", + "procity_fit_strobe_light_01.glb": "df345e87313b499bda5dec441aa553fb4ff39732", + "procity_fit_sub_bass_01.glb": "aa7508bf758e3e240ed51f5091082ce52cecbc96", + "procity_fit_topiary_01.glb": "99ae95024c6a01d01bab56e6151fbd0ef3a88e8b", + "procity_fit_tub_black_01.glb": "1e55e597e063aa454e8c359f0ab6bec5a08d7f09", + "procity_fit_tub_black_long_01.glb": "3f6c2935d267b5046f0517519e584f5e8eca3e41", + "procity_fit_tub_white_01.glb": "32fc23c3bcc35da595f3ccba997c50c1bed1b2ac", + "procity_fit_tub_white_long_01.glb": "380c9fed59141d95a7889ccdbabc7b4805a7fc88", + "procity_fit_tubs_3x3_01.glb": "72bec67884cf82dc8340382e8a538068691953ba", + "procity_fit_tubs_3x3_long_01.glb": "22efd2ba780593adc4bdeaf58ac6e1eae308f24e", + "procity_fit_tubs_4x2_vertical_01.glb": "be8d41e153dc57ec187bcd6b244f47ed7c78f99a", + "procity_fit_tubs_4x2_wide_01.glb": "770d6a6bc73953b3710a98821794bd3c74145e93", + "procity_fit_tubs_shelf_long_01.glb": "d15effbd8d3098c813cd024986002077c370aa0c", + "procity_fit_tubs_shelf_wide_01.glb": "e2cf1d1e633ccf4ee172a8c19a92531eacbe959c", + "procity_fit_urinal_trough_01.glb": "a857cbdaf157993dd822aa19042a0fb83c0d81b2", + "procity_fit_vinyl_sleeve_01.glb": "348c94af5f8363050ba8d516aa009cdcddb237d3", + "procity_fit_wall_shelves_big_01.glb": "6dda25529f7095b93e51f760b19a217d0b319432", + "procity_fit_wall_shelves_long_01.glb": "c77e3ce595746a07025dd2a8003552de527e8705", + "procity_fit_wheelie_bin_01.glb": "5826de87d818ecbce34190fa60829f6ef6ce937b", + "procity_fit_window_booth_01.glb": "9fe80b098955b5de24abfb6555fa86da80bf0a24", + "procity_fit_wooden_rack_long_01.glb": "4bef5d1069037c2267d04441d83b2680fd16d3a9", + "procity_fit_wooden_rack_wide_01.glb": "6757e16e33956ea430d3e29c1206ed940d1861fd", + "procity_fit_zip_drive_01.glb": "6b7fece8a4f646e693ec21ae6a1b94c792767da4" +} \ No newline at end of file diff --git a/pipeline/build_manifest.py b/pipeline/build_manifest.py index 1afd274..d8b53f6 100644 --- a/pipeline/build_manifest.py +++ b/pipeline/build_manifest.py @@ -28,6 +28,11 @@ R38_RESULTS = os.path.join(ROOT, "pipeline", "_r38_results.json") # roun # sha1-verified off the public depot, so they are catalogued the same round they were generated # (contrast R38, which held its results file at `.PENDING_PUBLISH` until the push landed). R39_RESULTS = os.path.join(ROOT, "pipeline", "_r39_results.json") # round-39 arcade kit +# [R41 §41.2] the three imported libraries: ~/Documents/3dstore (shop fittings), +# ~/Documents/not-tonight/art_incoming (pub/venue props), ultra's 3D=models/dj-gear (DJ gear). +R41_3DSTORE = os.path.join(ROOT, "pipeline", "_r41_3dstore_results.json") +R41_PUB = os.path.join(ROOT, "pipeline", "_r41_pub_results.json") +R41_DJGEAR = os.path.join(ROOT, "pipeline", "_r41_djgear_results.json") OUT = os.path.join(ROOT, "web", "assets", "manifest.json") AUDIO = os.path.join(ROOT, "web", "assets", "audio") # round-11 audio pack DEPOT = "https://digalot.fyi/3god" @@ -312,6 +317,35 @@ def build(): "thumb": "thumbs/" + r["file"].replace(".glb", ".png"), } (furniture if r["file"].startswith("procity_street_") else fittings)[fid] = entry + # ---- R41 §41.2: the three imported libraries ------------------------------------------------ + # These carry their own measured footprint/height (taken from the SHIPPED GLB, not from the + # Blender scene that wrote it — rig_join's in-scene bounds mis-reported a 0.114 m mixer as + # 0.629 m, and Lane C places on these numbers). They also carry the two facts C needs to spend + # the 162-draw interior margin deliberately: `draws` and `tris` per asset. + for results_file in (R41_3DSTORE, R41_PUB, R41_DJGEAR): + if not os.path.exists(results_file): + continue + for r in json.load(open(results_file)).get("results", []): + fid = r["file"].replace("procity_fit_", "").replace("_01.glb", "") + entry = { + "file": r["file"], + "footprint": r["footprint"], + "height": r["height"], + "thumb": r["thumb"], + "kind": r.get("kind"), + "tris": r.get("tris"), + "draws": r.get("draws", r.get("draws_after", 1)), + "source": r.get("src"), + } + if r.get("kind") == "djgear": + # what C/D need to drive it later: which contract nodes actually ship, and the + # full control-name list the source carried (dj-gear/manifest.json). + entry["mode"] = r.get("mode") + entry["control_nodes"] = r.get("contract_shipped", []) + entry["source_controls"] = r.get("named_controls", []) + entry["drivable"] = bool(r.get("contract_shipped")) + 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)) diff --git a/pipeline/check_contract.py b/pipeline/check_contract.py new file mode 100644 index 0000000..b5142e4 --- /dev/null +++ b/pipeline/check_contract.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Assert the DJ-gear named-node contract survives normalization — read from the GLB, not Blender. + + python3 pipeline/check_contract.py SRC_DIR OUT_DIR MAP.json + python3 pipeline/check_contract.py --list FILE.glb [FILE.glb ...] + +The contract 90sDJsim's GEAR driver reads (dj-gear/README.md) is five bare node names: + + Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP + +Integrator ruling 5 exists because `gltf-transform optimize` silently deleted `Fader_CROSS` from a +696-node mixer. "Silently" is the operative word, so this check parses the **shipped GLB's** node +list rather than asking the tool that produced it — a scene-graph scan inside Blender still sees +control EMPTIES that were never exported, and would have reported a contract that the file does +not actually carry. + +Verdict per model: PASS = every contract node present in the source is present in the output. + FAIL = one or more were lost (the ruling-5 failure mode). + N/A = the source carried none (a cartridge has no crossfader). +Exit 1 if any FAIL. +""" +import json, os, struct, sys + +CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"] + + +def glb_nodes(path): + """Node names from a GLB's JSON chunk. Dependency-free — no Blender, no pygltflib.""" + d = open(path, "rb").read() + if d[:4] != b"glTF": + raise ValueError("not a GLB") + off = 12 + while off + 8 <= len(d): + clen, ctype = struct.unpack(" skeleton-only per-clip GLB. + +Runs INSIDE Blender: + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python pipeline/clips_export.py -- \ + --list pipeline/clips_r41.json --fbx --out + +Stage 2 is `pipeline/clips_pack.py` (pure python, no Blender): it merges the per-clip GLBs into +the <=6 grouped GLBs under web/models/clips/ and writes web/assets/motion_manifest.json. + +WHY TWO STAGES. MIRPAMO's roadmap item "multi-clip single-GLB export" is not built yet, and the +documented Blender-5 slotted-action hazard (MIRPAMO/CLAUDE.md) bites exactly when you try to move +actions between armatures inside one .blend. Exporting one clip per GLB is the proven path — it is +how web/models/peds/sit.glb and look.glb were made (generator "Khronos glTF Blender I/O v5.1.20", +66 nodes, 0 meshes) — and the merge is then a deterministic glTF-level operation with no Blender +version risk at all. + +NO RETARGET IS NEEDED and none is performed: the Mixamo bank and PROCITY's ped fleet are the SAME +mixamorig skeleton (rigs.js `_canon` folds mixamorig1/4/... -> mixamorig), which is the whole point +of the house "one canonical clip bank" law. MIRPAMO's retargeter is for foreign skeletons (CMU/BVH) +and for rigging raw meshes; pointing it at a mixamorig->mixamorig pair would only add error, and it +would emit the ped MESH into the clip file, breaking the zero-draw property that makes this round +affordable. `make smoke` is still run as the tool ground-truth check (see E-progress.md). +""" +import json +import os +import sys + +import bpy + + +def argv_after_ddash(): + return sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + + +def parse_args(a): + out = {} + i = 0 + while i < len(a): + out[a[i].lstrip("-")] = a[i + 1] + i += 2 + return out + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def import_clip(path): + bpy.ops.import_scene.fbx(filepath=path, automatic_bone_orientation=False, + ignore_leaf_bones=False) + arms = [o for o in bpy.data.objects if o.type == "ARMATURE"] + if len(arms) != 1: + raise RuntimeError("expected exactly 1 armature, got %d in %s" % (len(arms), path)) + return arms[0] + + +def export_glb(arm, out_path, anim_name): + # name the action == the clip id so the exported animation carries it + if arm.animation_data and arm.animation_data.action: + arm.animation_data.action.name = anim_name + for o in bpy.data.objects: + o.select_set(o is arm) + bpy.context.view_layer.objects.active = arm + kw = dict(filepath=out_path, export_format="GLB", + export_animations=True, export_animation_mode="ACTIVE_ACTIONS", + export_skins=True, export_yup=True, + export_materials="NONE", export_texcoords=False, export_normals=False, + export_apply=False, use_selection=True) + try: + bpy.ops.export_scene.gltf(**kw) + except TypeError: # older/newer exporter arg drift — retry minimal + bpy.ops.export_scene.gltf(filepath=out_path, export_format="GLB", + export_animations=True, use_selection=True) + + +def main(): + a = parse_args(argv_after_ddash()) + sel = json.load(open(a["list"])) + fbx_dir, out_dir = a["fbx"], a["out"] + os.makedirs(out_dir, exist_ok=True) + results, fails = [], [] + for gname, g in sel["groups"].items(): + for c in g["clips"]: + src = os.path.join(fbx_dir, c["file"]) + dst = os.path.join(out_dir, "%s.glb" % c["id"]) + try: + reset() + arm = import_clip(src) + nbones = len(arm.data.bones) + act = arm.animation_data.action if arm.animation_data else None + fs, fe = (act.frame_range if act else (0, 0)) + export_glb(arm, dst, c["id"]) + results.append(dict(id=c["id"], group=gname, file=c["file"], bones=nbones, + frame_start=float(fs), frame_end=float(fe), + fps=bpy.context.scene.render.fps, + bytes=os.path.getsize(dst))) + print("[ok ] %-22s %-64s bones=%d frames=%.0f..%.0f -> %d B" + % (c["id"], c["file"], nbones, fs, fe, os.path.getsize(dst))) + except Exception as e: # noqa: BLE001 + fails.append(dict(id=c["id"], file=c["file"], err=repr(e))) + print("[FAIL] %-22s %s: %r" % (c["id"], c["file"], e)) + with open(os.path.join(out_dir, "_export_results.json"), "w") as f: + json.dump(dict(ok=results, fail=fails), f, indent=1) + print("\n%d exported / %d failed" % (len(results), len(fails))) + + +main() diff --git a/pipeline/clips_pack.py b/pipeline/clips_pack.py new file mode 100644 index 0000000..f5573e8 --- /dev/null +++ b/pipeline/clips_pack.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +"""PROCITY R41 §41.1 — stage 2 of 2: per-clip skeleton-only GLBs -> <=6 GROUPED clip GLBs. + + python3 pipeline/clips_pack.py --stage \ + --list pipeline/clips_r41.json --out web/models/clips \ + --manifest web/assets/motion_manifest.json + +Pure python3 stdlib — no Blender, no deps. Operates at the glTF level, which is safe here because +every per-clip GLB in the staging dir carries the IDENTICAL 66-node mixamorig skeleton (asserted, +not assumed: `--check` fails loudly on any node-name mismatch). Merging is therefore: keep group +clip #1's node graph + skin verbatim, then append every other clip's animation with its channel +targets remapped by node NAME. + +TRACK FILTER (`--tracks`, default `rot`). +PROCITY's consumer is `web/js/citizens/rigs.js` `_rotOnly`, which keeps ONLY `.quaternion` tracks and +additionally drops `Hips.quaternion` — every translation and scale track a clip GLB ships is decoded +by three.js and then thrown away on the same frame. Shipping them is pure boot cost. So the default +packs rotation channels only. `--tracks all` reproduces the unfiltered T/R/S file if a future round +needs root motion back (the pipeline is re-runnable from `clips_r41.json` in ~1 minute). + +LOOP SEAM is measured, not asserted: for every clip we compute the maximum per-bone angle between +the first and last rotation keyframe (`loopSeamDeg`). Small = the clip cycles cleanly and can be +played on repeat; large = it is a one-shot and should be played once and blended out. The manifest +carries both the curated `loopable` flag and the measured number so Lane D can second-guess it. +""" +import argparse +import json +import math +import os +import re +import struct + +COMP = {5120: ("b", 1), 5121: ("B", 1), 5122: ("h", 2), 5123: ("H", 2), + 5125: ("I", 4), 5126: ("f", 4)} +NCOMP = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16} + + +# ---------------------------------------------------------------- GLB read/write +def glb_read(path): + with open(path, "rb") as f: + data = f.read() + if data[:4] != b"glTF": + raise ValueError("%s: not a GLB" % path) + js, bin_ = None, b"" + off = 12 + while off < len(data): + ln, ty = struct.unpack_from(" int16 normalized VEC4 (glTF-legal animation output). + + Halves the dominant payload. glTF 2.0 dequantises SHORT as max(c/32767, -1.0) and three.js + r175 does exactly that in GLTFLoader `_getArrayFromAccessor` (verified in + web/vendor/addons/loaders/GLTFLoader.js). Worst-case angular error is ~0.006 deg — three + orders of magnitude under anything a 1.7 m citizen can show on screen. + """ + if ct != 5126: + return raw, ct, False + v = struct.unpack("<%df" % (count * 4), raw) + q = [max(-32767, min(32767, int(round(max(-1.0, min(1.0, x)) * 32767.0)))) for x in v] + return struct.pack("<%dh" % len(q), *q), 5122, True + + +class Sink: + """Accumulates tightly packed accessor payloads into one BIN chunk, deduped by content.""" + + def __init__(self): + self.bin = bytearray() + self.views = [] + self.accs = [] + self._seen = {} + self.dedup_saved = 0 + + def add(self, raw, count, ty, ct, normalized, mn, mx): + key = (bytes(raw), count, ty, ct, normalized) + if key in self._seen: # identical payload already in this file + self.dedup_saved += len(raw) + return self._seen[key] + while len(self.bin) % 4: + self.bin += b"\0" + off = len(self.bin) + self.bin += raw + self.views.append({"buffer": 0, "byteOffset": off, "byteLength": len(raw)}) + a = {"bufferView": len(self.views) - 1, "componentType": ct, + "count": count, "type": ty} + if normalized: + a["normalized"] = True + if mn is not None: + a["min"], a["max"] = mn, mx + self.accs.append(a) + self._seen[key] = len(self.accs) - 1 + return len(self.accs) - 1 + + +# The bank is not uniform about the mixamorig namespace: 45 of the 46 R41 clips export as +# `mixamorig1:Bone`, one (Idle_Smoking) as `mixamorig:Bone` — the exact split the house `_canon` +# trick in rigs.js exists to absorb (and the same split already shipped: idle.glb is mixamorig4:, +# sit.glb is mixamorig:). Joint NAMES and node ORDER are identical across all 46 (asserted below). +# Normalise to the bare `mixamorig:` namespace so the merge is name-exact and every group file +# reads the same; three.js sanitises the colon out anyway, so runtime binding is unchanged. +_NS = re.compile(r"mixamorig\d*:") + + +def canon_name(n): + return _NS.sub("mixamorig:", n) if n else n + + +def quat_angle_deg(q0, q1): + d = abs(sum(a * b for a, b in zip(q0, q1))) + return math.degrees(2.0 * math.acos(max(-1.0, min(1.0, d)))) + + +# ---------------------------------------------------------------- pack one group +FINGER = re.compile(r"Hand(Thumb|Index|Middle|Ring|Pinky)\d") + + +def pack_group(gname, clips, stage, out_dir, tracks, quant=True, drop_fingers=False): + keep = {"rotation"} if tracks == "rot" else {"rotation", "translation", "scale"} + base_js, base_bin = glb_read(os.path.join(stage, "%s.glb" % clips[0]["id"])) + for n in base_js["nodes"]: + if n.get("name"): + n["name"] = canon_name(n["name"]) + names = [n.get("name") for n in base_js["nodes"]] + if len(set(names)) != len(names): + raise SystemExit("%s: duplicate node names in base skeleton" % gname) + idx_of = {n: i for i, n in enumerate(names)} + + sink = Sink() + out = { + "asset": {"version": "2.0", + "generator": "PROCITY pipeline/clips_pack.py (R41 41.1) from %s" + % base_js["asset"].get("generator", "?")}, + "scene": base_js.get("scene", 0), + "scenes": base_js["scenes"], + "nodes": base_js["nodes"], + "animations": [], + "buffers": [{"byteLength": 0}], + } + if "skins" in base_js: + skins = json.loads(json.dumps(base_js["skins"])) + for sk in skins: + if "inverseBindMatrices" in sk: + sk["inverseBindMatrices"] = sink.add(*acc_bytes(base_js, base_bin, + sk["inverseBindMatrices"])) + out["skins"] = skins + + report = [] + for c in clips: + p = os.path.join(stage, "%s.glb" % c["id"]) + js, bin_ = glb_read(p) + nm = [canon_name(n.get("name")) for n in js["nodes"]] + if nm != names: + raise SystemExit("%s/%s: skeleton mismatch vs group base (%d vs %d nodes)" + % (gname, c["id"], len(nm), len(names))) + if len(js.get("animations", [])) != 1: + raise SystemExit("%s/%s: expected 1 animation, got %d" + % (gname, c["id"], len(js.get("animations", [])))) + anim = js["animations"][0] + smap, chans, samps = {}, [], [] + dur, seam, nrot = 0.0, 0.0, 0 + for ch in anim["channels"]: + path = ch["target"]["path"] + if path not in keep or "node" not in ch["target"]: + continue + tname = nm[ch["target"]["node"]] + if drop_fingers and FINGER.search(tname): + continue + si = ch["sampler"] + if si not in smap: + s = anim["samplers"][si] + t = acc_floats(js, bin_, s["input"]) + dur = max(dur, t[-1][0] if t else 0.0) + oraw, on, oty, oct, onorm, omn, omx = acc_bytes(js, bin_, s["output"]) + if path == "rotation": + q = acc_floats(js, bin_, s["output"]) + if len(q) > 1: + seam = max(seam, quat_angle_deg(q[0], q[-1])) + nrot += 1 + if quant and s.get("interpolation", "LINEAR") != "CUBICSPLINE": + oraw, oct, onorm = quantize_rot(oraw, on, oct) + omn = omx = None + smap[si] = len(samps) + samps.append({"input": sink.add(*acc_bytes(js, bin_, s["input"])), + "output": sink.add(oraw, on, oty, oct, onorm, omn, omx), + "interpolation": s.get("interpolation", "LINEAR")}) + chans.append({"sampler": smap[si], + "target": {"node": idx_of[tname], "path": path}}) + out["animations"].append({"name": c["id"], "channels": chans, "samplers": samps}) + report.append(dict(id=c["id"], duration=round(dur, 4), channels=len(chans), + samplers=len(samps), rot_bones=nrot, loopSeamDeg=round(seam, 2))) + + out["bufferViews"] = sink.views + out["accessors"] = sink.accs + out["buffers"][0]["byteLength"] = len(sink.bin) + ((4 - len(sink.bin) % 4) % 4) + os.makedirs(out_dir, exist_ok=True) + path = os.path.join(out_dir, "%s.glb" % gname) + size = glb_write(path, out, sink.bin) + return path, size, report, len(names), sink.dedup_saved + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--stage", required=True) + ap.add_argument("--list", default="pipeline/clips_r41.json") + ap.add_argument("--out", default="web/models/clips") + ap.add_argument("--manifest", default="web/assets/motion_manifest.json") + ap.add_argument("--tracks", choices=["rot", "all"], default="rot") + ap.add_argument("--no-quantize", action="store_true", + help="keep float32 rotation outputs (default: int16 normalized, ~2x smaller)") + ap.add_argument("--drop-fingers", action="store_true", + help="drop the 40 finger-bone channels (~half the payload; hands then hold " + "their bind pose). Off by default — fidelity kept, lever documented.") + ap.add_argument("--loop-seam-max", type=float, default=25.0, + help="a clip curated loopable but seaming worse than this is demoted") + a = ap.parse_args() + + sel = json.load(open(a.list)) + man = { + "_schema": ("PROCITY motion manifest v1 (R41 41.1). groups{} -> one GLB under " + "web/models/clips/, holding N named animations. clips{} -> " + "{group,category,duration,loopable,source,mixamo,pair?}. Load a group ONCE " + "(gltf.animations[] are named by clipId); index by clips[id].group."), + "_licence": ("Mixamo (Adobe) via ultra ~/Documents/MOTIONLIB/green/mixamo_full/. GREEN: " + "free to use inside games/projects, NO attribution required. Never " + "redistribute as a clip pack. R41 ships this licence regime only (ruling 2)."), + "_tracks": ("rotation-only" if a.tracks == "rot" else "translation+rotation+scale"), + "_encoding": ("rotation outputs are %s; %s" + % ("float32" if a.no_quantize else "int16 NORMALIZED (glTF-legal, three.js " + "r175 de-normalises in GLTFLoader._getArrayFromAccessor)", + "finger channels DROPPED" if a.drop_fingers else + "all 65 joints animated incl. fingers")), + "_note": ("Skeleton-only, 0 tris, 66 nodes (65 mixamorig joints + Armature) — the same " + "byte-shape as web/models/peds/idle.glb. rigs.js `_canon` folds the mixamorigN: " + "prefix, `_rotOnly` keeps quaternions and drops Hips.quaternion."), + "fps": 30, + "groups": {}, + "clips": {}, + } + total, dedup = 0, 0 + for gname, g in sel["groups"].items(): + path, size, rep, nnodes, dd = pack_group(gname, g["clips"], a.stage, a.out, a.tracks, + quant=not a.no_quantize, + drop_fingers=a.drop_fingers) + total += size + dedup += dd + byid = {r["id"]: r for r in rep} + man["groups"]["%s.glb" % gname] = { + "category": g["category"], "desc": g["desc"], "clips": [c["id"] for c in g["clips"]], + "bytes": size, "nodes": nnodes, + "durationSum": round(sum(r["duration"] for r in rep), 3), + } + for c in g["clips"]: + r = byid[c["id"]] + loop = bool(c.get("loop")) and r["loopSeamDeg"] <= a.loop_seam_max + e = {"group": "%s.glb" % gname, "category": g["category"], + "duration": r["duration"], "loopable": loop, + "loopSeamDeg": r["loopSeamDeg"], + "source": c["file"], "mixamo": c["mixamo"], + "channels": r["channels"], "rotBones": r["rot_bones"]} + if c.get("loop") and not loop: + e["loopDemoted"] = "curated loopable but loop seam %.1f deg > %.1f" % ( + r["loopSeamDeg"], a.loop_seam_max) + if c.get("pair"): + e["pair"] = c["pair"] + man["clips"][c["id"]] = e + print("[pack] %-11s %2d clips %7d B seam max %.1f deg" + % (gname, len(rep), size, max(r["loopSeamDeg"] for r in rep))) + man["totalBytes"] = total + man["clipCount"] = len(man["clips"]) + man["groupCount"] = len(man["groups"]) + man["dedupSavedBytes"] = dedup + os.makedirs(os.path.dirname(a.manifest), exist_ok=True) + with open(a.manifest, "w") as f: + json.dump(man, f, indent=1) + f.write("\n") + print("\n%d clips in %d group GLBs, %d B total (%.2f MB) -> %s" + % (man["clipCount"], man["groupCount"], total, total / 1048576.0, a.out)) + + +if __name__ == "__main__": + main() diff --git a/pipeline/clips_r41.json b/pipeline/clips_r41.json new file mode 100644 index 0000000..f215e6d --- /dev/null +++ b/pipeline/clips_r41.json @@ -0,0 +1,89 @@ +{ + "_comment": "PROCITY R41 §41.1 motion-library selection. SOURCE: ultra ~/Documents/MOTIONLIB/green/mixamo_full/ ONLY (Mixamo/Adobe, GREEN zone: free to use inside games, no attribution required, never redistribute as clip packs). Ruling 2: one licence regime this round. Every entry names an exact file in that bank's index.json — no wildcards (ruling 3).", + "_source_dir": "johnking@100.91.239.7:~/Documents/MOTIONLIB/green/mixamo_full/", + "_group_files": "web/models/clips/.glb — 6 groups, one fetch each (ruling 6)", + "groups": { + "idles": { + "category": "idle", + "desc": "the clone-army killer: 10 distinct standing idles", + "clips": [ + {"id": "idle_breathing", "file": "Breathing_Idle__c9c6d0d5.fbx", "mixamo": "Breathing Idle", "loop": true}, + {"id": "idle_standing", "file": "Standing_Idle__c9c831cf.fbx", "mixamo": "Standing Idle", "loop": true}, + {"id": "idle_look_around", "file": "Idle_Stand_Looking_Around__c9c6cdf7.fbx", "mixamo": "Looking Around", "loop": true}, + {"id": "idle_weight_shift", "file": "Shifting_Weight_From_Side_To_Side__c9c908f7.fbx","mixamo": "Weight Shift", "loop": true}, + {"id": "idle_cocky_lean", "file": "Cocky_Lean_Back__c9c90e3c.fbx", "mixamo": "Being Cocky", "loop": false}, + {"id": "idle_yawn", "file": "Big_Yawn_While_Standing__c9c98361.fbx", "mixamo": "Yawn", "loop": false}, + {"id": "idle_happy_1", "file": "Happy_Idle_Variation_1__c9ccf37e.fbx", "mixamo": "Happy Idle", "loop": true}, + {"id": "idle_happy_2", "file": "Happy_Idle_Variation_2__c9ccf445.fbx", "mixamo": "Happy Idle", "loop": true}, + {"id": "idle_old_man", "file": "Old_Man_Standing_Idle__c9c6c004.fbx", "mixamo": "Old Man Idle", "loop": true}, + {"id": "idle_smoking", "file": "Idle_Smoking__c9c603fd.fbx", "mixamo": "Smoking", "loop": true} + ] + }, + "browse": { + "category": "browse", + "desc": "shop-floor: hold / examine / open / carry / pick up", + "clips": [ + {"id": "browse_hold_idle", "file": "Holding_An_Object_Idle__c9cda11a.fbx", "mixamo": "Holding Idle", "loop": true}, + {"id": "browse_hold_walk", "file": "Holding_An_Object_While_Walking__c9cda1e7.fbx", "mixamo": "Holding Walk", "loop": true}, + {"id": "browse_hold_turn_l", "file": "Holding_An_Object_While_Turning_Left__c9cda2af.fbx", "mixamo": "Holding Turn Left", "loop": false, "pair": "browse_hold_turn_r"}, + {"id": "browse_hold_turn_r", "file": "Holding_An_Object_While_Turning_Right__c9cda378.fbx", "mixamo": "Holding Turn Right", "loop": false, "pair": "browse_hold_turn_l"}, + {"id": "browse_open_cabinet", "file": "Opening_Cabinet_Door__c9c6d49e.fbx", "mixamo": "Opening", "loop": false, "pair": "browse_close_cabinet"}, + {"id": "browse_close_cabinet","file": "Closing_Cabinet_Door__c9c6d55d.fbx", "mixamo": "Closing", "loop": false, "pair": "browse_open_cabinet"}, + {"id": "browse_carry_box", "file": "Carrying_A_Box__c9cd8d90.fbx", "mixamo": "Box Idle", "loop": true}, + {"id": "browse_pick_up", "file": "Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx","mixamo": "Picking Up Object","loop": false} + ] + }, + "sitlean": { + "category": "sitlean", + "desc": "benches and shopfronts: 4 seated + 4 leaning", + "clips": [ + {"id": "sit_bench_swing", "file": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx", "mixamo": "Sitting", "loop": true}, + {"id": "sit_hands_thighs", "file": "Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx", "mixamo": "Sitting Idle", "loop": true}, + {"id": "sit_impatient", "file": "Sitting_And_Impatiently_Waiting__c9ccad56.fbx", "mixamo": "Sitting", "loop": true}, + {"id": "sit_fidget_feet", "file": "Sit_With_Fidgeting_Feet__c9c699a5.fbx", "mixamo": "Sitting", "loop": true}, + {"id": "lean_wall_postures", "file": "Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx", "mixamo": "One Shoulder Lean", "loop": true}, + {"id": "lean_wall_leg_up", "file": "Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx", "mixamo": "Leaning", "loop": true}, + {"id": "lean_wall_shoulders", "file": "Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx", "mixamo": "Leaning", "loop": true}, + {"id": "lean_wall_walkman", "file": "Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx", "mixamo": "Leaning On A Wall", "loop": true} + ] + }, + "social": { + "category": "social", + "desc": "two-person street/shop talk; the handshake pair stays paired", + "clips": [ + {"id": "social_conversation", "file": "General_Conversation__c9c88301.fbx", "mixamo": "Talking", "loop": true}, + {"id": "social_chat_water", "file": "Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx", "mixamo": "Talking At Watercooler","loop": true}, + {"id": "social_greeting", "file": "Greeting_While_Standing__c9c7996a.fbx", "mixamo": "Standing Greeting", "loop": false}, + {"id": "social_shake_1", "file": "2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx", "mixamo": "Shaking Hands 1", "loop": false, "pair": "social_shake_2"}, + {"id": "social_shake_2", "file": "2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx", "mixamo": "Shaking Hands 2", "loop": false, "pair": "social_shake_1"}, + {"id": "social_nod_yes", "file": "Nodding_Head_Yes__c9c90d7b.fbx", "mixamo": "Head Nod Yes", "loop": false}, + {"id": "social_shake_no", "file": "Shaking_Head_No_Dismissively__c9c90b3c.fbx", "mixamo": "Shaking Head No", "loop": false}, + {"id": "social_wave", "file": "Waving__c9c5ed32.fbx", "mixamo": "Waving", "loop": false} + ] + }, + "locomotion": { + "category": "locomotion", + "desc": "civilian walk + turn set (the named Degree_Turn set is rifle-aiming — substituted, see AUDIT)", + "clips": [ + {"id": "walk_shopping_bag", "file": "Male_Walking_With_Shopping_Bag__c9c64027.fbx", "mixamo": "Walking With Shopping Bag", "loop": true}, + {"id": "turn_left_90", "file": "Turning_90_Degrees_Left__c9ceef5f.fbx", "mixamo": "Unarmed Turn Left 90", "loop": false, "pair": "turn_right_90"}, + {"id": "turn_right_90", "file": "Turning_90_Degrees_Right__c9cef01d.fbx", "mixamo": "Unarmed Turn Right 90", "loop": false, "pair": "turn_left_90"}, + {"id": "turn_walk_180", "file": "Turning_180_Degrees_While_Walking__c9c9c73f.fbx", "mixamo": "Walking Turn 180", "loop": false}, + {"id": "turn_in_place", "file": "Turning_In_Place__c9c70ef0.fbx", "mixamo": "Turning", "loop": true}, + {"id": "walk_to_stand", "file": "Walking_To_Standing_Idle__c9c8d966.fbx", "mixamo": "Stop Walking", "loop": false} + ] + }, + "venue": { + "category": "venue", + "desc": "pub / gig / record shop", + "clips": [ + {"id": "venue_clap_stand", "file": "Clap_While_Standing__c9c60ef2.fbx", "mixamo": "Clapping", "loop": true}, + {"id": "venue_clap_seated", "file": "Clap_While_Seated__c9c60fce.fbx", "mixamo": "Clapping", "loop": true}, + {"id": "venue_cheer", "file": "Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx", "mixamo": "Cheering", "loop": true}, + {"id": "venue_northern_soul", "file": "Basic_Northern_Soul_Step__c9cbe56d.fbx", "mixamo": "Northern Soul Dance","loop": true}, + {"id": "venue_bartending", "file": "Pouring_And_Serving_Drinks__c9c78799.fbx", "mixamo": "Bartending", "loop": true}, + {"id": "venue_headphones", "file": "Male_With_Headphones_Listening_To_Music__c9c63329.fbx", "mixamo": "Listening To Music", "loop": true} + ] + } + } +} diff --git a/pipeline/clips_verify.py b/pipeline/clips_verify.py new file mode 100644 index 0000000..a05b75d --- /dev/null +++ b/pipeline/clips_verify.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""PROCITY R41 §41.1 — gate: motion_manifest.json <-> web/models/clips/*.glb integrity. + + python3 pipeline/clips_verify.py # 0 errors = green + python3 pipeline/clips_verify.py --stage # + quantisation fidelity vs the float source + +Checks, all measured from the bytes (Lane F: this is the "every referenced clip resolves" gate): + 1. every group file named in the manifest exists, and its byte size matches the recorded one + 2. every clip id resolves to a NAMED animation inside its group GLB, and nothing extra rides along + 3. every group GLB is ZERO-DRAW — 0 meshes, 0 materials, 0 images, no Draco + 4. every group carries the same 66-node mixamorig skeleton (65 joints + Armature) + 5. every animation channel targets a node that exists, on a path the manifest declares + 6. recorded duration matches the max keyframe time in the file + 7. `_rotOnly` survivability: every clip keeps >=1 non-Hips quaternion track, i.e. the clip still + says something after web/js/citizens/rigs.js filters it + 8. with --stage: max angular error of the int16 rotation quantisation vs the float32 source +""" +import argparse +import json +import math +import os +import struct +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from clips_pack import acc_floats, canon_name, glb_read # noqa: E402 + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--manifest", default="web/assets/motion_manifest.json") + ap.add_argument("--clips", default="web/models/clips") + ap.add_argument("--stage", help="staging dir of per-clip float32 GLBs (fidelity check)") + a = ap.parse_args() + man = json.load(open(a.manifest)) + err, warn = [], [] + total = 0 + skel = None + + for gfile, g in man["groups"].items(): + p = os.path.join(a.clips, gfile) + if not os.path.exists(p): + err.append("%s: MISSING" % gfile) + continue + size = os.path.getsize(p) + total += size + if size != g["bytes"]: + err.append("%s: size %d != manifest %d" % (gfile, size, g["bytes"])) + js, bin_ = glb_read(p) + if js.get("meshes") or js.get("materials") or js.get("images"): + err.append("%s: NOT zero-draw (meshes=%d materials=%d images=%d)" + % (gfile, len(js.get("meshes", [])), len(js.get("materials", [])), + len(js.get("images", [])))) + if any("draco" in e.lower() for e in js.get("extensionsUsed", [])): + err.append("%s: Draco present" % gfile) + names = [canon_name(n.get("name")) for n in js["nodes"]] + if skel is None: + skel = names + elif names != skel: + err.append("%s: skeleton differs from %s" % (gfile, list(man["groups"])[0])) + joints = [n for n in names if n.startswith("mixamorig:")] + if len(names) != 66 or len(joints) != 65: + err.append("%s: expected 66 nodes / 65 mixamorig joints, got %d / %d" + % (gfile, len(names), len(joints))) + anims = {an.get("name"): an for an in js.get("animations", [])} + if sorted(anims) != sorted(g["clips"]): + err.append("%s: animations %s != manifest clips %s" + % (gfile, sorted(anims), sorted(g["clips"]))) + for cid in g["clips"]: + if cid not in anims: + continue + m = man["clips"][cid] + if m["group"] != gfile: + err.append("%s: clips[%s].group=%s" % (gfile, cid, m["group"])) + an = anims[cid] + dur, keeps = 0.0, 0 + for ch in an["channels"]: + ni = ch["target"]["node"] + if not (0 <= ni < len(names)): + err.append("%s/%s: channel targets node %d out of range" % (gfile, cid, ni)) + continue + if ch["target"]["path"] != "rotation" and man["_tracks"] == "rotation-only": + err.append("%s/%s: %s channel in a rotation-only build" + % (gfile, cid, ch["target"]["path"])) + if ch["target"]["path"] == "rotation" and not names[ni].endswith(":Hips"): + keeps += 1 + t = acc_floats(js, bin_, an["samplers"][ch["sampler"]]["input"]) + if t: + dur = max(dur, t[-1][0]) + if abs(dur - m["duration"]) > 1e-3: + err.append("%s/%s: duration %.4f != manifest %.4f" % (gfile, cid, dur, m["duration"])) + if keeps == 0: + err.append("%s/%s: nothing survives rigs.js `_rotOnly`" % (gfile, cid)) + if len(an["channels"]) != m["channels"]: + err.append("%s/%s: %d channels != manifest %d" + % (gfile, cid, len(an["channels"]), m["channels"])) + + if total != man["totalBytes"]: + err.append("totalBytes %d != measured %d" % (man["totalBytes"], total)) + if len(man["groups"]) > 6: + err.append("ruling 6: %d group files > 6" % len(man["groups"])) + if man["clipCount"] != len(man["clips"]): + err.append("clipCount mismatch") + + # 8. quantisation fidelity against the float32 staging GLBs + if a.stage: + worst, worst_id = 0.0, None + for cid, m in man["clips"].items(): + sp = os.path.join(a.stage, "%s.glb" % cid) + if not os.path.exists(sp): + warn.append("no staged source for %s" % cid) + continue + sj, sb = glb_read(sp) + gj_, gb = glb_read(os.path.join(a.clips, m["group"])) + snames = [canon_name(n.get("name")) for n in sj["nodes"]] + gnames = [canon_name(n.get("name")) for n in gj_["nodes"]] + ga = next(x for x in gj_["animations"] if x.get("name") == cid) + gmap = {gnames[c["target"]["node"]]: c for c in ga["channels"] + if c["target"]["path"] == "rotation"} + for ch in sj["animations"][0]["channels"]: + if ch["target"]["path"] != "rotation": + continue + bone = snames[ch["target"]["node"]] + if bone not in gmap: + continue + q0 = acc_floats(sj, sb, sj["animations"][0]["samplers"][ch["sampler"]]["output"]) + q1 = acc_floats(gj_, gb, ga["samplers"][gmap[bone]["sampler"]]["output"]) + if len(q0) != len(q1): + err.append("%s/%s: keyframe count %d != %d" % (cid, bone, len(q0), len(q1))) + continue + for x, y in zip(q0, q1): + if man["_encoding"].startswith("rotation outputs are int16"): + y = tuple(max(v / 32767.0, -1.0) for v in y) + d = abs(sum(p * q for p, q in zip(x, y))) + d /= (math.sqrt(sum(p * p for p in x)) * math.sqrt(sum(q * q for q in y)) or 1) + ang = math.degrees(2 * math.acos(max(-1.0, min(1.0, d)))) + if ang > worst: + worst, worst_id = ang, "%s/%s" % (cid, bone) + print("quantisation: worst angular error %.6f deg (%s)" % (worst, worst_id)) + if worst > 0.05: + err.append("quantisation error %.4f deg exceeds 0.05" % worst) + + print("%d clips / %d groups / %d bytes (%.2f MB)" + % (man["clipCount"], len(man["groups"]), total, total / 1048576.0)) + for w in warn: + print("WARN %s" % w) + for e in err: + print("ERROR %s" % e) + print("clips_verify: %s (%d errors, %d warnings)" + % ("GREEN" if not err else "RED", len(err), len(warn))) + return 1 if err else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pipeline/contact_sheet.py b/pipeline/contact_sheet.py new file mode 100644 index 0000000..ed41514 --- /dev/null +++ b/pipeline/contact_sheet.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Render a labelled contact sheet of many GLBs in ONE Blender run — the eyeball tool. + +The house rule is "always eyeball the thumbnails", and R41 §41.2 imports ~150 assets across three +libraries. One Blender startup per asset would cost more than the work; this lays every GLB out on +a grid in a single scene, one camera, one render, with a Blender text label and a metre reference +under each. Blender startup is the slow part, so 48 assets cost about what 2 do. + +Every tile is framed to the SAME world scale by default (`--absolute`), which is the only way a +contact sheet can answer "is this metre-correct" — a per-tile auto-zoom makes a 0.2 m stool and a +2 m fridge look identical, which is exactly how a wrong scale ships. A 1 m grid square sits under +each asset as the ruler. + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python pipeline/contact_sheet.py -- OUT.png GLB [GLB ...] + "$BL" --background --python pipeline/contact_sheet.py -- OUT.png --cols 8 --tile 320 DIR/*.glb + +Options (after OUT.png, in any order among the file list): + --cols N tiles per row (default: ceil(sqrt(n))) + --tile N pixel size per tile (default 256) + --fit per-tile auto-zoom instead of one shared scale (looks prettier, lies about size) + --title TEXT caption drawn at the top of the sheet +""" +import bpy, sys, os, math +from mathutils import Vector + +ARGV = sys.argv[sys.argv.index("--") + 1:] +OUT = ARGV[0] +rest = ARGV[1:] +COLS = TILE = None +FIT = False +TITLE = "" +files = [] +i = 0 +while i < len(rest): + a = rest[i] + if a == "--cols": + COLS = int(rest[i + 1]); i += 2 + elif a == "--tile": + TILE = int(rest[i + 1]); i += 2 + elif a == "--title": + TITLE = rest[i + 1]; i += 2 + elif a == "--fit": + FIT = True; i += 1 + else: + files.append(a); i += 1 +TILE = TILE or 256 +files = [f for f in files if f.lower().endswith(".glb")] +COLS = COLS or max(1, int(math.ceil(math.sqrt(len(files))))) +ROWS = int(math.ceil(len(files) / COLS)) + + +def wipe(): + for o in list(bpy.data.objects): + bpy.data.objects.remove(o, do_unlink=True) + for c in list(bpy.data.collections): + bpy.data.collections.remove(c) + + +def bounds(objs): + lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3) + for o in objs: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + for k in range(3): + lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k]) + if lo.x > 1e8: + return Vector((0, 0, 0)), Vector((0, 0, 0)) + return lo, hi + + +def label(text, x, y, size, colour=(0.1, 0.1, 0.1, 1)): + cu = bpy.data.curves.new(type="FONT", name="lbl") + cu.body = text + cu.align_x = 'CENTER' + cu.size = size + ob = bpy.data.objects.new("lbl", cu) + ob.location = (x, y, 0) + m = bpy.data.materials.new("lblmat") + m.use_nodes = True + bsdf = m.node_tree.nodes.get("Principled BSDF") + bsdf.inputs["Base Color"].default_value = colour + bsdf.inputs["Roughness"].default_value = 1.0 + ob.data.materials.append(m) + bpy.context.scene.collection.objects.link(ob) + return ob + + +def main(): + wipe() + scn = bpy.context.scene + THETA = math.radians(58) # camera tilt from straight-down + PITCH = 1.0 # world metres between tile centres, ACROSS (x) + # Rows are separated along world Y, which the tilted camera foreshortens by cos(THETA) ~ 0.53, + # while object HEIGHT projects at sin(THETA) ~ 0.85. Using one pitch for both axes therefore + # stacks every tall asset on top of the row behind it. Space the rows out by the reciprocal. + PITCH_Y = PITCH / math.cos(THETA) + + # --- import every GLB, park it on the grid ------------------------------------------------ + placed = [] + for n, path in enumerate(files): + r, c = divmod(n, COLS) + before = set(bpy.data.objects) + try: + bpy.ops.import_scene.gltf(filepath=path) + except Exception as e: + print(f"IMPORT FAIL {path}: {e}") + continue + new = [o for o in bpy.data.objects if o not in before] + roots = [o for o in new if o.parent is None] + meshes = [o for o in new if o.type == 'MESH'] + if not meshes: + for o in new: + bpy.data.objects.remove(o, do_unlink=True) + print(f"NO MESH {path}") + continue + lo, hi = bounds(meshes) + size = max(hi.x - lo.x, hi.y - lo.y, hi.z - lo.z, 1e-4) + cx, cy = (lo.x + hi.x) / 2, (lo.y + hi.y) / 2 + # scale: shared world scale (honest) or per-tile fit (pretty) + s = (PITCH * 0.62) / size if FIT else 1.0 + tx, ty = c * PITCH, -r * PITCH_Y + for o in roots: + o.scale = (o.scale.x * s, o.scale.y * s, o.scale.z * s) + o.location = (o.location.x * s + tx - cx * s, + o.location.y * s + ty - cy * s, + o.location.z * s - lo.z * s) + placed.append((path, size, hi.z - lo.z, meshes)) + label(os.path.basename(path)[:-4][:26], tx, ty - PITCH * 0.50, PITCH * 0.052) + label(f"{hi.x-lo.x:.2f} x {hi.y-lo.y:.2f} x {hi.z-lo.z:.2f} m", + tx, ty - PITCH * 0.60, PITCH * 0.042, (0.30, 0.30, 0.38, 1)) + + # --- a 1 m reference square under every tile (the ruler) ----------------------------------- + if not FIT: + for n in range(len(placed)): + r, c = divmod(n, COLS) + bpy.ops.mesh.primitive_plane_add(size=1.0, location=(c * PITCH, -r * PITCH_Y, -0.002)) + p = bpy.context.object + m = bpy.data.materials.new("grid") + m.use_nodes = True + b = m.node_tree.nodes.get("Principled BSDF") + b.inputs["Base Color"].default_value = (0.80, 0.80, 0.84, 1) + b.inputs["Roughness"].default_value = 1.0 + p.data.materials.append(m) + + # --- backdrop, light, camera ---------------------------------------------------------------- + W = (COLS - 1) * PITCH + H = (ROWS - 1) * PITCH_Y + tall = max((t[2] for t in placed), default=1.0) + bpy.ops.mesh.primitive_plane_add(size=max(W, H) * 4 + 40, location=(W / 2, -H / 2, -0.004)) + bg = bpy.context.object + m = bpy.data.materials.new("bg") + m.use_nodes = True + b = m.node_tree.nodes.get("Principled BSDF") + b.inputs["Base Color"].default_value = (0.94, 0.94, 0.96, 1) + b.inputs["Roughness"].default_value = 1.0 + bg.data.materials.append(m) + + scn.world.use_nodes = True + scn.world.node_tree.nodes["Background"].inputs[0].default_value = (0.9, 0.92, 0.96, 1) + scn.world.node_tree.nodes["Background"].inputs[1].default_value = 0.85 + bpy.ops.object.light_add(type='SUN', location=(W / 2 + 3, -H / 2 - 4, 8 + tall)) + sun = bpy.context.object + sun.data.energy = 1.9 + sun.rotation_euler = (math.radians(52), 0, math.radians(38)) + + # A CONTACT SHEET IS A COLOUR JUDGEMENT, so it must not go through a film emulation. + # Blender's default view transform (AgX in 5.x) desaturates hard: the first pass of the + # art_incoming props read as bleached white and looked like the vertex-colour transfer had + # failed, when measuring COLOR_0 in the exported GLBs showed the means intact to within 3%. + # The tool was lying, not the asset. Standard = what you see is what is in the file. + scn.view_settings.view_transform = 'Standard' + scn.view_settings.look = 'None' + + # Aim the camera AT the grid centre. A camera rotated theta about X looks along + # (0, sin theta, -cos theta), so it must sit back along the reverse of that ray from the target + # -- an arbitrary offset misses by t*sin(theta) and renders an empty backdrop (this tool's + # first take came back blank exactly that way). + tgt = Vector((W / 2, -H / 2 - PITCH * 0.15, PITCH * 0.20)) + cam_d = (max(W, H) + tall) * 3.0 + 10.0 + bpy.ops.object.camera_add(location=(tgt.x, + tgt.y - cam_d * math.sin(THETA), + tgt.z + cam_d * math.cos(THETA))) + cam = bpy.context.object + cam.data.type = 'ORTHO' + cam.data.clip_end = cam_d * 4 + cam.rotation_euler = (THETA, 0, 0) + scn.camera = cam + + # World extent the frame must contain. Screen-vertical mixes the ground span BETWEEN row + # centres ((ROWS-1) gaps, each PITCH_Y foreshortened by cos = PITCH) with the tallest object's + # height (x sin) and one label gutter. Counting a gutter PER ROW instead of between rows + # over-estimates badly at ROWS=1 and renders everything as postage stamps. + need_w = COLS * PITCH * 1.02 + need_h = (ROWS - 1) * PITCH + tall * math.sin(THETA) + PITCH * 0.75 \ + + (PITCH * 0.42 if TITLE else 0.0) + res_x = int(COLS * TILE) + res_y = max(TILE // 2, int(res_x * need_h / need_w)) + # ortho_scale governs the LARGER pixel dimension; solve so BOTH fit. + if res_x >= res_y: + cam.data.ortho_scale = max(need_w, need_h * res_x / res_y) + else: + cam.data.ortho_scale = max(need_h, need_w * res_y / res_x) + + if TITLE: + # well clear of row 0: tall assets project upward in screen space from y=0, so a title + # parked at +0.6 m lands on top of them. + label(TITLE, W / 2, PITCH_Y * 0.72 + tall, PITCH * 0.085, (0.05, 0.05, 0.1, 1)) + + # Blender 5.x renamed the EEVEE enum back to BLENDER_EEVEE; keep both spellings working. + engines = scn.bl_rna.properties["render"].fixed_type.properties["engine"].enum_items.keys() \ + if False else ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT') + for e in engines: + try: + scn.render.engine = e + break + except TypeError: + continue + scn.render.film_transparent = False + scn.render.resolution_x = res_x + scn.render.resolution_y = res_y + scn.render.image_settings.file_format = 'PNG' + scn.render.filepath = os.path.abspath(OUT) + os.makedirs(os.path.dirname(os.path.abspath(OUT)), exist_ok=True) + bpy.ops.render.render(write_still=True) + print(f"SHEET {OUT} {len(placed)}/{len(files)} rendered {COLS}x{ROWS}") + + +main() diff --git a/pipeline/merge_prims.py b/pipeline/merge_prims.py new file mode 100644 index 0000000..e006e58 --- /dev/null +++ b/pipeline/merge_prims.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Collapse a multi-material GLB to ONE primitive per mesh by baking baseColorFactor -> COLOR_0. + +WHY THIS EXISTS (R41 §41.2, the 3dstore import) +----------------------------------------------- +`~/Documents/3dstore/` was handed over as "45 of 46 pass the house law as-is". On tris, scale, +Draco and textures that is true. On the number that actually decides whether Lane C can place them +it is not: **14 of the files carry 16 materials on a single mesh**, one flat colour per record +sleeve (`Mat_Sleeve_`). glTF gives each material its own primitive, and three.js +`GLTFLoader` instantiates one `Mesh` per primitive — so a 1,020-tri "Black Tub" costs **16 draw +calls**, not one. The interior draw margin is 162. Ten tubs would be the entire budget. + +The materials are pure `baseColorFactor` — no textures, no UVs, nothing but a flat colour and a +metallic/roughness pair. That is exactly what a vertex colour reproduces. So we merge every +primitive of a mesh into one, write each source primitive's `baseColorFactor` into a `COLOR_0` +attribute on its own vertices, and emit a single white material with the dominant primitive's +PBR values. glTF multiplies `COLOR_0` by `baseColorFactor`, and both live in LINEAR space, so a +white factor times the original linear colour is **numerically the original colour**. Byte-exact +look, 16 draws -> 1. + +This is deliberately NOT `gltf-transform optimize` (ruling 5) and NOT a Blender re-export: the +geometry is already correct and every extra tool is another chance to move it. We rewrite the +container and touch nothing but the material binding. + + python3 pipeline/merge_prims.py IN.glb OUT.glb + python3 pipeline/merge_prims.py --check OUT.glb # report prims/draws/materials +""" +import json, os, struct, sys + +COMPONENT = {5120: ("b", 1), 5121: ("B", 1), 5122: ("h", 2), + 5123: ("H", 2), 5125: ("I", 4), 5126: ("f", 4)} +NCOMP = {"SCALAR": 1, "VEC2": 2, "VEC3": 3, "VEC4": 4, "MAT4": 16} + + +# ── container io ───────────────────────────────────────────────────────────────────────────────── +def read_glb(path): + d = open(path, "rb").read() + if d[:4] != b"glTF": + raise ValueError(f"{path}: not a GLB") + off, gltf, binc = 12, None, b"" + while off + 8 <= len(d): + clen, ctype = struct.unpack(" 1 else [0] * count + bv = gltf["bufferViews"][a["bufferView"]] + base = bv.get("byteOffset", 0) + a.get("byteOffset", 0) + stride = bv.get("byteStride") or (size * n) + out = [] + for i in range(count): + off = base + i * stride + v = struct.unpack_from("<" + fmt * n, binc, off) + out.append(v if n > 1 else v[0]) + return out + + +def norm_component(v, ctype): + """glTF normalized-integer -> float, per the spec's exact divisors.""" + if ctype == 5121: + return v / 255.0 + if ctype == 5123: + return v / 65535.0 + if ctype == 5120: + return max(v / 127.0, -1.0) + if ctype == 5122: + return max(v / 32767.0, -1.0) + return float(v) + + +# ── the merge ──────────────────────────────────────────────────────────────────────────────────── +def merge(gltf, binc): + """Rewrite every multi-primitive mesh as one primitive with baked COLOR_0. Returns a report.""" + mats = gltf.get("materials", []) + new_bin = bytearray() + new_views, new_accs = [], [] + + def add_accessor(data, fmt, size, ncomp, atype, ctype, minmax=False, target=None): + while len(new_bin) % 4: + new_bin.append(0) + off = len(new_bin) + for v in data: + new_bin.extend(struct.pack("<" + fmt * ncomp, *(v if ncomp > 1 else (v,)))) + bv = {"buffer": 0, "byteOffset": off, "byteLength": len(new_bin) - off} + if target: + bv["target"] = target + new_views.append(bv) + acc = {"bufferView": len(new_views) - 1, "componentType": ctype, + "count": len(data), "type": atype} + if minmax and data: + cols = list(zip(*data)) if ncomp > 1 else [data] + acc["min"] = [min(c) for c in cols] + acc["max"] = [max(c) for c in cols] + new_accs.append(acc) + return len(new_accs) - 1 + + report = [] + for mi, mesh in enumerate(gltf.get("meshes", [])): + prims = mesh.get("primitives", []) + if not prims: + continue + pos, nrm, col, idx = [], [], [], [] + tri_by_prim = [] + for p in prims: + attrs = p["attributes"] + P = read_accessor(gltf, binc, attrs["POSITION"]) + N = (read_accessor(gltf, binc, attrs["NORMAL"]) + if "NORMAL" in attrs else [(0.0, 1.0, 0.0)] * len(P)) + # existing COLOR_0 multiplies the factor (spec); normalize ints first + if "COLOR_0" in attrs: + ca = gltf["accessors"][attrs["COLOR_0"]] + raw = read_accessor(gltf, binc, attrs["COLOR_0"]) + ct, nc = ca["componentType"], NCOMP[ca["type"]] + existing = [tuple(norm_component(x, ct) for x in (v if nc > 1 else (v,))) + for v in raw] + existing = [(e + (1.0,)) if len(e) == 3 else e for e in existing] + else: + existing = None + m = mats[p["material"]] if p.get("material") is not None and mats else {} + bcf = (m.get("pbrMetallicRoughness", {}) or {}).get("baseColorFactor", [1, 1, 1, 1]) + bcf = tuple(float(x) for x in (list(bcf) + [1, 1, 1, 1])[:4]) + base = len(pos) + pos.extend(P) + nrm.extend(N) + if existing: + col.extend(tuple(bcf[k] * existing[i][k] for k in range(4)) + for i in range(len(P))) + else: + col.extend([bcf] * len(P)) + if "indices" in p: + I = read_accessor(gltf, binc, p["indices"]) + else: + I = list(range(len(P))) + idx.extend(i + base for i in I) + tri_by_prim.append((len(I) // 3, p.get("material"))) + + # PBR donor = the material covering the most triangles, so the dominant surface keeps its + # exact metallic/roughness. (On the 3dstore tubs that is a sleeve: metallic 0, rough 0.6.) + donor_mat = max(tri_by_prim, key=lambda t: t[0])[1] if tri_by_prim else None + donor = mats[donor_mat] if donor_mat is not None and mats else {} + dp = donor.get("pbrMetallicRoughness", {}) or {} + + ctype = 5125 if len(pos) > 65535 else 5123 + fmt = "I" if ctype == 5125 else "H" + i_acc = add_accessor(idx, fmt, 4 if ctype == 5125 else 2, 1, "SCALAR", ctype, + target=34963) + p_acc = add_accessor(pos, "f", 4, 3, "VEC3", 5126, minmax=True, target=34962) + n_acc = add_accessor(nrm, "f", 4, 3, "VEC3", 5126, target=34962) + c_acc = add_accessor(col, "f", 4, 4, "VEC4", 5126, target=34962) + + mesh["primitives"] = [{ + "attributes": {"POSITION": p_acc, "NORMAL": n_acc, "COLOR_0": c_acc}, + "indices": i_acc, "material": 0, "mode": 4, + }] + report.append({"mesh": mi, "name": mesh.get("name"), "prims_before": len(prims), + "prims_after": 1, "tris": len(idx) // 3, "verts": len(pos)}) + + gltf["materials"] = [{ + "name": "Merged_VertexColor", + "doubleSided": any(m.get("doubleSided") for m in mats) if mats else False, + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 1, 1], # identity: COLOR_0 carries it all + "metallicFactor": dp.get("metallicFactor", 1.0), + "roughnessFactor": dp.get("roughnessFactor", 1.0), + }, + }] + gltf["bufferViews"] = new_views + gltf["accessors"] = new_accs + gltf["buffers"] = [{"byteLength": len(new_bin)}] + for k in ("images", "textures", "samplers"): # nothing references them now + gltf.pop(k, None) + gltf["extensionsUsed"] = [e for e in gltf.get("extensionsUsed", []) + if "draco" not in e.lower()] or None + if not gltf["extensionsUsed"]: + gltf.pop("extensionsUsed") + return report, bytes(new_bin) + + +def check(path): + gltf, _ = read_glb(path) + meshes = gltf.get("meshes", []) + nodes = gltf.get("nodes", []) + draws = sum(len(meshes[n["mesh"]].get("primitives", [])) for n in nodes if "mesh" in n) + prims = sum(len(m.get("primitives", [])) for m in meshes) + tris = 0 + for m in meshes: + for p in m.get("primitives", []): + a = gltf["accessors"][p["indices"]] if "indices" in p else \ + gltf["accessors"][p["attributes"]["POSITION"]] + tris += a["count"] // 3 + return {"file": os.path.basename(path), "draws": draws, "prims": prims, + "materials": len(gltf.get("materials", [])), "tris": tris, + "images": len(gltf.get("images", []))} + + +def main(): + args = sys.argv[1:] + if args and args[0] == "--check": + for f in args[1:]: + print(check(f)) + return 0 + if len(args) != 2: + print(__doc__) + return 2 + src, dst = args + gltf, binc = read_glb(src) + before = check(src) + rep, nb = merge(gltf, binc) + os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True) + write_glb(dst, gltf, nb) + after = check(dst) + print(f"{os.path.basename(src):42} draws {before['draws']:>3} -> {after['draws']:>2} " + f"| mats {before['materials']:>2} -> {after['materials']} " + f"| tris {before['tris']} -> {after['tris']}" + f"{' *** TRI DRIFT ***' if before['tris'] != after['tris'] else ''}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pipeline/normalize.py b/pipeline/normalize.py index 9517fa5..0d88856 100644 --- a/pipeline/normalize.py +++ b/pipeline/normalize.py @@ -141,6 +141,150 @@ def clean_mesh(obj): return 0 +def nonmanifold_ratio(obj): + """Fraction of vertices on a non-manifold edge — the number that predicts whether COLLAPSE + decimation can work at all. Measured, not guessed: see remesh_shellsoup().""" + n = len(obj.data.vertices) + if not n: + return 0.0 + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.mesh.select_non_manifold() + bpy.ops.object.mode_set(mode='OBJECT') + return sum(1 for v in obj.data.vertices if v.select) / n + + +def remesh_shellsoup(obj, budget, voxel=None, thick=0.0): + """Voxel-remesh a shell-soup mesh to a manifold surface, decimate to budget, and carry the + source's vertex colours across. + + [R41 §41.2] `art_incoming`'s 48 pub props are trimesh output at ~200k tris that COLLAPSE + decimation cannot touch: loungeChair is 94% non-manifold (376,076 of 399,542 verts) and a + ratio-0.01 decimate moved it 1,319,836 -> 1,274,119, i.e. 3%. topiary behaved the same at 93% + non-manifold; pokie, at 29%, decimated 198k -> 47k normally. Decimate cannot merge across + overlapping shells, so the fix is to stop trying: rebuild the topology with a voxel remesh + (OpenVDB, always manifold), then decimate THAT, then transfer the colour back. + + This is the same "throw the topology away" move as bake_lowpoly.py, minus the Cycles bake — + these props carry COLOR_0 and no textures, so a vertex-colour transfer keeps them at one + material and zero texture bytes instead of adding a 1024px atlas each. + """ + src = obj + bpy.ops.object.select_all(action='DESELECT') + src.select_set(True) + bpy.context.view_layer.objects.active = src + bpy.ops.object.duplicate() + lp = bpy.context.view_layer.objects.active # the copy becomes the low-poly + lp.name = "LP" + + dims = lp.dimensions + v = voxel or max(max(dims) / 150.0, 1e-4) + + # A voxel remesh is a signed-distance field: it needs a VOLUME. Feed it a thin open shell and + # it returns a lattice of holes — the keg (a thin-walled scan) came back as a wire cage at + # every voxel size tried, while solid props like the couch remeshed cleanly. Giving the shell + # a wall thicker than one voxel first is what makes the field well-defined. + if thick: + sm = lp.modifiers.new("solid", 'SOLIDIFY') + sm.thickness = thick if thick > 0 else v * 2.5 + sm.offset = 0.0 + sm.use_even_offset = False + bpy.ops.object.modifier_apply(modifier=sm.name) + + rm = lp.modifiers.new("remesh", 'REMESH') + rm.mode = 'VOXEL' + rm.voxel_size = v + rm.adaptivity = 0.0 + bpy.ops.object.modifier_apply(modifier=rm.name) + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris() + bpy.ops.object.mode_set(mode='OBJECT') + n = len(lp.data.polygons) + if n > budget: + dm = lp.modifiers.new("dec", 'DECIMATE') + dm.ratio = max(0.002, budget / n) + bpy.ops.object.modifier_apply(modifier=dm.name) + + # Carry colour (and UVs, if the source is textured) across from the dense original by + # nearest-face interpolation. The destination layers must be CREATED first with + # `datalayout_transfer` — a Data Transfer modifier applied without it silently transfers + # nothing and the prop exports colourless, which is exactly how the first run of this came + # back "colour LOST" on all six. + transferred = False + src_cols = list(src.data.color_attributes) + src_uvs = list(src.data.uv_layers) + if src_cols or src_uvs: + try: + dt = lp.modifiers.new("xfer", 'DATA_TRANSFER') + dt.object = src + loops, verts = set(), set() + if src_cols: + dom = (src.data.color_attributes.active_color or src_cols[0]).domain + (loops if dom == 'CORNER' else verts).add( + 'COLOR_CORNER' if dom == 'CORNER' else 'COLOR_VERTEX') + if src_uvs: + loops.add('UV') + if loops: + dt.use_loop_data = True + dt.data_types_loops = loops + dt.loop_mapping = 'POLYINTERP_NEAREST' + if verts: + dt.use_vert_data = True + dt.data_types_verts = verts + dt.vert_mapping = 'POLYINTERP_NEAREST' + bpy.ops.object.datalayout_transfer(modifier=dt.name) # create dest layers + bpy.ops.object.modifier_apply(modifier=dt.name) + transferred = bool(lp.data.color_attributes) if src_cols else bool(lp.data.uv_layers) + except Exception as e: + print(f" colour/UV transfer failed: {e}") + + # the voxel remesh can leave degenerate faces; Blender warns "mesh is not valid" on export + lp.data.validate(verbose=False) + + lp_mesh_name = lp.data.name + bpy.data.objects.remove(src, do_unlink=True) + bpy.ops.object.select_all(action='DESELECT') + lp.select_set(True) + bpy.context.view_layer.objects.active = lp + print(f" remesh voxel={v:.4f} -> {n} tris -> {len(lp.data.polygons)} " + f"(colour {'transferred' if transferred else 'LOST'}) [{lp_mesh_name}]") + return lp, transferred + + +def wire_vertex_colour(obj): + """Route the active colour attribute into Base Color on every material. + + The glTF exporter only writes COLOR_0 when a material actually reads the attribute + (`export_vertex_color='MATERIAL'`), and these sources arrive with NO material block at all — + Blender invents a plain grey one on import. Without this the remeshed props would export + colourless and every thumbnail would be grey. + """ + if not obj.data.color_attributes: + return False + cname = obj.data.color_attributes.active_color.name \ + if obj.data.color_attributes.active_color else obj.data.color_attributes[0].name + if not obj.data.materials: + m = bpy.data.materials.new("vcol") + m.use_nodes = True + obj.data.materials.append(m) + for m in obj.data.materials: + if not m or not m.use_nodes: + continue + nt = m.node_tree + bsdf = nt.nodes.get("Principled BSDF") + if not bsdf: + continue + n = nt.nodes.new("ShaderNodeVertexColor") + n.layer_name = cname + nt.links.new(n.outputs["Color"], bsdf.inputs["Base Color"]) + if "Specular IOR Level" in bsdf.inputs: + bsdf.inputs["Specular IOR Level"].default_value = 0.2 + bsdf.inputs["Roughness"].default_value = 0.72 + return True + + def resize_textures(max_px=1024): for img in bpy.data.images: if img.size[0] > max_px or img.size[1] > max_px: @@ -154,14 +298,37 @@ def normalize_one(spec): 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) + tri_before = len(obj.data.polygons) + + # shell-soup path: rebuild topology before the decimate loop, which cannot touch it + nm = 0.0 + remeshed = vcol = False + if spec.get("remesh"): + nm = nonmanifold_ratio(obj) + obj, vcol = remesh_shellsoup(obj, spec.get("tris", 5000), spec.get("voxel"), + spec.get("shell_thickness", 0.0)) + remeshed = True # 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) + if rx or ry or rz or spec.get("yaw"): + # Select+activate explicitly: transform_apply acts on the SELECTION, and after clean_mesh + # (or a remesh) the object we mean is not guaranteed to still be it. A silently-skipped + # yaw is invisible in the log and ships a prop facing the wrong way. + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + obj.rotation_mode = 'XYZ' + before = tuple(round(v, 4) for v in obj.dimensions) + 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) + after = tuple(round(v, 4) for v in obj.dimensions) + if before == after and (spec.get("yaw", 0) % 180) not in (0,): + print(f" WARNING: yaw/up requested but dimensions unchanged {before} — not applied") + else: + print(f" oriented {before} -> {after}") # solidify: cap open bottoms → watertight, white sole (finish_glb.py:54) if spec.get("solidify"): @@ -235,17 +402,22 @@ def normalize_one(spec): mn, mx = bounds(obj) resize_textures(1024) + if spec.get("remesh") or spec.get("vertex_colour"): + vcol = wire_vertex_colour(obj) or vcol 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, - ) + exp = dict(filepath=out, export_format='GLB', export_yup=True, export_apply=True, + export_image_format='WEBP', export_image_quality=85) + try: # Blender >=4.2 gates COLOR_0 behind this enum + bpy.ops.export_scene.gltf(**exp, export_vertex_color='ACTIVE') + except TypeError: + bpy.ops.export_scene.gltf(**exp) 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, + "remeshed": remeshed, "nonmanifold": round(nm, 3), "vertex_colour": vcol, "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)], } diff --git a/pipeline/publish.py b/pipeline/publish.py index e34920d..2418ec7 100644 --- a/pipeline/publish.py +++ b/pipeline/publish.py @@ -67,13 +67,29 @@ def main(): drift_check(record) return only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else "" + # [R41] --files LIST.txt : publish exactly these basenames (one per line). `--only` is a + # substring match, which cannot express "these 110 assets from three libraries and nothing + # else". It matters because `_normalized/` is shared: other lanes stage rigs and clips in the + # same directory, and a blanket run would push whatever they happen to have half-written. + explicit = None + if "--files" in sys.argv: + listfile = sys.argv[sys.argv.index("--files") + 1] + explicit = {ln.strip() for ln in open(listfile) if ln.strip()} # Passwordless tailnet path (round-3 decision #4): GOD3_DEPOT overridden to an allowlisted # tailnet endpoint (e.g. http://100.94.195.115:8788) trusts this box — no cookie needed. tailnet = DEPOT != "https://digalot.fyi/3god" glbs = sorted(g for g in glob.glob(NORM + "/*.glb") - if os.path.basename(g) not in SKIP and (not only or only in os.path.basename(g))) + if os.path.basename(g) not in SKIP + and (explicit is None or os.path.basename(g) in explicit) + and (not only or only in os.path.basename(g))) if not glbs: print(f"no GLBs in {NORM} matching {only!r} — run normalize.py first"); sys.exit(1) + if explicit is not None: + missing = explicit - {os.path.basename(g) for g in glbs} + if missing: + print(f"ERROR: {len(missing)} name(s) in --files are not staged in {NORM}: " + f"{sorted(missing)[:5]}") + sys.exit(2) print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}" + (" [tailnet passwordless]" if tailnet else "")) if not DRY and not PW and not tailnet: diff --git a/pipeline/r41_3dstore.json b/pipeline/r41_3dstore.json new file mode 100644 index 0000000..fb2da11 --- /dev/null +++ b/pipeline/r41_3dstore.json @@ -0,0 +1,70 @@ +{ + "_comment": [ + "R41 §41.2 — the 3dstore import, BY NAME (integrator ruling 3: no wildcard copies).", + "Source: ~/Documents/3dstore/ (46 GLB). Every file below was measured with glb_stat.py on", + "2026-08-07, not taken from the handover note. 40 accepted, 6 rejected (see `reject`).", + "These assets need NO Blender pass: they are already metres, +Y up, base-origin, no Draco,", + "no textures, <=5k tris. The ONLY change is pipeline/merge_prims.py collapsing the", + "per-record-sleeve materials into one vertex-coloured primitive, because 16 materials on one", + "mesh is 16 draw calls and the interior margin is 162.", + "kind: bin|rack|counter|trolley|prop|arch|wall_mount — tells Lane C what it is standing on." + ], + "accept": [ + {"src": "Black Tub.glb", "id": "tub_black", "kind": "bin"}, + {"src": "Black Tub long.glb", "id": "tub_black_long", "kind": "bin"}, + {"src": "White Tub.glb", "id": "tub_white", "kind": "bin"}, + {"src": "White Tub long.glb", "id": "tub_white_long", "kind": "bin"}, + {"src": "Blue Crate.glb", "id": "crate_blue", "kind": "bin"}, + {"src": "Wooden Rack Bin.glb", "id": "rack_bin_wooden", "kind": "bin"}, + {"src": "endoffunk1.glb", "id": "bin_endoffunk", "kind": "bin"}, + {"src": "chiu-long-bin.glb", "id": "chiu_bin", "kind": "bin"}, + {"src": "chiu-long-bin-low.glb", "id": "chiu_bin_low", "kind": "bin"}, + {"src": "chiu-half-bin-sideways.glb", "id": "chiu_bin_half", "kind": "bin"}, + {"src": "chiu-long.glb", "id": "chiu_rack_long", "kind": "rack"}, + {"src": "chiu-short.glb", "id": "chiu_rack_short", "kind": "rack"}, + {"src": "mos-japhop.glb", "id": "mos_stack", "kind": "rack"}, + {"src": "mos-japhop-top.glb", "id": "mos_bin_top", "kind": "bin"}, + {"src": "mos-japhop-upper-mid.glb", "id": "mos_bin_upper_mid", "kind": "bin"}, + {"src": "mos-japhop-lower-mid.glb", "id": "mos_bin_lower_mid", "kind": "bin"}, + {"src": "mos-japhop-bottom.glb", "id": "mos_bin_bottom", "kind": "bin"}, + {"src": "big-4x4x4-01.glb", "id": "bin_big_4x4", "kind": "rack"}, + {"src": "tubs-4x2-wide.glb", "id": "tubs_4x2_wide", "kind": "rack"}, + {"src": "tubs-4x2-vertical.glb", "id": "tubs_4x2_vertical", "kind": "rack"}, + {"src": "white-tub-3x3.glb", "id": "tubs_3x3", "kind": "rack"}, + {"src": "white-tub-3x3-long.glb", "id": "tubs_3x3_long", "kind": "rack"}, + {"src": "3-top-4-low-tubs-on-shelf-long.glb", "id": "tubs_shelf_long", "kind": "rack"}, + {"src": "3-top-4-low-tubs-on-shelf-wide.glb", "id": "tubs_shelf_wide", "kind": "rack"}, + {"src": "wooden-rack-type-long.glb", "id": "wooden_rack_long", "kind": "rack"}, + {"src": "wooden-rack-type-wide.glb", "id": "wooden_rack_wide", "kind": "rack"}, + {"src": "library trolley.glb", "id": "library_trolley", "kind": "trolley"}, + {"src": "STATION 1.glb", "id": "counter_station", "kind": "counter"}, + {"src": "STATION 2.glb", "id": "counter_station_side", "kind": "counter"}, + {"src": "window-booth.glb", "id": "window_booth", "kind": "prop"}, + {"src": "vinylsleeve_v5.glb", "id": "vinyl_sleeve", "kind": "prop", + "rebase": true, + "_why": "the only 3dstore file NOT base-origin: minY -0.160 (centred, not seated)"}, + {"src": "AIR CON.glb", "id": "aircon_unit", "kind": "wall_mount"}, + {"src": "long guy.glb", "id": "shelf_board_long", "kind": "wall_mount"}, + {"src": "short guy.glb", "id": "shelf_board_short", "kind": "wall_mount"}, + {"src": "wall big.glb", "id": "wall_shelves_big", "kind": "wall_mount"}, + {"src": "wall long.glb", "id": "wall_shelves_long", "kind": "wall_mount"}, + {"src": "DOOR-front.glb", "id": "door_shop", "kind": "arch"}, + {"src": "GREY COLUMN-FRONT.glb", "id": "column_square", "kind": "arch"}, + {"src": "WHITE COLUMN.glb", "id": "column_wide", "kind": "arch"}, + {"src": "ROOF COLUMN.glb", "id": "beam_roof", "kind": "arch"} + ], + "reject": [ + {"src": "3dmrp.glb", + "why": "499,948 tris (100x the 5k prop budget) + a 4096px texture. Named in the tasking as the one reject; measured and confirmed."}, + {"src": "DOOR.glb", + "why": "0.010 m thick — a 1 cm plane, 12 tris. The solid door is DOOR-front.glb (0.19 m), which is accepted as door_shop."}, + {"src": "GREY COLUMN.glb", + "why": "0.010 m thick plane, 12 tris. The solid post is GREY COLUMN-FRONT.glb (0.22 x 0.22 m), accepted as column_square."}, + {"src": "Shelf Level.glb", + "why": "4.50 x 0.05 x 0.005 m — a 5 mm trim rail from the booth_room build script, not a placeable fitting."}, + {"src": "Shelf Level long.glb", + "why": "2.60 x 0.05 x 0.005 m — same 5 mm trim rail, short version."}, + {"src": "floor crate.glb", + "why": "0.34 x 0.34 x 0.002 m — a 2 mm floor decal. Would z-fight Lane C's interior floor; a real crate is crate_blue."} + ] +} diff --git a/pipeline/r41_djgear_batch.json b/pipeline/r41_djgear_batch.json new file mode 100644 index 0000000..07ae10b --- /dev/null +++ b/pipeline/r41_djgear_batch.json @@ -0,0 +1,250 @@ +[ + { + "src": "TURNTABLE-1200-RIGGED.glb", + "id": "deck_1200", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SL1.glb", + "id": "sl_deck_1", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SL2.glb", + "id": "sl_deck_2", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SL3.glb", + "id": "sl_deck_3", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SL4.glb", + "id": "sl_deck_4", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "TTM54i.glb", + "id": "mixer_ttm54i", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-05PRO.glb", + "id": "mixer_pmc05pro", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-06ProA.glb", + "id": "mixer_pmc06pro", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-07Pro.glb", + "id": "mixer_pmc07pro", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-08Pro.glb", + "id": "mixer_pmc08pro", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-10.glb", + "id": "mixer_pmc10", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-20.glb", + "id": "mixer_pmc20", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SH-DJ1200.glb", + "id": "mixer_shdj1200", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "MX-2200.glb", + "id": "mixer_mx2200", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMX-9000.glb", + "id": "mixer_pmx9000", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "TR-808.glb", + "id": "drum_808", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "TR-1000.glb", + "id": "drum_tr1000", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SP-1200.glb", + "id": "sampler_sp1200", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "MPC3000.glb", + "id": "sampler_mpc3000", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "S900.glb", + "id": "sampler_s900", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "HD25.glb", + "id": "headphones_hd25", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "M44-7.glb", + "id": "cart_m447", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "CONCORDE-PRO.glb", + "id": "cart_concorde", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "CONCORDE-CASE.glb", + "id": "cart_case", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "HEADSHELL-TECHNICS.glb", + "id": "headshell_technics", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "STANTON-500.glb", + "id": "cart_stanton", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "ZIP-100.glb", + "id": "zip_drive", + "category": "fit", + "mode": "static", + "height_m": 0, + "tris": 5000 + }, + { + "src": "TURNTABLE-1200-RIGGED.glb", + "id": "deck_1200_rigged", + "category": "fit", + "mode": "rigged", + "height_m": 0, + "tris": 5000 + }, + { + "src": "TTM54i.glb", + "id": "mixer_ttm54i_rigged", + "category": "fit", + "mode": "rigged", + "height_m": 0, + "tris": 5000 + }, + { + "src": "PMC-05PRO.glb", + "id": "mixer_pmc05pro_rigged", + "category": "fit", + "mode": "rigged", + "height_m": 0, + "tris": 5000 + }, + { + "src": "SH-DJ1200.glb", + "id": "mixer_shdj1200_rigged", + "category": "fit", + "mode": "rigged", + "height_m": 0, + "tris": 5000 + } +] \ No newline at end of file diff --git a/pipeline/r41_pub_batch.json b/pipeline/r41_pub_batch.json new file mode 100644 index 0000000..dd5576b --- /dev/null +++ b/pipeline/r41_pub_batch.json @@ -0,0 +1,423 @@ +[ + { + "src": "ashUrn.glb", + "id": "ash_urn", + "category": "fit", + "height_m": 0.75, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "ashtray.glb", + "id": "ashtray_post", + "category": "fit", + "height_m": 0.95, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "bainMarie.glb", + "id": "bain_marie", + "category": "fit", + "height_m": 0.95, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "balustrade.glb", + "id": "balustrade", + "category": "fit", + "height_m": 1.05, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "banquette.glb", + "id": "banquette", + "category": "fit", + "height_m": 0.85, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "barrier.glb", + "id": "crowd_barrier", + "category": "fit", + "height_m": 1.1, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "beerUmbrella.glb", + "id": "beer_umbrella", + "category": "fit", + "height_m": 2.3, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "binRound.glb", + "id": "bin_round", + "category": "fit", + "height_m": 0.75, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "bistroTable.glb", + "id": "bistro_table", + "category": "fit", + "height_m": 0.92, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "brokenBasin.glb", + "id": "broken_basin", + "category": "fit", + "height_m": 0.6, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "cableSnake.glb", + "id": "cable_snake", + "category": "fit", + "height_m": 0.2, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "canStack.glb", + "id": "can_stack", + "category": "fit", + "height_m": 0.45, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "cdj.glb", + "id": "cdj_deck", + "category": "fit", + "height_m": 0.15, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "champBucket.glb", + "id": "champ_bucket", + "category": "fit", + "height_m": 0.35, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "cocktailBar.glb", + "id": "cocktail_bar", + "category": "fit", + "height_m": 2.0, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "distroBox.glb", + "id": "distro_box", + "category": "fit", + "height_m": 1.2, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "djPlinth.glb", + "id": "dj_plinth", + "category": "fit", + "height_m": 1.0, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "fireExt.glb", + "id": "fire_ext", + "category": "fit", + "height_m": 0.6, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "firepit.glb", + "id": "firepit", + "category": "fit", + "height_m": 0.5, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "flightCase.glb", + "id": "flight_case", + "category": "fit", + "height_m": 0.75, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "jukebox.glb", + "id": "jukebox", + "category": "fit", + "height_m": 1.52, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1, + "yaw": 90 + }, + { + "src": "keg.glb", + "id": "keg", + "category": "fit", + "height_m": 0.6, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "loungeChair.glb", + "id": "lounge_chair", + "category": "fit", + "height_m": 0.8, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "marbleSink.glb", + "id": "marble_sink", + "category": "fit", + "height_m": 0.85, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "oldCouch.glb", + "id": "old_couch", + "category": "fit", + "height_m": 0.85, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "pallet.glb", + "id": "pallet", + "category": "fit", + "height_m": 0.2, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "picnicTable.glb", + "id": "picnic_table", + "category": "fit", + "height_m": 0.75, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "pieWarmer.glb", + "id": "pie_warmer", + "category": "fit", + "height_m": 0.55, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "pillar.glb", + "id": "concrete_plinth", + "category": "fit", + "height_m": 1.2, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "planter.glb", + "id": "planter", + "category": "fit", + "height_m": 0.55, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "plywoodBar.glb", + "id": "plywood_bar", + "category": "fit", + "height_m": 1.05, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "pokie.glb", + "id": "pokie", + "category": "fit", + "height_m": 1.7, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "raffleDrum.glb", + "id": "raffle_drum", + "category": "fit", + "height_m": 0.9, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "ropePost.glb", + "id": "rope_post", + "category": "fit", + "height_m": 1.0, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "sandwichBoard.glb", + "id": "sandwich_board", + "category": "fit", + "height_m": 1.0, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "smokeMachine.glb", + "id": "smoke_machine", + "category": "fit", + "height_m": 0.38, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "stackF1.glb", + "id": "speaker_stack", + "category": "fit", + "height_m": 1.8, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "strobe.glb", + "id": "strobe_light", + "category": "fit", + "height_m": 0.4, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "subBass.glb", + "id": "sub_bass", + "category": "fit", + "height_m": 0.7, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "topiary.glb", + "id": "topiary", + "category": "fit", + "height_m": 1.2, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "trough.glb", + "id": "urinal_trough", + "category": "fit", + "height_m": 0.7, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + }, + { + "src": "wheelieBin.glb", + "id": "wheelie_bin", + "category": "fit", + "height_m": 1.1, + "tris": 5000, + "remesh": true, + "voxel": 0.011111111111111112, + "shell_thickness": -1 + } +] \ No newline at end of file diff --git a/pipeline/r41_stage_3dstore.py b/pipeline/r41_stage_3dstore.py new file mode 100644 index 0000000..9dfb9c2 --- /dev/null +++ b/pipeline/r41_stage_3dstore.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Stage the named 3dstore fittings into the repo (R41 §41.2, step 1). + +No Blender. The source assets already satisfy the house GLB law measured end to end — metres, ++Y up, base-origin, no Draco, no textures, <=5k tris — so putting them through `normalize.py` +would re-join, re-triangulate and re-export geometry that is already correct, which is three +chances to move something for no gain. The two things they DO need are done here: + + 1. pipeline/merge_prims.py — collapse the per-sleeve materials to one vertex-coloured + primitive. 16 materials on one mesh is 16 draw calls; the interior margin is 162. + 2. base-origin repair for the one file that needs it (vinylsleeve_v5, minY -0.160). + +Reads pipeline/r41_3dstore.json (the by-name allow-list, ruling 3). Writes +pipeline/_normalized/procity_fit__01.glb, mirrors to web/assets/models/, and emits +pipeline/_r41_3dstore_results.json with measured footprints/heights for build_manifest.py. + + python3 pipeline/r41_stage_3dstore.py [--src ~/Documents/3dstore] [--dry-run] +""" +import json, os, shutil, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +sys.path.insert(0, HERE) +import merge_prims as MP +import glb_stat as GS + +SPEC = os.path.join(HERE, "r41_3dstore.json") +NORM = os.path.join(ROOT, "pipeline", "_normalized") +MODELS = os.path.join(ROOT, "web", "assets", "models") +RESULTS = os.path.join(HERE, "_r41_3dstore_results.json") + + +def world_bbox(path): + """World-space min/max, node transforms respected (glb_stat's own walk).""" + g, _ = GS._read_glb(path) + acc, meshes, nodes = g.get("accessors", []), g.get("meshes", []), g.get("nodes", []) + mn = [float("inf")] * 3 + mx = [float("-inf")] * 3 + scenes = g.get("scenes") or [{"nodes": list(range(len(nodes)))}] + + def walk(ni, M): + n = nodes[ni] + W = GS._mat_mul(M, GS._node_matrix(n)) + if "mesh" in n: + for p in meshes[n["mesh"]].get("primitives", []): + pos = p.get("attributes", {}).get("POSITION") + if pos is None: + continue + a = acc[pos] + if "min" not in a: + continue + for cx in (a["min"][0], a["max"][0]): + for cy in (a["min"][1], a["max"][1]): + for cz in (a["min"][2], a["max"][2]): + w = GS._xform(W, [cx, cy, cz]) + for k in range(3): + mn[k] = min(mn[k], w[k]); mx[k] = max(mx[k], w[k]) + for c in n.get("children", []): + walk(c, W) + + I = [[1 if i == j else 0 for j in range(4)] for i in range(4)] + for r in scenes[g.get("scene", 0)].get("nodes", []): + walk(r, I) + return mn, mx + + +def rebase(path): + """Seat the asset on y=0 and centre it in X/Z by wrapping the scene roots in one node. + + Wrapping rather than editing each root's TRS keeps every original transform intact and is + exactly reversible — the asset is correct, only its pivot was authored at the centre. + """ + g, binc = MP.read_glb(path) + mn, mx = world_bbox(path) + dx = -(mn[0] + mx[0]) / 2.0 + dy = -mn[1] + dz = -(mn[2] + mx[2]) / 2.0 + if abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6: + return False, (0, 0, 0) + scenes = g.setdefault("scenes", [{"nodes": list(range(len(g.get("nodes", []))))}]) + si = g.get("scene", 0) + roots = scenes[si].get("nodes", []) + g.setdefault("nodes", []).append({"name": "procity_base_origin", + "translation": [dx, dy, dz], "children": list(roots)}) + scenes[si]["nodes"] = [len(g["nodes"]) - 1] + MP.write_glb(path, g, binc) + return True, (round(dx, 4), round(dy, 4), round(dz, 4)) + + +def main(): + args = sys.argv[1:] + dry = "--dry-run" in args + src_dir = os.path.expanduser("~/Documents/3dstore") + if "--src" in args: + src_dir = os.path.expanduser(args[args.index("--src") + 1]) + spec = json.load(open(SPEC)) + + os.makedirs(NORM, exist_ok=True) + os.makedirs(MODELS, exist_ok=True) + results, errors = [], [] + + print(f"{'file':34} {'tris':>6} {'draws':>11} {'w x d x h (m)':>22} kind") + print("-" * 92) + for e in spec["accept"]: + s = os.path.join(src_dir, e["src"]) + if not os.path.isfile(s): + errors.append({"src": e["src"], "error": "source missing"}) + print(f"MISSING {e['src']}") + continue + name = f"procity_fit_{e['id']}_01.glb" + out = os.path.join(NORM, name) + try: + before = MP.check(s) + if dry: + print(f"{name:34} {before['tris']:>6} {before['draws']:>4} -> ? (dry-run)") + continue + # merge() mutates the glTF dict in place and returns the rebuilt BIN chunk + gltf, binc = MP.read_glb(s) + _rep, newbin = MP.merge(gltf, binc) + MP.write_glb(out, gltf, newbin) + moved = (False, (0, 0, 0)) + if e.get("rebase"): + moved = rebase(out) + mn, mx = world_bbox(out) + after = MP.check(out) + if after["tris"] != before["tris"]: + errors.append({"src": e["src"], "error": + f"tri drift {before['tris']} -> {after['tris']}"}) + w, h, d = round(mx[0] - mn[0], 3), round(mx[1] - mn[1], 3), round(mx[2] - mn[2], 3) + if abs(mn[1]) > 0.01: + errors.append({"src": e["src"], "error": f"not base-origin: minY={mn[1]:.3f}"}) + shutil.copy2(out, os.path.join(MODELS, name)) + results.append({ + "src": e["src"], "id": e["id"], "kind": e["kind"], "file": name, + "tris": after["tris"], "draws_before": before["draws"], + "draws_after": after["draws"], "materials_before": before["materials"], + # floor at 1 cm: a 3 mm record sleeve rounds to 0.00 at 2 dp, and the validator + # (rightly) calls a zero-width footprint insane. The number Lane C places on must + # stay a positive metre value. + "footprint": [max(round(w, 2), 0.01), max(round(d, 2), 0.01)], + "height": max(round(h, 3), 0.01), + "size_m": [w, d, h], "rebased": moved[0], "rebase_delta": moved[1], + "thumb": f"thumbs/{name[:-4]}.png", + }) + print(f"{name:34} {after['tris']:>6} {before['draws']:>4} -> {after['draws']:<4} " + f"{w:>7.2f} x{d:>6.2f} x{h:>6.2f} {e['kind']}" + + (" [rebased]" if moved[0] else "")) + except Exception as ex: + errors.append({"src": e["src"], "error": str(ex)}) + print(f"ERR {e['src']}: {ex}") + + if dry: + return 0 + db = sum(r["draws_before"] for r in results) + da = sum(r["draws_after"] for r in results) + json.dump({"results": results, "errors": errors, "rejects": spec["reject"], + "draws_before_total": db, "draws_after_total": da}, + open(RESULTS, "w"), indent=2) + print("-" * 92) + print(f"staged {len(results)}/{len(spec['accept'])} rejected {len(spec['reject'])} " + f"errors {len(errors)}") + print(f"DRAW COST across the set: {db} -> {da} ({db - da} draws removed, " + f"{100 * (db - da) / max(db, 1):.0f}%)") + for e in errors: + print(f" ERR {e}") + print(f"-> {RESULTS}") + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/pipeline/rig_join.py b/pipeline/rig_join.py new file mode 100644 index 0000000..c23c763 --- /dev/null +++ b/pipeline/rig_join.py @@ -0,0 +1,263 @@ +"""PROCITY DJ-gear normalizer — collapse draws WITHOUT breaking the named-node contract. + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python pipeline/rig_join.py -- BATCH.json OUT_DIR [THUMB_DIR] + +WHY (R41 §41.2, and the tension nobody had measured) +---------------------------------------------------- +Integrator ruling 5 forbids `gltf-transform optimize` on this library: it collapsed a 696-node +mixer to 25 nodes and deleted `Fader_CROSS`. True — and the reason it was reached for is also +true. Measured on ultra's 60 exports, these models cost, in DRAW CALLS: + + TR-1000 2145 · MPC-XL 1319 · MP2016 1201 · TR-808 1092 · PMC-08Pro 751 · TTM54i 659 + +The interior draw margin is **162**. One PMC-08Pro is 4.6x the entire budget for a room. So the +library as shipped is unplaceable, and the fix that makes it placeable is the one that is banned — +because both "join everything" and "join nothing" are wrong. + +The third option is to join SELECTIVELY. Everything that is not a control, and everything under a +control that is not the control itself, is scenery: it merges into one body mesh. Each node named +in the contract keeps its own object, its own name, and its own pivot, so it can still be driven: + + Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP + +That is 1 body + at most 5 controls = **<=6 draws** for a fully drivable deck, instead of 751 for +one that nobody can afford to place. `mode:"static"` drops the controls into the body too (1 draw) +for the dressing copies, which is what most of a record shop actually needs. + +The contract is VERIFIED, not assumed: every model is scanned for contract nodes before and after, +and `_rig_results.json` records both lists so a deletion can never pass silently. + +BATCH.json: [ {"src":"TTM54i.glb", "id":"mixer_ttm54i", "category":"fit", + "mode":"rigged"|"static", "height_m":0.10, "tris":5000, "yaw":0} ] +""" +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_rig" +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/dj-gear/glb")) +os.makedirs(OUT_DIR, exist_ok=True) +os.makedirs(THUMB_DIR, exist_ok=True) + +CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"] + + +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 world_bounds(objs): + lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3) + for o in objs: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + for k in range(3): + lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k]) + return lo, hi + + +def descendants(o): + out = [] + for c in o.children: + out.append(c) + out.extend(descendants(c)) + return out + + +def join_into(active, others): + """Join `others` into `active`, keeping active's origin (so a control keeps its pivot).""" + others = [o for o in others if o.type == 'MESH' and o is not active] + bpy.ops.object.select_all(action='DESELECT') + active.select_set(True) + for o in others: + o.select_set(True) + bpy.context.view_layer.objects.active = active + if others: + bpy.ops.object.join() + return active + + +def tris_of(o): + return len(o.data.polygons) + + +def decimate_to(o, budget): + bpy.ops.object.select_all(action='DESELECT') + o.select_set(True) + bpy.context.view_layer.objects.active = o + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.quads_convert_to_tris() + bpy.ops.object.mode_set(mode='OBJECT') + n = tris_of(o) + passes = 0 + while n > budget * 1.05 and passes < 6: + m = o.modifiers.new("dec", 'DECIMATE') + m.ratio = max(0.01, budget / n) + bpy.ops.object.modifier_apply(modifier=m.name) + new = tris_of(o) + passes += 1 + if new >= n * 0.97: + n = new + break + n = new + return n + + +def scan_contract(): + names = {o.name for o in bpy.data.objects} + return [c for c in CONTRACT if c in names] + + +def process(spec): + wipe() + src = spec["src"] if os.path.isabs(spec["src"]) else os.path.join(ROOT, spec["src"]) + bpy.ops.import_scene.gltf(filepath=src) + + before = scan_contract() + all_mesh = [o for o in bpy.data.objects if o.type == 'MESH'] + tri_before = sum(tris_of(o) for o in all_mesh) + draws_before = len(all_mesh) + mode = spec.get("mode", "static") + budget = spec.get("tris", 5000) + + keep, ctrl_meshes = [], [] + if mode == "rigged": + # Each contract node absorbs its own subtree and keeps its pivot; scenery goes to the body. + # + # The named node stays whatever the source made it. In this library the controls are + # EMPTIES with the cap/stem/rib parented underneath (dj-gear/README.md: "moving the bare + # node moves the whole fader"), so the empty IS the contract and the pivot. An earlier + # pass promoted the biggest mesh child to carry the name and deleted the empty — which + # silently dropped the empty's translation and flung Btn_STARTSTOP to the origin, making + # the rigged deck 0.28 m tall against the static copy's 0.12 m. Keep the empty. + for name in before: + o = bpy.data.objects.get(name) + if not o: + continue + subtree = [d for d in descendants(o) if d.type == 'MESH'] + if o.type == 'MESH': + join_into(o, subtree) + ctrl_meshes.append(o) + else: + if not subtree: + continue + host = max(subtree, key=tris_of) + join_into(host, [s for s in subtree if s is not host]) + ctrl_meshes.append(host) + # detach from the source hierarchy, keeping the world placement + bpy.ops.object.select_all(action='DESELECT') + o.select_set(True) + bpy.context.view_layer.objects.active = o + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + keep.append(o) + + keep_names = {o.name for o in keep} | {o.name for o in ctrl_meshes} + body_parts = [o for o in bpy.data.objects + if o.type == 'MESH' and o.name not in keep_names] + if not body_parts: + raise ValueError("no body geometry left") + body = max(body_parts, key=tris_of) + body = join_into(body, body_parts) + body.name = "Body" + + # controls become children of the body so one transform moves the whole unit + for o in keep: + bpy.ops.object.select_all(action='DESELECT') + o.select_set(True) + body.select_set(True) + bpy.context.view_layer.objects.active = body + bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) + + ctrl_tris = sum(tris_of(o) for o in ctrl_meshes) + decimate_to(body, max(600, budget - ctrl_tris)) + for o in ctrl_meshes: # keep controls readable, but bounded + if tris_of(o) > 400: + decimate_to(o, 400) + + # orientation, height-normalise, base-origin — on the whole unit + objs = [body] + keep + ctrl_meshes + bpy.ops.object.select_all(action='DESELECT') + for o in objs: + o.select_set(True) + bpy.context.view_layer.objects.active = body + if spec.get("yaw"): + body.rotation_mode = 'XYZ' + body.rotation_euler[2] += math.radians(spec["yaw"]) + bpy.context.view_layer.update() + + lo, hi = world_bounds(objs) + h = spec.get("height_m", 0) + if h and h > 0: + s = h / max(hi.z - lo.z, 1e-6) + body.scale *= s + bpy.context.view_layer.update() + lo, hi = world_bounds(objs) + body.location -= Vector(((lo.x + hi.x) / 2, (lo.y + hi.y) / 2, lo.z)) + bpy.context.view_layer.update() + lo, hi = world_bounds(objs) + + for img in bpy.data.images: + if img.size[0] > 1024 or img.size[1] > 1024: + sc = 1024 / max(img.size) + img.scale(max(1, int(img.size[0] * sc)), max(1, int(img.size[1] * sc))) + + name = f"procity_{spec['category']}_{spec['id']}_01.glb" + out = os.path.join(OUT_DIR, name) + bpy.ops.object.select_all(action='DESELECT') + for o in objs: + o.select_set(True) + bpy.context.view_layer.objects.active = body + bpy.ops.export_scene.gltf(filepath=out, export_format='GLB', export_yup=True, + use_selection=True, export_apply=False, + export_image_format='WEBP', export_image_quality=85) + + after = scan_contract() + return { + "src": spec["src"], "id": spec["id"], "file": name, "mode": mode, + "tri_before": tri_before, "tri_after": sum(tris_of(o) for o in objs if o.type == 'MESH'), + "draws_before": draws_before, + "draws_after": sum(1 for o in objs if o.type == 'MESH'), + "contract_before": before, "contract_after": after, + "contract_ok": set(before) == set(after) if mode == "rigged" else True, + "controls": sorted(o.name for o in keep), + "size_m": [round(hi.x - lo.x, 3), round(hi.z - lo.z, 3), round(hi.y - lo.y, 3)], + "footprint": [max(round(hi.x - lo.x, 2), 0.01), max(round(hi.y - lo.y, 2), 0.01)], + "height": max(round(hi.z - lo.z, 3), 0.01), + "out": out, "thumb": f"thumbs/{name[:-4]}.png", + } + + +def main(): + specs = json.load(open(BATCH)) + results, errors = [], [] + for spec in specs: + try: + r = process(spec) + results.append(r) + flag = "" if r["contract_ok"] else " *** CONTRACT BROKEN ***" + print(f"OK {r['file']:44} {r['mode']:6} tris {r['tri_before']:>7}->{r['tri_after']:>5} " + f"draws {r['draws_before']:>5}->{r['draws_after']:<3} " + f"contract {','.join(r['contract_after']) or '-'}{flag}") + except Exception as e: + import traceback + traceback.print_exc() + errors.append({"id": spec.get("id"), "src": spec.get("src"), "error": str(e)}) + print(f"ERR {spec.get('src')}: {e}") + json.dump({"results": results, "errors": errors}, + open(os.path.join(OUT_DIR, "_rig_results.json"), "w"), indent=2) + broken = [r for r in results if not r["contract_ok"]] + print(f"\nRIG_DONE ok={len(results)} err={len(errors)} contract_broken={len(broken)} -> {OUT_DIR}") + + +main() diff --git a/pipeline/thumbs.py b/pipeline/thumbs.py new file mode 100644 index 0000000..e22e9cc --- /dev/null +++ b/pipeline/thumbs.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Render a 256px manifest thumbnail for each GLB — many per Blender session. + +Separate from `normalize.py`'s `render_thumb` on purpose: that one is part of the normalize pass +and shades with BLENDER_WORKBENCH `color_type='TEXTURE'`, which renders a **vertex-coloured, +material-less** mesh flat grey. Two of R41's three libraries are exactly that (the 3dstore +fittings after `merge_prims.py`, and the `art_incoming` props, which carry COLOR_0 and no material +block at all), so their thumbnails would have shipped colourless. This renders in EEVEE off the +real material graph, so what Lane C sees in the thumb is what the game draws. + + BL=/Applications/Blender.app/Contents/MacOS/Blender + "$BL" --background --python pipeline/thumbs.py -- OUT_DIR GLB [GLB ...] +""" +import bpy, sys, os, math +from mathutils import Vector + +ARGV = sys.argv[sys.argv.index("--") + 1:] +OUT_DIR = ARGV[0] +FILES = [f for f in ARGV[1:] if f.lower().endswith(".glb")] +RES = 256 +os.makedirs(OUT_DIR, exist_ok=True) + + +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(objs): + lo = Vector((1e9,) * 3); hi = Vector((-1e9,) * 3) + for o in objs: + if o.type != 'MESH': + continue + for c in o.bound_box: + w = o.matrix_world @ Vector(c) + for k in range(3): + lo[k] = min(lo[k], w[k]); hi[k] = max(hi[k], w[k]) + return lo, hi + + +def setup_world(): + w = bpy.data.worlds.get("tw") or bpy.data.worlds.new("tw") + bpy.context.scene.world = w + w.use_nodes = True + w.node_tree.nodes["Background"].inputs[0].default_value = (0.85, 0.87, 0.92, 1) + w.node_tree.nodes["Background"].inputs[1].default_value = 1.6 + + +def main(): + scn = bpy.context.scene + for e in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'): + try: + scn.render.engine = e + break + except TypeError: + continue + scn.render.film_transparent = True + scn.render.resolution_x = scn.render.resolution_y = RES + scn.render.image_settings.file_format = 'PNG' + scn.render.image_settings.color_mode = 'RGBA' + scn.view_settings.view_transform = 'Standard' + + ok = 0 + for path in FILES: + wipe() + setup_world() + try: + bpy.ops.import_scene.gltf(filepath=path) + except Exception as ex: + print(f"IMPORT FAIL {path}: {ex}") + continue + meshes = [o for o in bpy.data.objects if o.type == 'MESH'] + if not meshes: + print(f"NO MESH {path}") + continue + lo, hi = bounds(meshes) + ctr = (lo + hi) / 2 + size = max((hi - lo).x, (hi - lo).y, (hi - lo).z) or 1.0 + + bpy.ops.object.light_add(type='AREA', + location=(ctr.x + size, ctr.y - size, hi.z + size * 1.2)) + L = bpy.context.object + L.data.energy = 260 * size * size + 40 + L.data.size = size * 2.5 + L.rotation_euler = (math.radians(40), 0, math.radians(35)) + bpy.ops.object.light_add(type='AREA', + location=(ctr.x - size, ctr.y - size * 1.4, ctr.z)) + L2 = bpy.context.object + L2.data.energy = 90 * size * size + 15 + L2.data.size = size * 3 + + bpy.ops.object.camera_add() + cam = bpy.context.object + scn.camera = cam + cam.data.type = 'ORTHO' + cam.data.ortho_scale = size * 1.5 + d = Vector((0.82, -1.0, 0.62)).normalized() + cam.location = ctr + d * (size * 4 + 2) + cam.rotation_euler = (-d).to_track_quat('-Z', 'Y').to_euler() + cam.data.clip_end = size * 20 + 50 + + scn.render.filepath = os.path.join(OUT_DIR, os.path.basename(path)[:-4] + ".png") + bpy.ops.render.render(write_still=True) + ok += 1 + print(f"THUMBS_DONE {ok}/{len(FILES)} -> {OUT_DIR}") + + +main() diff --git a/web/assets/manifest.json b/web/assets/manifest.json index 7f3c6ec..c16c52f 100644 --- a/web/assets/manifest.json +++ b/web/assets/manifest.json @@ -840,6 +840,1832 @@ "height": 1.702, "thumb": "thumbs/procity_fit_mic_stand_01.png" }, + "tub_black": { + "file": "procity_fit_tub_black_01.glb", + "footprint": [ + 0.36, + 0.53 + ], + "height": 0.229, + "thumb": "thumbs/procity_fit_tub_black_01.png", + "kind": "bin", + "tris": 1020, + "draws": 1, + "source": "Black Tub.glb" + }, + "tub_black_long": { + "file": "procity_fit_tub_black_long_01.glb", + "footprint": [ + 0.53, + 0.36 + ], + "height": 0.229, + "thumb": "thumbs/procity_fit_tub_black_long_01.png", + "kind": "bin", + "tris": 1536, + "draws": 1, + "source": "Black Tub long.glb" + }, + "tub_white": { + "file": "procity_fit_tub_white_01.glb", + "footprint": [ + 0.34, + 0.53 + ], + "height": 0.229, + "thumb": "thumbs/procity_fit_tub_white_01.png", + "kind": "bin", + "tris": 972, + "draws": 1, + "source": "White Tub.glb" + }, + "tub_white_long": { + "file": "procity_fit_tub_white_long_01.glb", + "footprint": [ + 0.53, + 0.36 + ], + "height": 0.229, + "thumb": "thumbs/procity_fit_tub_white_long_01.png", + "kind": "bin", + "tris": 1536, + "draws": 1, + "source": "White Tub long.glb" + }, + "crate_blue": { + "file": "procity_fit_crate_blue_01.glb", + "footprint": [ + 0.34, + 0.3 + ], + "height": 0.322, + "thumb": "thumbs/procity_fit_crate_blue_01.png", + "kind": "bin", + "tris": 936, + "draws": 1, + "source": "Blue Crate.glb" + }, + "rack_bin_wooden": { + "file": "procity_fit_rack_bin_wooden_01.glb", + "footprint": [ + 0.34, + 0.4 + ], + "height": 0.338, + "thumb": "thumbs/procity_fit_rack_bin_wooden_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "Wooden Rack Bin.glb" + }, + "bin_endoffunk": { + "file": "procity_fit_bin_endoffunk_01.glb", + "footprint": [ + 0.36, + 0.34 + ], + "height": 0.34, + "thumb": "thumbs/procity_fit_bin_endoffunk_01.png", + "kind": "bin", + "tris": 972, + "draws": 1, + "source": "endoffunk1.glb" + }, + "chiu_bin": { + "file": "procity_fit_chiu_bin_01.glb", + "footprint": [ + 0.37, + 0.43 + ], + "height": 0.35, + "thumb": "thumbs/procity_fit_chiu_bin_01.png", + "kind": "bin", + "tris": 996, + "draws": 1, + "source": "chiu-long-bin.glb" + }, + "chiu_bin_low": { + "file": "procity_fit_chiu_bin_low_01.glb", + "footprint": [ + 0.34, + 0.43 + ], + "height": 0.35, + "thumb": "thumbs/procity_fit_chiu_bin_low_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "chiu-long-bin-low.glb" + }, + "chiu_bin_half": { + "file": "procity_fit_chiu_bin_half_01.glb", + "footprint": [ + 0.24, + 0.43 + ], + "height": 0.35, + "thumb": "thumbs/procity_fit_chiu_bin_half_01.png", + "kind": "bin", + "tris": 600, + "draws": 1, + "source": "chiu-half-bin-sideways.glb" + }, + "chiu_rack_long": { + "file": "procity_fit_chiu_rack_long_01.glb", + "footprint": [ + 1.88, + 0.43 + ], + "height": 0.795, + "thumb": "thumbs/procity_fit_chiu_rack_long_01.png", + "kind": "rack", + "tris": 300, + "draws": 1, + "source": "chiu-long.glb" + }, + "chiu_rack_short": { + "file": "procity_fit_chiu_rack_short_01.glb", + "footprint": [ + 1.38, + 0.43 + ], + "height": 0.795, + "thumb": "thumbs/procity_fit_chiu_rack_short_01.png", + "kind": "rack", + "tris": 276, + "draws": 1, + "source": "chiu-short.glb" + }, + "mos_stack": { + "file": "procity_fit_mos_stack_01.glb", + "footprint": [ + 0.37, + 0.4 + ], + "height": 1.275, + "thumb": "thumbs/procity_fit_mos_stack_01.png", + "kind": "rack", + "tris": 204, + "draws": 1, + "source": "mos-japhop.glb" + }, + "mos_bin_top": { + "file": "procity_fit_mos_bin_top_01.glb", + "footprint": [ + 0.34, + 0.2 + ], + "height": 0.34, + "thumb": "thumbs/procity_fit_mos_bin_top_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "mos-japhop-top.glb" + }, + "mos_bin_upper_mid": { + "file": "procity_fit_mos_bin_upper_mid_01.glb", + "footprint": [ + 0.34, + 0.26 + ], + "height": 0.34, + "thumb": "thumbs/procity_fit_mos_bin_upper_mid_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "mos-japhop-upper-mid.glb" + }, + "mos_bin_lower_mid": { + "file": "procity_fit_mos_bin_lower_mid_01.glb", + "footprint": [ + 0.34, + 0.36 + ], + "height": 0.34, + "thumb": "thumbs/procity_fit_mos_bin_lower_mid_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "mos-japhop-lower-mid.glb" + }, + "mos_bin_bottom": { + "file": "procity_fit_mos_bin_bottom_01.glb", + "footprint": [ + 0.34, + 0.4 + ], + "height": 0.34, + "thumb": "thumbs/procity_fit_mos_bin_bottom_01.png", + "kind": "bin", + "tris": 912, + "draws": 1, + "source": "mos-japhop-bottom.glb" + }, + "bin_big_4x4": { + "file": "procity_fit_bin_big_4x4_01.glb", + "footprint": [ + 0.59, + 1.5 + ], + "height": 1.255, + "thumb": "thumbs/procity_fit_bin_big_4x4_01.png", + "kind": "rack", + "tris": 288, + "draws": 1, + "source": "big-4x4x4-01.glb" + }, + "tubs_4x2_wide": { + "file": "procity_fit_tubs_4x2_wide_01.glb", + "footprint": [ + 1.56, + 0.53 + ], + "height": 0.475, + "thumb": "thumbs/procity_fit_tubs_4x2_wide_01.png", + "kind": "rack", + "tris": 240, + "draws": 1, + "source": "tubs-4x2-wide.glb" + }, + "tubs_4x2_vertical": { + "file": "procity_fit_tubs_4x2_vertical_01.glb", + "footprint": [ + 0.53, + 1.56 + ], + "height": 0.475, + "thumb": "thumbs/procity_fit_tubs_4x2_vertical_01.png", + "kind": "rack", + "tris": 240, + "draws": 1, + "source": "tubs-4x2-vertical.glb" + }, + "tubs_3x3": { + "file": "procity_fit_tubs_3x3_01.glb", + "footprint": [ + 0.53, + 1.3 + ], + "height": 0.555, + "thumb": "thumbs/procity_fit_tubs_3x3_01.png", + "kind": "rack", + "tris": 168, + "draws": 1, + "source": "white-tub-3x3.glb" + }, + "tubs_3x3_long": { + "file": "procity_fit_tubs_3x3_long_01.glb", + "footprint": [ + 1.3, + 0.53 + ], + "height": 0.51, + "thumb": "thumbs/procity_fit_tubs_3x3_long_01.png", + "kind": "rack", + "tris": 168, + "draws": 1, + "source": "white-tub-3x3-long.glb" + }, + "tubs_shelf_long": { + "file": "procity_fit_tubs_shelf_long_01.glb", + "footprint": [ + 1.56, + 0.55 + ], + "height": 0.565, + "thumb": "thumbs/procity_fit_tubs_shelf_long_01.png", + "kind": "rack", + "tris": 228, + "draws": 1, + "source": "3-top-4-low-tubs-on-shelf-long.glb" + }, + "tubs_shelf_wide": { + "file": "procity_fit_tubs_shelf_wide_01.glb", + "footprint": [ + 0.55, + 1.56 + ], + "height": 0.575, + "thumb": "thumbs/procity_fit_tubs_shelf_wide_01.png", + "kind": "rack", + "tris": 228, + "draws": 1, + "source": "3-top-4-low-tubs-on-shelf-wide.glb" + }, + "wooden_rack_long": { + "file": "procity_fit_wooden_rack_long_01.glb", + "footprint": [ + 0.78, + 1.8 + ], + "height": 0.81, + "thumb": "thumbs/procity_fit_wooden_rack_long_01.png", + "kind": "rack", + "tris": 384, + "draws": 1, + "source": "wooden-rack-type-long.glb" + }, + "wooden_rack_wide": { + "file": "procity_fit_wooden_rack_wide_01.glb", + "footprint": [ + 1.8, + 0.78 + ], + "height": 0.93, + "thumb": "thumbs/procity_fit_wooden_rack_wide_01.png", + "kind": "rack", + "tris": 384, + "draws": 1, + "source": "wooden-rack-type-wide.glb" + }, + "library_trolley": { + "file": "procity_fit_library_trolley_01.glb", + "footprint": [ + 0.41, + 0.88 + ], + "height": 0.74, + "thumb": "thumbs/procity_fit_library_trolley_01.png", + "kind": "trolley", + "tris": 264, + "draws": 1, + "source": "library trolley.glb" + }, + "counter_station": { + "file": "procity_fit_counter_station_01.glb", + "footprint": [ + 0.88, + 0.5 + ], + "height": 0.87, + "thumb": "thumbs/procity_fit_counter_station_01.png", + "kind": "counter", + "tris": 192, + "draws": 1, + "source": "STATION 1.glb" + }, + "counter_station_side": { + "file": "procity_fit_counter_station_side_01.glb", + "footprint": [ + 0.5, + 0.88 + ], + "height": 0.87, + "thumb": "thumbs/procity_fit_counter_station_side_01.png", + "kind": "counter", + "tris": 192, + "draws": 1, + "source": "STATION 2.glb" + }, + "window_booth": { + "file": "procity_fit_window_booth_01.glb", + "footprint": [ + 1.85, + 0.09 + ], + "height": 0.72, + "thumb": "thumbs/procity_fit_window_booth_01.png", + "kind": "prop", + "tris": 144, + "draws": 1, + "source": "window-booth.glb" + }, + "vinyl_sleeve": { + "file": "procity_fit_vinyl_sleeve_01.glb", + "footprint": [ + 0.32, + 0.01 + ], + "height": 0.32, + "thumb": "thumbs/procity_fit_vinyl_sleeve_01.png", + "kind": "prop", + "tris": 180, + "draws": 1, + "source": "vinylsleeve_v5.glb" + }, + "aircon_unit": { + "file": "procity_fit_aircon_unit_01.glb", + "footprint": [ + 1.15, + 0.25 + ], + "height": 0.35, + "thumb": "thumbs/procity_fit_aircon_unit_01.png", + "kind": "wall_mount", + "tris": 12, + "draws": 1, + "source": "AIR CON.glb" + }, + "shelf_board_long": { + "file": "procity_fit_shelf_board_long_01.glb", + "footprint": [ + 4.6, + 0.48 + ], + "height": 0.03, + "thumb": "thumbs/procity_fit_shelf_board_long_01.png", + "kind": "wall_mount", + "tris": 12, + "draws": 1, + "source": "long guy.glb" + }, + "shelf_board_short": { + "file": "procity_fit_shelf_board_short_01.glb", + "footprint": [ + 0.48, + 1.95 + ], + "height": 0.03, + "thumb": "thumbs/procity_fit_shelf_board_short_01.png", + "kind": "wall_mount", + "tris": 12, + "draws": 1, + "source": "short guy.glb" + }, + "wall_shelves_big": { + "file": "procity_fit_wall_shelves_big_01.glb", + "footprint": [ + 4.65, + 0.05 + ], + "height": 0.76, + "thumb": "thumbs/procity_fit_wall_shelves_big_01.png", + "kind": "wall_mount", + "tris": 36, + "draws": 1, + "source": "wall big.glb" + }, + "wall_shelves_long": { + "file": "procity_fit_wall_shelves_long_01.glb", + "footprint": [ + 2.6, + 0.05 + ], + "height": 1.1, + "thumb": "thumbs/procity_fit_wall_shelves_long_01.png", + "kind": "wall_mount", + "tris": 48, + "draws": 1, + "source": "wall long.glb" + }, + "door_shop": { + "file": "procity_fit_door_shop_01.glb", + "footprint": [ + 1.7, + 0.19 + ], + "height": 2.3, + "thumb": "thumbs/procity_fit_door_shop_01.png", + "kind": "arch", + "tris": 144, + "draws": 1, + "source": "DOOR-front.glb" + }, + "column_square": { + "file": "procity_fit_column_square_01.glb", + "footprint": [ + 0.22, + 0.22 + ], + "height": 2.6, + "thumb": "thumbs/procity_fit_column_square_01.png", + "kind": "arch", + "tris": 12, + "draws": 1, + "source": "GREY COLUMN-FRONT.glb" + }, + "column_wide": { + "file": "procity_fit_column_wide_01.glb", + "footprint": [ + 0.36, + 0.46 + ], + "height": 2.6, + "thumb": "thumbs/procity_fit_column_wide_01.png", + "kind": "arch", + "tris": 12, + "draws": 1, + "source": "WHITE COLUMN.glb" + }, + "beam_roof": { + "file": "procity_fit_beam_roof_01.glb", + "footprint": [ + 6.5, + 0.32 + ], + "height": 0.36, + "thumb": "thumbs/procity_fit_beam_roof_01.png", + "kind": "arch", + "tris": 12, + "draws": 1, + "source": "ROOF COLUMN.glb" + }, + "ash_urn": { + "file": "procity_fit_ash_urn_01.glb", + "footprint": [ + 0.32, + 0.33 + ], + "height": 0.75, + "thumb": "thumbs/procity_fit_ash_urn_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "ashUrn.glb" + }, + "ashtray_post": { + "file": "procity_fit_ashtray_post_01.glb", + "footprint": [ + 0.41, + 0.41 + ], + "height": 0.95, + "thumb": "thumbs/procity_fit_ashtray_post_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "ashtray.glb" + }, + "bain_marie": { + "file": "procity_fit_bain_marie_01.glb", + "footprint": [ + 0.89, + 0.76 + ], + "height": 0.95, + "thumb": "thumbs/procity_fit_bain_marie_01.png", + "kind": "pub", + "tris": 4958, + "draws": 1, + "source": "bainMarie.glb" + }, + "banquette": { + "file": "procity_fit_banquette_01.glb", + "footprint": [ + 1.55, + 1.02 + ], + "height": 0.85, + "thumb": "thumbs/procity_fit_banquette_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "banquette.glb" + }, + "crowd_barrier": { + "file": "procity_fit_crowd_barrier_01.glb", + "footprint": [ + 1.97, + 0.3 + ], + "height": 1.1, + "thumb": "thumbs/procity_fit_crowd_barrier_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "barrier.glb" + }, + "beer_umbrella": { + "file": "procity_fit_beer_umbrella_01.glb", + "footprint": [ + 2.68, + 2.65 + ], + "height": 2.3, + "thumb": "thumbs/procity_fit_beer_umbrella_01.png", + "kind": "pub", + "tris": 4995, + "draws": 1, + "source": "beerUmbrella.glb" + }, + "bin_round": { + "file": "procity_fit_bin_round_01.glb", + "footprint": [ + 0.53, + 0.51 + ], + "height": 0.75, + "thumb": "thumbs/procity_fit_bin_round_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "binRound.glb" + }, + "bistro_table": { + "file": "procity_fit_bistro_table_01.glb", + "footprint": [ + 1.18, + 1.18 + ], + "height": 0.92, + "thumb": "thumbs/procity_fit_bistro_table_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "bistroTable.glb" + }, + "broken_basin": { + "file": "procity_fit_broken_basin_01.glb", + "footprint": [ + 0.59, + 0.49 + ], + "height": 0.6, + "thumb": "thumbs/procity_fit_broken_basin_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "brokenBasin.glb" + }, + "cable_snake": { + "file": "procity_fit_cable_snake_01.glb", + "footprint": [ + 0.45, + 0.47 + ], + "height": 0.2, + "thumb": "thumbs/procity_fit_cable_snake_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "cableSnake.glb" + }, + "can_stack": { + "file": "procity_fit_can_stack_01.glb", + "footprint": [ + 0.38, + 0.32 + ], + "height": 0.45, + "thumb": "thumbs/procity_fit_can_stack_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "canStack.glb" + }, + "cdj_deck": { + "file": "procity_fit_cdj_deck_01.glb", + "footprint": [ + 0.63, + 0.42 + ], + "height": 0.15, + "thumb": "thumbs/procity_fit_cdj_deck_01.png", + "kind": "pub", + "tris": 4998, + "draws": 1, + "source": "cdj.glb" + }, + "champ_bucket": { + "file": "procity_fit_champ_bucket_01.glb", + "footprint": [ + 0.24, + 0.2 + ], + "height": 0.35, + "thumb": "thumbs/procity_fit_champ_bucket_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "champBucket.glb" + }, + "distro_box": { + "file": "procity_fit_distro_box_01.glb", + "footprint": [ + 0.63, + 0.87 + ], + "height": 1.2, + "thumb": "thumbs/procity_fit_distro_box_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "distroBox.glb" + }, + "fire_ext": { + "file": "procity_fit_fire_ext_01.glb", + "footprint": [ + 0.34, + 0.19 + ], + "height": 0.6, + "thumb": "thumbs/procity_fit_fire_ext_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "fireExt.glb" + }, + "firepit": { + "file": "procity_fit_firepit_01.glb", + "footprint": [ + 0.76, + 0.56 + ], + "height": 0.5, + "thumb": "thumbs/procity_fit_firepit_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "firepit.glb" + }, + "flight_case": { + "file": "procity_fit_flight_case_01.glb", + "footprint": [ + 0.6, + 0.36 + ], + "height": 0.75, + "thumb": "thumbs/procity_fit_flight_case_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "flightCase.glb" + }, + "jukebox": { + "file": "procity_fit_jukebox_01.glb", + "footprint": [ + 0.97, + 0.41 + ], + "height": 1.52, + "thumb": "thumbs/procity_fit_jukebox_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "jukebox.glb" + }, + "keg": { + "file": "procity_fit_keg_01.glb", + "footprint": [ + 0.4, + 0.4 + ], + "height": 0.6, + "thumb": "thumbs/procity_fit_keg_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "keg.glb" + }, + "lounge_chair": { + "file": "procity_fit_lounge_chair_01.glb", + "footprint": [ + 0.69, + 0.69 + ], + "height": 0.8, + "thumb": "thumbs/procity_fit_lounge_chair_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "loungeChair.glb" + }, + "marble_sink": { + "file": "procity_fit_marble_sink_01.glb", + "footprint": [ + 1.15, + 0.56 + ], + "height": 0.85, + "thumb": "thumbs/procity_fit_marble_sink_01.png", + "kind": "pub", + "tris": 4998, + "draws": 1, + "source": "marbleSink.glb" + }, + "old_couch": { + "file": "procity_fit_old_couch_01.glb", + "footprint": [ + 1.8, + 0.95 + ], + "height": 0.85, + "thumb": "thumbs/procity_fit_old_couch_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "oldCouch.glb" + }, + "pallet": { + "file": "procity_fit_pallet_01.glb", + "footprint": [ + 0.79, + 0.73 + ], + "height": 0.2, + "thumb": "thumbs/procity_fit_pallet_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "pallet.glb" + }, + "picnic_table": { + "file": "procity_fit_picnic_table_01.glb", + "footprint": [ + 1.38, + 1.41 + ], + "height": 0.75, + "thumb": "thumbs/procity_fit_picnic_table_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "picnicTable.glb" + }, + "pie_warmer": { + "file": "procity_fit_pie_warmer_01.glb", + "footprint": [ + 0.61, + 0.71 + ], + "height": 0.55, + "thumb": "thumbs/procity_fit_pie_warmer_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "pieWarmer.glb" + }, + "concrete_plinth": { + "file": "procity_fit_concrete_plinth_01.glb", + "footprint": [ + 0.76, + 0.74 + ], + "height": 1.2, + "thumb": "thumbs/procity_fit_concrete_plinth_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "pillar.glb" + }, + "planter": { + "file": "procity_fit_planter_01.glb", + "footprint": [ + 0.98, + 0.96 + ], + "height": 0.55, + "thumb": "thumbs/procity_fit_planter_01.png", + "kind": "pub", + "tris": 4897, + "draws": 1, + "source": "planter.glb" + }, + "plywood_bar": { + "file": "procity_fit_plywood_bar_01.glb", + "footprint": [ + 1.51, + 0.82 + ], + "height": 1.05, + "thumb": "thumbs/procity_fit_plywood_bar_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "plywoodBar.glb" + }, + "pokie": { + "file": "procity_fit_pokie_01.glb", + "footprint": [ + 0.75, + 0.78 + ], + "height": 1.7, + "thumb": "thumbs/procity_fit_pokie_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "pokie.glb" + }, + "raffle_drum": { + "file": "procity_fit_raffle_drum_01.glb", + "footprint": [ + 0.37, + 0.37 + ], + "height": 0.9, + "thumb": "thumbs/procity_fit_raffle_drum_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "raffleDrum.glb" + }, + "rope_post": { + "file": "procity_fit_rope_post_01.glb", + "footprint": [ + 0.32, + 0.32 + ], + "height": 1.0, + "thumb": "thumbs/procity_fit_rope_post_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "ropePost.glb" + }, + "sandwich_board": { + "file": "procity_fit_sandwich_board_01.glb", + "footprint": [ + 0.58, + 0.56 + ], + "height": 1.0, + "thumb": "thumbs/procity_fit_sandwich_board_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "sandwichBoard.glb" + }, + "smoke_machine": { + "file": "procity_fit_smoke_machine_01.glb", + "footprint": [ + 0.17, + 0.24 + ], + "height": 0.38, + "thumb": "thumbs/procity_fit_smoke_machine_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "smokeMachine.glb" + }, + "speaker_stack": { + "file": "procity_fit_speaker_stack_01.glb", + "footprint": [ + 0.52, + 0.52 + ], + "height": 1.8, + "thumb": "thumbs/procity_fit_speaker_stack_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "stackF1.glb" + }, + "strobe_light": { + "file": "procity_fit_strobe_light_01.glb", + "footprint": [ + 0.22, + 0.24 + ], + "height": 0.4, + "thumb": "thumbs/procity_fit_strobe_light_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "strobe.glb" + }, + "sub_bass": { + "file": "procity_fit_sub_bass_01.glb", + "footprint": [ + 0.87, + 0.63 + ], + "height": 0.7, + "thumb": "thumbs/procity_fit_sub_bass_01.png", + "kind": "pub", + "tris": 4998, + "draws": 1, + "source": "subBass.glb" + }, + "topiary": { + "file": "procity_fit_topiary_01.glb", + "footprint": [ + 0.61, + 0.61 + ], + "height": 1.2, + "thumb": "thumbs/procity_fit_topiary_01.png", + "kind": "pub", + "tris": 4999, + "draws": 1, + "source": "topiary.glb" + }, + "urinal_trough": { + "file": "procity_fit_urinal_trough_01.glb", + "footprint": [ + 0.31, + 0.26 + ], + "height": 0.7, + "thumb": "thumbs/procity_fit_urinal_trough_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "trough.glb" + }, + "wheelie_bin": { + "file": "procity_fit_wheelie_bin_01.glb", + "footprint": [ + 0.72, + 0.92 + ], + "height": 1.1, + "thumb": "thumbs/procity_fit_wheelie_bin_01.png", + "kind": "pub", + "tris": 5000, + "draws": 1, + "source": "wheelieBin.glb" + }, + "deck_1200": { + "file": "procity_fit_deck_1200_01.glb", + "footprint": [ + 0.45, + 0.51 + ], + "height": 0.118, + "thumb": "thumbs/procity_fit_deck_1200_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "TURNTABLE-1200-RIGGED.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Arm_LIFT", + "Arm_YAW", + "Btn_STARTSTOP", + "Fader_PITCH", + "Platter", + "Platter_SPIN" + ], + "drivable": false + }, + "sl_deck_1": { + "file": "procity_fit_sl_deck_1_01.glb", + "footprint": [ + 0.25, + 0.23 + ], + "height": 0.041, + "thumb": "thumbs/procity_fit_sl_deck_1_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SL1.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Knob_MIC_GAIN_MOUNT", + "Knob_MIC_GAIN_ROOT" + ], + "drivable": false + }, + "sl_deck_2": { + "file": "procity_fit_sl_deck_2_01.glb", + "footprint": [ + 0.22, + 0.22 + ], + "height": 0.036, + "thumb": "thumbs/procity_fit_sl_deck_2_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SL2.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Knob_CD_PHONO_MOUNT", + "Knob_CD_PHONO_ROOT" + ], + "drivable": false + }, + "sl_deck_3": { + "file": "procity_fit_sl_deck_3_01.glb", + "footprint": [ + 0.25, + 0.26 + ], + "height": 0.037, + "thumb": "thumbs/procity_fit_sl_deck_3_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SL3.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "sl_deck_4": { + "file": "procity_fit_sl_deck_4_01.glb", + "footprint": [ + 0.25, + 0.33 + ], + "height": 0.043, + "thumb": "thumbs/procity_fit_sl_deck_4_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SL4.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_SAMPLE_RATE", + "Knob_PHONO_CD_MOUNT", + "Knob_PHONO_CD_ROOT", + "Sw_SAMPLE_RATE_FMT" + ], + "drivable": false + }, + "mixer_ttm54i": { + "file": "procity_fit_mixer_ttm54i_01.glb", + "footprint": [ + 0.23, + 0.42 + ], + "height": 0.107, + "thumb": "thumbs/procity_fit_mixer_ttm54i_01.png", + "kind": "djgear", + "tris": 4999, + "draws": 1, + "source": "TTM54i.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_EFFECTS_1", + "Btn_EFFECTS_2", + "Btn_HAMSTER_1", + "Btn_HAMSTER_2", + "Btn_XF_HAMSTER", + "Btn_XF_HAMSTER_FMOUNT", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind", + "Fader_BALANCE_2", + "Fader_BALANCE_2_ind", + "Fader_CROSS", + "Fader_CROSS_ind" + ], + "drivable": false + }, + "mixer_pmc05pro": { + "file": "procity_fit_mixer_pmc05pro_01.glb", + "footprint": [ + 0.26, + 0.4 + ], + "height": 0.111, + "thumb": "thumbs/procity_fit_mixer_pmc05pro_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "PMC-05PRO.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_CF_MONITOR", + "Fader_CF_MONITOR_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_MON_LEVEL", + "Fader_MON_LEVEL_ind", + "Fader_PGM1", + "Fader_PGM1_ind", + "Fader_PGM2", + "Fader_PGM2_ind", + "Knob_CF_MODE", + "Knob_CF_MODE_ROOT" + ], + "drivable": false + }, + "mixer_pmc06pro": { + "file": "procity_fit_mixer_pmc06pro_01.glb", + "footprint": [ + 0.15, + 0.47 + ], + "height": 0.074, + "thumb": "thumbs/procity_fit_mixer_pmc06pro_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "PMC-06ProA.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_CF_REV", + "Fader_CROSS", + "Fader_MON_CF", + "Fader_PGM1_LEVEL", + "Fader_PGM2_LEVEL", + "Knob_CF_MODE", + "Knob_CF_MODE_MOUNT", + "Knob_CF_MODE_ROOT", + "Knob_CF_MODE_k", + "Knob_CF_MODE_k.001", + "Knob_CF_MODE_k.002", + "Knob_CF_MODE_k.003" + ], + "drivable": false + }, + "mixer_pmc07pro": { + "file": "procity_fit_mixer_pmc07pro_01.glb", + "footprint": [ + 0.25, + 0.42 + ], + "height": 0.113, + "thumb": "thumbs/procity_fit_mixer_pmc07pro_01.png", + "kind": "djgear", + "tris": 4999, + "draws": 1, + "source": "PMC-07Pro.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_BAL_1", + "Fader_BAL_2", + "Fader_CF_MON", + "Fader_CROSS", + "Fader_EQ1_HI", + "Fader_EQ1_LO", + "Fader_EQ1_MID", + "Fader_EQ2_HI", + "Fader_EQ2_LO", + "Fader_EQ2_MID", + "Fader_MASTER_LEVEL", + "Fader_MICEQ_HI" + ], + "drivable": false + }, + "mixer_pmc08pro": { + "file": "procity_fit_mixer_pmc08pro_01.glb", + "footprint": [ + 0.26, + 0.46 + ], + "height": 0.119, + "thumb": "thumbs/procity_fit_mixer_pmc08pro_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "PMC-08Pro.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_BAL_1", + "Fader_BAL_2", + "Fader_CF_REV", + "Fader_CF_REV_FMOUNT", + "Fader_CROSS", + "Fader_EFF_BAL", + "Fader_EFF_LEVEL", + "Fader_IF1_REV", + "Fader_IF1_REV_FMOUNT", + "Fader_IF2_REV", + "Fader_IF2_REV_FMOUNT", + "Fader_ISO1_HI" + ], + "drivable": false + }, + "mixer_pmc10": { + "file": "procity_fit_mixer_pmc10_01.glb", + "footprint": [ + 0.48, + 0.22 + ], + "height": 0.222, + "thumb": "thumbs/procity_fit_mixer_pmc10_01.png", + "kind": "djgear", + "tris": 4999, + "draws": 1, + "source": "PMC-10.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_BALANCE", + "Fader_BALANCE_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_EQ_1", + "Fader_EQ_1_ind", + "Fader_EQ_2", + "Fader_EQ_2_ind", + "Fader_EQ_3", + "Fader_EQ_3_ind", + "Fader_EQ_4", + "Fader_EQ_4_ind" + ], + "drivable": false + }, + "mixer_pmc20": { + "file": "procity_fit_mixer_pmc20_01.glb", + "footprint": [ + 0.51, + 0.22 + ], + "height": 0.222, + "thumb": "thumbs/procity_fit_mixer_pmc20_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "PMC-20.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_EFFECT_1", + "Btn_EFFECT_2", + "Btn_EFFECT_3", + "Btn_MON_EFFECT", + "Btn_MON_PGMA", + "Btn_MON_PGMB", + "Btn_MON_PGMC", + "Btn_POWER", + "Btn_TRANSFORMER_1", + "Btn_TRANSFORMER_2", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind" + ], + "drivable": false + }, + "mixer_shdj1200": { + "file": "procity_fit_mixer_shdj1200_01.glb", + "footprint": [ + 0.26, + 0.39 + ], + "height": 0.114, + "thumb": "thumbs/procity_fit_mixer_shdj1200_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SH-DJ1200.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_A", + "Fader_A_ind", + "Fader_B", + "Fader_B_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Knob_AUX", + "Knob_AUX_ROOT", + "Knob_AUX_k", + "Knob_AUX_k.001", + "Knob_AUX_k.002", + "Knob_AUX_k.003" + ], + "drivable": false + }, + "mixer_mx2200": { + "file": "procity_fit_mixer_mx2200_01.glb", + "footprint": [ + 0.3, + 0.31 + ], + "height": 0.103, + "thumb": "thumbs/procity_fit_mixer_mx2200_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "MX-2200.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_CROSS", + "Fader_CUE", + "Fader_MIC", + "Fader_PHONO1", + "Fader_PHONO2", + "Sw_CUE", + "Sw_CUE_ROOT", + "Sw_CUE_base", + "Sw_CUE_tip", + "Sw_DJMIC", + "Sw_DJMIC_ROOT", + "Sw_DJMIC_base" + ], + "drivable": false + }, + "mixer_pmx9000": { + "file": "procity_fit_mixer_pmx9000_01.glb", + "footprint": [ + 0.48, + 0.37 + ], + "height": 0.178, + "thumb": "thumbs/procity_fit_mixer_pmx9000_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "PMX-9000.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Fader_CUE_LEVEL", + "Fader_EQ_15K", + "Fader_EQ_1K", + "Fader_EQ_250", + "Fader_EQ_55", + "Fader_EQ_5K", + "Fader_IN1_LEVEL", + "Fader_IN1_PRESET", + "Fader_IN2_LEVEL", + "Fader_IN2_PRESET", + "Fader_IN3_LEVEL", + "Fader_IN3_PRESET" + ], + "drivable": false + }, + "drum_808": { + "file": "procity_fit_drum_808_01.glb", + "footprint": [ + 0.53, + 0.47 + ], + "height": 0.153, + "thumb": "thumbs/procity_fit_drum_808_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "TR-808.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_START_STOP", + "Btn_TAP", + "Knob_AUTO_FILL", + "Knob_AUTO_FILL_MOUNT", + "Knob_AUTO_FILL_ROOT", + "Knob_AUTO_FILL_k", + "Knob_AUTO_FILL_k.001", + "Knob_AUTO_FILL_k.002", + "Knob_AUTO_FILL_k.003", + "Knob_AUTO_FILL_k.004", + "Knob_AUTO_FILL_k.005", + "Knob_AUTO_FILL_k.006" + ], + "drivable": false + }, + "drum_tr1000": { + "file": "procity_fit_drum_tr1000_01.glb", + "footprint": [ + 0.47, + 0.37 + ], + "height": 0.161, + "thumb": "thumbs/procity_fit_drum_tr1000_01.png", + "kind": "djgear", + "tris": 4897, + "draws": 1, + "source": "TR-1000.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_FILL_IN_TRIG", + "Btn_FILL_IN_TRIG_BMOUNT", + "Btn_INST_BD", + "Btn_INST_BD_BMOUNT", + "Btn_INST_CC", + "Btn_INST_CC_BMOUNT", + "Btn_INST_CH", + "Btn_INST_CH_BMOUNT", + "Btn_INST_HC", + "Btn_INST_HC_BMOUNT", + "Btn_INST_HT", + "Btn_INST_HT_BMOUNT" + ], + "drivable": false + }, + "sampler_sp1200": { + "file": "procity_fit_sampler_sp1200_01.glb", + "footprint": [ + 0.44, + 0.62 + ], + "height": 0.191, + "thumb": "thumbs/procity_fit_sampler_sp1200_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "SP-1200.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_ABCD", + "Btn_DISK", + "Btn_ENTER", + "Btn_KP_0", + "Btn_KP_1", + "Btn_KP_2", + "Btn_KP_3", + "Btn_KP_4", + "Btn_KP_5", + "Btn_KP_6", + "Btn_KP_7", + "Btn_KP_8" + ], + "drivable": false + }, + "sampler_mpc3000": { + "file": "procity_fit_sampler_mpc3000_01.glb", + "footprint": [ + 0.47, + 0.59 + ], + "height": 0.119, + "thumb": "thumbs/procity_fit_sampler_mpc3000_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "MPC3000.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_16_LEVELS", + "Btn_COUNT_IN", + "Btn_CUR_DOWN", + "Btn_CUR_LEFT", + "Btn_CUR_RIGHT", + "Btn_CUR_UP", + "Btn_ERASE", + "Btn_FN_DISK", + "Btn_FN_EDIT", + "Btn_FN_MAIN", + "Btn_FN_MIDI", + "Btn_FN_MIX" + ], + "drivable": false + }, + "sampler_s900": { + "file": "procity_fit_sampler_s900_01.glb", + "footprint": [ + 0.47, + 0.56 + ], + "height": 0.135, + "thumb": "thumbs/procity_fit_sampler_s900_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "S900.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_DISK", + "Btn_EDIT_PRGRM", + "Btn_EDIT_SAMPLE", + "Btn_ENT", + "Btn_KP_0", + "Btn_KP_1", + "Btn_KP_2", + "Btn_KP_3", + "Btn_KP_4", + "Btn_KP_5", + "Btn_KP_6", + "Btn_KP_7" + ], + "drivable": false + }, + "headphones_hd25": { + "file": "procity_fit_headphones_hd25_01.glb", + "footprint": [ + 0.23, + 0.16 + ], + "height": 0.302, + "thumb": "thumbs/procity_fit_headphones_hd25_01.png", + "kind": "djgear", + "tris": 4999, + "draws": 1, + "source": "HD25.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "cart_m447": { + "file": "procity_fit_cart_m447_01.glb", + "footprint": [ + 0.04, + 0.02 + ], + "height": 0.028, + "thumb": "thumbs/procity_fit_cart_m447_01.png", + "kind": "djgear", + "tris": 1387, + "draws": 1, + "source": "M44-7.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "cart_concorde": { + "file": "procity_fit_cart_concorde_01.glb", + "footprint": [ + 0.1, + 0.02 + ], + "height": 0.057, + "thumb": "thumbs/procity_fit_cart_concorde_01.png", + "kind": "djgear", + "tris": 2853, + "draws": 1, + "source": "CONCORDE-PRO.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "cart_case": { + "file": "procity_fit_cart_case_01.glb", + "footprint": [ + 0.22, + 0.18 + ], + "height": 0.208, + "thumb": "thumbs/procity_fit_cart_case_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "CONCORDE-CASE.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "headshell_technics": { + "file": "procity_fit_headshell_technics_01.glb", + "footprint": [ + 0.08, + 0.02 + ], + "height": 0.023, + "thumb": "thumbs/procity_fit_headshell_technics_01.png", + "kind": "djgear", + "tris": 2097, + "draws": 1, + "source": "HEADSHELL-TECHNICS.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "cart_stanton": { + "file": "procity_fit_cart_stanton_01.glb", + "footprint": [ + 0.04, + 0.02 + ], + "height": 0.025, + "thumb": "thumbs/procity_fit_cart_stanton_01.png", + "kind": "djgear", + "tris": 3290, + "draws": 1, + "source": "STANTON-500.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [], + "drivable": false + }, + "zip_drive": { + "file": "procity_fit_zip_drive_01.glb", + "footprint": [ + 0.14, + 0.23 + ], + "height": 0.039, + "thumb": "thumbs/procity_fit_zip_drive_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 1, + "source": "ZIP-100.glb", + "mode": "static", + "control_nodes": [], + "source_controls": [ + "Btn_EJECT" + ], + "drivable": false + }, + "deck_1200_rigged": { + "file": "procity_fit_deck_1200_rigged_01.glb", + "footprint": [ + 0.51, + 0.56 + ], + "height": 0.11, + "thumb": "thumbs/procity_fit_deck_1200_rigged_01.png", + "kind": "djgear", + "tris": 3821, + "draws": 5, + "source": "TURNTABLE-1200-RIGGED.glb", + "mode": "rigged", + "control_nodes": [ + "Platter_SPIN", + "Arm_YAW", + "Fader_PITCH", + "Btn_STARTSTOP" + ], + "source_controls": [ + "Arm_LIFT", + "Arm_YAW", + "Btn_STARTSTOP", + "Fader_PITCH", + "Platter", + "Platter_SPIN" + ], + "drivable": true + }, + "mixer_ttm54i_rigged": { + "file": "procity_fit_mixer_ttm54i_rigged_01.glb", + "footprint": [ + 0.23, + 0.42 + ], + "height": 0.107, + "thumb": "thumbs/procity_fit_mixer_ttm54i_rigged_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 2, + "source": "TTM54i.glb", + "mode": "rigged", + "control_nodes": [ + "Fader_CROSS" + ], + "source_controls": [ + "Btn_EFFECTS_1", + "Btn_EFFECTS_2", + "Btn_HAMSTER_1", + "Btn_HAMSTER_2", + "Btn_XF_HAMSTER", + "Btn_XF_HAMSTER_FMOUNT", + "Fader_BALANCE_1", + "Fader_BALANCE_1_ind", + "Fader_BALANCE_2", + "Fader_BALANCE_2_ind", + "Fader_CROSS", + "Fader_CROSS_ind" + ], + "drivable": true + }, + "mixer_pmc05pro_rigged": { + "file": "procity_fit_mixer_pmc05pro_rigged_01.glb", + "footprint": [ + 0.26, + 0.4 + ], + "height": 0.111, + "thumb": "thumbs/procity_fit_mixer_pmc05pro_rigged_01.png", + "kind": "djgear", + "tris": 5000, + "draws": 2, + "source": "PMC-05PRO.glb", + "mode": "rigged", + "control_nodes": [ + "Fader_CROSS" + ], + "source_controls": [ + "Fader_CF_MONITOR", + "Fader_CF_MONITOR_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Fader_MON_LEVEL", + "Fader_MON_LEVEL_ind", + "Fader_PGM1", + "Fader_PGM1_ind", + "Fader_PGM2", + "Fader_PGM2_ind", + "Knob_CF_MODE", + "Knob_CF_MODE_ROOT" + ], + "drivable": true + }, + "mixer_shdj1200_rigged": { + "file": "procity_fit_mixer_shdj1200_rigged_01.glb", + "footprint": [ + 0.26, + 0.39 + ], + "height": 0.114, + "thumb": "thumbs/procity_fit_mixer_shdj1200_rigged_01.png", + "kind": "djgear", + "tris": 4999, + "draws": 2, + "source": "SH-DJ1200.glb", + "mode": "rigged", + "control_nodes": [ + "Fader_CROSS" + ], + "source_controls": [ + "Fader_A", + "Fader_A_ind", + "Fader_B", + "Fader_B_ind", + "Fader_CROSS", + "Fader_CROSS_ind", + "Knob_AUX", + "Knob_AUX_ROOT", + "Knob_AUX_k", + "Knob_AUX_k.001", + "Knob_AUX_k.002", + "Knob_AUX_k.003" + ], + "drivable": true + }, "cash_register": { "file": "vintage-cash-register.glb", "footprint": [ diff --git a/web/assets/motion_manifest.json b/web/assets/motion_manifest.json new file mode 100644 index 0000000..b3be245 --- /dev/null +++ b/web/assets/motion_manifest.json @@ -0,0 +1,631 @@ +{ + "_schema": "PROCITY motion manifest v1 (R41 41.1). groups{} -> one GLB under web/models/clips/, holding N named animations. clips{} -> {group,category,duration,loopable,source,mixamo,pair?}. Load a group ONCE (gltf.animations[] are named by clipId); index by clips[id].group.", + "_licence": "Mixamo (Adobe) via ultra ~/Documents/MOTIONLIB/green/mixamo_full/. GREEN: free to use inside games/projects, NO attribution required. Never redistribute as a clip pack. R41 ships this licence regime only (ruling 2).", + "_tracks": "rotation-only", + "_encoding": "rotation outputs are int16 NORMALIZED (glTF-legal, three.js r175 de-normalises in GLTFLoader._getArrayFromAccessor); all 65 joints animated incl. fingers", + "_note": "Skeleton-only, 0 tris, 66 nodes (65 mixamorig joints + Armature) \u2014 the same byte-shape as web/models/peds/idle.glb. rigs.js `_canon` folds the mixamorigN: prefix, `_rotOnly` keeps quaternions and drops Hips.quaternion.", + "fps": 30, + "groups": { + "idles.glb": { + "category": "idle", + "desc": "the clone-army killer: 10 distinct standing idles", + "clips": [ + "idle_breathing", + "idle_standing", + "idle_look_around", + "idle_weight_shift", + "idle_cocky_lean", + "idle_yawn", + "idle_happy_1", + "idle_happy_2", + "idle_old_man", + "idle_smoking" + ], + "bytes": 1029496, + "nodes": 66, + "durationSum": 78.267 + }, + "browse.glb": { + "category": "browse", + "desc": "shop-floor: hold / examine / open / carry / pick up", + "clips": [ + "browse_hold_idle", + "browse_hold_walk", + "browse_hold_turn_l", + "browse_hold_turn_r", + "browse_open_cabinet", + "browse_close_cabinet", + "browse_carry_box", + "browse_pick_up" + ], + "bytes": 476056, + "nodes": 66, + "durationSum": 30.833 + }, + "sitlean.glb": { + "category": "sitlean", + "desc": "benches and shopfronts: 4 seated + 4 leaning", + "clips": [ + "sit_bench_swing", + "sit_hands_thighs", + "sit_impatient", + "sit_fidget_feet", + "lean_wall_postures", + "lean_wall_leg_up", + "lean_wall_shoulders", + "lean_wall_walkman" + ], + "bytes": 358112, + "nodes": 66, + "durationSum": 27.533 + }, + "social.glb": { + "category": "social", + "desc": "two-person street/shop talk; the handshake pair stays paired", + "clips": [ + "social_conversation", + "social_chat_water", + "social_greeting", + "social_shake_1", + "social_shake_2", + "social_nod_yes", + "social_shake_no", + "social_wave" + ], + "bytes": 895648, + "nodes": 66, + "durationSum": 73.867 + }, + "locomotion.glb": { + "category": "locomotion", + "desc": "civilian walk + turn set (the named Degree_Turn set is rifle-aiming \u2014 substituted, see AUDIT)", + "clips": [ + "walk_shopping_bag", + "turn_left_90", + "turn_right_90", + "turn_walk_180", + "turn_in_place", + "walk_to_stand" + ], + "bytes": 212776, + "nodes": 66, + "durationSum": 12.133 + }, + "venue.glb": { + "category": "venue", + "desc": "pub / gig / record shop", + "clips": [ + "venue_clap_stand", + "venue_clap_seated", + "venue_cheer", + "venue_northern_soul", + "venue_bartending", + "venue_headphones" + ], + "bytes": 526036, + "nodes": 66, + "durationSum": 39.533 + } + }, + "clips": { + "idle_breathing": { + "group": "idles.glb", + "category": "idle", + "duration": 9.9667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Breathing_Idle__c9c6d0d5.fbx", + "mixamo": "Breathing Idle", + "channels": 65, + "rotBones": 65 + }, + "idle_standing": { + "group": "idles.glb", + "category": "idle", + "duration": 6.0333, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Standing_Idle__c9c831cf.fbx", + "mixamo": "Standing Idle", + "channels": 65, + "rotBones": 65 + }, + "idle_look_around": { + "group": "idles.glb", + "category": "idle", + "duration": 6.3667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Idle_Stand_Looking_Around__c9c6cdf7.fbx", + "mixamo": "Looking Around", + "channels": 65, + "rotBones": 65 + }, + "idle_weight_shift": { + "group": "idles.glb", + "category": "idle", + "duration": 9.4667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Shifting_Weight_From_Side_To_Side__c9c908f7.fbx", + "mixamo": "Weight Shift", + "channels": 65, + "rotBones": 65 + }, + "idle_cocky_lean": { + "group": "idles.glb", + "category": "idle", + "duration": 2.9333, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Cocky_Lean_Back__c9c90e3c.fbx", + "mixamo": "Being Cocky", + "channels": 65, + "rotBones": 65 + }, + "idle_yawn": { + "group": "idles.glb", + "category": "idle", + "duration": 8.3667, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Big_Yawn_While_Standing__c9c98361.fbx", + "mixamo": "Yawn", + "channels": 65, + "rotBones": 65 + }, + "idle_happy_1": { + "group": "idles.glb", + "category": "idle", + "duration": 2.0333, + "loopable": true, + "loopSeamDeg": 0.28, + "source": "Happy_Idle_Variation_1__c9ccf37e.fbx", + "mixamo": "Happy Idle", + "channels": 65, + "rotBones": 65 + }, + "idle_happy_2": { + "group": "idles.glb", + "category": "idle", + "duration": 2.9667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Happy_Idle_Variation_2__c9ccf445.fbx", + "mixamo": "Happy Idle", + "channels": 65, + "rotBones": 65 + }, + "idle_old_man": { + "group": "idles.glb", + "category": "idle", + "duration": 12.2, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Old_Man_Standing_Idle__c9c6c004.fbx", + "mixamo": "Old Man Idle", + "channels": 65, + "rotBones": 65 + }, + "idle_smoking": { + "group": "idles.glb", + "category": "idle", + "duration": 17.9333, + "loopable": true, + "loopSeamDeg": 0.37, + "source": "Idle_Smoking__c9c603fd.fbx", + "mixamo": "Smoking", + "channels": 65, + "rotBones": 65 + }, + "browse_hold_idle": { + "group": "browse.glb", + "category": "browse", + "duration": 6.0333, + "loopable": true, + "loopSeamDeg": 0.06, + "source": "Holding_An_Object_Idle__c9cda11a.fbx", + "mixamo": "Holding Idle", + "channels": 65, + "rotBones": 65 + }, + "browse_hold_walk": { + "group": "browse.glb", + "category": "browse", + "duration": 1.4, + "loopable": true, + "loopSeamDeg": 6.35, + "source": "Holding_An_Object_While_Walking__c9cda1e7.fbx", + "mixamo": "Holding Walk", + "channels": 65, + "rotBones": 65 + }, + "browse_hold_turn_l": { + "group": "browse.glb", + "category": "browse", + "duration": 1.2333, + "loopable": false, + "loopSeamDeg": 172.54, + "source": "Holding_An_Object_While_Turning_Left__c9cda2af.fbx", + "mixamo": "Holding Turn Left", + "channels": 65, + "rotBones": 65, + "pair": "browse_hold_turn_r" + }, + "browse_hold_turn_r": { + "group": "browse.glb", + "category": "browse", + "duration": 1.3333, + "loopable": false, + "loopSeamDeg": 147.31, + "source": "Holding_An_Object_While_Turning_Right__c9cda378.fbx", + "mixamo": "Holding Turn Right", + "channels": 65, + "rotBones": 65, + "pair": "browse_hold_turn_l" + }, + "browse_open_cabinet": { + "group": "browse.glb", + "category": "browse", + "duration": 4.8333, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Opening_Cabinet_Door__c9c6d49e.fbx", + "mixamo": "Opening", + "channels": 65, + "rotBones": 65, + "pair": "browse_close_cabinet" + }, + "browse_close_cabinet": { + "group": "browse.glb", + "category": "browse", + "duration": 6.2, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Closing_Cabinet_Door__c9c6d55d.fbx", + "mixamo": "Closing", + "channels": 65, + "rotBones": 65, + "pair": "browse_open_cabinet" + }, + "browse_carry_box": { + "group": "browse.glb", + "category": "browse", + "duration": 6.3333, + "loopable": true, + "loopSeamDeg": 0.03, + "source": "Carrying_A_Box__c9cd8d90.fbx", + "mixamo": "Box Idle", + "channels": 65, + "rotBones": 65 + }, + "browse_pick_up": { + "group": "browse.glb", + "category": "browse", + "duration": 3.4667, + "loopable": false, + "loopSeamDeg": 92.15, + "source": "Picking_Up_An_Object_With_One_Hand__c9c6c5eb.fbx", + "mixamo": "Picking Up Object", + "channels": 65, + "rotBones": 65 + }, + "sit_bench_swing": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 2.1333, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Little_Girl_Sitting_On_A_Bench_Swinging_Her_Legs__c9c89165.fbx", + "mixamo": "Sitting", + "channels": 65, + "rotBones": 65 + }, + "sit_hands_thighs": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 6.4333, + "loopable": true, + "loopSeamDeg": 0.05, + "source": "Sitting_In_Chair_Hands_Resting_On_Thighs__c9cb9783.fbx", + "mixamo": "Sitting Idle", + "channels": 65, + "rotBones": 65 + }, + "sit_impatient": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 7.0333, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Sitting_And_Impatiently_Waiting__c9ccad56.fbx", + "mixamo": "Sitting", + "channels": 65, + "rotBones": 65 + }, + "sit_fidget_feet": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 1.2, + "loopable": true, + "loopSeamDeg": 0.19, + "source": "Sit_With_Fidgeting_Feet__c9c699a5.fbx", + "mixamo": "Sitting", + "channels": 65, + "rotBones": 65 + }, + "lean_wall_postures": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 2.0333, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Leaning_Against_A_Wall_With_Various_Postures__c9c695f0.fbx", + "mixamo": "One Shoulder Lean", + "channels": 65, + "rotBones": 65 + }, + "lean_wall_leg_up": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 3.3667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Leaning_Square_Against_A_Wall_With_One_Leg_Up__c9c696b0.fbx", + "mixamo": "Leaning", + "channels": 65, + "rotBones": 65 + }, + "lean_wall_shoulders": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 3.3667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Leaning_Square_With_Shoulders_Against_A_Wall__c9c69770.fbx", + "mixamo": "Leaning", + "channels": 65, + "rotBones": 65 + }, + "lean_wall_walkman": { + "group": "sitlean.glb", + "category": "sitlean", + "duration": 1.9667, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Little_Girl_Leaning_On_A_Wall_Listening_To_Music__c9c8bace.fbx", + "mixamo": "Leaning On A Wall", + "channels": 65, + "rotBones": 65 + }, + "social_conversation": { + "group": "social.glb", + "category": "social", + "duration": 10.3, + "loopable": true, + "loopSeamDeg": 0.29, + "source": "General_Conversation__c9c88301.fbx", + "mixamo": "Talking", + "channels": 65, + "rotBones": 65 + }, + "social_chat_water": { + "group": "social.glb", + "category": "social", + "duration": 44.6, + "loopable": true, + "loopSeamDeg": 0.78, + "source": "Having_A_Chat_At_The_Watercooler__c9c9f1c9.fbx", + "mixamo": "Talking At Watercooler", + "channels": 65, + "rotBones": 65 + }, + "social_greeting": { + "group": "social.glb", + "category": "social", + "duration": 5.1333, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Greeting_While_Standing__c9c7996a.fbx", + "mixamo": "Standing Greeting", + "channels": 65, + "rotBones": 65 + }, + "social_shake_1": { + "group": "social.glb", + "category": "social", + "duration": 4.4, + "loopable": false, + "loopSeamDeg": 0.03, + "source": "2_People_Shaking_Hands_Part_1_-_Female__c9c9f343.fbx", + "mixamo": "Shaking Hands 1", + "channels": 65, + "rotBones": 65, + "pair": "social_shake_2" + }, + "social_shake_2": { + "group": "social.glb", + "category": "social", + "duration": 4.4, + "loopable": false, + "loopSeamDeg": 0.13, + "source": "2_People_Shaking_Hands_Part_2_-_Male__c9c9f403.fbx", + "mixamo": "Shaking Hands 2", + "channels": 65, + "rotBones": 65, + "pair": "social_shake_1" + }, + "social_nod_yes": { + "group": "social.glb", + "category": "social", + "duration": 2.6333, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Nodding_Head_Yes__c9c90d7b.fbx", + "mixamo": "Head Nod Yes", + "channels": 65, + "rotBones": 65 + }, + "social_shake_no": { + "group": "social.glb", + "category": "social", + "duration": 1.8333, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Shaking_Head_No_Dismissively__c9c90b3c.fbx", + "mixamo": "Shaking Head No", + "channels": 65, + "rotBones": 65 + }, + "social_wave": { + "group": "social.glb", + "category": "social", + "duration": 0.5667, + "loopable": false, + "loopSeamDeg": 0.04, + "source": "Waving__c9c5ed32.fbx", + "mixamo": "Waving", + "channels": 65, + "rotBones": 65 + }, + "walk_shopping_bag": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 1.3, + "loopable": true, + "loopSeamDeg": 0.03, + "source": "Male_Walking_With_Shopping_Bag__c9c64027.fbx", + "mixamo": "Walking With Shopping Bag", + "channels": 65, + "rotBones": 65 + }, + "turn_left_90": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 1.5333, + "loopable": false, + "loopSeamDeg": 84.6, + "source": "Turning_90_Degrees_Left__c9ceef5f.fbx", + "mixamo": "Unarmed Turn Left 90", + "channels": 65, + "rotBones": 65, + "pair": "turn_right_90" + }, + "turn_right_90": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 1.5333, + "loopable": false, + "loopSeamDeg": 83.16, + "source": "Turning_90_Degrees_Right__c9cef01d.fbx", + "mixamo": "Unarmed Turn Right 90", + "channels": 65, + "rotBones": 65, + "pair": "turn_left_90" + }, + "turn_walk_180": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 1.0333, + "loopable": false, + "loopSeamDeg": 180.0, + "source": "Turning_180_Degrees_While_Walking__c9c9c73f.fbx", + "mixamo": "Walking Turn 180", + "channels": 65, + "rotBones": 65 + }, + "turn_in_place": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 3.7, + "loopable": false, + "loopSeamDeg": 36.68, + "source": "Turning_In_Place__c9c70ef0.fbx", + "mixamo": "Turning", + "channels": 65, + "rotBones": 65, + "loopDemoted": "curated loopable but loop seam 36.7 deg > 25.0" + }, + "walk_to_stand": { + "group": "locomotion.glb", + "category": "locomotion", + "duration": 3.0333, + "loopable": false, + "loopSeamDeg": 55.33, + "source": "Walking_To_Standing_Idle__c9c8d966.fbx", + "mixamo": "Stop Walking", + "channels": 65, + "rotBones": 65 + }, + "venue_clap_stand": { + "group": "venue.glb", + "category": "venue", + "duration": 1.2, + "loopable": true, + "loopSeamDeg": 0.07, + "source": "Clap_While_Standing__c9c60ef2.fbx", + "mixamo": "Clapping", + "channels": 65, + "rotBones": 65 + }, + "venue_clap_seated": { + "group": "venue.glb", + "category": "venue", + "duration": 1.2, + "loopable": true, + "loopSeamDeg": 0.07, + "source": "Clap_While_Seated__c9c60fce.fbx", + "mixamo": "Clapping", + "channels": 65, + "rotBones": 65 + }, + "venue_cheer": { + "group": "venue.glb", + "category": "venue", + "duration": 2.9333, + "loopable": true, + "loopSeamDeg": 0.04, + "source": "Male_Cheering_With_Two_Fists_Pump__c9c66dfd.fbx", + "mixamo": "Cheering", + "channels": 65, + "rotBones": 65 + }, + "venue_northern_soul": { + "group": "venue.glb", + "category": "venue", + "duration": 16.1, + "loopable": true, + "loopSeamDeg": 0.41, + "source": "Basic_Northern_Soul_Step__c9cbe56d.fbx", + "mixamo": "Northern Soul Dance", + "channels": 65, + "rotBones": 65 + }, + "venue_bartending": { + "group": "venue.glb", + "category": "venue", + "duration": 13.6, + "loopable": true, + "loopSeamDeg": 0.03, + "source": "Pouring_And_Serving_Drinks__c9c78799.fbx", + "mixamo": "Bartending", + "channels": 65, + "rotBones": 65 + }, + "venue_headphones": { + "group": "venue.glb", + "category": "venue", + "duration": 4.5, + "loopable": true, + "loopSeamDeg": 0.03, + "source": "Male_With_Headphones_Listening_To_Music__c9c63329.fbx", + "mixamo": "Listening To Music", + "channels": 65, + "rotBones": 65 + } + }, + "totalBytes": 3498124, + "clipCount": 46, + "groupCount": 6, + "dedupSavedBytes": 1341724 +} diff --git a/web/assets/thumbs/procity_fit_aircon_unit_01.png b/web/assets/thumbs/procity_fit_aircon_unit_01.png new file mode 100644 index 0000000..54c6321 Binary files /dev/null and b/web/assets/thumbs/procity_fit_aircon_unit_01.png differ diff --git a/web/assets/thumbs/procity_fit_ash_urn_01.png b/web/assets/thumbs/procity_fit_ash_urn_01.png new file mode 100644 index 0000000..ac904d3 Binary files /dev/null and b/web/assets/thumbs/procity_fit_ash_urn_01.png differ diff --git a/web/assets/thumbs/procity_fit_ashtray_post_01.png b/web/assets/thumbs/procity_fit_ashtray_post_01.png new file mode 100644 index 0000000..e787641 Binary files /dev/null and b/web/assets/thumbs/procity_fit_ashtray_post_01.png differ diff --git a/web/assets/thumbs/procity_fit_bain_marie_01.png b/web/assets/thumbs/procity_fit_bain_marie_01.png new file mode 100644 index 0000000..b6b49bd Binary files /dev/null and b/web/assets/thumbs/procity_fit_bain_marie_01.png differ diff --git a/web/assets/thumbs/procity_fit_banquette_01.png b/web/assets/thumbs/procity_fit_banquette_01.png new file mode 100644 index 0000000..8dfa393 Binary files /dev/null and b/web/assets/thumbs/procity_fit_banquette_01.png differ diff --git a/web/assets/thumbs/procity_fit_beam_roof_01.png b/web/assets/thumbs/procity_fit_beam_roof_01.png new file mode 100644 index 0000000..e82fd49 Binary files /dev/null and b/web/assets/thumbs/procity_fit_beam_roof_01.png differ diff --git a/web/assets/thumbs/procity_fit_beer_umbrella_01.png b/web/assets/thumbs/procity_fit_beer_umbrella_01.png new file mode 100644 index 0000000..8c20e8c Binary files /dev/null and b/web/assets/thumbs/procity_fit_beer_umbrella_01.png differ diff --git a/web/assets/thumbs/procity_fit_bin_big_4x4_01.png b/web/assets/thumbs/procity_fit_bin_big_4x4_01.png new file mode 100644 index 0000000..6e88b9b Binary files /dev/null and b/web/assets/thumbs/procity_fit_bin_big_4x4_01.png differ diff --git a/web/assets/thumbs/procity_fit_bin_endoffunk_01.png b/web/assets/thumbs/procity_fit_bin_endoffunk_01.png new file mode 100644 index 0000000..e5bd459 Binary files /dev/null and b/web/assets/thumbs/procity_fit_bin_endoffunk_01.png differ diff --git a/web/assets/thumbs/procity_fit_bin_round_01.png b/web/assets/thumbs/procity_fit_bin_round_01.png new file mode 100644 index 0000000..21e6745 Binary files /dev/null and b/web/assets/thumbs/procity_fit_bin_round_01.png differ diff --git a/web/assets/thumbs/procity_fit_bistro_table_01.png b/web/assets/thumbs/procity_fit_bistro_table_01.png new file mode 100644 index 0000000..dc50337 Binary files /dev/null and b/web/assets/thumbs/procity_fit_bistro_table_01.png differ diff --git a/web/assets/thumbs/procity_fit_broken_basin_01.png b/web/assets/thumbs/procity_fit_broken_basin_01.png new file mode 100644 index 0000000..2e0634c Binary files /dev/null and b/web/assets/thumbs/procity_fit_broken_basin_01.png differ diff --git a/web/assets/thumbs/procity_fit_cable_snake_01.png b/web/assets/thumbs/procity_fit_cable_snake_01.png new file mode 100644 index 0000000..2f79a48 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cable_snake_01.png differ diff --git a/web/assets/thumbs/procity_fit_can_stack_01.png b/web/assets/thumbs/procity_fit_can_stack_01.png new file mode 100644 index 0000000..60eda7f Binary files /dev/null and b/web/assets/thumbs/procity_fit_can_stack_01.png differ diff --git a/web/assets/thumbs/procity_fit_cart_case_01.png b/web/assets/thumbs/procity_fit_cart_case_01.png new file mode 100644 index 0000000..ff463d4 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cart_case_01.png differ diff --git a/web/assets/thumbs/procity_fit_cart_concorde_01.png b/web/assets/thumbs/procity_fit_cart_concorde_01.png new file mode 100644 index 0000000..bd369bc Binary files /dev/null and b/web/assets/thumbs/procity_fit_cart_concorde_01.png differ diff --git a/web/assets/thumbs/procity_fit_cart_m447_01.png b/web/assets/thumbs/procity_fit_cart_m447_01.png new file mode 100644 index 0000000..a0074f0 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cart_m447_01.png differ diff --git a/web/assets/thumbs/procity_fit_cart_stanton_01.png b/web/assets/thumbs/procity_fit_cart_stanton_01.png new file mode 100644 index 0000000..a6211b7 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cart_stanton_01.png differ diff --git a/web/assets/thumbs/procity_fit_cdj_deck_01.png b/web/assets/thumbs/procity_fit_cdj_deck_01.png new file mode 100644 index 0000000..4a58f76 Binary files /dev/null and b/web/assets/thumbs/procity_fit_cdj_deck_01.png differ diff --git a/web/assets/thumbs/procity_fit_champ_bucket_01.png b/web/assets/thumbs/procity_fit_champ_bucket_01.png new file mode 100644 index 0000000..0458d57 Binary files /dev/null and b/web/assets/thumbs/procity_fit_champ_bucket_01.png differ diff --git a/web/assets/thumbs/procity_fit_chiu_bin_01.png b/web/assets/thumbs/procity_fit_chiu_bin_01.png new file mode 100644 index 0000000..8911264 Binary files /dev/null and b/web/assets/thumbs/procity_fit_chiu_bin_01.png differ diff --git a/web/assets/thumbs/procity_fit_chiu_bin_half_01.png b/web/assets/thumbs/procity_fit_chiu_bin_half_01.png new file mode 100644 index 0000000..fdf4d06 Binary files /dev/null and b/web/assets/thumbs/procity_fit_chiu_bin_half_01.png differ diff --git a/web/assets/thumbs/procity_fit_chiu_bin_low_01.png b/web/assets/thumbs/procity_fit_chiu_bin_low_01.png new file mode 100644 index 0000000..bc21145 Binary files /dev/null and b/web/assets/thumbs/procity_fit_chiu_bin_low_01.png differ diff --git a/web/assets/thumbs/procity_fit_chiu_rack_long_01.png b/web/assets/thumbs/procity_fit_chiu_rack_long_01.png new file mode 100644 index 0000000..2f5e554 Binary files /dev/null and b/web/assets/thumbs/procity_fit_chiu_rack_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_chiu_rack_short_01.png b/web/assets/thumbs/procity_fit_chiu_rack_short_01.png new file mode 100644 index 0000000..aebf987 Binary files /dev/null and b/web/assets/thumbs/procity_fit_chiu_rack_short_01.png differ diff --git a/web/assets/thumbs/procity_fit_column_square_01.png b/web/assets/thumbs/procity_fit_column_square_01.png new file mode 100644 index 0000000..d9a1312 Binary files /dev/null and b/web/assets/thumbs/procity_fit_column_square_01.png differ diff --git a/web/assets/thumbs/procity_fit_column_wide_01.png b/web/assets/thumbs/procity_fit_column_wide_01.png new file mode 100644 index 0000000..13cbb08 Binary files /dev/null and b/web/assets/thumbs/procity_fit_column_wide_01.png differ diff --git a/web/assets/thumbs/procity_fit_concrete_plinth_01.png b/web/assets/thumbs/procity_fit_concrete_plinth_01.png new file mode 100644 index 0000000..78ee2af Binary files /dev/null and b/web/assets/thumbs/procity_fit_concrete_plinth_01.png differ diff --git a/web/assets/thumbs/procity_fit_counter_station_01.png b/web/assets/thumbs/procity_fit_counter_station_01.png new file mode 100644 index 0000000..fa2e7b6 Binary files /dev/null and b/web/assets/thumbs/procity_fit_counter_station_01.png differ diff --git a/web/assets/thumbs/procity_fit_counter_station_side_01.png b/web/assets/thumbs/procity_fit_counter_station_side_01.png new file mode 100644 index 0000000..5976e11 Binary files /dev/null and b/web/assets/thumbs/procity_fit_counter_station_side_01.png differ diff --git a/web/assets/thumbs/procity_fit_crate_blue_01.png b/web/assets/thumbs/procity_fit_crate_blue_01.png new file mode 100644 index 0000000..8609812 Binary files /dev/null and b/web/assets/thumbs/procity_fit_crate_blue_01.png differ diff --git a/web/assets/thumbs/procity_fit_crowd_barrier_01.png b/web/assets/thumbs/procity_fit_crowd_barrier_01.png new file mode 100644 index 0000000..dd3d4ec Binary files /dev/null and b/web/assets/thumbs/procity_fit_crowd_barrier_01.png differ diff --git a/web/assets/thumbs/procity_fit_deck_1200_01.png b/web/assets/thumbs/procity_fit_deck_1200_01.png new file mode 100644 index 0000000..3c93743 Binary files /dev/null and b/web/assets/thumbs/procity_fit_deck_1200_01.png differ diff --git a/web/assets/thumbs/procity_fit_deck_1200_rigged_01.png b/web/assets/thumbs/procity_fit_deck_1200_rigged_01.png new file mode 100644 index 0000000..4f6765b Binary files /dev/null and b/web/assets/thumbs/procity_fit_deck_1200_rigged_01.png differ diff --git a/web/assets/thumbs/procity_fit_distro_box_01.png b/web/assets/thumbs/procity_fit_distro_box_01.png new file mode 100644 index 0000000..4c12e64 Binary files /dev/null and b/web/assets/thumbs/procity_fit_distro_box_01.png differ diff --git a/web/assets/thumbs/procity_fit_door_shop_01.png b/web/assets/thumbs/procity_fit_door_shop_01.png new file mode 100644 index 0000000..d6cb8f2 Binary files /dev/null and b/web/assets/thumbs/procity_fit_door_shop_01.png differ diff --git a/web/assets/thumbs/procity_fit_drum_808_01.png b/web/assets/thumbs/procity_fit_drum_808_01.png new file mode 100644 index 0000000..70f222e Binary files /dev/null and b/web/assets/thumbs/procity_fit_drum_808_01.png differ diff --git a/web/assets/thumbs/procity_fit_drum_tr1000_01.png b/web/assets/thumbs/procity_fit_drum_tr1000_01.png new file mode 100644 index 0000000..e09d4d0 Binary files /dev/null and b/web/assets/thumbs/procity_fit_drum_tr1000_01.png differ diff --git a/web/assets/thumbs/procity_fit_fire_ext_01.png b/web/assets/thumbs/procity_fit_fire_ext_01.png new file mode 100644 index 0000000..f1ea5cf Binary files /dev/null and b/web/assets/thumbs/procity_fit_fire_ext_01.png differ diff --git a/web/assets/thumbs/procity_fit_firepit_01.png b/web/assets/thumbs/procity_fit_firepit_01.png new file mode 100644 index 0000000..51ff12c Binary files /dev/null and b/web/assets/thumbs/procity_fit_firepit_01.png differ diff --git a/web/assets/thumbs/procity_fit_flight_case_01.png b/web/assets/thumbs/procity_fit_flight_case_01.png new file mode 100644 index 0000000..de8129b Binary files /dev/null and b/web/assets/thumbs/procity_fit_flight_case_01.png differ diff --git a/web/assets/thumbs/procity_fit_headphones_hd25_01.png b/web/assets/thumbs/procity_fit_headphones_hd25_01.png new file mode 100644 index 0000000..d66c2b8 Binary files /dev/null and b/web/assets/thumbs/procity_fit_headphones_hd25_01.png differ diff --git a/web/assets/thumbs/procity_fit_headshell_technics_01.png b/web/assets/thumbs/procity_fit_headshell_technics_01.png new file mode 100644 index 0000000..3e92c54 Binary files /dev/null and b/web/assets/thumbs/procity_fit_headshell_technics_01.png differ diff --git a/web/assets/thumbs/procity_fit_jukebox_01.png b/web/assets/thumbs/procity_fit_jukebox_01.png new file mode 100644 index 0000000..c37eed4 Binary files /dev/null and b/web/assets/thumbs/procity_fit_jukebox_01.png differ diff --git a/web/assets/thumbs/procity_fit_keg_01.png b/web/assets/thumbs/procity_fit_keg_01.png new file mode 100644 index 0000000..aab2056 Binary files /dev/null and b/web/assets/thumbs/procity_fit_keg_01.png differ diff --git a/web/assets/thumbs/procity_fit_library_trolley_01.png b/web/assets/thumbs/procity_fit_library_trolley_01.png new file mode 100644 index 0000000..15566d2 Binary files /dev/null and b/web/assets/thumbs/procity_fit_library_trolley_01.png differ diff --git a/web/assets/thumbs/procity_fit_lounge_chair_01.png b/web/assets/thumbs/procity_fit_lounge_chair_01.png new file mode 100644 index 0000000..bb2244e Binary files /dev/null and b/web/assets/thumbs/procity_fit_lounge_chair_01.png differ diff --git a/web/assets/thumbs/procity_fit_marble_sink_01.png b/web/assets/thumbs/procity_fit_marble_sink_01.png new file mode 100644 index 0000000..2b83496 Binary files /dev/null and b/web/assets/thumbs/procity_fit_marble_sink_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_mx2200_01.png b/web/assets/thumbs/procity_fit_mixer_mx2200_01.png new file mode 100644 index 0000000..0e1b046 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_mx2200_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc05pro_01.png b/web/assets/thumbs/procity_fit_mixer_pmc05pro_01.png new file mode 100644 index 0000000..64eecb7 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc05pro_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc05pro_rigged_01.png b/web/assets/thumbs/procity_fit_mixer_pmc05pro_rigged_01.png new file mode 100644 index 0000000..26380c4 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc05pro_rigged_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc06pro_01.png b/web/assets/thumbs/procity_fit_mixer_pmc06pro_01.png new file mode 100644 index 0000000..84eba8e Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc06pro_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc07pro_01.png b/web/assets/thumbs/procity_fit_mixer_pmc07pro_01.png new file mode 100644 index 0000000..72f3b5c Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc07pro_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc08pro_01.png b/web/assets/thumbs/procity_fit_mixer_pmc08pro_01.png new file mode 100644 index 0000000..9ff8db7 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc08pro_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc10_01.png b/web/assets/thumbs/procity_fit_mixer_pmc10_01.png new file mode 100644 index 0000000..92b82b2 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc10_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmc20_01.png b/web/assets/thumbs/procity_fit_mixer_pmc20_01.png new file mode 100644 index 0000000..7e3aa38 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmc20_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_pmx9000_01.png b/web/assets/thumbs/procity_fit_mixer_pmx9000_01.png new file mode 100644 index 0000000..d66029c Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_pmx9000_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_shdj1200_01.png b/web/assets/thumbs/procity_fit_mixer_shdj1200_01.png new file mode 100644 index 0000000..555c437 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_shdj1200_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_shdj1200_rigged_01.png b/web/assets/thumbs/procity_fit_mixer_shdj1200_rigged_01.png new file mode 100644 index 0000000..8bc416e Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_shdj1200_rigged_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_ttm54i_01.png b/web/assets/thumbs/procity_fit_mixer_ttm54i_01.png new file mode 100644 index 0000000..1739161 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_ttm54i_01.png differ diff --git a/web/assets/thumbs/procity_fit_mixer_ttm54i_rigged_01.png b/web/assets/thumbs/procity_fit_mixer_ttm54i_rigged_01.png new file mode 100644 index 0000000..e75ab15 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mixer_ttm54i_rigged_01.png differ diff --git a/web/assets/thumbs/procity_fit_mos_bin_bottom_01.png b/web/assets/thumbs/procity_fit_mos_bin_bottom_01.png new file mode 100644 index 0000000..3b215b4 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mos_bin_bottom_01.png differ diff --git a/web/assets/thumbs/procity_fit_mos_bin_lower_mid_01.png b/web/assets/thumbs/procity_fit_mos_bin_lower_mid_01.png new file mode 100644 index 0000000..3aed77b Binary files /dev/null and b/web/assets/thumbs/procity_fit_mos_bin_lower_mid_01.png differ diff --git a/web/assets/thumbs/procity_fit_mos_bin_top_01.png b/web/assets/thumbs/procity_fit_mos_bin_top_01.png new file mode 100644 index 0000000..724c034 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mos_bin_top_01.png differ diff --git a/web/assets/thumbs/procity_fit_mos_bin_upper_mid_01.png b/web/assets/thumbs/procity_fit_mos_bin_upper_mid_01.png new file mode 100644 index 0000000..7bb3600 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mos_bin_upper_mid_01.png differ diff --git a/web/assets/thumbs/procity_fit_mos_stack_01.png b/web/assets/thumbs/procity_fit_mos_stack_01.png new file mode 100644 index 0000000..473eeb3 Binary files /dev/null and b/web/assets/thumbs/procity_fit_mos_stack_01.png differ diff --git a/web/assets/thumbs/procity_fit_old_couch_01.png b/web/assets/thumbs/procity_fit_old_couch_01.png new file mode 100644 index 0000000..b476e2d Binary files /dev/null and b/web/assets/thumbs/procity_fit_old_couch_01.png differ diff --git a/web/assets/thumbs/procity_fit_pallet_01.png b/web/assets/thumbs/procity_fit_pallet_01.png new file mode 100644 index 0000000..f4e26d7 Binary files /dev/null and b/web/assets/thumbs/procity_fit_pallet_01.png differ diff --git a/web/assets/thumbs/procity_fit_picnic_table_01.png b/web/assets/thumbs/procity_fit_picnic_table_01.png new file mode 100644 index 0000000..4d770f2 Binary files /dev/null and b/web/assets/thumbs/procity_fit_picnic_table_01.png differ diff --git a/web/assets/thumbs/procity_fit_pie_warmer_01.png b/web/assets/thumbs/procity_fit_pie_warmer_01.png new file mode 100644 index 0000000..ef57f20 Binary files /dev/null and b/web/assets/thumbs/procity_fit_pie_warmer_01.png differ diff --git a/web/assets/thumbs/procity_fit_planter_01.png b/web/assets/thumbs/procity_fit_planter_01.png new file mode 100644 index 0000000..8559417 Binary files /dev/null and b/web/assets/thumbs/procity_fit_planter_01.png differ diff --git a/web/assets/thumbs/procity_fit_plywood_bar_01.png b/web/assets/thumbs/procity_fit_plywood_bar_01.png new file mode 100644 index 0000000..2a960de Binary files /dev/null and b/web/assets/thumbs/procity_fit_plywood_bar_01.png differ diff --git a/web/assets/thumbs/procity_fit_pokie_01.png b/web/assets/thumbs/procity_fit_pokie_01.png new file mode 100644 index 0000000..ec4855f Binary files /dev/null and b/web/assets/thumbs/procity_fit_pokie_01.png differ diff --git a/web/assets/thumbs/procity_fit_rack_bin_wooden_01.png b/web/assets/thumbs/procity_fit_rack_bin_wooden_01.png new file mode 100644 index 0000000..a41250d Binary files /dev/null and b/web/assets/thumbs/procity_fit_rack_bin_wooden_01.png differ diff --git a/web/assets/thumbs/procity_fit_raffle_drum_01.png b/web/assets/thumbs/procity_fit_raffle_drum_01.png new file mode 100644 index 0000000..8d6a441 Binary files /dev/null and b/web/assets/thumbs/procity_fit_raffle_drum_01.png differ diff --git a/web/assets/thumbs/procity_fit_rope_post_01.png b/web/assets/thumbs/procity_fit_rope_post_01.png new file mode 100644 index 0000000..b6ed5b9 Binary files /dev/null and b/web/assets/thumbs/procity_fit_rope_post_01.png differ diff --git a/web/assets/thumbs/procity_fit_sampler_mpc3000_01.png b/web/assets/thumbs/procity_fit_sampler_mpc3000_01.png new file mode 100644 index 0000000..a28d737 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sampler_mpc3000_01.png differ diff --git a/web/assets/thumbs/procity_fit_sampler_s900_01.png b/web/assets/thumbs/procity_fit_sampler_s900_01.png new file mode 100644 index 0000000..6996f57 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sampler_s900_01.png differ diff --git a/web/assets/thumbs/procity_fit_sampler_sp1200_01.png b/web/assets/thumbs/procity_fit_sampler_sp1200_01.png new file mode 100644 index 0000000..5c2f978 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sampler_sp1200_01.png differ diff --git a/web/assets/thumbs/procity_fit_sandwich_board_01.png b/web/assets/thumbs/procity_fit_sandwich_board_01.png new file mode 100644 index 0000000..ecdb5f9 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sandwich_board_01.png differ diff --git a/web/assets/thumbs/procity_fit_shelf_board_long_01.png b/web/assets/thumbs/procity_fit_shelf_board_long_01.png new file mode 100644 index 0000000..881150e Binary files /dev/null and b/web/assets/thumbs/procity_fit_shelf_board_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_shelf_board_short_01.png b/web/assets/thumbs/procity_fit_shelf_board_short_01.png new file mode 100644 index 0000000..7f2ec37 Binary files /dev/null and b/web/assets/thumbs/procity_fit_shelf_board_short_01.png differ diff --git a/web/assets/thumbs/procity_fit_sl_deck_1_01.png b/web/assets/thumbs/procity_fit_sl_deck_1_01.png new file mode 100644 index 0000000..bfae049 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sl_deck_1_01.png differ diff --git a/web/assets/thumbs/procity_fit_sl_deck_2_01.png b/web/assets/thumbs/procity_fit_sl_deck_2_01.png new file mode 100644 index 0000000..c16c037 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sl_deck_2_01.png differ diff --git a/web/assets/thumbs/procity_fit_sl_deck_3_01.png b/web/assets/thumbs/procity_fit_sl_deck_3_01.png new file mode 100644 index 0000000..f22422e Binary files /dev/null and b/web/assets/thumbs/procity_fit_sl_deck_3_01.png differ diff --git a/web/assets/thumbs/procity_fit_sl_deck_4_01.png b/web/assets/thumbs/procity_fit_sl_deck_4_01.png new file mode 100644 index 0000000..1121e8f Binary files /dev/null and b/web/assets/thumbs/procity_fit_sl_deck_4_01.png differ diff --git a/web/assets/thumbs/procity_fit_smoke_machine_01.png b/web/assets/thumbs/procity_fit_smoke_machine_01.png new file mode 100644 index 0000000..655bef5 Binary files /dev/null and b/web/assets/thumbs/procity_fit_smoke_machine_01.png differ diff --git a/web/assets/thumbs/procity_fit_speaker_stack_01.png b/web/assets/thumbs/procity_fit_speaker_stack_01.png new file mode 100644 index 0000000..31fba8a Binary files /dev/null and b/web/assets/thumbs/procity_fit_speaker_stack_01.png differ diff --git a/web/assets/thumbs/procity_fit_strobe_light_01.png b/web/assets/thumbs/procity_fit_strobe_light_01.png new file mode 100644 index 0000000..539ed00 Binary files /dev/null and b/web/assets/thumbs/procity_fit_strobe_light_01.png differ diff --git a/web/assets/thumbs/procity_fit_sub_bass_01.png b/web/assets/thumbs/procity_fit_sub_bass_01.png new file mode 100644 index 0000000..3f986f7 Binary files /dev/null and b/web/assets/thumbs/procity_fit_sub_bass_01.png differ diff --git a/web/assets/thumbs/procity_fit_topiary_01.png b/web/assets/thumbs/procity_fit_topiary_01.png new file mode 100644 index 0000000..b956a83 Binary files /dev/null and b/web/assets/thumbs/procity_fit_topiary_01.png differ diff --git a/web/assets/thumbs/procity_fit_tub_black_01.png b/web/assets/thumbs/procity_fit_tub_black_01.png new file mode 100644 index 0000000..bb714a8 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tub_black_01.png differ diff --git a/web/assets/thumbs/procity_fit_tub_black_long_01.png b/web/assets/thumbs/procity_fit_tub_black_long_01.png new file mode 100644 index 0000000..8ee1940 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tub_black_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_tub_white_01.png b/web/assets/thumbs/procity_fit_tub_white_01.png new file mode 100644 index 0000000..a7edf38 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tub_white_01.png differ diff --git a/web/assets/thumbs/procity_fit_tub_white_long_01.png b/web/assets/thumbs/procity_fit_tub_white_long_01.png new file mode 100644 index 0000000..5d606f0 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tub_white_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_3x3_01.png b/web/assets/thumbs/procity_fit_tubs_3x3_01.png new file mode 100644 index 0000000..378dda1 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_3x3_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_3x3_long_01.png b/web/assets/thumbs/procity_fit_tubs_3x3_long_01.png new file mode 100644 index 0000000..c187026 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_3x3_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_4x2_vertical_01.png b/web/assets/thumbs/procity_fit_tubs_4x2_vertical_01.png new file mode 100644 index 0000000..559454b Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_4x2_vertical_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_4x2_wide_01.png b/web/assets/thumbs/procity_fit_tubs_4x2_wide_01.png new file mode 100644 index 0000000..eca87a8 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_4x2_wide_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_shelf_long_01.png b/web/assets/thumbs/procity_fit_tubs_shelf_long_01.png new file mode 100644 index 0000000..f9fa320 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_shelf_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_tubs_shelf_wide_01.png b/web/assets/thumbs/procity_fit_tubs_shelf_wide_01.png new file mode 100644 index 0000000..c8b4a85 Binary files /dev/null and b/web/assets/thumbs/procity_fit_tubs_shelf_wide_01.png differ diff --git a/web/assets/thumbs/procity_fit_urinal_trough_01.png b/web/assets/thumbs/procity_fit_urinal_trough_01.png new file mode 100644 index 0000000..e171701 Binary files /dev/null and b/web/assets/thumbs/procity_fit_urinal_trough_01.png differ diff --git a/web/assets/thumbs/procity_fit_vinyl_sleeve_01.png b/web/assets/thumbs/procity_fit_vinyl_sleeve_01.png new file mode 100644 index 0000000..09608a1 Binary files /dev/null and b/web/assets/thumbs/procity_fit_vinyl_sleeve_01.png differ diff --git a/web/assets/thumbs/procity_fit_wall_shelves_big_01.png b/web/assets/thumbs/procity_fit_wall_shelves_big_01.png new file mode 100644 index 0000000..75a7bf4 Binary files /dev/null and b/web/assets/thumbs/procity_fit_wall_shelves_big_01.png differ diff --git a/web/assets/thumbs/procity_fit_wall_shelves_long_01.png b/web/assets/thumbs/procity_fit_wall_shelves_long_01.png new file mode 100644 index 0000000..d2e8c97 Binary files /dev/null and b/web/assets/thumbs/procity_fit_wall_shelves_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_wheelie_bin_01.png b/web/assets/thumbs/procity_fit_wheelie_bin_01.png new file mode 100644 index 0000000..f3a6d49 Binary files /dev/null and b/web/assets/thumbs/procity_fit_wheelie_bin_01.png differ diff --git a/web/assets/thumbs/procity_fit_window_booth_01.png b/web/assets/thumbs/procity_fit_window_booth_01.png new file mode 100644 index 0000000..fbd9767 Binary files /dev/null and b/web/assets/thumbs/procity_fit_window_booth_01.png differ diff --git a/web/assets/thumbs/procity_fit_wooden_rack_long_01.png b/web/assets/thumbs/procity_fit_wooden_rack_long_01.png new file mode 100644 index 0000000..f31a6d1 Binary files /dev/null and b/web/assets/thumbs/procity_fit_wooden_rack_long_01.png differ diff --git a/web/assets/thumbs/procity_fit_wooden_rack_wide_01.png b/web/assets/thumbs/procity_fit_wooden_rack_wide_01.png new file mode 100644 index 0000000..39632ba Binary files /dev/null and b/web/assets/thumbs/procity_fit_wooden_rack_wide_01.png differ diff --git a/web/assets/thumbs/procity_fit_zip_drive_01.png b/web/assets/thumbs/procity_fit_zip_drive_01.png new file mode 100644 index 0000000..fe779b9 Binary files /dev/null and b/web/assets/thumbs/procity_fit_zip_drive_01.png differ diff --git a/web/models/clips/browse.glb b/web/models/clips/browse.glb new file mode 100644 index 0000000..d1e9541 Binary files /dev/null and b/web/models/clips/browse.glb differ diff --git a/web/models/clips/idles.glb b/web/models/clips/idles.glb new file mode 100644 index 0000000..b2a8be9 Binary files /dev/null and b/web/models/clips/idles.glb differ diff --git a/web/models/clips/locomotion.glb b/web/models/clips/locomotion.glb new file mode 100644 index 0000000..0e06fdc Binary files /dev/null and b/web/models/clips/locomotion.glb differ diff --git a/web/models/clips/sitlean.glb b/web/models/clips/sitlean.glb new file mode 100644 index 0000000..06265fa Binary files /dev/null and b/web/models/clips/sitlean.glb differ diff --git a/web/models/clips/social.glb b/web/models/clips/social.glb new file mode 100644 index 0000000..825594d Binary files /dev/null and b/web/models/clips/social.glb differ diff --git a/web/models/clips/venue.glb b/web/models/clips/venue.glb new file mode 100644 index 0000000..de2b4ad Binary files /dev/null and b/web/models/clips/venue.glb differ