Organize projects into folders with READMEs and preview renders.
Move the Ring solar siding adapter into its own directory, add repo and project documentation, and introduce a preview script (OpenSCAD+xvfb or PyVista fallback) for catalog images.
This commit is contained in:
@@ -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/) | 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
|
||||||
|
└── <project-name>/
|
||||||
|
├── README.md # Purpose, parameters, workflow
|
||||||
|
├── preview.png # Catalog render (committed)
|
||||||
|
├── <project-name>.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.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Ring Solar Siding Adapter
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
Executable
+125
@@ -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: <project>/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()
|
||||||
Reference in New Issue
Block a user