// 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; // Lid (top shell) height — half of v2's 45 mm. Bottom shell grows to fill the // rest of H. The front panel sits between two equal frames of bottom-shell // color: `lid_h` of frame above (= top shell) and `lid_h` of frame below // (= bottom-shell front-wall section under the panel). lid_h = 22.5; split_z = H - lid_h; // bottom shell height (= 92.5) // Corner-rounding radius for the cube's four vertical edges. Outer envelope // and interior cavity both get the same radius so wall thickness stays uniform. corner_r = 4.0; // ---- 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_frame_h = lid_h; // bottom-shell front-wall height under the panel // ---- 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). // Vertically centered: each port's z is `panel_h/2 - port_h/2`, with // panel_h derived below. function port_z(h) = (split_z - front_frame_h)/2 - h/2; panel_ports = [ ["hdmi_in", 14.0, 6.5, 20, port_z(6.5)], ["hdmi_out", 14.0, 6.5, 39, port_z(6.5)], ["usb_a", 13.0, 6.0, 58, port_z(6.0)], ["usb_c", 9.0, 3.5, 76, port_z(3.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 = front_frame_h; // slot bottom = panel bottom 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 = front_frame_h; // panel sits on top of the bottom-shell front 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]); } } // 2D rounded rectangle, W × D footprint, vertical edges rounded with `corner_r` module rounded_outer_2d() { offset(r = corner_r) offset(r = -corner_r) square([W, D]); } // 2D footprint of the interior cavity — same rounding so wall thickness stays // `wall` everywhere (including the diagonal at each corner). module rounded_inner_2d() { offset(r = corner_r) offset(r = -corner_r) translate([wall, wall]) square([W - 2*wall, D - 2*wall]); } // 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. // Rib extends the full height of the bottom-shell side walls; the slot only // exists above the front-wall section (z >= front_frame_h). 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]) 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() { union() { // Solid outer envelope (z=[0, split_z]) with rounded vertical edges, // minus the interior cavity, the front opening above the frame wall, // and the back-wall power-port cutout. difference() { linear_extrude(split_z) rounded_outer_2d(); // Hollow interior (above floor) translate([0, 0, wall]) linear_extrude(split_z - wall + 0.01) rounded_inner_2d(); // Front opening — only above the frame wall (z >= front_frame_h) translate([wall, -0.5, front_frame_h]) cube([W - 2*wall, wall + 1, split_z - front_frame_h + 0.01]); back_port_cuts(); } // Interior features (seam ledge / lip / ribs) — clipped to the rounded // envelope so they don't square off the corners where they would otherwise // protrude past the outer face. intersection() { union() { seam_ledge_3sided(); lip_ring_3sided(); left_front_rib(); right_front_rib(); } linear_extrude(split_z + lip_h + 1) rounded_outer_2d(); } // Snap bumps (positioned safely inside the envelope, no clip needed) for (p = bump_positions) translate(p) sphere(d = snap_d); } } module top_shell() { difference() { // Rounded outer envelope from z=split_z to z=H translate([0, 0, split_z]) linear_extrude(H - split_z) rounded_outer_2d(); // Hollow interior, stopping `wall` below H to leave a ceiling translate([0, 0, split_z]) linear_extrude(H - split_z - wall + 0.01) rounded_inner_2d(); // Snap dimples (recessed into the inner wall surface) 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(); }