Lane E round 4 (E2): fix record_crate + ship counter-with-till, both live on depot
- record_crate: R3 normalize left a sourceless texture (packed AO/roughness map didn't survive WebP export; metallicRoughness+occlusion pointed at it) → three.js 'undefined uri' crash. New pipeline/fix_glb_textures.py strips material slots that reference a sourceless texture. Verified loads in vendored GLTFLoader; re-published. - counter_till (new fitting): ~2m timber counter + period till, flux_local→trellis_mac on-device, 1.61x0.64m / 1.15m tall, 32k tris. Verified loads; published. Lane C maps kind counter -> counter_till (balcao 4m 'counter' kept for long-bar use). - publish.py: --only <name> filter + passwordless tailnet detection (GOD3_DEPOT override). - manifest rebuilt (15 fittings, alias counter->counter_till); validate --depot 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aad9baa584
commit
dafb079ff4
@ -4,6 +4,33 @@
|
|||||||
Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in
|
Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in
|
||||||
other lanes' files was touched.*
|
other lanes' files was touched.*
|
||||||
|
|
||||||
|
## Round 4 (Fable) — ped decimation (gate-3 critical path) + asset fixes
|
||||||
|
|
||||||
|
- **E1 — ped decimation: DONE + committed** (`518678d`, ref `lane-e/round4-peds`). The 19 ped rigs
|
||||||
|
in `web/models/peds/` were **924k tris total** (31k–56k each) — the gate-3 blocker. Decimated to
|
||||||
|
**≤3k each** (1564–2805; fleet **924k→46.5k**, ~2.45k avg) via `gltf-transform weld → simplify
|
||||||
|
--ratio 0.05 --error 0.12` (meshoptimizer — works on these manifold skinned meshes, unlike R3
|
||||||
|
TRELLIS shell-soup). **Skinning + skeleton fully preserved** (65 joints intact, `walk`/`idle`
|
||||||
|
untouched); contact sheet `docs/shots/laneE/peds_decimated.png` clean at street distance. Originals
|
||||||
|
gitignored in `pipeline/.peds_orig/`; per-ped in `pipeline/_peds_decim.json`. → **Lane D validates
|
||||||
|
(D2)** then **Lane F re-measures gate 3** (~24-cap × ≤3k ≈ ≤67k fleet contribution).
|
||||||
|
- **E2a — `record_crate` FIXED, verified + published.** Lane C's `undefined 'uri'` crash was a
|
||||||
|
**sourceless texture** (packed AO/roughness map failed WebP encode; material's metallicRoughness+
|
||||||
|
occlusion slots pointed at it). New tool `pipeline/fix_glb_textures.py` strips those slots (three.js
|
||||||
|
loads textures lazily → no crash). **Verified it loads in the real vendored `GLTFLoader`** (meshes=1,
|
||||||
|
tris=5000). **Re-published to the depot** (the fixed version is live, tex refs all-sourced).
|
||||||
|
- **E2b — counter-with-till (~2 m): LIVE.** New `counter_till` fitting
|
||||||
|
(`procity_fit_counter_till_01.glb`, **1.61 × 0.64 m, 1.15 m tall**, 32k tris) — timber counter +
|
||||||
|
cabinet doors + period till. Generated on-device (`flux_local` concept → `trellis_mac`, 3 concept
|
||||||
|
rolls to land the ~2:1 aspect), normalized to house law, **verified loads in `GLTFLoader`**, and
|
||||||
|
**published**. Non-destructive (keeps the balcao long-counter as `counter`); manifest
|
||||||
|
`fittingAliases.counter` → `counter_till`; Lane C re-maps kind `counter` → `counter_till` in `glb.js`.
|
||||||
|
- **Publish: DONE** (John approved). Both re-exports live; **manifest `validate --depot` GREEN
|
||||||
|
(0 err, 23 GLBs live)**. `publish.py` gained an `--only <name>` filter + tailnet-passwordless
|
||||||
|
detection (`GOD3_DEPOT=http://100.94.195.115:8788`, decision #4).
|
||||||
|
- **Not doing** (per Fable decisions): fal.ai spend (glass-case/bus-shelter parked); hero-prop
|
||||||
|
bake-to-lowpoly.
|
||||||
|
|
||||||
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
|
## Round 3 (Fable) — hero props on MODELBEAST + local-depot mode
|
||||||
|
|
||||||
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`
|
- **Hero props, free & on-device** (`pipeline/gen_props.py`): `flux_local` concept image → `trellis_mac`
|
||||||
|
|||||||
49
docs/LANES/LANE_E_NOTES.md
Normal file
49
docs/LANES/LANE_E_NOTES.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# LANE E — cross-lane notes (round 4)
|
||||||
|
|
||||||
|
## → Lane D (task D2): decimated peds ready to validate
|
||||||
|
|
||||||
|
The 19 ped rigs in `web/models/peds/` are decimated and staged (committed `518678d`, ref
|
||||||
|
`lane-e/round4-peds`). Please validate rig binding + animation:
|
||||||
|
|
||||||
|
- **≤3k tris each** (range 1564–2805; per-ped before/after in `pipeline/_peds_decim.json`).
|
||||||
|
Fleet total **924k → 46.5k tris** (48.6k → 2.45k avg). At the 24-rig near cap ≈ **≤67k**.
|
||||||
|
- **Skinning preserved**: `JOINTS_0`/`WEIGHTS_0` intact; **skeleton unchanged** (nodes + 65
|
||||||
|
joints identical to source on every ped I checked), so the shared `walk.glb`/`idle.glb`
|
||||||
|
clips retarget unchanged. **`walk.glb`/`idle.glb` were NOT touched.**
|
||||||
|
- Method: `gltf-transform weld` → `simplify --ratio 0.05 --error 0.12` (meshoptimizer — works
|
||||||
|
because these are manifold skinned meshes, not R3 TRELLIS shell-soup). Contact sheet:
|
||||||
|
`docs/shots/laneE/peds_decimated.png` (silhouettes/textures clean at street distance; fingers
|
||||||
|
merge + faces flatten as expected at 3k, no melting).
|
||||||
|
- Originals backed up (gitignored) at `pipeline/.peds_orig/` if you need a before/after diff.
|
||||||
|
|
||||||
|
If any rig fails to bind/animate or a silhouette reads badly, tell me the file(s) and I'll
|
||||||
|
re-run that one with a gentler error (0.08 keeps ~50% more tris but still <5k). Then F re-measures
|
||||||
|
gate 3 after your sign-off.
|
||||||
|
|
||||||
|
## → Lane C: E2 asset re-exports
|
||||||
|
|
||||||
|
1. **`procity_fit_record_crate_01.glb` — FIXED.** Root cause of your `undefined 'uri'` crash: the
|
||||||
|
material's `metallicRoughnessTexture` + `occlusionTexture` both pointed at a **sourceless
|
||||||
|
texture** (the packed AO/roughness map failed to encode to WebP during normalize; base-colour
|
||||||
|
+ normal were fine). Fix: `pipeline/fix_glb_textures.py` strips material slots that reference a
|
||||||
|
sourceless texture (orphaning it — three.js loads textures lazily, so no crash). **Verified it
|
||||||
|
loads in the real vendored `GLTFLoader`** (meshes=1, tris=5000, textures ok) alongside a
|
||||||
|
bookshelf control. Re-published to the depot — re-map `record_crate`/`recordBin` in `glb.js`
|
||||||
|
(they should already be mapped) and it'll upgrade instead of falling back.
|
||||||
|
2. **counter-with-till — LIVE.** New fitting `counter_till` (`procity_fit_counter_till_01.glb`):
|
||||||
|
**1.61 × 0.64 m footprint, 1.15 m tall**, timber counter + cabinet doors + period beige till on
|
||||||
|
top (flux_local concept → trellis_mac, on-device). Fixes the R3 4 m till-less counter that
|
||||||
|
squashed. **Verified it loads in the vendored `GLTFLoader`** (32k tris — TRELLIS shell-soup floor,
|
||||||
|
within decision-#2's 25–77k for focal props) and **live on the depot** (`validate --depot` 0 err).
|
||||||
|
→ **In `glb.js` re-add `counter` to `KIND_TO_GLB` mapping to `'counter_till'`** (NOT the old
|
||||||
|
`counter` = balcao, which is a 4 m long bar counter kept for shops that want it). The manifest
|
||||||
|
`fittingAliases.counter` → `counter_till` too. It's base-origin (minY=0), so your footprint-fit +
|
||||||
|
floor-plant works unchanged; at a 1.4–2.0 m counter slot it scales to a believable ~1.0–1.4 m
|
||||||
|
tall counter.
|
||||||
|
|
||||||
|
## → Fable: status
|
||||||
|
|
||||||
|
- **E1 (gate-3 critical path): DONE.** Peds ≤3k, committed + pinned. Unblocks D2 → F3.
|
||||||
|
- **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.
|
||||||
BIN
docs/shots/laneE/counter_till.png
Normal file
BIN
docs/shots/laneE/counter_till.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
@ -179,3 +179,18 @@ intact, skeleton unchanged (nodes + 65 joints identical to source), so the share
|
|||||||
(fine for street distance). Originals backed up (gitignored) at `pipeline/.peds_orig/`; per-ped
|
(fine for street distance). Originals backed up (gitignored) at `pipeline/.peds_orig/`; per-ped
|
||||||
before/after in `pipeline/_peds_decim.json`. Lane D validates rig binding + anims (task D2); Lane F
|
before/after in `pipeline/_peds_decim.json`. Lane D validates rig binding + anims (task D2); Lane F
|
||||||
re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution).
|
re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution).
|
||||||
|
|
||||||
|
## Round 4 — E2 asset re-exports (published)
|
||||||
|
|
||||||
|
- **`procity_fit_record_crate_01.glb` re-export (fixed).** The R3 normalize left a **sourceless
|
||||||
|
texture** (packed AO/roughness map didn't survive the WebP export; the material's
|
||||||
|
`metallicRoughnessTexture` + `occlusionTexture` pointed at it), which crashed three.js GLTFLoader
|
||||||
|
with `undefined 'uri'`. `pipeline/fix_glb_textures.py` strips material slots that reference a
|
||||||
|
sourceless texture (base-colour + normal kept; the orphan is never loaded). Verified it loads in
|
||||||
|
the vendored `GLTFLoader` (5000 tris, textures ok); re-published to the depot.
|
||||||
|
- **`procity_fit_counter_till_01.glb` (new).** ~2 m timber shop counter with a period beige till —
|
||||||
|
`flux_local` concept (3 rolls for the ~2:1 aspect) → `trellis_mac` mesh → `normalize.py`. 1.61 ×
|
||||||
|
0.64 m, 1.15 m tall, 32k tris (TRELLIS shell-soup floor, within decision-#2's 25–77k for focal
|
||||||
|
props). **Provenance: 100% synthetic/on-device** (FLUX.2-klein-4B + TRELLIS.2 on the M3 Ultra), no
|
||||||
|
third-party source — cleanest licence zone. Replaces the R3 4 m till-less counter for the interior
|
||||||
|
counter slot (Lane C maps `counter` → `counter_till`); the balcao 4 m `counter` stays for long-bar use.
|
||||||
|
|||||||
@ -84,6 +84,23 @@
|
|||||||
1.91,
|
1.91,
|
||||||
0.93
|
0.93
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/_m_counter3/trellis2_counter_till_v3.glb",
|
||||||
|
"file": "procity_fit_counter_till_01.glb",
|
||||||
|
"out": "/tmp/counter_norm/procity_fit_counter_till_01.glb",
|
||||||
|
"thumb": "web/assets/thumbs/procity_fit_counter_till_01.png",
|
||||||
|
"tri_before": 195285,
|
||||||
|
"tri_after": 32234,
|
||||||
|
"size_m": [
|
||||||
|
1.609,
|
||||||
|
1.15,
|
||||||
|
0.642
|
||||||
|
],
|
||||||
|
"footprint": [
|
||||||
|
1.61,
|
||||||
|
0.64
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"errors": []
|
"errors": []
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
"procity_fit_clothes_rack_01.glb",
|
"procity_fit_clothes_rack_01.glb",
|
||||||
"procity_fit_coffee_table_01.glb",
|
"procity_fit_coffee_table_01.glb",
|
||||||
"procity_fit_counter_01.glb",
|
"procity_fit_counter_01.glb",
|
||||||
|
"procity_fit_counter_till_01.glb",
|
||||||
"procity_fit_cube_shelf_wide_01.glb",
|
"procity_fit_cube_shelf_wide_01.glb",
|
||||||
"procity_fit_drinks_fridge_01.glb",
|
"procity_fit_drinks_fridge_01.glb",
|
||||||
"procity_fit_listening_booth_01.glb",
|
"procity_fit_listening_booth_01.glb",
|
||||||
@ -19,6 +20,5 @@
|
|||||||
"procity_street_longbench_01.glb",
|
"procity_street_longbench_01.glb",
|
||||||
"procity_street_novelty_record_01.glb",
|
"procity_street_novelty_record_01.glb",
|
||||||
"procity_street_park_bench_01.glb",
|
"procity_street_park_bench_01.glb",
|
||||||
"procity_street_streetlight_01.glb",
|
"procity_street_streetlight_01.glb"
|
||||||
"vintage-cash-register.glb"
|
|
||||||
]
|
]
|
||||||
3
pipeline/batch_counter.json
Normal file
3
pipeline/batch_counter.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
{"src": "/Users/m3ultra/Documents/procity/pipeline/.genprops/_m_counter3/trellis2_counter_till_v3.glb", "id": "counter_till", "category": "fit", "height_m": 1.15, "tris": 8000}
|
||||||
|
]
|
||||||
@ -168,7 +168,8 @@ def build():
|
|||||||
# stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings
|
# stay primitives — see MESHGOD_BATCH.md). Lets a consumer that iterates registry SHOP_TYPES.fittings
|
||||||
# resolve to a GLB. Lane C's glb.js already maps its own kinds straight to the physical ids below.
|
# resolve to a GLB. Lane C's glb.js already maps its own kinds straight to the physical ids below.
|
||||||
fitting_aliases = {
|
fitting_aliases = {
|
||||||
"record_bins": "record_crate", "crates": "box_crate", "counter": "counter",
|
# registry 'counter' → the ~2m counter-with-till (counter_till), NOT the 4m balcao 'counter'
|
||||||
|
"record_bins": "record_crate", "crates": "box_crate", "counter": "counter_till",
|
||||||
"clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf",
|
"clothes_racks": "clothes_rack", "bricabrac_shelving": "wire_shelf", "book_wall": "bookshelf",
|
||||||
"cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table",
|
"cube_shelves": "cube_shelf_wide", "display_tables": "coffee_table",
|
||||||
"bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table",
|
"bookshelves": "bookshelf", "vhs_shelving": "wire_shelf", "trestle_tables": "work_table",
|
||||||
|
|||||||
78
pipeline/fix_glb_textures.py
Normal file
78
pipeline/fix_glb_textures.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Repair a GLB whose material references a texture with no image source.
|
||||||
|
|
||||||
|
Blender's WebP glTF export occasionally emits a texture slot whose image failed to encode
|
||||||
|
(no `source`, no `EXT_texture_webp.source`) — e.g. a packed metallic-roughness/AO map. three.js
|
||||||
|
GLTFLoader then throws `Cannot read properties of undefined (reading 'uri')` when a material points
|
||||||
|
at it. This strips every material texture-slot that points at a sourceless texture (base-colour /
|
||||||
|
normal are kept; the orphaned texture is never loaded because GLTFLoader loads textures lazily),
|
||||||
|
then repacks the GLB (BIN chunk untouched).
|
||||||
|
|
||||||
|
python3 fix_glb_textures.py in.glb out.glb
|
||||||
|
"""
|
||||||
|
import json, struct, sys
|
||||||
|
|
||||||
|
SLOTS = ("baseColorTexture", "metallicRoughnessTexture", "normalTexture",
|
||||||
|
"occlusionTexture", "emissiveTexture")
|
||||||
|
|
||||||
|
|
||||||
|
def read_glb(path):
|
||||||
|
d = open(path, "rb").read()
|
||||||
|
off, gltf, chunks = 12, None, []
|
||||||
|
while off < len(d):
|
||||||
|
clen, ctype = struct.unpack("<II", d[off:off + 8])
|
||||||
|
body = d[off + 8: off + 8 + clen]
|
||||||
|
if ctype == 0x4E4F534A:
|
||||||
|
gltf = json.loads(body.decode("utf-8"))
|
||||||
|
chunks.append((ctype, body))
|
||||||
|
off += 8 + clen
|
||||||
|
return gltf, chunks
|
||||||
|
|
||||||
|
|
||||||
|
def has_source(tex):
|
||||||
|
if "source" in tex:
|
||||||
|
return True
|
||||||
|
ext = tex.get("extensions", {})
|
||||||
|
return any("source" in v for v in ext.values() if isinstance(v, dict))
|
||||||
|
|
||||||
|
|
||||||
|
def repair(gltf):
|
||||||
|
textures = gltf.get("textures", [])
|
||||||
|
bad = {i for i, t in enumerate(textures) if not has_source(t)}
|
||||||
|
if not bad:
|
||||||
|
return 0
|
||||||
|
stripped = 0
|
||||||
|
for m in gltf.get("materials", []):
|
||||||
|
pbr = m.get("pbrMetallicRoughness", {})
|
||||||
|
for container in (m, pbr):
|
||||||
|
for slot in list(container.keys()):
|
||||||
|
if slot in SLOTS and isinstance(container[slot], dict) \
|
||||||
|
and container[slot].get("index") in bad:
|
||||||
|
del container[slot]
|
||||||
|
stripped += 1
|
||||||
|
return stripped
|
||||||
|
|
||||||
|
|
||||||
|
def write_glb(gltf, chunks, out):
|
||||||
|
new_json = json.dumps(gltf, separators=(",", ":")).encode("utf-8")
|
||||||
|
new_json += b" " * ((4 - len(new_json) % 4) % 4) # pad to 4 bytes
|
||||||
|
body = struct.pack("<II", len(new_json), 0x4E4F534A) + new_json
|
||||||
|
for ctype, data in chunks:
|
||||||
|
if ctype == 0x4E4F534A:
|
||||||
|
continue
|
||||||
|
pad = b"\x00" * ((4 - len(data) % 4) % 4)
|
||||||
|
body += struct.pack("<II", len(data) + len(pad), ctype) + data + pad
|
||||||
|
header = struct.pack("<III", 0x46546C67, 2, 12 + len(body))
|
||||||
|
open(out, "wb").write(header + body)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
inp, out = sys.argv[1], sys.argv[2]
|
||||||
|
gltf, chunks = read_glb(inp)
|
||||||
|
n = repair(gltf)
|
||||||
|
write_glb(gltf, chunks, out)
|
||||||
|
print(f"stripped {n} material slot(s) pointing at sourceless textures → {out}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -60,13 +60,20 @@ def post(path, body, ctype):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
glbs = sorted(g for g in glob.glob(NORM + "/*.glb") if os.path.basename(g) not in SKIP)
|
only = sys.argv[sys.argv.index("--only") + 1] if "--only" in sys.argv else ""
|
||||||
|
# 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 not glbs:
|
if not glbs:
|
||||||
print(f"no GLBs in {NORM} — run normalize.py first"); sys.exit(1)
|
print(f"no GLBs in {NORM} matching {only!r} — run normalize.py first"); sys.exit(1)
|
||||||
print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}")
|
print(f"depot: {DEPOT} assets: {len(glbs)} {'(DRY RUN)' if DRY else ''}"
|
||||||
if not DRY and not PW:
|
+ (" [tailnet passwordless]" if tailnet else ""))
|
||||||
print("ERROR: GOD3_PW not set in environment. Ask John for the depot password, then:\n"
|
if not DRY and not PW and not tailnet:
|
||||||
" export GOD3_PW=... && python3 pipeline/publish.py")
|
print("ERROR: GOD3_PW not set and no tailnet GOD3_DEPOT. Either:\n"
|
||||||
|
" export GOD3_DEPOT=http://100.94.195.115:8788 # passwordless from an allowlisted box\n"
|
||||||
|
" or export GOD3_PW=... # public depot (ask John)")
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
published = []
|
published = []
|
||||||
for g in glbs:
|
for g in glbs:
|
||||||
|
|||||||
@ -684,6 +684,15 @@
|
|||||||
"height": 0.5,
|
"height": 0.5,
|
||||||
"thumb": "thumbs/procity_fit_milkshake_mixer_01.png"
|
"thumb": "thumbs/procity_fit_milkshake_mixer_01.png"
|
||||||
},
|
},
|
||||||
|
"counter_till": {
|
||||||
|
"file": "procity_fit_counter_till_01.glb",
|
||||||
|
"footprint": [
|
||||||
|
1.61,
|
||||||
|
0.64
|
||||||
|
],
|
||||||
|
"height": 1.15,
|
||||||
|
"thumb": "thumbs/procity_fit_counter_till_01.png"
|
||||||
|
},
|
||||||
"cash_register": {
|
"cash_register": {
|
||||||
"file": "vintage-cash-register.glb",
|
"file": "vintage-cash-register.glb",
|
||||||
"footprint": [
|
"footprint": [
|
||||||
@ -770,7 +779,7 @@
|
|||||||
"fittingAliases": {
|
"fittingAliases": {
|
||||||
"record_bins": "record_crate",
|
"record_bins": "record_crate",
|
||||||
"crates": "box_crate",
|
"crates": "box_crate",
|
||||||
"counter": "counter",
|
"counter": "counter_till",
|
||||||
"clothes_racks": "clothes_rack",
|
"clothes_racks": "clothes_rack",
|
||||||
"bricabrac_shelving": "wire_shelf",
|
"bricabrac_shelving": "wire_shelf",
|
||||||
"book_wall": "bookshelf",
|
"book_wall": "bookshelf",
|
||||||
|
|||||||
BIN
web/assets/thumbs/procity_fit_counter_till_01.png
Normal file
BIN
web/assets/thumbs/procity_fit_counter_till_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Loading…
Reference in New Issue
Block a user