From 3d250a0aaa2fbadc12d143fda817b7a55512c748 Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Tue, 12 May 2026 17:20:57 +0000 Subject: [PATCH] init --- .gitignore | 2 ++ Dockerfile | 10 ++++++++++ Makefile | 36 ++++++++++++++++++++++++++++++++++++ cube.scad | 256 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 file(s) changed, 304 insertion(s)(+), 0 deletion(s)(-) diff --git a/.gitignore b/.gitignore new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +out/ +out diff --git a/Dockerfile b/Dockerfile new file mode 100644 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + openscad \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /work +ENTRYPOINT ["openscad"] diff --git a/Makefile b/Makefile new file mode 100644 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +IMAGE := streamplace-cube-openscad +RUN := podman run --rm -v $(CURDIR):/work $(IMAGE) + +.PHONY: image stl preview clean help + +help: + @echo "Targets:" + @echo " make image — build the OpenSCAD podman image" + @echo " make stl — render out/cube_{bottom,top,front}.stl" + @echo " make preview — render out/preview.png (host openscad; exploded view)" + @echo " make clean — remove out/" + +image: + podman build -t $(IMAGE) . + +out: + mkdir -p out + +stl: out + $(RUN) -D 'part="bottom"' -o /work/out/cube_bottom.stl /work/cube.scad + $(RUN) -D 'part="top"' -o /work/out/cube_top.stl /work/cube.scad + $(RUN) -D 'part="front"' -o /work/out/cube_front.stl /work/cube.scad + +# Preview uses the host's openscad — the containerized version's headless PNG +# path hangs on this hardware. `--viewall --autocenter` makes openscad auto-fit +# the bounding box; the rotation triple chooses a nice 3/4 angle. +preview: out + openscad -D 'part="preview"' \ + --imgsize 1400,1100 \ + --autocenter --viewall \ + --camera 0,0,0,55,0,25,0 \ + --colorscheme Tomorrow \ + -o out/preview.png cube.scad + +clean: + rm -rf out diff --git a/cube.scad b/cube.scad new file mode 100644 --- /dev/null +++ b/cube.scad @@ -0,0 +1,256 @@ +// Streamplace Cube — three-piece parametric enclosure shell (v2 prototype) +// +// Three pieces, designed for three filament colors: +// 1. Bottom shell — floor + 3 walls (left, right, back) + front channel ribs; +// back wall carries the USB-C power port. +// 2. Top shell — ceiling + 4 walls; snaps onto the bottom shell on three sides +// and locks the front panel in place by capping its top edge. +// 3. Front panel — flat slab that slides down into the bottom shell's grooves +// and carries the user-facing ports (HDMI in/out, USB-A, USB-C data). +// +// Render via the included Dockerfile / Makefile: +// make stl -> out/cube_bottom.stl + out/cube_top.stl + out/cube_front.stl +// make preview -> out/preview.png (exploded view, uses host openscad) + +part = "preview"; // "bottom" | "top" | "front" | "preview" + +// ---- main dimensions ---- +W = 115; // width (X) +D = 115; // depth (Y) — front face is -Y, back face is +Y +H = 115; // height (Z) +wall = 2.0; +split_z = 70; // bottom shell height (top shell = H - split_z = 45) + +// ---- snap lip (3-sided: left, right, back; no front) ---- +lip_h = 6.0; +lip_thick = 1.6; +lip_clear = 0.3; +lip_inset = wall + lip_clear; +snap_d = 1.6; +snap_z = 3.5; +dimple_d = 2.2; + +// ---- seam ledge (3-sided) ---- +ledge_h = 1.5; + +// ---- front panel parameters ---- +panel_thick = 2.5; // panel Y thickness +panel_recess = 1.0; // panel front face recessed from cube front face by this +slide_clear = 0.3; // sliding-fit clearance (per side) + +// ---- front channel rib parameters ---- +rib_x_extra = 3.0; // rib extends inward from inner wall face by this in X +rib_y = panel_recess + panel_thick + slide_clear/2 + 1.5; // rib Y depth +slot_x_depth = 1.5; // slot X depth (cut into rib from interior face inward) + +// ---- ports ---- +port_clear = 0.5; + +// back wall: just power +back_ports = [ + ["usb_c_power", 9.0, 3.5, + (W - 9.0 - port_clear) / 2, // x left — centered on back wall + 18], // z above floor +]; + +// front panel: data ports (panel-local coords; x=0 is panel left edge, z=0 is bottom) +panel_ports = [ + ["hdmi_in", 14.0, 6.5, 20, 25], + ["hdmi_out", 14.0, 6.5, 39, 25], + ["usb_a", 13.0, 6.0, 58, 26], + ["usb_c", 9.0, 3.5, 76, 27.5], +]; + +// ---- preview / explode ---- +preview_gap_z = 14; // top shell raised this much above the seam +preview_gap_y = 18; // front panel pulled this far forward in -Y + +$fn = 56; + +// ============================================================ +// derived dimensions +// ============================================================ + +// Slot in the LEFT rib (absolute coords) +slot_x_min_L = wall + rib_x_extra - slot_x_depth; +slot_x_max_L = wall + rib_x_extra + 0.1; // breaks through inner face +slot_y_min = panel_recess - slide_clear/2; +slot_y_max = panel_recess + panel_thick + slide_clear/2; +slot_z_min = wall; +slot_z_max = split_z + 1; // extends above rib top + +// Panel X extent — left edge at -X end of left slot (with clearance), mirrored +panel_x_min = slot_x_min_L + slide_clear; +panel_x_max = W - slot_x_min_L - slide_clear; + +// Panel Y / Z extents +panel_y_min = panel_recess; +panel_y_max = panel_recess + panel_thick; +panel_z_min = wall; +panel_z_max = split_z; // top of panel meets the top shell exactly + +// Snap positions (3 sides, no front) +bump_positions = [ + [W/2, D - lip_inset, split_z + snap_z], // back (+Y) + [lip_inset, D/2, split_z + snap_z], // left (-X) + [W - lip_inset, D/2, split_z + snap_z], // right (+X) +]; +dimple_positions = [ + [W/2, D - wall, split_z + snap_z], + [wall, D/2, split_z + snap_z], + [W - wall, D/2, split_z + snap_z], +]; + +// ============================================================ +// helpers +// ============================================================ + +module hollow_box(z0, z1, t = wall) { + difference() { + translate([0, 0, z0]) cube([W, D, z1 - z0]); + translate([t, t, z0 - 0.01]) + cube([W - 2*t, D - 2*t, (z1 - z0) + 0.02]); + } +} + +// 3-sided seam ledge — full 4-sided ring with the front strip cut out +module seam_ledge_3sided() { + difference() { + difference() { + translate([0, 0, split_z - ledge_h]) + cube([W, D, ledge_h]); + translate([lip_inset + lip_thick, + lip_inset + lip_thick, + split_z - ledge_h - 0.01]) + cube([W - 2*(lip_inset + lip_thick), + D - 2*(lip_inset + lip_thick), + ledge_h + 0.02]); + } + translate([wall + 0.01, -0.5, split_z - ledge_h - 0.5]) + cube([W - 2*wall - 0.02, + lip_inset + lip_thick + 0.5, + ledge_h + 1]); + } +} + +// 3-sided lip ring (no front) +module lip_ring_3sided() { + difference() { + difference() { + translate([lip_inset, lip_inset, split_z]) + cube([W - 2*lip_inset, D - 2*lip_inset, lip_h]); + translate([lip_inset + lip_thick, + lip_inset + lip_thick, + split_z - 0.01]) + cube([W - 2*(lip_inset + lip_thick), + D - 2*(lip_inset + lip_thick), + lip_h + 0.02]); + } + translate([wall, -0.5, split_z - 0.01]) + cube([W - 2*wall, + lip_inset + lip_thick + 0.5, + lip_h + 0.02]); + } +} + +// Front-left channel rib: block at the inner front-left corner with a vertical +// slot cut through its inner face. Panel's left edge slides down into the slot. +module left_front_rib() { + difference() { + translate([wall, 0, wall]) + cube([rib_x_extra, rib_y, split_z - wall]); + translate([slot_x_min_L, slot_y_min, slot_z_min - 0.01]) + cube([slot_x_max_L - slot_x_min_L, + slot_y_max - slot_y_min, + slot_z_max - slot_z_min + 0.01]); + } +} + +module right_front_rib() { + translate([W, 0, 0]) mirror([1, 0, 0]) left_front_rib(); +} + +module back_port_cuts() { + for (p = back_ports) { + pw = p[1] + port_clear; + ph = p[2] + port_clear; + translate([p[3], D - wall - 0.5, p[4]]) + cube([pw, wall + 1, ph]); + } +} + +// ============================================================ +// shells +// ============================================================ + +module bottom_shell() { + difference() { + union() { + // 3 outer walls (left, right, back) + translate([0, 0, wall]) cube([wall, D, split_z - wall]); + translate([W - wall, 0, wall]) cube([wall, D, split_z - wall]); + translate([wall, D - wall, wall]) cube([W - 2*wall, wall, split_z - wall]); + + // floor + cube([W, D, wall]); + + // 3-sided seam ledge + lip + seam_ledge_3sided(); + lip_ring_3sided(); + + // front channel ribs (grooves for the front panel) + left_front_rib(); + right_front_rib(); + + // snap bumps on the lip (3 of them) + for (p = bump_positions) + translate(p) sphere(d = snap_d); + } + back_port_cuts(); + } +} + +module top_shell() { + difference() { + union() { + hollow_box(split_z, H); + translate([0, 0, H - wall]) cube([W, D, wall]); // ceiling + } + for (p = dimple_positions) + translate(p) sphere(d = dimple_d); + } +} + +module front_panel() { + difference() { + translate([panel_x_min, panel_y_min, panel_z_min]) + cube([panel_x_max - panel_x_min, + panel_y_max - panel_y_min, + panel_z_max - panel_z_min]); + for (p = panel_ports) { + pw = p[1] + port_clear; + ph = p[2] + port_clear; + translate([panel_x_min + p[3], + panel_y_min - 0.5, + panel_z_min + p[4]]) + cube([pw, panel_y_max - panel_y_min + 1, ph]); + } + } +} + +// ============================================================ +// dispatch +// ============================================================ + +if (part == "bottom") { + bottom_shell(); +} else if (part == "top") { + top_shell(); +} else if (part == "front") { + front_panel(); +} else { + // preview — explode along +Z (top) and -Y (front panel) + color("hotpink") bottom_shell(); + color("ghostwhite") translate([0, 0, preview_gap_z]) top_shell(); + color("dimgray") translate([0, -preview_gap_y, 0]) front_panel(); +} -- tangled.sh