diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..7fe973b --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,13 @@ +{ + "servers": { + "openscad": { + "command": "uv", + "args": [ + "run", + "--with", + "git+https://github.com/quellant/openscad-mcp.git", + "openscad-mcp" + ] + } + } +} diff --git a/hardware/transmitter/enclosure/.gitignore b/hardware/transmitter/enclosure/.gitignore new file mode 100644 index 0000000..68cd221 --- /dev/null +++ b/hardware/transmitter/enclosure/.gitignore @@ -0,0 +1,2 @@ +renders/ +.deps/ diff --git a/hardware/transmitter/enclosure/Makefile b/hardware/transmitter/enclosure/Makefile new file mode 100644 index 0000000..c8f01d1 --- /dev/null +++ b/hardware/transmitter/enclosure/Makefile @@ -0,0 +1,236 @@ +# Copyright (C) 2026 Peter Buchegger +# +# This file is part of OpenDriveHub. +# +# OpenDriveHub is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# OpenDriveHub is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with OpenDriveHub. If not, see . +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# ============================================================================= +# OpenDriveHub Transmitter Enclosure – Build System +# ============================================================================= +# +# Auto-discovers .scad part files and generates PNG render / STL export targets. +# Adding a new .scad file automatically makes it available – no edits needed. +# +# Prerequisites: +# - OpenSCAD (command-line) +# - DISPLAY environment variable (Xvfb for headless systems) +# +# Quick start: +# make help Show all targets and options +# make renders Render every part in every view +# make -j$(nproc) renders Parallel rendering +# make stl Export all parts as STL +# ============================================================================= + +# Disable built-in rules for faster parsing +MAKEFLAGS += --no-builtin-rules +.SUFFIXES: + +# ─── Configuration (override via command line or environment) ───────────────── + +OPENSCAD ?= openscad +IMG_WIDTH ?= 1920 +IMG_HEIGHT ?= 1080 +COLORSCHEME ?= Cornfield +FN ?= 60 +RENDER_DIR ?= renders +EXPLODE ?= 30 +PNG_MODE ?= --render + +# ─── Views & Camera Angles ─────────────────────────────────────────────────── +# +# Each view is defined by a camera rotation (rot_x,rot_y,rot_z) and a +# projection type (p=perspective, o=orthographic). +# --autocenter and --viewall handle translation and distance automatically. +# +# To add a custom view, define CAM_ and PROJ_, then add +# to the VIEWS list. + +VIEWS ?= iso_front iso_back top bottom front back left right + +CAM_iso_front := 55,0,25 +CAM_iso_back := 55,0,205 +CAM_top := 0,0,0 +CAM_bottom := 180,0,0 +CAM_front := 90,0,0 +CAM_back := 90,0,180 +CAM_left := 90,0,90 +CAM_right := 90,0,270 + +PROJ_iso_front := p +PROJ_iso_back := p +PROJ_top := o +PROJ_bottom := o +PROJ_front := o +PROJ_back := o +PROJ_left := o +PROJ_right := o + +# ─── Auto-Discovery ────────────────────────────────────────────────────────── +# +# Library files are included/used by other files and produce no geometry on +# their own. Everything else is treated as a renderable part. +# Stems (filename without .scad) must be unique across subdirectories. + +SCAD_EXCLUDE := parameters.scad utils.scad + +SCAD_PARTS := $(shell find . -name '*.scad' \ + $(foreach x,$(SCAD_EXCLUDE),! -name '$(x)') \ + ! -path './$(RENDER_DIR)/*' \ + -printf '%P\n' | sort) + +# Common dependencies – a change here rebuilds everything +SCAD_DEPS := $(wildcard parameters.scad utils.scad) + +# Unique part stems (filename without extension) +STEMS := $(sort $(foreach p,$(SCAD_PARTS),$(basename $(notdir $(p))))) + +# Source map: SRCMAP_ → relative path to .scad file +$(foreach p,$(SCAD_PARTS),$(eval SRCMAP_$(basename $(notdir $(p))) := $(p))) + +# ─── Generated Target Lists ────────────────────────────────────────────────── + +ALL_PNGS := $(foreach s,$(STEMS),$(foreach v,$(VIEWS),$(RENDER_DIR)/$(s)/$(v).png)) +ALL_STLS := $(foreach s,$(STEMS),$(RENDER_DIR)/$(s)/$(s).stl) + +EXPLODED_VIEWS := iso_front iso_back +EXPLODED_PNGS := $(foreach v,$(EXPLODED_VIEWS),$(RENDER_DIR)/assembly/exploded_$(v).png) + +# ─── Phony Targets ─────────────────────────────────────────────────────────── + +.PHONY: all renders stl assembly-exploded \ + clean clean-all list-parts help \ + $(foreach s,$(STEMS),render-$(s) stl-$(s)) + +# ─── Main Targets ──────────────────────────────────────────────────────────── + +all: renders + +renders: $(ALL_PNGS) + @echo "" + @echo "✓ All $(words $(ALL_PNGS)) renders complete in $(RENDER_DIR)/." + +stl: $(ALL_STLS) + @echo "" + @echo "✓ All $(words $(ALL_STLS)) STL exports complete in $(RENDER_DIR)/." + +assembly-exploded: $(EXPLODED_PNGS) + @echo "" + @echo "✓ Exploded assembly views complete." + +# ─── Per-Part Convenience Targets ───────────────────────────────────────────── +# Usage: make render-assembly, make stl-joystick_zone, etc. + +$(foreach s,$(STEMS),$(eval render-$(s): $(foreach v,$(VIEWS),$(RENDER_DIR)/$(s)/$(v).png))) +$(foreach s,$(STEMS),$(eval stl-$(s): $(RENDER_DIR)/$(s)/$(s).stl)) + +# ─── Build Rules ────────────────────────────────────────────────────────────── +# +# Uses .SECONDEXPANSION so that the implicit rule can resolve the source .scad +# file from the target path at prerequisite-expansion time: +# +# Target: renders//.png +# $* = / +# word 1 = → SRCMAP lookup → source .scad path +# word 2 = → CAM / PROJ lookup + +.SECONDEXPANSION: + +# --- PNG render rule --- +$(RENDER_DIR)/%.png: $$(SRCMAP_$$(word 1,$$(subst /, ,$$*))) $$(SCAD_DEPS) + @mkdir -p $(dir $@) + @echo "[RENDER] $*" + @$(OPENSCAD) $(PNG_MODE) --autocenter --viewall \ + --imgsize=$(IMG_WIDTH),$(IMG_HEIGHT) \ + --colorscheme=$(COLORSCHEME) \ + --camera=0,0,0,$(CAM_$(word 2,$(subst /, ,$*))),0 \ + --projection=$(PROJ_$(word 2,$(subst /, ,$*))) \ + -D '$$fn=$(FN)' \ + -o $@ $< + +# --- STL export rule --- +$(RENDER_DIR)/%.stl: $$(SRCMAP_$$(word 1,$$(subst /, ,$$*))) $$(SCAD_DEPS) + @mkdir -p $(dir $@) + @echo "[STL] $(word 1,$(subst /, ,$*))" + @$(OPENSCAD) -D '$$fn=$(FN)' -o $@ $< + +# --- Exploded assembly (special case with explode parameter) --- +$(RENDER_DIR)/assembly/exploded_%.png: assembly.scad $$(SCAD_DEPS) + @mkdir -p $(dir $@) + @echo "[RENDER] assembly/exploded_$*" + @$(OPENSCAD) $(PNG_MODE) --autocenter --viewall \ + --imgsize=$(IMG_WIDTH),$(IMG_HEIGHT) \ + --colorscheme=$(COLORSCHEME) \ + --camera=0,0,0,$(CAM_$*),0 \ + --projection=$(PROJ_$*) \ + -D '$$fn=$(FN)' -D 'explode=$(EXPLODE)' \ + -o $@ $< + +# ─── Cleanup ───────────────────────────────────────────────────────────────── + +clean: + rm -rf $(foreach s,$(STEMS),$(RENDER_DIR)/$(s)) + +clean-all: + rm -rf $(RENDER_DIR) + +# ─── Information ────────────────────────────────────────────────────────────── + +list-parts: + @echo "Discovered $(words $(STEMS)) part(s):" + @$(foreach s,$(STEMS),echo " $(s) ← $(SRCMAP_$(s))";) + @echo "" + @echo "Library files (not rendered):" + @$(foreach x,$(SCAD_EXCLUDE),echo " $(x)";) + +help: + @echo "OpenDriveHub Transmitter Enclosure – Build System" + @echo "" + @echo "Usage: make [TARGET] [OPTION=value ...]" + @echo "" + @echo "Main targets:" + @echo " all / renders Render all parts in all views ($(words $(ALL_PNGS)) images)" + @echo " stl Export all parts as STL ($(words $(ALL_STLS)) files)" + @echo " assembly-exploded Exploded assembly renders" + @echo " clean Remove per-part render directories" + @echo " clean-all Remove entire $(RENDER_DIR)/ directory" + @echo " list-parts Show auto-discovered parts" + @echo " help Show this help" + @echo "" + @echo "Per-part targets:" + @$(foreach s,$(STEMS),echo " render-$(s) / stl-$(s)";) + @echo "" + @echo "Options (override on command line):" + @echo " IMG_WIDTH=1920 Render width in pixels" + @echo " IMG_HEIGHT=1080 Render height in pixels" + @echo " FN=60 Curve resolution (OpenSCAD \$$fn)" + @echo " COLORSCHEME=Cornfield Color scheme (Cornfield|Metallic|Sunset|...)" + @echo " EXPLODE=30 Explosion distance for exploded views" + @echo " PNG_MODE=--render Use --preview to show transparent components" + @echo " VIEWS='iso_front ...' Space-separated list of views to render" + @echo "" + @echo "Adding views:" + @echo " Define CAM_ (rot_x,rot_y,rot_z) and PROJ_ (p|o)," + @echo " then add to VIEWS." + @echo "" + @echo "Examples:" + @echo " make renders # All parts, all views" + @echo " make -j\$$(nproc) renders # Parallel rendering" + @echo " make render-assembly # Single part, all views" + @echo " make render-assembly VIEWS='iso_front top' # Single part, 2 views" + @echo " make assembly-exploded # Exploded assembly" + @echo " make stl-joystick_zone # Single STL export" + @echo " make renders IMG_WIDTH=3840 IMG_HEIGHT=2160 # 4K resolution" diff --git a/hardware/transmitter/enclosure/README.md b/hardware/transmitter/enclosure/README.md new file mode 100644 index 0000000..76fbe1a --- /dev/null +++ b/hardware/transmitter/enclosure/README.md @@ -0,0 +1,125 @@ +# Transmitter Enclosure + +3D-printable modular enclosure for the OpenDriveHub transmitter, designed in +[OpenSCAD](https://openscad.org/). + +--- + +## Zone Architecture + +The enclosure is built from three zones arranged **horizontally** (front to +back), connected via tongue-and-groove joints and M3 screws. As held by the +user (body → away): + +``` +[Joystick Zone] → [Module Zone] → [Display Zone] + front middle rear +``` + +| # | Zone | File | Width | Depth | +|---|------|------|-------|-------| +| 1 | Joystick (2× D400-R4 + grips) | `joystick/joystick_zone.scad` | 195 mm | 70 mm | +| 2 | Module grid (5×3 × 30 mm bays) | `modules/module_zone.scad` | 195 mm | 110 mm | +| 3 | Display (ILI9341 2.8″ landscape) | `display/display_zone.scad` | 195 mm | 65 mm | + +All zones share the same **width (~195 mm)** and **height (~42.5 mm)**. +Each zone fits on a **200×200 mm print bed** individually. + +Open `assembly.scad` in OpenSCAD to see all zones together. Set +`explode = 30;` in `parameters.scad` for an exploded view. + +--- + +## File Structure + +``` +enclosure/ +├── assembly.scad ← Full assembly view +├── parameters.scad ← All dimensions (edit this!) +├── utils.scad ← Helper modules + zone connectors +├── joystick/ +│ └── joystick_zone.scad ← Joystick zone with grips + battery +├── modules/ +│ └── module_zone.scad ← Module grid + electronics underneath +├── display/ +│ └── display_zone.scad ← Display bezel (LCD landscape) +└── renders/ ← Pre-rendered PNG images +``` + +--- + +## Key Parameters + +All dimensions are defined in `parameters.scad`. Key values: + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `wall_thickness` | 2.5 mm | Outer wall thickness | +| `zone_width` | ~195 mm | Uniform width for all zones | +| `zone_height` | ~42.5 mm | Zone shell height | +| `bay_unit_size` | 30 mm | Module bay size | +| `bay_grid_cols` × `bay_grid_rows` | 5 × 3 | Module grid layout | +| `joy_center_distance` | 125 mm | Joystick center-to-center | +| `tongue_width` | 15 mm | Tongue-and-groove joint width | +| `print_bed_x/y` | 200 mm | Max print bed dimensions | +| `explode` | 0 | Exploded view distance | + +--- + +## Bill of Materials (Hardware) + +| Qty | Item | Purpose | +|-----|------|---------| +| ~12 | M3×8 socket head cap screws | Zone-to-zone connection | +| ~12 | M3×5×5 brass heat-set inserts | Threaded mounting points | +| 15 | M3 screws (short) | Module retention (rear) | +| 4 | M3 screws (various lengths) | Display PCB mounting | +| 1 | Power switch (12×8 mm) | Battery on/off | +| 1 | XT30 connector pair | Battery connection | + +--- + +## Printing Guidelines + +- **Material:** PLA or PETG (PETG recommended for durability) +- **Layer height:** 0.2 mm +- **Infill:** 20–30% (gyroid or grid) +- **Walls:** 3 perimeters minimum +- **Supports:** Only needed for grip sections +- **Print bed:** Each zone fits on 200×200 mm individually +- **Orientation:** Print each zone upside-down (open top facing build plate) + +--- + +## Assembly + +1. Print all three zones +2. Install heat-set inserts into mounting points +3. Mount ESP32 and TCA9548A into the module zone (underneath grid) +4. Slide zones together via tongue-and-groove joints +5. Secure with M3 screws through the joint edges +6. Insert LCD into the display zone bezel +7. Insert battery into the joystick zone compartment +8. Mount joysticks from the top (panel-mount) + +--- + +## Customization + +Edit `parameters.scad` to adapt the design: + +- **Different joysticks:** Adjust `joy_*` parameters +- **Fewer/more module bays:** Change `bay_grid_cols` and `bay_grid_rows` +- **Larger display:** Adjust `display_*` parameters +- **Different battery:** Adjust `battery_*` parameters +- **Thicker walls:** Increase `wall_thickness` + +After changing parameters, open `assembly.scad` to verify the design and +check the console output for dimension/fit warnings. + +--- + +## License + +This hardware design is licensed under GPL-3.0-or-later, consistent with the +OpenDriveHub project. See [LICENSE](../../../LICENSE). diff --git a/hardware/transmitter/enclosure/assembly.scad b/hardware/transmitter/enclosure/assembly.scad new file mode 100644 index 0000000..4c2aef9 --- /dev/null +++ b/hardware/transmitter/enclosure/assembly.scad @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Full Assembly +// ============================================================================= +// Three zones arranged horizontally (front to back along Y axis): +// +// Front (body) Back +// [Joystick Zone] → [Module Zone] → [Display Zone] +// Y=0 Y=joy_d Y=joy_d+mod_d +// +// Set 'explode' in parameters.scad to spread zones apart for visibility. + +use +use +use + +include + +// Zone Y positions (front to back) +y_joystick = 0; +y_modules = joy_zone_depth + explode; +y_display = joy_zone_depth + module_zone_depth + 2 * explode; + +module full_assembly() { + // Zone 1: Joystick Zone (front) + color(color_joystick) + translate([0, y_joystick, 0]) + joystick_zone(); + + // Zone 2: Module Zone (middle) + color(color_modules) + translate([0, y_modules, 0]) + module_zone(); + + // Zone 3: Display Zone (rear) + color(color_display) + translate([0, y_display, 0]) + display_zone(); +} + +// Render +full_assembly(); + +// Dimension report +echo(str("=== OpenDriveHub Transmitter Enclosure ===")); +echo(str(" Zone width (uniform): ", zone_width, " mm")); +echo(str(" Joystick zone depth: ", joy_zone_depth, " mm")); +echo(str(" Module zone depth: ", module_zone_depth, " mm")); +echo(str(" Display zone depth: ", display_zone_depth, " mm")); +echo(str(" Total depth: ", total_depth, " mm")); +echo(str(" Zone height: ", zone_height, " mm")); +echo(str(" Module grid: ", bay_grid_cols, "x", bay_grid_rows, + " = ", bay_grid_width, "x", bay_grid_depth, " mm")); +echo(str(" Print bed: ", print_bed_x, "x", print_bed_y, " mm")); +echo(str(" Each zone fits bed: ", + zone_width <= print_bed_x && joy_zone_depth <= print_bed_y && + module_zone_depth <= print_bed_y && display_zone_depth <= print_bed_y + ? "YES" : "NO")); +echo(str(" Explode distance: ", explode, " mm")); diff --git a/hardware/transmitter/enclosure/display/display_zone.scad b/hardware/transmitter/enclosure/display/display_zone.scad new file mode 100644 index 0000000..22cab61 --- /dev/null +++ b/hardware/transmitter/enclosure/display/display_zone.scad @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Display Zone +// ============================================================================= +// Rear zone (farthest from the user's body). Contains the ILI9341 2.8" LCD +// mounted in landscape orientation with a bezel/viewing window. +// +// Front edge has grooves to receive module zone tongues. + +use <../utils.scad> +include <../parameters.scad> + +module display_zone() { + w = zone_width; + d = display_zone_depth; + h = zone_height; + + // LCD position (centered on X and Y within this zone) + lcd_x = (w - display_pcb_width) / 2; + lcd_y = (d - display_pcb_depth) / 2; + + // LCD sits near the top surface + lcd_z = h - display_module_thick - 2; + + bezel_overlap = 2.0; // Bezel overlaps the viewing area + + difference() { + union() { + // Zone shell + zone_shell(w, d, h); + + // LCD support ledge (inside, around the LCD) + ledge_h = 1.5; + translate([lcd_x - 1, lcd_y - 1, lcd_z - ledge_h]) + difference() { + cube([display_pcb_width + 2, display_pcb_depth + 2, + ledge_h]); + translate([2, 2, -0.1]) + cube([display_pcb_width - 2, display_pcb_depth - 2, + ledge_h + 0.2]); + } + + // Mounting posts at front edge (through-holes for module zone screws) + front_mounts = zone_mount_positions(w, d); + for (pos = front_mounts) { + translate([pos[0], pos[1], 0]) + mounting_post_through(h - 1); + } + } + + // Grooves on front edge (receive module zone tongues) + t_positions = tongue_positions(w); + for (tx = t_positions) { + groove(tx); + } + + // LCD PCB pocket (slightly larger than PCB for insertion) + translate([lcd_x - fit_tolerance, lcd_y - fit_tolerance, lcd_z]) + cube([display_pcb_width + 2 * fit_tolerance, + display_pcb_depth + 2 * fit_tolerance, + display_module_thick + 3]); + + // Display viewing window (through the top wall, reduced by bezel + // overlap so the LCD is held in place by the frame) + translate([lcd_x + display_view_offset_x + bezel_overlap, + lcd_y + display_view_offset_y + bezel_overlap, + -0.1]) + cube([display_view_width - 2 * bezel_overlap, + display_view_depth - 2 * bezel_overlap, + h + 0.2]); + + // LCD mounting screw holes + hole_positions = [ + [lcd_x + display_mount_inset, + lcd_y + display_mount_inset], + [lcd_x + display_pcb_width - display_mount_inset, + lcd_y + display_mount_inset], + [lcd_x + display_mount_inset, + lcd_y + display_pcb_depth - display_mount_inset], + [lcd_x + display_pcb_width - display_mount_inset, + lcd_y + display_pcb_depth - display_mount_inset], + ]; + for (pos = hole_positions) { + translate([pos[0], pos[1], lcd_z - 5]) + cylinder(d = display_mount_hole_dia, h = 10); + } + + // Cable slot at front wall + translate([w / 2, 0, h / 2]) + cable_slot(); + } +} + +// LCD placeholder for visualization +module _lcd_placeholder() { + // PCB + color([0.1, 0.3, 0.6, 0.8]) + cube([display_pcb_width, display_pcb_depth, display_pcb_thick]); + // Active display area + color([0.05, 0.05, 0.05]) + translate([display_view_offset_x, display_view_offset_y, + display_pcb_thick]) + cube([display_view_width, display_view_depth, 1.5]); +} + +// Preview +display_zone(); + +if (show_components) { + w = zone_width; + d = display_zone_depth; + lcd_x = (w - display_pcb_width) / 2; + lcd_y = (d - display_pcb_depth) / 2; + lcd_z = zone_height - display_module_thick - 2; + + translate([lcd_x, lcd_y, lcd_z]) + %_lcd_placeholder(); +} diff --git a/hardware/transmitter/enclosure/joystick/joystick_zone.scad b/hardware/transmitter/enclosure/joystick/joystick_zone.scad new file mode 100644 index 0000000..418ca28 --- /dev/null +++ b/hardware/transmitter/enclosure/joystick/joystick_zone.scad @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Joystick Zone +// ============================================================================= +// Front zone (closest to the user's body). Contains two D400-R4 joysticks +// with ergonomic grips on the left and right sides. +// Battery compartment is integrated underneath. +// +// Rear edge has tongues for connecting to the module zone. + +use <../utils.scad> +include <../parameters.scad> + +module joystick_zone() { + w = zone_width; + d = joy_zone_depth; + h = zone_height; + + // Joystick positions (centered on X, centered on Y within this zone) + joy_y = d / 2; + joy_left_x = w / 2 - joy_center_distance / 2; + joy_right_x = w / 2 + joy_center_distance / 2; + + difference() { + union() { + // Zone shell (open-top box) + zone_shell(w, d, h); + + // Tongues on rear edge (connect to module zone) + t_positions = tongue_positions(w); + for (tx = t_positions) { + translate([0, d, 0]) + tongue(tx); + } + + // Grips (hanging down from sides) + // Left grip + translate([0, d * 0.15, 0]) + _grip_shape(false); + // Right grip + translate([w, d * 0.15, 0]) + _grip_shape(true); + + // Internal reinforcement around joystick holes + for (jx = [joy_left_x, joy_right_x]) { + translate([jx, joy_y, wall_thickness]) + difference() { + cylinder(d = joy_panel_cutout + 8, h = 4); + translate([0, 0, -0.1]) + cylinder(d = joy_panel_cutout, h = 4.2); + } + } + + // Mounting posts at rear edge (heat-set inserts for module zone) + mount_positions = zone_mount_positions(w, d); + for (pos = mount_positions) { + translate([pos[0], pos[1], 0]) + mounting_post(h - 1); + } + } + + // Joystick panel cutouts (through the top wall) + for (jx = [joy_left_x, joy_right_x]) { + // Shaft hole through top + translate([jx, joy_y, -0.1]) + cylinder(d = joy_panel_cutout, h = h + 0.2); + + // Mounting screw holes + for (i = [0:3]) { + angle = i * 90 + 45; + translate([ + jx + cos(angle) * joy_mounting_hole_pcd / 2, + joy_y + sin(angle) * joy_mounting_hole_pcd / 2, + -0.1 + ]) + cylinder(d = joy_mounting_hole_dia, h = h + 0.2); + } + } + + // Battery compartment cavity (underneath, accessible from bottom/side) + bat_cavity_w = battery_width + 2 * battery_clearance; + bat_cavity_l = battery_length + 2 * battery_clearance; + bat_cavity_h = battery_height + battery_clearance; + translate([w / 2 - bat_cavity_w / 2, + d / 2 - bat_cavity_l / 2, + wall_thickness]) + cube([bat_cavity_w, bat_cavity_l, bat_cavity_h]); + + // Battery access opening (bottom) + translate([w / 2 - battery_width / 2, + d / 2 - battery_length / 2, + -0.1]) + cube([battery_width, battery_length, wall_thickness + 0.2]); + + // Cable slot at rear wall (to module zone) + translate([w / 2, d, h / 2]) + cable_slot(); + + // Side cable slots + for (x = [-0.1, w - wall_thickness]) { + translate([x, d / 2 - cable_slot_width / 2, h / 2]) + cube([wall_thickness + 0.2, cable_slot_width, cable_slot_height]); + } + } +} + +// Grip shape: extends downward from the zone side +module _grip_shape(is_right) { + mirror_x = is_right ? 1 : 0; + mirror([mirror_x, 0, 0]) + translate([0, 0, 0]) { + hull() { + // Top attachment (flush with zone side) + translate([-wall_thickness, 0, -5]) + cube([wall_thickness, grip_depth, 10]); + + // Bottom grip section + translate([-grip_width, grip_depth * 0.1, -grip_length + 10]) + rounded_cube_3d([grip_width, grip_depth * 0.8, 15], + grip_fillet); + } + } +} + +// Joystick placeholder for visualization +module _joy_placeholder() { + // Shaft above panel + color([0.2, 0.2, 0.2]) + cylinder(d = 8, h = joy_shaft_height); + color([0.1, 0.1, 0.1]) + translate([0, 0, joy_shaft_height - 15]) + sphere(d = 20); + // Body below panel (inside the zone) + color([0.5, 0.5, 0.5, 0.5]) + translate([0, 0, -joy_body_height]) + cylinder(d = joy_body_width * 0.8, h = joy_body_height); +} + +// Battery cover: slides or clips into the bottom opening of the joystick zone. +// Print this part separately. +module battery_cover() { + cover_tol = 0.3; // Fit tolerance on each side + lip = 1.5; // Lip that sits inside the cavity to retain the cover + cover_w = battery_width - 2 * cover_tol; + cover_l = battery_length - 2 * cover_tol; + cover_h = wall_thickness; + + difference() { + union() { + // Main plate (flush with bottom of joystick zone) + rounded_cube([cover_w, cover_l, cover_h], corner_radius); + + // Retention lip (sits inside the battery opening) + translate([lip, lip, cover_h]) + cube([cover_w - 2 * lip, cover_l - 2 * lip, lip]); + } + + // Finger-pull recess for easy removal + translate([cover_w / 2, cover_l / 2, -0.1]) + cylinder(d = 15, h = cover_h * 0.6); + } +} + +// Preview +joystick_zone(); + +if (show_components) { + w = zone_width; + d = joy_zone_depth; + joy_y = d / 2; + joy_left_x = w / 2 - joy_center_distance / 2; + joy_right_x = w / 2 + joy_center_distance / 2; + + translate([joy_left_x, joy_y, zone_height]) + %_joy_placeholder(); + translate([joy_right_x, joy_y, zone_height]) + %_joy_placeholder(); +} diff --git a/hardware/transmitter/enclosure/modules/module_zone.scad b/hardware/transmitter/enclosure/modules/module_zone.scad new file mode 100644 index 0000000..6b3155b --- /dev/null +++ b/hardware/transmitter/enclosure/modules/module_zone.scad @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Module Zone +// ============================================================================= +// Middle zone containing the 5×3 module grid (30×30mm bays). +// Modules are inserted from the top, secured with screws from behind. +// Electronics (ESP32 + TCA9548A mux) are mounted underneath the grid. +// +// Front edge has grooves to receive joystick zone tongues. +// Rear edge has tongues for connecting to the display zone. + +use <../utils.scad> +include <../parameters.scad> + +module module_zone() { + w = zone_width; + d = module_zone_depth; + h = zone_height; + + // Grid position (centered on X, centered on Y) + grid_w = bay_grid_width; + grid_d = bay_grid_depth; + grid_x = (w - grid_w) / 2; + grid_y = (d - grid_d) / 2; + + difference() { + union() { + // Zone shell + zone_shell(w, d, h); + + // Module grid structure (raised from floor) + translate([grid_x, grid_y, 0]) + _module_grid_structure(grid_w, grid_d); + + // Tongues on rear edge (connect to display zone) + t_positions = tongue_positions(w); + for (tx = t_positions) { + translate([0, d, 0]) + tongue(tx); + } + + // Mounting posts at front edge (through-holes for joystick zone screws) + front_mounts = zone_mount_positions(w, d); + for (pos = front_mounts) { + translate([pos[0], pos[1], 0]) + mounting_post_through(h - 1); + } + + // Mounting posts at rear edge (heat-set inserts for display zone) + rear_mounts = zone_mount_positions(w, d); + for (pos = rear_mounts) { + translate([pos[0], d - pos[1], 0]) + mounting_post(h - 1); + } + } + + // Grooves on front edge (receive joystick zone tongues) + t_positions = tongue_positions(w); + for (tx = t_positions) { + groove(tx); + } + + // Cable slots front and rear walls + translate([w / 2, 0, h / 2]) + cable_slot(); + translate([w / 2, d, h / 2]) + cable_slot(); + + // USB port opening (side wall, for ESP32 access) + usb_x = w - wall_thickness - 0.1; + translate([usb_x, d / 2 - esp32_usb_width / 2 - 2, + wall_thickness + 3]) + cube([wall_thickness + 0.2, esp32_usb_width + 4, + esp32_usb_height + 2]); + } + + // ESP32 mount (underneath grid, on the floor) + esp32_x = grid_x + grid_w / 2 - esp32_pcb_width / 2; + esp32_y = grid_y + 5; + translate([esp32_x, esp32_y, wall_thickness]) + _esp32_standoffs(); + + // TCA9548A mount (next to ESP32) + mux_x = grid_x + grid_w / 2 - mux_pcb_width / 2; + mux_y = grid_y + grid_d - mux_pcb_length - 5; + translate([mux_x, mux_y, wall_thickness]) + _mux_standoffs(); +} + +// Module grid: walls forming the 5×3 bay layout +module _module_grid_structure(grid_w, grid_d) { + bay_pitch = bay_unit_size + bay_wall; + grid_h = bay_depth + wall_thickness; + + // Grid sits at the top of the zone (bays open upward) + z_offset = zone_height - grid_h; + + translate([0, 0, z_offset]) { + difference() { + // Solid grid block + cube([grid_w, grid_d, grid_h]); + + // Bay openings (from top) + for (col = [0:bay_grid_cols - 1]) { + for (row = [0:bay_grid_rows - 1]) { + x = bay_wall + col * bay_pitch; + y = bay_wall + row * bay_pitch; + translate([x, y, wall_thickness]) + cube([bay_unit_size, bay_unit_size, bay_depth + 0.1]); + } + } + } + + // Screw bosses for module retention (rear face of each bay) + for (col = [0:bay_grid_cols - 1]) { + for (row = [0:bay_grid_rows - 1]) { + x = bay_wall + col * bay_pitch + bay_unit_size / 2; + y = bay_wall + (row + 1) * bay_pitch - bay_wall / 2; + translate([x, y, wall_thickness + bay_depth / 2]) + rotate([-90, 0, 0]) + difference() { + cylinder(d = m3_screw_dia + 4, h = bay_wall); + translate([0, 0, -0.1]) + cylinder(d = m3_screw_dia, + h = bay_wall + 0.2); + } + } + } + } +} + +// Simple standoffs for ESP32 +module _esp32_standoffs() { + standoff_h = 3; + for (x = [2, esp32_pcb_width - 2]) + for (y = [2, esp32_pcb_length - 2]) + translate([x, y, 0]) + cylinder(d = 4, h = standoff_h); +} + +// Simple standoffs for TCA9548A mux +module _mux_standoffs() { + standoff_h = 3; + for (x = [2, mux_pcb_width - 2]) + for (y = [2, mux_pcb_length - 2]) + translate([x, y, 0]) + cylinder(d = 4, h = standoff_h); +} + +// Preview +module_zone(); diff --git a/hardware/transmitter/enclosure/parameters.scad b/hardware/transmitter/enclosure/parameters.scad new file mode 100644 index 0000000..115770f --- /dev/null +++ b/hardware/transmitter/enclosure/parameters.scad @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Central Parameters +// ============================================================================= +// All dimensions in millimeters. Change values here to adjust the entire design. +// +// LAYOUT: Three zones arranged horizontally (front to back, Y axis): +// [Joystick Zone] → [Module Zone] → [Display Zone] +// All zones share the same width (X). Each zone fits on the print bed. + +// --- Print Bed Constraints --------------------------------------------------- +print_bed_x = 200; +print_bed_y = 200; + +// --- General Construction ---------------------------------------------------- +wall_thickness = 2.5; // Outer wall thickness +inner_wall = 1.6; // Internal divider walls +corner_radius = 3.0; // Outer corner rounding +fit_tolerance = 0.3; // General 3D-print fit tolerance + +// --- Screw & Fastener Dimensions --------------------------------------------- +// M3 Heat-Set Insert (standard M3×5×5 brass insert) +m3_screw_dia = 3.2; // M3 clearance hole +m3_screw_head_dia = 5.8; // M3 socket head cap screw head +m3_screw_head_h = 3.0; // M3 head height +m3_insert_dia = 4.6; // Heat-set insert outer diameter +m3_insert_depth = 5.0; // Heat-set insert depth +m3_insert_hole_dia = 4.8; // Hole diameter for heat-set insert (slightly larger) + +// --- Zone Connection (tongue-and-groove between zones) ----------------------- +tongue_width = 15.0; // Width of each tongue +tongue_height = 8.0; // Height of tongue (Z) +tongue_depth = 5.0; // How far the tongue protrudes (Y) +tongue_tolerance = 0.3; // Gap for fit +tongue_count = 3; // Number of tongues per joint edge + +// --- Joystick D400-R4 -------------------------------------------------------- +// JH-D400X-R4 3-axis joystick (panel-mount) +joy_body_width = 49.6; // Joystick body width (X) +joy_body_length = 94.5; // Joystick body length (Y) +joy_body_height = 35.0; // Body height below panel +joy_shaft_height = 40.0; // Shaft height above panel +joy_panel_cutout = 28.0; // Circular panel cutout diameter +joy_mounting_dia = 36.0; // Mounting ring outer diameter +joy_mounting_hole_dia = 3.2; // Mounting screw holes +joy_mounting_hole_pcd = 40.0; // Mounting hole pitch circle diameter + +// Ergonomic spacing +joy_center_distance = 125; // Center-to-center distance between joysticks + +// --- Module Bay -------------------------------------------------------------- +bay_unit_size = 30.0; // Single bay unit (30×30mm) +bay_depth = 25.0; // Bay depth (how deep the module sits) +bay_wall = 1.2; // Wall between adjacent bays +bay_screw_inset = 4.0; // Screw hole distance from bay edge (rear mounting) + +// Grid configuration +bay_grid_cols = 5; // Number of columns (along X) +bay_grid_rows = 3; // Number of rows (along Y) + +// Computed grid dimensions +bay_grid_width = bay_grid_cols * bay_unit_size + (bay_grid_cols + 1) * bay_wall; +bay_grid_depth = bay_grid_rows * bay_unit_size + (bay_grid_rows + 1) * bay_wall; + +// --- Display (ILI9341 2.8" TFT LCD, mounted LANDSCAPE) ---------------------- +// In landscape orientation: long edge = X (width), short edge = Y (depth) +display_pcb_width = 86.0; // PCB along X (landscape) +display_pcb_depth = 50.0; // PCB along Y (landscape) +display_pcb_thick = 1.6; // PCB thickness +display_module_thick = 5.0; // Total module thickness (PCB + components) + +display_view_width = 57.6; // Visible area along X (landscape) +display_view_depth = 43.2; // Visible area along Y (landscape) +display_view_offset_x = 5.0; // View area offset from PCB left edge +display_view_offset_y = 3.4; // View area offset from PCB front edge + +display_mount_hole_dia = 3.0; // Mounting hole diameter +display_mount_inset = 2.5; // Mounting hole distance from PCB edge + +// --- ESP32 DevKitC (38-pin) -------------------------------------------------- +esp32_pcb_width = 28.0; +esp32_pcb_length = 55.0; +esp32_pcb_thick = 1.6; +esp32_total_height = 10.0; // Including components underneath +esp32_usb_width = 8.0; // Micro-USB port width +esp32_usb_height = 3.5; // Micro-USB port height + +// --- TCA9548A I²C Mux Breakout ----------------------------------------------- +mux_pcb_width = 18.0; +mux_pcb_length = 25.0; +mux_pcb_thick = 1.6; + +// --- Battery (2S LiPo) ------------------------------------------------------ +battery_width = 35.0; // Typical 2S 1000-1500mAh LiPo +battery_length = 68.0; +battery_height = 18.0; +battery_clearance = 2.0; // Extra space around battery + +// --- Ergonomic Grip ---------------------------------------------------------- +grip_length = 80.0; // Grip length (Z, downward from joystick zone) +grip_width = 35.0; // Grip cross-section width +grip_depth = 30.0; // Grip cross-section depth +grip_fillet = 8.0; // Grip edge rounding + +// --- Zone Dimensions --------------------------------------------------------- +// Uniform width for all zones (X axis) – driven by the widest content +zone_width = max( + joy_center_distance + joy_body_width + 20, // Joystick spacing + margin + bay_grid_width + 20, // Module grid + margin + display_pcb_width + 20 // Display PCB + margin +); + +// Zone heights (Z axis) – the enclosure shell height +zone_height = joy_body_height + wall_thickness + 5; // ~42.5mm + +// Zone depths (Y axis, front-to-back for each zone) +joy_zone_depth = joy_body_width + 20; // ~70mm +module_zone_depth = bay_grid_depth + 15; // ~110mm +display_zone_depth = display_pcb_depth + 15; // ~65mm + +// Total transmitter depth +total_depth = joy_zone_depth + module_zone_depth + display_zone_depth; + +// --- Mounting Posts ----------------------------------------------------------- +mount_post_dia = 8.0; // Outer diameter of mounting post +mount_post_inset_x = 8.0; // Inset from zone edge (X) +mount_post_inset_y = 8.0; // Inset from zone edge (Y) + +// --- Alignment Pins ---------------------------------------------------------- +alignment_pin_dia = 3.0; // Diameter of alignment pins +alignment_pin_height = 4.0; // Height of alignment pins +alignment_hole_dia = 3.4; // Diameter of alignment holes (with tolerance) +alignment_hole_depth = 4.5; // Depth of alignment holes + +// --- Cable Routing ----------------------------------------------------------- +cable_slot_width = 10.0; // Width of cable routing slots between zones +cable_slot_height = 4.0; // Height of cable routing slots + +// --- Colors (for assembly visualization) ------------------------------------- +color_joystick = [0.2, 0.5, 0.8, 0.9]; +color_modules = [0.8, 0.5, 0.2, 0.9]; +color_display = [0.2, 0.8, 0.3, 0.9]; +color_electronics = [0.6, 0.2, 0.6, 0.9]; +color_battery = [0.8, 0.2, 0.2, 0.9]; +color_grip = [0.4, 0.4, 0.4, 0.9]; + +// --- Debug / Visualization --------------------------------------------------- +$fn = 60; // Default facet count for curves +explode = 0; // Explosion distance (0 = assembled, >0 = exploded view) +show_components = true; // Show placeholder components (joysticks, display, etc.) diff --git a/hardware/transmitter/enclosure/utils.scad b/hardware/transmitter/enclosure/utils.scad new file mode 100644 index 0000000..83d37a9 --- /dev/null +++ b/hardware/transmitter/enclosure/utils.scad @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2026 Peter Buchegger + * + * This file is part of OpenDriveHub. + * + * OpenDriveHub is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenDriveHub is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenDriveHub. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +// ============================================================================= +// OpenDriveHub Transmitter Enclosure – Utility Modules +// ============================================================================= + +include + +// --- Rounded Cube ------------------------------------------------------------ +// A cube with rounded edges on the XY plane. +// size: [x, y, z], radius: corner radius +module rounded_cube(size, radius) { + r = min(radius, min(size[0], size[1]) / 2); + hull() { + for (x = [r, size[0] - r]) + for (y = [r, size[1] - r]) + translate([x, y, 0]) + cylinder(h = size[2], r = r); + } +} + +// --- Fully Rounded Cube (all edges) ------------------------------------------ +module rounded_cube_3d(size, radius) { + r = min(radius, min(size[0], min(size[1], size[2])) / 2); + hull() { + for (x = [r, size[0] - r]) + for (y = [r, size[1] - r]) + for (z = [r, size[2] - r]) + translate([x, y, z]) + sphere(r = r); + } +} + +// --- Screw Clearance Hole ---------------------------------------------------- +// Vertical through-hole for a screw (clearance fit). +// dia: screw diameter, depth: hole depth, head_dia/head_h: counterbore +module screw_hole(dia, depth, head_dia = 0, head_h = 0) { + cylinder(d = dia, h = depth); + if (head_dia > 0 && head_h > 0) { + translate([0, 0, depth - head_h]) + cylinder(d = head_dia, h = head_h + 0.1); + } +} + +// --- M3 Clearance Hole (convenience) ----------------------------------------- +module m3_clearance_hole(depth) { + screw_hole( + dia = m3_screw_dia, + depth = depth, + head_dia = m3_screw_head_dia, + head_h = m3_screw_head_h + ); +} + +// --- Heat-Set Insert Hole ---------------------------------------------------- +// Hole designed for pressing in a brass heat-set insert from the top. +module heat_insert_hole(dia = m3_insert_hole_dia, depth = m3_insert_depth) { + // Slightly tapered entry for easier insertion + cylinder(d1 = dia + 0.4, d2 = dia, h = 0.8); + cylinder(d = dia, h = depth); +} + +// --- Mounting Post ----------------------------------------------------------- +// Cylindrical post with a heat-set insert hole at the top. +module mounting_post(height, outer_dia = mount_post_dia, + insert_dia = m3_insert_hole_dia, + insert_depth = m3_insert_depth) { + difference() { + cylinder(d = outer_dia, h = height); + translate([0, 0, height - insert_depth]) + heat_insert_hole(dia = insert_dia, depth = insert_depth + 0.1); + } +} + +// --- Mounting Post with Through-Hole ----------------------------------------- +// Post with a clearance hole going all the way through (for bolting layers). +module mounting_post_through(height, outer_dia = mount_post_dia, + hole_dia = m3_screw_dia) { + difference() { + cylinder(d = outer_dia, h = height); + translate([0, 0, -0.1]) + cylinder(d = hole_dia, h = height + 0.2); + } +} + +// --- Alignment Pin ----------------------------------------------------------- +module alignment_pin(dia = alignment_pin_dia, height = alignment_pin_height) { + // Chamfered tip for easier insertion + cylinder(d = dia, h = height - 1); + translate([0, 0, height - 1]) + cylinder(d1 = dia, d2 = dia - 1, h = 1); +} + +// --- Alignment Hole ---------------------------------------------------------- +module alignment_hole(dia = alignment_hole_dia, depth = alignment_hole_depth) { + // Chamfered entry + cylinder(d1 = dia + 0.5, d2 = dia, h = 0.5); + cylinder(d = dia, h = depth); +} + +// --- Cable Slot -------------------------------------------------------------- +// Rectangular slot for routing cables between layers. +module cable_slot(width = cable_slot_width, height = cable_slot_height, + wall = wall_thickness) { + translate([-width / 2, -wall / 2 - 0.1, 0]) + cube([width, wall + 0.2, height]); +} + +// --- Rubber Foot Recess ------------------------------------------------------ +module rubber_foot_recess(dia = 10, depth = 1.5) { + cylinder(d = dia, h = depth); +} + +// --- Plate with Mounting Holes ----------------------------------------------- +// Generates mounting hole positions for a rectangular plate. +// Returns a list of [x, y] positions for use with for() loops. +// Usage: for (pos = plate_mount_positions(w, d)) translate(pos) ... +function plate_mount_positions(width, depth, + inset_x = mount_post_inset_x, + inset_y = mount_post_inset_y) = + [ + [inset_x, inset_y], // Front-left + [width - inset_x, inset_y], // Front-right + [inset_x, depth - inset_y], // Rear-left + [width - inset_x, depth - inset_y], // Rear-right + [width / 2, inset_y], // Front-center + [width / 2, depth - inset_y], // Rear-center + ]; + +// --- Text Label (debossed) --------------------------------------------------- +module text_label(txt, size = 5, depth = 0.4) { + linear_extrude(height = depth) + text(txt, size = size, halign = "center", valign = "center", + font = "Liberation Sans:style=Bold"); +} + +// --- Fillet (2D profile for hull operations) --------------------------------- +module fillet_2d(r) { + offset(r = r) offset(delta = -r) children(); +} + +// --- Mirror Copy (creates original + mirrored copy) -------------------------- +module mirror_copy(axis = [1, 0, 0]) { + children(); + mirror(axis) children(); +} + +// --- Tongue (male half of tongue-and-groove joint) --------------------------- +// Placed on the rear edge of a zone (protrudes in +Y). +// x_pos: X position of tongue center on the zone edge. +module tongue(x_pos, z_pos = wall_thickness, w = tongue_width, + h = tongue_height, d = tongue_depth) { + translate([x_pos - w / 2, 0, z_pos]) + cube([w, d, h]); +} + +// --- Groove (female half of tongue-and-groove joint) ------------------------- +// Cut into the front edge of the next zone (receives tongue from -Y). +// x_pos: X position of groove center on the zone edge. +module groove(x_pos, z_pos = wall_thickness, w = tongue_width, + h = tongue_height, d = tongue_depth, + tol = tongue_tolerance) { + translate([x_pos - w / 2 - tol, -0.1, z_pos - tol]) + cube([w + 2 * tol, d + tol + 0.1, h + 2 * tol]); +} + +// --- Tongue positions for a given zone width --------------------------------- +// Evenly distributes tongue_count tongues across the width. +function tongue_positions(width, count = tongue_count) = + [for (i = [0:count - 1]) width / (count + 1) * (i + 1)]; + +// --- Zone Shell (open-top box) ----------------------------------------------- +// Creates a box shell for a zone: walls + floor, open on top. +module zone_shell(width, depth, height, radius = corner_radius) { + difference() { + rounded_cube([width, depth, height], radius); + translate([wall_thickness, wall_thickness, wall_thickness]) + rounded_cube([width - 2 * wall_thickness, + depth - 2 * wall_thickness, + height + 0.1], + max(0.1, radius - wall_thickness)); + } +} + +// --- Zone Screw Bosses ------------------------------------------------------- +// Mounting bosses at corners and mid-points for screwing zones together or +// attaching top panels. +function zone_mount_positions(width, depth, + inset_x = mount_post_inset_x, + inset_y = mount_post_inset_y) = + [ + [inset_x, inset_y], + [width - inset_x, inset_y], + [inset_x, depth - inset_y], + [width - inset_x, depth - inset_y], + ];