feat(case): add parametric 3D-printed enclosure concept
Typewriter-body OpenSCAD model for the GDEY0579T93 panel and ESP32-S3-DevKitC-1: glueless screen retention (bezel lip + foam gasket + screwed bracket), baseplate-as-chassis board mounting, and a recessed deck nameplate. Ships render previews, a dimensioned concept drawing, and just recipes to open/render/export.
2
hardware/case/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# generated mesh exports — regenerate with `just stl`
|
||||
*.stl
|
||||
168
hardware/case/README.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Enclosure — typewriter body (concept)
|
||||
|
||||
Part of [**Typoena**](../../README.md) — the distraction-free DIY writing
|
||||
machine. This page covers the enclosure only; the project root README covers
|
||||
the whole appliance (hardware, software stack, roadmap).
|
||||
|
||||
A 3D-printable case for Typoena. 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. There is **no platen part** (it complicates the print) — the rounded
|
||||
back-top edge is a subtle roll that nods to one for free.
|
||||
|
||||
> **Status: v0 concept, not yet printed.** Outer form and the screen-retention
|
||||
> / board-mounting strategy are worked out and render cleanly. Board hole
|
||||
> positions and port offsets are placeholders marked `<< MEASURE >>` in the
|
||||
> `.scad` — confirm them against the real board before printing a final.
|
||||
|
||||

|
||||

|
||||
|
||||
## Files
|
||||
|
||||
| File | What |
|
||||
| --- | --- |
|
||||
| [`typoena-case.scad`](typoena-case.scad) | The parametric model. All dimensions live at the top. |
|
||||
| [`concept.html`](concept.html) | Dimensioned side/front/top drawing (open in a browser). |
|
||||
| `renders/` | PNG previews (regenerated by the commands below). |
|
||||
|
||||
## Render / preview
|
||||
|
||||
Needs [OpenSCAD](https://openscad.org). Open `typoena-case.scad` in the GUI and
|
||||
flip the `show` variable, or from the CLI:
|
||||
|
||||
```sh
|
||||
cd hardware/case
|
||||
# assembled, coloured, screen ghosted in
|
||||
openscad -o renders/assembled.png --imgsize=1100,825 --colorscheme=Tomorrow \
|
||||
--camera=0,0,0,62,0,22,0 --viewall --autocenter \
|
||||
-D 'show="assembled"' typoena-case.scad
|
||||
|
||||
# export a printable part to STL (body | bracket | baseplate)
|
||||
openscad -o body.stl -D 'show="body"' typoena-case.scad
|
||||
```
|
||||
|
||||
`show` accepts `assembled` · `body` · `bracket` · `baseplate` · `print_plate`.
|
||||
|
||||
## Dimensions
|
||||
|
||||
From the datasheets, baked into the model:
|
||||
|
||||
- **Panel — GDEY0579T93:** glass outline **150.92 × 56.94 × 1.0 mm**, active
|
||||
area **139.00 × 47.74 mm**, pitch 0.1755 mm. Strip aspect ~2.9:1.
|
||||
- **Board — ESP32-S3-DevKitC-1:** ~**70 × 28 mm**, USB-C ×2 + reset/boot on one
|
||||
short edge (that edge faces the back wall).
|
||||
- **Body (default):** 176 W × 104 D, 24 mm front → 58 mm back, deck reclined
|
||||
**~21°**. Walls 2.4 mm, deck 2.6 mm, corner radius 8 mm.
|
||||
|
||||
The deck angle is the one knob worth tuning first — see below.
|
||||
|
||||
## How the hardware goes in — glueless
|
||||
|
||||
The whole design avoids glue on the fragile 1 mm glass and keeps every part
|
||||
serviceable.
|
||||
|
||||

|
||||
|
||||
### Screen (bezel lip + foam + screwed bracket)
|
||||
|
||||
```
|
||||
front face the sandwich, front → back:
|
||||
┌───────────────┐ 1. deck BEZEL LIP (overlaps ~4–5 mm of the
|
||||
│ ┌─────────┐ │ glass's inactive border only, never the
|
||||
│ │ active │ │ ← lip active area — lip_t = 1.4 mm of material)
|
||||
│ └─────────┘ │ 2. GLASS drops into the recess from behind;
|
||||
└───────────────┘ the recess walls locate it in X/Y
|
||||
3. FOAM gasket (non-adhesive closed-cell,
|
||||
[lip][glass][foam][bracket] foam_t ≈ 1 mm) spreads the clamp load
|
||||
↑ screwed to 4 bosses so you never point-crack the glass
|
||||
4. printed BRACKET, screwed to 4 bosses,
|
||||
presses the stack forward
|
||||
```
|
||||
|
||||
- The through-**aperture** is a hair larger than the active area and stays
|
||||
*inside* the glass-minus-lip envelope, so the lip covers only dead border.
|
||||
- The recess opens straight into the cavity, and an **FPC slot** on the
|
||||
up-slope edge lets the ribbon fold back to the DESPI-C579 breakout.
|
||||
- Foam does three jobs: cushions the glass, takes up print tolerance
|
||||
(±0.2–0.5 mm), and removes any need for adhesive. Cut it from a plain EVA/
|
||||
PORON sheet — no sticky backing.
|
||||
- Alternative if you want no screws: replace the bracket with printed
|
||||
cantilever clips. It works, but clips point-load the glass edge; the
|
||||
foam+bracket route is gentler and I'd default to it.
|
||||
|
||||

|
||||
|
||||
### Boards (the baseplate is the chassis)
|
||||
|
||||
Mount everything to the **baseplate** on the bench, then drop it in and close
|
||||
from below — far easier than fishing screws inside a shell.
|
||||
|
||||
- ESP32 + DESPI-C579 sit on printed **standoffs** (M2.5 self-tap). Positions in
|
||||
`esp_holes` / `brk_holes` are placeholders — set them to your board's holes.
|
||||
No mounting holes on your board rev? Switch to slide-in edge rails.
|
||||
- The baseplate screws **up into 4 corner posts** in the shell.
|
||||
- A **cable relief** notch at the back lets the keyboard's USB-C cable exit and
|
||||
route around to the front.
|
||||
|
||||

|
||||
|
||||
### Assembly order
|
||||
|
||||
1. Lay glass into the deck recess (from inside), add the foam gasket, screw the
|
||||
bracket down onto the 4 bosses.
|
||||
2. Screw ESP32 + breakout to the baseplate standoffs.
|
||||
3. Connect the FPC (screen → breakout) through the slot.
|
||||
4. Screw the baseplate up into the corner posts.
|
||||
|
||||
## Tune first
|
||||
|
||||
- **`Hb` (back height) → deck angle.** 18–22° is typewriter-shallow; raise `Hb`
|
||||
toward ~28–35° if the screen reads too edge-on when you're sitting close.
|
||||
- **`<< MEASURE >>` items:** `esp_holes`, `brk_holes`, `port_x`, `port_z`,
|
||||
`screen_off` (the real panel's active area is offset toward the FPC edge).
|
||||
|
||||
## Print notes
|
||||
|
||||

|
||||
|
||||
- **Material:** PLA/PETG. Print the body in matte **indigo** (`#130f40`), the
|
||||
bracket/base in cream or brass — two-tone reads unmistakably "typewriter" for
|
||||
the price of a filament swap.
|
||||
- **Make the engrave read:** on a body this dark the recessed `TYPOENA` is
|
||||
near-invisible until you give it contrast — a swipe of paint pen in the recess,
|
||||
or a 3–4 layer filament swap across the nameplate band mid-print.
|
||||
- **Shell, not solid:** 2.4 mm walls + open bottom keep material low despite the
|
||||
chunky body form.
|
||||
- **Orientation:** body deck-up (or on its back) needs little/no support; the
|
||||
bezel lip is a small overhang. Bracket and baseplate print flat.
|
||||
|
||||
## Nameplate font
|
||||
|
||||
The `TYPOENA` engrave on the deck (recessed, faces the user) is set via the
|
||||
`name_font` parameter. Current pick: **Monaspace Krypton** (GitHub's mechanical
|
||||
mono). OpenSCAD only renders fonts installed on the system:
|
||||
|
||||
```sh
|
||||
# current: Monaspace Krypton (installs the whole family)
|
||||
brew install --cask font-monaspace
|
||||
|
||||
# alternative tried: Cutive Mono (typewriter slab-serif)
|
||||
curl -sL -o ~/Library/Fonts/CutiveMono-Regular.ttf \
|
||||
https://github.com/google/fonts/raw/main/ofl/cutivemono/CutiveMono-Regular.ttf
|
||||
|
||||
fc-cache -f # so the OpenSCAD CLI picks either up
|
||||
```
|
||||
|
||||
To audition other faces: Google Fonts, filtered to monospace and previewing the
|
||||
name —
|
||||
<https://fonts.google.com/?preview.text=TYPOENA%0ATypoena%0Atypoena&categoryFilters=Appearance:%2FMonospace%2FMonospace>
|
||||
|
||||
## Open questions / TODO
|
||||
|
||||
- [ ] Confirm the GDEY0579T93 active-area **offset** and which edge the **FPC**
|
||||
exits; adjust `screen_off` / the FPC slot.
|
||||
- [ ] Real ESP32-S3-DevKitC-1 mounting-hole + port coordinates.
|
||||
- [ ] Optional **hinged lid** over the deck (portable-typewriter-case echo,
|
||||
protects the glass in a bag) — `docs/hardware.md` calls for one; not yet
|
||||
modelled.
|
||||
- [ ] Decide feet: printed (modelled) vs. stick-on rubber bumpers.
|
||||
255
hardware/case/concept.html
Normal file
@@ -0,0 +1,255 @@
|
||||
<title>Typoena — Enclosure Concept</title>
|
||||
<style>
|
||||
:root{
|
||||
--paper:#E7E1D0; --sheet:#F2EDE0; --ink:#26231D; --ink-soft:#5b564c;
|
||||
--acc:#3C6152; --acc-l:#6E9483; --dim:#B0553B; --hair:#B9B09A;
|
||||
--screen:#F7F4EA;
|
||||
--serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,"Times New Roman",serif;
|
||||
--mono:"SFMono-Regular",ui-monospace,Menlo,Consolas,"Courier New",monospace;
|
||||
}
|
||||
*{box-sizing:border-box;}
|
||||
body{margin:0;}
|
||||
.wrap{
|
||||
background:
|
||||
radial-gradient(120% 120% at 50% 0%, #ECE7D8 0%, var(--paper) 60%);
|
||||
min-height:100%;
|
||||
padding:clamp(16px,3vw,44px);
|
||||
color:var(--ink);
|
||||
font-family:var(--serif);
|
||||
}
|
||||
.sheet{
|
||||
max-width:1180px;margin:0 auto;background:var(--sheet);
|
||||
border:1.5px solid var(--ink);
|
||||
box-shadow:0 1px 0 var(--ink),0 22px 50px -28px rgba(38,35,29,.55);
|
||||
}
|
||||
/* titleblock */
|
||||
.titleblock{
|
||||
display:grid;grid-template-columns:1.7fr .8fr .8fr .9fr;
|
||||
border-bottom:1.5px solid var(--ink);
|
||||
}
|
||||
.tb{padding:12px 16px;border-right:1px solid var(--ink);}
|
||||
.tb:last-child{border-right:0;}
|
||||
.tb .k{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft);}
|
||||
.tb .v{font-family:var(--mono);font-size:14px;margin-top:3px;font-variant-numeric:tabular-nums;}
|
||||
.tb-title .name{font-family:var(--serif);font-size:clamp(22px,3.4vw,34px);line-height:1.02;letter-spacing:-.01em;font-weight:600;text-wrap:balance;}
|
||||
.tb-title .sub{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--acc);margin-top:6px;}
|
||||
/* intro */
|
||||
.intro{padding:18px 22px 6px;border-bottom:1px solid var(--hair);}
|
||||
.intro p{margin:0 auto;max-width:70ch;font-size:16px;line-height:1.5;color:#3a362e;}
|
||||
.intro .lead{font-style:italic;color:var(--acc);}
|
||||
/* view panels */
|
||||
.views{display:grid;grid-template-columns:1fr;gap:0;}
|
||||
.panel{padding:14px 18px 18px;border-bottom:1px solid var(--hair);}
|
||||
.panel h2{
|
||||
font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;
|
||||
color:var(--ink);margin:0 0 2px;display:flex;justify-content:space-between;align-items:baseline;
|
||||
}
|
||||
.panel h2 .tag{color:var(--dim);}
|
||||
.panel .cap{font-family:var(--serif);font-style:italic;color:var(--ink-soft);font-size:13px;margin:0 0 8px;}
|
||||
.lower{display:grid;grid-template-columns:1fr 1fr;}
|
||||
.lower .panel:first-child{border-right:1px solid var(--hair);}
|
||||
svg{width:100%;height:auto;display:block;}
|
||||
svg text{font-family:var(--mono);}
|
||||
/* notes */
|
||||
.notes{padding:18px 22px 24px;display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:18px 26px;}
|
||||
.note h3{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;margin:0 0 6px;color:var(--acc);display:flex;gap:8px;align-items:baseline;}
|
||||
.note h3 .n{color:var(--dim);font-variant-numeric:tabular-nums;}
|
||||
.note p{margin:0;font-size:14px;line-height:1.45;color:#3a362e;}
|
||||
.note p b{color:var(--ink);font-weight:600;}
|
||||
.foot{border-top:1.5px solid var(--ink);padding:10px 18px;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;
|
||||
font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);}
|
||||
.legend{display:flex;gap:16px;flex-wrap:wrap;align-items:center;font-family:var(--mono);font-size:10.5px;letter-spacing:.08em;color:var(--ink-soft);margin-top:6px;}
|
||||
.legend span{display:inline-flex;gap:6px;align-items:center;}
|
||||
.sw{width:14px;height:10px;border:1px solid var(--ink);display:inline-block;}
|
||||
@media (max-width:720px){
|
||||
.titleblock{grid-template-columns:1fr 1fr;}
|
||||
.tb-title{grid-column:1/-1;border-right:0;border-bottom:1px solid var(--ink);}
|
||||
.lower{grid-template-columns:1fr;}
|
||||
.lower .panel:first-child{border-right:0;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="sheet">
|
||||
|
||||
<div class="titleblock">
|
||||
<div class="tb tb-title">
|
||||
<div class="name">Typoena</div>
|
||||
<div class="sub">Enclosure concept · typewriter body</div>
|
||||
</div>
|
||||
<div class="tb"><div class="k">Panel</div><div class="v">GDEY0579T93</div><div class="k" style="margin-top:8px">Board</div><div class="v">ESP32-S3</div></div>
|
||||
<div class="tb"><div class="k">Units</div><div class="v">mm</div><div class="k" style="margin-top:8px">Scale</div><div class="v">to-fit</div></div>
|
||||
<div class="tb"><div class="k">Rev</div><div class="v">v0 · concept</div><div class="k" style="margin-top:8px">Date</div><div class="v">2026-07-11</div></div>
|
||||
</div>
|
||||
|
||||
<div class="intro">
|
||||
<p><span class="lead">The screen is the sheet of paper.</span> A shallow sage wedge with a reclined deck; the e-paper strip sits where a typewriter's page would be, a decorative platen roller and two knobs ride the back edge, and the keyboard you bring rests in front. Every dimension below is from the real datasheets — this is the shape to react to before I cut the parametric model.</p>
|
||||
<div class="legend">
|
||||
<span><i class="sw" style="background:var(--acc)"></i>body</span>
|
||||
<span><i class="sw" style="background:var(--acc-l)"></i>platen / deck / feet</span>
|
||||
<span><i class="sw" style="background:var(--screen)"></i>e-paper</span>
|
||||
<span><i class="sw" style="background:var(--dim);border-color:var(--dim)"></i>dimensions</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="views">
|
||||
<div class="panel">
|
||||
<h2>Side profile <span class="tag">— the silhouette</span></h2>
|
||||
<p class="cap">The money view: shallow deck reclined ~18°, platen & knob at the back, ports on the tail, your keyboard ghosted in front.</p>
|
||||
<svg id="v-side" viewBox="-82 -96 200 118" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Side profile of the enclosure"></svg>
|
||||
</div>
|
||||
<div class="lower">
|
||||
<div class="panel">
|
||||
<h2>Front elevation <span class="tag">— width</span></h2>
|
||||
<p class="cap">Letterbox screen across the deck; knobs poke past the sides.</p>
|
||||
<svg id="v-front" viewBox="-34 -96 244 118" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Front elevation"></svg>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>Top plan <span class="tag">— deck layout</span></h2>
|
||||
<p class="cap">Screen forward, platen band aft, ports along the back edge.</p>
|
||||
<svg id="v-top" viewBox="-34 -120 244 136" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Top plan view"></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<div class="note"><h3><span class="n">01</span> Cheap-ish body</h3><p>You picked the higher-material form, but it stays a <b>shell</b>: ~2.4 mm walls, open bottom, a screwed baseplate. Hollow inside, no infill spent on a solid block.</p></div>
|
||||
<div class="note"><h3><span class="n">02</span> Color does the work</h3><p>Print the body in <b>matte sage</b>, platen & knobs in cream or black. Two-tone via a filament change reads unmistakably "typewriter" for free.</p></div>
|
||||
<div class="note"><h3><span class="n">03</span> Decorative cues</h3><p>Platen roller + two knobs are <b>cosmetic</b> (your call) — printed separately, no wiring, no GPIO. Round feet finish the machined look.</p></div>
|
||||
<div class="note"><h3><span class="n">04</span> Fragile glass</h3><p>The 1 mm panel drops into a rebate behind a bezel lip that overlaps ~6 mm; the FPC folds back to the DESPI-C579 breakout in the cavity.</p></div>
|
||||
<div class="note"><h3><span class="n">05</span> The one tunable</h3><p><b>Deck angle</b> is the ergonomics dial. 18° is typewriter-shallow; 28–35° reads better seated. Say the word and I'll steepen it.</p></div>
|
||||
<div class="note"><h3><span class="n">06</span> Optional lid</h3><p>Your docs say "hinged lid" — a cover latching over the deck echoes a portable typewriter's case and guards the glass in a bag. Toggled off by default.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="foot">
|
||||
<span>Typoena · writing appliance</span>
|
||||
<span>Screen 150.9 × 56.9 × 1.0 · active 139.0 × 47.7 · pitch 0.1755</span>
|
||||
<span>Orthographic · third-angle · not for manufacture</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// ---- parameters (mm) ----
|
||||
var W=176, D=104, Hf=24, Hb=58; // body: width, depth, front/back height
|
||||
var SW=151, SH=57, AW=139.0, AH=47.74; // screen glass + active area
|
||||
var sx0=(W-SW)/2, sx1=(W+SW)/2; // screen x extents
|
||||
var sy0=8, sy1=8+54; // screen y extents on deck (57 along slope ~ 54 in plan)
|
||||
var Py=92, Pr=11, Kr=14; // platen y-centre, roller radius, knob radius
|
||||
function zTop(y){ return Hf + (Hb-Hf)*y/D; }
|
||||
var Pz=zTop(Py)+Pr; // platen centre height (sits on deck)
|
||||
var footR=6;
|
||||
|
||||
// ---- palette ----
|
||||
var INK="#26231D", PAPER="#F7F4EA", ACC="#3C6152", ACCL="#6E9483",
|
||||
DIM="#B0553B", HAIR="#B9B09A", GHOST="#8f8a7c";
|
||||
var MONO="'SFMono-Regular',ui-monospace,Menlo,Consolas,'Courier New',monospace";
|
||||
|
||||
// ---- primitives (model coords; +b is up, drawn as svg y=-b) ----
|
||||
function poly(pts,fill,stroke,sw){var p=pts.map(function(q){return q[0]+","+(-q[1]);}).join(" ");
|
||||
return '<polygon points="'+p+'" fill="'+fill+'" stroke="'+stroke+'" stroke-width="'+sw+'" stroke-linejoin="round"/>';}
|
||||
function rect(a,b,w,h,fill,stroke,sw,rx,dash){
|
||||
return '<rect x="'+a+'" y="'+(-(b+h))+'" width="'+w+'" height="'+h+'" rx="'+(rx||0)+'" fill="'+fill+'" stroke="'+stroke+'" stroke-width="'+sw+'"'+(dash?' stroke-dasharray="'+dash+'"':'')+'/>';}
|
||||
function circ(a,b,r,fill,stroke,sw){return '<circle cx="'+a+'" cy="'+(-b)+'" r="'+r+'" fill="'+fill+'" stroke="'+stroke+'" stroke-width="'+sw+'"/>';}
|
||||
function line(a1,b1,a2,b2,stroke,sw,dash){
|
||||
return '<line x1="'+a1+'" y1="'+(-b1)+'" x2="'+a2+'" y2="'+(-b2)+'" stroke="'+stroke+'" stroke-width="'+sw+'" stroke-linecap="round"'+(dash?' stroke-dasharray="'+dash+'"':'')+'/>';}
|
||||
function txt(a,b,s,o){o=o||{};var fs=o.fs||3.4,fill=o.fill||INK,anc=o.anchor||"middle",
|
||||
rot=o.rot?' transform="rotate('+o.rot+' '+a+' '+(-b)+')"':'',ls=o.ls!=null?o.ls:0.2;
|
||||
return '<text x="'+a+'" y="'+(-b)+'" font-size="'+fs+'" fill="'+fill+'" text-anchor="'+anc+'" letter-spacing="'+ls+'"'+rot+' font-family="'+MONO+'">'+s+'</text>';}
|
||||
|
||||
// dimension helpers (oxide red)
|
||||
function hdim(a1,a2,b,label,below){var t=1.6,s="";
|
||||
s+=line(a1,b,a2,b,DIM,0.3);
|
||||
s+=line(a1-t,b-t,a1+t,b+t,DIM,0.3);
|
||||
s+=line(a2-t,b-t,a2+t,b+t,DIM,0.3);
|
||||
s+=txt((a1+a2)/2, b+(below?-4.2:2.4), label, {fs:3.2,fill:DIM});
|
||||
return s;}
|
||||
function vdim(b1,b2,a,label,right){var t=1.6,s="";
|
||||
s+=line(a,b1,a,b2,DIM,0.3);
|
||||
s+=line(a-t,b1-t,a+t,b1+t,DIM,0.3);
|
||||
s+=line(a-t,b2-t,a+t,b2+t,DIM,0.3);
|
||||
s+=txt(a+(right?3.2:-3.2),(b1+b2)/2,label,{fs:3.2,fill:DIM,rot:-90});
|
||||
return s;}
|
||||
function screenlines(x0,y0,x1,y1,n){var s="",i,y;for(i=1;i<=n;i++){y=y0+(y1-y0)*i/(n+1);
|
||||
s+=line(x0+2,y,x1-2,y,HAIR,0.3);}return s;}
|
||||
|
||||
// ---- SIDE (a=depth Y, b=height Z) ----
|
||||
(function(){var s="";
|
||||
s+=line(-66,0,110,0,HAIR,0.4); // ground
|
||||
s+=rect(-58,0,50,12,"rgba(60,97,82,.06)",GHOST,0.4,2,"2 1.6"); // keyboard ghost
|
||||
s+=txt(-33,-4,"BYO KEYBOARD",{fs:2.9,fill:GHOST});
|
||||
s+=poly([[0,0],[D,0],[D,Hb],[0,Hf]],ACC,INK,0.7); // wedge
|
||||
s+=txt(30,37,"DECK ≈18°",{fs:3.1,fill:PAPER});
|
||||
// screen strip on slope
|
||||
s+=line(sy0,zTop(sy0),sy1,zTop(sy1),INK,3.9);
|
||||
s+=line(sy0,zTop(sy0),sy1,zTop(sy1),PAPER,3.1);
|
||||
// platen roller + knob
|
||||
s+=circ(Py,Pz,Pr,ACCL,INK,0.5);
|
||||
s+=circ(Py,Pz,Kr,ACCL,INK,0.6);
|
||||
s+=circ(Py,Pz,2.4,ACC,INK,0.4);
|
||||
s+=txt(Py,Pz+Kr+3,"PLATEN Ø22",{fs:3,fill:DIM});
|
||||
// ports on back face
|
||||
s+=rect(D-6,9,6,4,PAPER,INK,0.4,.6);
|
||||
s+=rect(D-6,15,6,4,PAPER,INK,0.4,.6);
|
||||
s+=rect(D-6,21,7,3,PAPER,INK,0.4,.6);
|
||||
s+=line(D,16,D+8,16,DIM,0.3);
|
||||
s+=txt(D+9,16,"USB-C ×2 + µSD",{fs:2.9,fill:INK,anchor:"start"});
|
||||
// feet
|
||||
s+=rect(6,-5,12,5,ACCL,INK,0.4,1);
|
||||
s+=rect(D-18,-5,12,5,ACCL,INK,0.4,1);
|
||||
// dims
|
||||
s+=hdim(0,D,-9,"104 depth",true);
|
||||
s+=vdim(0,Hf,-4,"24");
|
||||
s+=vdim(0,Hb,D+4,"58",true);
|
||||
document.getElementById("v-side").innerHTML=s;
|
||||
})();
|
||||
|
||||
// ---- FRONT (a=width X, b=height Z) ----
|
||||
(function(){var s="";
|
||||
s+=line(-14,0,W+14,0,HAIR,0.4);
|
||||
s+=rect(0,0,W,Hf,ACC,INK,0.7,2); // front face
|
||||
s+=rect(0,Hf,W,Hb-Hf,ACCL,INK,0.5); // deck sliver behind
|
||||
// screen (foreshortened band)
|
||||
s+=rect(sx0,27,SW,18,PAPER,INK,0.5,1);
|
||||
s+=screenlines(sx0,27,sx1,45,4);
|
||||
// platen bar + knobs
|
||||
s+=rect(2,Pz-4,W-4,8,ACCL,INK,0.5,4);
|
||||
s+=circ(-2,Pz,Kr,ACCL,INK,0.6); s+=circ(-2,Pz,2.4,ACC,INK,0.4);
|
||||
s+=circ(W+2,Pz,Kr,ACCL,INK,0.6); s+=circ(W+2,Pz,2.4,ACC,INK,0.4);
|
||||
// feet
|
||||
s+=rect(8,-5,14,5,ACCL,INK,0.4,1);
|
||||
s+=rect(W-22,-5,14,5,ACCL,INK,0.4,1);
|
||||
// dims
|
||||
s+=hdim(0,W,-9,"176 width",true);
|
||||
s+=hdim(-16,W+16,Pz+Kr+2,"≈208 across knobs");
|
||||
s+=hdim(sx0,sx1,20,"151 glass · 139 active");
|
||||
document.getElementById("v-front").innerHTML=s;
|
||||
})();
|
||||
|
||||
// ---- TOP (a=width X, b=depth Y; front y=0 at bottom) ----
|
||||
(function(){var s="";
|
||||
s+=rect(0,0,W,D,"rgba(60,97,82,.10)",INK,0.7,4); // body outline
|
||||
// screen
|
||||
s+=rect(sx0,sy0,SW,sy1-sy0,PAPER,INK,0.5,1);
|
||||
s+=screenlines(sx0,sy0,sx1,sy1,5);
|
||||
s+=txt(W/2,(sy0+sy1)/2,"e-paper",{fs:3.4,fill:ACC});
|
||||
s+=txt(W/2,sy0-4,"active 139.0 × 47.74",{fs:2.8,fill:INK});
|
||||
// platen band + knobs
|
||||
s+=rect(2,Py-6,W-4,12,ACCL,INK,0.5,4);
|
||||
s+=circ(-6,Py,Kr,ACCL,INK,0.6); s+=circ(-6,Py,2.4,ACC,INK,0.4);
|
||||
s+=circ(W+6,Py,Kr,ACCL,INK,0.6); s+=circ(W+6,Py,2.4,ACC,INK,0.4);
|
||||
s+=txt(W/2,Py,"platen (decorative)",{fs:2.9,fill:INK});
|
||||
// ports along back edge (y=D)
|
||||
s+=rect(60,D-2,7,4,PAPER,INK,0.4,.6);
|
||||
s+=rect(72,D-2,7,4,PAPER,INK,0.4,.6);
|
||||
s+=rect(86,D-2,8,3,PAPER,INK,0.4,.6);
|
||||
s+=txt(W/2,D+4,"USB-C ×2 · microSD",{fs:2.8,fill:DIM});
|
||||
// dims
|
||||
s+=hdim(0,W,-6,"176",true);
|
||||
s+=vdim(0,D,W+8,"104 deep",true);
|
||||
document.getElementById("v-top").innerHTML=s;
|
||||
})();
|
||||
})();
|
||||
</script>
|
||||
BIN
hardware/case/renders/assembled.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
hardware/case/renders/baseplate.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
hardware/case/renders/body.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
hardware/case/renders/bracket.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
hardware/case/renders/front34.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
hardware/case/renders/nameplate.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
hardware/case/renders/print.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
277
hardware/case/typoena-case.scad
Normal file
@@ -0,0 +1,277 @@
|
||||
// ============================================================================
|
||||
// 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
|
||||
// ============================================================================
|
||||
|
||||
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; // extra X/Y active-area offset
|
||||
|
||||
// ---- 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; // width of the ribbon slot on the up-slope edge
|
||||
|
||||
// ---- 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 behind the screen — PLACEHOLDER:
|
||||
brk_holes = [[W/2-20, 78],[W/2+20, 78]];
|
||||
|
||||
// ---- colours (for the assembled render) -----------------------------------
|
||||
C_body = "#130f40"; // deep indigo (chosen)
|
||||
C_plate = "#C9C3B2";
|
||||
C_bracket= "#2B2B2B";
|
||||
C_screen = "#F7F4EA";
|
||||
|
||||
// ===========================================================================
|
||||
// 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([screen_off, screen_cy + screen_off, 0]) {
|
||||
// window
|
||||
translate([0,0,-30]) cube([A_ap_w, A_ap_h, 66], center=true);
|
||||
// glass pocket behind the lip
|
||||
translate([0,0,-30-lip_t]) cube([P_w, P_h, 60], center=true);
|
||||
// ribbon slot on the up-slope edge
|
||||
translate([0, P_h/2, -30-lip_t]) cube([fpc_w, 12, 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); }
|
||||
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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||