pre-bemis

This commit is contained in:
Marcelo
2026-04-22 05:04:19 +00:00
parent ac1a7900c8
commit 80d27f83b6
91 changed files with 11769 additions and 820 deletions

View 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);
```

View File

@@ -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();
}

View 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();

View 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
}

View 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)"

View 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

View 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

View 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"

View 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"

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View 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();

View 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();

View 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();

View 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();

View 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();

View 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.510.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();