pre-bemis
This commit is contained in:
232
rpi-case/.claude/skills/openscad/SKILL.md
Normal file
232
rpi-case/.claude/skills/openscad/SKILL.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
name: openscad
|
||||
description: "Create and render OpenSCAD 3D models. Generate preview images from multiple angles, extract customizable parameters, validate syntax, and export STL files for 3D printing platforms like MakerWorld."
|
||||
---
|
||||
|
||||
# OpenSCAD Skill
|
||||
|
||||
Create, validate, and export OpenSCAD 3D models. Supports parameter customization, visual preview from multiple angles, and STL export for 3D printing platforms like MakerWorld.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
OpenSCAD must be installed. Install via Homebrew:
|
||||
```bash
|
||||
brew install openscad
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
This skill provides several tools in the `tools/` directory:
|
||||
|
||||
### Preview Generation
|
||||
```bash
|
||||
# Generate a single preview image
|
||||
./tools/preview.sh model.scad output.png [--camera=x,y,z,tx,ty,tz,dist] [--size=800x600]
|
||||
|
||||
# Generate multi-angle preview (front, back, left, right, top, iso)
|
||||
./tools/multi-preview.sh model.scad output_dir/
|
||||
```
|
||||
|
||||
### STL Export
|
||||
```bash
|
||||
# Export to STL for 3D printing
|
||||
./tools/export-stl.sh model.scad output.stl [-D 'param=value']
|
||||
```
|
||||
|
||||
### Parameter Extraction
|
||||
```bash
|
||||
# Extract customizable parameters from an OpenSCAD file
|
||||
./tools/extract-params.sh model.scad
|
||||
```
|
||||
|
||||
### Validation
|
||||
```bash
|
||||
# Check for syntax errors and warnings
|
||||
./tools/validate.sh model.scad
|
||||
```
|
||||
|
||||
## Visual Validation (Required)
|
||||
|
||||
**Always validate your OpenSCAD models visually after creating or modifying them.**
|
||||
|
||||
After writing or editing any OpenSCAD file:
|
||||
|
||||
1. **Generate multi-angle previews** using `multi-preview.sh`
|
||||
2. **View each generated image** using the `read` tool
|
||||
3. **Check for issues** from multiple perspectives:
|
||||
- Front/back: Verify symmetry, features, and proportions
|
||||
- Left/right: Check depth and side profiles
|
||||
- Top: Ensure top features are correct
|
||||
- Isometric: Overall shape validation
|
||||
4. **Iterate if needed**: If something looks wrong, fix the code and re-validate
|
||||
|
||||
This catches issues that syntax validation alone cannot detect:
|
||||
- Inverted normals or inside-out geometry
|
||||
- Misaligned features or incorrect boolean operations
|
||||
- Proportions that don't match the intended design
|
||||
- Missing or floating geometry
|
||||
- Z-fighting or overlapping surfaces
|
||||
|
||||
**Never deliver an OpenSCAD model without visually confirming it looks correct from multiple angles.**
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Creating an OpenSCAD Model
|
||||
|
||||
Write OpenSCAD code with customizable parameters at the top:
|
||||
|
||||
```openscad
|
||||
// Customizable parameters
|
||||
wall_thickness = 2; // [1:0.5:5] Wall thickness in mm
|
||||
width = 50; // [20:100] Width in mm
|
||||
height = 30; // [10:80] Height in mm
|
||||
rounded = true; // Add rounded corners
|
||||
|
||||
// Model code below
|
||||
module main_shape() {
|
||||
if (rounded) {
|
||||
minkowski() {
|
||||
cube([width - 4, width - 4, height - 2]);
|
||||
sphere(r = 2);
|
||||
}
|
||||
} else {
|
||||
cube([width, width, height]);
|
||||
}
|
||||
}
|
||||
|
||||
difference() {
|
||||
main_shape();
|
||||
translate([wall_thickness, wall_thickness, wall_thickness])
|
||||
scale([1 - 2*wall_thickness/width, 1 - 2*wall_thickness/width, 1])
|
||||
main_shape();
|
||||
}
|
||||
```
|
||||
|
||||
Parameter comment format:
|
||||
- `// [min:max]` - numeric range
|
||||
- `// [min:step:max]` - numeric range with step
|
||||
- `// [opt1, opt2, opt3]` - dropdown options
|
||||
- `// Description text` - plain description
|
||||
|
||||
### 2. Validate the Model
|
||||
```bash
|
||||
./tools/validate.sh model.scad
|
||||
```
|
||||
|
||||
### 3. Generate Previews
|
||||
|
||||
Generate preview images to visually validate the model:
|
||||
```bash
|
||||
./tools/multi-preview.sh model.scad ./previews/
|
||||
```
|
||||
|
||||
This creates PNG images from multiple angles. Use the `read` tool to view them.
|
||||
|
||||
### 4. Export to STL
|
||||
```bash
|
||||
./tools/export-stl.sh model.scad output.stl
|
||||
# With custom parameters:
|
||||
./tools/export-stl.sh model.scad output.stl -D 'width=60' -D 'height=40'
|
||||
```
|
||||
|
||||
## Camera Positions
|
||||
|
||||
Common camera angles for previews:
|
||||
- **Isometric**: `--camera=0,0,0,45,0,45,200`
|
||||
- **Front**: `--camera=0,0,0,90,0,0,200`
|
||||
- **Top**: `--camera=0,0,0,0,0,0,200`
|
||||
- **Right**: `--camera=0,0,0,90,0,90,200`
|
||||
|
||||
Format: `x,y,z,rotx,roty,rotz,distance`
|
||||
|
||||
## MakerWorld Publishing
|
||||
|
||||
For MakerWorld, you typically need:
|
||||
1. STL file(s) exported via `export-stl.sh`
|
||||
2. Preview images (at least one good isometric view)
|
||||
3. A description of customizable parameters
|
||||
|
||||
Consider creating a `model.json` with metadata:
|
||||
```json
|
||||
{
|
||||
"name": "Model Name",
|
||||
"description": "Description for MakerWorld",
|
||||
"parameters": [...],
|
||||
"tags": ["functional", "container", "organizer"]
|
||||
}
|
||||
```
|
||||
|
||||
## Example: Full Workflow
|
||||
|
||||
```bash
|
||||
# 1. Create the model (write .scad file)
|
||||
|
||||
# 2. Validate syntax
|
||||
./tools/validate.sh box.scad
|
||||
|
||||
# 3. Generate multi-angle previews
|
||||
./tools/multi-preview.sh box.scad ./previews/
|
||||
|
||||
# 4. IMPORTANT: View and validate ALL preview images
|
||||
# Use the read tool on each PNG file to visually inspect:
|
||||
# - previews/box_front.png
|
||||
# - previews/box_back.png
|
||||
# - previews/box_left.png
|
||||
# - previews/box_right.png
|
||||
# - previews/box_top.png
|
||||
# - previews/box_iso.png
|
||||
# Look for geometry issues, misalignments, or unexpected results.
|
||||
# If anything looks wrong, go back to step 1 and fix it!
|
||||
|
||||
# 5. Extract and review parameters
|
||||
./tools/extract-params.sh box.scad
|
||||
|
||||
# 6. Export STL with default parameters
|
||||
./tools/export-stl.sh box.scad box.stl
|
||||
|
||||
# 7. Export STL with custom parameters
|
||||
./tools/export-stl.sh box.scad box_large.stl -D 'width=80' -D 'height=60'
|
||||
```
|
||||
|
||||
**Remember**: Never skip the visual validation step. Many issues (wrong dimensions, boolean operation errors, inverted geometry) are only visible when you actually look at the rendered model.
|
||||
|
||||
## OpenSCAD Quick Reference
|
||||
|
||||
### Basic Shapes
|
||||
```openscad
|
||||
cube([x, y, z]);
|
||||
sphere(r = radius);
|
||||
cylinder(h = height, r = radius);
|
||||
cylinder(h = height, r1 = bottom_r, r2 = top_r); // cone
|
||||
```
|
||||
|
||||
### Transformations
|
||||
```openscad
|
||||
translate([x, y, z]) object();
|
||||
rotate([rx, ry, rz]) object();
|
||||
scale([sx, sy, sz]) object();
|
||||
mirror([x, y, z]) object();
|
||||
```
|
||||
|
||||
### Boolean Operations
|
||||
```openscad
|
||||
union() { a(); b(); } // combine
|
||||
difference() { a(); b(); } // subtract b from a
|
||||
intersection() { a(); b(); } // overlap only
|
||||
```
|
||||
|
||||
### Advanced
|
||||
```openscad
|
||||
linear_extrude(height) 2d_shape();
|
||||
rotate_extrude() 2d_shape();
|
||||
hull() { objects(); } // convex hull
|
||||
minkowski() { a(); b(); } // minkowski sum (rounding)
|
||||
```
|
||||
|
||||
### 2D Shapes
|
||||
```openscad
|
||||
circle(r = radius);
|
||||
square([x, y]);
|
||||
polygon(points = [[x1,y1], [x2,y2], ...]);
|
||||
text("string", size = 10);
|
||||
```
|
||||
@@ -0,0 +1,92 @@
|
||||
// Parametric Box with Lid
|
||||
// A customizable storage box for 3D printing
|
||||
|
||||
// === Box Parameters ===
|
||||
width = 60; // [20:200] Width in mm
|
||||
depth = 40; // [20:200] Depth in mm
|
||||
height = 30; // [10:150] Height in mm
|
||||
wall_thickness = 2; // [1:0.5:5] Wall thickness in mm
|
||||
|
||||
// === Lid Parameters ===
|
||||
include_lid = true; // Include a separate lid
|
||||
lid_height = 8; // [5:30] Lid height in mm
|
||||
lid_tolerance = 0.3; // [0.1:0.1:0.8] Gap for lid fit
|
||||
|
||||
// === Style Options ===
|
||||
corner_radius = 3; // [0:10] Corner rounding radius
|
||||
add_grip = true; // Add grip indents to lid
|
||||
|
||||
// === Internal ===
|
||||
$fn = 32; // Smoothness
|
||||
|
||||
// Rounded box module
|
||||
module rounded_box(w, d, h, r) {
|
||||
if (r > 0) {
|
||||
hull() {
|
||||
for (x = [r, w-r]) {
|
||||
for (y = [r, d-r]) {
|
||||
translate([x, y, 0])
|
||||
cylinder(h = h, r = r);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cube([w, d, h]);
|
||||
}
|
||||
}
|
||||
|
||||
// Main box body
|
||||
module box_body() {
|
||||
difference() {
|
||||
rounded_box(width, depth, height, corner_radius);
|
||||
|
||||
// Hollow inside
|
||||
translate([wall_thickness, wall_thickness, wall_thickness])
|
||||
rounded_box(
|
||||
width - 2*wall_thickness,
|
||||
depth - 2*wall_thickness,
|
||||
height, // Open top
|
||||
max(0, corner_radius - wall_thickness)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Lid
|
||||
module lid() {
|
||||
inner_w = width - 2*wall_thickness - 2*lid_tolerance;
|
||||
inner_d = depth - 2*wall_thickness - 2*lid_tolerance;
|
||||
lip_height = lid_height * 0.6;
|
||||
|
||||
difference() {
|
||||
union() {
|
||||
// Top cap
|
||||
rounded_box(width, depth, wall_thickness, corner_radius);
|
||||
|
||||
// Inner lip
|
||||
translate([wall_thickness + lid_tolerance, wall_thickness + lid_tolerance, -lip_height + wall_thickness])
|
||||
rounded_box(inner_w, inner_d, lip_height, max(0, corner_radius - wall_thickness));
|
||||
}
|
||||
|
||||
// Grip indents
|
||||
if (add_grip) {
|
||||
for (x = [width * 0.3, width * 0.7]) {
|
||||
translate([x, -1, wall_thickness/2])
|
||||
rotate([-90, 0, 0])
|
||||
cylinder(h = 5, r = 3, $fn = 16);
|
||||
translate([x, depth - 4, wall_thickness/2])
|
||||
rotate([-90, 0, 0])
|
||||
cylinder(h = 5, r = 3, $fn = 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render
|
||||
box_body();
|
||||
|
||||
if (include_lid) {
|
||||
// Position lid next to box for printing
|
||||
translate([width + 10, 0, lid_height - wall_thickness])
|
||||
rotate([180, 0, 0])
|
||||
lid();
|
||||
}
|
||||
95
rpi-case/.claude/skills/openscad/examples/phone_stand.scad
Normal file
95
rpi-case/.claude/skills/openscad/examples/phone_stand.scad
Normal file
@@ -0,0 +1,95 @@
|
||||
// Adjustable Phone/Tablet Stand
|
||||
// Parametric stand with customizable angle and size
|
||||
|
||||
// === Device Parameters ===
|
||||
device_width = 80; // [50:200] Device width in mm
|
||||
device_thickness = 12; // [6:20] Device thickness (with case)
|
||||
|
||||
// === Stand Parameters ===
|
||||
stand_angle = 65; // [45:85] Viewing angle in degrees
|
||||
stand_depth = 80; // [50:150] Base depth in mm
|
||||
stand_height = 100; // [60:200] Back support height in mm
|
||||
|
||||
// === Construction ===
|
||||
material_thickness = 4; // [2:0.5:8] Material thickness
|
||||
slot_depth = 15; // [10:30] How deep device sits in slot
|
||||
|
||||
// === Features ===
|
||||
cable_hole = true; // Add cable pass-through hole
|
||||
cable_diameter = 15; // [8:25] Cable hole diameter
|
||||
add_feet = true; // Add anti-slip feet
|
||||
|
||||
// === Quality ===
|
||||
$fn = 48;
|
||||
|
||||
module stand_profile() {
|
||||
// 2D profile of the stand side
|
||||
polygon([
|
||||
[0, 0], // Front bottom
|
||||
[stand_depth, 0], // Back bottom
|
||||
[stand_depth, material_thickness], // Back bottom inner
|
||||
[stand_depth - material_thickness, material_thickness], // Base top back
|
||||
[slot_depth + material_thickness, material_thickness], // Base top front (behind slot)
|
||||
[slot_depth + material_thickness, slot_depth * tan(90 - stand_angle) + material_thickness], // Slot back
|
||||
[material_thickness, slot_depth * tan(90 - stand_angle) + material_thickness + device_thickness / sin(stand_angle)], // Slot front top
|
||||
[0, slot_depth * tan(90 - stand_angle) + material_thickness], // Front face bottom of slot
|
||||
[0, 0] // Close
|
||||
]);
|
||||
}
|
||||
|
||||
module back_support() {
|
||||
// Back angled support
|
||||
translate([stand_depth - material_thickness, 0, material_thickness]) {
|
||||
rotate([0, -90 + stand_angle, 0]) {
|
||||
cube([stand_height, device_width, material_thickness]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module cable_cutout() {
|
||||
if (cable_hole) {
|
||||
translate([stand_depth/2, device_width/2, -1])
|
||||
cylinder(h = material_thickness + 2, d = cable_diameter);
|
||||
}
|
||||
}
|
||||
|
||||
module foot() {
|
||||
cylinder(h = 2, d1 = 10, d2 = 8);
|
||||
}
|
||||
|
||||
module stand() {
|
||||
difference() {
|
||||
union() {
|
||||
// Left side
|
||||
linear_extrude(material_thickness)
|
||||
stand_profile();
|
||||
|
||||
// Right side
|
||||
translate([0, device_width - material_thickness, 0])
|
||||
linear_extrude(material_thickness)
|
||||
stand_profile();
|
||||
|
||||
// Base plate
|
||||
cube([stand_depth, device_width, material_thickness]);
|
||||
|
||||
// Front lip
|
||||
cube([material_thickness, device_width, slot_depth * tan(90 - stand_angle) + material_thickness]);
|
||||
|
||||
// Back support
|
||||
back_support();
|
||||
}
|
||||
|
||||
// Cable hole
|
||||
cable_cutout();
|
||||
}
|
||||
|
||||
// Feet
|
||||
if (add_feet) {
|
||||
translate([10, 10, 0]) foot();
|
||||
translate([10, device_width - 10, 0]) foot();
|
||||
translate([stand_depth - 10, 10, 0]) foot();
|
||||
translate([stand_depth - 10, device_width - 10, 0]) foot();
|
||||
}
|
||||
}
|
||||
|
||||
stand();
|
||||
50
rpi-case/.claude/skills/openscad/tools/common.sh
Executable file
50
rpi-case/.claude/skills/openscad/tools/common.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Common utilities for OpenSCAD tools
|
||||
|
||||
# Find OpenSCAD executable
|
||||
find_openscad() {
|
||||
# Check common locations
|
||||
if command -v openscad &> /dev/null; then
|
||||
echo "openscad"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# macOS Application bundle
|
||||
if [ -d "/Applications/OpenSCAD.app" ]; then
|
||||
echo "/Applications/OpenSCAD.app/Contents/MacOS/OpenSCAD"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Homebrew on Apple Silicon
|
||||
if [ -x "/opt/homebrew/bin/openscad" ]; then
|
||||
echo "/opt/homebrew/bin/openscad"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Homebrew on Intel
|
||||
if [ -x "/usr/local/bin/openscad" ]; then
|
||||
echo "/usr/local/bin/openscad"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if OpenSCAD is available
|
||||
check_openscad() {
|
||||
OPENSCAD=$(find_openscad) || {
|
||||
echo "Error: OpenSCAD not found!"
|
||||
echo ""
|
||||
echo "Install OpenSCAD using one of:"
|
||||
echo " brew install openscad"
|
||||
echo " Download from https://openscad.org/downloads.html"
|
||||
exit 1
|
||||
}
|
||||
export OPENSCAD
|
||||
}
|
||||
|
||||
# Get version info
|
||||
openscad_version() {
|
||||
check_openscad
|
||||
$OPENSCAD --version 2>&1
|
||||
}
|
||||
56
rpi-case/.claude/skills/openscad/tools/export-stl.sh
Executable file
56
rpi-case/.claude/skills/openscad/tools/export-stl.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# Export OpenSCAD file to STL
|
||||
# Usage: export-stl.sh input.scad output.stl [-D 'var=value' ...]
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
check_openscad
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 input.scad output.stl [-D 'var=value' ...]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 box.scad box.stl"
|
||||
echo " $0 box.scad box_large.stl -D 'width=80' -D 'height=60'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
|
||||
# Collect -D parameters
|
||||
DEFINES=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-D)
|
||||
shift
|
||||
DEFINES+=("-D" "$1")
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
echo "Exporting STL: $INPUT -> $OUTPUT"
|
||||
if [ ${#DEFINES[@]} -gt 0 ]; then
|
||||
echo "Parameters: ${DEFINES[*]}"
|
||||
fi
|
||||
|
||||
$OPENSCAD \
|
||||
"${DEFINES[@]}" \
|
||||
-o "$OUTPUT" \
|
||||
"$INPUT"
|
||||
|
||||
# Show file info
|
||||
SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||||
echo "STL exported: $OUTPUT ($SIZE)"
|
||||
147
rpi-case/.claude/skills/openscad/tools/extract-params.sh
Executable file
147
rpi-case/.claude/skills/openscad/tools/extract-params.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
# Extract customizable parameters from an OpenSCAD file
|
||||
# Usage: extract-params.sh input.scad [--json]
|
||||
#
|
||||
# Parses parameter declarations with special comments:
|
||||
# param = value; // [min:max] Description
|
||||
# param = value; // [min:step:max] Description
|
||||
# param = value; // [opt1, opt2] Description
|
||||
# param = value; // Description only
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 input.scad [--json]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
JSON_OUTPUT=false
|
||||
|
||||
if [ "$2" = "--json" ]; then
|
||||
JSON_OUTPUT=true
|
||||
fi
|
||||
|
||||
if [ ! -f "$INPUT" ]; then
|
||||
echo "Error: File not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract parameters using Python for better parsing
|
||||
extract_params() {
|
||||
python3 -c '
|
||||
import sys
|
||||
import re
|
||||
|
||||
filename = sys.argv[1]
|
||||
in_block = 0
|
||||
|
||||
with open(filename, "r") as f:
|
||||
for line in f:
|
||||
# Track block depth (skip params inside modules/functions)
|
||||
in_block += line.count("{") - line.count("}")
|
||||
if in_block > 0:
|
||||
continue
|
||||
|
||||
# Match: varname = value; // comment
|
||||
match = re.match(r"^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([^;]+);\s*(?://\s*(.*))?", line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
var_name = match.group(1)
|
||||
value = match.group(2).strip()
|
||||
comment = match.group(3) or ""
|
||||
|
||||
# Determine type
|
||||
if value in ("true", "false"):
|
||||
var_type = "boolean"
|
||||
elif re.match(r"^-?\d+$", value):
|
||||
var_type = "integer"
|
||||
elif re.match(r"^-?\d*\.?\d+$", value):
|
||||
var_type = "number"
|
||||
elif value.startswith("\"") and value.endswith("\""):
|
||||
var_type = "string"
|
||||
value = value[1:-1] # Remove quotes
|
||||
elif value.startswith("["):
|
||||
var_type = "array"
|
||||
else:
|
||||
var_type = "expression"
|
||||
|
||||
# Parse comment for range/options
|
||||
range_val = ""
|
||||
options_val = ""
|
||||
description = comment
|
||||
|
||||
range_match = re.match(r"\[([^\]]+)\]\s*(.*)", comment)
|
||||
if range_match:
|
||||
bracket_content = range_match.group(1)
|
||||
description = range_match.group(2)
|
||||
|
||||
# Check if numeric range (contains :) or options (contains ,)
|
||||
if ":" in bracket_content and not "," in bracket_content:
|
||||
range_val = bracket_content
|
||||
else:
|
||||
options_val = bracket_content
|
||||
|
||||
# Output pipe-delimited
|
||||
print(f"{var_name}|{value}|{var_type}|{range_val}|{options_val}|{description}")
|
||||
' "$INPUT"
|
||||
}
|
||||
|
||||
if [ "$JSON_OUTPUT" = true ]; then
|
||||
echo "["
|
||||
first=true
|
||||
while IFS='|' read -r name value type range options description; do
|
||||
if [ "$first" = true ]; then
|
||||
first=false
|
||||
else
|
||||
echo ","
|
||||
fi
|
||||
|
||||
# Escape quotes in values
|
||||
value=$(echo "$value" | sed 's/"/\\"/g')
|
||||
description=$(echo "$description" | sed 's/"/\\"/g')
|
||||
|
||||
# Build JSON object
|
||||
printf ' {\n'
|
||||
printf ' "name": "%s",\n' "$name"
|
||||
printf ' "value": "%s",\n' "$value"
|
||||
printf ' "type": "%s"' "$type"
|
||||
|
||||
if [ -n "$range" ]; then
|
||||
printf ',\n "range": "%s"' "$range"
|
||||
fi
|
||||
if [ -n "$options" ]; then
|
||||
printf ',\n "options": "%s"' "$options"
|
||||
fi
|
||||
if [ -n "$description" ]; then
|
||||
printf ',\n "description": "%s"' "$description"
|
||||
fi
|
||||
printf '\n }'
|
||||
done < <(extract_params)
|
||||
echo ""
|
||||
echo "]"
|
||||
else
|
||||
echo "Parameters in: $INPUT"
|
||||
echo "==============================================="
|
||||
printf "%-20s %-15s %-10s %s\n" "NAME" "VALUE" "TYPE" "CONSTRAINT/DESC"
|
||||
echo "-----------------------------------------------"
|
||||
|
||||
while IFS='|' read -r name value type range options description; do
|
||||
constraint=""
|
||||
if [ -n "$range" ]; then
|
||||
constraint="[$range]"
|
||||
elif [ -n "$options" ]; then
|
||||
constraint="[$options]"
|
||||
fi
|
||||
if [ -n "$description" ]; then
|
||||
if [ -n "$constraint" ]; then
|
||||
constraint="$constraint $description"
|
||||
else
|
||||
constraint="$description"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%-20s %-15s %-10s %s\n" "$name" "$value" "$type" "$constraint"
|
||||
done < <(extract_params)
|
||||
fi
|
||||
68
rpi-case/.claude/skills/openscad/tools/multi-preview.sh
Executable file
68
rpi-case/.claude/skills/openscad/tools/multi-preview.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# Generate preview images from multiple angles
|
||||
# Usage: multi-preview.sh input.scad output_dir/ [-D 'var=value']
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
check_openscad
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 input.scad output_dir/ [-D 'var=value' ...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
OUTPUT_DIR="$2"
|
||||
shift 2
|
||||
|
||||
# Collect -D parameters
|
||||
DEFINES=()
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-D)
|
||||
shift
|
||||
DEFINES+=("-D" "$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Get base name without extension
|
||||
BASENAME=$(basename "$INPUT" .scad)
|
||||
|
||||
echo "Generating multi-angle previews for: $INPUT"
|
||||
echo "Output directory: $OUTPUT_DIR"
|
||||
echo ""
|
||||
|
||||
# Define angles as name:camera pairs
|
||||
# Camera format: translate_x,translate_y,translate_z,rot_x,rot_y,rot_z,distance
|
||||
ANGLES="iso:0,0,0,55,0,25,0
|
||||
front:0,0,0,90,0,0,0
|
||||
back:0,0,0,90,0,180,0
|
||||
left:0,0,0,90,0,90,0
|
||||
right:0,0,0,90,0,-90,0
|
||||
top:0,0,0,0,0,0,0"
|
||||
|
||||
echo "$ANGLES" | while IFS=: read -r angle camera; do
|
||||
output="$OUTPUT_DIR/${BASENAME}_${angle}.png"
|
||||
|
||||
echo " Rendering $angle view..."
|
||||
$OPENSCAD \
|
||||
--camera="$camera" \
|
||||
--imgsize="800,600" \
|
||||
--colorscheme="Tomorrow Night" \
|
||||
--autocenter \
|
||||
--viewall \
|
||||
"${DEFINES[@]}" \
|
||||
-o "$output" \
|
||||
"$INPUT" 2>/dev/null
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Generated previews:"
|
||||
ls -la "$OUTPUT_DIR"/${BASENAME}_*.png
|
||||
74
rpi-case/.claude/skills/openscad/tools/preview.sh
Executable file
74
rpi-case/.claude/skills/openscad/tools/preview.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# Generate a preview PNG from an OpenSCAD file
|
||||
# Usage: preview.sh input.scad output.png [options]
|
||||
#
|
||||
# Options:
|
||||
# --camera=x,y,z,rx,ry,rz,dist Camera position
|
||||
# --size=WxH Image size (default: 800x600)
|
||||
# -D 'var=value' Set parameter value
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
check_openscad
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 input.scad output.png [--camera=...] [--size=WxH] [-D 'var=val']"
|
||||
echo ""
|
||||
echo "Camera format: x,y,z,rotx,roty,rotz,distance"
|
||||
echo "Common cameras:"
|
||||
echo " Isometric: --camera=0,0,0,55,0,25,200"
|
||||
echo " Front: --camera=0,0,0,90,0,0,200"
|
||||
echo " Top: --camera=0,0,0,0,0,0,200"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
|
||||
# Defaults
|
||||
CAMERA="0,0,0,55,0,25,0"
|
||||
SIZE="800,600"
|
||||
DEFINES=()
|
||||
|
||||
# Parse options
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--camera=*)
|
||||
CAMERA="${1#--camera=}"
|
||||
;;
|
||||
--size=*)
|
||||
SIZE="${1#--size=}"
|
||||
SIZE="${SIZE/x/,}"
|
||||
;;
|
||||
-D)
|
||||
shift
|
||||
DEFINES+=("-D" "$1")
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Ensure output directory exists
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
# Run OpenSCAD
|
||||
echo "Rendering preview: $INPUT -> $OUTPUT"
|
||||
$OPENSCAD \
|
||||
--camera="$CAMERA" \
|
||||
--imgsize="${SIZE}" \
|
||||
--colorscheme="Tomorrow Night" \
|
||||
--autocenter \
|
||||
--viewall \
|
||||
"${DEFINES[@]}" \
|
||||
-o "$OUTPUT" \
|
||||
"$INPUT"
|
||||
|
||||
echo "Preview saved to: $OUTPUT"
|
||||
91
rpi-case/.claude/skills/openscad/tools/render-with-params.sh
Executable file
91
rpi-case/.claude/skills/openscad/tools/render-with-params.sh
Executable file
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
# Render OpenSCAD with parameters from a JSON file
|
||||
# Usage: render-with-params.sh input.scad params.json output.stl|output.png
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
check_openscad
|
||||
|
||||
if [ $# -lt 3 ]; then
|
||||
echo "Usage: $0 input.scad params.json output.[stl|png]"
|
||||
echo ""
|
||||
echo "params.json format:"
|
||||
echo ' {"width": 60, "height": 40, "include_lid": true}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
PARAMS_FILE="$2"
|
||||
OUTPUT="$3"
|
||||
|
||||
if [ ! -f "$INPUT" ]; then
|
||||
echo "Error: Input file not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PARAMS_FILE" ]; then
|
||||
echo "Error: Params file not found: $PARAMS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build -D arguments from JSON
|
||||
DEFINES=()
|
||||
while IFS= read -r line; do
|
||||
# Parse each key-value pair
|
||||
key=$(echo "$line" | cut -d'=' -f1)
|
||||
value=$(echo "$line" | cut -d'=' -f2-)
|
||||
|
||||
if [ -n "$key" ]; then
|
||||
DEFINES+=("-D" "$key=$value")
|
||||
fi
|
||||
done < <(
|
||||
# Use python or jq to parse JSON to key=value lines
|
||||
if command -v python3 &> /dev/null; then
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$PARAMS_FILE') as f:
|
||||
params = json.load(f)
|
||||
for k, v in params.items():
|
||||
if isinstance(v, bool):
|
||||
print(f'{k}={str(v).lower()}')
|
||||
elif isinstance(v, str):
|
||||
print(f'{k}=\"{v}\"')
|
||||
else:
|
||||
print(f'{k}={v}')
|
||||
"
|
||||
elif command -v jq &> /dev/null; then
|
||||
jq -r 'to_entries | .[] | "\(.key)=\(.value)"' "$PARAMS_FILE"
|
||||
else
|
||||
echo "Error: Requires python3 or jq to parse JSON"
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
|
||||
echo "Rendering with parameters from: $PARAMS_FILE"
|
||||
echo "Parameters: ${DEFINES[*]}"
|
||||
|
||||
# Determine output type and set appropriate options
|
||||
EXT="${OUTPUT##*.}"
|
||||
case "$EXT" in
|
||||
stl|STL)
|
||||
$OPENSCAD "${DEFINES[@]}" -o "$OUTPUT" "$INPUT"
|
||||
;;
|
||||
png|PNG)
|
||||
$OPENSCAD "${DEFINES[@]}" \
|
||||
--camera="0,0,0,55,0,25,0" \
|
||||
--imgsize="800,600" \
|
||||
--colorscheme="Tomorrow Night" \
|
||||
--autocenter --viewall \
|
||||
-o "$OUTPUT" "$INPUT"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported output format: $EXT"
|
||||
echo "Supported: stl, png"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Output saved: $OUTPUT"
|
||||
46
rpi-case/.claude/skills/openscad/tools/validate.sh
Executable file
46
rpi-case/.claude/skills/openscad/tools/validate.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Validate an OpenSCAD file for syntax errors
|
||||
# Usage: validate.sh input.scad
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
check_openscad
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 input.scad"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
INPUT="$1"
|
||||
|
||||
if [ ! -f "$INPUT" ]; then
|
||||
echo "Error: File not found: $INPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Validating: $INPUT"
|
||||
|
||||
# Create temp file for output
|
||||
TEMP_OUTPUT=$(mktemp /tmp/openscad_validate.XXXXXX.echo)
|
||||
trap "rm -f $TEMP_OUTPUT" EXIT
|
||||
|
||||
# Run OpenSCAD with echo output (fastest way to check syntax)
|
||||
# Using --export-format=echo just parses and evaluates without rendering
|
||||
if $OPENSCAD -o "$TEMP_OUTPUT" --export-format=echo "$INPUT" 2>&1; then
|
||||
echo "✓ Syntax OK"
|
||||
|
||||
# Check for warnings in stderr
|
||||
if [ -s "$TEMP_OUTPUT" ]; then
|
||||
echo ""
|
||||
echo "Echo output:"
|
||||
cat "$TEMP_OUTPUT"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Validation failed"
|
||||
exit 1
|
||||
fi
|
||||
BIN
rpi-case/previews/001/rpi5_industrial_case_001_back.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
rpi-case/previews/001/rpi5_industrial_case_001_front.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_front.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
rpi-case/previews/001/rpi5_industrial_case_001_iso.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_iso.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
BIN
rpi-case/previews/001/rpi5_industrial_case_001_left.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_left.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
rpi-case/previews/001/rpi5_industrial_case_001_right.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_right.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
BIN
rpi-case/previews/001/rpi5_industrial_case_001_top.png
Normal file
BIN
rpi-case/previews/001/rpi5_industrial_case_001_top.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
289
rpi-case/rpi5_industrial_case_001.scad
Normal file
289
rpi-case/rpi5_industrial_case_001.scad
Normal file
@@ -0,0 +1,289 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure for Luckfox DHX-10.1" Touchscreen
|
||||
// Version: 001
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 236; // screen outer width (mm)
|
||||
scr_h = 144; // screen outer height (mm)
|
||||
scr_d = 19; // screen outer depth (mm)
|
||||
scr_active_w = 222; // active area width (mm) ← confirm
|
||||
scr_active_h = 130; // active area height (mm) ← confirm
|
||||
scr_mount_x = 75; // screen M2.5 mount pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // screen M2.5 mount pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board depth incl. tallest component (mm)
|
||||
pi_mnt_x = 58; // Pi mount hole pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount hole pattern Y (mm)
|
||||
pi_standoff = 5; // standoff height between screen rear and Pi (mm)
|
||||
// Pi offset from screen center (positive = up, right)
|
||||
pi_offset_x = 0; // horizontal offset of Pi center from screen center
|
||||
pi_offset_y = 5; // vertical offset upward from screen center
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 2.5; // wall thickness (mm)
|
||||
chamfer = 1.5; // external edge chamfer (mm)
|
||||
recess = 1.0; // screen recess depth in front bezel (mm)
|
||||
gap = 0.3; // fit clearance between bezel and rear cover
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3; // vent slot width (mm)
|
||||
vent_l = 20; // vent slot length (mm)
|
||||
vent_sp = 4; // slot pitch (edge to edge) (mm)
|
||||
soc_vent_sz = 30; // SoC vent zone size (mm sq)
|
||||
|
||||
// ── CABLE GLAND PARAMETERS ───────────────────────────────────
|
||||
gland_count = 2; // number of cable glands
|
||||
gland_dia = 16.5; // M16 clearance hole diameter (mm)
|
||||
gland_spacing = 40; // spacing between gland centers (mm)
|
||||
|
||||
// ── PEDESTAL PARAMETERS ──────────────────────────────────────
|
||||
ped_tilt = 75; // tilt angle from vertical (deg) — screen tilts back
|
||||
ped_depth = 80; // foot depth front-to-back (mm)
|
||||
ped_width = 200; // foot width (mm)
|
||||
ped_thick = 6; // foot plate thickness (mm)
|
||||
ped_brace_h = 30; // height of triangular brace
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole
|
||||
insert_dia = 4.2; // M3 heat-set insert OD
|
||||
insert_h = 6; // heat-set insert depth
|
||||
|
||||
// ── DERIVED DIMENSIONS ───────────────────────────────────────
|
||||
// Total rear cavity depth = standoffs + Pi + cable headroom
|
||||
rear_d = pi_standoff + pi_d + 10; // 10 mm cable headroom
|
||||
// Outer enclosure size
|
||||
enc_w = scr_w + 2*wall;
|
||||
enc_h = scr_h + 2*wall;
|
||||
enc_d = rear_d + wall; // rear cover depth
|
||||
|
||||
// Pi center position relative to screen center
|
||||
pi_cx = scr_w/2 + pi_offset_x;
|
||||
pi_cy = scr_h/2 + pi_offset_y;
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// MODULES
|
||||
// ============================================================
|
||||
|
||||
// Chamfered box (external chamfer via intersection with offset cube)
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c,c,0]) cube([w-2*c, h-2*c, d]);
|
||||
translate([0,c,c]) cube([w, h-2*c, d-2*c]);
|
||||
translate([c,0,c]) cube([w-2*c, h, d-2*c]);
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded slot (for vents)
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
// M2.5 mounting hole
|
||||
module m25_hole(d=10) {
|
||||
cylinder(d=2.7, h=d);
|
||||
}
|
||||
|
||||
// Heat-set insert boss + M3 hole
|
||||
module insert_boss(h=insert_h+4) {
|
||||
difference() {
|
||||
cylinder(d=insert_dia+3, h=h);
|
||||
cylinder(d=insert_dia, h=insert_h);
|
||||
translate([0,0,insert_h]) cylinder(d=m3_dia, h=h);
|
||||
}
|
||||
}
|
||||
|
||||
// Single vent slot row (horizontal slots)
|
||||
module vent_row(count, slot_len, slot_w, pitch, depth) {
|
||||
for(i=[0:count-1]) {
|
||||
translate([i*(slot_w+pitch), 0, 0])
|
||||
slot(slot_len, slot_w, depth+0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL
|
||||
// ============================================================
|
||||
module front_bezel() {
|
||||
difference() {
|
||||
// Outer chamfered shell
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Active display window (recessed by 1 mm, then open)
|
||||
translate([(enc_w - scr_active_w)/2,
|
||||
(enc_h - scr_active_h)/2,
|
||||
-0.1])
|
||||
cube([scr_active_w, scr_active_h, wall + recess + 0.2]);
|
||||
|
||||
// Bezel lip sits 1 mm over screen edge — recess pocket
|
||||
translate([(enc_w - scr_w)/2,
|
||||
(enc_h - scr_h)/2,
|
||||
wall])
|
||||
cube([scr_w, scr_h, recess + 0.1]);
|
||||
|
||||
// Corner M3 screw holes (through bezel flange, 4 corners)
|
||||
for(x=[wall+6, enc_w-wall-6])
|
||||
for(y=[wall+6, enc_h-wall-6])
|
||||
translate([x, y, -0.1])
|
||||
cylinder(d=m3_dia, h=wall+recess+0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
module rear_cover() {
|
||||
difference() {
|
||||
union() {
|
||||
// Main body
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
|
||||
// Pedestal foot (integral)
|
||||
pedestal_foot();
|
||||
|
||||
// Heat-set insert bosses at 4 corners (inside)
|
||||
for(x=[wall+6, enc_w-wall-6])
|
||||
for(y=[wall+6, enc_h-wall-6])
|
||||
translate([x, y, enc_d])
|
||||
rotate([180,0,0])
|
||||
insert_boss();
|
||||
}
|
||||
|
||||
// Hollow interior
|
||||
translate([wall, wall, wall])
|
||||
cube([scr_w, scr_h, enc_d]);
|
||||
|
||||
// ── PORT CUTOUTS ──────────────────────────────────────
|
||||
|
||||
// USB-C power + 2× HDMI on LEFT edge (Pi left side)
|
||||
// Pi left edge X position in enclosure coords
|
||||
pi_left_x = pi_cx - pi_w/2 + wall;
|
||||
// USB-C power (Pi left edge, near bottom of Pi)
|
||||
translate([-0.1,
|
||||
pi_cy - 8 + wall,
|
||||
wall + pi_standoff + 2])
|
||||
cube([wall+0.2, 10, 10]);
|
||||
// HDMI #1
|
||||
translate([-0.1,
|
||||
pi_cx - pi_w/2 + wall + 15,
|
||||
wall + pi_standoff + 2])
|
||||
cube([wall+0.2, 16, 8]);
|
||||
// HDMI #2
|
||||
translate([-0.1,
|
||||
pi_cx - pi_w/2 + wall + 34,
|
||||
wall + pi_standoff + 2])
|
||||
cube([wall+0.2, 16, 8]);
|
||||
|
||||
// Ethernet RJ45 on RIGHT edge
|
||||
translate([enc_w - wall - 0.1,
|
||||
pi_cy + pi_h/2 - 22 + wall,
|
||||
wall + pi_standoff + 1])
|
||||
cube([wall+0.2, 22, 16]);
|
||||
|
||||
// USB-A ×4 on RIGHT edge
|
||||
translate([enc_w - wall - 0.1,
|
||||
pi_cy - pi_h/2 + wall + 2,
|
||||
wall + pi_standoff + 1])
|
||||
cube([wall+0.2, 50, 14]);
|
||||
|
||||
// GPIO header on TOP edge
|
||||
translate([pi_cx - 30 + wall,
|
||||
enc_h - wall - 0.1,
|
||||
wall + pi_standoff])
|
||||
cube([52, wall+0.2, 12]);
|
||||
|
||||
// USB-C touch on left side edge of SCREEN (not Pi)
|
||||
translate([-0.1, enc_h/2 - 6, wall + scr_d - 5])
|
||||
cube([wall+0.2, 12, 8]);
|
||||
|
||||
// ── COOLING VENTS ──────────────────────────────────────
|
||||
|
||||
// Bottom intake slots
|
||||
translate([enc_w/2 - (5*(vent_w+vent_sp))/2, -0.1, wall+8])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(5, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Top exhaust slots
|
||||
translate([enc_w/2 - (5*(vent_w+vent_sp))/2,
|
||||
enc_h - wall + 0.1,
|
||||
wall+8])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(5, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// SoC direct vent (rear panel, over Pi SoC area)
|
||||
// SoC assumed ~center of Pi board
|
||||
translate([pi_cx - soc_vent_sz/2 + wall,
|
||||
pi_cy - soc_vent_sz/2 + wall,
|
||||
enc_d - wall - 0.1]) {
|
||||
count_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i=[0:count_soc-1])
|
||||
translate([i*(vent_w+vent_sp), soc_vent_sz/2-vent_l/2, 0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// ── CABLE GLANDS ──────────────────────────────────────
|
||||
for(i=[0:gland_count-1]) {
|
||||
cx = enc_w/2 + (i - (gland_count-1)/2) * gland_spacing;
|
||||
translate([cx, -0.1, wall + gland_dia/2 + 4])
|
||||
rotate([-90,0,0])
|
||||
cylinder(d=gland_dia, h=wall+0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PEDESTAL FOOT (integral with rear cover)
|
||||
// ============================================================
|
||||
module pedestal_foot() {
|
||||
// The foot projects from the bottom of the rear cover.
|
||||
// It's a wedge that creates the tilt angle.
|
||||
// When the assembly stands on the foot, the screen tilts back ped_tilt°.
|
||||
//
|
||||
// tilt_angle from vertical → wedge front height > back height.
|
||||
// foot_front_h = ped_depth * tan(90-ped_tilt)
|
||||
foot_front_h = ped_depth * tan(90 - ped_tilt);
|
||||
|
||||
foot_x0 = (enc_w - ped_width) / 2;
|
||||
|
||||
translate([foot_x0, 0, 0]) {
|
||||
// Wedge base plate
|
||||
hull() {
|
||||
// Front edge (taller)
|
||||
translate([0, -ped_depth, 0])
|
||||
cube([ped_width, 0.1, foot_front_h + ped_thick]);
|
||||
// Back edge (at enc base, flush)
|
||||
translate([0, 0, 0])
|
||||
cube([ped_width, 0.1, ped_thick]);
|
||||
}
|
||||
|
||||
// Triangular side braces for rigidity
|
||||
for(bx=[0, ped_width-ped_thick]) {
|
||||
translate([bx, -ped_depth, 0])
|
||||
linear_extrude(ped_thick)
|
||||
polygon([[0,0],
|
||||
[ped_depth, 0],
|
||||
[ped_depth, foot_front_h]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDER — exploded assembly view
|
||||
// ============================================================
|
||||
// Front bezel at Z=0 (face down for printing, shown face up)
|
||||
color("DarkSlateGray", 0.9)
|
||||
translate([0, 0, enc_d + 5])
|
||||
front_bezel();
|
||||
|
||||
// Rear cover
|
||||
color("SlateGray", 0.9)
|
||||
rear_cover();
|
||||
300
rpi-case/rpi5_industrial_case_002.scad
Normal file
300
rpi-case/rpi5_industrial_case_002.scad
Normal file
@@ -0,0 +1,300 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure — Luckfox DHX-10.1" Touchscreen
|
||||
// Version: 002
|
||||
// Fixes vs 001:
|
||||
// 1. Pedestal foot now projects from REAR FACE in -Z direction
|
||||
// 2. Tilt wedge orientation corrected (leans screen back, not forward)
|
||||
// 3. Cable glands moved to rear panel face (foot owns the bottom edge)
|
||||
// 4. GPIO cutout repositioned to match Pi board top-edge location
|
||||
// 5. Port cutout Z-depths corrected using pi_enc_cx/cy consistently
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 236; // screen outer width (mm)
|
||||
scr_h = 144; // screen outer height (mm)
|
||||
scr_d = 19; // screen outer depth (mm)
|
||||
scr_active_w = 222; // active area width (mm) ← confirm
|
||||
scr_active_h = 130; // active area height (mm) ← confirm
|
||||
scr_mount_x = 75; // screen rear M2.5 mount pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // screen rear M2.5 mount pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board depth incl. tallest component (mm)
|
||||
pi_mnt_x = 58; // Pi mount hole pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount hole pattern Y (mm)
|
||||
pi_standoff = 5; // standoff height: screen rear → Pi board (mm)
|
||||
pi_offset_x = 0; // Pi centre horizontal offset from screen centre (mm)
|
||||
pi_offset_y = 5; // Pi centre vertical offset upward from screen centre (mm)
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 2.5; // wall thickness (mm)
|
||||
chamfer = 1.5; // external edge chamfer (mm)
|
||||
recess = 1.0; // screen recess depth in front bezel (mm)
|
||||
gap = 0.3; // bezel ↔ rear cover fit clearance (mm)
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3; // vent slot width (mm)
|
||||
vent_l = 20; // vent slot length (mm)
|
||||
vent_sp = 4; // slot spacing edge-to-edge (mm)
|
||||
soc_vent_sz = 30; // SoC direct-vent zone size (mm, square)
|
||||
|
||||
// ── CABLE GLAND PARAMETERS ───────────────────────────────────
|
||||
gland_count = 2; // number of M16 cable glands
|
||||
gland_dia = 16.5; // M16 clearance hole diameter (mm)
|
||||
gland_spacing= 40; // centre-to-centre spacing (mm)
|
||||
|
||||
// ── PEDESTAL PARAMETERS ──────────────────────────────────────
|
||||
// ped_tilt = angle of screen from horizontal (deg).
|
||||
// 75° from horizontal = 15° lean-back from vertical (near-upright monitor stance).
|
||||
// The foot is a wedge that, when flat on a desk, holds the rear cover at
|
||||
// (90 - ped_tilt)° from vertical.
|
||||
ped_tilt = 75; // screen angle from horizontal (deg)
|
||||
ped_depth = 80; // foot plate depth front-to-back (mm)
|
||||
ped_width = 200; // foot plate width (mm)
|
||||
ped_thick = 6; // foot plate thickness (mm)
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole diameter (mm)
|
||||
insert_dia = 4.2; // M3 heat-set insert OD (mm)
|
||||
insert_h = 6; // heat-set insert depth (mm)
|
||||
|
||||
// ── DERIVED DIMENSIONS (do not edit) ─────────────────────────
|
||||
rear_d = pi_standoff + pi_d + 10; // rear cavity depth (10 mm cable headroom)
|
||||
enc_w = scr_w + 2*wall; // enclosure outer width
|
||||
enc_h = scr_h + 2*wall; // enclosure outer height
|
||||
enc_d = rear_d + wall; // rear cover total depth
|
||||
|
||||
// Pi centre in enclosure coordinates (enclosure origin = rear-cover corner)
|
||||
pi_enc_cx = wall + scr_w/2 + pi_offset_x; // = 120.5 with defaults
|
||||
pi_enc_cy = wall + scr_h/2 + pi_offset_y; // = 79.5 with defaults
|
||||
|
||||
// Z position of Pi board surface (measured from rear of rear cover)
|
||||
pi_z = wall + pi_standoff; // = 7.5 with defaults
|
||||
|
||||
// Foot wedge geometry
|
||||
// foot_drop: how far the far tip drops below Y=0 so the bottom surface
|
||||
// becomes horizontal when the unit stands at ped_tilt from horizontal.
|
||||
foot_drop = ped_depth * tan(90 - ped_tilt); // ≈ 21.4 mm for ped_tilt=75
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// PRIMITIVES
|
||||
// ============================================================
|
||||
|
||||
// Chamfered box — chamfer on all 12 edges via hull of 3 axis-aligned cubes
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c,c,0]) cube([w-2*c, h-2*c, d ]);
|
||||
translate([0,c,c]) cube([w, h-2*c, d-2*c ]);
|
||||
translate([c,0,c]) cube([w-2*c, h, d-2*c ]);
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded-end vent slot, length along Y, centred at origin
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
// Row of n vent slots along X
|
||||
module vent_row(n, len, w, spacing, depth) {
|
||||
for(i=[0:n-1])
|
||||
translate([i*(w+spacing), 0, 0])
|
||||
slot(len, w, depth);
|
||||
}
|
||||
|
||||
// Heat-set insert boss (M3)
|
||||
module insert_boss(h=insert_h+4) {
|
||||
difference() {
|
||||
cylinder(d=insert_dia+3, h=h);
|
||||
cylinder(d=insert_dia, h=insert_h);
|
||||
translate([0,0,insert_h]) cylinder(d=m3_dia, h=h);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PEDESTAL FOOT (integral with rear cover, no supports needed)
|
||||
// ============================================================
|
||||
// Geometry in model space (rear cover lying on its back, rear face = Z=0):
|
||||
// • Foot extends in the -Z direction from Z=0 (behind the rear face)
|
||||
// • Top surface is flush with the enclosure bottom at Y=0
|
||||
// • Bottom surface is angled: at Z=0 it is ped_thick below Y=0;
|
||||
// at Z=-ped_depth it is (ped_thick + foot_drop) below Y=0.
|
||||
// • When the unit stands on the desk the angled surface lies flat and the
|
||||
// screen tilts back (90-ped_tilt)° from vertical.
|
||||
// • Print orientation: rear cover face-down (foot on bed), zero supports.
|
||||
module pedestal_foot() {
|
||||
foot_x0 = (enc_w - ped_width) / 2;
|
||||
|
||||
translate([foot_x0, 0, 0]) {
|
||||
// Main wedge plate
|
||||
hull() {
|
||||
translate([0, -ped_thick, 0 ])
|
||||
cube([ped_width, ped_thick, wall ]);
|
||||
translate([0, -(ped_thick+foot_drop), -ped_depth])
|
||||
cube([ped_width, ped_thick+foot_drop, wall ]);
|
||||
}
|
||||
// Left and right stiffening ribs
|
||||
for(bx = [0, ped_width - ped_thick]) {
|
||||
hull() {
|
||||
translate([bx, -ped_thick, 0 ])
|
||||
cube([ped_thick, ped_thick, wall ]);
|
||||
translate([bx, -(ped_thick+foot_drop), -ped_depth])
|
||||
cube([ped_thick, ped_thick+foot_drop, wall ]);
|
||||
// Toe point keeps underside triangular (no saggy bridge)
|
||||
translate([bx, -ped_thick, -ped_depth])
|
||||
cube([ped_thick, ped_thick, wall ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL
|
||||
// ============================================================
|
||||
module front_bezel() {
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Active display window (full cut-through)
|
||||
translate([(enc_w-scr_active_w)/2, (enc_h-scr_active_h)/2, -0.1])
|
||||
cube([scr_active_w, scr_active_h, wall+recess+0.2]);
|
||||
|
||||
// Recess pocket so bezel lip sits 1 mm over screen edge
|
||||
translate([(enc_w-scr_w)/2, (enc_h-scr_h)/2, wall])
|
||||
cube([scr_w, scr_h, recess+0.1]);
|
||||
|
||||
// M3 corner screw holes (4×)
|
||||
for(x = [wall+6, enc_w-wall-6])
|
||||
for(y = [wall+6, enc_h-wall-6])
|
||||
translate([x, y, -0.1]) cylinder(d=m3_dia, h=wall+recess+0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
module rear_cover() {
|
||||
n_vent = 6;
|
||||
vent_block_w = n_vent*(vent_w+vent_sp) - vent_sp; // total width of vent array
|
||||
|
||||
difference() {
|
||||
union() {
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
pedestal_foot();
|
||||
|
||||
// M3 insert bosses at 4 corners (inner face)
|
||||
for(x = [wall+6, enc_w-wall-6])
|
||||
for(y = [wall+6, enc_h-wall-6])
|
||||
translate([x, y, enc_d])
|
||||
rotate([180,0,0]) insert_boss();
|
||||
}
|
||||
|
||||
// ── HOLLOW INTERIOR ───────────────────────────────────
|
||||
translate([wall, wall, wall]) cube([scr_w, scr_h, enc_d]);
|
||||
|
||||
// ── LEFT WALL: USB-C power + HDMI ×2 ─────────────────
|
||||
// These are on the Pi's left short-edge (56 mm face), facing X=0.
|
||||
// Cutout Y-centre is set near the Pi's lower half.
|
||||
// Z positions follow port heights above PCB surface.
|
||||
// (Short ribbon extensions needed to reach the wall at X=0.)
|
||||
//
|
||||
// USB-C power
|
||||
translate([-0.1,
|
||||
pi_enc_cy - pi_h/2 + 3,
|
||||
pi_z + 2])
|
||||
cube([wall+0.2, 11, 11]);
|
||||
// HDMI 0
|
||||
translate([-0.1,
|
||||
pi_enc_cy - pi_h/2 + 16,
|
||||
pi_z + 2])
|
||||
cube([wall+0.2, 17, 9]);
|
||||
// HDMI 1
|
||||
translate([-0.1,
|
||||
pi_enc_cy - pi_h/2 + 35,
|
||||
pi_z + 2])
|
||||
cube([wall+0.2, 17, 9]);
|
||||
|
||||
// ── RIGHT WALL: RJ45 + USB-A ×4 ──────────────────────
|
||||
// RJ45 (top of Pi right edge in board orientation)
|
||||
translate([enc_w-wall-0.1,
|
||||
pi_enc_cy + pi_h/2 - 24,
|
||||
pi_z + 1])
|
||||
cube([wall+0.2, 22, 16]);
|
||||
// USB-A ×4 (two stacked pairs, below RJ45 on right edge)
|
||||
translate([enc_w-wall-0.1,
|
||||
pi_enc_cy - pi_h/2 + 2,
|
||||
pi_z + 1])
|
||||
cube([wall+0.2, 50, 15]);
|
||||
|
||||
// ── TOP WALL: GPIO header (40-pin) ────────────────────
|
||||
// GPIO is on the Pi's top long edge (85 mm edge at Y = pi_enc_cy + pi_h/2).
|
||||
// Cutout aligns with the header strip X-extent (51 mm) centred on Pi.
|
||||
// Z-extent: board surface + header height (~11 mm).
|
||||
translate([pi_enc_cx - 26,
|
||||
pi_enc_cy + pi_h/2 - 0.1,
|
||||
pi_z])
|
||||
cube([52, wall+0.2, 11]);
|
||||
|
||||
// ── USB-C TOUCH: screen side edge (left, near screen depth) ───
|
||||
translate([-0.1,
|
||||
enc_h/2 - 6,
|
||||
wall + scr_d - 5])
|
||||
cube([wall+0.2, 12, 8]);
|
||||
|
||||
// ── COOLING VENTS ─────────────────────────────────────
|
||||
|
||||
// Bottom intake slots (6 × 3×20 mm, 4 mm spacing)
|
||||
translate([enc_w/2 - vent_block_w/2,
|
||||
-0.1,
|
||||
wall + 8])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Top exhaust slots
|
||||
translate([enc_w/2 - vent_block_w/2,
|
||||
enc_h - wall + 0.1,
|
||||
wall + 8])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// SoC direct-vent array on rear panel, centred over Pi SoC
|
||||
translate([pi_enc_cx - soc_vent_sz/2,
|
||||
pi_enc_cy - soc_vent_sz/2,
|
||||
enc_d - wall - 0.1]) {
|
||||
n_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i = [0:n_soc-1])
|
||||
translate([i*(vent_w+vent_sp),
|
||||
soc_vent_sz/2 - vent_l/2,
|
||||
0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// ── CABLE GLANDS: rear panel face, bottom area ────────
|
||||
// Two M16 glands through the rear face (Z=0 plane).
|
||||
// Positioned below the Pi, above the foot junction.
|
||||
for(i = [0:gland_count-1]) {
|
||||
cx = enc_w/2 + (i - (gland_count-1)/2) * gland_spacing;
|
||||
translate([cx,
|
||||
wall + gland_dia/2 + 4,
|
||||
-0.1])
|
||||
cylinder(d=gland_dia, h=wall+0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// RENDER — exploded assembly (front bezel floats above rear cover)
|
||||
// ============================================================
|
||||
color("DarkSlateGray", 0.9)
|
||||
translate([0, 0, enc_d + 10])
|
||||
front_bezel();
|
||||
|
||||
color("SlateGray", 0.85)
|
||||
rear_cover();
|
||||
328
rpi-case/rpi5_industrial_case_003.scad
Normal file
328
rpi-case/rpi5_industrial_case_003.scad
Normal file
@@ -0,0 +1,328 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure — Luckfox DHX-10.1" Touchscreen
|
||||
// Version: 003
|
||||
// Fixes vs 002:
|
||||
// 1. Kickstand completely redesigned — shorter, thinner, clearly
|
||||
// attached to rear cover bottom, triangular gussets on each side
|
||||
// 2. GPIO top-wall cutout removed (GPIO is fully internal; access
|
||||
// requires removing the rear cover, correct for industrial use)
|
||||
// 3. Pi cavity depth verified and annotated
|
||||
// 4. Bezel two-piece connection: corner bosses on rear cover +
|
||||
// matching through-holes on bezel — intentional removable joint
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 236; // screen outer width (mm)
|
||||
scr_h = 144; // screen outer height (mm)
|
||||
scr_d = 19; // screen outer depth (mm)
|
||||
scr_active_w = 222; // active area width (mm) ← confirm with screen datasheet
|
||||
scr_active_h = 130; // active area height (mm) ← confirm with screen datasheet
|
||||
scr_mount_x = 75; // screen rear M2.5 hole pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // screen rear M2.5 hole pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board + tallest component height (mm)
|
||||
pi_mnt_x = 58; // Pi mount hole pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount hole pattern Y (mm)
|
||||
pi_standoff = 5; // standoff height: screen rear face → Pi PCB (mm)
|
||||
pi_offset_x = 0; // Pi centre X offset from screen centre (mm)
|
||||
pi_offset_y = 5; // Pi centre Y offset upward from screen centre (mm)
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 2.5; // wall thickness throughout (mm)
|
||||
chamfer = 1.5; // external edge chamfer size (mm)
|
||||
recess = 1.0; // screen recess depth in front bezel (mm)
|
||||
gap = 0.3; // bezel ↔ rear cover fit clearance (mm)
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3; // vent slot width (mm)
|
||||
vent_l = 20; // vent slot length (mm)
|
||||
vent_sp = 4; // slot gap edge-to-edge (mm)
|
||||
soc_vent_sz = 30; // SoC direct-vent zone size (mm, square)
|
||||
|
||||
// ── CABLE GLAND PARAMETERS ───────────────────────────────────
|
||||
gland_count = 2; // number of M16 cable glands
|
||||
gland_dia = 16.5; // M16 clearance hole diameter (mm)
|
||||
gland_spacing= 40; // centre-to-centre spacing (mm)
|
||||
|
||||
// ── KICKSTAND PARAMETERS ─────────────────────────────────────
|
||||
// The kickstand is a flat plate + two triangular gussets, integral
|
||||
// with the rear cover bottom. When the unit stands on the kickstand
|
||||
// the plate lies flat on the desk and the screen tilts back
|
||||
// (90 - ks_tilt) degrees from vertical.
|
||||
ks_tilt = 75; // screen angle from horizontal when standing (deg)
|
||||
// 75° from horiz = 15° lean-back from vertical
|
||||
ks_depth = 55; // plate reach behind rear face (mm) — shorter than 002
|
||||
ks_width = 180; // plate span across enclosure width (mm)
|
||||
ks_thick = 5; // plate thickness (mm)
|
||||
ks_gusset_h = 30; // gusset height up the rear cover face (mm)
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole (mm)
|
||||
insert_dia = 4.2; // M3 heat-set insert OD (mm)
|
||||
insert_h = 6; // heat-set insert depth (mm)
|
||||
boss_od = insert_dia + 3.5; // insert boss outer diameter (mm)
|
||||
corner_inset = wall + boss_od/2 + 1; // corner boss/hole X and Y inset (mm)
|
||||
|
||||
// ── DERIVED DIMENSIONS ───────────────────────────────────────
|
||||
//
|
||||
// Rear cavity depth check:
|
||||
// pi_standoff (5) + pi_d (17) + cable headroom (10) = 32 mm → rear_d
|
||||
// rear_d is the full depth of the rear cover cavity.
|
||||
// The screen body (scr_d=19 mm) is NOT included in rear_d;
|
||||
// the rear cover encloses only the space BEHIND the screen rear face.
|
||||
//
|
||||
rear_d = pi_standoff + pi_d + 10; // = 32 mm, Pi fits with 10 mm to spare
|
||||
enc_w = scr_w + 2*wall; // enclosure outer width (241 mm)
|
||||
enc_h = scr_h + 2*wall; // enclosure outer height (149 mm)
|
||||
enc_d = rear_d + wall; // rear cover total depth ( 34.5 mm)
|
||||
|
||||
// Pi centre in enclosure XY coordinates (wall-offset from screen centre)
|
||||
pi_enc_cx = wall + scr_w/2 + pi_offset_x; // 120.5 mm with defaults
|
||||
pi_enc_cy = wall + scr_h/2 + pi_offset_y; // 79.5 mm with defaults
|
||||
|
||||
// Z of Pi PCB surface, measured from rear cover rear face
|
||||
pi_z = wall + pi_standoff; // 7.5 mm with defaults
|
||||
|
||||
// Kickstand tip drop: how far below Y=0 the far edge must sit so the
|
||||
// bottom surface is horizontal when the unit tilts to ks_tilt from horizontal
|
||||
ks_drop = ks_depth * tan(90 - ks_tilt); // ≈ 14.7 mm for ks_tilt=75
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// PRIMITIVES
|
||||
// ============================================================
|
||||
|
||||
// Chamfered rectangular box (all 12 edges, chamfer = c)
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c, c, 0]) cube([w-2*c, h-2*c, d ]);
|
||||
translate([0, c, c]) cube([w, h-2*c, d-2*c ]);
|
||||
translate([c, 0, c]) cube([w-2*c, h, d-2*c ]);
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded-end vent slot: length along Y, width w, extrudes in +Z by d
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
// Row of n vent slots stepping in X
|
||||
module vent_row(n, len, w, spacing, depth) {
|
||||
for(i = [0:n-1])
|
||||
translate([i*(w+spacing), 0, 0])
|
||||
slot(len, w, depth);
|
||||
}
|
||||
|
||||
// M3 heat-set insert boss (sits proud from an inner face)
|
||||
module insert_boss(total_h = insert_h + 4) {
|
||||
difference() {
|
||||
cylinder(d=boss_od, h=total_h);
|
||||
cylinder(d=insert_dia, h=insert_h);
|
||||
translate([0,0,insert_h]) cylinder(d=m3_dia, h=total_h);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// KICKSTAND
|
||||
// ============================================================
|
||||
// Geometry (all in rear-cover model space, rear face = Z=0 plane):
|
||||
//
|
||||
// Side view (Y-Z plane):
|
||||
//
|
||||
// Y=ks_gusset_h ─┐
|
||||
// │ ← gusset strip on rear face
|
||||
// Y=0 ───────────┼──────────────────────────────────── rear cover bottom
|
||||
// │╲ ← gusset triangle
|
||||
// plate ──┼─╲──────────────────────────────────────────
|
||||
// (ks_thick)│ ╲ (sloping, thicker at tip)
|
||||
// ╲ ╲___________________________________
|
||||
// Y=-(ks_thick+ks_drop) Z=-ks_depth
|
||||
//
|
||||
// The plate and gussets are extruded across ks_width in X.
|
||||
// The gussets (hull triangles) brace the plate against the rear face,
|
||||
// preventing the kickstand from snapping off at the root.
|
||||
//
|
||||
module kickstand() {
|
||||
ks_x0 = (enc_w - ks_width) / 2;
|
||||
|
||||
translate([ks_x0, 0, 0]) {
|
||||
|
||||
// ── Main plate ────────────────────────────────────────
|
||||
// Wedge: root at Z=0 is ks_thick tall;
|
||||
// tip at Z=-ks_depth is (ks_thick+ks_drop) tall.
|
||||
// Top surface flush with enclosure bottom (Y=0).
|
||||
hull() {
|
||||
// Root strip — along rear face
|
||||
translate([0, -ks_thick, 0])
|
||||
cube([ks_width, ks_thick, wall]);
|
||||
// Tip strip — at full reach, thicker to keep plate horizontal
|
||||
translate([0, -(ks_thick + ks_drop), -ks_depth])
|
||||
cube([ks_width, ks_thick + ks_drop, wall]);
|
||||
}
|
||||
|
||||
// ── Triangular gussets (left + right ends) ────────────
|
||||
// Each gusset is a hull of three patches:
|
||||
// A – vertical strip up the rear face (height = ks_gusset_h)
|
||||
// B – small square at plate root (Y=-ks_thick, Z=0)
|
||||
// C – small square at plate tip (Y=-(ks_thick+ks_drop), Z=-ks_depth)
|
||||
for(bx = [0, ks_width - ks_thick]) {
|
||||
hull() {
|
||||
// A: attachment strip going up the rear face
|
||||
translate([bx, 0, -wall])
|
||||
cube([ks_thick, ks_gusset_h, wall]);
|
||||
// B: plate root corner
|
||||
translate([bx, -ks_thick, -wall])
|
||||
cube([ks_thick, ks_thick, wall]);
|
||||
// C: plate tip corner
|
||||
translate([bx, -(ks_thick + ks_drop), -ks_depth])
|
||||
cube([ks_thick, ks_thick, wall]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL
|
||||
// ============================================================
|
||||
// Two-piece design: bezel + rear cover join with M3 screws through
|
||||
// the bezel corners into heat-set inserts in the rear cover bosses.
|
||||
// The bezel is intentionally removable for Pi access.
|
||||
module front_bezel() {
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Active display window (full depth cut)
|
||||
translate([(enc_w - scr_active_w)/2,
|
||||
(enc_h - scr_active_h)/2,
|
||||
-0.1])
|
||||
cube([scr_active_w, scr_active_h, wall+recess+0.2]);
|
||||
|
||||
// 1 mm recess pocket — bezel lip grips screen edge
|
||||
translate([(enc_w - scr_w)/2,
|
||||
(enc_h - scr_h)/2,
|
||||
wall])
|
||||
cube([scr_w, scr_h, recess+0.1]);
|
||||
|
||||
// M3 screw clearance holes at 4 corners
|
||||
for(x = [corner_inset, enc_w - corner_inset])
|
||||
for(y = [corner_inset, enc_h - corner_inset])
|
||||
translate([x, y, -0.1])
|
||||
cylinder(d=m3_dia, h=wall+recess+0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
module rear_cover() {
|
||||
n_vent = 6;
|
||||
vent_block_w = n_vent*(vent_w+vent_sp) - vent_sp;
|
||||
|
||||
difference() {
|
||||
union() {
|
||||
// Main body
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
|
||||
// Kickstand (integral, no supports needed — prints face-down)
|
||||
kickstand();
|
||||
|
||||
// Insert bosses at 4 corners (inner rear face, flush with enc_d)
|
||||
for(x = [corner_inset, enc_w - corner_inset])
|
||||
for(y = [corner_inset, enc_h - corner_inset])
|
||||
translate([x, y, enc_d])
|
||||
rotate([180, 0, 0])
|
||||
insert_boss();
|
||||
}
|
||||
|
||||
// ── HOLLOW INTERIOR ───────────────────────────────────
|
||||
// Cavity = full screen footprint, from wall to enc_d (open toward bezel)
|
||||
translate([wall, wall, wall])
|
||||
cube([scr_w, scr_h, enc_d]);
|
||||
|
||||
// ── PORT CUTOUTS ──────────────────────────────────────
|
||||
// NOTE: The Pi's port edges are internal (Pi centred on screen).
|
||||
// Cutouts in the enclosure walls are reference openings for
|
||||
// short cable extensions routed to the wall. Adjust Y/Z offsets
|
||||
// to match your exact cable routing once screen mount is verified.
|
||||
|
||||
// LEFT WALL — USB-C power + HDMI ×2
|
||||
// Approximate Y positions relative to Pi bottom edge
|
||||
pi_bot = pi_enc_cy - pi_h/2;
|
||||
// USB-C power
|
||||
translate([-0.1, pi_bot + 3, pi_z + 2]) cube([wall+0.2, 11, 11]);
|
||||
// HDMI 0
|
||||
translate([-0.1, pi_bot + 16, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
// HDMI 1
|
||||
translate([-0.1, pi_bot + 35, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
|
||||
// RIGHT WALL — RJ45 + USB-A ×4
|
||||
pi_top = pi_enc_cy + pi_h/2;
|
||||
// RJ45
|
||||
translate([enc_w-wall-0.1, pi_top - 24, pi_z + 1])
|
||||
cube([wall+0.2, 22, 16]);
|
||||
// USB-A ×4 (two stacked pairs)
|
||||
translate([enc_w-wall-0.1, pi_bot + 2, pi_z + 1])
|
||||
cube([wall+0.2, 50, 15]);
|
||||
|
||||
// BOTTOM WALL — USB-C touch connector on screen side edge
|
||||
// (screen's own USB-C touch port, not Pi — sits at screen depth)
|
||||
translate([-0.1,
|
||||
enc_h/2 - 6,
|
||||
wall + scr_d - 5])
|
||||
cube([wall+0.2, 12, 8]);
|
||||
|
||||
// ── COOLING VENTS ─────────────────────────────────────
|
||||
|
||||
// Bottom intake — 6 slots through bottom wall
|
||||
translate([enc_w/2 - vent_block_w/2, -0.1, wall + 8])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Top exhaust — 6 slots through top wall
|
||||
translate([enc_w/2 - vent_block_w/2,
|
||||
enc_h - wall + 0.1,
|
||||
wall + 8])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// SoC direct-vent — slot array in rear panel centred over Pi SoC
|
||||
translate([pi_enc_cx - soc_vent_sz/2,
|
||||
pi_enc_cy - soc_vent_sz/2,
|
||||
enc_d - wall - 0.1]) {
|
||||
n_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i = [0:n_soc-1])
|
||||
translate([i*(vent_w+vent_sp),
|
||||
soc_vent_sz/2 - vent_l/2,
|
||||
0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// ── CABLE GLANDS — rear panel, bottom area ────────────
|
||||
// Two M16 glands through the rear face (Z=0 plane).
|
||||
// Positioned below Pi, above kickstand root.
|
||||
for(i = [0:gland_count-1]) {
|
||||
cx = enc_w/2 + (i - (gland_count-1)/2) * gland_spacing;
|
||||
translate([cx, wall + gland_dia/2 + 4, -0.1])
|
||||
cylinder(d=gland_dia, h=wall+0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SCENE — exploded assembly view
|
||||
// Front bezel floats above rear cover to show the joint
|
||||
// ============================================================
|
||||
color("DarkSlateGray", 0.9)
|
||||
translate([0, 0, enc_d + 12])
|
||||
front_bezel();
|
||||
|
||||
color("SlateGray", 0.85)
|
||||
rear_cover();
|
||||
338
rpi-case/rpi5_industrial_case_004.scad
Normal file
338
rpi-case/rpi5_industrial_case_004.scad
Normal file
@@ -0,0 +1,338 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure — Luckfox DHX-10.1" Touchscreen
|
||||
// Version: 004
|
||||
// Changes vs 003:
|
||||
// 1. KICKSTAND is now a separate, removable piece (own module + color)
|
||||
// - Full enc_w width (no shorter ks_width param)
|
||||
// - 3 prongs on rear edge that slide into slots in case bottom wall
|
||||
// - Tapered prong tips for easy insertion
|
||||
// 2. Prong slots added to rear cover bottom wall
|
||||
// 3. USB-C touch cutout on left wall REMOVED (per user request)
|
||||
// 4. Wall thickness increased 2.5 → 4 mm for rigidity
|
||||
// 5. Bezel screw holes now countersunk + clearly sized
|
||||
// 6. Insert bosses on rear cover made taller and more prominent
|
||||
// so the two-piece (bezel + rear cover) joint is obvious in renders
|
||||
// 7. Render shows THREE separate bodies: bezel / rear cover / kickstand
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 236; // screen outer width (mm)
|
||||
scr_h = 144; // screen outer height (mm)
|
||||
scr_d = 19; // screen outer depth (mm)
|
||||
scr_active_w = 222; // active display area width (mm) ← confirm
|
||||
scr_active_h = 130; // active display area height (mm) ← confirm
|
||||
scr_mount_x = 75; // screen rear M2.5 hole pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // screen rear M2.5 hole pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board + tallest component (mm)
|
||||
pi_mnt_x = 58; // Pi mount hole pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount hole pattern Y (mm)
|
||||
pi_standoff = 5; // standoff height screen-rear → Pi PCB (mm)
|
||||
pi_offset_x = 0; // Pi centre X offset from screen centre (mm)
|
||||
pi_offset_y = 5; // Pi centre Y offset upward from screen centre (mm)
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 4.0; // wall thickness — increased for rigidity (mm)
|
||||
chamfer = 1.5; // external edge chamfer (mm)
|
||||
recess = 1.0; // screen recess in front bezel (mm)
|
||||
gap = 0.3; // bezel ↔ rear cover fit clearance (mm)
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3; // vent slot width (mm)
|
||||
vent_l = 20; // vent slot length (mm)
|
||||
vent_sp = 4; // slot gap edge-to-edge (mm)
|
||||
soc_vent_sz = 30; // SoC direct-vent area size (mm)
|
||||
|
||||
// ── CABLE GLAND PARAMETERS ───────────────────────────────────
|
||||
gland_count = 2; // number of M16 cable glands
|
||||
gland_dia = 16.5; // M16 clearance hole diameter (mm)
|
||||
gland_spacing= 40; // gland centre-to-centre (mm)
|
||||
|
||||
// ── KICKSTAND PARAMETERS ─────────────────────────────────────
|
||||
// Separate removable piece — slides onto case bottom via 3 prongs.
|
||||
// Wedge shape: thin at rear (prong end), thick at front desk-contact end.
|
||||
// When flat on desk the screen sits at ks_tilt degrees from horizontal.
|
||||
ks_tilt = 75; // screen angle from horizontal when standing (deg)
|
||||
// 75° from horiz ≈ 15° lean-back from vertical
|
||||
ks_depth = 60; // plate reach in -Z from case rear face (mm)
|
||||
ks_thick = 5; // plate thickness at thin (rear) end (mm)
|
||||
|
||||
// Prong dimensions — 3 prongs slide into 3 slots in case bottom wall
|
||||
ks_prong_n = 3; // number of prongs
|
||||
ks_prong_w = 12; // prong width (X, mm)
|
||||
ks_prong_h = 14; // prong insertion height into cavity (mm)
|
||||
ks_prong_t = 5; // prong depth (Z, mm) — same as slot depth
|
||||
ks_prong_clr = 0.25; // diametral clearance for fit (mm)
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole (mm)
|
||||
m3_cs_dia = 6.5; // M3 countersink diameter (mm)
|
||||
m3_cs_depth = 3.0; // countersink depth (mm)
|
||||
insert_dia = 4.2; // M3 heat-set insert OD (mm)
|
||||
insert_h = 6; // heat-set insert depth (mm)
|
||||
boss_od = 10; // insert boss outer diameter — prominent (mm)
|
||||
boss_h = insert_h + 5; // boss total height from inner face (mm)
|
||||
|
||||
// ── DERIVED DIMENSIONS ───────────────────────────────────────
|
||||
rear_d = pi_standoff + pi_d + 10; // cavity depth = 32 mm
|
||||
enc_w = scr_w + 2*wall; // outer width = 244 mm
|
||||
enc_h = scr_h + 2*wall; // outer height = 152 mm
|
||||
enc_d = rear_d + wall; // rear cover depth = 36 mm
|
||||
|
||||
// Corner inset for boss/screw centres (keeps them inside the wall)
|
||||
corner_inset = wall + boss_od/2 + 0.5; // ≈ 9.5 mm
|
||||
|
||||
// Pi centre in enclosure coordinates
|
||||
pi_enc_cx = wall + scr_w/2 + pi_offset_x; // 122 mm
|
||||
pi_enc_cy = wall + scr_h/2 + pi_offset_y; // 81 mm
|
||||
pi_z = wall + pi_standoff; // 9 mm (Pi PCB Z from rear face)
|
||||
|
||||
// Kickstand geometry
|
||||
ks_drop = ks_depth * tan(90 - ks_tilt); // ≈ 16 mm tip drop
|
||||
|
||||
// Prong slot Z position — prongs sit right at the case rear face
|
||||
ks_prong_z = 0; // prong rear face flush with case Z=0
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// PRIMITIVES
|
||||
// ============================================================
|
||||
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c, c, 0]) cube([w-2*c, h-2*c, d ]);
|
||||
translate([0, c, c]) cube([w, h-2*c, d-2*c]);
|
||||
translate([c, 0, c]) cube([w-2*c, h, d-2*c]);
|
||||
}
|
||||
}
|
||||
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
module vent_row(n, len, w, spacing, depth) {
|
||||
for(i = [0:n-1])
|
||||
translate([i*(w+spacing), 0, 0])
|
||||
slot(len, w, depth);
|
||||
}
|
||||
|
||||
// Heat-set insert boss
|
||||
module insert_boss() {
|
||||
difference() {
|
||||
cylinder(d=boss_od, h=boss_h);
|
||||
cylinder(d=insert_dia, h=insert_h);
|
||||
translate([0,0,insert_h]) cylinder(d=m3_dia, h=boss_h);
|
||||
}
|
||||
}
|
||||
|
||||
// Countersunk M3 hole (for bezel face)
|
||||
module m3_countersunk(depth) {
|
||||
cylinder(d=m3_dia, h=depth+0.1);
|
||||
translate([0, 0, depth - m3_cs_depth])
|
||||
cylinder(d1=m3_dia, d2=m3_cs_dia, h=m3_cs_depth+0.1);
|
||||
}
|
||||
|
||||
// Prong slot — cut from bottom wall, prong inserts up into cavity
|
||||
module prong_slot() {
|
||||
translate([-ks_prong_w/2 - ks_prong_clr,
|
||||
-0.1,
|
||||
ks_prong_z - ks_prong_clr])
|
||||
cube([ks_prong_w + 2*ks_prong_clr,
|
||||
ks_prong_h + wall + 0.2,
|
||||
ks_prong_t + 2*ks_prong_clr]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// KICKSTAND (separate removable piece)
|
||||
// ============================================================
|
||||
//
|
||||
// Side view (Y-Z plane, unit standing on kickstand):
|
||||
//
|
||||
// Y=0 (case bottom) ─────────────────────────────
|
||||
// │↑↑↑ prongs (insert into case)
|
||||
// Y=-ks_thick ─────┼─────────────────────────────────┐
|
||||
// \ wedge plate (bottom surface │
|
||||
// \ angled so it lies flat when │
|
||||
// Y=-(ks_thick+ks_drop)\ screen tilts to ks_tilt) │
|
||||
// └──────────────────────────────┘
|
||||
// Z=0 Z=-ks_depth
|
||||
//
|
||||
module kickstand() {
|
||||
ks_front_h = ks_thick + ks_drop; // total height at the front (desk-contact) edge
|
||||
|
||||
// Main wedge plate
|
||||
hull() {
|
||||
// Rear (thin) edge, at Z=0 — where prongs attach
|
||||
translate([0, -ks_thick, 0])
|
||||
cube([enc_w, ks_thick, wall]);
|
||||
// Front (thick) edge, at Z=-ks_depth — rests on desk
|
||||
translate([0, -ks_front_h, -ks_depth])
|
||||
cube([enc_w, ks_front_h, wall]);
|
||||
}
|
||||
|
||||
// Three prongs — evenly spaced, rise from Y=0 into case cavity
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px - ks_prong_w/2, 0, ks_prong_z]) {
|
||||
// Tapered tip so prong slides in easily
|
||||
hull() {
|
||||
// Base: full-width prong up to the tapered section
|
||||
cube([ks_prong_w,
|
||||
ks_prong_h - 2,
|
||||
ks_prong_t]);
|
||||
// Tip: narrowed 1 mm per side for 45° insertion chamfer
|
||||
translate([1, ks_prong_h - 2, 0])
|
||||
cube([ks_prong_w - 2, 2, ks_prong_t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL
|
||||
// ============================================================
|
||||
// Removable front frame — held to rear cover by 4× M3 screws
|
||||
// through countersunk holes at each corner, threading into
|
||||
// heat-set inserts pressed into the rear cover's corner bosses.
|
||||
module front_bezel() {
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Active display window
|
||||
translate([(enc_w - scr_active_w)/2,
|
||||
(enc_h - scr_active_h)/2,
|
||||
-0.1])
|
||||
cube([scr_active_w, scr_active_h, wall+recess+0.2]);
|
||||
|
||||
// 1 mm recess pocket — bezel lip grips screen edge
|
||||
translate([(enc_w - scr_w)/2,
|
||||
(enc_h - scr_h)/2,
|
||||
wall])
|
||||
cube([scr_w, scr_h, recess+0.1]);
|
||||
|
||||
// 4× M3 countersunk screw holes at corners
|
||||
for(x = [corner_inset, enc_w - corner_inset])
|
||||
for(y = [corner_inset, enc_h - corner_inset])
|
||||
translate([x, y, 0])
|
||||
m3_countersunk(wall + recess);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
module rear_cover() {
|
||||
n_vent = 6;
|
||||
vent_block_w = n_vent*(vent_w+vent_sp) - vent_sp;
|
||||
|
||||
difference() {
|
||||
union() {
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
|
||||
// ── Insert bosses: 4 corners, proud on inner rear face ──
|
||||
// These are clearly visible tall cylinders that receive the
|
||||
// heat-set inserts; M3 screws from the bezel thread into them.
|
||||
for(x = [corner_inset, enc_w - corner_inset])
|
||||
for(y = [corner_inset, enc_h - corner_inset])
|
||||
translate([x, y, enc_d])
|
||||
rotate([180, 0, 0])
|
||||
insert_boss();
|
||||
}
|
||||
|
||||
// ── HOLLOW INTERIOR ───────────────────────────────────
|
||||
translate([wall, wall, wall])
|
||||
cube([scr_w, scr_h, enc_d]);
|
||||
|
||||
// ── PORT CUTOUTS ──────────────────────────────────────
|
||||
|
||||
// LEFT WALL — USB-C power + HDMI ×2 (Pi left short-edge ports)
|
||||
pi_bot = pi_enc_cy - pi_h/2;
|
||||
// USB-C power
|
||||
translate([-0.1, pi_bot + 3, pi_z + 2]) cube([wall+0.2, 11, 11]);
|
||||
// HDMI 0
|
||||
translate([-0.1, pi_bot + 16, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
// HDMI 1
|
||||
translate([-0.1, pi_bot + 35, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
|
||||
// RIGHT WALL — RJ45 + USB-A ×4 (Pi right short-edge ports)
|
||||
pi_top = pi_enc_cy + pi_h/2;
|
||||
// RJ45
|
||||
translate([enc_w-wall-0.1, pi_top - 24, pi_z + 1])
|
||||
cube([wall+0.2, 22, 16]);
|
||||
// USB-A ×4
|
||||
translate([enc_w-wall-0.1, pi_bot + 2, pi_z + 1])
|
||||
cube([wall+0.2, 50, 15]);
|
||||
|
||||
// NOTE: USB-C touch cutout (screen side) REMOVED per v004 request.
|
||||
// GPIO header is internal — access by removing bezel + rear cover.
|
||||
|
||||
// ── COOLING VENTS ─────────────────────────────────────
|
||||
|
||||
// Bottom intake slots (through bottom wall)
|
||||
translate([enc_w/2 - vent_block_w/2, -0.1, wall + 8])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Top exhaust slots (through top wall)
|
||||
translate([enc_w/2 - vent_block_w/2,
|
||||
enc_h - wall + 0.1,
|
||||
wall + 8])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// SoC direct-vent on rear panel
|
||||
translate([pi_enc_cx - soc_vent_sz/2,
|
||||
pi_enc_cy - soc_vent_sz/2,
|
||||
enc_d - wall - 0.1]) {
|
||||
n_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i = [0:n_soc-1])
|
||||
translate([i*(vent_w+vent_sp),
|
||||
soc_vent_sz/2 - vent_l/2,
|
||||
0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// ── CABLE GLANDS — rear panel, bottom area ────────────
|
||||
for(i = [0:gland_count-1]) {
|
||||
cx = enc_w/2 + (i - (gland_count-1)/2) * gland_spacing;
|
||||
translate([cx, wall + gland_dia/2 + 4, -0.1])
|
||||
cylinder(d=gland_dia, h=wall+0.2);
|
||||
}
|
||||
|
||||
// ── KICKSTAND PRONG SLOTS — bottom wall ───────────────
|
||||
// 3 slots matching kickstand prong positions and sizes.
|
||||
// Slots pass through the bottom wall into the cavity so prongs
|
||||
// engage wall + (ks_prong_h - wall) mm inside the cavity.
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px, 0, ks_prong_z])
|
||||
prong_slot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SCENE — three separate bodies, exploded for clarity
|
||||
// ============================================================
|
||||
|
||||
// Front bezel — exploded up, face toward viewer
|
||||
color("DarkSlateGray", 0.92)
|
||||
translate([0, 0, enc_d + 14])
|
||||
front_bezel();
|
||||
|
||||
// Rear cover — at origin
|
||||
color("SlateGray", 0.88)
|
||||
rear_cover();
|
||||
|
||||
// Kickstand — exploded below, separate piece
|
||||
color("DimGray", 0.85)
|
||||
translate([0, -(ks_thick + ks_drop + 20), 0])
|
||||
kickstand();
|
||||
354
rpi-case/rpi5_industrial_case_005.scad
Normal file
354
rpi-case/rpi5_industrial_case_005.scad
Normal file
@@ -0,0 +1,354 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure — 7" Capacitive Touchscreen
|
||||
// Version: 005
|
||||
//
|
||||
// ASSEMBLY LOGIC (read before printing):
|
||||
//
|
||||
// THREE separate printed parts:
|
||||
//
|
||||
// 1. REAR COVER — the main box. Open face points toward screen.
|
||||
// Four corner TOWERS rise from the open front face; each tower
|
||||
// has a self-tapping M3 pilot hole that opens toward the screen.
|
||||
// The kickstand prong columns rise from the inner bottom face.
|
||||
//
|
||||
// 2. FRONT BEZEL — the display frame. Four countersunk M3 holes at
|
||||
// the corners align with the rear cover's tower holes.
|
||||
// Assembly: lay screen face-down, place rear cover over it,
|
||||
// lay bezel over the front, drive 4× M3×30 self-tapping screws
|
||||
// from the bezel face through into the corner towers.
|
||||
//
|
||||
// 3. KICKSTAND — separate wedge plate. Slide its 3 prongs upward
|
||||
// into the 3 slots in the case bottom wall. The prong guide
|
||||
// columns inside the case prevent the prongs from falling into
|
||||
// the main cavity and give a solid 14 mm engagement.
|
||||
//
|
||||
// Changes vs 004:
|
||||
// - Screen resized to 164.9 × 124.27 mm (7" 1024×600 capacitive)
|
||||
// - Corner towers replace insert bosses: visible M3 holes on the
|
||||
// OPEN front face of the rear cover — no hidden geometry
|
||||
// - Prong guide columns (nested-difference CSG) give the slots a
|
||||
// closed ceiling so the kickstand prongs cannot fall through
|
||||
// - Left-wall USB-C touch cutout permanently removed
|
||||
// - Wall 4 mm retained
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 164.9; // screen outer width (mm)
|
||||
scr_h = 124.27; // screen outer height (mm)
|
||||
scr_d = 12; // screen body depth (mm) ← confirm with calipers
|
||||
scr_active_w = 154; // active display width (mm) ← confirm
|
||||
scr_active_h = 90; // active display height (mm) ← confirm
|
||||
scr_mount_x = 75; // rear M2.5 hole pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // rear M2.5 hole pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board + tallest component (mm)
|
||||
pi_mnt_x = 58; // Pi mount hole pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount hole pattern Y (mm)
|
||||
pi_standoff = 5; // standoff: screen rear → Pi PCB (mm)
|
||||
pi_offset_x = 0; // Pi centre X offset from screen centre (mm)
|
||||
pi_offset_y = 0; // Pi centre Y offset from screen centre (mm)
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 4.0; // wall thickness (mm)
|
||||
chamfer = 1.5; // external edge chamfer (mm)
|
||||
recess = 1.0; // screen recess depth in front bezel (mm)
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3; // slot width (mm)
|
||||
vent_l = 18; // slot length (mm)
|
||||
vent_sp = 4; // gap edge-to-edge (mm)
|
||||
soc_vent_sz = 28; // SoC vent zone (mm, square)
|
||||
|
||||
// ── CABLE GLAND PARAMETERS ───────────────────────────────────
|
||||
gland_count = 2; // number of M16 cable glands
|
||||
gland_dia = 16.5; // M16 clearance hole diameter (mm)
|
||||
gland_spacing= 36; // gland centre-to-centre (mm)
|
||||
|
||||
// ── KICKSTAND PARAMETERS ─────────────────────────────────────
|
||||
ks_tilt = 75; // screen angle from horizontal when standing (deg)
|
||||
ks_depth = 60; // plate reach behind rear face (mm)
|
||||
ks_thick = 5; // plate thickness at thin (prong) end (mm)
|
||||
ks_prong_n = 3; // number of prongs
|
||||
ks_prong_w = 12; // prong width in X (mm)
|
||||
ks_prong_h = 14; // prong engagement height (mm) — guided inside column
|
||||
ks_prong_t = 5; // prong thickness in Z (mm)
|
||||
ks_prong_clr = 0.25; // clearance per side for slide fit (mm)
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole (mm)
|
||||
m3_pilot = 2.5; // M3 self-tapping pilot hole (mm)
|
||||
m3_cs_dia = 6.5; // M3 countersink OD (mm)
|
||||
m3_cs_depth = 3.5; // countersink depth (mm)
|
||||
// Corner tower — a full-depth solid pillar at each inner corner.
|
||||
// The M3 pilot hole is drilled from the open front face of the rear cover.
|
||||
tower_w = 10; // tower footprint width and depth (mm)
|
||||
tower_hole_d = 12; // M3 pilot hole depth from front face (mm)
|
||||
|
||||
// ── DERIVED DIMENSIONS ───────────────────────────────────────
|
||||
rear_d = pi_standoff + pi_d + 10; // rear cavity depth = 32 mm
|
||||
enc_w = scr_w + 2*wall; // outer width = 172.9 mm
|
||||
enc_h = scr_h + 2*wall; // outer height = 132.27 mm
|
||||
enc_d = rear_d + wall; // rear cover depth = 36 mm
|
||||
|
||||
// Pi centre in enclosure coordinates
|
||||
pi_enc_cx = wall + scr_w/2 + pi_offset_x;
|
||||
pi_enc_cy = wall + scr_h/2 + pi_offset_y;
|
||||
pi_z = wall + pi_standoff; // Pi PCB Z from rear face
|
||||
|
||||
// Corner tower position — inset so tower is entirely within the wall zone
|
||||
// (tower must NOT overlap the screen footprint X=wall..wall+scr_w)
|
||||
tower_cx = wall/2; // tower centre offset from outer edge
|
||||
// Four tower centre positions
|
||||
tower_xs = [tower_cx, enc_w - tower_cx];
|
||||
tower_ys = [tower_cx, enc_h - tower_cx];
|
||||
|
||||
// Kickstand wedge geometry
|
||||
ks_drop = ks_depth * tan(90 - ks_tilt); // tip drop ≈ 16 mm
|
||||
|
||||
// Prong column Z extent (guide column behind and into cavity)
|
||||
col_z_size = ks_prong_t + 2*wall; // = 13 mm
|
||||
col_y_size = ks_prong_h; // = 14 mm (above inner bottom face)
|
||||
col_x_size = ks_prong_w + wall; // = 16 mm (centred on prong)
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// PRIMITIVES
|
||||
// ============================================================
|
||||
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c, c, 0]) cube([w-2*c, h-2*c, d ]);
|
||||
translate([0, c, c]) cube([w, h-2*c, d-2*c ]);
|
||||
translate([c, 0, c]) cube([w-2*c, h, d-2*c ]);
|
||||
}
|
||||
}
|
||||
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
module vent_row(n, len, w, spacing, depth) {
|
||||
for(i = [0:n-1])
|
||||
translate([i*(w+spacing), 0, 0])
|
||||
slot(len, w, depth);
|
||||
}
|
||||
|
||||
// Countersunk M3 through-hole for bezel face
|
||||
module m3_countersunk(total_depth) {
|
||||
cylinder(d=m3_dia, h=total_depth+0.1);
|
||||
cylinder(d1=m3_cs_dia, d2=m3_dia, h=m3_cs_depth+0.1);
|
||||
}
|
||||
|
||||
// Prong slot cutter — used in both the rear cover and the guide column
|
||||
// Origin at the prong centre-X, outer bottom face (Y=0), prong-Z start
|
||||
module prong_slot_cut() {
|
||||
translate([-ks_prong_w/2 - ks_prong_clr,
|
||||
-0.1,
|
||||
-ks_prong_clr])
|
||||
cube([ks_prong_w + 2*ks_prong_clr,
|
||||
wall + ks_prong_h + 0.2,
|
||||
ks_prong_t + 2*ks_prong_clr]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// KICKSTAND (separate removable piece, print separately)
|
||||
// ============================================================
|
||||
module kickstand() {
|
||||
ks_front_h = ks_thick + ks_drop; // height at the thick/front end
|
||||
|
||||
// Wedge plate — thin at prong end (Z=0), thick at desk end (Z=-ks_depth)
|
||||
hull() {
|
||||
translate([0, -ks_thick, 0])
|
||||
cube([enc_w, ks_thick, wall]);
|
||||
translate([0, -ks_front_h, -ks_depth])
|
||||
cube([enc_w, ks_front_h, wall]);
|
||||
}
|
||||
|
||||
// Three tapered prongs on the top edge (rear/thin end)
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px - ks_prong_w/2, 0, 0]) {
|
||||
// Body of prong
|
||||
cube([ks_prong_w, ks_prong_h - 2, ks_prong_t]);
|
||||
// Tapered tip (last 2 mm, narrowed 1 mm per side)
|
||||
translate([0, ks_prong_h - 2, 0])
|
||||
hull() {
|
||||
cube([ks_prong_w, 0.01, ks_prong_t]);
|
||||
translate([1, 2, 0])
|
||||
cube([ks_prong_w - 2, 0.01, ks_prong_t]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL (removable — 4× M3 countersunk screws)
|
||||
// ============================================================
|
||||
module front_bezel() {
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Display window
|
||||
translate([(enc_w - scr_active_w)/2,
|
||||
(enc_h - scr_active_h)/2,
|
||||
-0.1])
|
||||
cube([scr_active_w, scr_active_h, wall+recess+0.2]);
|
||||
|
||||
// 1 mm recess pocket — bezel lip grips screen edge
|
||||
translate([(enc_w - scr_w)/2,
|
||||
(enc_h - scr_h)/2,
|
||||
wall])
|
||||
cube([scr_w, scr_h, recess+0.1]);
|
||||
|
||||
// 4× M3 countersunk screw holes, aligned to corner towers
|
||||
for(x = tower_xs) for(y = tower_ys)
|
||||
translate([x, y, 0])
|
||||
m3_countersunk(wall + recess);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
//
|
||||
// CORNER TOWER DESIGN — replaces hidden insert bosses:
|
||||
// Each corner has a solid square tower (tower_w × tower_w) that
|
||||
// runs the FULL DEPTH of the cavity (from inner rear face to the
|
||||
// open front at Z=enc_d). An M3 pilot hole enters from the front
|
||||
// face (Z=enc_d) and goes tower_hole_d into the tower body.
|
||||
// Because the tower reaches the front opening, the holes are
|
||||
// plainly visible from the front of the assembled unit — no
|
||||
// hidden geometry.
|
||||
//
|
||||
// PRONG COLUMN DESIGN — prevents prongs falling into cavity:
|
||||
// A solid rectangular column rises from the inner bottom face at
|
||||
// each prong position. The prong slot cuts through the bottom wall
|
||||
// AND the column. The column ceiling is at Y=wall+ks_prong_h,
|
||||
// which acts as the hard stop for the prong — it cannot travel
|
||||
// beyond that height. The column is added AFTER the main interior
|
||||
// hollow is subtracted (nested-difference CSG), so the hollow
|
||||
// does not remove it.
|
||||
//
|
||||
module rear_cover() {
|
||||
n_vent = 5;
|
||||
vent_block_w = n_vent*(vent_w+vent_sp) - vent_sp;
|
||||
|
||||
difference() {
|
||||
|
||||
// ── SOLID GEOMETRY ────────────────────────────────────
|
||||
union() {
|
||||
|
||||
// 1. Main shell with interior already removed
|
||||
// (nested so subsequent additions are NOT removed by hollow)
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
// Interior hollow — from rear inner face to front opening
|
||||
translate([wall, wall, wall])
|
||||
cube([scr_w, scr_h, enc_d]);
|
||||
}
|
||||
|
||||
// 2. Corner towers — full cavity height, clearly visible from front
|
||||
for(x = tower_xs) for(y = tower_ys)
|
||||
translate([x - tower_w/2, y - tower_w/2, wall])
|
||||
cube([tower_w, tower_w, enc_d - wall]);
|
||||
|
||||
// 3. Prong guide columns — solid pillars on inner bottom face
|
||||
// One per prong, gives 14 mm of guided engagement
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px - col_x_size/2,
|
||||
wall,
|
||||
0])
|
||||
cube([col_x_size, col_y_size, col_z_size]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ALL CUTOUTS ───────────────────────────────────────
|
||||
|
||||
// Corner tower M3 pilot holes (from front/open face, going inward)
|
||||
for(x = tower_xs) for(y = tower_ys)
|
||||
translate([x, y, enc_d + 0.1])
|
||||
rotate([180, 0, 0])
|
||||
cylinder(d=m3_pilot, h=tower_hole_d);
|
||||
|
||||
// ── PORT CUTOUTS ──────────────────────────────────────
|
||||
pi_bot = pi_enc_cy - pi_h/2;
|
||||
pi_top = pi_enc_cy + pi_h/2;
|
||||
|
||||
// LEFT WALL — USB-C power + HDMI ×2
|
||||
translate([-0.1, pi_bot + 3, pi_z + 2]) cube([wall+0.2, 11, 11]);
|
||||
translate([-0.1, pi_bot + 16, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
translate([-0.1, pi_bot + 35, pi_z + 2]) cube([wall+0.2, 17, 9]);
|
||||
|
||||
// RIGHT WALL — RJ45 + USB-A ×4
|
||||
translate([enc_w-wall-0.1, pi_top - 24, pi_z + 1])
|
||||
cube([wall+0.2, 22, 16]);
|
||||
translate([enc_w-wall-0.1, pi_bot + 2, pi_z + 1])
|
||||
cube([wall+0.2, 50, 15]);
|
||||
|
||||
// ── COOLING VENTS ─────────────────────────────────────
|
||||
|
||||
// Bottom intake
|
||||
translate([enc_w/2 - vent_block_w/2, -0.1, wall + 6])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Top exhaust
|
||||
translate([enc_w/2 - vent_block_w/2, enc_h - wall + 0.1, wall + 6])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// SoC direct-vent on rear panel
|
||||
translate([pi_enc_cx - soc_vent_sz/2,
|
||||
pi_enc_cy - soc_vent_sz/2,
|
||||
enc_d - wall - 0.1]) {
|
||||
n_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i = [0:n_soc-1])
|
||||
translate([i*(vent_w+vent_sp), soc_vent_sz/2 - vent_l/2, 0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// ── CABLE GLANDS — rear panel face ────────────────────
|
||||
for(i = [0:gland_count-1]) {
|
||||
cx = enc_w/2 + (i - (gland_count-1)/2) * gland_spacing;
|
||||
translate([cx, wall + gland_dia/2 + 4, -0.1])
|
||||
cylinder(d=gland_dia, h=wall+0.2);
|
||||
}
|
||||
|
||||
// ── KICKSTAND PRONG SLOTS ─────────────────────────────
|
||||
// Each slot cuts through the outer bottom wall AND the guide
|
||||
// column above it. The column ceiling at Y=wall+ks_prong_h
|
||||
// is the hard stop — the prong cannot fall through.
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px, 0, 0])
|
||||
prong_slot_cut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SCENE — three parts exploded for visual clarity
|
||||
// ============================================================
|
||||
|
||||
// FRONT BEZEL — floated forward (toward viewer)
|
||||
color("DarkSlateGray", 0.92)
|
||||
translate([0, 0, enc_d + 14])
|
||||
front_bezel();
|
||||
|
||||
// REAR COVER — at origin
|
||||
color("SlateGray", 0.88)
|
||||
rear_cover();
|
||||
|
||||
// KICKSTAND — floated below the case to show it is a separate piece
|
||||
color("DimGray", 0.85)
|
||||
translate([0, -(ks_thick + ks_drop + 20), 0])
|
||||
kickstand();
|
||||
338
rpi-case/rpi5_industrial_case_006.scad
Normal file
338
rpi-case/rpi5_industrial_case_006.scad
Normal file
@@ -0,0 +1,338 @@
|
||||
// ============================================================
|
||||
// RPi5 Industrial Enclosure — 7" Capacitive Touchscreen
|
||||
// Version: 006
|
||||
//
|
||||
// ASSEMBLY (three printed parts):
|
||||
//
|
||||
// REAR COVER — box, open face toward screen.
|
||||
// • 4 corner towers, full cavity height, M3 pilot hole from front.
|
||||
// • 3 prong guide columns on inner bottom face — the columns give
|
||||
// the prong slots a solid ceiling so nothing falls through.
|
||||
// • ALL SIDE WALLS ARE SOLID. No port holes.
|
||||
//
|
||||
// FRONT BEZEL — display frame.
|
||||
// • 4× M3 countersunk holes, corners, aligned to tower holes.
|
||||
// • Screw route: bezel face → screen gap → tower pilot hole.
|
||||
// • Use M3 × 30 mm self-tapping screws.
|
||||
//
|
||||
// KICKSTAND — separate removable wedge plate.
|
||||
// • Main wedge behind case creates the tilt.
|
||||
// • Thin ledge extends UNDER the rear of the case 14 mm.
|
||||
// The ledge carries the 3 prongs which enter the case
|
||||
// through the BOTTOM face only — rear wall stays solid.
|
||||
// • Slide ledge+prongs under the case from behind; prongs
|
||||
// click up into their columns and lock the stand.
|
||||
//
|
||||
// Changes vs 005:
|
||||
// • Right-wall USB-A rectangle REMOVED (and all side-wall cuts).
|
||||
// Both left and right walls are now fully solid.
|
||||
// • Prong slots moved to Z = 5.5–10.5 mm (inside cavity).
|
||||
// Rear face (Z = 0) is completely uncut — no holes on back.
|
||||
// • Kickstand gains a 14 mm ledge so prongs reach the new slot Z.
|
||||
// • Cable glands removed (no rear-face holes).
|
||||
// ============================================================
|
||||
|
||||
// ── SCREEN PARAMETERS ───────────────────────────────────────
|
||||
scr_w = 164.9; // screen outer width (mm)
|
||||
scr_h = 124.27; // screen outer height (mm)
|
||||
scr_d = 12; // screen body depth (mm) ← confirm with calipers
|
||||
scr_active_w = 154; // active area width (mm) ← confirm from datasheet
|
||||
scr_active_h = 90; // active area height (mm) ← confirm from datasheet
|
||||
scr_mount_x = 75; // rear M2.5 mount pattern X (mm) ← verify
|
||||
scr_mount_y = 75; // rear M2.5 mount pattern Y (mm) ← verify
|
||||
|
||||
// ── RASPBERRY PI 5 PARAMETERS ────────────────────────────────
|
||||
pi_w = 85; // Pi board width (mm)
|
||||
pi_h = 56; // Pi board height (mm)
|
||||
pi_d = 17; // Pi board + tallest component (mm)
|
||||
pi_mnt_x = 58; // Pi mount pattern X (mm)
|
||||
pi_mnt_y = 49; // Pi mount pattern Y (mm)
|
||||
pi_standoff = 5; // standoff: screen rear → Pi PCB (mm)
|
||||
pi_offset_x = 0; // Pi centre X offset from screen centre (mm)
|
||||
pi_offset_y = 0; // Pi centre Y offset from screen centre (mm)
|
||||
|
||||
// ── ENCLOSURE PARAMETERS ─────────────────────────────────────
|
||||
wall = 4.0; // wall thickness (mm)
|
||||
chamfer = 1.5; // external edge chamfer (mm)
|
||||
recess = 1.0; // screen recess depth in front bezel (mm)
|
||||
|
||||
// ── VENT PARAMETERS ──────────────────────────────────────────
|
||||
vent_w = 3;
|
||||
vent_l = 18;
|
||||
vent_sp = 4;
|
||||
soc_vent_sz = 28;
|
||||
|
||||
// ── KICKSTAND PARAMETERS ─────────────────────────────────────
|
||||
ks_tilt = 75; // screen angle from horizontal when standing (deg)
|
||||
ks_depth = 60; // wedge reach behind rear face (mm)
|
||||
ks_thick = 5; // plate/ledge thickness (mm)
|
||||
|
||||
// Prong dimensions
|
||||
ks_prong_n = 3; // number of prongs
|
||||
ks_prong_w = 12; // prong width in X (mm)
|
||||
ks_prong_h = 14; // prong engagement height (mm)
|
||||
ks_prong_t = 5; // prong thickness in Z (mm)
|
||||
ks_prong_clr = 0.25; // clearance per side (mm)
|
||||
|
||||
// Ledge: extends under the case so prongs reach inside the cavity
|
||||
// without cutting through the rear wall.
|
||||
ks_ledge = 14; // ledge depth in +Z under case (mm)
|
||||
// must be > wall + ks_prong_t + margin
|
||||
|
||||
// Z start of prong slot measured from rear face.
|
||||
// Must be > wall so the slot never touches the rear face.
|
||||
ks_prong_z0 = wall + 1.5; // = 5.5 mm — slot from 5.25 to 10.75 mm
|
||||
|
||||
// ── ASSEMBLY PARAMETERS ──────────────────────────────────────
|
||||
m3_dia = 3.4; // M3 clearance hole (mm)
|
||||
m3_pilot = 2.5; // M3 self-tapping pilot (mm)
|
||||
m3_cs_dia = 6.5; // M3 countersink OD (mm)
|
||||
m3_cs_depth = 3.5; // countersink depth (mm)
|
||||
tower_w = 9; // corner tower footprint (mm square)
|
||||
tower_hole_d = 14; // pilot hole depth into tower from front face (mm)
|
||||
corner_inset = 7; // tower/hole centre from outer edge (mm)
|
||||
|
||||
// ── DERIVED ──────────────────────────────────────────────────
|
||||
rear_d = pi_standoff + pi_d + 10; // cavity depth = 32 mm
|
||||
enc_w = scr_w + 2*wall; // 172.9 mm
|
||||
enc_h = scr_h + 2*wall; // 132.27 mm
|
||||
enc_d = rear_d + wall; // 36 mm
|
||||
|
||||
pi_enc_cx = wall + scr_w/2 + pi_offset_x;
|
||||
pi_enc_cy = wall + scr_h/2 + pi_offset_y;
|
||||
pi_z = wall + pi_standoff;
|
||||
|
||||
ks_drop = ks_depth * tan(90 - ks_tilt); // wedge tip drop ≈ 16 mm
|
||||
|
||||
// Guide column dimensions (added back inside cavity after hollow)
|
||||
col_w = ks_prong_w + wall; // 16 mm — solid wall each side of slot
|
||||
col_h = ks_prong_h; // 14 mm — prong engagement
|
||||
col_d = ks_ledge; // 14 mm — same as ledge depth
|
||||
|
||||
$fn = 48;
|
||||
|
||||
// ============================================================
|
||||
// PRIMITIVES
|
||||
// ============================================================
|
||||
|
||||
module cbox(w, h, d, c=chamfer) {
|
||||
hull() {
|
||||
translate([c, c, 0]) cube([w-2*c, h-2*c, d ]);
|
||||
translate([0, c, c]) cube([w, h-2*c, d-2*c ]);
|
||||
translate([c, 0, c]) cube([w-2*c, h, d-2*c ]);
|
||||
}
|
||||
}
|
||||
|
||||
module slot(len, w, d) {
|
||||
r = w/2;
|
||||
hull() {
|
||||
translate([0, -len/2+r, 0]) cylinder(r=r, h=d);
|
||||
translate([0, len/2-r, 0]) cylinder(r=r, h=d);
|
||||
}
|
||||
}
|
||||
|
||||
module vent_row(n, len, w, spacing, depth) {
|
||||
for(i = [0:n-1])
|
||||
translate([i*(w+spacing), 0, 0])
|
||||
slot(len, w, depth);
|
||||
}
|
||||
|
||||
module m3_countersunk(total_d) {
|
||||
cylinder(d=m3_dia, h=total_d+0.1);
|
||||
cylinder(d1=m3_cs_dia, d2=m3_dia, h=m3_cs_depth+0.1);
|
||||
}
|
||||
|
||||
// Prong slot cutter — called with origin at (prong_cx, 0, ks_prong_z0).
|
||||
// Cuts bottom wall (Y: -0.1 → wall) + guide column (Y: wall → wall+col_h).
|
||||
// Z extent stays within [ks_prong_z0-clr, ks_prong_z0+ks_prong_t+clr]
|
||||
// which is entirely inside the cavity — rear face untouched.
|
||||
module prong_slot_cut() {
|
||||
translate([-ks_prong_w/2 - ks_prong_clr,
|
||||
-0.1,
|
||||
-ks_prong_clr])
|
||||
cube([ks_prong_w + 2*ks_prong_clr,
|
||||
wall + col_h + 0.2,
|
||||
ks_prong_t + 2*ks_prong_clr]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// KICKSTAND (separate removable piece — print separately)
|
||||
// ============================================================
|
||||
//
|
||||
// Side cross-section (Y-Z plane):
|
||||
//
|
||||
// Y=0 (case bottom) ─────┬─────────────────┐ ← ledge top (fits under case)
|
||||
// Y=-ks_thick ─────┴─────────────────┘ ← ledge bottom
|
||||
// Z=ks_ledge Z=0 │
|
||||
// │ ← wedge (behind case)
|
||||
// thick ╲ │ thin
|
||||
// desk contact → ────────╲─┘
|
||||
// Z=-ks_depth Z=0
|
||||
//
|
||||
// The ledge (Z=0→ks_ledge) slides under the case rear.
|
||||
// Prongs rise from Y=0 at Z=ks_prong_z0, entering the case bottom slots.
|
||||
// The wedge (Z=-ks_depth→0) rests on the desk and creates the tilt.
|
||||
//
|
||||
module kickstand() {
|
||||
ks_front_h = ks_thick + ks_drop; // thick end of wedge
|
||||
|
||||
// 1. Wedge behind case (Z = -ks_depth → 0)
|
||||
hull() {
|
||||
translate([0, -ks_thick, 0 ]) cube([enc_w, ks_thick, wall]);
|
||||
translate([0, -ks_front_h, -ks_depth]) cube([enc_w, ks_front_h, wall]);
|
||||
}
|
||||
|
||||
// 2. Ledge under rear of case (Z = 0 → ks_ledge)
|
||||
translate([0, -ks_thick, 0])
|
||||
cube([enc_w, ks_thick, ks_ledge]);
|
||||
|
||||
// 3. Three prongs rising from ledge top (Y=0) into case bottom slots
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px - ks_prong_w/2, 0, ks_prong_z0]) {
|
||||
// Main shaft
|
||||
cube([ks_prong_w, ks_prong_h - 2, ks_prong_t]);
|
||||
// Tapered tip (45° chamfer for easy insertion)
|
||||
translate([0, ks_prong_h - 2, 0])
|
||||
hull() {
|
||||
cube([ks_prong_w, 0.01, ks_prong_t ]);
|
||||
translate([1, 2, 0])
|
||||
cube([ks_prong_w-2, 0.01, ks_prong_t ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FRONT BEZEL
|
||||
// ============================================================
|
||||
module front_bezel() {
|
||||
ci = corner_inset;
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, wall + recess);
|
||||
|
||||
// Display window
|
||||
translate([(enc_w - scr_active_w)/2,
|
||||
(enc_h - scr_active_h)/2, -0.1])
|
||||
cube([scr_active_w, scr_active_h, wall+recess+0.2]);
|
||||
|
||||
// 1 mm recess pocket grips screen edge
|
||||
translate([(enc_w - scr_w)/2,
|
||||
(enc_h - scr_h)/2, wall])
|
||||
cube([scr_w, scr_h, recess+0.1]);
|
||||
|
||||
// 4× M3 countersunk screw holes at corners
|
||||
for(x = [ci, enc_w-ci])
|
||||
for(y = [ci, enc_h-ci])
|
||||
translate([x, y, 0])
|
||||
m3_countersunk(wall + recess);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REAR COVER
|
||||
// ============================================================
|
||||
module rear_cover() {
|
||||
ci = corner_inset;
|
||||
n_vent = 5;
|
||||
vbw = n_vent*(vent_w+vent_sp) - vent_sp; // vent block width
|
||||
|
||||
difference() {
|
||||
|
||||
// ── SOLID GEOMETRY (nested CSG) ───────────────────────
|
||||
union() {
|
||||
|
||||
// A) Shell with interior already removed.
|
||||
// Nested so the additions below are NOT eaten by the hollow.
|
||||
difference() {
|
||||
cbox(enc_w, enc_h, enc_d);
|
||||
translate([wall, wall, wall])
|
||||
cube([scr_w, scr_h, enc_d]);
|
||||
}
|
||||
|
||||
// B) Corner towers — full cavity height.
|
||||
// M3 pilot hole is drilled from the open front face,
|
||||
// clearly visible when looking into the case before assembly.
|
||||
for(x = [ci, enc_w-ci])
|
||||
for(y = [ci, enc_h-ci])
|
||||
translate([x - tower_w/2, y - tower_w/2, wall])
|
||||
cube([tower_w, tower_w, enc_d - wall]);
|
||||
|
||||
// C) Prong guide columns.
|
||||
// One solid column per prong, rising from the inner bottom
|
||||
// face (Y=wall) by col_h=14 mm, spanning Z=0→col_d=14 mm.
|
||||
// The prong slot cuts through this column, giving a closed
|
||||
// ceiling at Y=wall+col_h — prongs cannot fall through.
|
||||
// The column Z=0→5.25 mm is NOT cut by the slot, so the
|
||||
// rear face (Z=0) remains completely solid at these spots.
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px - col_w/2, wall, 0])
|
||||
cube([col_w, col_h, col_d]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CUTOUTS (applied to everything above) ─────────────
|
||||
|
||||
// Corner tower pilot holes from open front face
|
||||
for(x = [ci, enc_w-ci])
|
||||
for(y = [ci, enc_h-ci])
|
||||
translate([x, y, enc_d + 0.1])
|
||||
rotate([180, 0, 0])
|
||||
cylinder(d=m3_pilot, h=tower_hole_d);
|
||||
|
||||
// Cooling — bottom intake slots (through bottom wall, Y direction)
|
||||
translate([enc_w/2 - vbw/2, -0.1, wall + 6])
|
||||
rotate([-90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Cooling — top exhaust slots (through top wall, Y direction)
|
||||
translate([enc_w/2 - vbw/2, enc_h - wall + 0.1, wall + 6])
|
||||
rotate([90, 0, 0])
|
||||
vent_row(n_vent, vent_l, vent_w, vent_sp, wall+0.2);
|
||||
|
||||
// Cooling — SoC direct vent on rear panel
|
||||
translate([pi_enc_cx - soc_vent_sz/2,
|
||||
pi_enc_cy - soc_vent_sz/2,
|
||||
enc_d - wall - 0.1]) {
|
||||
n_soc = floor(soc_vent_sz / (vent_w + vent_sp));
|
||||
for(i = [0:n_soc-1])
|
||||
translate([i*(vent_w+vent_sp),
|
||||
soc_vent_sz/2 - vent_l/2, 0])
|
||||
slot(vent_l, vent_w, wall+0.2);
|
||||
}
|
||||
|
||||
// Kickstand prong slots — bottom face ONLY.
|
||||
// Slot Z: [ks_prong_z0-clr, ks_prong_z0+ks_prong_t+clr]
|
||||
// = [5.25, 10.75] mm — entirely past the rear wall (Z=0-4 mm).
|
||||
// Rear face at Z=0 is untouched.
|
||||
for(i = [0:ks_prong_n-1]) {
|
||||
px = enc_w * (i+1) / (ks_prong_n+1);
|
||||
translate([px, 0, ks_prong_z0])
|
||||
prong_slot_cut();
|
||||
}
|
||||
|
||||
// NOTE: All side-wall port cutouts removed — both left and right
|
||||
// walls are solid. Access to Pi ports via short extension cables
|
||||
// routed through user-drilled holes as needed for the installation.
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SCENE — three parts exploded for inspection
|
||||
// ============================================================
|
||||
|
||||
// Front bezel — floated forward
|
||||
color("DarkSlateGray", 0.92)
|
||||
translate([0, 0, enc_d + 14])
|
||||
front_bezel();
|
||||
|
||||
// Rear cover — at origin
|
||||
color("SlateGray", 0.88)
|
||||
rear_cover();
|
||||
|
||||
// Kickstand — floated below and behind to show it is separate
|
||||
// In real use: slide it up from below until prongs click into columns.
|
||||
color("DimGray", 0.85)
|
||||
translate([0, -(ks_thick + ks_drop + 20), 0])
|
||||
kickstand();
|
||||
Reference in New Issue
Block a user