Gap in the left frame member aligned with the body's FPC clearance so the flex folds back through the bracket plane into the cavity instead of being pinched. Refresh renders and the bracket alt-text.
356 lines
16 KiB
OpenSCAD
356 lines
16 KiB
OpenSCAD
// ============================================================================
|
||
// Typoena — 3D-printed enclosure · "typewriter body" · rev v0 (concept)
|
||
// ----------------------------------------------------------------------------
|
||
// A shallow sage wedge. The e-paper strip sits on a reclined deck where a
|
||
// typewriter's sheet of paper would be; the keyboard you bring rests in front.
|
||
// No platen part (keeps the print simple) — the rounded back-top edge is a
|
||
// subtle roll that nods to one for free.
|
||
//
|
||
// Everything here is PARAMETRIC. Numbers that come from a datasheet are noted;
|
||
// numbers marked << MEASURE >> are best-guesses you must confirm against the
|
||
// real board / breakout before printing a final.
|
||
//
|
||
// Units: millimetres. Render: see hardware/case/README.md
|
||
//
|
||
// Parts (set `show` below):
|
||
// "assembled" – everything in place, coloured (screen ghosted in)
|
||
// "body" – the shell only (print deck-up or on its back)
|
||
// "bracket" – the screen retaining frame (print flat)
|
||
// "baseplate" – the chassis / bottom cover (print flat)
|
||
// "print_plate" – all printed parts laid out side by side
|
||
// "section" – vertical cross-section: how the screen is trapped
|
||
// "plan" – exploded horizontal section: deck lifted off the cavity
|
||
// "plan_up" – just the top half (deck / screen / bracket)
|
||
// "plan_down" – just the bottom half (cavity: standoffs, posts, ports)
|
||
// ============================================================================
|
||
|
||
show = "assembled";
|
||
$fn = 48;
|
||
|
||
// ---- body envelope --------------------------------------------------------
|
||
W = 176; // width (X) — screen 150.9 + bezel + walls
|
||
D = 104; // depth (Y) — front (keyboard) .. back (ports)
|
||
Hf = 24; // height at the FRONT edge
|
||
Hb = 58; // height at the BACK edge (Hf<Hb makes the reclined deck)
|
||
wall = 2.4; // side/back wall thickness
|
||
top_wall = 2.6; // deck thickness (before the bezel lip is cut into it)
|
||
corner_r = 8; // rounded vertical + top-edge radius (the "machined" look)
|
||
|
||
// deck slope, derived from the pillar centres (this is the *true* top plane)
|
||
theta = atan((Hb - Hf) / (D - 2*corner_r)); // ~21 deg with the defaults
|
||
// >> THE ergonomics dial. Raise Hb for a more vertical, easier-to-read screen;
|
||
// lower it for a flatter, more typewriter-like deck. 18-22 deg = shallow,
|
||
// 28-35 deg reads better when you're sitting close.
|
||
|
||
// ---- e-paper panel : GDEY0579T93 (datasheet) ------------------------------
|
||
G_w = 150.92; G_h = 56.94; G_t = 1.0; // glass outline W x H x thickness
|
||
A_w = 139.00; A_h = 47.74; // active area (must stay uncovered)
|
||
// NOTE: the real panel's active area is offset toward the FPC edge — this model
|
||
// centres it. << MEASURE >> your panel's border and shift screen_off if needed.
|
||
screen_off = 0; // (legacy) kept 0; see active_off_*
|
||
// This panel's flex (FPC) leaves the LEFT short edge — the user's left as they
|
||
// face the screen, i.e. the low-X side (world x < W/2). The aperture is centred
|
||
// on the ACTIVE area, which sits off-centre on the glass — measure yours and
|
||
// nudge these (+x = toward the right, away from the FPC edge). << MEASURE >>
|
||
active_off_x = 0;
|
||
active_off_y = 0;
|
||
|
||
// ---- screen retention (glueless) ------------------------------------------
|
||
lip_over = 4.0; // how far the front bezel lip overlaps the glass border
|
||
lip_t = 1.4; // deck material left in FRONT of the glass (the visible lip)
|
||
glass_gap = 0.5; // clearance around the glass in its pocket
|
||
foam_t = 1.0; // non-adhesive closed-cell foam gasket behind the glass
|
||
bracket_t = 2.6; // printed retaining frame thickness
|
||
fpc_w = 26; // ribbon-slot span along the LEFT short edge (the FPC side)
|
||
|
||
// ---- deck nameplate (engraved, faces the user) ----------------------------
|
||
name_text = "TYPOENA";
|
||
name_size = 6.5; // cap height in mm
|
||
name_depth = 0.8; // engrave depth — raise for a bolder, deeper cut
|
||
name_font = "Monaspace Krypton"; // install once — see README (Nameplate font)
|
||
|
||
A_ap_w = A_w + 2; // through-aperture (a hair bigger than active)
|
||
A_ap_h = A_h + 1; // still smaller than glass minus 2*lip
|
||
P_w = G_w + glass_gap; // glass pocket (locates the glass in X/Y)
|
||
P_h = G_h + glass_gap;
|
||
|
||
// screen placed centred on the deck (measured up the slope)
|
||
deck_L = (D - 2*corner_r) / cos(theta); // deck length along the slope
|
||
screen_cy = deck_L/2; // centre it
|
||
boss_r = 3.4; // M2 self-tap boss for the bracket
|
||
|
||
// ---- ports on the back wall (ESP32-S3-DevKitC-1 edge) --------------------
|
||
port_z = 7; // height of the port centres off the desk << MEASURE >>
|
||
usbc_w = 9.6; usbc_h = 3.6; // USB-C opening (with clearance)
|
||
sd_w = 12; sd_h = 2.4; // microSD slot
|
||
// X positions of the three openings along the back << MEASURE to your board >>
|
||
port_x = [W/2 - 15, W/2, W/2 + 17]; // usb-c (kbd), usb-c (power), µSD
|
||
|
||
// ---- baseplate / chassis --------------------------------------------------
|
||
bp_t = 2.6; // baseplate thickness
|
||
bp_gap = 0.5; // clearance so it drops into the shell
|
||
foot_r = 7; // round feet (the little typewriter feet)
|
||
foot_h = 3.5;
|
||
post_r = 4.2; // corner screw posts inside the shell (M2.5 self-tap)
|
||
post_pilot = 1.15;
|
||
|
||
// board mounting standoffs on the baseplate << MEASURE hole positions >>
|
||
standoff_h = 6;
|
||
standoff_pilot = 1.15;
|
||
// ESP32-S3-DevKitC-1 is ~70 x 28 mm; these are PLACEHOLDER hole coords:
|
||
esp_holes = [[W/2-33, 30],[W/2+33, 30],[W/2-33, 54],[W/2+33, 54]];
|
||
// DESPI-C579 breakout sits in the cavity on the LEFT, under the FPC exit; SPI
|
||
// wires (MOSI/SCLK/CS/DC/RST/BUSY + 3V3/GND) run from here across to the ESP32.
|
||
// PLACEHOLDER hole coords << MEASURE >>:
|
||
brk_holes = [[22, 40],[22, 66]];
|
||
|
||
// ---- colours (for the assembled render) -----------------------------------
|
||
C_body = "#B6CEB4";
|
||
C_plate = "#C9C3B2";
|
||
C_bracket= "#2B2B2B";
|
||
C_screen = "#F7F4EA";
|
||
C_foam = "#8a8f94";
|
||
|
||
// ---- cutaway sections -----------------------------------------------------
|
||
plan_z = 22; // height of the horizontal "plan" cut
|
||
plan_explode = 62; // gap between the halves in the exploded "plan" view
|
||
|
||
// ===========================================================================
|
||
// helpers
|
||
// ===========================================================================
|
||
module rrect(w, d, r) { // 2D rounded rectangle, centred
|
||
hull() for (mx=[-1,1], my=[-1,1])
|
||
translate([mx*(w/2-r), my*(d/2-r)]) circle(r=r);
|
||
}
|
||
|
||
// place children onto the reclined deck plane. Origin at the FRONT-TOP edge
|
||
// (world y=0, z=Hf) — where the true hull top surface actually begins; anchor
|
||
// it at the pillar centre instead and everything lands ~3mm below the surface.
|
||
// local frame: X = width, Y = up the slope, Z = out of the deck (normal).
|
||
module on_deck() {
|
||
translate([W/2, 0, Hf]) rotate([theta, 0, 0]) children();
|
||
}
|
||
|
||
// ===========================================================================
|
||
// body
|
||
// ===========================================================================
|
||
module body_outer() {
|
||
hull() {
|
||
translate([corner_r, corner_r, 0]) cylinder(h=Hf, r=corner_r);
|
||
translate([W-corner_r, corner_r, 0]) cylinder(h=Hf, r=corner_r);
|
||
translate([corner_r, D-corner_r, 0]) cylinder(h=Hb, r=corner_r);
|
||
translate([W-corner_r, D-corner_r, 0]) cylinder(h=Hb, r=corner_r);
|
||
}
|
||
}
|
||
|
||
module body_cavity() {
|
||
ri = corner_r - wall;
|
||
hull() {
|
||
translate([corner_r, corner_r, -3]) cylinder(h=Hf-top_wall+3, r=ri);
|
||
translate([W-corner_r, corner_r, -3]) cylinder(h=Hf-top_wall+3, r=ri);
|
||
translate([corner_r, D-corner_r, -3]) cylinder(h=Hb-top_wall+3, r=ri);
|
||
translate([W-corner_r, D-corner_r, -3]) cylinder(h=Hb-top_wall+3, r=ri);
|
||
}
|
||
}
|
||
|
||
// 4 corner posts the baseplate screws up into
|
||
module corner_posts() {
|
||
for (px=[corner_r+3, W-corner_r-3], py=[corner_r+3, D-corner_r-3]) {
|
||
h = (py < D/2) ? Hf-top_wall : Hb-top_wall;
|
||
translate([px, py, 0]) difference() {
|
||
cylinder(h=h, r=post_r);
|
||
translate([0,0,-1]) cylinder(h=h+2, r=post_pilot);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4 bosses just OUTSIDE the glass pocket for the retaining bracket
|
||
module bracket_bosses() {
|
||
on_deck() for (bx=[-(P_w/2+5), P_w/2+5],
|
||
by=[screen_cy-(P_h/2+5), screen_cy+(P_h/2+5)]) {
|
||
blen = lip_t + G_t + foam_t + bracket_t + 6;
|
||
translate([bx, by, -lip_t-blen]) difference() {
|
||
cylinder(h=blen, r=boss_r);
|
||
translate([0,0,-1]) cylinder(h=blen+2, r=1.0); // M2 self-tap
|
||
}
|
||
}
|
||
}
|
||
|
||
// deck cuts: through-aperture, glass pocket (leaves the front lip), FPC slot
|
||
module screen_cuts() {
|
||
on_deck() translate([0, screen_cy, 0]) {
|
||
// window — centred on the ACTIVE area (offset toward the FPC/left edge)
|
||
translate([active_off_x, active_off_y, -30])
|
||
cube([A_ap_w, A_ap_h, 66], center=true);
|
||
// glass pocket behind the lip — centred on the glass outline
|
||
translate([0, 0, -30-lip_t]) cube([P_w, P_h, 60], center=true);
|
||
// FPC clearance: an internal notch in the LEFT recess wall, kept BELOW
|
||
// the bezel lip so it stays invisible from outside — the flex passes the
|
||
// glass's left edge and folds back into the cavity, to the breakout
|
||
translate([-P_w/2, 0, -30-lip_t]) cube([14, fpc_w, 60], center=true);
|
||
}
|
||
}
|
||
|
||
module port_cuts() {
|
||
// USB-C x2 + microSD through the back wall (y = D)
|
||
for (i=[0:2]) {
|
||
pw = (i==2) ? sd_w : usbc_w;
|
||
ph = (i==2) ? sd_h : usbc_h;
|
||
translate([port_x[i], D-wall-1, port_z])
|
||
rotate([-90,0,0]) linear_extrude(wall+2)
|
||
offset(r=0.8) square([pw-1.6, ph-1.6], center=true);
|
||
}
|
||
}
|
||
|
||
// engraved nameplate on the DECK, in the band between the front edge and the
|
||
// screen — faces the user as they write. Sits flat on the reclined deck.
|
||
module nameplate() {
|
||
name_y = (screen_cy - P_h/2) / 2; // centre of the front deck band
|
||
on_deck() translate([screen_off, name_y, -name_depth])
|
||
linear_extrude(name_depth + 0.6)
|
||
text(name_text, size=name_size, halign="center", valign="center",
|
||
font=name_font, spacing=1.1);
|
||
}
|
||
|
||
module case_body() {
|
||
difference() {
|
||
union() {
|
||
difference() { body_outer(); body_cavity(); }
|
||
corner_posts();
|
||
bracket_bosses();
|
||
}
|
||
screen_cuts();
|
||
port_cuts();
|
||
nameplate(); // engrave (comment out for a blank face)
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// screen retaining bracket (printed flat, screwed to the 4 bosses)
|
||
// ===========================================================================
|
||
module bracket() {
|
||
ow = P_w + 18; oh = P_h + 18;
|
||
difference() {
|
||
linear_extrude(bracket_t)
|
||
difference() {
|
||
rrect(ow, oh, 4);
|
||
rrect(A_ap_w+2, A_ap_h+2, 2);
|
||
// ribbon (FPC) relief: a gap in the LEFT frame member, lined up
|
||
// with the body's FPC clearance so the flex can fold back through
|
||
// the bracket plane into the cavity instead of being pinched
|
||
translate([-(ow + A_ap_w+2)/4, 0])
|
||
square([(ow - (A_ap_w+2))/2 + 4, fpc_w], center=true);
|
||
}
|
||
for (bx=[-(P_w/2+5), P_w/2+5], by=[-(P_h/2+5), P_h/2+5])
|
||
translate([bx, by, -1]) cylinder(h=bracket_t+2, r=1.45); // M2 clear
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// baseplate / chassis
|
||
// ===========================================================================
|
||
module baseplate() {
|
||
iw = W - 2*wall - bp_gap;
|
||
id = D - 2*wall - bp_gap;
|
||
difference() {
|
||
union() {
|
||
// plate (centred on the footprint)
|
||
translate([W/2, D/2, 0]) linear_extrude(bp_t) rrect(iw, id, corner_r-wall);
|
||
// round feet underneath
|
||
for (fx=[corner_r+6, W-corner_r-6], fy=[corner_r+6, D-corner_r-6])
|
||
translate([fx, fy, -foot_h]) cylinder(h=foot_h+0.1, r=foot_r);
|
||
// board standoffs on top
|
||
for (h = concat(esp_holes, brk_holes))
|
||
translate([h[0], h[1], bp_t]) cylinder(h=standoff_h, r=3);
|
||
}
|
||
// corner screw clearance (into the body posts)
|
||
for (px=[corner_r+3, W-corner_r-3], py=[corner_r+3, D-corner_r-3])
|
||
translate([px, py, -foot_h-1]) cylinder(h=bp_t+foot_h+2, r=1.6);
|
||
// standoff pilot holes
|
||
for (h = concat(esp_holes, brk_holes))
|
||
translate([h[0], h[1], bp_t-1]) cylinder(h=standoff_h+2, r=standoff_pilot);
|
||
// cable / connector relief at the back
|
||
translate([W/2, D-wall-3, -1]) cube([30, 8, bp_t+2], center=false);
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// assemblies
|
||
// ===========================================================================
|
||
module ghost_screen() {
|
||
on_deck() translate([screen_off, screen_cy+screen_off, -lip_t-G_t/2])
|
||
color(C_screen) cube([G_w, G_h, G_t], center=true);
|
||
}
|
||
module placed_bracket() {
|
||
on_deck() translate([screen_off, screen_cy+screen_off,
|
||
-lip_t-G_t-foam_t-bracket_t])
|
||
color(C_bracket) bracket();
|
||
}
|
||
// foam gasket (non-adhesive) — a border frame between glass and bracket
|
||
module foam() {
|
||
linear_extrude(foam_t)
|
||
difference() { rrect(P_w+4, P_h+4, 3); rrect(A_ap_w, A_ap_h, 2); }
|
||
}
|
||
module placed_foam() {
|
||
on_deck() translate([screen_off, screen_cy+screen_off, -lip_t-G_t-foam_t])
|
||
color(C_foam) foam();
|
||
}
|
||
// full coloured assembly, reused by the plan sections
|
||
module plan_assembly() {
|
||
color(C_body) case_body();
|
||
ghost_screen();
|
||
placed_foam();
|
||
placed_bracket();
|
||
translate([0,0,-0.01]) color(C_plate) baseplate();
|
||
}
|
||
// the two halves of the horizontal cut at plan_z
|
||
module plan_down() { // bottom: the cavity (standoffs, posts, ports)
|
||
intersection() {
|
||
plan_assembly();
|
||
translate([-60, -60, plan_z-200]) cube([W+120, D+120, 200]);
|
||
}
|
||
}
|
||
module plan_up() { // top: the deck / lid (screen, bracket)
|
||
intersection() {
|
||
plan_assembly();
|
||
translate([-60, -60, plan_z]) cube([W+120, D+120, 200]);
|
||
}
|
||
}
|
||
|
||
if (show == "assembled") {
|
||
color(C_body) case_body();
|
||
ghost_screen();
|
||
placed_bracket();
|
||
translate([0,0,-0.01]) color(C_plate) baseplate();
|
||
} else if (show == "body") {
|
||
color(C_body) case_body();
|
||
} else if (show == "bracket") {
|
||
color(C_bracket) bracket();
|
||
} else if (show == "baseplate") {
|
||
color(C_plate) baseplate();
|
||
} else if (show == "print_plate") {
|
||
color(C_body) case_body();
|
||
translate([W+30, 0, 0]) color(C_plate) baseplate();
|
||
translate([W+30, D+30, foot_h]) color(C_bracket) bracket();
|
||
} else if (show == "section") {
|
||
// VERTICAL slice (remove +X half): cut face shows the screen clamp, and the
|
||
// retained LEFT half exposes the internal FPC clearance behind the bezel
|
||
difference() {
|
||
union() {
|
||
color(C_body) case_body();
|
||
ghost_screen();
|
||
placed_foam();
|
||
placed_bracket();
|
||
translate([0,0,-0.01]) color(C_plate) baseplate();
|
||
}
|
||
translate([W/2, -30, -70]) cube([W, D+60, 220]);
|
||
}
|
||
} else if (show == "plan") {
|
||
// EXPLODED horizontal section: deck/lid half lifted off the cavity half
|
||
plan_down();
|
||
translate([0, 0, plan_explode]) plan_up();
|
||
} else if (show == "plan_up") {
|
||
plan_up(); // just the top half — deck, screen, bracket
|
||
} else if (show == "plan_down") {
|
||
plan_down(); // just the bottom half — cavity, standoffs, ports
|
||
} |