ShapeFinity designs 3D-printable trays that fill a drawer. You lay out trays in a 2D drawer plan, shape each tray (footprint, walls, floor openings, dividers, finger scoops, label tabs, finger notches), stack trays in deep drawers, split oversized trays into pieces that fit your printer, and export STL meshes or exact STEP solids. It is an ES-module app: a geometry kernel, a splicing module, a three.js viewer and an OpenCascade worker, all running in the browser.
Tabs walk through the job: Drawer → 2D Layout → 3D Layout → Tray Designer → Dividers → Splicing. All state (drawer, trays, settings) is one object, autosaved to localStorage (drawerTrayPlanner.v1) with undo/redo, and can be saved or loaded as a project file. Units display as mm or inches; all geometry is in mm.
| File | Size | Role |
|---|---|---|
js/app.js | 186 KB | state, UI panels, 2D canvas views, stacks, export wiring |
js/geometry.js | 71 KB | the tray kernel: contours, profile, holes, dividers, scoops, label tabs, finger notches, mesh, STEP model |
js/splice.js | 15 KB | cut shapes, splitting into pieces, bed fit, automatic cuts |
js/view3d.js | 10 KB | three.js scene, section view, selection highlight |
js/step.js + step-worker.js | 15 KB | OpenCascade B-rep solids → STEP (and exact STL per piece), placed in the drawer; the drawer itself as a solid |
manifold-3d (CDN) | ~1 MB | mesh booleans for the finger notches in the page's own mesh (loaded in the background) |
js/export.js | 3 KB | binary STL writer and a store-only ZIP writer |
The drawer is an inside width × depth × height (or a preset). Obstructions (a drawer slide, a lip, a screw boss) are blocks on the perimeter: length along a wall, depth into the drawer, height. Trays can avoid them with notches, 45°-sided cut-outs in a wall. Trays start from presets measured from real trays (Tall 2.25″, Standard 1.38″, Short 1.14″ high; solid, slotted, honeycomb, 2×2, L- and T-shaped…) and are placed on a 2D plan with snapping to the drawer walls and to other trays, keeping a set gap. The 3D Layout tab shows the whole drawer filled. Every tray, and each tray (or splice piece) against the print bed, is checked: outside the drawer, overlapping another tray, hitting an obstruction, too tall, or too big to print.
For deep drawers, trays can be stacked. A stacked tray is an ordinary tray with one extra field, base: the id of the tray at the bottom of its stack. Levels go up in the order the trays appear in the list. Everything that works on a tray (meshing, STEP, splicing, dividers) works on a stacked tray unchanged.
shape, w, d, pts, legs, orient, uneven, notches, R, Rc) and the position (x, y) are copied from the base to every level before each redraw, save and export. The height, walls, ribs, nesting profile, floor, dividers and splice cuts are each tray's own.bi = bInsetupper + clearupper, lip inset ti = min(tInsetlower, walllower) − clearlower, and the upper tray sinks (bi − ti) × tSlopelower when ti < bi < walllower. Otherwise (a chamfer turned off, or the foot misses the lip) it sits on the rim and sinks 0. With the defaults the sink is (1.60 − 0.64) × 2 = 1.92 mm.height − seat depth, where the upper tray's foot would land on them.A tray is a profile sweep, the way a potter's template sweeps a pot: one 2D cross-section of the wall is swept all the way around the footprint. All geometry is in millimeters, x right, y up in plan, z up.
[inset, z] points: how far in from the outermost edge, and how high.buildProfile walks the cross-section from the bottom outside edge, up the outside, across the rim, down the inside, and around the fillet onto the floor. Four features shape it (defaults from the Standard/Tall presets):
hw = √(R² − (R − b)²) = 2.57 mm (or a 45° V for the "v45" style). Ribs are placed every pitch (9.6) downward from top (7.7) below the rim, but only where they fit between a minimum height (above the floor, fillet and foot chamfer) and the lip chamfer. Each arc is sampled at 13 points.(b_i − t_i) × slope deep, and nesting works only if t_i < b_i < t.| Default Tall tray (57.15 mm) | Calculation | Result |
|---|---|---|
| Foot inset b_i / height | 1.40 + 0.2 clearance; × slope 2 | 1.60 / 3.20 mm |
| Lip inset t_i / height | min(0.84, 1.85) − 0.2; (1.85 − 0.64) × 2 | 0.64 / 2.42 mm |
| Nesting sink | (1.60 − 0.64) × 2 | 1.92 mm |
| Rib band | z from max(8, 1.6 + 2 + 0.5, 3.2 + 0.5) to 57.15 − 2.42 − 0.5 | 8.0 – 54.23 mm |
| Rib centers | 57.15 − 7.7 = 49.45, then every 9.6 down while it fits | 11.05, 20.65, 30.25, 39.85, 49.45 |
| Profile points | outside + rim + inside (5 ribs × 13 points each side) + fillet | ≈ 144 |
Offsetting a polygon with straight-line math makes corners grow or shrink unevenly and changes the number of points. Instead, every corner is treated as a fillet arc and rebuilt at each inset δ:
For the STEP export the same contour is also produced as exact segments: straight lines plus arcs given by start, midpoint and end, so OpenCascade gets true cylinders and tori rather than facets.
Each opening is a template polygon moved to every grid point: a circle (16–40 points, about 2.4 per mm of diameter), a hexagon (size = across flats, so corner radius = size ÷ √3), a square, a diamond, or a slot (a rectangle with semicircle ends, 11 points per end). Templates can be rotated by the pattern angle.
py = px × √3 ÷ 2. Rows are kept centered instead of drifting sideways.Dividers are either a grid (n × m equal compartments) or custom segments drawn in the Dividers tab. A custom segment is stored as fractions of the inner box ({ax, p, a, b}: the line x = p from y = a to b, or the reverse), so it scales with the tray. An open end runs to the wall; an end on another divider or free reaches half a divider thickness further, so corners and T-joints close. Arc dividers (round compartments) and honeycomb cells are also supported. In 3D each divider is a polygon in (along, z), thickened ± half its thickness; its ends sit inside the walls and step in above the foot chamfer so they never poke out of the bottom, and its top sits a set drop below the rim. The Dividers tab shows this both ways: Height = tray height − drop, and Below rim = drop; storing the drop keeps the dividers the same distance below the rim when the tray height changes.
Removable dividers are left out of the tray. In their place each end that meets a wall gets a slot: two rails 1.2 mm wide standing 1.6 mm out from the wall face, either side of the divider, with the set clearance on each side. The rails are built like short dividers (same axis, offset by half the divider + clearance + half a rail) and run from just inside the wall to 1.6 mm out, floor to divider top. The plates are separate solids: each divider's stretch between the wall faces, ended the clearance short of the wall (inside the slot), standing on the floor up to the divider top. Their bottom corners at a wall are chamfered by the floor fillet (or the scoop radius, on the scoop's side) plus the clearance. Where two plates cross they half-lap: the front-to-back one gets a slot from the top down to mid-height, the side-to-side one a slot from the bottom up, each the divider thickness + clearance wide, overlapping the clearance in height. An end that stops on another divider is interlocked, keeping every plate a flat profile so it prints lying down: at a T-joint (it meets the other part way along) the through plate gets a slot from the top to mid-height and the ending plate's upper half runs on through it to the far face as a tongue, its lower half stopping the clearance short; at an L-corner (it meets the other at that one's end) the front-to-back plate's upper half and the other plate's lower half run on to the far face, a corner half-lap. With an L-corner radius, rounded corners stay: the two plates stop at the quarter ring (as fixed dividers do), and plates joined through rings become one piece (a box of four rounded corners is a single ring-shaped insert), merged with manifold-3d in the page and fused in STEP. A piece with a ring can't lie flat, so the flat STL keeps it standing as it sits in the tray, on its bottom edge; straight plates lie down.
Following the wall. Divider ends, rail roots and the backs of scoops and label tabs follow the wall's real cross-section up their height (ribs, foot chamfer, rim lead-in, floor fillet), read off the profile at every height where it bends: dividers half way into the wall, rails 0.5 mm, tabs 0.4 mm and scoops 0.3 mm in from the actual inner face (so they reach into every rib groove), but never closer to the outer face than 0.15–0.3 mm (so nothing pokes out through the foot chamfer). The depths differ so no two of them share a face, which keeps the CAD union valid. Where the wall meets a divider along a straight wall the end position is exact; near a corner fillet it is interpolated.
Each is a flat profile swept sideways: a closed loop of lines and three-point arcs in a vertical plane, extruded along a horizontal vector. The mesh samples the arcs; the STEP worker builds them as true arcs and prisms, so the solids are exact.
ShapeUtils) plus T-junction repair and a strict conformity check. Regular hole grids create long runs of collinear vertices that trip ear clipping, so on failure it falls back to cdt2d, a constrained Delaunay triangulation with exact arithmetic.The 3D view (three.js 0.160) shows iso/top/front/side/bottom views, a section plane (upright or level, any angle and position) with stencil-capped cut faces, and edge lines. The selected tray is lit with orange edges; in the Tray Designer the rest of its stack is drawn as clickable ghosts at their heights.
When a tray is bigger than the bed (bed size is a setting), it is cut into pieces to glue back together. Along a cut, u runs along it and v across it, and the cut is a polyline v = f(u) that never turns back, running 30 mm past the tray at each end so it cuts cleanly. Styles:
Dragging an offset or mirrored cut along its length moves both diagonals together (one reach grows as the other shrinks, neither below 0); dragging a zig-zag along its length changes the shift, along its own turned line. Each piece is the tray intersected with the sides of its cuts, pulled back half the glue gap (the cut polyline offset with mitred corners). A cut is flagged if it leaves through a side wall instead of running wall to wall, and cuts on the same axis are flagged if they cross or come within 4 mm.
Auto cuts tries every combination of 0–5 evenly spaced cuts across the width (n) and 0–5 across the depth (m). A combination is rejected if its cuts cross or come too close (4 mm), or if any piece fails the bed test (a piece fits if its w × d fits the bed either way round). Among the rest it keeps the lowest score, (n + 1)(m + 1) × 100 + n + m: first the fewest pieces, then the fewest cuts. If nothing up to 6 × 6 pieces fits, it reports that. The joint style, flip, zig-zag tooth size and angle you chose are kept on the new cuts.
| Format | How it's made |
|---|---|
| STL (binary) | From the page's own triangle mesh: 80-byte header, triangle count, then normal + 3 vertices + 2-byte attribute per triangle. Named name_W x D x Hmm.stl. Spliced trays give one STL per piece in a ZIP built by a small store-only (uncompressed) ZIP writer. Removable divider plates go in their own …_dividers.stl in the ZIP: in place with drawer positions on, otherwise laid flat side by side for printing. |
| STEP | A worker loads opencascade.js (the same 14 MB build as the Thread Designer) on first use. The wall section is swept along the footprint with the binormal held vertical, so walls become exact planes, cylinders, cones and tori rather than facets; the floor plate is fused on, openings cut as one compound, dividers (or rails), scoops and label tabs fused one at a time, finger notches cut as one compound. Removable divider plates are added as solids of their own. Spliced pieces are intersections with prisms of their cut regions; each piece is its own solid in one STEP file. |
| Drawer positions | On by default: each tray (and each splice piece) is moved from its centred model to its place in the drawer, stacked trays to their height. Origin at the drawer's front-left inside floor corner, X right, Y toward the back, Z up. STL vertices are shifted directly; STEP solids are moved with an OpenCascade transform baked into the geometry, so every CAD program reads the same coordinates. |
| Whole drawer | The All trays exports, with drawer positions on, add the drawer as a reference: a 2 mm floor and walls outside the inside size, plus the obstructions, as boxes (fused into one solid in STEP). |