diff --git a/README.md b/README.md new file mode 100644 index 0000000..84cb74f --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# OpenSCAD Projects + +Parametric 3D-printable parts designed in [OpenSCAD](https://openscad.org/). Each project lives in its own folder with a `.scad` source file, a `README.md`, and a `preview.png` render. + +Designs are tuned for FDM printing on a **Bambu Lab P2S** (256 × 256 × 256 mm build volume). + +## Requirements + +- [OpenSCAD](https://openscad.org/downloads.html) 2021.01 or newer (tested with the Customizer UI) +- A slicer for your printer (e.g. Bambu Studio) + +Most models here use **vanilla OpenSCAD** only. Future projects may use [BOSL2](https://github.com/BelfrySCAD/BOSL2); see `.cursor/skills/openscad-bosl2/` for house-style conventions when working with that library. + +## Quick start + +1. Clone the repo and open a project's `.scad` file in OpenSCAD. +2. Adjust parameters in the **Customizer** panel (View → Customizer). +3. Press **F6** to render, then **File → Export → Export as STL**. +4. Slice and print. Generated `.stl` files are gitignored; export locally as needed. + +## Projects + +| Preview | Project | Description | +|---------|---------|-------------| +| ![Ring Solar Siding Adapter](ring-solar-siding-adapter/preview.png) | [ring-solar-siding-adapter](ring-solar-siding-adapter/) | Adapter plate to mount a Ring Solar Charger on horizontal beaded vinyl lap siding | + +## Preview images + +OpenSCAD can export **PNG** previews from the command line when OpenGL is available (local GUI, or headless via `xvfb-run`): + +```bash +openscad --render --viewall --imgsize=1200,900 \ + -o ring-solar-siding-adapter/preview.png \ + ring-solar-siding-adapter/ring-solar-siding-adapter.scad +``` + +OpenSCAD does **not** export JPG natively; convert PNG if needed. + +**Headless WSL / CI** — install PyVista for the script's fallback renderer (smooth shaded, no wireframe): + +```bash +pip install pyvista +python3 scripts/render-preview.py ring-solar-siding-adapter +``` + +**Best quality (native OpenSCAD look)** — install a virtual display so OpenSCAD can export PNG directly: + +```bash +sudo apt install xvfb +python3 scripts/render-preview.py ring-solar-siding-adapter +``` + +The script tries OpenSCAD + `xvfb-run` first, then falls back to PyVista. + +## Repository layout + +``` +openscad-projects/ +├── README.md +├── AGENTS.md +├── .gitignore # Ignores exported *.stl files +├── scripts/ +│ └── render-preview.py # Headless preview generator +└── / + ├── README.md # Purpose, parameters, workflow + ├── preview.png # Catalog render (committed) + ├── .scad # Parametric source + └── *.backup.scad # Optional design checkpoints +``` + +When adding a new project, create a kebab-case folder, add a single primary `.scad` file, write a README describing purpose and parameters, and generate a preview. + +## Printer + +Designs target a **Bambu Lab P2S**. Build volume and material notes live in [`.cursor/skills/openscad-bosl2/references/bambu-p2s.md`](.cursor/skills/openscad-bosl2/references/bambu-p2s.md). Parts from this repo should fit within 256 × 256 × 256 mm unless noted otherwise. + +## License + +No license file is included yet. Add one before publishing or sharing models externally. diff --git a/ring-solar-siding-adapter/README.md b/ring-solar-siding-adapter/README.md new file mode 100644 index 0000000..20a7b84 --- /dev/null +++ b/ring-solar-siding-adapter/README.md @@ -0,0 +1,145 @@ +# Ring Solar Siding Adapter + +![Preview](preview.png) + +Parametric adapter plate that sits between a **Ring Solar Charger** (integrated battery-doorbell solar unit) and horizontal beaded vinyl lap siding. + +| | | +|---|---| +| **Source** | [`ring-solar-siding-adapter.scad`](ring-solar-siding-adapter.scad) | +| **Footprint** | ~93 × 171 mm (matches the Ring back) | +| **Units** | Millimeters throughout | + +## What it is + +- **Front face:** flat — the Ring unit mounts here. +- **Rear face:** shaped to match siding — bead groove at the top, flat shelf in the middle, curved taper at the bottom. + +## What it's for + +The Ring's back is flat; siding is not. Without an adapter, you get gaps, uneven pressure, and a poor mount. + +This plate: + +- Conforms to the siding profile so the rear sits flush against the wall. +- Receives the siding bead in a semicircular groove near the top. +- Provides two through-holes aligned with the Ring's house-mount screws — screws pass through Ring → adapter → siding → sheathing. + +Typical stack: + +``` +Ring Solar Charger + ↓ screws + Adapter plate ← this part + ↓ + Vinyl siding + ↓ + Sheathing +``` + +## Rear profile (side view, bottom → top) + +``` +plate top ───────────── bead coping groove (receives siding bead) + flat rear shelf +taper start ───────────── flat_upper_ratio sets this boundary + curved taper +plate bottom ──────────── deepest projection toward front +``` + +**Depth convention:** lower depth = toward the wall; higher depth = toward the front (Ring). + +## Parameters + +### Plate + +| Parameter | Meaning | +|-----------|---------| +| `plate_width` | Width of the adapter (Ring back width). | +| `plate_height` | Height of the adapter (Ring back height). | +| `min_wall_thickness` | Minimum solid depth at the thinnest point (bead groove pinch). Also scales bottom taper reach. | +| `edge_round` | Rounds front edges via minkowski. Keep `0` while tuning; set ~1 for final print (slower render). | + +### Siding profile + +| Parameter | Meaning | +|-----------|---------| +| `siding_projection` | Nominal max siding depth from wall. Feeds `face_inset_z` via clearance and inset. | +| `face_inset_depth` | How far the flat upper rear sits in from max projection. | +| `bead_coping_radius` | Radius of the semicircular bead groove (measured on your siding). | +| `bead_coping_offset_top` | Distance from plate top to center of the bead groove. | +| `flat_upper_ratio` | Fraction of plate height (from top down) with a flat rear. Lower = taper starts higher. | +| `bottom_taper_depth_ratio` | How far the bottom rear reaches toward the front beyond the flat shelf: `min_wall_thickness × this`. Higher = more projection at bottom. | +| `rear_extension` | Pushes flat rear and taper toward the wall without moving the front face or bead groove vs front/top. Adds overall depth. | +| `course_pitch` | Bead-to-bead spacing on your siding (~6.5"). Reference for tuning. | +| `profile_segments` | Curve sampling resolution. Higher = smoother, slower. | +| `profile_mode` | `curved` = smooth taper; `stepped` = debug fallback with sharp transitions. | +| `rear_clearance` | Small gap so the part doesn't bind on siding. | + +**Common tuning relationships:** + +| Goal | Parameter | +|------|-----------| +| Move taper start up/down | `flat_upper_ratio` | +| Bottom taper more/less toward front | `bottom_taper_depth_ratio` | +| Whole plate deeper front-to-back (bead stays put) | `rear_extension` | +| Bead groove up/down | `bead_coping_offset_top` | +| Groove size | `bead_coping_radius` | + +### Mounting holes + +| Parameter | Meaning | +|-----------|---------| +| `mount_hole_offset_top` | Upper hole center distance from plate top. | +| `mount_hole_offset_bottom` | Lower hole center distance from plate bottom. | +| `mount_hole_dia` | Hole diameter (4.5 mm for 4 mm Ring screws). | +| `mount_hole_x` | Horizontal offset from center (`0` = centered). | + +### Export + +| Parameter | Meaning | +|-----------|---------| +| `part_mode` | What to generate (see below). | +| `profile_strip_width` | Width of the narrow siding fit-test strip. | +| `front_slice_depth` | Thickness of the front-face test cap (mm). | + +**Export modes:** + +| Mode | Purpose | +|------|---------| +| `full` | Complete adapter with holes — final part. | +| `profile_strip` | Narrow strip through the rear profile — cheap siding fit test. | +| `front_slice` | Thin front cap with holes — check width, height, hole placement vs Ring. | + +## Suggested workflow + +1. **`profile_strip`** — tune siding profile (`flat_upper_ratio`, `bottom_taper_depth_ratio`, `rear_extension`, bead params). +2. **`front_slice`** — verify Ring hole alignment. +3. **`full`** — print final plate (PETG/ASA recommended outdoors; PLA OK for tests). + +**Print orientation:** front face (Ring side) on the bed, rear profile vertical. + +## Related files + +| File | Purpose | +|------|---------| +| [`ring-solar-siding-adapter.backup.scad`](ring-solar-siding-adapter.backup.scad) | Earlier geometry — flat upper rear + lower taper only, no bead coping groove | +| `*.stl` | Exported meshes (gitignored; export locally) | + +## Regenerate preview + +From the repo root: + +```bash +pip install pyvista # headless fallback (WSL without display) +sudo apt install xvfb # optional — native OpenSCAD PNG export +python3 scripts/render-preview.py ring-solar-siding-adapter +``` + +Or export a PNG directly from OpenSCAD when a display (or `xvfb-run`) is available: + +```bash +xvfb-run -a openscad --render --viewall --imgsize=1200,900 \ + -o ring-solar-siding-adapter/preview.png \ + ring-solar-siding-adapter/ring-solar-siding-adapter.scad +``` diff --git a/ring-solar-siding-adapter/preview.png b/ring-solar-siding-adapter/preview.png new file mode 100644 index 0000000..ac45357 Binary files /dev/null and b/ring-solar-siding-adapter/preview.png differ diff --git a/ring-solar-siding-adapter.backup.scad b/ring-solar-siding-adapter/ring-solar-siding-adapter.backup.scad similarity index 100% rename from ring-solar-siding-adapter.backup.scad rename to ring-solar-siding-adapter/ring-solar-siding-adapter.backup.scad diff --git a/ring-solar-siding-adapter.scad b/ring-solar-siding-adapter/ring-solar-siding-adapter.scad similarity index 100% rename from ring-solar-siding-adapter.scad rename to ring-solar-siding-adapter/ring-solar-siding-adapter.scad diff --git a/scripts/render-preview.py b/scripts/render-preview.py new file mode 100755 index 0000000..22ad6ed --- /dev/null +++ b/scripts/render-preview.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Render a preview PNG for an OpenSCAD project folder. + +Prefers OpenSCAD's native PNG export (needs a display or xvfb-run). Falls back +to PyVista offscreen rendering from an exported STL (no display required). + +Usage: + python3 scripts/render-preview.py ring-solar-siding-adapter + python3 scripts/render-preview.py ring-solar-siding-adapter --output custom.png + +Dependencies (fallback path only): + pip install pyvista + sudo apt install xvfb # optional, enables native OpenSCAD PNG export +""" + +from __future__ import annotations + +import argparse +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def find_scad(project_dir: Path) -> Path: + matches = sorted(project_dir.glob("*.scad")) + matches = [p for p in matches if not p.name.endswith(".backup.scad")] + if len(matches) != 1: + names = ", ".join(p.name for p in matches) or "(none)" + raise SystemExit(f"Expected exactly one .scad in {project_dir}, found: {names}") + return matches[0] + + +def export_stl(scad: Path, stl: Path) -> None: + cmd = ["openscad", "--export-format=binstl", "-o", str(stl), str(scad)] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + sys.stderr.write(result.stderr or result.stdout) + raise SystemExit(f"openscad STL export failed ({result.returncode})") + + +def render_openscad_png(scad: Path, png: Path, width: int, height: int) -> bool: + """Return True if OpenSCAD wrote a PNG (requires display or xvfb-run).""" + openscad = ["openscad", "--render", "--viewall", f"--imgsize={width},{height}"] + xvfb = shutil.which("xvfb-run") + cmd = ([xvfb, "-a"] if xvfb else []) + openscad + ["-o", str(png), str(scad)] + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0 and png.is_file() and png.stat().st_size > 0: + return True + if result.stderr: + sys.stderr.write(result.stderr) + return False + + +def render_pyvista_png(stl: Path, png: Path, width: int, height: int) -> None: + try: + import pyvista as pv + except ImportError as exc: + raise SystemExit( + "No display for OpenSCAD PNG export and PyVista is not installed.\n" + "Install one of:\n" + " sudo apt install xvfb # native OpenSCAD previews\n" + " pip install pyvista # headless fallback" + ) from exc + + pv.OFF_SCREEN = True + mesh = pv.read(stl) + plotter = pv.Plotter(off_screen=True, window_size=(width, height)) + plotter.set_background("#1a2332") + plotter.add_mesh(mesh, color="#6bb3dc", smooth_shading=True, show_edges=False) + plotter.camera_position = "iso" + png.parent.mkdir(parents=True, exist_ok=True) + plotter.show(screenshot=str(png), auto_close=True) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "project", + type=Path, + help="Project folder name or path (e.g. ring-solar-siding-adapter)", + ) + parser.add_argument( + "--output", + type=Path, + help="Output PNG path (default: /preview.png)", + ) + parser.add_argument("--width", type=int, default=1200) + parser.add_argument("--height", type=int, default=900) + parser.add_argument( + "--force-fallback", + action="store_true", + help="Skip OpenSCAD PNG and use PyVista STL render", + ) + args = parser.parse_args() + + project_dir = args.project + if not project_dir.is_absolute(): + project_dir = REPO_ROOT / project_dir + project_dir = project_dir.resolve() + if not project_dir.is_dir(): + raise SystemExit(f"Project directory not found: {project_dir}") + + scad = find_scad(project_dir) + png = args.output or (project_dir / "preview.png") + if not png.is_absolute(): + png = project_dir / png + + if not args.force_fallback and render_openscad_png(scad, png, args.width, args.height): + print(f"Wrote {png} (OpenSCAD)") + return + + with tempfile.TemporaryDirectory() as tmp: + stl = Path(tmp) / "preview.stl" + export_stl(scad, stl) + render_pyvista_png(stl, png, args.width, args.height) + + print(f"Wrote {png} (PyVista fallback)") + + +if __name__ == "__main__": + main()