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.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user