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:
2026-06-13 17:09:11 -04:00
parent 31e750b4d8
commit 5d15a1fd82
6 changed files with 349 additions and 0 deletions
+145
View File
@@ -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
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

@@ -0,0 +1,136 @@
// Ring Solar Charger — Vinyl Siding Adapter Plate
// BACKUP: flat upper rear + slow lower taper, no bead coping groove yet.
// Units: millimeters. Vanilla OpenSCAD only.
/* [Plate] */
plate_width = 93.1;
plate_height = 171.45;
min_wall_thickness = 3.0;
edge_round = 0;
/* [Siding Profile] */
siding_projection = 27.0;
face_inset_depth = 8.0;
flat_upper_ratio = 0.55;
course_pitch = 165.1;
profile_segments = 8;
profile_mode = "curved";
rear_clearance = 0.2;
/* [Mounting Holes] */
mount_hole_offset_top = 50.0;
mount_hole_offset_bottom = 44.7;
mount_hole_dia = 4.5;
mount_hole_x = 0;
/* [Export] */
part_mode = "full";
profile_strip_width = 20;
/* [Hidden] */
$fn = 64;
epsilon = 0.01;
function projection_z() = siding_projection - rear_clearance;
function face_inset_z() = projection_z() - face_inset_depth;
function face_y_flat_end() = plate_height * (1 - flat_upper_ratio);
function lower_curve_z(y) =
let(
y1 = face_y_flat_end(),
t = y / max(y1, epsilon),
ease = pow(sin(t * 90), 1.4),
z_bottom = projection_z(),
z_top = face_inset_z()
)
z_top + (z_bottom - z_top) * (1 - ease);
function stepped_rear_z(y) =
y >= face_y_flat_end() ? face_inset_z() : projection_z();
function rear_z_at(y) =
y >= face_y_flat_end() ? face_inset_z()
: profile_mode == "stepped" ? stepped_rear_z(y)
: lower_curve_z(y);
function rear_profile_points() =
let(step = plate_height / max(profile_segments * 12, 24))
[for (y = [0 : step : plate_height])
[y, rear_z_at(y)]];
function max_rear_z() =
let(pts = rear_profile_points())
max([for (p = pts) p[1]]);
function front_z() = max_rear_z() + min_wall_thickness;
function rear_y_min() =
min([for (p = rear_profile_points()) p[1]]);
function minkowski_active() =
edge_round > 0 && min_wall_thickness >= 2 * edge_round + 1;
function hole_y_min() =
rear_y_min() - (minkowski_active() ? edge_round : 0) - epsilon;
function hole_y_max() =
front_z() + (minkowski_active() ? edge_round : 0) + epsilon;
function cross_section_polygon() =
let(
rear_pts = rear_profile_points(),
fz = front_z()
)
[
for (p = rear_pts) p,
[plate_height, fz],
[0, fz]
];
module cross_section_2d() {
polygon(cross_section_polygon());
}
module adapter_body(width) {
linear_extrude(width, center = true)
cross_section_2d();
}
module mount_holes() {
y_min = hole_y_min();
y_max = hole_y_max();
hole_depth = y_max - y_min;
center_y = (y_min + y_max) / 2;
for (y_pos = [plate_height - mount_hole_offset_top, mount_hole_offset_bottom]) {
translate([y_pos, center_y, mount_hole_x])
rotate([90, 0, 0])
cylinder(h = hole_depth, d = mount_hole_dia, center = true);
}
}
module rounded_body(width) {
if (minkowski_active()) {
minkowski() {
adapter_body(max(width - 2 * edge_round, 1));
sphere(r = edge_round);
}
} else {
adapter_body(width);
}
}
module adapter_plate() {
width = part_mode == "profile_strip" ? profile_strip_width : plate_width;
if (part_mode == "full") {
difference() {
rounded_body(width);
mount_holes();
}
} else {
rounded_body(width);
}
}
adapter_plate();
@@ -0,0 +1,209 @@
// Ring Solar Charger — Vinyl Siding Adapter Plate
// Mounts the integrated Ring Solar Charger on horizontal beaded vinyl lap siding.
// Units: millimeters. Vanilla OpenSCAD only.
//
// Rear profile (y=0 bottom, y=plate_height top):
// TOP — bead coping cavity scooped into rear face (concave semicircle)
// MID — flat rear shelf continues downward
// BOTTOM — slow taper outward toward siding projection
//
// Backup (flat + taper only): ring-solar-siding-adapter.backup.scad
/* [Plate] */
plate_width = 93.1;
plate_height = 171.45;
min_wall_thickness = 5.0;
edge_round = 0;
/* [Siding Profile] */
siding_projection = 27.0;
face_inset_depth = 8.0;
bead_coping_radius = 10.00; // Semicircle radius for bead groove (measured on siding)
bead_coping_offset_top = 11.00; // Distance from plate top to groove center (tune on wall)
flat_upper_ratio = 0.55; // Fraction of plate height that stays flat at the top
bottom_taper_depth_ratio = 1.6; // Rear depth at plate bottom = min_wall_thickness * this (outward from flat shelf)
rear_extension = 7.5; // Extra depth toward wall; front face + bead groove stay fixed vs front/top
course_pitch = 165.1;
profile_segments = 8;
profile_mode = "curved"; // [curved, stepped]
rear_clearance = 0.2;
/* [Mounting Holes] */
mount_hole_offset_top = 50.0;
mount_hole_offset_bottom = 44.7;
mount_hole_dia = 4.5;
mount_hole_x = 0;
/* [Export] */
part_mode = "full"; // [full, profile_strip, front_slice]
profile_strip_width = 20;
front_slice_depth = 3.5; // [2:0.5:8]
/* [Hidden] */
$fn = 64;
epsilon = 0.01;
// ---------------------------------------------------------------------------
// Siding rear-profile geometry
// OpenSCAD 2D polygon uses [plate_Y, plate_Z]; linear_extrude() goes along +Z (width).
// OpenSCAD 3D: X = plate height (y), Y = depth (z), Z = plate width.
// Lower depth Y = toward wall (back). Higher depth Y = toward Ring (front).
// ---------------------------------------------------------------------------
function projection_z() = siding_projection - rear_clearance;
function face_inset_z() = projection_z() - face_inset_depth;
// Flat rear shelf shifted toward the wall; bead groove keeps absolute depth at face_inset_z.
function flat_rear_z() = face_inset_z() - rear_extension;
function bead_coping_y_center() = plate_height - bead_coping_offset_top;
// Bottom of flat rear shelf (flat_upper_ratio measured from plate top downward).
function face_y_flat_end() = plate_height * (1 - flat_upper_ratio);
// Rear depth where the bottom taper ends (outward from flat shelf, in wall-thickness units).
function bottom_rear_z() =
flat_rear_z() + min_wall_thickness * bottom_taper_depth_ratio;
// Slow taper from bottom_rear_z at the plate bottom up to the flat rear shelf.
function lower_curve_z(y) =
let(
y1 = face_y_flat_end(),
t = y / max(y1, epsilon),
ease = pow(sin(t * 90), 1.4),
z_bottom = bottom_rear_z(),
z_top = flat_rear_z()
)
z_top + (z_bottom - z_top) * (1 - ease);
function stepped_rear_z(y) =
y >= face_y_flat_end() ? flat_rear_z() : bottom_rear_z();
// Bead coping: fixed depth vs front/top. Flat chord at face_inset_z; arc scoops into plate.
function bead_coping_rear_z(y) =
let(
r = bead_coping_radius,
shelf = face_inset_z(),
dy = y - bead_coping_y_center(),
disc = r * r - dy * dy
)
disc >= 0 ? shelf + sqrt(disc) : flat_rear_z();
// Flat top shelf + bead cavity + lower taper.
function rear_z_at(y) =
y >= face_y_flat_end() ? bead_coping_rear_z(y)
: profile_mode == "stepped" ? stepped_rear_z(y)
: lower_curve_z(y);
function rear_profile_points() =
let(
step = plate_height / max(profile_segments * 12, 24),
yc = bead_coping_y_center(),
r = bead_coping_radius,
arc_step = r / 16
)
concat(
[for (y = [0 : step : max(yc - r - epsilon, 0)]) [y, rear_z_at(y)]],
[for (y = [yc - r : arc_step : yc + r]) [y, rear_z_at(y)]],
[for (y = [min(yc + r + step, plate_height) : step : plate_height]) [y, rear_z_at(y)]]
);
function max_rear_z() =
let(pts = rear_profile_points())
max([for (p = pts) p[1]]);
// Front face anchored to bead groove + min wall (independent of rear_extension).
function front_z() =
face_inset_z() + bead_coping_radius + min_wall_thickness;
function rear_y_min() =
min([for (p = rear_profile_points()) p[1]]);
function minkowski_active() =
edge_round > 0 && min_wall_thickness >= 2 * edge_round + 1;
function hole_y_min() =
rear_y_min() - (minkowski_active() ? edge_round : 0) - epsilon;
function hole_y_max() =
front_z() + (minkowski_active() ? edge_round : 0) + epsilon;
function cross_section_polygon() =
let(
rear_pts = rear_profile_points(),
fz = front_z()
)
[
for (p = rear_pts) p,
[plate_height, fz],
[0, fz]
];
// ---------------------------------------------------------------------------
// Modules
// ---------------------------------------------------------------------------
module cross_section_2d() {
polygon(cross_section_polygon());
}
module adapter_body(width) {
linear_extrude(width, center = true)
cross_section_2d();
}
module mount_holes() {
y_min = hole_y_min();
y_max = hole_y_max();
hole_depth = y_max - y_min;
center_y = (y_min + y_max) / 2;
for (y_pos = [plate_height - mount_hole_offset_top, mount_hole_offset_bottom]) {
translate([y_pos, center_y, mount_hole_x])
rotate([90, 0, 0])
cylinder(h = hole_depth, d = mount_hole_dia, center = true);
}
}
module rounded_body(width) {
if (minkowski_active()) {
minkowski() {
adapter_body(max(width - 2 * edge_round, 1));
sphere(r = edge_round);
}
} else {
adapter_body(width);
}
}
// Keep only the front face cap for fit-checking width, height, and hole placement.
module front_slice_mask(width) {
fz = front_z();
translate([plate_height / 2, fz - front_slice_depth / 2, 0])
cube(
[plate_height + 2 * epsilon, front_slice_depth + 2 * epsilon, width + 2 * epsilon],
center = true
);
}
module adapter_plate() {
width = part_mode == "profile_strip" ? profile_strip_width : plate_width;
if (part_mode == "full") {
difference() {
rounded_body(width);
mount_holes();
}
} else if (part_mode == "front_slice") {
intersection() {
difference() {
rounded_body(width);
mount_holes();
}
front_slice_mask(width);
}
} else {
rounded_body(width);
}
}
adapter_plate();