ShapeFinity Tray Maker: How It Works

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.

Workflow and files

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.

FileSizeRole
js/app.js186 KBstate, UI panels, 2D canvas views, stacks, export wiring
js/geometry.js71 KBthe tray kernel: contours, profile, holes, dividers, scoops, label tabs, finger notches, mesh, STEP model
js/splice.js15 KBcut shapes, splitting into pieces, bed fit, automatic cuts
js/view3d.js10 KBthree.js scene, section view, selection highlight
js/step.js + step-worker.js15 KBOpenCascade B-rep solids → STEP (and exact STL per piece), placed in the drawer; the drawer itself as a solid
manifold-3d (CDN)~1 MBmesh booleans for the finger notches in the page's own mesh (loaded in the background)
js/export.js3 KBbinary STL writer and a store-only ZIP writer

Drawer and layout

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.

Stacked trays

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.

  • Shared 2D shape. The footprint fields (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.
  • Nesting seat depth between two levels. The upper tray's foot chamfer lands on the lower tray's rim lead-in, each with its own fit clearance: foot inset 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 in the drawer. The base sits at z = 0; each level sits at the level below's z + its height − the seat depth between them. The stack's top (top level's z + height) is checked against the drawer height.
  • Checks. Overlap, outside-the-drawer and free-area tests use only the bases. A stacked tray is tested against an obstruction only if the obstruction is taller than the tray's z. A tray with something stacked on it is flagged if its dividers reach above height − seat depth, where the upper tray's foot would land on them.
  • Editing. Moving, rotating or reshaping happens on the base; rotating turns each level's own dividers and splice cuts with it. Reordering can put any level at the bottom: that tray becomes the base and the others point to it. Removing the base promotes the next level up.
  • Export. With drawer positions kept, each level is moved to its z as well as its x, y.

The tray model: a profile swept around the footprint

wall cross-section: [inset, z] points footprint offset inward by each point's inset, raised to its z outside face → top lip → inside face → floor top
Every profile point becomes one ring; neighboring rings have the same number of points, so they stitch into a closed shell.

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.

  • The footprint is a polygon: a rectangle, an L or T with adjustable legs, or a rectangle with 45° notches. It is cleaned first: duplicate and collinear points removed, and forced counter-clockwise (positive area by the shoelace formula).
  • The profile is a list of [inset, z] points: how far in from the outermost edge, and how high.
  • Each profile point becomes one horizontal ring: the footprint offset inward by that inset, at that height.

Building the wall profile

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):

outermost extent floor (t = 1.6) floor fillet (2.0) foot chamfer (nesting seat) grip ribs bulge out 1.31 mm lip chamfer (top inside) wall t = 1.85
Not to scale: one wall's cross-section with the inside to the right.
  1. Wall: thickness t = 1.85 mm. The outside face sits at inset b (the rib bulge), the inside face at b + t.
  2. Grip ribs: horizontal ribs that bulge out of the outside face (and the inside follows, so the wall stays even). Each is a circular arc of radius R (3.175) rising b = 1.31 above the face, so its half-height is 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.
  3. Nesting: a chamfered foot at the bottom outside (inset b_i, slope 2) and a chamfered lip at the top inside (leaving t_i of the wall). With fit clearance c, the foot moves in and the lip out by c each. The foot of an upper tray seats on the lip of the one below; it sinks (b_i − t_i) × slope deep, and nesting works only if t_i < b_i < t.
  4. Floor fillet: a quarter-circle (radius 2, 9 points) where the inside wall meets the floor top, for strength and easy cleaning.
Default Tall tray (57.15 mm)CalculationResult
Foot inset b_i / height1.40 + 0.2 clearance; × slope 21.60 / 3.20 mm
Lip inset t_i / heightmin(0.84, 1.85) − 0.2; (1.85 − 0.64) × 20.64 / 2.42 mm
Nesting sink(1.60 − 0.64) × 21.92 mm
Rib bandz from max(8, 1.6 + 2 + 0.5, 3.2 + 0.5) to 57.15 − 2.42 − 0.58.0 – 54.23 mm
Rib centers57.15 − 7.7 = 49.45, then every 9.6 down while it fits11.05, 20.65, 30.25, 39.85, 49.45
Profile pointsoutside + rim + inside (5 ribs × 13 points each side) + fillet≈ 144

Offset contours and corner fillets

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 δ:

  • Convex corners have radius R (7 mm on the outermost edge); offset inward by δ the radius becomes R − δ, around the same center. Concave (inside) corners of L/T shapes have radius Rc, becoming Rc + δ. Each arc's center sits on the corner's angle bisector, and both edges stay tangent to it.
  • Every corner always emits segs + 1 points (9 by default) whatever its radius, even when the radius shrinks to a minimum of 0.02 mm, so ring k and ring k + 1 have exactly the same number of points in the same order. That's what makes stitching trivial and the shell watertight.
  • Short edges. A fillet of radius r at a corner that turns by θ uses r·tan(θ/2) of each adjoining edge. If two fillets would need more than an edge's length (notches, short custom edges), both are scaled down to fit; when a convex and a concave corner share the edge, the concave one gives way first. Because convex radii shrink and concave ones grow by the same δ as the edges move, the amount of edge each fillet uses works out the same at every inset, so checking once at δ = 0 covers the whole sweep.

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.

Floor openings

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.

  • Grid: pitch px across and py between rows; with stagger, every other row shifts half a pitch. "Keep hex spacing" ties py to px so all neighbors are equally far apart: py = px × √3 ÷ 2. Rows are kept centered instead of drifting sideways.
  • Honeycomb layout (hex and round only): a true hexagonal lattice where the web (solid plastic between neighboring holes) is the same everywhere; hexes are turned 30° so their flats face each other.
  • Keep-outs: an opening is kept only if all of it lies inside the floor with the margin to every wall, to each divider along the length it actually runs, to arc dividers in their ring, and to splice joints (which count as walls). Neighbors must keep the minimum web: the template is inflated by half the web and overlapping openings are rejected.

Dividers

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.

Finger scoops, label tabs and finger notches

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.

  • Walls. The footprint's straight edges are classed by the way their outward normal faces (front, back, left, right) and numbered along that side in the direction the CCW contour runs. That numbering, and a position measured the same way, turn with the tray, so rotating it only changes the side. Scoops and tabs go on walls that face their side within 40° and are at least 10 mm long; a notch can go on any wall.
  • Compartment faces. A scoop or tab runs along the straight part of each such wall at the inner face (between its corner fillets) and, with fixed dividers, along each divider face toward the compartment beyond it. For label tabs each face is split where dividers cross it (and round finger notches), giving one tab per compartment. At a convex corner a piece that reaches the end of its straight part runs on past it (to the sharp corner and 0.5 mm more) and is then trimmed to a vertical prism of the inner wall face set a hair further into the wall than the solid's own back, so tabs and scoops fill the corner fillets. The run-on is a piece of its own butting onto the straight part, trimmed just inside the outer face, so the straight part can still follow the ribs. The trim is a boolean (manifold-3d in the page, OpenCascade common in STEP); until the library has loaded the page shows them stopping at the straight part. At a concave corner the wall curves away, so they stop there.
  • Finger scoop. The section is a quarter-circle fillet of the set radius, from the floor up the wall, clamped to 85 % of the compartment's height, running the face's length. Floor openings keep their margin from the band it covers.
  • Label tab. A shelf whose top sits the set distance below the rim (and no higher than the divider top on a divider face), as deep as set, 1.2 mm thick at the tip, with a 45° underside back to the wall so it prints without supports. With a length set, it sits at the start, centre or end of the compartment (stored along the wall's own direction, so it turns with the tray). A tray stacked on top is flagged if it would sit on the tabs.
  • Overlap into the wall. Each added solid reaches a little way back into the wall so it fuses: rails 0.5 mm, tabs 0.4 mm, scoops 0.3 mm. The depths differ so no two of them share a face plane, which keeps the CAD union valid; tabs end flush on the divider or rail they meet for the same reason.
  • Finger notch. A cutting tool: a U in the plane of the wall, the set width and depth from the rim, its bottom rounded (a full semicircle when the depth allows) and its edges rounded into the rim with the rim radius, swept from 1.5 mm outside the wall to 2.5 mm past its inner face. A divider (or removable plate) that meets that wall within the notch gets a second tool in its own plane: half the same U, from the wall face as far in as the notch is half wide, so it dips to the notch's bottom instead of standing up in the opening. In STEP the tools are cut one at a time (they overlap there), and each removable plate is cut too. The page's mesh has no booleans of its own, so the notches are cut with manifold-3d: each closed shell the tool reaches (body, dividers, rails, scoops, tabs) is welded into an indexed mesh, the tools are subtracted, and the result goes back into the triangle list, still watertight. The library loads in the background; until it has, the Model box shows the notches as loading. STEP subtracts the same tools with OpenCascade.

Building the mesh and checking it

  1. Walls: for each pair of neighboring rings k and k + 1 and each point i, the quad between them becomes two triangles: (Ai, Ai+1, Bi+1) and (Ai, Bi+1, Bi). A plain rectangular Tall tray has about 144 rings of 36 points: (144 − 1) × 36 × 2 ≈ 10,300 wall triangles.
  2. Floor caps: the bottom face (z = 0) and the floor top (z = floor thickness) are polygons with holes. Fast path: ear clipping (three.js 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.
  3. Hole walls: two triangles per edge of each opening, from z = 0 to the floor top.
  4. Orientation: the signed volume (sum over triangles of v₀ · (v₁ × v₂) ÷ 6) must be positive; if it isn't, every triangle is flipped so normals point outward.
  5. Dividers (or the rails of removable ones), scoops and label tabs are added as separate closed shells overlapping the body (slicers union them automatically), each checked and flipped the same way. Finger notches are then subtracted from every shell they reach (manifold-3d). Removable divider plates are a separate mesh.
  6. Watertight check: every edge must be shared by exactly two triangles (edge-manifold). The result, triangle count, hole count, rib count, volume and nesting sink are reported.

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.

Splicing trays to fit the print bed

offset 45° joint (default) zig-zag (45° teeth) wall joints sit apart; glued wall faces are √2 longer than a square cut

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:

  • Offset 45°: like a scarf joint seen from above. Each side wall is crossed on a 45° diagonal and the floor straight between them, so the bending stress at one wall joint is carried by solid wall at the other. The two diagonals lean opposite ways, and each has its own reach (how far in from its wall it runs); the two together are capped at the cut's length.
  • Mirrored 45°: the same, but the second diagonal mirrors the first, so both wall joints are on the same side of the straight part.
  • Zig-zag: 90° teeth whose faces are at 45° to the cut's line. The tooth depth is rounded so a whole number of teeth spans the cut. The line can be turned up to ±45° (beyond that the teeth would fold back on themselves; at exactly 45° one face of each tooth runs square across). The teeth carry on past the tray and are trimmed at the cut's ends, so every corner inside the tray is 90°. A shift slides the teeth along the line; it repeats every two teeth.
  • Straight: a plain cut, turned up to ±60° about its middle.

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.

Export

FormatHow 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.
STEPA 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 positionsOn 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 drawerThe 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).