d8a914bf2a
Add collapse/expand toggle for the AppShell navigation sidebar and the editor properties panel (both persisted to localStorage). Bundles other in-progress editor work including position anchors, outlet sizing, PBR textures, window slope/frame depth, curtain metadata, and various 2D/3D rendering tweaks.
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Download CC0 PBR texture sets from ambientCG and resize them to 512px JPEGs
|
|
suitable for the house-plan-maker 3D view. Outputs land in
|
|
apps/client/public/textures/{floors,walls}/<slug>/{color,normal,roughness}.jpg
|
|
|
|
Texture credit: ambientCG (https://ambientcg.com), CC0 1.0 Universal.
|
|
|
|
Run from repo root:
|
|
py -3 scripts/fetch-textures.py
|
|
|
|
Re-running is idempotent — files are skipped if they already exist.
|
|
"""
|
|
|
|
import io
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
print("ERROR: PIL/Pillow is required. Install with: py -3 -m pip install pillow", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
OUT_ROOT = REPO_ROOT / "apps" / "client" / "public" / "textures"
|
|
TARGET_SIZE = 512
|
|
|
|
# Map our enum values to ambientCG asset ids. The names follow the
|
|
# Camera/Wood/Tile/etc. categories on https://ambientcg.com.
|
|
FLOORS = {
|
|
# FloorType -> ambientCG asset
|
|
"CONCRETE": "Concrete034",
|
|
"WOOD_LIGHT": "WoodFloor043",
|
|
"WOOD_MEDIUM": "WoodFloor041",
|
|
"WOOD_DARK": "WoodFloor051",
|
|
"WOOD_HERRINGBONE": "WoodFloor057",
|
|
"TILE_WHITE": "Tiles074",
|
|
"TILE_GRAY": "Tiles101",
|
|
"LAMINATE": "WoodFloor044",
|
|
}
|
|
|
|
WALLS = {
|
|
# WallFinish -> ambientCG asset (PAINT skips textures, uses wallColor)
|
|
"PLASTER": "Plaster001",
|
|
"BRICK": "Bricks023",
|
|
"CONCRETE": "Concrete019",
|
|
"WOOD_PANEL": "WoodSiding001",
|
|
"WALLPAPER": "Fabric030",
|
|
}
|
|
|
|
# Maps we want from each set. ambientCG file naming: <Asset>_1K-JPG_<Map>.jpg
|
|
MAPS = {
|
|
"color": "Color",
|
|
"normal": "NormalGL",
|
|
"roughness": "Roughness",
|
|
}
|
|
|
|
|
|
def download_zip(asset_id: str) -> bytes:
|
|
url = f"https://ambientcg.com/get?file={asset_id}_1K-JPG.zip"
|
|
print(f" fetching {url}")
|
|
req = urllib.request.Request(url, headers={"User-Agent": "house-plan-maker/1.0"})
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
return resp.read()
|
|
|
|
|
|
def extract_and_resize(zip_bytes: bytes, asset_id: str, out_dir: Path) -> bool:
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
zf = zipfile.ZipFile(io.BytesIO(zip_bytes))
|
|
except zipfile.BadZipFile:
|
|
# Server returned an HTML error page, not a zip.
|
|
print(f" ERROR: {asset_id} did not return a zip — preview: {zip_bytes[:120]!r}")
|
|
return False
|
|
|
|
names = {n.lower(): n for n in zf.namelist()}
|
|
success = True
|
|
|
|
for out_name, ambient_map in MAPS.items():
|
|
out_path = out_dir / f"{out_name}.jpg"
|
|
if out_path.exists():
|
|
print(f" skip (exists): {out_path.relative_to(REPO_ROOT)}")
|
|
continue
|
|
# Find the matching file in the zip (case-insensitive substring match).
|
|
needle = f"_{ambient_map}.jpg".lower()
|
|
match_lower = next((k for k in names if k.endswith(needle)), None)
|
|
if not match_lower:
|
|
print(f" WARN: no {ambient_map} map in {asset_id}")
|
|
success = False
|
|
continue
|
|
with zf.open(names[match_lower]) as src:
|
|
img = Image.open(src)
|
|
img = img.convert("RGB")
|
|
img = img.resize((TARGET_SIZE, TARGET_SIZE), Image.LANCZOS)
|
|
img.save(out_path, "JPEG", quality=82, optimize=True)
|
|
print(f" wrote {out_path.relative_to(REPO_ROOT)} ({out_path.stat().st_size // 1024} KB)")
|
|
|
|
return success
|
|
|
|
|
|
def fetch_set(category: str, label: str, asset_id: str) -> None:
|
|
out_dir = OUT_ROOT / category / label.lower()
|
|
print(f"[{category}/{label}] -> {asset_id}")
|
|
# Skip whole asset if all three target files already exist.
|
|
if all((out_dir / f"{m}.jpg").exists() for m in MAPS):
|
|
print(f" all maps already present, skipping fetch")
|
|
return
|
|
try:
|
|
zip_bytes = download_zip(asset_id)
|
|
except Exception as e:
|
|
print(f" ERROR fetching {asset_id}: {e}")
|
|
return
|
|
extract_and_resize(zip_bytes, asset_id, out_dir)
|
|
|
|
|
|
def main() -> None:
|
|
print(f"writing to {OUT_ROOT}")
|
|
OUT_ROOT.mkdir(parents=True, exist_ok=True)
|
|
|
|
for label, asset_id in FLOORS.items():
|
|
fetch_set("floors", label, asset_id)
|
|
|
|
for label, asset_id in WALLS.items():
|
|
fetch_set("walls", label, asset_id)
|
|
|
|
print("done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|