Files
alexei.dolgolyov d8a914bf2a feat: editor improvements and collapsible sidebars
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.
2026-04-08 12:27:57 +03:00

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()