> Canonical HTML: https://kerfscript.mylasertools.com/ # KerfScript KerfScript is a programming language for laser cutting. Design a part once, then change its size, text or material thickness. It handles cut, score and engrave layers and exports SVG or DXF. - [Try the browser playground](https://kerfscript.mylasertools.com/playground/) - [Browse 96 models](https://kerfscript.mylasertools.com/models/index.md) - [Read the documentation](https://kerfscript.mylasertools.com/docs/index.md) - [Source code](https://github.com/mylasertools/kerfscript) - [npm package](https://www.npmjs.com/package/@mylasertools/kerfscript) The syntax is familiar to OpenSCAD users, but existing files may need changes. See the [OpenSCAD guide](https://kerfscript.mylasertools.com/docs/openscad/index.md). KerfScript 0.7.0 implements language version 0.3. It is available under GNU AGPL 3.0 or a free commercial licence (email hi@mylasertools.com). Designs and generated files remain yours. See [licence details](https://kerfscript.mylasertools.com/licence/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/about/ # About KerfScript KerfScript is a programming language for laser cutting, designed and maintained by [MyLaserTools](https://mylasertools.com). It lets you describe designs in code, adjust dimensions and export SVG or DXF. Layers, kerf compensation and joinery are built in. Version 0.7.0 implements language version 0.3. Read the [version policy](https://kerfscript.mylasertools.com/docs/versions/index.md) and [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). The [source and issue tracker](https://github.com/mylasertools/kerfscript) are public. Install the package with `npm install @mylasertools/kerfscript`. For a problem, open an issue or email hi@mylasertools.com. See the [licence](https://kerfscript.mylasertools.com/licence/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/ # KerfScript documentation Build a part, add engraving, measure kerf and export a cut file. - [Getting started](https://kerfscript.mylasertools.com/docs/getting-started/index.md) - [Layers](https://kerfscript.mylasertools.com/docs/layers/index.md) - [Kerf](https://kerfscript.mylasertools.com/docs/kerf/index.md) - [Parts and sheets](https://kerfscript.mylasertools.com/docs/parts-and-sheets/index.md) - [Assembly verification](https://kerfscript.mylasertools.com/docs/verification/index.md) - [Standard library](https://kerfscript.mylasertools.com/docs/library/index.md) - [Shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md) - [Settings](https://kerfscript.mylasertools.com/docs/customizer/index.md) - [Desktop app](https://kerfscript.mylasertools.com/docs/desktop/index.md) - [Command line](https://kerfscript.mylasertools.com/docs/cli/index.md) - [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md) - [Editors](https://kerfscript.mylasertools.com/docs/editors/index.md) - [AI assistants](https://kerfscript.mylasertools.com/docs/mcp/index.md) - [WebAssembly](https://kerfscript.mylasertools.com/docs/webassembly/index.md) - [Importing SVG](https://kerfscript.mylasertools.com/docs/import/index.md) - [Coming from OpenSCAD](https://kerfscript.mylasertools.com/docs/openscad/index.md) - [Laser software](https://kerfscript.mylasertools.com/docs/compatibility/index.md) - [Versions](https://kerfscript.mylasertools.com/docs/versions/index.md) - [Specification](https://kerfscript.mylasertools.com/docs/specification/index.md) - [Language reference](https://kerfscript.mylasertools.com/docs/reference/index.md) --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/getting-started/ # Getting started Make a small sign with two hanging holes and engraved text. You can follow this guide in the [playground](https://kerfscript.mylasertools.com/playground/) without installing anything. ## Your first part Clear the playground editor and enter: ```kerf rect([80, 50], r = 4); ``` This draws an 80 × 50 mm rectangle with 4 mm corner radii. KerfScript uses millimetres for lengths and degrees for angles. Shapes go on the cut layer unless you assign another layer. ## Holes Replace the rectangle with this code: ```kerf difference() { rect([80, 50], r = 4); translate([10, 25]) circle(d = 5); translate([70, 25]) circle(d = 5); } ``` `difference()` subtracts the two circles from the rectangle. Each hole is 5 mm across, centred 10 mm from a side and halfway up the sign. `translate([x, y])` moves a shape. The x axis points right and the y axis points up. A rectangle starts at `[0, 0]`; use `center = true` to centre it on that point. ## Engraving and scoring Add a scored border and engraved text: ```kerf difference() { rect([80, 50], r = 4); translate([10, 25]) circle(d = 5); translate([70, 25]) circle(d = 5); } score() translate([20, 10]) rect([40, 30], r = 2); engrave() translate([40, 25]) text("Oak", size = 10, halign = "center", valign = "center"); ``` `score()` marks the border's outline. `engrave()` fills the letters. Both stay within the sign's cut outline, excluding the holes. The playground supplies a font for this example. See [Layers](https://kerfscript.mylasertools.com/docs/layers/index.md) for custom layers and guide lines. ## Settings Give dimensions names so you can change them without editing every shape: ```kerf width = 80; // [40:5:200] height = 50; // [30:5:120] label = "Oak"; difference() { rect([width, height], r = 4); translate([10, height / 2]) circle(d = 5); translate([width - 10, height / 2]) circle(d = 5); } engrave() translate([width / 2, height / 2]) text(label, size = 10, halign = "center", valign = "center"); ``` The comments create sliders for width and height. `[40:5:200]` means a minimum of 40, a step of 5 and a maximum of 200. `label` becomes a text field. The text and holes stay centred as the dimensions change. In the playground, changing a control also updates its value in the code. See [Settings](https://kerfscript.mylasertools.com/docs/customizer/index.md) for menus, groups and captions. ## Kerf The beam removes some material along each cut. To account for that width, add your measured kerf near the top of the program: ```kerf $kerf = 0.15; ``` Here, `0.15` is an example in millimetres. Use the [calibration square](https://kerfscript.mylasertools.com/models/calibration-square/calibration-square.kerf) to measure a value for your machine and material. [Kerf](https://kerfscript.mylasertools.com/docs/kerf/index.md) explains the measurement and how it affects the exported outlines. ## Getting the file Choose **SVG** or **DXF** in the playground, then open the file in your laser software. Assign the cut, score and engrave layers their power, speed and operation order. In DXF, set engraved areas to fill. If you used `$kerf`, leave kerf compensation off in the laser software to avoid applying it twice. The [import guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md) covers sizes, layers and tested programs. Download **.kerf** to keep an editable copy of the design. ## Next - [Layers](https://kerfscript.mylasertools.com/docs/layers/index.md): separate cutting, scoring and engraving. - [Parts and sheets](https://kerfscript.mylasertools.com/docs/parts-and-sheets/index.md): arrange several parts by material. - [Coming from OpenSCAD](https://kerfscript.mylasertools.com/docs/openscad/index.md): adapt an existing 2D design. - [Language reference](https://kerfscript.mylasertools.com/docs/reference/index.md): look up syntax and functions. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/layers/ # Layers Layers separate cutting, scoring and engraving in the exported file. Use them to give different parts of a design different operations in your laser software. Unassigned shapes go on the `cut` layer. ## The built-in layers | Layer | Wrap shapes in | Colour in the file | Intended operation | | --------- | -------------- | ------------------ | -------------------------- | | `engrave` | `engrave()` | black `#000000` | Fill an area | | `score` | `score()` | blue `#0000ff` | Mark a line or outline | | `cut` | `cut()` | red `#ff0000` | Cut through the material | | `guide` | `guide()` | light blue | Show construction geometry | This coaster has a cut outline and hole, an engraved ring and a scored border: ```kerf difference() { circle(d = 90); circle(d = 6); } engrave() difference() { circle(d = 74); circle(d = 68); } score() circle(d = 84); ``` The layer describes the operation. Set its power, speed and passes in your laser software. Place `#` or `%` before a module call to move it to the guide layer. Guides are excluded from the output unless you request them with `--guides` on the command line or `guides: true` in the API. ## Areas and lines Shapes such as `rect()`, `circle()` and `text()` define areas. On an engrave layer, those areas are filled. On cut and score layers, their outlines become paths. `line()`, `polyline()`, `arc()` and `living_hinge()` produce lines. They remain lines on every layer. A living hinge, for example, is a pattern of open cuts within a part. Use `stroke(width)` to turn a line into an area of a specified width. Use `outline()` to turn an area's boundary into lines. ### Overlapping shapes merge The areas inside one `score()` are merged first, like a `union()`, and only the outline of the result is scored. Two nested circles become one disc, so this scores a single ring: ```kerf circle(d = 60); score() { circle(d = 50); circle(d = 40); // inside the first circle: merged away } ``` To score every shape's own outline, turn each one into lines first with `outline()`. Lines are never merged, so this scores both rings: ```kerf circle(d = 60); score() { outline() circle(d = 50); outline() circle(d = 40); } ``` One `outline()` around both circles would not help: it merges them before it traces them. The same rule holds on the cut layer and on custom layers. Use `difference()` when you want the area between two outlines, such as a ring to engrave. ## The material Closed shapes on a part's cut layer define its material. KerfScript clips engraving, scoring and open cut lines to that area, excluding holes. Start with a cut shape when adding marks to a part. If the cut shapes form separate pieces, KerfScript reports a warning. Check for a missing connection or define the pieces as separate parts. ## Your own layers Use `layer(name, mode, color)` when you need another operation, such as a second engraving depth: ```kerf rect([60, 40]); layer("deep", mode = "fill", color = "#00aa00") translate([10, 10]) rect([20, 20]); engrave() translate([40, 20]) circle(d = 12); ``` The rectangle and circle use separate engraving layers. You can assign each a different power in your laser software. Supported modes are `"cut"`, `"line"`, `"fill"` and `"guide"`. ## The back face `engrave(side = "back")`, `score(side = "back")` and `layer(name, side = "back")` mark the face underneath, made after the parts are turned over. Draw the marks where they are on the part seen from the top; text reads the right way round on the back. They go into each sheet's back job, turned over left to right, and not into the front file. ## Painting in order `paint()` lays shapes like paint, each over the ones before: every child loses the area of the children after it. Give it a layer for each child, and a picture in several tones comes out with no two layers overlapping: ```kerf layer("light", mode = "fill", color = "#c8a070"); layer("dark", mode = "fill", color = "#000000"); part("tile") { rect([60, 60]); paint(["light", "dark"]) { circle(d = 50); translate([30, 30]) circle(d = 20); } } ``` A child given the layer `""` is left out, but still covers the ones before it: a bare patch. ## Printing on parts To print on the parts and then cut them, `layer(name, mode = "fill", bleed = 2)` lets the printed layer run 2 mm past each part's edge, so the cut never leaves a bare rim. `$registration = true` puts marks in three corners of every sheet, on the `registration` layer, for a print-and-cut machine to line the cut up with the print. ## Order in the file KerfScript writes layers in this order: engrave, custom fill layers, custom line and cut layers, score, cut, then guide, so a line is never hidden under a fill. `layer(name, order = n)` moves a custom layer among the others, lowest first. Within a cut layer, inner paths precede outer outlines. Laser software may reorder these paths. Check its job preview so engraving runs before cutting and holes are cut before the surrounding part. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/kerf/ # Kerf Kerf is the width of material removed by a laser cut. Allowing for it helps parts finish at the intended size and joints fit together. Measure it for the material and cutting settings you use. ## Setting it Set `$kerf` in millimetres: ```kerf $kerf = 0.15; ``` With this value, KerfScript moves an outer cut path outward by 0.075 mm and a hole's path inward by the same amount. The offset compensates for material removed on either side of the beam. Only closed cut outlines receive the offset. Engraving, scoring and open cuts such as living hinges keep their original paths. To use a value for a particular material, set it inside `material()`: ```kerf material(thickness = 4, kerf = 0.12, name = "birch") { part("side", qty = 2) finger_rect([100, 60], "out"); } ``` On the command line, `--kerf 0.1` overrides the value for every part. Apply kerf compensation in either KerfScript or your laser software, once per job. ## Measuring your kerf 1. Cut the [calibration square](https://kerfscript.mylasertools.com/models/calibration-square/calibration-square.kerf) with `$kerf = 0`. Leave any offset in your laser software at zero too. 2. Measure the square across the middle with calipers. 3. Subtract that measurement from 100 mm. A square measuring 99.82 mm gives a kerf of 0.18 mm. 4. Set `$kerf = 0.18` and cut another sample. Check the outside against 100 mm and the hole against 20 mm, then adjust the value if needed. Repeat the measurement when the material or cutting settings change. Thick material can have a different cut width at its top and bottom faces. The [kerf gauge](https://kerfscript.mylasertools.com/docs/library/index.md#calibration) in the calibration library measures it another way, without calipers on a square: it cuts strips in a frame with single lines. Push the strips to one side and divide the gap they leave by the number of strips plus one. ## Checking a fit After calibrating kerf, measure your sheet's thickness and set `$thickness` to that measurement. The nominal thickness on a material label can differ from the sheet itself. The [kerf and fit test](https://kerfscript.mylasertools.com/models/kerf-test/kerf-test.kerf) cuts slots slightly narrower and wider than `$thickness`. Try a scrap from the same sheet in each slot. A positive label means a wider slot; a negative label means a narrower one. Choose the fit you need and use the result to refine your dimensions. Both kerf and sheet thickness affect the fit. A tight or loose slot alone does not tell you which value needs adjusting. For finger joints, `joint_test()` in the calibration library cuts a pair of pieces for each of a few kerf values; the pair that holds without glue has the kerf to use. The [calibration set](https://kerfscript.mylasertools.com/models/calibration-set/calibration-set.kerf) cuts the gauge, the slots and the joint pairs together. ## Limits - Compensation can close a hole or notch narrower than the kerf. KerfScript warns when a hole disappears. - Narrow bridges and fingers may be fragile or char during cutting even when their dimensions are correct. Test small features in your material. - Compensation adjusts the 2D outline. It does not remove taper from the cut face in thick material. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/parts-and-sheets/ # Parts and sheets Use parts to name the pieces in a design, set quantities and assign materials. KerfScript lays them out on sheets, grouping parts that use the same material and thickness. ## Parts ```kerf part("side", qty = 2) rect([100, 60]); part("base") rect([100, 100]); ``` This creates two side panels and one base. Each `part()` has its own layers and appears in the parts list with dimensions and cut length. The default quantity is one. Shapes outside a `part()` are collected into a part named `part`. Transforms and operations such as `translate()`, `difference()` and `offset()` can wrap a part and change all of it. Layer wrappers such as `score()` and `engrave()` go inside the part instead, since a whole part on the score layer would have nothing to cut: ```kerf part("tag") { rect([60, 30], r = 3); score() translate([5, 5]) rect([50, 20]); } ``` ## Assembly Give a part a place in 3D and apps can show the design put together. `rotate` turns the part and `at` moves it. The turn happens first, the same as OpenSCAD's `translate(at) rotate(r)`. Each part is drawn flat with its thickness standing up along +Z. ```kerf material(thickness = 3) { part("base", at = [0, 0, 0]) rect([100, 60]); part("wall", at = [0, 3, 3], rotate = [90, 0, 0]) rect([100, 40]); } ``` A part with copies takes one placement for all of them, or a list with one for each copy: ```kerf part("leg", qty = 4, at = [[0, 0, 0], [80, 0, 0], [0, 40, 0], [80, 40, 0]], rotate = [90, 0, 0]) rect([20, 60]); ``` `box()` from `kerf/box` places its panels itself, so every box assembles. Parts without `at` or `rotate` lie flat beside the assembled view, each copy in rows to its right, and the render names them in a warning. When placed parts run into each other, the render warns which two collide. That is what a finger edge set to `"out"` where it should be `"in"` looks like, or a part placed one thickness off. Joints that interlock exactly do not count, and neither does an overlap smaller than about a cubic millimetre, the kind of nick a laser kerf or a press fit takes up. A tab half a millimetre out of its slot is well over that and gets a warning. The warning says roughly where in the assembly the two meet. ### Turning a part any way `rotate` turns about x, then y, then z. To say where a part's own axes point instead, use `orient(x, z)`: the part's x axis along the first direction and its top face towards the second. ```kerf // A panel leaning back 15 degrees, its top face to the front part("back", at = [0, 60, 3], rotate = orient([1, 0, 0], [0, -cos(15), sin(15)])) rect([100, 80]); ``` `rotation([x, y, z])` gives the matrix of a turn, matrices multiply with `*`, and `euler(m)` turns a matrix back into angles for `rotate`: `euler(rotation([0, 0, 30]) * rotation([90, 0, 0]))` stands a part up, then turns it 30 degrees about z. ### Placing a group `place(at, rotate) { ... }` moves and turns everything inside it, with each part's own placement inside the group's. Build a drawer, a crate or a whole library box at the origin and place it where it goes: ```kerf use place(at = [20, 10, 30], rotate = [0, 0, 90]) box([80, 60, 40], name = "crate"); ``` ### Stacks, mirrored pairs and glued layers - `stack = 3` glues three layers at each placement, one thickness apart: thick feet, posts and blocks from thin sheet. The parts list says how many make a stack. - `mirror = true` cuts the part mirrored left to right. A lopsided end plate's other half is the same part mirrored, and its engraving still reads the right way round. - `glue = "top plate", face = "top"` lays a part on another part's face, its `at` and `rotate` measured on that face. Move the plate, and what is glued on it moves too. A stack glued under a part (`face = "bottom"`) grows down, away from it. ```kerf part("seat", at = [0, 0, 40]) rect([80, 80], r = 6); part("cushion", glue = "seat", at = [6, 6, 0]) rect([68, 68], r = 10); part("leg", qty = 4, stack = 4, at = [[0, 0, 0], [70, 0, 0], [0, 70, 0], [70, 70, 0]]) rect([10, 10]); ``` ### Slots from the assembly Draw the tabs on one part and let the part they go into cut its own slots: `slots = ["back", "divider"]` cuts, through this part, the cross-section of those parts wherever their material passes through it in the assembly, `clearance = 0.1` wider. It works at any angle, so a back leaning 15 degrees gets slots as long as its tabs' slanted cross-section, and a strip bent by a living hinge cuts slots for the tabs on its straight runs. A part must reach 1 mm into it (or half way through a thinner one) to cut a slot, and a blind tab's slot goes through: one that only dips into the surface is misplaced, and the collision check reports it rather than a slot hiding it. The layers of a stack each cut the slots that pass through them. When they differ, as a bench top of `stack = 2` taking the legs' tabs in its lower layer and a vice's in its upper one, the stack becomes a part per layer, `top, layer 1` and `top, layer 2`, so blind joints don't show through. ```kerf part("shelf", slots = ["back"]) rect([100, 60]); part("back", at = [0, 55, 3], rotate = [75, 0, 0]) { rect([100, 60]); for (x = [20, 70]) translate([x, -6]) rect([10, 6.01]); } ``` Parts that only touch cut nothing: a wall standing on a shelf without tabs leaves it whole. ### Bends A living hinge bends a part, and the assembled view can show it bent. `bend = [x0, x1, angle]` bends the part along its x between x0 and x1, the far end folding up towards its top face by `angle` degrees (down for a negative angle); give a list for more than one bend. The hinge's middle layer keeps its length, so the radius follows from the hinge's width. Parts glued on a bent part take `on = "name"` and move with the run they sit on. `flex_strip()` in `kerf/hinge` works out the bends of a strip that follows a profile. ```kerf use // A cover that wraps round a 20 mm spine part("cover", bend = [[100, 100 + bend_length(10, 90), 90], [130, 130 + bend_length(10, 90), 90]]) flex_rect([260, 150], [100 + bend_length(10, 90) / 2, 130 + bend_length(10, 90) / 2], width = bend_length(10, 90)); ``` The collision check takes each flat run of a bent part on its own. ### Numbered parts `$label_parts = true;` numbers the parts 1, 2, 3 in the order they are made and engraves each number small on the part's back face, where it has the most room, so a buyer can tell alike pieces apart once they are out of the sheet. The numbers come in the back job (`design-back.svg`), and `kerf check` puts them in the parts list and after each step: `step 2: Glue the lock on the front (parts 6, 7, 8)`. JavaScript gets each part's `label`. A part with no room for its number warns. ### Steps and hardware `step("Glue the sides to the bottom") { ... }` makes an assembly step of the parts inside it. The steps keep the order they first run in. The 3D view in the playground and on the model pages builds the model a step at a time, each step's parts coming down into place. Parts outside any step are there from the start. A step inside a step is a sub-step, named after both: a chair module with its own `step("Glue the seat")`, placed inside `step("Add the chair")`, adds the step "Add the chair: Glue the seat". So a piece of furniture builds step by step on its own and inside a room. `step("Add the chair", substeps = false)` takes everything inside into the one step instead. `hardware(kind, from, to, size)` lists what else the model needs: a dowel from one point to another, a screw, a magnet, a strip of paper. `size` is a diameter, or `[w, h]` for a flat piece. `kerf check` lists the hardware with the parts, and the 3D view shows it. The kinds are these; a colour (`pink`, `yellow`, `red`, `green`, `blue`, `orange`, `purple`, `black`, `grey`) works too, and any other kind warns and shows grey. JavaScript gets the list as `HARDWARE_KINDS`. | Kind | What it is | Shown | | --------- | ------------------------- | ---------------- | | `dowel` | a wooden dowel or axle | matt, #e2c48f | | `steel` | a steel rod, pin or axle | metal, #a7adb3 | | `brass` | a brass rod, tube or pin | metal, #c9a227 | | `screw` | a screw | metal, #9aa0a6 | | `bolt` | a bolt | metal, #9aa0a6 | | `nut` | a nut | metal, #9aa0a6 | | `magnet` | a disc or block magnet | metal, #55585c | | `paper` | a sheet or strip of paper | matt, #fbfaf6 | | `card` | card stock | matt, #f4f1e8 | | `felt` | felt pads or lining | matt, #6f6a64 | | `elastic` | an elastic band or cord | matt, #262626 | | `cord` | cord, string or thread | matt, #b23a3a | | `ribbon` | ribbon or tape | matt, #c2485a | | `wire` | wire | metal, #b87333 | | `led` | an LED or a strip of them | glowing, #fff4c2 | | `white` | anything white | matt, #f7f7f4 | ```kerf step("Stand the walls in the base") { part("base", at = [-30, -40, 0]) rect([60, 80]); for (y = [-40, 37]) part(str("wall ", y), at = [-30, y + 3, 3], rotate = [90, 0, 0]) rect([60, 40]); } step("Slide the axle through the walls") hardware("dowel", [0, -45, 25], [0, 45, 25], 6); ``` ## Materials `material()` sets the thickness, kerf and material name for its parts: ```kerf material(thickness = 3, kerf = 0.15, name = "plywood") { part("front") rect([100, 60]); part("back") rect([100, 60]); } material(thickness = 2, kerf = 0.1, name = "acrylic") { part("window") rect([80, 40]); } ``` The two plywood panels share a sheet. The acrylic window goes on a separate sheet. Each part records the `$thickness`, `$kerf` and `$material` values in effect where it is defined. ## Sheets Set the available sheet size and spacing in millimetres: ```kerf $sheet = [300, 200]; $gap = 4; ``` With the default `$layout = "pack"`, KerfScript arranges copies of each part in rows, leaving the specified gap. It rotates a part 90° if that lets it fit the sheet. The default sheet is 600 × 400 mm, with a 3 mm gap between parts and at the sheet edges. A part larger than the sheet produces a warning. Set `$layout = "none"` to keep the positions from your code instead of packing the parts. The CLI `--margin` option and API `margin` option let you set the edge spacing separately from the gap between parts. Your laser's bed can be set once for a project: `"sheet": [600, 400]` in the project's `kerf.json` is the sheet for every design in it that does not set `$sheet`. With `$nest = true` (or `--nest`), small parts go into the holes of bigger ones before the rows are packed: the rings of a stacked vase or globe each inside the next, washers inside a wheel. A part goes into a hole only where its outline, `$gap` all round, fits. ## One file per sheet A combined SVG stacks all sheets in one preview. For the material example above, export the plywood and acrylic sheets separately and cut each with the matching material settings. - On the command line, add `--split` to write `design-1.svg`, `design-2.svg` and so on. `kerf check` lists each sheet's material and thickness. - In JavaScript, `result.sheets` provides the SVG, DXF, material and thickness for each sheet. DXF exports contain one sheet per file. ## The back face Marks for a part's underside go on `engrave(side = "back")` or `score(side = "back")`, drawn where they are on the part seen from the top. They are left out of the sheet's file and go into a second job, the sheet turned over left to right: `design-back.svg` from the command line, `sheet.back` in JavaScript. Cut and engrave the front, turn the whole sheet over without moving the parts in it, line it up on the same corner and run the back job. ```kerf part("coaster") { circle(d = 90); engrave() text("TEA", size = 16, halign = "center", valign = "center"); engrave(side = "back") text("made for you", size = 6, halign = "center"); } ``` --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/verification/ # Assembly verification `kerf verify` checks declared requirements against the evaluated geometry of a design: fit, retention, installation, support, stability and operating motion for rigid sheet parts and cylindrical/box hardware. Each claim names its scope and assumptions. Checks measure the evaluated geometry independently of the formulas that generated it. ```sh kerf verify model.kerf --spec model.verify.json -o report.json ``` The specification is JSON data. The command never imports or executes a sibling JavaScript file. Supported options are `--spec`, `-o`/`--out` and `--font`. Parameters belong in each specification configuration. `kerf check --sweep` remains available for ordinary warning/parameter sweeps. ## Reading the result `--summary` also writes a triage to standard error: every claim that did not pass, failures first, grouped by status and diagnostic code, each with its configurations, the bodies involved and the first measurements. The report and the exit code are unchanged; `triage()` and `triageText()` give the same from JavaScript. The command returns `{ "kind": "report", "report": ... }`, or `invalid-spec` / `invalid-snapshot` with input issues. Each claim has a status, diagnostic code, configuration, implicated part/hardware references, measurements, assumptions and coverage. Exit codes are: | Code | Meaning | | ---- | ------------------------------------------------------------------------------- | | 0 | Every required claim passed within its reported scope | | 1 | A required claim or required design configuration failed | | 2 | A required claim is unverified, input is invalid, or a report cannot be written | A `pass` applies to the listed requirements and configurations. Reports also list uncovered capabilities; a passing fit alone does not establish retention or motion. `strength` currently returns `unverified`. Material capacity, adhesive strength, friction, wear, torque, actual gear/cam driving contact and flexible-body dynamics need separate evidence. Unsupported cases and exhausted checking budgets remain unverified. Missing cutting metadata, open cutting lines affecting rigid material, bends, legacy `echo("hardware", ...)`, compiler warnings, exhausted budgets and numerical boundary cases cannot produce a required geometric pass. Cases refused by the renderer remain failed required configurations; they are not dropped from coverage. ## A glue contact specification This example checks a patch's bottom face against a host's top face. Measurements use millimetres and square millimetres, after all model scaling and placement. ```json { "schemaVersion": 1, "model": "Mounted decoration", "configurations": [{ "id": "default" }], "claims": [ { "id": "decoration-contact", "kind": "glue-contact", "a": { "part": "host" }, "b": { "part": "patch" }, "faceA": "top", "faceB": "bottom", "minimumArea": 110 } ] } ``` Parts bind by unique name and copy, with `copy` defaulting to 1. Hardware binds by unique native `hardware(..., label = "shaft")` labels. A missing or ambiguous reference is an input error. Array positions cannot identify hardware. Claim IDs starting with `$` are reserved for verifier diagnostics. Configurations may contain `params`; `kerf verify` refuses a name the model does not assign (`unknown-parameter`, exit 2), so a typo cannot verify the defaults instead. Claims may restrict their `configurations` to a subset, and may set `required: false` for informational checks. At least one claim must be required. Optional checks never hide a required failure. A revolute `range` may span more than half a turn: a pose gives the hinge's angle relative to the first configuration up to whole turns, and the check accepts it when some whole turn of it lies in the range. Captured-shaft stops pair with the bearings by their position along the shaft, in any order. ## Supported contracts | Kind | Fields in addition to `id` and `kind` | What is measured | | --------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `glue-contact` | `a`, `b`, `faceA`, `faceB`, `minimumArea` | Opposing coplanar nominal faces and overlap after subtracting holes | | `tab-slot` | `tab`, `receiver`, `region: [x0,y0,x1,y1]`, `clearance: [min,max]`, `flushTolerance`, `minimumLigament` | Rectangular tongue, closed rectangular slot, both directional total clearances, side gaps, through engagement and remaining edge material | | `captured-shaft` | `shaft`, two `bearings`, `stack`, two `stops`, `clearance`, `endplay`, `minimumOverlap`, `maximumStopGap` | Coaxial bore fit, bearing engagement, ordered axial stack, endplay and positive end capture | | `material-assignment` | No extra fields | A stock profile exists for every sheet part and contains its design thickness | | `cut-invariant` | At least two configurations | Actual local cut regions/lines, material, thickness, compensation and inventory stay fixed across the declared poses | | `rigid-clearance` | `minimumGap`, optional `bodies` | Clearance between actual extruded cut regions and native hardware; defaults to the whole inventory | | `retention` | `roots`, `links`, optional `removable` | Every permanent component has checked attachment geometry and installed fastening leading to a supported root | | `assembly-access` | `initial`, `steps`, `minimumGap`, optional `scope`, `maxIntervals` | Continuous straight insertion and optional tool paths against already installed bodies | | `assembly-support` | `plan`, `retention`, optional `holds` | Previously installed components remain retained or explicitly held at every step; final temporary holds are released | | `stability` | `supports`, `loads`, `ground`, `gravity`, `minimumMargin`, optional `retention` | The declared gravity line lies within the actual ground contact support polygon | | `joint-constraints` | `bodies`, `roots`, `joints`, `expectedDof` | Fixed/revolute/prismatic/gear pose constraints and constraint-Jacobian mobility at the declared configurations | | `motion-clearance` | `from`, `to`, `moving`, `minimumGap`, optional `constraints`, `maxIntervals` | Continuous clearance for explicit rigid translations or rotations about fixed world axes | `tab-slot.region` selects the tongue in the tab's own coordinates. It cannot be an arbitrary crop out of a larger plate: at most one edge may continue into the parent, along the insertion direction. Shapes outside this limited contract remain unverified. A successful fit does not establish fastening or retention. `captured-shaft` supports cylindrical hardware and circular polygon bores whose panel normals follow the shaft direction. `stack` lists the spacers/rotors between the two bearing faces. Each stop is a hardware reference with an `attachment`: ```json { "body": { "hardware": "nut" }, "attachment": { "kind": "threaded", "thread": { "hardware": "usable thread envelope" }, "minimumEngagement": 3, "secured": true } } ``` The other supported attachment is `{ "kind": "integral" }`, for a head at the shaft end. `{ "kind": "unknown" }` leaves fastening unverified. Integral purchase construction and installation of matching threaded hardware are explicit assumptions. A rendered collar alone does not prove fastening. Hardware dimensions are nominal in this implementation; thread capacity, clamp force, resistance to loosening remain separate properties. Check the rotor's full swept envelope with `motion-clearance`. Working synthetic examples are in `test/fixtures/verification/`, including a captured-shaft specification, a fully contracted glue coupon, two material redesigns and a slider path. These are geometric test fixtures, not rated fastening designs. ## Retention and installation A retention root identifies a body whose support is assumed. Each link references a passed geometric contract and states the fastening actually installed: ```json { "id": "attached", "kind": "retention", "roots": [{ "part": "host" }], "links": [{ "claim": "decoration-contact", "fastening": "glued", "installed": true }] } ``` `glued` means a cured fixed adhesive bond is assumed at measured glue contact. `captured` uses a passed shaft-capture contract. A tab fitting a slot does not prove a lock: labelling it `locked` leaves retention unverified. Every permanent native part and hardware item must reach a root. Explicit `removable` items are excluded from this claim and still need their own support case. `assembly-access.initial` contains zero or one already supported body. Every other scoped body must occur exactly once in `steps`. Each step has `id`, `part`, `from: [dx,dy,dz]` (the starting displacement from its final pose) and `held: true` for the operator or fixture holding the incoming part. The checker moves it in a straight line to its final position, testing all already installed bodies. A gap of zero allows nonpenetrating terminal contact; no whole body collision exemptions are applied. Rotation/flex insertion and automatic assembly planning are unsupported. Optional `tool: {from, to, diameter, travel}` describes a cylindrical tool at the working position and its starting displacement `travel`. Its approach is checked after the incoming body is installed, including against that body. The envelope must describe the tool's free space accurately. `assembly-support.plan` references the access claim, and `retention` references the final retention claim. Each installation step can name geometric contract IDs in `fasten`; those connections become effective only after all their bodies are installed. Optional `holds: [{step, bodies}]` declare temporary support at each step boundary. Incoming `held: true` covers insertion only. Earlier loose bodies must also be held until fastening, and the final assembly must stand without temporary holds. Adhesive installation/cure timing remains an explicit assumption. For `stability`, list a positive `mass` for every body in `loads`, with optional local `center: [x,y,z]`. Otherwise the checker uses a uniform sheet or hardware centroid, subtracting holes. List the bodies touching the horizontal `ground` plane in `supports`; `gravity` is a downward world vector. Multiple loads require a passed retention claim covering every load. The checker rejects ground penetration and measures the gravity-line margin within the convex hull of actual contacts. Sliding resistance, impact and structural capacity are separate properties. ## Mechanisms and continuous motion `joint-constraints` declares ideal connections between body references `a` and `b`: | Joint | Additional fields | | ----------- | ------------------------------------------------------------------------------------------------------------------------ | | `fixed` | No extra fields; relative transform must match the first declared configuration | | `revolute` | Local `anchorA`, `anchorB`, `axisA`, `axisB`; optional `range: [min,max]` in degrees relative to the first configuration | | `prismatic` | Local `axisA`, `travel: [min,max]` relative to the first configuration | | `gear` | Local `axisA`, `axisB`, positive `ratio`; angles satisfy `angleB + ratio * angleA = 0` | Roots stay fixed. The constraint Jacobian establishes the number of independent instantaneous degrees of freedom for at most 64 scoped bodies. Unintended free bodies, separated anchors, wrong guide motion and conflicting mobility fail. Redundant constraint rows are reported; singular ranks remain unverified. Gear pose deltas use the principal quarter-turn branch; add intermediate configurations for larger pose changes. The ideal joints do not establish physical bearings, retainers or driving tooth contact. Native `glue` and `stack` relationships add fixed constraints after contact checks. Installed measured adhesive links also add fixed constraints. A glued decoration cannot become a rotor simply because an animation turns it. Motion uses one analytic world-space trajectory per moving body. Every unlisted body stays fixed. For example: ```json { "id": "turn", "kind": "motion-clearance", "from": "closed", "to": "open", "constraints": "hinge", "moving": [ { "body": { "part": "arm" }, "trajectory": { "kind": "rotate", "origin": [0, 0, 0], "axis": [0, 0, 1], "angle": 45 } } ], "minimumGap": 0.2 } ``` The other trajectory is `{ "kind": "translate", "by": [dx,dy,dz] }`. All controls share phase 0 to 1. The destination must match the rendered endpoint, cut geometry and inventory must stay fixed, and installed bonds must move together. With `constraints`, both endpoints must pass that joint claim and the analytic path must obey its supported joint tree, travel stops and gear rates. General closed linkages and composed moving-axis constraints remain unverified. Continuous clearance uses convex material decomposition preserving holes, exact box envelopes, and inner/outer cylindrical bounds. Separating-axis bounds and adaptive interval subdivision include all intermediate poses through a conservative displacement bound. Approximation boundaries, unresolved contacts and exhausted budgets return `unverified`. The report records a failing phase when available. This proves the authored analytic path; arbitrary parameter expressions between rendered poses do not acquire a continuous guarantee. Add a separate static clearance claim to cover pairs that stay fixed throughout the path. ## Material profiles and frozen cuts Optional `materials` map the evaluated material names to measured stock profiles: ```json { "plywood": { "thickness": [2.9, 3.1], "kerf": [0.12, 0.18], "edgeError": 0.02, "lot": "measured sheet A" } } ``` `thickness` and `kerf` are actual manufacturing intervals. `edgeError` is an additional nonnegative uncertainty per cut edge. The snapshot records the design's compensation kerf separately. Profiles do not redraw a slot for each possible actual thickness. Larger actual kerf enlarges an internal opening and shrinks an external tongue relative to its compensated design. Tab/slot checks bound dimensional fit at the captured placements. The receiving panel's actual maximum thickness also controls shoulder interference and through engagement. Shaft stack checks bound endplay with fixed bearing locations; varying bearing thickness changes retainer seating and currently makes capture unverified. Profiles do not provide general tolerance propagation through an arbitrary mechanism. Glue-contact claims remain nominal geometric claims even when profiles are present. A redesign for a different stock size is a separate configuration, producing a new set of cuts. Global `materials` profiles apply by default. Each configuration may override them with its own `materials` map, so 3 mm and 4 mm redesigns can have different measured lots. Choose supported configurations deliberately; finite configurations and pose samples do not prove a continuous parameter range. Apply `cut-invariant` only to operating poses within one manufactured design, not across intentional stock redesigns. Continuous clearance and static stability currently use nominal geometry; profile intervals apply to the supported fit/stack checks. ## JavaScript API ```js import { render, snapshotFromRender, verify, verificationIsCurrent, } from '@mylasertools/kerfscript'; const rendered = render(source, { params, analysis: true }); const snapshot = snapshotFromRender(rendered, { configuration: 'default', params, provenance: { sourceHash, engineHash, // SHA-256 of exact source and loaded runtime artifact dependencies: {}, // logical source names -> SHA-256 of resolved bytes fonts: {}, // font names -> SHA-256 of actual font bytes }, }); const result = verify(specification, [snapshot]); if (result.kind === 'report') { const current = verificationIsCurrent(result.report, specification, [snapshot]); } ``` The application supplies actual fingerprints; the portable core performs no file access or hashing. Missing provenance makes the overall result unverified. The CLI records source, loaded engine artifact, resolved external dependencies and fonts itself. Reports retain exact parameters and the resolved specification, so a cached result can be checked against changed inputs. A package version alone cannot identify an edited checkout. Before reusing an archived report, capture current fingerprints and call `verificationIsCurrent(report, specification, snapshots)`. Source, runtime, dependency, font, parameter, render-setting or requirement/profile changes make it stale. This checks supplied fingerprints; applications must hash actual input bytes. `analysis: true` captures nominal cut regions and cut lines before toolpath kerf compensation. It is opt-in and does not affect cutting output. The snapshot copies and freezes the geometry; later mutations of a render result cannot change it. Without analysis, unavailable cut lines remain `null`, distinct from a known empty list. Verification never reconstructs authoritative cuts from the viewer's marks. `verify(spec, snapshots, limits)` accepts `maxPoints` (default 200000), `maxClaims` (1000), `maxConfigurations` (100) and `maxOperations` (2000000). Motion/access claims may set `maxIntervals` from 1 to 10000 (default 1000). The command also bounds cases before rendering. The point budget includes the cylindrical/box hardware envelopes used by the checker. Coordinate limits are 1000000 mm in local cut geometry and 10000000 mm for placement translations. Linear acceptance predicates use a 0.001 mm uncertainty band; glue area uncertainty scales with boundary length. Near-boundary results are unverified. Zero-gap nonpenetrating terminal contact is supported for straight insertions and fixed relative envelopes. Geometry invariance compares normalized exact coordinates, not bounding dimensions or rounded cut lengths. The verification API is available in the JavaScript build; the WASI JSON command surface has not yet gained a `verify` operation. ## Previewing findings The site's assembly viewer exposes `view.verification(report, configuration)`. It lists failures/unverified claims and highlights implicated part copies and hardware. `createAssemblyView(element, {onVerificationSelect(finding) { ... }})` lets the host select a configuration or a reported failing `measurements.phase`. Installation findings also identify `assemblyStep`. Every `show()` clears previous diagnostics; the host should check report freshness before attaching a report to new inputs. The viewer does not display a global certification badge. `poseAssembly(rendered, moving, phase)` returns a render result with the exact analytic motion applied to its assembly and hardware. It preserves the original render result and cut files, so a diagnostic preview can show the same path that was checked. It changes the preview pose only; rerender and recapture the source to verify a new manufacturing configuration. Run the public paired fixtures after building: ```sh npm run build npm run verify:fixtures node scripts/verify-models.mjs path/to/contracts path/to/reports ``` --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/library/ # Standard library KerfScript comes with libraries for the parts most laser-cut designs need: joints, hinges, dividers, hanging holes, boxes and test pieces. They are built into every KerfScript renderer: the playground, the desktop app, the command line and the JavaScript and WebAssembly libraries. A design that uses them works everywhere without extra files. ```kerf use part("shelf") tab_rect([200, 60], [false, true, false, true]); ``` `use` brings in a library's modules and functions. Its variables stay inside it, and a module or function of your own with the same name is used instead of the library's. Editors with the [language server](https://kerfscript.mylasertools.com/docs/editors/index.md) show each module's description when you hover over it. | Library | Makes | | ---------------------------------- | -------------------------------------------------------------- | | [`kerf/box`](#boxes) | Finger-jointed boxes and dividers that slide into them | | [`kerf/tabs`](#tabs-and-slots) | Tabs on one panel and the slots in another that they go into | | [`kerf/hinge`](#living-hinges) | Living hinges that bend flat sheet, and a box with round sides | | [`kerf/dividers`](#dividers) | Dividers that slot into each other like an egg crate | | [`kerf/hanging`](#hanging) | Keyholes, holes and loops for hanging things on a wall | | [`kerf/enclosure`](#enclosures) | Boxes with a screwed-on lid, cutouts for ports and vents | | [`kerf/calibration`](#calibration) | Test pieces that measure the kerf and how your sheet fits | | [`kerf/gears`](#gears-and-cams) | Spur gears, racks, lantern gears, cams and tappets | | [`kerf/lock`](#lock) | A lock with a key, three layers deep | | [`kerf/rings`](#rings) | Round things of stacked rings on a crossed core | | [`kerf/joints`](#joints) | Two profiles halved into each other: crosses, fonts, domes | | [`kerf/sizes`](#sizes) | Paper, card and photo sizes | | [`kerf/symbols`](#symbols) | 867 pictograms by name | | [`kerf/geo`](#maps) | The world's land, for maps and globes | Sizes are in millimetres. Joints use `$thickness`, the sheet thickness, so set it to what you measure. Each section has an example in the gallery that you can open in the playground. ## Boxes `use ` ![Finger-jointed box from kerf/box](library/box.svg) Finger-jointed boxes. Example: [box.kerf](https://kerfscript.mylasertools.com/models/box/box.kerf). ### `box(size, lid = false, finger, name = "")` The panels of a box as parts `bottom`, `front`, `back`, `left`, `right` and, with a lid, `lid`, placed for the assembled view. Shapes given to `box()` go on the front, drawn from its bottom left corner as seen from outside. Wrap it in `place()` to set it anywhere: ```kerf use place(at = [0, 0, 40]) box([120, 80, 60], name = "crate") engrave() translate([60, 30]) text("TEA", size = 14, halign = "center"); ``` | Parameter | Default | Meaning | | --------- | ------------------------ | ------------------------------------------------------ | | `size` | | Outside `[width, depth, height]`, lid included | | `lid` | `false` | Add a lid, finger-jointed to the walls like the bottom | | `finger` | `max(3 * $thickness, 6)` | Finger width | | `name` | `""` | Goes before each panel's name, as `crate front` | ### `divider(size, finger)` A divider `[width, height]` with finger tabs on both sides, to slide into rows of `finger_holes()` cut in two walls. ## Tabs and slots `use ` ![Spice shelf from kerf/tabs](library/tabs.svg) Tab and slot joints, for one panel standing on or through another. The tabs are as deep as `$thickness`, so they finish flush with the far side. Example: [spice-shelf.kerf](https://kerfscript.mylasertools.com/models/spice-shelf/spice-shelf.kerf), a wall shelf held together by tabs alone. ```kerf use // A side with a slot for the shelf 40 mm up, and the shelf part("side") difference() { rect([60, 120]); translate([0, 40]) slots(60); } part("shelf") tab_rect([150, 60], [false, true, false, true]); ``` ### `tabs(length, count, width, depth)` Tabs along x from 0 to `length`, hanging below y = 0: join them to the bottom edge of a panel. | Parameter | Default | Meaning | | --------- | ------------------------------- | -------------------------- | | `length` | | Length of the edge | | `count` | one for every 50 mm, at least 1 | Number of tabs | | `width` | half the space between tabs | Width of each tab | | `depth` | `$thickness` | How far the tabs stand out | ### `slots(length, count, width, clearance = 0.1)` The slots that `tabs()` with the same `length`, `count` and `width` go through, centred on y = 0. `clearance` is added to their width and to `$thickness`; [cut a slot gauge](#calibration) to find the right value for your sheet. ### `tab_rect(size, edges = true, count, width)` A rectangle `[width, height]` with tabs outside it on some edges. `edges` is `true` or `false` for all four, or `[bottom, right, top, left]`. ### `tab_positions(length, count)` The centres of the tabs along an edge, for placing anything else that lines up with them. ## Living hinges `use ` ![Flex box from kerf/hinge](library/hinge.svg) Living hinges: rows of cuts that let plywood bend. They work best in 3 mm plywood or thinner. Example: [flex-box.kerf](https://kerfscript.mylasertools.com/models/flex-box/flex-box.kerf), and [hinge-card.kerf](https://kerfscript.mylasertools.com/models/hinge-card/hinge-card.kerf) for a simple fold. ```kerf use // A strip that bends 90° around a 10 mm radius in the middle flex_rect([200, 60], [100], width = bend_length(10)); ``` ### `bend_length(r, angle = 90)` How long a living hinge must be to bend `angle` degrees around an inside radius `r`: the length of the arc through the middle of the sheet. ### `flex_rect(size, bends, width, spacing = 1.5, bridge = 3, r = 0)` A rectangle with a living hinge across its full height at each x in `bends`. | Parameter | Default | Meaning | | --------- | ----------------------------- | ----------------------------------------------------------- | | `size` | | `[width, height]` | | `bends` | | List of x positions, the middle of each hinge | | `width` | `bend_length(3 * $thickness)` | Width of each hinge | | `spacing` | `1.5` | Distance between the rows of cuts; closer bends more easily | | `bridge` | `3` | Uncut length between cuts | | `r` | `0` | Corner radius of the rectangle | ### `flex_box(size, r = 15, lid = true, clearance = 0.1, spacing = 1.5, bridge = 3)` A box with round corners whose four sides are one strip, part `side`, bent at the corners. The tabs of part `bottom` go through slots along the strip. Part `lid` rests on top, and part `lid insert`, glued under it, keeps it in place. `size` is the outside `[width, depth, height]`, lid included, and `r` the outside corner radius, more than two thicknesses. The assembled view shows the strip bent round the bottom. ### `flex_panel(size, r, sw, sd)` The bottom of `flex_box()`: a rounded rectangle with tabs on its straight edges, `sw` long on the front and back and `sd` on the sides. ### `narrow_hinge(size, spacing = 1.5, bridge = 1.5)` A hinge for a narrow strap or band, as lines across `[w, h]`: cuts from alternate edges, each stopping `bridge` short of the far edge, so the strip zigzags and bends evenly however narrow it is. Use it where `living_hinge()` would leave only a cut or two across. ### `flex_strip(path, width, name = "strip", tabs = true, straight = 10, r, spacing = 1.5, bridge = 3, narrow = false)` A strip that follows a profile: `path` is a list of `[x, y]` points along its middle in a side plate's plane, as `path_points()` or a list comprehension gives them. The strip has a living hinge wherever the path curves and tabs along both edges wherever it runs straight. It is one part, placed in the plate's coordinates with its edge at z = 0 and its width running down to `-width`, and bent in the assembled view. ```kerf use W = 80; path = [for (a = [180:5:360]) [40, 40] + 40 * [cos(a), sin(a)]]; module plate() offset(delta = 12) polygon(concat(path, [[80, 70], [0, 70]])); part("end A", rotate = [90, 0, 0], slots = ["tray"]) plate(); part("end B", at = [0, W + $thickness, 0], rotate = [90, 0, 0], slots = ["tray"]) plate(); place(rotate = [90, 0, 0]) flex_strip(path, W, name = "tray"); ``` Wrap it in `place()` with the plate's placement, put the other plate `width` away, and give both plates `slots`: they cut the slots for the tabs where the strip passes through them. Segments of the path shorter than `straight` are taken as curve; a sharp corner bends round radius `r`, two thicknesses by default. `narrow` uses `narrow_hinge()`. ### `strip_bends(path, straight = 10, r)` The bends `flex_strip()` makes, as `[s0, s1, angle]` along the path: where each curve starts and ends and how far it turns, left positive. Give them to `part(bend = ...)` for a strip of your own. ### `strip_merge(list)` Joins bends from `strip_bends()` that touch into one. ## Dividers `use ` ![Drawer organizer from kerf/dividers](library/dividers.svg) Dividers with slots halfway up, so a divider running one way and one running the other slide into each other. Example: [drawer-organizer.kerf](https://kerfscript.mylasertools.com/models/drawer-organizer/drawer-organizer.kerf), a box from `kerf/box` with a grid of dividers inside. ```kerf use // Four compartments across, two front to back, in a 200 × 120 space grid_dividers([200, 120, 40], cols = 4, rows = 2); ``` ### `grid_dividers(size, cols = 2, rows = 2, clearance = 0.15, gap = 0.5, scoops = false, at = [0, 0, 0])` The dividers for a space `[width, depth, height]`, such as the inside of a box, making `cols` compartments across and `rows` front to back. Parts `long divider` run left to right and `cross divider` front to back. | Parameter | Default | Meaning | | ----------- | ----------- | --------------------------------------------------------------------- | | `size` | | Inside `[width, depth, height]`; the dividers are this tall | | `cols` | `2` | Compartments across | | `rows` | `2` | Compartments front to back | | `clearance` | `0.15` | Added to the slot width | | `gap` | `0.5` | How far the dividers stop short of the walls | | `scoops` | `false` | Dip the top edge over each compartment, to reach in | | `at` | `[0, 0, 0]` | Where the space's front left bottom corner is, for the assembled view | ### `slotted_divider(length, height, at = [], from = "top", clearance = 0.15, scoops = 0)` One divider, with a slot half its height at each x in `at`: from the top, or from the bottom with `from = "bottom"`. Two dividers that cross take one of each. `scoops` is the number of finger dips in the top edge. ## Hanging `use ` ![Wall sign from kerf/hanging](library/hanging.svg) Ways to hang a panel. Each module cuts its holes out of the shapes it is given; given nothing, it makes the holes themselves, for a `difference()` of your own. Example: [wall-sign.kerf](https://kerfscript.mylasertools.com/models/wall-sign/wall-sign.kerf), with all three ways. ```kerf use // Two keyholes 120 mm apart, 12 mm below the top edge keyholes(120, at = [80, 48]) rect([160, 60], r = 5); ``` ### `keyhole(at = [0, 0], head = 9, shank = 4.5, drop = 10)` A keyhole for a screw: the head goes through the round hole, then the panel drops `drop` mm so the screw rests at `at`, at the top of the slot. `head` and `shank` are the widths of the hole and the slot: measure the screw and add a little. `drop` must be at least `(head + shank) / 2`. ### `keyholes(spacing, at = [0, 0], head = 9, shank = 4.5, drop = 10)` Two keyholes `spacing` apart, centred on `at`, for a panel that hangs level. ### `hanging_hole(at = [0, 0], d = 5, spacing = 0)` A round hole for a nail, a hook or a cord, or two `spacing` apart. ### `hanging_loop(at = [0, 0], d = 4, wall = 3)` A ring `wall` wide around a hole of diameter `d`, joined to the shapes it is given. Put it over the edge of an ornament or a tag: the hole stays open where the ring overlaps the shape. Overlap the ring with the shape, or the loop comes out as a separate piece. ## Enclosures `use ` ![Project enclosure from kerf/enclosure](library/enclosure.svg) A box whose lid comes off again, for electronics. The walls and the bottom are finger jointed. The lid overhangs the walls a little and is held by screws that go down into the walls' top edges, each into a nut that sits in a T-slot. Example: [project-enclosure.kerf](https://kerfscript.mylasertools.com/models/project-enclosure/project-enclosure.kerf). ```kerf use enclosure([150, 100, 60], cutouts = [ ["front", [36, 28], 8], // a round hole for a jack ["back", [72, 15], [13, 6.5]], // a rectangle for a USB port ["lid", [75, 50], [70, 30], "vents"], ]); ``` ### `enclosure(size, screw = 3, screw_length = 12, nut, lip, cutouts = [], finger)` Parts `bottom`, `lid`, `front`, `back`, `left` and `right`, placed for the assembled view. Two screws go into the front wall and two into the back, and one more each for every 80 mm of width. | Parameter | Default | Meaning | | -------------- | ---------------------------- | ---------------------------------------------- | | `size` | | Outside `[width, depth, height]`, lid included | | `screw` | `3` | Screw diameter: 3 for M3 | | `screw_length` | `12` | Screw length under the head | | `nut` | `[screw * 1.9, screw * 0.9]` | `[width, height]` of the slot for the nut | | `lip` | `screw` | How far the lid overhangs the walls | | `cutouts` | `[]` | Holes in the panels, see below | | `finger` | `max(3 * $thickness, 6)` | Finger width | The walls stand `lip` in from the edge of the lid, so a design for an `[150, 100, 60]` enclosure with M3 screws has walls 144 × 94 mm. ### `enclosure_cutouts(side, cutouts)` The holes in `cutouts` for one panel; `enclosure()` cuts them. Each is `[side, [x, y], size]`: - `side` is `"front"`, `"back"`, `"left"`, `"right"`, `"lid"` or `"bottom"`. - `[x, y]` is the middle of the hole on the panel as it is drawn, from its bottom left corner. On the front and back x runs left to right as seen from the front; on the sides it runs from front to back. y runs up from the bottom of the wall. - A number `size` is a round hole of that diameter, `[width, height]` a rectangle. Add `"vents"` after a rectangle to fill it with vents. ### `vents(size, slot = 3, pitch = 6)` Ventilation slots across `[width, height]`, centred on the origin: upright slots `slot` wide with round ends, `pitch` apart. ## Calibration `use ` ![Calibration set from kerf/calibration](library/calibration.svg) Test pieces to cut once for every new material, before a design that has to fit. Their labels are text, so they need a font, as [`text()`](https://kerfscript.mylasertools.com/docs/reference/index.md#shapes) does; `labels = false` leaves them off. Example: [calibration-set.kerf](https://kerfscript.mylasertools.com/models/calibration-set/calibration-set.kerf); the [Kerf](https://kerfscript.mylasertools.com/docs/kerf/index.md) guide explains what to do with the results. ```kerf use $thickness = 3; part("kerf gauge") kerf_gauge(); part("slot gauge") slot_gauge(); joint_test([0.1, 0.15, 0.2]); ``` ### `kerf_gauge(strips = 10, size = [60, 25], labels = true)` A frame with `strips` strips inside, all separated by single cuts. Single cut lines are not moved by kerf compensation, so each takes away one kerf. Push the strips to one side and measure the gap they leave: the kerf is the gap divided by `strips + 1`. `size` is the window the strips fill. ### `slot_gauge(steps = 7, step = 0.1, depth = 15, labels = true)` A comb of `steps` slots, from narrower than `$thickness` to wider, `step` apart, each labelled with the difference. Try a scrap of the same sheet in each: the one that fits as you want is the clearance for `slots()` and `grid_dividers()`. ### `joint_test(kerfs = [0.1, 0.15, 0.2], size = [40, 20], finger, labels = true)` A pair of finger-jointed pieces for each kerf in `kerfs`, parts `joint out` and `joint in`, each cut with that kerf compensation. The pair that holds together without glue or forcing has the kerf to set for this sheet. ## Gears and cams `use ` Gears, racks, the lantern gears of old mills, and the cams and tappets that make a model move. Gears go by their module `mod`: the pitch diameter is `mod * teeth`, and two gears mesh when they have the same module and their centres are `gear_distance()` apart. ```kerf use part("big") spur_gear(36, 2, bore = 6, spokes = 5); part("small", at = [gear_distance(36, 12, 2), 0, 0], rotate = [0, 0, gear_phase(12)]) spur_gear(12, 2, bore = 6); ``` ### `spur_gear(teeth, mod = 2, bore = 5, pressure = 20, backlash = 0.1, spokes = 0, square = false)` A gear with involute teeth centred on the origin, a tooth along +x, and a hole for its axle: round, or `square` to turn with a square axle. `spokes` from 3 to 6 cuts windows between spokes. `backlash` thins each tooth so meshed gears don't bind. ### `gear_points(teeth, mod = 2, pressure = 20, backlash = 0.1, steps = 6)` The gear's outline as a list of points, for gears of your own shape. ### `gear_window(r0, r1, span, w)` One rounded window between two spokes, from radius `r0` to `r1`. ### `gear_radius(teeth, mod = 2)`, `gear_distance(teeth_a, teeth_b, mod = 2)` The pitch radius, and the distance between the centres of two gears in mesh. ### `gear_turn(driver, driven, angle)` How far a gear with `driven` teeth turns when one with `driver` teeth in mesh with it turns `angle` degrees: the other way round. ### `gear_phase(teeth)` The turn that lines a gear's teeth up with the gear beside it along x. ### `rack(teeth, mod = 2, height, pressure = 20, backlash = 0.1)` A straight rack along +x, its pitch line at y = 0. A `spur_gear()` of the same module meshes with it from `gear_radius()` above. ### `lantern_disc(staves, r, d = 6, bore = 6, rim)`, `peg_wheel(pegs, r, d = 6, bore = 6, rim)` A lantern gear is two discs with dowel staves between them; `lantern_disc()` is one disc, with holes for `staves` staves of diameter `d` on radius `r`. A `peg_wheel()` has dowel pegs standing out of its face, which drive the staves of a lantern gear at right angles. Match the pitch of the pegs and the staves. ### `peg_radius(n, pitch)` The radius for `n` pegs or staves `pitch` apart. ### `eccentric(r, offset, bore = 6, square = false)`, `snail_cam(r0, r1, bore = 6, steps = 72, square = false)`, `tappets(n, r, w = 10, hub = 14, bore = 6, square = false)` Cams: an `eccentric()` pushes a follower up and down by twice `offset`; a `snail_cam()` lifts it slowly from `r0` to `r1` over a turn and drops it at once, as a trip hammer works; `tappets()` lift a lever `n` times a turn. ## Lock `use ` A lock with a key, for boxes, diaries and money boxes. A flat key turns an eccentric cam that slides a bolt; half a turn throws its nose out past the lock's top edge into a notch in whatever it locks. It is three layers of the current material: a base, a frame with the cam and the bolt, and a plate with the keyhole. ### `cam_lock(throw = 7, locked = true, name = "Lock", key = true, ring = true)` The lock's parts, lying flat with the key's axis on the z axis and the bolt moving along +y. Wrap it in `place()` to set it on a box front or a cover. `throw` is how far the bolt moves; `locked` shows it thrown; `ring` engraves a ring round the keyhole. ### `lock_bounds(throw = 7)`, `lock_nose(throw = 7)` The lock's outline as `[x0, y0, x1, y1]` round the key's axis, and the bolt's nose as `[width, y of its tip when locked]`: cut a notch a little wider than the nose where it goes. ### `lock_key(t = $thickness, ws = 5)`, `lock_dims(throw)` The key, drawn standing on y = 0, and the lock's inner sizes. ## Rings `use ` Round things built from stacked rings threaded on a core of two crossed plates: drums, barrels, vases, towers and turned legs. ```kerf use ring_stack([for (k = [0:11]) 60 + 20 * sin(k * 15)], 30, name = "vase"); part("base", at = [0, 0, -$thickness], slots = ["vase core A", "vase core B"]) circle(d = 90); ``` ### `ring_stack(diameters, hole, name = "ring", count, feet = true, clearance = 0.1)` Rings with outsides `diameters`, bottom up, on z = 0 and the z axis, all with a round `hole`; or `count` alike rings of one diameter. The core plates, `name core A` and `name core B`, are half-lapped, with two tabs each below z = 0 for a base: give the base `slots` naming them. The plates keep `$min_width` beside their halving slot and the feet keep it between their slots in the base, so a hole too small for either stops with the smallest hole that works (about 7 mm, and 12 mm with feet, in 3 mm ply). ### `ring_core_width(hole, clearance = 0.1)` The width of a core plate whose corners just clear a round hole. ## Joints `use ` Two profiles halved into each other at right angles, standing on a floor: a cross on its foot, a font, a dome, a tree. ```kerf use module tree() polygon([[-14, 0], [14, 0], [14, 4], [4, 4], [4, 8], [20, 8], [0, 50], [-20, 8], [-4, 8], [-4, 4], [-14, 4]]); part("floor", at = [-30, -30, -$thickness], slots = ["tree front", "tree side"]) square(60); halved_pair("tree", cap = 30, feet = [[-10, 4], [10, 4]]) { tree(); engrave() translate([0, 20]) circle(d = 4); } ``` ### `halved_pair(name, at = [0, 0, 0], cap, feet = [], side_feet, clearance = 0.1)` The first child is the profile, drawn with x across and y up, centred on x = 0 with the floor at y = 0. The front, `name front`, is the whole profile along x, slotted from below; the side, `name side`, is the profile along y capped at height `cap` (all of it without), slotted from above, the slots meeting halfway up the side. A second child is drawn on both faces, clipped to each part: their engraving or print. `feet` are tabs below the floor on the front, x positions (3 mm wide) or `[x, width]` pairs, and `side_feet` the side's (the same unless given); give the floor `slots` naming both parts. The strips beside both slots must keep `$min_width`, and a foot may not run into the front's slot: either stops with where. ## Sizes `use ` Paper, card and photo sizes in millimetres, for holders, folders, frames and boxes made to fit them. ### `paper_size(name)`, `card_size(name)`, `photo_size(name)` `[width, height]` by name, ignoring case: paper from `"A0"` to `"A8"`, `"B4"` to `"B6"`, `"C4"` to `"C6"`, `"DL"`, `"letter"`, `"legal"`, `"tabloid"`, `"half letter"`, `"executive"`, `"pocket"` and `"traveler"`, standing; cards `"business"`, `"us business"`, `"credit"`, `"japan"`, `"playing"`, `"bridge"`, `"tarot"`, `"postcard"` and `"us postcard"`, and photos `"4x6"`, `"5x7"`, `"8x10"`, `"10x15"`, `"13x18"` and the instant film sizes, lying down. An unknown name stops with the list of known ones. ### `landscape(size)`, `portrait(size)` A size turned to lie wider than tall, or to stand taller than wide. ### `lookup_size(name, table)`, `size_names(table)` The size of a name in a table of `[name, size]`, and the names in it. ## Symbols `use ` 867 pictograms by name, from open icon sets: animals, food, nature, sports, travel, holidays, music, flowers and more. Each comes from a set under the Apache 2.0, MIT or CC0 licence, named by `symbol_source()`; the [notices](../THIRD-PARTY-NOTICES.md) list them. ```kerf use part("tag") { circle(d = 40); engrave() symbol("paw", 24); } ``` ### `symbol(name, size = 10, center = true, fill = "nonzero", box = "drawn")` The symbol `size` mm across the longer side of what it draws, centred on the origin, or with its bottom left corner there. With `box = "grid"`, `size` and the centre are those of the icon set's own grid (24 x 24 for Material Design Icons), so icons from one set keep their sizes and places relative to each other: paw prints in a trail, an icon swapped for another. ### `symbol_names(category)`, `symbol_size(name, size = 10, box = "drawn")`, `symbol_source(name)`, `symbol_entry(name)` The names of every symbol, or of one category; the `[width, height]` a symbol draws at a size; where it comes from as `[source, licence]`; and its row of the table, `[name, category, drawn box, source, path, grid]`, the boxes as `[x, y, w, h]`. ## Maps `use ` The world's land from Natural Earth (public domain), for maps, globes and country-shaped signs. ### `world_map(width = 200, projection = "equirectangular", south = -60)` The land as shapes, a map `width` mm wide centred on the origin, as `"equirectangular"` (twice as wide as tall) or `"mercator"`. Land south of `south` is left out. ### `is_land(lat, lon)` Whether a point is on land, to the nearest degree: quick enough to call for every point of a globe. ### `geo_point(lat, lon, width = 200, projection = "equirectangular")` Where a place goes on the map, to mark a city or a journey. ### `land_polygons()` The land as polygons: each a list of rings of `[longitude, latitude]`. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/shared-libraries/ # Shared libraries A shared library is a folder of `.kerf` files with a `kerf.json` that names it. You publish one by tagging a GitHub repository, or by putting a `.tar.gz` anywhere that serves it over https. A project lists the libraries its designs use in its own `kerf.json`. `kerf install` copies them into `kerf_libraries/` and pins each one in `kerf.lock`, by its exact commit and a hash of its files, so a design renders the same way on every machine, now and later. The [standard library](https://kerfscript.mylasertools.com/docs/library/index.md) is built into every renderer and needs none of this: `use ` always works. ## Use a library In the folder that holds your designs: ```sh kerf add github:maker/kerf-feet@v1.0.0 ``` This creates a `kerf.json` the first time: ```json { "libraries": { "feet": "github:maker/kerf-feet@v1.0.0" } } ``` The library's own `kerf.json` gives its name, here `feet`. Its files go into `kerf_libraries/feet/`, and `kerf.lock` records exactly what was installed. A design then loads them with the library's name first: ```kerf use part("base") difference() { rect([100, 60]); feet([100, 60]); } ``` `kerf render`, `kerf check`, the desktop app and the language server in your editor all find it. The playground has the standard library only. A source is one of: | Source | Means | | ----------------------------- | ----------------------------------------------------------------------------------- | | `github:owner/repo@v1.0.0` | A GitHub repository at a tag, a branch or a commit; without `@`, its default branch | | `https://…/feet-1.0.0.tar.gz` | A `.tar.gz` such as `kerf pack` writes, from any https link | | `./libraries/feet` | A folder on this computer, for a library you are writing | To change version, add it again with the new tag; `kerf add` replaces the source. `kerf remove feet` takes a library out. ## What to commit Commit `kerf.json` and `kerf.lock`. Then either: - commit `kerf_libraries/` too, so the project works without a download, in the desktop app as well, or - leave it out (add it to `.gitignore`) and run `kerf install` after each clone. It downloads the exact files `kerf.lock` pins. ## How a design finds a library For a path that does not start with `kerf/`, a design looks in its project, the nearest folder at or above the design's own with a `kerf.json`: 1. `use `, where `name` is the project's own name in its `kerf.json`: a file of the project, from its top folder. A library uses this for its own files, and so do the designs and examples inside it. 2. `use `, where `kerf_libraries/name/` exists: that installed library's file. 3. An absolute path, as `use `: that file, wherever the design is. Handy for a scratch design outside the project; a design to share should use the other forms. 4. Anything else: a file in the design's folder or below it, as before. Each path is tried as written and with `.kerf` added. A design in `projects/shelf/designs/` therefore finds `use ` in `projects/shelf/kerf_libraries/feet/feet.kerf` when `projects/shelf/` has the `kerf.json`. ## Reproducing a design `kerf.lock` holds one entry for each library, including the ones other libraries use: ```json { "lockVersion": 1, "libraries": { "feet": { "source": "github:maker/kerf-feet@v1.0.0", "version": "1.0.0", "resolved": "https://github.com/maker/kerf-feet/archive/3f2c…e91a.tar.gz", "integrity": "sha256-4hC4hj++68l+quohmEdLL/KYF9lwtTD7Hon6G+5V9kY=" } } } ``` - `resolved` is the exact commit. A tag can be moved to other code later; the commit cannot, so `kerf install` downloads from here, not from the tag. - `integrity` is a sha256 hash of the library's files. `kerf install` refuses files that do not match it. The hash ignores line endings, so git's conversions on Windows do not change it. In CI, install exactly what is pinned, and fail if `kerf.lock` would need to change: ```sh kerf install --frozen kerf render designs/shelf.kerf -o shelf.svg ``` `kerf install --check` downloads nothing. It checks that `kerf.json`, `kerf.lock` and `kerf_libraries/` agree, and that no one has edited an installed file. It exits with 1 and lists the differences when they do not. The libraries are one part of a design's output; the renderer is the other. Pin the `kerf` or npm package version too, as in [Versions](https://kerfscript.mylasertools.com/docs/versions/index.md). ## Libraries that use libraries A library's `kerf.json` can name libraries of its own, and `kerf install` installs them too. There is one copy of each name in `kerf_libraries/`. When two libraries want the same library from different sources, the install stops and asks you to choose. Add that library to your own `kerf.json`; the project's choice wins: ```json { "libraries": { "stand": "github:maker/kerf-stand@v2.0.0", "lamp": "github:other/kerf-lamp@v1.4.0", "feet": "github:maker/kerf-feet@v1.1.0" } } ``` A library's own `use <...>` looks next to it first: `lib/props.kerf` saying `use ` finds `lib/finish.kerf`, wherever the design that uses `lib/props.kerf` is. ## Publish a library A library's top folder holds its `kerf.json`: ```json { "name": "feet", "version": "1.0.0", "description": "Holes for rubber feet", "license": "MIT", "language": "0.2", "libraries": {} } ``` | Field | Use | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `name` | Required. Lowercase letters, digits, `-` and `_`. Designs write `use `. `kerf` is the standard library's | | `version` | Required to pack. `1.2.0`: raise the first number when a module's parameters change, or when existing designs would cut differently | | `description` | One line on what it makes | | `license` | The licence others may use it under | | `language` | The oldest KerfScript language version it works with; older `kerf` versions refuse to install it | | `libraries` | Libraries it uses, from `github:` or `https:` sources | A library is its `.kerf` files in any folder, its `kerf.json`, and the `README`, `LICENSE` and `CHANGELOG` in its top folder. Pictures, tests and dot folders such as `.github` are left out when it is installed. Inside the library, load its own files by its name, `use `, not `use `. That works while you write it, since the project is named `feet`, and once it is installed next to someone else's designs. Put a comment above each module: editors show it on hover, as for the [standard library](https://kerfscript.mylasertools.com/docs/library/index.md). Before each release, run: ```sh kerf pack ``` It parses every `.kerf` file and checks every `use <...>`. Each must name the standard library, the library's own files as ``, or a library in its `kerf.json`. Then it writes `feet-1.0.0.tar.gz`. To publish: 1. On GitHub, commit, then tag the version and push the tag: `git tag v1.0.0 && git push origin v1.0.0`. Others run `kerf add github:you/kerf-feet@v1.0.0`. 2. Anywhere else, upload the `.tar.gz` (a GitHub release asset works too), and others run `kerf add https://…/feet-1.0.0.tar.gz`. Do not move a tag or replace a file once it is published. The projects that pinned it would no longer install; publish a new version instead. ## Safety A KerfScript program cannot read files or reach the network, so a library can only make shapes and run within the renderer's step limits. `kerf install` writes only inside `kerf_libraries/`. It refuses an archive with a file that would land outside its folder, a plain `http://` link (except on this computer), and a download over 20 MB. The desktop app reads only `.kerf` files, and only from the design's folder and the project's. ## From JavaScript The `@mylasertools/kerfscript/libraries` module, for Node.js, finds and installs libraries the same way the command line does: ```js import { render } from '@mylasertools/kerfscript'; import { resolver, install, check } from '@mylasertools/kerfscript/libraries'; import { readFileSync } from 'node:fs'; await install('projects/shelf', { frozen: true }); const file = 'projects/shelf/designs/shelf.kerf'; const result = render(readFileSync(file, 'utf8'), { resolve: resolver(file) }); ``` | Export | Does | | ----------------------------------------- | ---------------------------------------------------------------- | | `resolver(designFile)` | A `resolve` option for `render()` that finds libraries as above | | `libraryFile(folder, path)` | The file `use ` names for a design in `folder` | | `findProject(folder)` | The nearest folder at or above `folder` with a `kerf.json` | | `install(root, { frozen, fetch, log })` | `kerf install`; resolves to the new `kerf.lock` | | `add(root, source)`, `remove(root, name)` | `kerf add`, `kerf remove` | | `check(root)` | `kerf install --check`: a list of problems, empty when all agree | | `pack(root, out?)` | `kerf pack` | Problems are thrown as `LibraryError`, with a message to show as it is. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/customizer/ # Settings Expose dimensions, text and other choices as controls beside your design. Comments define sliders and menus, while the variable values provide their defaults. KerfScript uses OpenSCAD's customizer comment format. ```kerf /* [Size] */ // Outer width width = 120; // [40:5:400] // Outer depth depth = 80; // [200] /* [Style] */ shape = "round"; // [round:Round, square:Square] holes = 3; // [2, 3, 4] lid = true; label = "Tools"; /* [Hidden] */ wall = 3; rect([width, depth]); ``` This example defines several control types. The rectangle uses `width` and `depth`; the other variables illustrate settings you can use elsewhere in a design. ## What becomes a setting A variable declared at the top level becomes a setting when its value is a number, string, boolean or list of numbers. Variables defined by expressions, such as `half_width = width / 2`, are calculated from the other values and do not get their own controls. ## Comments Put a range or choice list after the assignment: | Comment after the value | Control | | --------------------------------- | ------------------------------------ | | `// [40:5:400]` | Slider from 40 to 400, in steps of 5 | | `// [40:400]` | Slider from 40 to 400 | | `// [200]` | Number with an upper limit of 200 | | `// [2, 3, 4]` | Choice of 2, 3 or 4 | | `// [round:Round, square:Square]` | Menu with a label for each value | | No comment on a boolean | Checkbox | | No comment on a string | Text field | `/* [Size] */` starts a group named Size. `/* [Hidden] */` hides the settings that follow it, until another group begins. A `//` comment immediately above a variable supplies its caption. Keep the file's introductory comment separate with a blank line so it does not become the first setting's caption. ## Changing settings without editing the file The CLI accepts values with `-D` or a JSON file with `--params`: ```sh kerf render box.kerf -D width=200 kerf render box.kerf --params box-settings.json kerf params box.kerf ``` `box-settings.json` can contain `{"width": 200, "lid": false}`. Use `kerf params` to list a design's settings as JSON. String values need quotes, for example `-D 'label="Oak"'` for a design with a `label` setting. In JavaScript, pass values through `render()`'s `params` option and read the settings with `parameters()`. See the [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/desktop/ # Desktop app The KerfScript app is the [playground](https://kerfscript.mylasertools.com/playground/) on your computer: the same editor, settings and preview, with your designs saved as `.kerf` files. It works offline. ## Install Download the app for your system from the [releases](https://github.com/mylasertools/kerfscript/releases): | System | File | | ------- | ---------------------------------------------------------------------------- | | Windows | `kerfscript-desktop--windows-x64-setup.exe`, or the `.msi` | | macOS | `kerfscript-desktop--macos-universal.dmg` (Apple silicon and Intel) | | Linux | `.AppImage`, `.deb` or `.rpm`, for x64 or arm64 | Package manager listings (winget, Chocolatey, Flathub and the AUR) follow as each one is approved. The app is not signed with an Apple or Microsoft certificate yet: - **macOS** says the app cannot be checked for malicious software. Open it once with right click → **Open**, or run `xattr -d com.apple.quarantine /Applications/KerfScript.app`. - **Windows** SmartScreen may say it protected your PC. Choose **More info** → **Run anyway**. ## Files | Action | Keys | | --------------------- | --------------------------------------------- | | Open a `.kerf` file | Ctrl+O (⌘O on a Mac), or the **Open…** button | | Save | Ctrl+S | | Save under a new name | Ctrl+Shift+S, or **Save as…** | Double-click a `.kerf` file to open it in the app. The title bar shows the file name, or Untitled, and a dot when there are unsaved changes. The app asks before it closes, or opens another design or an example over them, also for a design you have not saved yet. A design can be up to 1,000,000 characters long, the same as in the playground. Saving writes the whole file before it replaces the old one, so a full disk or a crash cannot leave half a design. SVG, DXF and sheet ZIP exports open a save dialog in the design's folder. On Linux, a name typed without `.kerf`, `.svg`, `.dxf` or `.zip` gets it, and the app asks before replacing a file of that name. ## LightBurn **Open in LightBurn** sends the cut file straight to [LightBurn](https://lightburnsoftware.com) 2.2 or later, through LightBurn's [REST API](https://github.com/LightBurnSoftware/LightBurn-REST). With several sheets, each sheet in the cut files list has its own **LightBurn** link. LightBurn has to be open on the same computer. The first time, LightBurn asks whether to allow KerfScript; choose **Allow** within 30 seconds. The window may open behind KerfScript. KerfScript asks only to send files: it cannot read your projects, change cut settings or start the laser. The file opens as a new LightBurn project, as if you had used File → Open, and LightBurn asks first if your current project has unsaved changes. Check the layers and cut settings in LightBurn before you run the job. To stop KerfScript sending files, remove it in LightBurn under Settings → Extensions → Manage API Authorizations. LightBurn is a trademark of LightBurn Software, Inc. KerfScript is not made, endorsed or certified by LightBurn Software. ## Libraries `use ` and `include ` find files in the open design's folder and the folders below it: with the design at `projects/box.kerf`, `use ` reads `projects/lib/tabs.kerf`. Unlike the [command line](https://kerfscript.mylasertools.com/docs/cli/index.md), the app reads only `.kerf` files, and not from folders above the design's, such as `use <../shared/tabs>`; keep them next to the design or in a folder below it. That way a design you download cannot read other files on your computer. Standard library paths such as `use ` work everywhere. A design that has not been saved yet can only use the standard library. Libraries installed with `kerf install` work too: `use ` reads `kerf_libraries/feet/feet.kerf` in the design's project, the nearest folder at or above the design's with a `kerf.json`. See [Shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md). ## Fonts `text()` uses Atkinson Hyperlegible, as in the playground. The app does not read fonts installed on your system yet; use the [command line](https://kerfscript.mylasertools.com/docs/cli/index.md) with `--font` for other fonts. ## About and licences **About** shows the app's version, a link to its source code, and the licences of KerfScript and of everything built into the app. ## Sharing **Copy web link** copies a link that opens your code in the playground on the website, so people without the app can see it. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/cli/ # Command line Use the `kerf` command to export designs, change parameters and inspect parts from your terminal. Install the package with Node.js 20 or newer: ```sh npm install --global @mylasertools/kerfscript kerf render box.kerf -o box.svg ``` You can also run it through `npx`: ```sh npx @mylasertools/kerfscript render box.kerf -o box.svg ``` ## Without Node.js The [releases](https://github.com/mylasertools/kerfscript/releases) page has a standalone `kerf` for Windows, macOS and Linux (x64 and arm64) that needs no Node.js: `kerf---.zip` or `.tar.gz`, and `kerfscript-cli--linux-.deb` or `.rpm`. It has the Atkinson Hyperlegible font built in, like the npm package. | System | Install | | ---------------- | ------------------------------------------------------------------------------------------------------------- | | macOS, Linux | `brew install mylasertools/tap/kerfscript` | | Windows | `winget install MyLaserTools.KerfScript.CLI`, or `choco install kerfscript` | | Windows (Scoop) | `scoop bucket add mylasertools https://github.com/mylasertools/scoop-bucket`, then `scoop install kerfscript` | | Linux (Snap) | `sudo snap install kerfscript`; the command is `kerfscript.kerf` | | Arch Linux (AUR) | `kerfscript-bin` | | macOS (MacPorts) | `sudo port install kerfscript` | | Docker | `docker run --rm -v "$PWD:/work" --user "$(id -u):$(id -g)" ghcr.io/mylasertools/kerfscript render box.kerf` | Package manager listings follow each release as they are approved. The binaries are not signed yet. On macOS, Homebrew and MacPorts install them without a warning; a downloaded `kerf` needs `xattr -d com.apple.quarantine kerf` once. On Windows, SmartScreen may warn the first time. Save a `.kerf` file from the [models](https://kerfscript.mylasertools.com/models/) to try these commands. ## Commands ```sh kerf render [-o out.svg] [options] kerf params kerf check kerf verify --spec [-o report.json] [--summary] kerf diff [-D name=value] [--json] kerf instructions [-o out.html] [options] kerf fmt kerf fmt --check kerf fmt - kerf lsp [--timeout ] kerf import [-o out.kerf] [--dpi ] [--tolerance ] [--kerf ] [--pack] kerf add kerf remove kerf install [--frozen | --check] kerf pack [folder] [-o file] ``` `render` exports SVG, DXF or JSON, using the output file's extension to choose the format. `params` lists the editable settings as JSON. `check` reports parts, dimensions, cut lengths, sheets and warnings. `verify` checks explicit rigid fit, retention, installation and motion contracts and writes a JSON report. It exits with 0 for passed required claims, 1 for failures, and 2 for unverified or invalid inputs. See [Assembly verification](https://kerfscript.mylasertools.com/docs/verification/index.md) for supported claims, material profiles and the limits of a pass. `fmt` formats `.kerf` files in place, searching folders for them, except `kerf_libraries/`, whose files `kerf.lock` pins as they are. It sets indentation (two spaces), the spaces between tokens, blank lines (at most one in a row) and line endings, and keeps your line breaks and every comment where it is, customizer comments included. It never joins or splits lines. `--check` changes nothing: it lists the files that are not formatted and exits with 1 when there are any, for CI. `kerf fmt -` formats standard input to standard output, for editors. A file that does not parse is reported and left alone. `import` turns an SVG drawing into a `.kerf` program next to it, or to the `-o` file; `-o -` prints it. It does not replace a program already next to the drawing unless that file is named with `-o`. It writes the circles, rectangles, tabs, slots, finger joints, hanger holes and keyholes it finds as the modules that make them, and checks each part against the drawing: see [Importing SVG drawings](https://kerfscript.mylasertools.com/docs/import/index.md) for what it finds and for its options. `lsp` runs the language server on standard input and output, for editors: see [Editors](https://kerfscript.mylasertools.com/docs/editors/index.md). It stops checking a design after `--timeout` seconds, 10 by default. `add`, `remove`, `install` and `pack` manage [shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md) in the project around the current folder, the nearest one with a `kerf.json`. `add` takes `github:owner/repo@tag`, an https link to a `.tar.gz` or a folder, records it in `kerf.json` and installs it into `kerf_libraries/`. `install` installs what `kerf.json` names, from the exact commits and files `kerf.lock` pins; `--frozen` fails instead of changing `kerf.lock`, for CI, and `--check` downloads nothing and exits with 1 when `kerf_libraries/` differs from `kerf.lock`. `pack` checks a library and writes `name-version.tar.gz` to publish. `render` and `check` find the installed libraries by themselves. ## Options | Option | Use | | ---------------------- | ------------------------------------------------------------------------------------ | | `-o, --out ` | Output path ending in `.svg`, `.dxf` or `.json`; defaults to `.svg` | | `-D name=value` | Override a parameter; repeat for multiple values | | `-p, --params ` | Load parameter values from a JSON file | | `--font ` | Supply a font file; repeat for multiple fonts. `Atkinson` is built in | | `--kerf ` | Override kerf for every part | | `--sheet x` | Set the sheet width and height in millimetres | | `--gap ` | Set spacing between parts | | `--margin ` | Set spacing at sheet edges; defaults to the gap | | `--no-pack` | Keep the positions defined in the program | | `--guides` | Include the guide layer | | `--split` | Export one file per sheet, and each sheet's back-face job | | `--nest` | Pack small parts into the holes of bigger ones | | `--stats` | With `check`: points in the files, by part and layer | | `--sweep name=a:s:b` | With `check`: try every value from a to b in steps of s, or `name=a,b,c` | | `-o out.png`, `--px n` | A picture of each sheet, `n` pixels wide (1200), to look at rather than cut | | `--call 'm(1, 2)'` | Run one call of the file's modules instead of its program; `-D` sets variables first | | `--params-list file` | With `check`: try each set of settings in a JSON list of objects | | `-h, --help` | Show command help | | `-v, --version` | Show version numbers | `-D` accepts KerfScript values, including numbers, quoted strings, booleans and lists. For example: `-D width=200` or `-D 'name="Ada"'`. `kerf check` lists the parts, how many make each glued stack and what a part is glued to, the hardware to buy, alike pieces counted together, and the assembly steps. A design with marks on the parts' back faces writes a second file beside the first, `design-back.svg`: the sheet turned over left to right, for the job after the parts are turned over. `--sweep` checks a design over many settings at once. Repeat it to try every combination; each line says `ok` or lists what went wrong, and the command exits with 1 if any combination warns or fails: ```sh kerf check tray.kerf --sweep '$thickness=3:0.5:6' --sweep size=small,large ``` Settings that only make sense together go in one `--sweep`, their names joined by commas and each set of values after a `;`, so the sets are tried as they are rather than crossed. `--params-list` reads the sets from a JSON list instead; both cross with the other sweeps: ```sh kerf check room.kerf --sweep 'width,depth,height=115,140,190;160,200,260' --sweep '$thickness=3,4' kerf check room.kerf --params-list sizes.json ``` `--call` tries one module of a library on its own, without writing a design for it. The file's modules and functions are available, as with `use`; `-D` values are set first, so they can be special variables or names in the call: ```sh kerf render lib/props.kerf --call 'roses(40, 30)' -D '$thickness=4' -o roses.svg kerf check lib/kerf/box.kerf --call 'box([w, 40, 30])' -D w=60 ``` `kerf instructions` writes the assembly instructions as one HTML page to print or save as PDF: the parts list with each part's number, the sheets, then a page per `step()` with the model drawn as it stands after it, the step's parts in orange, and the parts and hardware it adds. With `$label_parts = true` the numbers match the ones engraved on the parts. ```sh kerf instructions lock-box.kerf -o lock-box-instructions.html ``` `kerf diff` renders two designs (or one design before and after a change) with the same settings and lists, part by part and layer by layer, what differs: the area of shapes that changed, open lines added or removed, and where, in the part's own coordinates. It exits 1 if anything changed, so a refactor can prove it changed nothing: ```sh kerf diff old/garage.kerf garage.kerf ``` The `"sheet": [600, 400]` in a project's `kerf.json` is the sheet for every design in the project that does not set `$sheet`: set your laser's bed once. ## Examples ```sh kerf render name-sign.kerf -D 'name="Ada"' --font Inter-Bold.ttf -o ada.svg kerf render box.kerf --sheet 400x300 --kerf 0.12 --split -o box.svg kerf render box.kerf -o box.dxf kerf check box.kerf kerf import ornament.svg -o ornament.kerf ``` For multiple sheets, `--split` creates numbered files such as `box-1.svg` and `box-2.svg`. Use it for DXF exports of a design with several sheets. Warnings are written to standard error and allow rendering to continue. An error stops the command with a non-zero exit code. It shows the line with the problem underlined, the calls it happened inside and, for a built-in given a bad argument, what that built-in takes: ```text error: Unknown variable "widht". Did you mean "width"? --> panel.kerf:3:9 | 3 | rect([widht, 20]); | ^^^^^ = in module panel(), called at panel.kerf:5:1 ``` ## Fonts The CLI has one font built in, Atkinson Hyperlegible, as `Atkinson`: the same font and name as the playground. Text that names no font uses it. Supply other font files with `--font`. The CLI does not look up fonts installed on your system. `--font Inter-Bold.ttf` makes the font available as `Inter-Bold`. A design can request it with `font = "Inter-Bold"`. The first supplied font becomes the default for text that does not name one, and `Atkinson` stays available. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/javascript/ # JavaScript API Add KerfScript rendering to a JavaScript application with the `@mylasertools/kerfscript` package. The library runs in browsers and Node.js and includes TypeScript definitions. ```sh npm install @mylasertools/kerfscript ``` ## render() Pass a program as a string. The result contains its geometry, files, parts and warnings: ```js import { render } from '@mylasertools/kerfscript'; const source = 'width = 80; rect([width, 50]);'; const result = render(source, { params: { width: 120 }, sheet: [600, 400], margin: 0, }); console.log(result.svg); ``` This exports a 120 × 50 mm rectangle. `params` overrides `width` without changing the source string. ### Options | Option | Value | | -------------- | ------------------------------------------------------------------------------------- | | `params` | Parameter values keyed by name | | `fonts` | Font files keyed by name, as `ArrayBuffer` or `Uint8Array` | | `defaultFont` | Name to use when `text()` does not specify a font | | `resolve` | `(path) => source` callback for libraries outside the standard library | | `kerf` | Kerf override for every part, in millimetres | | `layout` | `"pack"` or `"none"` | | `sheet` | `[width, height]` in millimetres, over the program's `$sheet` | | `defaultSheet` | `[width, height]` for programs that do not set `$sheet`: the laser bed | | `nest` | Pack small parts into the holes of bigger ones, as `$nest` | | `gap` | Space between parts, in millimetres | | `margin` | Space at sheet edges, in millimetres; defaults to `gap` | | `guides` | Whether to include guide geometry | | `analysis` | Capture nominal cut geometry before compensation, for [verification](https://kerfscript.mylasertools.com/docs/verification/index.md) | | `maxSteps` | Evaluation step limit | | `limits` | Memory limits, see below; any you leave out keep their default | Supply a font for programs that use text: ```js const response = await fetch('/fonts/Inter.ttf'); const font = await response.arrayBuffer(); const result = render(source, { fonts: { Inter: font }, defaultFont: 'Inter', }); ``` The URL should point to a font file provided by your application. ### Result | Field | Contents | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `svg` | Combined SVG containing all sheets | | `width`, `height` | Combined SVG dimensions in millimetres | | `sheets` | Each sheet's dimensions, material, thickness, SVG, DXF, paths and placed items | | `parts` | Part names, quantities, dimensions, piece counts, cut lengths and material properties | | `assembly` | One entry per copy of a part with `name`, `copy`, `thickness`, `material`, `outline`, a 4 × 4 `matrix` and `placed`, for showing the design put together. Parts without `at` or `rotate` have `placed: false`. A bent part has `bends`; a part in a step has `step`; `marks` holds what is engraved and scored on it, front and back | | `steps` | The text of each assembly step, in order; a part's `step` counts from 1 | | `hardware` | Each `hardware()`: `kind`, `from`, `to`, `size`, `up`, `sides`, `step` and `label` | | `paths` | Layer paths with `name`, `mode`, `color`, `d`, `regions` and `lines` | | `layers` | Layer definitions in output order | | `warnings` | Problems that did not stop the render | | `echoes` | Output from the program's `echo()` calls | | `analysis` | When requested: nominal cut regions and lines per definition, with compensation kerf; see [assembly verification](https://kerfscript.mylasertools.com/docs/verification/index.md) | Each sheet with back-face marks has `back`, with its own `svg`, `dxf` and `paths`: the sheet turned over left to right. `bendPoint(bends, thickness, [x, y, z])` gives where a point of a bent part goes, in the part's own coordinates, before its `matrix`; and `meshPieces(outline, bends)` cuts a part's outline into pieces narrow enough across its hinges to bend smoothly. A 3D view extrudes each piece by the thickness, moves its points with `bendPoint()` and then by the `matrix`. Use `result.svg` for a combined preview. Use `result.sheets[i].svg` or `.dxf` to export individual sheets, particularly when they use different materials. For a custom preview, `d` contains all paths in a layer. `regions` contains closed outlines and `lines` contains open paths. On a `fill` layer, draw regions with the even-odd fill rule and draw open paths as strokes. ## parameters() Read a program's editable settings without rendering its geometry: ```js import { parameters } from '@mylasertools/kerfscript'; const source = 'width = 80; // [40:5:200]\nrect([width, 50]);'; const settings = parameters(source).parameters; ``` Each setting includes `name`, `caption`, `group`, `type`, `initial` and `span`. Ranges add `min`, `max` and `step`; menus add `options`. `type` is `"number"`, `"string"` or `"boolean"`. A numeric vector has type `"number"` and an array as its `initial` value. `span` gives the source offsets `[start, end]` of the value. An editor can use them to update one setting: ```js const width = settings.find((p) => p.name === 'width'); if (width) { const next = source.slice(0, width.span[0]) + '120' + source.slice(width.span[1]); } ``` Read the settings again after editing the source; the offsets refer to the string passed to `parameters()`. ## format() `format(source)` returns the program with its layout set, as `kerf fmt` does: two-space indentation, spaces between tokens, at most one blank line in a row and a newline at the end. Line breaks and comments stay where they are. It throws a `KerfError` for a program that does not parse, and checks its own result: the output always has the same tokens and comments as the input. ```js import { format } from '@mylasertools/kerfscript'; format('x=1;module m(a,b=2){rect([a,b]);}'); // 'x = 1; module m(a, b = 2) { rect([a, b]); }\n' ``` `format(source, { indent: 4 })` uses four spaces per level. ## instructionsHtml() `instructionsHtml(result, title)` turns a `render()` result into the page `kerf instructions` writes: a standalone HTML document with the parts, the sheets and a drawing for each step. ## diffRenders() `diffRenders(a, b)` compares two results of `render(source, { layout: "none" })` and returns what differs, one entry per part copy and layer: `part`, `copy`, `layer`, `change` (`"added"`, `"removed"` or `"changed"`), `area` (mm² of shapes that differ), `linesAdded`, `linesRemoved` and `at`, the box of the difference in the part's own coordinates. With layout `"none"`, each sheet item's `offset` is the move from the part's coordinates (y down) to the sheet's. ## importSvg() `importSvg(svg, options)` turns an SVG drawing into a program, as `kerf import` does. `svg` is the text of the drawing, or the bytes of the file as a `Uint8Array` or `ArrayBuffer`; bytes are read as UTF-8 or UTF-16, which some programs write. It returns: - `source`: the program - `parts`: the number of parts - `found`: what was recognised, as counts by kind - `summary`: the same in words, such as `2 parts; found 3 circles.` - `notes`: what to know about the drawing, also written at the top of the program The options are those of `kerf import`: `dpi`, `tolerance`, `kerf` and `pack`, and `name`, the drawing's name for the comment at the top. See [Importing SVG drawings](https://kerfscript.mylasertools.com/docs/import/index.md). ```js import { importSvg, render } from '@mylasertools/kerfscript'; const file = input.files[0]; const { source, summary } = importSvg(await file.arrayBuffer(), { name: file.name }); const result = render(source, { fonts, defaultFont: 'Atkinson' }); ``` A drawing with text needs a font to render, as any program with `text()` does. A large drawing can take some seconds to import: in a browser, call `importSvg()` in a web worker so the page stays responsive. It throws an `Error` for a file that is not SVG. ## Syntax colouring `@mylasertools/kerfscript/highlight` colours KerfScript code without loading the interpreter. It never throws, so it works on unfinished code in an editor. Its categories match the VS Code extension's grammar. ```js import { highlight, highlightLine } from '@mylasertools/kerfscript/highlight'; // HTML with a t-keyword, t-builtin, t-comment ... span per token pre.innerHTML = highlight(source); // Line by line, for editors such as CodeMirror const state = { inBlock: false }; for (const line of source.split('\n')) { for (const { kind, text } of highlightLine(line, state)) { // kind: comment, section, control, string, number, keyword, constant, // definition, builtin, layer, call, special, modifier or plain } } ``` `highlight()` never puts a span across a line break, so its result can be split into lines. `section` and `control` are customizer comments (`/* [Size] */` and `// [40:5:400]`). ## Built-in signatures `SIGNATURES` lists every built-in module and function with its parameters, defaults and a line of documentation, for editors, completion and help text. `@mylasertools/kerfscript/signatures` has the same table without the interpreter. ```js import { formatSignature, signature } from '@mylasertools/kerfscript/signatures'; const rect = signature('rect'); formatSignature(rect); // 'rect(size = 1, center = false, r = 0)' rect.doc; // 'A rectangle with its corner at the origin' rect.params[2]; // { name: 'r', default: '0', doc: 'Corner radius', named: true } ``` Each signature has a `kind` (`module`, `function`, or `both` for `echo` and `assert`), `params` in positional order, and `section`, the heading in the [language reference](https://kerfscript.mylasertools.com/docs/reference/index.md) that documents it. A parameter with `named: true` can only be given by name. `rest` marks built-ins that take any number of values, such as `str(...)`, and `forms` lists other ways to call one, such as `log(base, x)`. ## Language service `@mylasertools/kerfscript/language` answers what an editor asks about a program. It is what `kerf lsp` uses ([Editors](https://kerfscript.mylasertools.com/docs/editors/index.md)), and it runs in browsers too, for an editor on a web page. Positions are offsets into the source in UTF-16 code units, as JavaScript strings count them. ```js import { analyze, complete, hover, problems, signatureHelp, } from '@mylasertools/kerfscript/language'; const a = analyze(source, { resolve }); hover(a, offset); // { start, end, text } with Markdown text, or undefined complete(a, offset); // { from, to, items: [{ label, kind, detail, doc, insert }] } signatureHelp(a, offset); // { label, params: [{ start, end, doc }], active } problems(source, { resolve }); // [{ severity, message, start, end, related }] ``` `analyze()` reads the program's names, scopes and libraries once for the other functions, and works on code that does not parse: the lines with errors are left out. Its `resolve` option finds libraries, as `render()`'s does. | Function | Returns | | --------------------------- | --------------------------------------------------------------------------------- | | `hover(a, offset)` | What the name or library path at the offset is, as Markdown | | `complete(a, offset)` | What can be typed there, to replace the source from `from` to `to` | | `signatureHelp(a, offset)` | The parameters of the call being typed and the one being typed | | `definition(a, offset)` | Where the name is defined; `file` names the library when it is not in the program | | `references(a, offset)` | Every place in the program the name is defined and used | | `rename(a, offset, name)` | The places to change, or `{ error }` for a built-in or a library's name | | `outline(a)` | Modules, functions and variables, nested as written | | `problems(source, options)` | The errors and warnings from running the program; it takes `render()`'s options | `problems()` runs the program, so call it in a worker on a page, as the playground does with `render()`. ## Errors Language errors use `KerfError`: | Property | Value | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `message` | the message with its position, as `Unknown variable "w" (line 3, column 9)` | | `reason` | the message without its position | | `line`, `col` | where the error is, when known; columns count UTF-16 code units | | `file` | the library path when the error is inside a library, as `kerf/box` | | `trace` | the module and function calls it happened inside, innermost first: `{ kind, name, line, col, file, count }`, where `count` is how often a recursive call is on the stack | | `call` | the built-in that was given an unusable argument | | `echoes` | `echo()` output before the error | | `warnings` | warnings before the error | | `missingFonts` | fonts the application needs to supply | `formatError()` writes all of that the way the command line shows it: the source line with the problem underlined, the calls, and what a built-in takes. Pass the source, and `resolve` to show lines inside your own libraries; the standard library is found without it. ```js import { formatError, render } from '@mylasertools/kerfscript'; try { render(source); } catch (error) { console.error(formatError(error, source, { file: 'panel.kerf' })); } ``` ```text error: Unknown variable "widht". Did you mean "width"? --> panel.kerf:3:9 | 3 | rect([widht, 20]); | ^^^^^ = in module panel(), called at panel.kerf:5:1 ``` Show it in a monospaced font so the marks line up. `formatError()` takes any thrown value; for one that is not a `KerfError` it gives `error: `. ## Running untrusted code Run browser renders in a web worker so they do not block the page. Set a timeout and terminate the worker if it takes too long. Evaluation limits bound program steps and recursion, but geometry work also takes time. Size limits stop a program before it builds more than a browser tab can usually hold: part copies, circle segments, list sizes, text length, shape points, exported points and sheets. They bound what a program makes, not every byte the renderer uses, so keep the worker's timeout as well, and run programs you did not write where the page can end them. The defaults are in [Limits](https://kerfscript.mylasertools.com/docs/reference/index.md#limits) and in `DEFAULT_LIMITS`. Lower them with the `limits` option if your worker has little memory: ```js import { render, DEFAULT_LIMITS } from '@mylasertools/kerfscript'; render(source, { limits: { maxPoints: DEFAULT_LIMITS.maxPoints / 2 } }); ``` KerfScript programs have no direct file or network access. `use` and `include` can load the standard library and sources returned by your `resolve` callback. Restrict that callback to libraries you intend to expose. In Node.js, `resolver(file)` from `@mylasertools/kerfscript/libraries` finds a design's files and installed libraries as the command line does; see [Shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md#from-javascript). --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/editors/ # Editors KerfScript has a language server, `kerf lsp`, that gives editors what they need to work on `.kerf` files: - problems while you type: the design runs as you edit, and its errors and warnings are marked where they are, with the module calls an error happened inside - hover: how to call a built-in, with its parameters and a link to the reference; how your own modules, functions and variables are defined, with the comment above them; the range of a setting - completion of modules where a statement starts, functions and variables in expressions, named arguments inside a call, and library paths after `use <`, including those of [shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md) in `kerf_libraries/` - parameter hints for the argument being typed - go to definition, find references, highlighting of a name's uses and rename, following the [scope rules](https://kerfscript.mylasertools.com/docs/reference/index.md#scope): a parameter's references include the named arguments that set it - an outline of modules, functions and variables, and links on `use <...>` paths - in VS Code, checks and hover help for the fields of `kerf.json` - formatting with [`kerf fmt`](https://kerfscript.mylasertools.com/docs/cli/index.md#commands) Libraries next to a design, as in `use `, are read from its folder, including unsaved changes in open editors. A design is checked in a worker thread and stopped after 10 seconds (`kerf lsp --timeout ` changes that), so a slow design never holds up typing. ## VS Code Install the KerfScript extension. It comes with the language server, so nothing else is needed, and formats on save; turn that off with `"[kerfscript]": { "editor.formatOnSave": false }` in your settings. The extension is in [`editors/vscode`](https://github.com/mylasertools/kerfscript/tree/main/editors/vscode). To build it yourself: ```sh cd editors/vscode npm ci npx vsce package code --install-extension kerfscript-0.1.0.vsix ``` ## Other editors Any editor with a Language Server Protocol client can run the server. Install the [command line](https://kerfscript.mylasertools.com/docs/cli/index.md) so `kerf` is on the path, and start it as `kerf lsp`, which talks over standard input and output. The settings below are where to start. They follow each editor's documentation but have not been tried in those editors; the server itself is tested through the protocol, in VS Code and as the standalone `kerf` binary. ### Neovim (0.11 or later) ```lua vim.filetype.add({ extension = { kerf = 'kerfscript' } }) vim.lsp.config('kerf', { cmd = { 'kerf', 'lsp' }, filetypes = { 'kerfscript' }, root_markers = { '.git' }, }) vim.lsp.enable('kerf') ``` ### Helix In `languages.toml`: ```toml [language-server.kerf] command = "kerf" args = ["lsp"] [[language]] name = "kerfscript" scope = "source.kerfscript" file-types = ["kerf"] comment-token = "//" block-comment-tokens = { start = "/*", end = "*/" } indent = { tab-width = 2, unit = " " } language-servers = ["kerf"] ``` ### Emacs (Eglot) ```elisp (define-derived-mode kerfscript-mode prog-mode "KerfScript" (setq-local comment-start "// ")) (add-to-list 'auto-mode-alist '("\\.kerf\\'" . kerfscript-mode)) (with-eval-after-load 'eglot (add-to-list 'eglot-server-programs '(kerfscript-mode "kerf" "lsp"))) ``` ### Sublime Text With the [LSP](https://packagecontrol.io/packages/LSP) package, and the [TextMate grammar](https://github.com/mylasertools/kerfscript/blob/main/editors/vscode/syntaxes/kerfscript.tmLanguage.json) installed as a syntax, in `LSP.sublime-settings`: ```json { "clients": { "kerf": { "enabled": true, "command": ["kerf", "lsp"], "selector": "source.kerfscript" } } } ``` ## In your own tools The language server's answers come from `@mylasertools/kerfscript/language`, which works in browsers too: see [Language service](https://kerfscript.mylasertools.com/docs/javascript/index.md#language-service). --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/mcp/ # AI assistants The KerfScript MCP server lets an AI assistant write and check laser-cutting designs. Through the [Model Context Protocol](https://modelcontextprotocol.io) it reads this documentation and the examples, finds a design's settings and renders it to SVG or DXF, so the assistant can try its code before giving it to you. It has four tools: | Tool | What it does | | ----------------------- | ------------------------------------------------------------------------------ | | `kerfscript_docs` | Reads the guides and the language reference | | `kerfscript_examples` | Finds examples by title or tag and returns their source | | `kerfscript_parameters` | Lists a design's settings with their ranges and choices | | `kerfscript_render` | Renders a design to SVG or DXF, with its parts, sizes, layers and any warnings | ## Connecting The hosted server needs no account or key: ```text https://mcp.mylasertools.com/mcp ``` **Claude Code** ```sh claude mcp add --transport http kerfscript https://mcp.mylasertools.com/mcp ``` **Claude** (desktop and web): Settings → Connectors → Add custom connector, and enter the address above. **Cursor**: add it to `.cursor/mcp.json`: ```json { "mcpServers": { "kerfscript": { "url": "https://mcp.mylasertools.com/mcp" } } } ``` **VS Code**: add it to `.vscode/mcp.json`: ```json { "servers": { "kerfscript": { "type": "http", "url": "https://mcp.mylasertools.com/mcp" } } } ``` Other assistants that support remote MCP servers take the same address. ## Running it yourself The same server runs on your computer with Node.js 20 or newer. Designs then never leave your machine: ```json { "mcpServers": { "kerfscript": { "command": "npx", "args": ["-y", "@mylasertools/kerfscript-mcp"] } } } ``` In Claude Code: ```sh claude mcp add kerfscript -- npx -y @mylasertools/kerfscript-mcp ``` ## Limits A design may be up to 64,000 characters, with up to 20 libraries of its own passed alongside it. It cannot read files or fetch anything from the network. Each render stops at 100,000 steps, 100 parts and 10 sheets, and text uses the bundled Atkinson Hyperlegible font. For bigger designs or other fonts, use the [command line](https://kerfscript.mylasertools.com/docs/cli/index.md) or the [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md). The hosted server allows each client about 60 requests a minute and 1,000 a day. It does not store or log the designs you send. A render that succeeds does not mean the parts fit. Check a new design against your material with a [test piece](https://kerfscript.mylasertools.com/docs/library/index.md#calibration) before cutting it. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/webassembly/ # WebAssembly `kerfscript.wasm` runs KerfScript from any language that can host a WebAssembly module: Python, Rust, Go, C#, Java, Ruby and others. It is a WASI (preview 1) module built from the same source as the JavaScript package, so it makes the same SVG and DXF files, byte for byte. For JavaScript applications, use the [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md) instead: it is smaller and faster. ## Getting the module Download `kerfscript-.wasm` from the [releases](https://github.com/mylasertools/kerfscript/releases) page and save it as `kerfscript.wasm`, or get `kerfscript-wasm-.zip`, which has the module with this guide and its licences. You can also build it from the repository with [Javy](https://github.com/bytecodealliance/javy): ```sh npm run build npm run wasm # writes dist/kerfscript.wasm ``` Set `JAVY` to the javy binary if it is not on the `PATH`. ## How it works The module reads one JSON request from standard input, writes one JSON response to standard output and exits. Start a fresh instance for each request. From a shell, with [Wasmtime](https://wasmtime.dev): ```sh echo '{"source": "rect([80, 50]);"}' | wasmtime run kerfscript.wasm ``` ### Request | Field | Value | | ------------- | --------------------------------------------------------------------------- | | `command` | `"render"` (default), `"params"`, `"version"` or `"licenses"` | | `source` | The program, as a string | | `params` | Parameter values keyed by name | | `files` | Libraries for `include` and `use`, keyed by path: `{"lib/tab.kerf": "..."}` | | `fonts` | Font files keyed by name, base64 encoded. `Atkinson` is built in | | `defaultFont` | Name to use when `text()` does not specify a font | | `kerf` | Kerf override for every part, in millimetres | | `layout` | `"pack"` (default) or `"none"` | | `sheet` | Sheet size `[width, height]` in millimetres | | `gap` | Space between packed parts | | `margin` | Space between the parts and the sheet edge | | `guides` | `true` to include guide layer geometry | These match the [render() options](https://kerfscript.mylasertools.com/docs/javascript/index.md#options). A path in `files` can leave out `.kerf`, as in the program. ### Response Every response has `ok`. When it is `false`, `error` holds the message and `echoes` holds what the program echoed before it stopped. The module never exits with an error code for a bad program. - `render`: `svg` (all sheets, for preview), `width`, `height`, `parts`, `assembly`, `warnings`, `echoes` and `sheets`. `assembly` is the same as from [`render()`](https://kerfscript.mylasertools.com/docs/javascript/index.md): one entry per copy of a part, with its outline, thickness and placement. Each sheet has `width`, `height`, `material`, `thickness`, `svg` and `dxf`; write one file per sheet to cut. - `params`: `parameters`, as from [`parameters()`](https://kerfscript.mylasertools.com/docs/javascript/index.md). - `version`: `version` (package) and `language`. - `licenses`: `files`, the licence texts of the module and of everything built into it, keyed by file name: `LICENSE`, `COMMERCIAL-LICENSE.md`, `THIRD-PARTY-NOTICES.md`, `THIRD-PARTY-LICENSES.md` (the JavaScript engine and its Rust crates) and `OFL.txt`. The release ZIP has the same files. ## Python With the [`wasmtime`](https://pypi.org/project/wasmtime/) package: ```python import json, os, tempfile from wasmtime import Engine, Linker, Module, Store, WasiConfig engine = Engine() module = Module.from_file(engine, "kerfscript.wasm") linker = Linker(engine) linker.define_wasi() def kerf(request): with tempfile.TemporaryDirectory() as d: inp, out = os.path.join(d, "in.json"), os.path.join(d, "out.json") with open(inp, "w", encoding="utf-8") as f: json.dump(request, f) wasi = WasiConfig() wasi.stdin_file = inp wasi.stdout_file = out store = Store(engine) store.set_wasi(wasi) linker.instantiate(store, module).exports(store)["_start"](store) with open(out, encoding="utf-8") as f: return json.load(f) result = kerf({"source": "w = 80; rect([w, 50]);", "params": {"w": 120}}) for i, sheet in enumerate(result["sheets"]): with open(f"sheet-{i + 1}.svg", "w", encoding="utf-8", newline="") as f: f.write(sheet["svg"]) ``` Compile the module once and reuse it; each request only needs a new `Store`. ## Node.js The JavaScript package is the better fit in Node.js, but the module also runs under `node:wasi`, which is how the test suite checks it: ```js import { closeSync, openSync, readFileSync, writeFileSync } from 'node:fs'; import { WASI } from 'node:wasi'; const module = await WebAssembly.compile(readFileSync('kerfscript.wasm')); writeFileSync('in.json', JSON.stringify({ source: 'rect([80, 50]);' })); const stdin = openSync('in.json', 'r'); const stdout = openSync('out.json', 'w'); const wasi = new WASI({ version: 'preview1', stdin, stdout, returnOnExit: true }); wasi.start(await WebAssembly.instantiate(module, wasi.getImportObject())); closeSync(stdin); closeSync(stdout); console.log(JSON.parse(readFileSync('out.json', 'utf8')).svg); ``` ## Performance The module carries its own JavaScript engine (QuickJS), so it is about 2 MB and renders roughly three to five times slower than Node.js. The example designs take well under a second. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/import/ # Importing SVG drawings `kerf import` turns an SVG drawing into a KerfScript program that cuts the same shapes. Use it to make an existing design parametric, or to continue a design drawn in Inkscape, Illustrator or a box generator. ```sh kerf import ornament.svg # writes ornament.kerf, if there is none kerf import ornament.svg -o tree.kerf kerf import ornament.svg -o - # prints the program ``` In the [playground](https://kerfscript.mylasertools.com/playground/), choose **Import SVG** or drop an SVG file on the editor: the program opens in the editor, and the console says what was found. The importer looks for the shapes KerfScript has modules for and writes those modules instead of a list of points: | In the drawing | In the program | | -------------------------------------------------- | ----------------------------------------------- | | Circles, ellipses, rectangles, rounded rectangles | `circle()`, `ellipse()`, `rect()` | | Regular polygons and stars | `regular_polygon()`, `star()` | | A small round hole high up in the middle of a part | `hanging_hole()`, or two of them with `spacing` | | A round hole with a slot, for hanging on a screw | `keyhole()`, or `keyholes()` for a level pair | | A ring around a hole on the edge of an ornament | `hanging_loop()` | | A rectangle with tabs | `tab_rect()`, or `tabs()` on each side | | A row of slots the tabs go through | `slots()` | | A rectangle with finger joints | `finger_rect()` | | Holes a divider's fingers go through | `finger_holes()` | | Fingers and notches from another program | `rect()` with the notches cut out, in loops | | The same hole repeated in a row or a grid | a `for` loop | | Anything else | `polygon()` or `path()`, exactly as drawn | For example, a sign with a hanger hole, a keyhole and a row of holes comes back as: ```kerf // sign.svg: imported from an SVG drawing. // 1 part; found 1 rectangle, 1 hanger hole, 7 circles, 1 hole pattern, 1 keyhole. use $layout = "none"; // keep the parts where the drawing has them translate([3, 3]) part("sign") { difference() { rect([120, 80], r = 5); hanging_hole([60, 70]); for (x = [30:10:90]) translate([x, 30]) circle(d = 3); translate([20, 60]) keyhole(); } } ``` Change a number and render it to get a new version of the design. ## Checked against the drawing Every part is run once it is written and compared with the drawing. A shape is only written as a module when its outline is within 0.05 mm of the drawn one; set this with `--tolerance`. When a part's modules do not match the drawing closely enough, the part is written as it is drawn, and a comment at the top of the program says so. A part that no shape can reproduce exactly is cut as the lines it is drawn with. Nothing in the drawing is left out because it was not recognised. ## Parts, holes and layers Each closed cut outline is a part, and the outlines inside it are its holes. An outline inside a hole is a part again: a piece that falls out. Parts are named after their group or element, as `id`, Inkscape label or Illustrator layer name, and otherwise numbered. Colours become [layers](https://kerfscript.mylasertools.com/docs/layers/index.md), as most laser software reads them: - Red lines are cut and blue lines are scored. Without red, the colour used most is cut. - Filled shapes without a line are engraved. A drawing with no lines at all is a silhouette, and its filled shapes are cut out. - Groups named cut, score, engrave or guide put their shapes on that layer, whatever their colour. Files that KerfScript wrote come back on the layers they were written from. - Lines in other colours go on a layer of their own, named after the colour, set to mark and not cut. Lines that meet end to end are joined. A loop of lines becomes a hole when nothing is inside it, and stays as cut lines when it holds marks or other lines, as a piece to press out does. Some drawings cannot be held as shapes and stay cut lines exactly as drawn: outlines that touch or cross a hole of the same part, holes that overlap, outlines that cross themselves or have a slit drawn into them, and closed paths with no area. Lines and marks that leave the part they start in are kept on their own, so the part does not trim them. In a silhouette, each filled shape is a piece of its own. Text keeps its words, size and alignment and is written as `text()` in the built-in font, with the drawing's font named in a comment; to use that font, add `font = "..."` to the `text()` and pass the font file to `kerf render` with `--font`. Text on the cut layer cuts the letters of that font, not the drawing's: convert it to paths in your editor to cut it as drawn. Embedded images and hidden elements are left out, and the comment at the top says how many. ## Material thickness and kerf The depth of tabs and finger joints gives the material thickness, written as `$thickness`. Change it and the joints follow. A part whose joints are deeper, such as a tenon through a thick leg, gets its own `material(thickness = ...)`. Many drawings are made for one laser: their cut outlines are moved out by half the beam's width, which rounds every outer corner a little. The importer finds that, moves the outlines back to the sizes they were designed at, and sets [`$kerf`](https://kerfscript.mylasertools.com/docs/kerf/index.md) so the program cuts the same. A part that cannot be moved back exactly, such as a strip as narrow as the kerf, keeps its outlines as drawn and is cut with `material(kerf = 0)`. Use `--kerf 0` to take the outlines as drawn, or `--kerf 0.15` to give the kerf yourself. ## Layout The program keeps the parts where the drawing has them, with `$layout = "none"`. With `--pack`, identical parts become one part with a quantity, and KerfScript arranges them on sheets as usual. ## Units Drawings in millimetres, centimetres, inches or points are read at their size. A drawing measured in pixels is read at 96 pixels per inch, or 72 for an Adobe Illustrator file; give another with `--dpi`. ## In JavaScript `importSvg(svg, options)` does the same in a browser or in Node.js, and returns the program as `source`. `svg` is the text of the drawing or the bytes of its file: ```js import { importSvg } from '@mylasertools/kerfscript'; const { source, summary, notes } = importSvg(svgText, { tolerance: 0.05 }); ``` `summary` says in words how many parts there are and what was found, and `notes` holds the remarks written at the top of the program. The options are `dpi`, `tolerance`, `kerf`, `pack` and `name`, the drawing's name for the first comment. See [JavaScript](https://kerfscript.mylasertools.com/docs/javascript/index.md#importsvg). --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/openscad/ # Coming from OpenSCAD KerfScript uses syntax familiar to OpenSCAD users, with features for laser cutting. Existing OpenSCAD files may need changes to run in KerfScript. This guide covers the shared syntax and the differences that affect a port. ## The same You can use familiar building blocks: - Modules, functions, `for`, `if`, `let`, `each`, list comprehensions, ranges and function literals. - `children()`, `children(index)` and `$children`. - Dynamically scoped `$` variables, plus `$fn`, `$fa` and `$fs` for curve resolution. - 2D shapes: `square()` or `rect()`, `circle()`, `polygon()` and `text()`. - Transforms: `translate`, `rotate`, `scale`, `mirror` and `multmatrix`. - Shape operations: `union`, `difference`, `intersection`, `hull` and `offset`. - `echo()`, `assert()`, `use <...>` and `include <...>`. - Customizer comments for ranges, choices, groups and captions. See the [language reference](https://kerfscript.mylasertools.com/docs/reference/index.md) for supported arguments. ## Different | Feature | OpenSCAD | KerfScript | | ----------- | ------------------------------------------------------------ | ---------------------------------------------------------- | | Geometry | 3D and 2D | 2D parts for laser cutting | | Output | 3D formats and 2D exports | SVG and DXF with operation layers | | Assignments | The last assignment in a scope applies throughout that scope | Assignments take effect in order | | Type errors | Many produce `undef` and a warning | Stop evaluation with an error | | `color()` | Sets a shape's colour | Use `cut()`, `score()`, `engrave()` or `layer()` | | Fonts | Looks up system fonts | Uses fonts supplied by the application or CLI | | Libraries | Can load local files | Loads the standard library and sources allowed by the host | Assignment order is especially important when adapting a design. A value assigned later in a KerfScript program does not change earlier geometry. ## Not in KerfScript KerfScript does not support `cube`, `sphere`, `cylinder`, `polyhedron`, `linear_extrude`, `rotate_extrude` or `projection`. It also does not support `minkowski`, `resize`, `color`, `import`, `surface` or `render`. Calls to these modules produce an error. For a design based on an extruded 2D outline, start with that outline and remove the extrusion. ## Only in KerfScript - **Operations:** `cut()`, `score()`, `engrave()`, `guide()` and `layer()`. - **Material settings:** `$kerf` for cut compensation and `$thickness` for material-dependent joints. - **Parts and layout:** `part()`, `material()`, quantities, `$sheet` and `$gap`. - **Joinery:** `finger_rect`, `finger_holes`, `tslot` and `living_hinge`. - **Paths:** `line`, `polyline`, `arc`, `path`, `stroke` and `outline`. - **Additional shapes:** `ellipse`, `regular_polygon`, `star` and rounded rectangles with `rect(r = ...)`. - **Text measurement:** `text_width()` and `text_metrics()`. ## Porting a 2D design 1. Keep the 2D geometry and remove extrusion or other 3D operations. 2. Assign shapes to cut, score or engrave layers. 3. Check repeated assignments and put each value before the code that uses it. 4. Supply the fonts used by `text()`. 5. Set the measured material thickness and kerf. Use the joinery modules where they suit the design. Render the result and check the dimensions and layers before exporting. The [layers](https://kerfscript.mylasertools.com/docs/layers/index.md), [kerf](https://kerfscript.mylasertools.com/docs/kerf/index.md) and [parts and sheets](https://kerfscript.mylasertools.com/docs/parts-and-sheets/index.md) guides cover those steps. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/compatibility/ # Laser software Export SVG or DXF from KerfScript, then set up the cutting job in your laser software. Check the imported size and layers before assigning power, speed and operation order. ## What is in the files **SVG** - Millimetre dimensions with a matching `viewBox`. - Named layer groups for engraving, scoring, cutting and any custom layers. - Red cut paths, blue score paths and black filled engraving. - Inner cut paths before outer outlines in the file. **DXF** - R12 polylines with millimetre units. - Named layers such as `CUT`, `SCORE` and `ENGRAVE`. - Engraving as outlines. Set the engraving layer to fill in your laser software. - One sheet per file. Use the CLI's `--split` option for several sheets. ## Checked | Program | Version | SVG | DXF | Checks | | -------- | ------- | ------ | ------ | -------------------------------- | | Inkscape | 1.4.2 | Passed | Passed | Dimensions, layers and SVG fills | Six reference models passed these import checks on 28 October 2026. Drawing dimensions matched within 0.02 mm, including kerf compensation. These results cover file imports; they do not establish a fit for a particular machine or material. ## Not checked yet LightBurn, xTool Studio, xTool Creative Space, RDWorks and LaserGRBL have not yet been verified with the reference models. Import handling can differ between programs, especially for units, layer colours and fills. ## Checklist for a new program Download the calibration square and box from the [models](https://kerfscript.mylasertools.com/models/) and check their imports: 1. **Size:** the calibration square's cut path is 100.15 mm wide with `$kerf = 0.15`, allowing for a finished 100 mm square. If the imported size differs, check the unit and DPI settings. 2. **Layers:** cutting, scoring and engraving can be assigned separate operations. 3. **Fill:** engraved text has filled letters. For DXF, select fill in the engraving layer's settings. 4. **Holes:** cut holes remain open and contain no engraving. 5. **Order:** the job preview cuts inner paths before outer outlines. 6. **Parts:** the box contains all six panels without overlaps. ## What the file does not decide Set power, speed, passes and focus for your machine and material. Check the job preview for engraving before cutting and holes before outer outlines; laser software can reorder the paths in a file. For multiple sheets, use a separate file and matching material settings for each sheet. If KerfScript has already applied kerf compensation, leave the laser software's kerf offset at zero. Test the job on scrap before cutting the final material. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/versions/ # Versions KerfScript 0.7.0 implements language version 0.3. The package version tracks the software release; the language version tracks the syntax and behavior of `.kerf` programs. Package updates can include bug fixes, export options and JavaScript API changes without changing the language version. Use `kerf --version` to see both numbers, or read the `VERSION` and `LANGUAGE_VERSION` exports in JavaScript. ## Before 1.0 The language may change before 1.0. Release notes identify changes that affect existing programs or their output, including geometry and text layout fixes. Removing or renaming a language feature requires a new language version. Check the changelog when upgrading a tool that renders saved designs. ## From 1.0 The compatibility policy for language version 1.x is to keep existing programs working throughout that series. New features will be added without changing existing syntax. Bug fixes that affect output will be listed in the changelog; other breaking changes will require language version 2.0. ## Pinning a version For repeatable exports, record the interpreter version, settings and font files used with a design. Applications that store designs should retain that information and compare the output before upgrading the interpreter. Pin the npm package to an exact version when your application needs a fixed renderer, for example `@mylasertools/kerfscript@0.7.0`. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/specification/ # Specification Language version 0.3. Copyright (c) 2026 MyLaserTools. This document is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). This document defines KerfScript programs precisely: what text is a program, what it evaluates to, and when it is an error. The [language reference](https://kerfscript.mylasertools.com/docs/reference/index.md) explains the same language for people writing designs and lists the built-in modules and functions; where the two differ, this document is the one to fix. The [conformance suite](../conformance/README.md) turns these rules into small programs with expected results. Each section below names the cases that test it. An implementation conforms to language version 0.3 when it passes every case. ## Notation Grammar rules use EBNF: `{ x }` repeats `x` zero or more times, `[ x ]` makes it optional, `|` separates choices and quoted text is literal. Rules in `lower_case` are syntax, built from the tokens in [Lexical structure](#lexical-structure); rules in `UPPER_CASE` are tokens. ## Source text A program is a string of Unicode text. Positions in messages are a line and a column, both counted from 1. Lines end at `\n`; a `\r` before it is whitespace. Columns count UTF-16 code units, as JavaScript strings and the Language Server Protocol do, so a character outside the Basic Multilingual Plane such as an emoji takes two columns. ## Lexical structure Cases: `lexical/*`. The text is read from start to end as a sequence of tokens. Whitespace (space, tab, `\r`, `\n`) and comments separate tokens and are otherwise ignored. ```ebnf LINE_COMMENT = "//" { any character except "\n" } ; BLOCK_COMMENT = "/*" { any character } "*/" ; (* ends at the first "*/" *) NUMBER = ( DIGITS [ "." { DIGIT } ] [ EXPONENT ] | "." DIGITS [ EXPONENT ] ) [ UNIT ] ; UNIT = "mm" | "cm" | "m" | "in" | "ft" ; (* not followed by a LETTER, DIGIT or "_" *) EXPONENT = ( "e" | "E" ) [ "+" | "-" ] DIGITS ; DIGITS = DIGIT { DIGIT } ; STRING = '"' { CHARACTER | ESCAPE } '"' ; (* CHARACTER is anything but '"', "\" and "\n" *) ESCAPE = "\" ( "n" | "t" | "u" HEX HEX HEX HEX | any other character ) ; NAME = ( LETTER | "_" | "$" ) { LETTER | DIGIT | "_" } ; (* ASCII letters and digits *) PATH = "<" PATH_CHARACTER { PATH_CHARACTER } ">" ; PATH_CHARACTER = LETTER | DIGIT | "_" | "." | "/" | "-" ; PUNCTUATOR = "<=" | ">=" | "==" | "!=" | "&&" | "||" | "+" | "-" | "*" | "/" | "%" | "^" | "<" | ">" | "!" | "?" | ":" | "=" | "." | "," | ";" | "(" | ")" | "[" | "]" | "{" | "}" | "#" ; ``` - Where two punctuators could start at the same place, the longer one is taken: `<=` is one token, not `<` and `=`. - A number token's value is the nearest IEEE 754 double. `5.` is the number 5. A number has no sign; `-1` is unary minus applied to `1`. - In a string, `\n` is a line break, `\t` a tab and `\uXXXX` the UTF-16 code unit with that hex value. A backslash before any other character gives that character, so `\"` is `"`, `\\` is `\` and `\q` is `q`. A `\u` not followed by four hex digits is an error. - A `PATH` token is only read directly after the name `use` or `include` (whitespace and comments may come between). Anywhere else `<` is a punctuator. - These names are keywords and cannot name a variable, function, module or parameter: `module function if else for let each use include true false undef`. - A name starting with `$` is a special variable (see [Special variables](#special-variables)). Errors, each at the position given: | Text | Message | Position | | ---------------------------------------- | -------------------------- | --------------- | | a `/*` with no `*/` after it | `Unclosed block comment` | the `/*` | | a string with no closing `"` on its line | `Unclosed string` | the opening `"` | | `\u` without four hex digits | `Bad \u escape` | the `\` | | a character that starts no token | `Unexpected character "c"` | the character | ## Syntax Cases: `errors/missing-*`, `lexical/keyword-as-name`. ```ebnf program = { statement } ; statement = ";" | "{" { statement } "}" | "module" NAME params body | "function" NAME params "=" expr ";" | ( "use" | "include" ) PATH [ ";" ] | "for" bindings body | "if" "(" expr ")" body [ "else" body ] | "let" bindings body | NAME "=" expr ";" | [ modifier ] NAME arguments body ; body = ";" | "{" { statement } "}" | statement ; modifier = "*" | "!" | "#" | "%" ; params = "(" [ param { "," param } [ "," ] ] ")" ; param = NAME [ "=" expr ] ; arguments = "(" [ argument { "," argument } [ "," ] ] ")" ; argument = [ NAME "=" ] expr ; bindings = "(" [ binding { "," binding } [ "," ] ] ")" ; binding = NAME "=" expr ; expr = "function" params expr | "let" bindings expr | or [ "?" expr ":" expr ] ; or = and { "||" and } ; and = equality { "&&" equality } ; equality = comparison { ( "==" | "!=" ) comparison } ; comparison = sum { ( "<" | "<=" | ">" | ">=" ) sum } ; sum = product { ( "+" | "-" ) product } ; product = unary { ( "*" | "/" | "%" ) unary } ; unary = ( "-" | "+" | "!" ) unary | power ; power = postfix [ "^" unary ] ; postfix = primary { arguments | "[" expr "]" | "." NAME } ; primary = NUMBER | STRING | "true" | "false" | "undef" | NAME | "(" expr ")" | list | record ; record = "{" [ field { "," field } [ "," ] ] "}" ; field = NAME "=" expr ; list = "[" "]" | "[" expr ":" expr [ ":" expr ] "]" | "[" item { "," item } [ "," ] "]" ; item = "for" bindings item | "if" "(" expr ")" item [ "else" item ] | "let" bindings item | "each" expr | "(" item ")" (* only when the item inside starts with for or if *) | expr ; ``` The grammar is read with one rule at a time from left to right, choosing the first choice that fits. Where that needs more than the next token: - A statement that starts with a name followed by `=` is an assignment. - `function` followed by a name is a function definition; followed by `(` it is a function value. - `let` followed by `(` is a `let` statement or expression. - In a list, a first item followed by `:` makes a range: `[0:5]`, `[0:2:10]`. With three parts the middle one is the step. - A `use` or `include` statement may end with `;` or not. - In arguments and bindings, the name before `=` can be any name, including a keyword; it only matters when it matches a parameter. - Binary operators on the same level group to the left: `a - b - c` is `(a - b) - c`. `^` groups to the right and binds tighter than unary minus on its left: `2 ^ 3 ^ 2` is 512 and `-2 ^ 2` is -4. `?:` nests to the right. - An `else` belongs to the nearest `if`. - A `{` where a value is expected starts a record; where a statement is, a block. A record that names a field twice is the error `Field is given twice`, at the second. A token that fits no rule is an error: `Expected , found ""`, or `found end of file`, at that token. ## Values Cases: `values/*`. | Type | Values | | -------- | -------------------------------------------------------------------------- | | number | IEEE 754 doubles, including infinity and NaN | | string | sequences of Unicode characters | | boolean | `true` and `false` | | undef | the one value `undef` | | list | ordered sequences of values | | range | a start, step and end number; `[a:b]` has step 1 | | function | a parameter list, a body expression and the scope the function was made in | | record | values by field name, in the order the fields were written (from 0.3) | `type_of(v)` gives the type's name as above. Values cannot be changed after they are made; every operation makes a new value. Strings count, index and iterate by Unicode character (code point): `len("😀")` is 1. ### Truth In a condition (`if`, `?:`, `&&`, `||`, `!`, a comprehension's `if`, `assert`), `false`, `0`, NaN, `""`, `[]` and `undef` are false. Every other value is true, including `[0]`, `"0"`, ranges and functions. ### Equality `a == b` is true when: - both are numbers, strings or booleans of the same type and value (so `1 == "1"` and `0 == false` are false, and NaN is not equal to itself), - both are `undef`, - both are lists of the same length whose items are equal in order, - both are ranges with the same start, step and end (`[0:2] == [0:1:2]`), - both are records with the same field names whose values are equal, in any order, - or both are the same function value. `a != b` is `!(a == b)`. ### Writing values `echo()` and `str()` write values as text. A number is written as an integer when it is one, and otherwise with up to 10 significant digits (`1 / 3` is `0.3333333333`, `0.1 + 0.2` is `0.3`). Very large and small numbers use exponent form (`1e+21`); infinities are `Infinity` and `-Infinity`, NaN is `NaN`, and -0 is `0`. Lists are written as `[a, b]`, ranges as `[start:step:end]`, records as `{a = 1, b = "x"}`, functions as `function(a, b)` and `undef` as `undef`. Strings are written as they are at the top level and in double quotes, with escapes, inside a list or after a name. `echo()` writes its arguments in the order they are given, a named one as `name = value`. ## Expressions Cases: `operators/*`, `lists/*`, `functions/*`. ### Arithmetic | Operation | Operands | Result | | ---------------- | --------------------------------- | ------------------------------------------------------------- | | `a + b`, `a - b` | numbers | sum, difference | | | two strings (`+` only) | the strings joined | | | two lists of the same length | item by item | | `a * b` | numbers | product | | | list and number, either order | each item times the number | | | two lists of numbers, same length | dot product | | | matrix (list of rows) and list | each row times the list | | | list and matrix | the list times each column; the matrix needs one row per item | | `a / b` | numbers; list and number | quotient; each item divided by the number | | `a % b` | numbers | remainder, with the sign of `a` (`-7 % 3` is -1) | | `a ^ b` | numbers | `a` to the power `b` | | `-a` | number or list | `a * -1` | | `+a` | anything | `a` unchanged | Arithmetic follows IEEE 754: `1 / 0` is infinity and `0 / 0` is NaN. Any other combination of operands, including `undef`, booleans and lists of different lengths, is an error at the operator: `Can't add boolean and number`, `Can't add lists of length 2 and 3`. ### Comparison and logic `<`, `<=`, `>` and `>=` compare two numbers, or two strings by UTF-16 code unit (so `"B" < "a"`). Any other pair is an error: `Can't compare number < string`. Comparisons give `true` or `false`; `1 < 2 < 3` compares the boolean `true` with 3 and is an error. `!a` is `true` when `a` is false and `false` otherwise. `a && b` and `a || b` give `true` or `false`, never one of their operands, and skip `b` when `a` decides the result, so `false && missing` is `false` without an error. `c ? a : b` evaluates only the chosen side. ### Names A name in an expression is looked up in this order: 1. a variable of that name in the current scope or a scope around it (special variables follow the caller chain, see [Special variables](#special-variables)); 2. a function definition of that name, which gives a function value; 3. for a name starting with `$`, `undef`; 4. otherwise the error `Unknown variable "name"`. ### Indexing and members `a[i]` needs a number `i` (otherwise `List index must be a number, got `), rounded down. On a list it gives the item at that position from 0; on a string, the character; on a range, `[start, step, end][i]`. An index outside the list, a negative index and an index into any other value give `undef`. `a.x`, `a.y` and `a.z` are `a[0]`, `a[1]` and `a[2]` on a list. `r.begin`, `r.step` and `r.end` read a range. On a record, `r.name` and `r["name"]` read its field, `undef` when it has none of that name. Any other member gives `undef`. `len(r)` counts a record's fields, `keys(r)` lists their names in order, and `merge(a, b)` is a record with both's fields, `b`'s where both have one. A record is true in a condition. ### Calls In `f(args)`, when `f` is a name: 1. If a variable named `f` is in scope and holds a function, its value is called. 2. Otherwise a function definition named `f` is called. 3. Otherwise a built-in function named `f` is called. 4. Otherwise, if a variable named `f` is in scope, the error is `"f" is a number, not a function` (naming the variable's type). 5. Otherwise the error is `Unknown function f()`. So a variable that does not hold a function never hides a function or a built-in of the same name: `len = 3; echo(len([1, 2]))` writes 2. (Before language version 0.3 any variable hid them.) Calling a value that is not a function in any other way is an error: `Can't call a number`. A module or function defined with the name of a built-in hides the built-in wherever the definition is visible, and the program warns `module rect hides the built-in rect(); rename it to use both`, with its line. The standard library's own definitions do not warn. Arguments are evaluated left to right in the caller's scope, then bound to parameters: - a named argument binds the parameter of that name; - the remaining parameters take positional arguments in order; - a parameter with neither takes its default, evaluated in the new scope so it can use the parameters before it, or `undef` without one; - positional arguments beyond the parameters are ignored with the warning `f(): takes at most N positional arguments, got M; the rest are ignored (line n)`; - a named argument that starts with `$` sets that special variable for the call; any other named argument that matches no parameter is ignored with the warning `f(): unknown argument "name" (line n)`. Built-in modules and functions bind arguments the same way, with the parameters listed in the signature table (`SIGNATURES` in the JavaScript API). Some of their parameters can only be given by name, as `r` in `rect(10, r = 2)`; `echo()` takes any named arguments. A function call evaluates its body expression in a new scope whose parent is the scope the function was defined or made in. A function value made with `function (params) expr` keeps the scope it was made in and reads its variables when it is called, so it sees later reassignments in that scope. ### Ranges A range value is made by `[start:end]` or `[start:step:end]`; all three must be numbers (`Range values must be numbers`). A range is not a list, but `for`, `each` and comprehensions go through its values: `start`, `start + step`, ... while not past `end`, with `end` included when it is reached within 1e-9 of a step. The range is empty when the step is 0 or points away from the end. A range with more values than the list limit is an error when it is gone through. ### List comprehensions Inside `[...]`, items produce zero or more values each: - `expr` produces its value; - `each expr` produces each value of a list, range or string, the value itself for any other value, and nothing for `undef`; - `for (a = A, b = B) item` produces `item` for each value of `A`, and inside it for each value of `B`, with the names bound in a new scope; - `if (c) item [else item]` produces the chosen item, or nothing; - `let (a = 1, b = a + 1) item` binds names in order in a new scope. `for` goes through a list, a range or the characters of a string. Any other value is gone through once as itself, and `undef` not at all. ## Statements and scope Cases: `scope/*`, `modules/*`. ### Evaluation A program is a block. Evaluating a block: 1. Every `module` and `function` definition in the block is registered first, so it can be called anywhere in the block, also before it. `use` statements in the block are processed in this step too (see [Libraries](#libraries)). A later definition with the same name replaces an earlier one. 2. Then each statement runs in order. A block has its own scope, created when the block starts. The bodies of `if`, `else`, `for` (once per value), `let`, modules, `{ ... }` blocks and module children each run in a new scope whose parent is the scope around them. ### Assignment `name = expr;` evaluates `expr` and binds `name` in the current scope from that point on. A later assignment to the same name in the same scope replaces the value for the statements after it. Reading a variable before its first assignment is an error (`Unknown variable "name"`). An assignment inside a block does not change a variable outside it. Assignments at the top level of the program are its parameters. A renderer can replace their values by name (render options `params`, `kerf render -D`); the replaced assignment's expression is then not evaluated. ### Control statements - `if (c) A else B` runs `A` when `c` is true, otherwise `B`. - `for (a = A, b = B) body` runs the body for each value of `A`, and inside that for each value of `B`, like a comprehension. - `let (a = 1, b = a + 1) body` binds names in order, then runs the body. ### Module calls A statement `name(args) children` calls a module: 1. A module definition named `name` in scope runs its body in a new scope whose parent is the scope it was defined in, with the arguments bound like a function call's. `children()` inside it runs the call's child statements in the caller's scope; `children(index)` runs only the child statement at that position from 0, or those at a list or range of positions (a position outside them runs nothing); `$children` is the number of child statements that are not assignments or definitions. A child statement run again in the same call, with the same special variables, `place()` frame and step, gives the same shape and may be reused rather than run again; one that echoes, warns, makes parts, hardware or steps is always run again. 2. Otherwise a built-in module named `name` runs. 3. Otherwise the error is `Unknown module name().`, with a hint for OpenSCAD's `color()` and `square()`. A modifier changes the call: `*` skips it entirely, so its arguments and children are not evaluated; `#` and `%` move all of its shapes to the `guide` layer; `!` has no effect and is accepted for OpenSCAD files. `call(name, ...)` calls the module whose name is the string `name` (the first positional argument, or `name = ...`), user-defined or built-in, with the remaining arguments and the call's children, as if it were written `name(...)`. A `name` that is not a string is the error `call() needs a module's name as a string, got `; an unknown name is `Unknown module ()`. A user module named `call` hides this. ### Special variables A name starting with `$` is a special variable. It is looked up through the scopes of callers instead of the scopes around the definition: a module or function sees the value its caller had, even when the caller set it after the definition. Passing `$name = value` as an argument sets it for that call. An unset special variable is `undef`. These start with a value: | Name | Value | | ------------ | ------------ | | `$fn` | 0 | | `$fa` | 2 | | `$fs` | 0.2 | | `$thickness` | 3 | | `$kerf` | 0 | | `$material` | `"material"` | | `$sheet` | `[600, 400]` | | `$gap` | 3 | | `$layout` | `"pack"` | | `$scale` | 1 | `PI` is an ordinary variable set to π in the outermost scope. A number with a unit (language version 0.3) is a length in the real world: its value times 1 for `mm`, 10 for `cm`, 1000 for `m`, 25.4 for `in` and 304.8 for `ft`, divided by `$scale` where it is evaluated. A `$scale` that is not a number more than 0 is the error `$scale must be a number more than 0`. Cases: `values/units*`. ## Libraries Cases: `imports/*`. `use ` and `include ` read another program, a library. The renderer decides which text a path names: the built-in standard library answers paths that start with `kerf/`, and the application supplies the rest (the `resolve` render option). The command line, the desktop app and the language server look in the design's project, the nearest folder at or above the design's with a `kerf.json`: `` is a file of the project when `name` is the project's own name, or of the installed library in `kerf_libraries/name/`; an absolute path is that file; any other path is a file in the design's folder. Each tries `path`, then `path.kerf` ([Shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md)). From language version 0.3, a `use` or `include` inside a library whose path has a folder, as `lib/props`, first asks for the path beside it, `lib/` joined with the path (`..` going up a folder), and only when that names nothing for the path as written. A path that names nothing is the error `Library not found`. Each path is read and parsed once per render. `use ` makes the library's modules and functions available in the block containing the `use`, from the start of that block: - The library's top-level assignments are evaluated, in order, in a scope of its own whose parent is the block that uses it. Its functions and modules see those variables; the program does not. - The library's other statements, including `echo()` and shapes, do not run. - Files the library `include`s are part of the library, each once. - Using the same library again has no further effect. `include ` runs the library's statements in place, in the current scope, as if its text were written there: its assignments, definitions and shapes all take effect. Including a file twice runs it twice. A library that uses or includes itself, directly or through others, is an error at the statement that closes the loop: `Libraries use each other in a loop: → → `, or `Library includes itself`. Positions inside a library carry its path: `(line 1, column 1 in )`. ## Errors Cases: `errors/*`, and the errors in every other group. An error stops the program. It has a message and, in nearly every case, a position, written after the message as `(line L, column C)`, or `(line L, column C in )` inside a library. The position is the start of the token the error is about: the name in a call that fails, the operator of an operation that fails, or the name of a built-in given an unusable argument. Output from `echo()` before the error is kept and reported with it. An unknown variable, function, module or named argument whose name is close to a known one (one edit for names of three to five characters, two for longer ones, or a difference only in case) adds `. Did you mean "name"?` to the message. An error inside a module or function call also records the call: its name and the position it was called from, innermost first. A call that is on the stack more than once, through recursion, is recorded once with a count. `formatError()` in the [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md#errors) and the command line show these under the source line. Errors come from: - text that is not a program (see [Lexical structure](#lexical-structure) and [Syntax](#syntax)); - unknown names, calls of values that are not functions, and operations on the wrong types (see [Expressions](#expressions)); - `assert(condition, message)` with a false condition: `Assertion failed`, followed by `: message` when there is one; - built-in modules and functions given unusable arguments; - limits. A program from someone else can run in a browser, so these are enforced: | Limit | Default | Message starts with | | --------------------------------------------------------- | ---------- | ------------------------------------------ | | Statements, calls and loop steps | 5,000,000 | `Stopped after N steps` | | Nested module calls | 1,000 | `Recursion too deep in module name()` | | Nested function calls | 2,000 | `Recursion too deep in name()` | | Items in one list, characters in one string | 1,000,000 | `a list of N items` / `a range of N items` | | Items in all lists and strings | 3,000,000 | `The lists and strings hold more than` | | Points in one shape a module makes | 2,000,000 | `The shapes have more than` | | Points in all shapes made, `children()` not counted again | 20,000,000 | `Making the shapes took more than` | The [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md#running-untrusted-code) lists every limit and how to change it. A warning does not stop the program. Warnings are collected in order, each message once, and returned with the output. Examples are unknown named arguments, positional arguments past the parameters and a program that makes no shapes. Running out of interpreter stack in a call that calls itself, before the limit on nested calls, is the same error as reaching the limit, at the call that went one level too deep. ## Output Cases: `shapes/*`. A program's shapes are 2D regions and lines, each on a layer (`cut` unless placed on another), grouped into parts. The [language reference](https://kerfscript.mylasertools.com/docs/reference/index.md#layers) describes layers, parts, materials, kerf compensation and sheet packing, and lists the built-in modules that make shapes. Shapes outside every `part()` form one part named `part`. Parts placed in 3D compose with every enclosing `place()`, outermost first, then the part's own `at` and `rotate`, then for `stack = n` a move of k thicknesses along the part's z for its k-th layer. A part with `glue` is placed on its host's face instead, after every part has run. `step()` numbers steps from 1 in the order they first run; a part or hardware outside every step has step 0. A `step(text)` run inside another step is the step named `outer: text`, where `outer` is the enclosing step's name; inside `step(..., substeps = false)` an inner `step()` only runs its children, in the outer step. Cases: `parts/*`. ## Known differences These are behaviours of KerfScript 0.4 that this specification records as they are, but that may change before language version 1.0: - An expression nested so deeply that the interpreter runs out of stack, outside any recursion, stops with `Recursion too deep: the program calls itself without stopping` and no position. --- > Canonical HTML: https://kerfscript.mylasertools.com/docs/reference/ # KerfScript language reference Language version 0.3. Copyright (c) 2026 MyLaserTools. This document is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/). "KerfScript" is a trademark of MyLaserTools, see [TRADEMARKS.md](https://github.com/mylasertools/kerfscript/blob/main/TRADEMARKS.md). This reference covers KerfScript syntax, built-in modules and functions. For a first project, follow [Getting started](https://kerfscript.mylasertools.com/docs/getting-started/index.md). The [specification](https://kerfscript.mylasertools.com/docs/specification/index.md) gives the formal grammar and the exact rules for scope, evaluation, libraries and errors. Programs build 2D shapes, assign them to cut, score or engrave layers, and group them into parts. Rendering applies kerf compensation, arranges the parts on sheets and produces SVG or DXF files. Lengths are in millimetres and angles are in degrees. Positive x points right; positive y points up. ```kerf // A coaster with a hole, an engraved ring and a scored border. size = 90; // [60:5:120] difference() { circle(d = size); circle(d = 6); } engrave() difference() { circle(d = size - 16); circle(d = size - 22); } score() circle(d = size - 6); ``` ## Lexical structure - Comments: `// line` and `/* block */`. - Numbers: `12`, `0.5`, `.5`, `1e3`. - Strings: `"text"` with `\n`, `\t`, `\"`, `\\` and `\uXXXX` escapes. - Names: letters, digits and `_`, not starting with a digit. Names starting with `$` are **special variables** (see Scope). - Keywords: `module function if else for let each use include true false undef`. ## Values | Type | Examples | | -------- | ------------------------------------------------------------ | | number | `3`, `-0.25`, `PI` | | string | `"Oak"` | | boolean | `true`, `false` | | undef | `undef`, a missing argument, an index past the end of a list | | list | `[1, 2]`, `[[0, 0], [10, 0], [5, 8]]` | | range | `[0:5]` (0 to 5 step 1), `[0:2:10]` (0 to 10 step 2) | | function | `function(x) x * 2` | | record | `{kind = "armchair", x = 10, items = []}` | Points are lists `[x, y]`. `p.x` and `p.y` read the first two items. A record holds values by name. `s.kind` (or `s["kind"]`) reads a field, and a field it doesn't have reads as `undef`, so a table of scene entries can't put a value in the wrong place: ```kerf chair = {kind = "armchair", x = 10, items = ["cushion"]}; echo(chair.kind, chair.y); // armchair, undef moved = merge(chair, {x = 40}); // the same with x = 40 echo(keys(moved), len(moved)); // ["kind", "x", "items"], 3 ``` Records are equal when they have the same fields with equal values. ## Expressions Operators are listed from lowest to highest precedence. Use parentheses to change the order of evaluation. | Operator | Meaning | | ------------------- | -------------------------------------------------------------------------------------------------------------- | | `c ? a : b` | conditional | | `\|\|` | or | | `&&` | and | | `==` `!=` | equality, lists compare item by item | | `<` `<=` `>` `>=` | compare numbers or strings | | `+` `-` | add or subtract numbers or equal length lists; `+` joins strings | | `*` `/` `%` | multiply, divide, remainder; list times number scales; list times list is the dot product; matrix times vector | | `-x` `+x` `!x` | negate, same, not | | `a ^ b` | power, right associative and tighter than unary minus | | `f(x)` `a[i]` `a.x` | call, index, member | Unsupported combinations, such as a list plus a number or `undef + 1`, produce an error at the source line. In conditions, `false`, `0`, `""`, `[]` and `undef` count as false. Use `let (a = 1, b = a + 1) expr` to define names for one expression. Each binding can use the ones before it. ### List comprehensions Build a list with `for`, filter items with `if`, or insert the contents of another list with `each`: ```kerf squares = [for (i = [1:5]) i * i]; odd = [for (i = [1:9]) if (i % 2) i]; grid = [for (x = [0:2], y = [0:2]) [x * 10, y * 10]]; flat = [each [1, 2], 3]; ``` ## Statements Assignments and function definitions end with a semicolon. Modules create geometry and can receive child statements. In the syntax below, `...` stands for the body of a statement: ```kerf name = expr; // assignment function name(a, b = 2) = expr; // function module name(a, b = 2) { ... } // module (makes shapes) name(args) child; // module call with one child name(args) { child; child; } // module call with a block of children for (i = range_or_list, j = ...) ... // repeats, combined as a union if (cond) ... else ... let (a = 1) ... use // import modules and functions include // run another file in place ``` Arguments can be positional or named: `rect([20, 10], r = 2)`. A library's own `use ` and `include ` look next to that library first, then where the main program's would: `lib/props.kerf` saying `use ` finds `lib/finish.kerf`. ### Lengths in real units A number written with a unit, as `140cm`, `2.5in`, `1.2m`, `30mm` or `3ft`, is a length in the real world: it comes out in millimetres divided by `$scale`. With `$scale = 12`, `140cm` is 116.67 mm, so a dollhouse and a book-nook version come from one model. Plain numbers are millimetres on the sheet and never scale: keep `$thickness`, `$kerf` and joint sizes plain. A setting written with a unit is not shown in the customizer. ### Scope - Assignments run **in order**. A later assignment to the same name replaces the earlier one from that point on. - `module` and `function` definitions are visible in their whole block, so you can call them before they are defined. - Blocks, loops and module bodies have their own scope. - Variables, functions and modules have separate names. A variable that does not hold a function never hides a function or built-in: after `len = 3;`, `len([1, 2])` is still 2. - **Special variables** (`$name`) are dynamically scoped: a value set by a caller is seen by everything it calls. Pass them as arguments too: `circle(5, $fn = 6)`. - Top-level assignments are the program's **parameters**. Renderers override them by name (`kerf render -D width=200`). ### Modifiers Place `*` before a module call to disable it. Use `#` or `%` to move its geometry to the guide layer. Guides are included only when requested in the render options. `!` is accepted for OpenSCAD files and has no effect. ### children() Inside a module, `children()` renders all child statements passed by the caller. `children(index)` selects one child by its position from 0, or several with a list of positions. `$children` is the number of children. ```kerf module hang_hole(d = 5) { difference() { children(); translate([0, -d * 1.5]) circle(d = d); } } ``` ## Special variables | Name | Default | Meaning | | ----------------- | ------------- | --------------------------------------------------------------------------- | | `$thickness` | `3` | material thickness, used by joinery and stored on parts | | `$kerf` | `0` | beam width; cut outlines are offset by half of it | | `$material` | `"material"` | material name; parts are packed per material and thickness | | `$sheet` | `[600, 400]` | sheet size for packing | | `$gap` | `3` | space between packed parts and around the sheet edge | | `$layout` | `"pack"` | `"pack"` packs parts onto sheets, `"none"` keeps your placement | | `$fn` `$fa` `$fs` | `0` `2` `0.2` | curve resolution: fixed segment count, or max angle and max segment length | | `$font` | | default font name for `text()` | | `$mirrored` | `false` | set inside a mirrored part and on the back face: `text()` reads right | | `$scale` | `1` | the model's scale: `140cm` is 1400 mm divided by `$scale` | | `$nest` | `false` | pack small parts into the holes of bigger ones | | `$registration` | `false` | print-and-cut marks in three corners of every sheet | | `$label_parts` | `false` | engrave each part's number on its back face, as in the parts list and steps | | `$min_width` | `1.5` | warn where a part's material is narrower than this; `0` turns it off | | `$bounds` | | set by `measure()` for the children after the one it measures | ## Layers Every shape is on a layer. Shapes not wrapped in a layer module are on `cut`. | Layer | Mode | Colour | Intended operation | | --------- | ----- | --------- | ---------------------------------------------------- | | `engrave` | fill | `#000000` | the area is engraved | | `score` | line | `#0000ff` | lines and shape outlines are marked, not cut through | | `cut` | cut | `#ff0000` | outlines are cut through, with kerf compensation | | `guide` | guide | `#00a0ff` | shown only when guides are requested | ```kerf cut() ... engrave() ... score() ... guide() ... layer("pocket", mode = "fill", color = "#00aa00") ... ``` Use `layer(name, mode, color)` to define a custom layer. Its mode is `"cut"`, `"line"`, `"fill"` or `"guide"`. Marks can go on a part's **back face** too: `engrave(side = "back")`, `score(side = "back")` or `layer(name, side = "back")`. Draw them where they are on the part seen from the top, as everything else; text on the back reads the right way round on the back. Each sheet with back marks gets a second job (`sheet.back`, or `name-back.svg` from the command line), the sheet turned over left to right: cut the sheet, turn it over without moving the parts, line it up on the same corner and run the back job. Files list layers in this order: engrave, custom fill layers, custom line and cut layers, score, cut, guide, so a line is never covered by a fill. `layer(name, order = n)` moves a custom layer among the others, lowest first; at the same order fills still go under lines. For printing on the parts before cutting, `layer(name, bleed = 2)` lets that layer run 2 mm past the part's edge instead of being clipped to it, and `$registration = true` puts marks in three corners of every sheet on a `registration` layer, for a print-and-cut machine to line up on. Set the actual operation order, power and speed in your laser software. See [Laser software](https://kerfscript.mylasertools.com/docs/compatibility/index.md) for import details. When a part is rendered: - The part's **material** is the union of its cut layer shapes. - Engrave, score and custom layers are clipped to the material, so nothing is marked inside holes or off the part. Score outlines become lines first. - Cut lines (from `line()`, `living_hinge()` and similar) are clipped to the material. - If the material breaks into more than one piece, the renderer warns. - In the output, holes and inner cuts come before outer outlines. ## Parts and sheets Use `part()` to name a component and set its quantity. Wrap parts in `material()` when they share a thickness, kerf and material name: ```kerf material(thickness = 4, kerf = 0.12, name = "birch") { part("side", qty = 2) finger_rect([100, 60], "out"); part("base") rect([100, 100]); } ``` - `part(name, qty = 1, at, rotate) { ... }` makes a named part. It records `$thickness`, `$kerf` and `$material` where it is called. `at = [x, y, z]` and `rotate = [x, y, z]` (degrees) place the part in the assembled view: rotated first, then moved, with its thickness along +Z. A part with copies takes one placement or a list with one per copy. `render()` returns the placed parts as `assembly` and warns when two of them collide. `rotate = orient(x, z)` turns the part's own x and z axes onto two directions; `euler(rotation(a) * rotation(b))` turns by `b`, then by `a`. - More about a part, all named: - `stack = n`: n layers glued at each placement, one thickness apart along the part's z. The parts list says how many make a stack. - `mirror = true`: the part is cut mirrored left to right (x becomes -x), and its text still reads the right way round. Make the left of a pair by mirroring the right. A list with one `true` or `false` per copy mirrors only those copies, `qty = 2, mirror = [false, true]`, so a pair of sides shows its art on the outside of both; the mirrored copies are listed as a part of their own, `(mirrored)`. - `bend = [x0, x1, angle]`, or a list of them: a living hinge from x0 to x1 along the part's x bends it by `angle` degrees in the assembled view, folding the far end up towards its top face (down for a negative angle). The hinge's middle layer keeps its length. - `on = "name"`: the part is glued to that bent part, and moves with the run of it under its centre. - `glue = "name"`, `face = "top"`: the part is glued flat on that part's top (or bottom) face; its `at` and `rotate` are measured on the face, and the parts list says what it is glued to. With as many copies as the part it is glued on, one goes on each. - `slots = ["name", ...]`: where the named parts' material passes through this part in the assembly, slots are cut through it the shape of their cross-section, at any angle, `clearance = 0.1` wider. Draw tabs on one part and let the parts they go into cut their own slots. A name may have `*` for any run of characters, and `"!name"` leaves out parts the names before it took: `slots = ["*", "!back"]` cuts a slot for every part that passes through except the back. A slot clears every layer, marks included, and a part that only touches the edge leaves no notch. - `place(at, rotate) { ... }` places the parts and hardware inside in the assembled view, as part()'s `at` and `rotate` do; their own placements are inside it. A library module wrapped in `place()` can be set anywhere. - `step(text) { ... }`: an assembly step. Parts and hardware inside are added in it, in the order steps first run; `render()` returns the steps' text as `steps` and each placed part's `step`. The 3D view shows the model built up a step at a time. A step inside a step is a sub-step, `"outer: inner"`; `step(text, substeps = false)` takes inner steps in. - `hardware(kind, from, to, size = 6, up, sides = 24, label)` lists hardware the assembly needs, from one point to another: a dowel, a screw, a magnet, paper. `size` is a diameter, or `[w, h]` for a flat section whose `h` points along `up` (by default up, or along y for something standing upright). A round one is drawn with `sides` sides, 3 to 128. `render()` returns it as `hardware`, and `kerf check` lists it. - Shapes outside any part form one part named `part`. - `material(thickness, kerf, name) { ... }` sets those special variables for its children. - With `$layout = "pack"`, parts are grouped by material and thickness and repeated `qty` times. They are arranged in rows on sheets of `$sheet` and may be rotated 90° to fit. Parts larger than the sheet produce a warning. A project's `kerf.json` can give `"sheet": [w, h]`, the laser's bed, for every design in it that does not set `$sheet`. - With `$nest = true`, small parts are packed inside the holes of bigger ones, as rings inside rings. See [Parts and sheets](https://kerfscript.mylasertools.com/docs/parts-and-sheets/index.md) for spacing, layout and export examples. ## Built-in modules ### Shapes | Module | Notes | | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | | `rect(size, center = false, r = 0)` | `size` is a number or `[w, h]`; `r` rounds the corners. `square` is an alias | | `circle(r)` or `circle(d = ...)` | | | `ellipse([rx, ry])` | | | `regular_polygon(n, r)` | corner radius `r` | | `star(n, r, inner)` | | | `polygon(points, paths)` | even-odd fill, `paths` lists point indexes per outline | | `path(d, fill = "evenodd", close = false)` | SVG path data; closed subpaths become shapes, open ones lines unless `close` | | `text(text, size = 10, font, halign, valign, spacing = 1)` | `halign`: left, center, right; `valign`: baseline, top, center, bottom | | `text(text, size, fit = width)` | no wider than `fit`: the size shrinks to fit, never above `size` if given | | `line(from, to)` | a line, not an area | | `line(from, to, width)` | a strip `width` wide between the points, an area | | `spline(points, width, steps = 8, closed = false)` | a smooth curve through the points; with `width`, an area along it | | `polyline(points, closed = false)` | | | `arc(r, start = 0, end = 90)` | arc line, angles in degrees | Supply geometry as children of a shape-processing module, for example `translate([10, 0]) circle(d = 5);`. Fonts used by `text()` must be supplied by the application or through the [command line](https://kerfscript.mylasertools.com/docs/cli/index.md). The playground, the command line and `kerfscript.wasm` have `Atkinson` (Atkinson Hyperlegible) built in. ### Transforms | Module | Effect | | ---------------------------- | ---------------------------------------------------------------------- | | `translate([x, y])` | Move geometry by x and y | | `rotate(a, center = [x, y])` | Rotate by `a` degrees around a point; the default centre is the origin | | `scale(v)` | Scale both axes by the number `v` | | `scale([sx, sy])` | Scale each axis separately | | `mirror([nx, ny])` | Reflect across a line through the origin, perpendicular to this vector | | `multmatrix(m)` | Apply a 2 × 3 transformation matrix | ### Combining | Module | Notes | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `union()` / `group()` | joins children | | `difference()` | removes every later child's area from every layer of the first child | | `intersection()` | keeps the area common to all children; lines of any child are clipped to the others' area, and a child of lines alone leaves only lines | | `fill_holes(min_area)` | fills holes smaller than `min_area` mm², or all of them | | `close(r = 0.5)` | closes gaps and notches narrower than `2 r`, keeping the outline | | `pattern(spacing, stagger = 0)` | repeats its first child over the area of the others, clipped at the edge | | `hatch(angle = 45, pitch = 5, width)` | parallel lines across the children's area, or stripes `width` wide | | `paint(layers)` | each child loses the area of every later one; child i goes on `layers[i]`. A child wholly on an area of its own layer warns | | `measure(draw = false)` | `$bounds` = `[x0, y0, x1, y1]` of the first child, for the children after | | `hull()` | convex hull; each named part inside gets its own hull | | `offset(r)` / `offset(delta, chamfer = false)` | grow (positive) or shrink areas; `r` rounds corners | | `stroke(width)` | turns lines into areas of that width, for cutting single line art | | `outline()` | turns areas into their outline lines | ### Joinery | Module | Notes | | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `finger_rect(size, edges, finger)` | rectangle with finger joints. `edges` is one value or `[bottom, right, top, left]`, each `"out"`, `"in"` or `"flat"`. An `"out"` edge keeps its corners, an `"in"` edge notches them; join an `"out"` edge to an `"in"` edge of the same length. Notch depth is `$thickness`; `finger` defaults to `max(3 * $thickness, 6)` and the count is always odd | | `finger_holes(length, edge = "out", finger)` | row of slots along x, centred on y = 0, that an `edge` finger edge of the same length plugs into (for shelves and dividers) | | `tslot(d = 3, length = 12, nut = [w, h], nut_at)` | T-slot for a screw and nut, opening at y = 0 and running up; subtract it from a panel edge | | `living_hinge(size, spacing = 1.5, bridge = 3, cut)` | straight-line flex pattern as cut lines filling `[w, h]` | ### Other | Module | Effect | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `echo(...)` | Report values without creating geometry | | `assert(condition, message)` | Stop with `message` when `condition` is false | | `children(index)` | Render a child passed to the current module; omit `index` to render all children | | `call(name, ...)` | Call the module named by the string `name` with the other arguments and the children: `call(entry.draw, 40, 30)` | ## Built-in functions Functions return values. Arguments can be positional or named with the parameter names below, as in `pow(x = 2, y = 10)` or `log(8, base = 2)`. An unknown name gives a warning. `concat`, `str` and `chr` take only positional arguments. ### Numbers and vectors | Function | Result | | ---------------------------------------------- | -------------------------------------------------------------------- | | `abs(x)`, `sign(x)` | Absolute value, or sign as -1, 0 or 1 | | `floor(x)`, `ceil(x)`, `round(x)` | Round down, up or to the nearest integer | | `sqrt(x)`, `pow(x, y)` | Square root, or x raised to y | | `exp(x)`, `ln(x)` | Exponential with base e, or natural logarithm | | `log(x)`, `log(base, x)` | Base-10 logarithm, or logarithm with a chosen base | | `sin(x)`, `cos(x)`, `tan(x)` | Trigonometric functions with angles in degrees | | `asin(x)`, `acos(x)`, `atan(x)`, `atan2(y, x)` | Inverse trigonometric functions, returning degrees | | `min(values)`, `max(values)` | Minimum or maximum of a list; also accept separate values | | `clamp(x, low, high)` | Limit x to the given interval | | `lerp(a, b, t)` | Interpolate between numbers or vectors; t = 0 gives a, t = 1 gives b | | `norm(v)` | Length of a vector | | `cross(a, b)` | Cross product; a scalar for 2D vectors or a vector for 3D vectors | ### Paths and rotations A path is a list of `[x, y]` points. | Function | Result | | -------------------------------------------------- | -------------------------------------------------------------------- | | `path_length(points)` | Length along the path | | `point_at(points, s)` | The point `s` along the path; past either end it carries on straight | | `heading_at(points, s)` | The path's direction `s` along it, in degrees from +x | | `path_points(d, tolerance)` | The points of SVG path data, a list of points for each subpath | | `rotation(angles)` | The 3 × 3 matrix of `[x, y, z]` degrees as `part()` turns; use `*` | | `euler(matrix)` | The `[x, y, z]` angles of a rotation matrix, for `rotate` | | `orient(x, z = [0, 0, 1])` | The angles that turn a part's own x and z axes onto two directions | | `spline_points(points, steps = 8, closed = false)` | The points of the smooth curve `spline()` draws | ### Lists and strings | Function | Result | | ------------------------------ | -------------------------------------------------------------------------------------------- | | `len(value)` | Number of items in a list, characters in a string or fields in a record | | `keys(record)` | A record's field names, in the order they were written | | `merge(a, b)` | A record with the fields of both, `b`'s winning | | `concat(...)` | Join lists and individual values into one list | | `reverse(value)` | Reverse a list or string | | `sum(values)` | Sum a list of numbers | | `slice(value, start = 0, end)` | Extract part of a list or string; `end` is exclusive and defaults to its length | | `str(...)` | Convert values to text and join them | | `chr(...)`, `ord(text)` | Convert Unicode code points to text, or read the first character's code point | | `upper(text)`, `lower(text)` | Change letter case | | `split(text, separator = " ")` | Split text into a list of strings | | `join(values, separator = "")` | Join values into a string | | `search(value, list)` | Index of the first equal item, or `undef` if absent; also searches characters in strings | | `contains(value, item)` | Whether a list contains an item or a string contains a substring | | `lookup(x, table)` | Interpolate in a table of `[x, y]` pairs; use the nearest endpoint outside the table's range | ### Types `is_num(value)`, `is_string(value)`, `is_bool(value)`, `is_list(value)`, `is_undef(value)`, `is_function(value)` and `is_record(value)` return a boolean. `type_of(value)` returns the type name as a string. ### Random values `rands(min, max, count, seed = 1)` returns a list of numbers between the bounds. The same seed gives the same sequence. ### Text measurements `text_width(text, size, font)` returns the advance width, including glyph spacing. `text_metrics(text, size, font)` returns `[advance, [[x0, y0], [x1, y1]], ascent, descent]`. With `fit = width` they measure the text at the size `text()` would fit it to. Both require the font to be available to the renderer, just like `text()`. ### Joint dimensions `finger_count(length, finger)` returns the odd finger count used by `finger_rect()` for an edge of that length. ## Customizer comments Top-level assignments with a literal number, string, boolean or numeric list can appear as editable settings. Comments supply captions, ranges, choices and groups: ```kerf /* [Size] */ // Outer width width = 120; // [40:5:400] depth = 80; // [200] shape = "round"; // [round:Round, square:Square] count = 3; // [2, 3, 4] /* [Hidden] */ seed = 7; ``` - `/* [Size] */` starts a group; the settings below it are listed under _Size_. - `// Outer width`, the comment line just above an assignment, is its caption. - `[40:5:400]` is a slider from 40 to 400 in steps of 5; `[200]` is a slider up to 200. - `[round:Round, square:Square]` is a menu of values with labels; `[2, 3, 4]` is a menu of values. - Settings after `/* [Hidden] */` are not listed. Nothing else may follow the brackets on the same line: with `// [40:5:400] min:step:max` the setting is a plain number field, not a slider. These comments follow OpenSCAD's customizer format. Read [Settings](https://kerfscript.mylasertools.com/docs/customizer/index.md) for supported controls, or use `kerf params` to inspect the settings in a file. ## Standard library Every renderer has these libraries built in. Load one with `use`, as `use `; [Standard library](https://kerfscript.mylasertools.com/docs/library/index.md) describes each module and its parameters. Libraries others publish are installed with `kerf add` and loaded by their name, as `use `; see [Shared libraries](https://kerfscript.mylasertools.com/docs/shared-libraries/index.md). | Library | Modules and functions | | ------------------ | --------------------------------------------------------------------------- | | `kerf/box` | `box([w, d, h], lid = false, finger)`, `divider([w, h], finger)` | | `kerf/tabs` | `tabs()`, `slots()`, `tab_rect()`, `tab_positions()`: tab and slot joints | | `kerf/hinge` | `flex_rect()`, `flex_box()`, `flex_panel()`, `bend_length()`: living hinges | | `kerf/dividers` | `grid_dividers()`, `slotted_divider()`: egg-crate dividers | | `kerf/hanging` | `keyhole()`, `keyholes()`, `hanging_hole()`, `hanging_loop()` | | `kerf/enclosure` | `enclosure()`, `enclosure_cutouts()`, `vents()`: boxes with a screwed lid | | `kerf/calibration` | `kerf_gauge()`, `slot_gauge()`, `joint_test()`: test pieces | | `kerf/joints` | `halved_pair()`: two profiles halved into each other | `box()` makes the six (or five) panels of a finger-jointed box as parts `bottom`, `lid`, `front`, `back`, `left`, `right`, from the outer size. `divider()` makes a divider with finger tabs on both sides. ## Output - **SVG**: millimetre units, one Inkscape layer group per KerfScript layer, colours as above, 0.1 mm hairline strokes, `data-part` on every path. - **DXF**: R12 (AC1009) polylines on layers named after KerfScript layers, with matching AutoCAD colours. Names are upper case with characters other than letters, digits, `_` and `-` as `_`; when two layers come out the same, the later one gets `_2`, `_3` and so on. The built-in layers keep `CUT`, `SCORE`, `ENGRAVE` and `GUIDE`, so a layer of your own named `Cut` becomes `CUT_2`. ## Limits The interpreter limits call depth to 1,000 for modules and 2,000 for functions. A range a loop or built-in function goes through is limited to `maxListLength` items (below). The default evaluation limit is 5,000,000 steps; applications can change it with `maxSteps`. Comparing lists with `==`, multiplying two lists, and built-in functions that go through a list or string take a step for each item; `len()` of a list, `ord()`, `type_of()` and the `is_` functions take one. Exceeding a limit stops evaluation with an error. Programs are also limited in how much memory they can ask for: | Limit | Default | What it counts | | ------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- | | `maxParts` | 10,000 | copies of all parts, adding up every `qty` | | `maxSegments` | 10,000 | segments in one circle, ellipse or arc, and corners in one polygon | | `maxListLength` | 1,000,000 | items in one list or range, or characters in one string, including the text `echo()` writes in one call | | `maxListItems` | 3,000,000 | items in all lists the program builds, plus 4 for each list, one for every 8 characters of the strings it builds or echoes | | `maxTextLength` | 2,000 | characters in one `text()` | | `maxPoints` | 2,000,000 | points in any one shape a module makes; all the shapes made along the way, plus 100 for each, may add up to ten times this | | `maxOutputPoints` | 1,000,000 | points in the exported files, counting every copy | | `maxSheets` | 200 | sheets in the layout | | `maxCollisionPairs` | 20,000 | pairs of touching placed parts checked for collisions; past it the check stops with a warning | Applications can change them with the `limits` render option. A list can hold the same list many times, as in `b = [a, a]`. `echo()`, `str()` and `join()` write every copy out, so their text can be far longer than the list; they stop with an error once it passes `maxListLength`. Geometry operations also take time. Applications that render user programs should run them in a worker with a timeout; see the [JavaScript API](https://kerfscript.mylasertools.com/docs/javascript/index.md#running-untrusted-code). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ # Models made with KerfScript Choose a model to see its parts, settings and source code. Customize it in the playground or download a cut file. - [Finger-jointed box](https://kerfscript.mylasertools.com/models/box/index.md): Finger-jointed box with a lid. - [Spur gear pair](https://kerfscript.mylasertools.com/models/gears/index.md): A pair of spur gears with involute teeth, cut from one sheet, made with spur_gear() from kerf/gears. - [Jigsaw puzzle](https://kerfscript.mylasertools.com/models/jigsaw/index.md): A jigsaw puzzle with interlocking pieces. Choose the size and piece count, or change the seed for a new pattern. - [Clock face](https://kerfscript.mylasertools.com/models/clock-face/index.md): A clock face: cut outline, scored minute marks, engraved hours. - [Hadal skeleton sculpture](https://kerfscript.mylasertools.com/models/hadal/index.md): Hadal: the skeleton of an imagined deep-sea ray on a display stand. Nine ribs slot onto a spine, which sits in two cradles on a plinth. The ribs lean more along the body, so the flat pieces look like a turning animal. - [Xenospora seed pod](https://kerfscript.mylasertools.com/models/xenospora/index.md): Xenospora: a seed pod from another planet. Twelve flat crescent fins stand around a crossed core between a base and a crown. The fins only look twisted: each one is flat, set at a tangent to the circle. - [Umbra shadow machine](https://kerfscript.mylasertools.com/models/umbra/index.md): Umbra: a shadow machine. Six flat fins stand on a base at different depths and angles. Lit by a small LED through a 1 mm pinhole, their shadows line up on a wall 900 mm away into one dragon standing on a rock. Each fin outline is the dragon's shadow projected back onto that fin's plane. - [Wave Cathedral](https://kerfscript.mylasertools.com/models/wave-cathedral/index.md): Wave Cathedral: a vault made of tides. Seventeen flat arches, each a little different, slot onto two notched rails on a base. Two crests run along the roof and the crown drifts from side to side, so the flat ribs read as a rolling wave. - [Morph contour sculptures](https://kerfscript.mylasertools.com/models/morph/index.md): Morph: one program, five sculptures. Flat contour layers stack on three dowels, with spacer plates between them; only each layer's outline turns and swells, so the stack becomes a bowl, an urn, a twisted tower, a lotus or a saddle. Pick a form, or choose custom and shape your own. - [Orbit planetary gears](https://kerfscript.mylasertools.com/models/orbit/index.md): Orbit: a planetary gear demonstrator. A crank turns the sun gear in the middle; three planets roll around it inside a fixed 72-tooth ring and carry the carrier arms with them. The carrier turns once for every four turns of the crank, in the same direction, while the planets spin the other way. - [Trace drawing machine](https://kerfscript.mylasertools.com/models/trace/index.md): Trace: a drawing machine. A crank turns the carrier, which rolls a 24-tooth gear around the inside of a fixed ring. A pencil held in that gear, off its centre, draws a hypotrochoid on paper laid on the base: with the 80-tooth ring, ten lobes that close after three turns of the crank. - [Stride walking linkage](https://kerfscript.mylasertools.com/models/stride/index.md): Stride: Theo Jansen's walking linkage on a standing board. Seven flat links turn on brass bushes in front of the board; turn the crank and the foot traces the scored path, lifting and stepping like a Strandbeest leg. - [Tidal wave automaton](https://kerfscript.mylasertools.com/models/tidal/index.md): Tidal: a hand-cranked wave automaton. Round cams sit off-centre on a square shaft, each turned a little further than the last, so as you turn the crank they lift seven fins one after another and the fins ripple like a wave. - [Ascent marble lift](https://kerfscript.mylasertools.com/models/ascent/index.md): Ascent: a hand-cranked marble lift. Turn the crank clockwise and a pocket in the laminated wheel carries one marble up the left side, over the top and down to an opening in the shroud on the right, where it drops out and rolls down a sloping chute into a covered catch box. - [Expanse sliding organizer](https://kerfscript.mylasertools.com/models/expanse/index.md): Expanse: a split-top organizer. Turn the disc rim at the front and the two trays on top glide apart, sideways and in step, to uncover a well in the middle; turn it back and they close over it again. Two pins on the disc push the trays' followers along fixed guide rails. - [Celestial geared orrery](https://kerfscript.mylasertools.com/models/celestial/index.md): Celestial: a hand-cranked geared orrery. The crank turns three drive gears pinned together on one axle; each meshes with an orbit gear on a fixed journal beside it, so the three planets circle a fixed sun at half, once and twice the crank speed, the other way. Stylized speeds, not astronomical. - [Flight moth automaton](https://kerfscript.mylasertools.com/models/flight/index.md): Flight: a moth automaton on a standing board. Turn the crank behind the board and a pin on the front disc pushes a slider up and down between two guide posts; two links from the slider rock the wings on their fixed roots, so both wings beat together. - [Bloom expanding flower](https://kerfscript.mylasertools.com/models/bloom/index.md): Bloom: a mechanical flower that opens. Turn the drive ring in the middle and six couplers push six sculpted petals straight out along their radial guides, all together, so the flower grows from 34 to 42 cm across; turn it back and the petals close again. - [Index Geneva sequencer](https://kerfscript.mylasertools.com/models/index-sequencer/index.md): Index: a hand-cranked Geneva sequencer. Turn the grip continuously and a pin on the driver arm drops into a slot of the wheel, turns it on by one station and slides out again. The wheel then stays locked while the driver goes round, so the numbered dial advances one step per turn, then waits. - [Vault three-dial puzzle box](https://kerfscript.mylasertools.com/models/vault/index.md): Vault: a puzzle box (not a safe) with three dials on its lid. Each dial turns a notched gate wheel under the lid; dial in the code and the three notches line up with the fingers of a sliding fence, so the knob slides the fence off its catch in the front wall and the lid lifts out. - [Lambda Chebyshev walker](https://kerfscript.mylasertools.com/models/lambda/index.md): Lambda: a four-legged walker on Chebyshev's lambda linkage. One square shaft turns four eccentrics a quarter turn apart; each drives a lambda whose far end follows an approximately level path before lifting and swinging back. The legs hang from those ends on flat soles. Turn the handle anticlockwise, seen from the handle side, to follow the walking motion. - [Crawl spider walker](https://kerfscript.mylasertools.com/models/crawl/index.md): Crawl: an eight-legged walker on Klann linkages. Each leg is a six-bar linkage: the crank drives a connecting arm held by a lower rocker, and the arm swings the leg, held at the top by an upper rocker. Turn the handle and each foot follows a walking path, then lifts and reaches forward. The legs are phased to support the body through the crank cycle. - [Inversor straight-line linkage](https://kerfscript.mylasertools.com/models/inversor/index.md): Inversor: Peaucellier and Lipkin's linkage of 1864, the first to turn a circle into an exactly straight line. Two long arms from a fixed pivot hold a rhombus of four short links; swing the handle on the input link and the pencil at the far corner draws a ruler-straight line along the scored scale. - [Echo pantograph](https://kerfscript.mylasertools.com/models/echo/index.md): Echo: a pantograph for copying a drawing larger. Four arms make a hinged parallelogram anchored to the board; trace the original with the stylus and the pencil draws it again, 1.5 to 4 times the size. Move the stylus and the pencil to the holes engraved with the same ratio. To copy smaller, swap them. - [Weave pintograph](https://kerfscript.mylasertools.com/models/weave/index.md): Weave: a pintograph, a drawing machine with two turning discs. The crank turns a small drive gear that meshes with both discs; the right one has a few more teeth than the left, so it slowly falls behind. An arm on a pin in each disc carries the pencil where the two arms cross, and it draws loop after loop, each a little different, weaving a cushion of lines. - [Tempo anchor escapement](https://kerfscript.mylasertools.com/models/tempo/index.md): Tempo: a model of a Graham deadbeat escapement. A falling weight turns the escape wheel; its teeth push the two pallets of the anchor in turn, and the anchor swings the pendulum and lets the wheel on by half a tooth each beat. The hand stands dead still between beats. Wind the knob behind, start the pendulum swinging and set the beat with the crutch bolt. - [Hours wooden gear clock](https://kerfscript.mylasertools.com/models/hours/index.md): Hours: a weight-driven wall clock with a wooden gear train between open skeleton plates. The weight turns the drum, the train turns the minute arbor once an hour, and a deadbeat anchor on the pendulum lets the escape wheel through half a tooth a beat. Wind it by turning the knob below the centre clockwise; set the time by turning the hands, which are a push fit. - [Tally odometer counter](https://kerfscript.mylasertools.com/models/tally/index.md): Tally: a hand-cranked counter with an odometer carry. Each turn of the crank moves the units drum on one digit; as any drum rolls from 9 to 0, two teeth on its side turn a pinion a quarter turn, and the pinion moves the next drum on one digit. In between, the pinions are locked, so the count in the lid's window can't slip. - [Cascade binary marble adder](https://kerfscript.mylasertools.com/models/cascade/index.md): Cascade: a marble computer that adds in binary. A column of rockers on a leaning board holds a number, one bit each: 1, 2, 4, 8 and 16 from the top. A marble that meets a 0 sets it to 1 and drops into the return channel; one that meets a 1 sets it back to 0 and rolls on to the next bit, the carry. Drop a marble into the slot marked 4 to add 4; lift the bar behind to reset. - [Helix Archimedes screw marble lift](https://kerfscript.mylasertools.com/models/helix/index.md): Helix: an Archimedes screw marble lift. Turn the crank and a screw of 120 stacked plates, each a step round from the last on a square bar, pushes 16 mm marbles up a trough of dowels. At the top they drop onto a chute and roll back down a zig-zag track behind the screw to a ramp that feeds the screw again near the bottom. - [Peck pecking hens](https://kerfscript.mylasertools.com/models/peck/index.md): Peck: the folk toy of hens pecking grain on a round paddle. Each hen's head rocks on a bolt through her body, and a string from her neck runs down through the paddle to a weight hanging below. Hold the handle and swing the paddle in small circles: the weight swings round, pulls each string in turn and the hens peck one after another, their tails bobbing up behind. - [Dabble duck automaton](https://kerfscript.mylasertools.com/models/dabble/index.md): Dabble: a hand-cranked duck automaton on an open cam box. Three cams on a square shaft lift three dowel pushrods through the deck: as you turn the crank the duck dips its head to the engraved pond while its tail tips up, lifts its head again, then raises its wings. The pushrods are only lifted; the duck's own weight keeps them on the cams, so turn it slowly. - [Wellspring saqiya water wheel](https://kerfscript.mylasertools.com/models/wellspring/index.md): Wellspring: a saqiya, the animal-driven water wheel al-Jazari drew in the 13th century. Turn the ox round its walk and the draw-bar turns a peg wheel above the gear pit; its dowel pegs drive a lantern pinion at right angles, and the wheel on the same axle lifts a ring of pots out of the well and tips them into the trough at the top. - [Fracture recursive relief](https://kerfscript.mylasertools.com/models/fracture/index.md): Fracture: a recursive vortex in relief. Each triangle drops its middle triangle and repeats in its three corners, then every point turns by an angle that grows with its distance from the centre. Deeper layers inset each opening a step further, so small holes stay shallow and large channels run down to a solid backer. - [Morphogen terrain relief](https://kerfscript.mylasertools.com/models/morphogen/index.md): Morphogen: a living-looking terrain drawn by a field. Two waves cross in a space that is twisted around an off-centre point and folded by two shears; each layer cuts away where the field rises above a level. Deeper layers use higher levels, so channels narrow and split into pockets towards the backer. - [Tilt maze](https://kerfscript.mylasertools.com/models/tilt-maze/index.md): Tilt maze: roll a ball bearing from the dot to the star. Every seed draws a new maze, and there is always exactly one way through. The maze layer is stacked as many times as the ball needs, under a clear cover. - [Layered island](https://kerfscript.mylasertools.com/models/topo-island/index.md): Layered island: a made-up mountain as a stack of contour layers, like a topographic map you can touch. The height of the land is a formula, and each layer is traced where the land crosses its height. Every layer is scored with the outline of the one above, so gluing up is easy. - [Fractal tree ornament](https://kerfscript.mylasertools.com/models/fractal-tree/index.md): Fractal tree: a module that calls itself. Each branch splits in two, a little shorter and thinner, until the twigs end in leaves. The seed bends every branch differently, so no two trees are the same. - [Round badge](https://kerfscript.mylasertools.com/models/round-badge/index.md): Round badge: text set around a circle, letter by letter. Each letter is measured, then placed and turned to follow the ring, so any words fit. A start for logos, coasters, seals and patches. - [Gift tags from a list](https://kerfscript.mylasertools.com/models/gift-tags/index.md): Gift tags from a list: type the names, get one tag each. Every name is measured and sized to fill its tag, short or long. Use it for place cards, keyrings or labels by changing the shape and the second line. - [SLOT desk sign](https://kerfscript.mylasertools.com/models/slot-desk-sign/index.md): SLOT, a first project: a desk sign in two pieces. A panel with a tab slips into a slot in a base. Change the word, the outline, the motif and the size; the tab, the slot and the base follow. The word and the motif are scored, so nothing falls out of the letters. - [Pocket Wildlife owl card](https://kerfscript.mylasertools.com/models/pocket-owl/index.md): Pocket Wildlife owl: a perched owl with raised wings in six slot-together pieces, on a 150 x 100 mm card. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. - [Pocket Wildlife dinosaur card](https://kerfscript.mylasertools.com/models/pocket-dinosaur/index.md): Pocket Wildlife dinosaur: a long-necked dinosaur with a ribbed body in six slot-together pieces, on a 150 x 100 mm card. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. - [Pocket Wildlife dragonfly card](https://kerfscript.mylasertools.com/models/pocket-dragonfly/index.md): Pocket Wildlife dragonfly: six legs, big eyes and two pairs of wings in seven slot-together pieces, on a 150 x 100 mm card. Each pair of wings drops over a tongue on the thorax. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. - [Drawer organizer](https://kerfscript.mylasertools.com/models/drawer-organizer/index.md): Drawer organizer: a finger-jointed tray from the box library, and a grid of dividers from the dividers library that slot into each other like an egg crate. Measure your drawer, then pick how many compartments you want. - [Tab-and-slot spice shelf](https://kerfscript.mylasertools.com/models/spice-shelf/index.md): Spice shelf: a wall shelf held together by tabs and slots, no glue or screws in the joints. The shelves and their front rails tab into the sides, and the sides tab into the back, which hangs on two screws by its keyholes. - [Project enclosure](https://kerfscript.mylasertools.com/models/project-enclosure/index.md): Project enclosure: a box for a small electronics project, its lid held by four M3 screws and nuts so it comes off again. The front has a switch and a power jack, the back a USB port, and the lid and the sides vents. Change the cutouts to suit your board. - [Solstice lantern](https://kerfscript.mylasertools.com/models/solstice/index.md): Solstice: a lantern with two sun walls and two moon walls, held between a base and an open crown by tabs. For a battery LED, not a candle. - [Winterlight ornaments](https://kerfscript.mylasertools.com/models/winterlight/index.md): Winterlight: three Christmas ornaments, a snow crystal, a fir grove under the moon and an eight-point star. Each is one piece with its own cord loop. - [Snowflake ornament](https://kerfscript.mylasertools.com/models/snowflake/index.md): A six-armed snowflake ornament. Each arm is one branch, repeated. - [Slot-together phone stand](https://kerfscript.mylasertools.com/models/phone-stand/index.md): A phone stand made from three slotted parts. Adjust the width, angle and material thickness to suit your phone. - [Guilloche coaster](https://kerfscript.mylasertools.com/models/guilloche-coaster/index.md): A round coaster with an interlaced guilloche pattern. Adjust the diameter, wave count and number of scored rings. - [Sunflower lamp panel](https://kerfscript.mylasertools.com/models/sunflower-panel/index.md): A lamp panel with a spiral of holes inspired by sunflower seeds. Adjust the panel size, hole count and hole sizes. - [Honeycomb trivet](https://kerfscript.mylasertools.com/models/honeycomb-trivet/index.md): A hexagonal trivet with a honeycomb pattern of cutouts. - [Engraved ruler](https://kerfscript.mylasertools.com/models/ruler/index.md): A ruler with scored millimetre ticks and engraved numbers. - [Living hinge card](https://kerfscript.mylasertools.com/models/hinge-card/index.md): Card with a living hinge down the middle, from the hinge library. - [Flex box with round corners](https://kerfscript.mylasertools.com/models/flex-box/index.md): Flex box: a box with round corners, its four sides one strip of plywood bent by living hinges. The bottom's tabs lock into slots along the strip, and the lid sits on top, kept in place by an insert glued under it. Bend the strip slowly; warm, slightly damp plywood bends best. - [Name sign](https://kerfscript.mylasertools.com/models/name-sign/index.md): Name sign: plate sized to the text, with two hanging holes. - [Hanging wall sign](https://kerfscript.mylasertools.com/models/wall-sign/index.md): Wall sign: a name or a word on a panel that hangs on screws, a nail or a cord, from the hanging library. The keyholes take a screw head; measure yours and set the sizes to match. - [Calibration square](https://kerfscript.mylasertools.com/models/calibration-square/index.md): A 100 mm square with a 20 mm hole for checking dimensions and kerf. Measure the outside and the hole after cutting to check compensation. See the kerf guide for the calibration steps. - [Kerf and fit test](https://kerfscript.mylasertools.com/models/kerf-test/index.md): A row of slots for testing the fit of your material. Try a scrap from the same sheet in each slot. Positive labels mean wider slots; negative labels mean narrower ones. Measure both kerf and sheet thickness before adjusting the design. - [Calibration set](https://kerfscript.mylasertools.com/models/calibration-set/index.md): Calibration set: cut it once for every new material, before anything that has to fit. The kerf gauge measures the beam, the slot gauge how wide a slot your sheet needs, and the joint pairs which kerf setting makes finger joints that hold. - [Tea pavilion dispenser](https://kerfscript.mylasertools.com/models/tea-pavilion/index.md): Tea pavilion: a packet dispenser with a plain gable roof and a front lip. Packet width, depth and stack height generate the enclosure dimensions. Assembly: glue the wall and floor joints, then glue one roof slope in place. Leave the other slope loose for refilling from above. - [Jewellery tray box](https://kerfscript.mylasertools.com/models/jewel-tray-box/index.md): Jewel tray box: an open box, lift-off lid and removable compartment grid. The lid locates on a glued inset plate; it is not a friction-fit hinge. Assembly: glue the box joints and lid layers. Leave the grid removable. - [Earring screen](https://kerfscript.mylasertools.com/models/earring-screen/index.md): Earring screen: a plain perforated panel plugs into a broad foot. Hole pairs, row spacing and sheet thickness are independent controls. Assembly: test the slot fit, then glue the panel tabs into the foot. - [Napkin bridge](https://kerfscript.mylasertools.com/models/napkin-bridge/index.md): Napkin bridge: two scooped cheeks stand in a slotted rectangular foot. The clear gap follows the napkin stack, rather than a fixed sheet size. Assembly: glue both cheeks into the foot after checking the slot fit. - [Post sorter](https://kerfscript.mylasertools.com/models/post-sorter/index.md): Post sorter: a row of tabbed dividers with a rising top line. Bay clearance and bay count set the base depth automatically. Assembly: glue each divider into the matching slots in the base. - [Coaster caddy](https://kerfscript.mylasertools.com/models/coaster-caddy/index.md): Coaster caddy: a small finger-jointed bin with an open front scoop. The interior follows the coaster diameter and stacked thickness. Assembly: glue the finger joints. Coasters are not included in the file. - [Tissue carton cover](https://kerfscript.mylasertools.com/models/tissue-cover/index.md): Tissue cover: an open-bottom sleeve sized around a refill carton. A rounded dispensing slot is cut into the finger-jointed top panel. Assembly: glue the top and four walls; slide it over the carton. - [Stem lab tube stand](https://kerfscript.mylasertools.com/models/stem-lab/index.md): Stem lab: two perforated decks hold removable glass tubes above a base. Tube diameter and pitch define the deck size and the side supports. Assembly: glue decks into side slots. Glass tubes are separate hardware. Water belongs inside the tubes; the plywood is not a watertight vase. - [Perforated display deck](https://kerfscript.mylasertools.com/models/paint-deck/index.md): Perforated deck: a raised shelf locates bottles or display eggs over a foot. Hole pitch, diameter and the grid count determine the footprint. An egg display is a parameter preset of this construction, not another model. Assembly: glue the two side supports into the foot, then add the deck. - [Miniature bench](https://kerfscript.mylasertools.com/models/miniature-bench/index.md): Miniature bench: a small seat and back tab into two shaped side supports. This is a display miniature. Dimensions are not a human seating design. Assembly: fit the seat and back between the sides, then glue the joints. - [Book brace pair](https://kerfscript.mylasertools.com/models/book-brace/index.md): Book brace: a bookend pair with tabbed uprights and triangular gussets. The foot extends beneath the books so their weight helps stabilize it. Assembly: glue each upright and gusset into its foot. Add non-slip pads. - [Cable harbour device tray](https://kerfscript.mylasertools.com/models/cable-harbour/index.md): Cable harbour: a divided device tray with cable exits through its back. Bay width, count and cable diameter generate the internal layout. Assembly: glue the box, then fit the dividers without glue for cleaning. - [Ribbon spool rail](https://kerfscript.mylasertools.com/models/ribbon-rail/index.md): Ribbon rail: a pair of uprights hold a removable wooden dowel over a base. Spool count and width set the space between uprights. Dowel not included. Assembly: glue the uprights into the base; pass the dowel through both. - [Bottle display tote](https://kerfscript.mylasertools.com/models/bottle-tote/index.md): Bottle tote: a compartment tray with a tall central handle partition. Bottle diameter and column count generate the footprint and handle span. Assembly: glue all joints and the handle partition to the floor. Use for light display bottles until the chosen material and joints are load tested. - [Pocket valet tray](https://kerfscript.mylasertools.com/models/pocket-valet/index.md): Pocket valet: a shallow tray with a removable key and watch perch. A tabbed upright holds a horizontal shelf with open key-ring notches. Assembly: glue the tray and upright joints; add felt if desired. - [Business card pocket](https://kerfscript.mylasertools.com/models/card-pocket/index.md): Card pocket: an open-top card tray with a scooped front and a taller back. Interior width and depth follow the cards and stack, with editable ease. Assembly: glue the five finger-jointed panels. Cards are not supplied. - [Plant display plinth](https://kerfscript.mylasertools.com/models/plant-plinth/index.md): Plant plinth: two crossing half-lapped frames support a circular top. Pot diameter sets the top and footprint; height sets the support frames. Assembly: cross the frames at their half-depth slots and glue on the top. Use as a small display stand after checking the material's load capacity. - [Small-parts drawer stack](https://kerfscript.mylasertools.com/models/drawer-stack/index.md): Drawer stack: a finger-jointed cabinet with separate sliding tray drawers. Drawer count and clearances generate the shelf levels and outer height. Assembly: glue the carcass and shelf tabs, then glue each drawer square. Sand the runners and test the slide clearance before applying a finish. - [Weaving loom and shuttle](https://kerfscript.mylasertools.com/models/loom-card/index.md): Loom card: a reusable small weaving frame with rounded yarn notches. Warp count and pitch generate equal notches along both short edges. Assembly: wind a continuous warp between opposite notches. The second part is a blunt shuttle with two open yarn pockets, not a cutting tool. - [Ribbed lantern shade](https://kerfscript.mylasertools.com/models/ribbed-lantern/index.md): Ribbed lantern shade: curved radial fins locate in two notched rings. Ring tabs are cut into each fin, so changes to thickness update both joints. Assembly: glue the fins into both rings. The shade stands on the fin ends. Shade only: place a separate cool battery LED inside, with no flame. - [Captive sliding tile puzzle](https://kerfscript.mylasertools.com/models/slide-grid/index.md): Slide grid: a solvably shuffled sliding puzzle with a removable clear retainer. Circular finger ports let tiles slide beneath the cover while keeping them in. Assembly: glue the plywood spacer to the floor. Add tiles, then bolt the acrylic cover on with thin washers for vertical play. Undo it to change tiles. - [Lift-out game box insert](https://kerfscript.mylasertools.com/models/game-insert/index.md): Game insert: lift-out card trays and token bins fill a measured game box. Card dimensions constrain the front bank; token bins fill the remaining area. Assembly: glue each bin separately. Leave all bins loose on the carrier floor. No game-specific artwork or component shapes are used. - [Articulated cable chain](https://kerfscript.mylasertools.com/models/cable-chain/index.md): Cable chain: alternating wide and narrow U-links pivot on bought side bolts. Each link has two cheeks and one tabbed floor bridge. The channel stays open. Assembly: glue each U-link square, then connect adjacent cheeks with short bolts, washers and locknuts. Keep the pivots loose enough to turn freely. This free-pivot prototype has no mechanical bend stops or machine rating. - [Cantilever sewing and tool case](https://kerfscript.mylasertools.com/models/cantilever-case/index.md): Cantilever case: two banks of trays rise outward on parallel pivot links. The pivot offsets and link lengths are computed from the tray dimensions. Assembly: glue the base and each tray square. Bolt the links outside the front and back walls, using washers and standoffs to separate link layers. Link pivots must turn freely. Do not glue the links to the trays. - [Beam balance with suspended pans](https://kerfscript.mylasertools.com/models/beam-balance/index.md): Beam balance: a stable pivoted beam with two level, cord-suspended pans. Glue the fork uprights into the foot and glue a rim onto each pan. Fit the beam on a loose pivot bolt, then tie three equal cords per pan. The tilt control demonstrates motion; it does not calculate equilibrium. - [Sector-lock tilting stand](https://kerfscript.mylasertools.com/models/sector-stand/index.md): Sector stand: a tilting deck on two trunnions, locked through curved slots. Glue the fixed cheeks into the foot, and the moving flanges and front lip into the deck. Bolt each flange to its cheek at the pivot and arc slot. Loosen both arc bolts before adjusting, then tighten them again. - [Laminated screw press](https://kerfscript.mylasertools.com/models/screw-press/index.md): Screw press: laminated platens and hand knobs with captive hex nuts. Clamp the lower platen onto four threaded rods using washers and nuts. Glue the knob laminations around measured hex nuts without gluing the threads. Put washers beneath the knobs and turn them evenly to press. A small craft press for paper and light glue-ups, not for heavy pressing. - [Independent wheel calendar](https://kerfscript.mylasertools.com/models/wheel-calendar/index.md): Wheel calendar: three independent numeric/date wheels on a tabbed stand. Glue the backboard into the foot, then bolt on the wheels with washers. Read the value beneath each fixed pointer. Set weekday, month and day by hand; there is no automatic month-length or leap-year mechanism. - [Cord-laced roll-up desk mat](https://kerfscript.mylasertools.com/models/slat-roll/index.md): Slat roll: a flexible desk mat assembled from repeated rigid wooden slats. Align the slats with the specified gap and lace two cords through their paired holes. Leave the spans between slats free to hinge; knot the ends. The curl control shows a bending pose, not an elastic-material simulation. - [Lock box with a key](https://kerfscript.mylasertools.com/models/lock-box/index.md): Lock box: a finger-jointed box whose lid locks with a key. The lock is three layers glued on the front: turn the key half a turn and its cam throws a bolt up into the keeper, a slotted plate under the lid's front edge, and the lid cannot lift. The lid's insert keeps it square on the walls. Made with box() from kerf/box and cam_lock() from kerf/lock. - [Stacked ring vase](https://kerfscript.mylasertools.com/models/ring-vase/index.md): Ring vase: a vase of stacked rings threaded on two crossed core plates, its outline a smooth curve from the foot to the lip. Rings of the same size are one part, so the cut list stays short. The base cuts its own slots for the core's feet. A glass test tube or a dried posy goes inside. Made with ring_stack() from kerf/rings. - [World map sign](https://kerfscript.mylasertools.com/models/world-map/index.md): World map sign: the world's land engraved on a panel, a heart where a place is (home, or where two people met), its name below, and a message on the back. Two keyholes hang it on the wall. Made with world_map() and geo_point() from kerf/geo, a symbol from kerf/symbols and keyholes() from kerf/hanging. - [Bent pen tray](https://kerfscript.mylasertools.com/models/pen-tray/index.md): Pen tray: a desk tray whose round-bottomed trough is one strip of plywood bent by a living hinge, between two end plates. The strip's tabs go through slots in the ends, which the ends cut for themselves from where the strip passes through them. Made with flex_strip() from kerf/hinge. - [Symbol name tags](https://kerfscript.mylasertools.com/models/symbol-tags/index.md): Symbol tags: key rings or bag tags, each with a pictogram and a name. The symbols come by name from the 867 in kerf/symbols; list them with symbol_names(), or one category with symbol_names("animals"). The names fit their tag, and the hole has a ring round it for the key ring. --- > Canonical HTML: https://kerfscript.mylasertools.com/models/box/ # Finger-jointed box Finger-jointed box with a lid. This design has 6 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=box). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/box/box.kerf) - [SVG](https://kerfscript.mylasertools.com/models/box/box.svg) - [DXF](https://kerfscript.mylasertools.com/models/box/box.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | bottom | 1 | 120 × 80 mm | | lid | 1 | 120 × 80 mm | | front | 1 | 120 × 50 mm | | back | 1 | 120 × 50 mm | | left | 1 | 80 × 50 mm | | right | 1 | 80 × 50 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `120` | 40 to 400 | | `depth` | `80` | 40 to 400 | | `height` | `50` | 20 to 300 | | `lid` | `true` | on or off | | `$thickness` | `3` | 1.5 to 10 | | `$kerf` | `0.15` | 0 to 0.5 | ## Source ```kerf // Finger-jointed box with a lid. use /* [Size] */ // Outer width width = 120; // [40:5:400] // Outer depth depth = 80; // [40:5:400] // Outer height height = 50; // [20:5:300] // Closed box with a lid lid = true; /* [Material] */ // Sheet thickness $thickness = 3; // [1.5:0.5:10] // Beam width, compensated on every cut $kerf = 0.15; // [0:0.01:0.5] box([width, depth, height], lid = lid); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/gears/ # Spur gear pair A pair of spur gears with involute teeth, cut from one sheet, made with spur_gear() from kerf/gears. This design has 2 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=gears). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/gears/gears.kerf) - [SVG](https://kerfscript.mylasertools.com/models/gears/gears.svg) - [DXF](https://kerfscript.mylasertools.com/models/gears/gears.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | large gear | 1 | 102 × 102 mm | | small gear | 1 | 48 × 47.2 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `teeth_large` | `32` | 12 to 80 | | `teeth_small` | `14` | 8 to 40 | | `tooth` | `3` | 1.5 to 6 | | `pressure` | `20` | 14.5, 20, 25 | | `bore` | `6` | 2 to 20 | | `spokes_from` | `24` | 12 to 100 | ## Source ```kerf // A pair of spur gears with involute teeth, cut from one sheet, made with // spur_gear() from kerf/gears. /* [Gears] */ // Teeth on the large gear teeth_large = 32; // [12:1:80] // Teeth on the small gear teeth_small = 14; // [8:1:40] // Tooth size (module) in mm tooth = 3; // [1.5:0.25:6] // Pressure angle pressure = 20; // [14.5, 20, 25] /* [Hub] */ // Shaft hole diameter bore = 6; // [2:0.5:20] // Lightening holes in gears with this many teeth or more spokes_from = 24; // [12:1:100] /* [Hidden] */ $fn = 96; use // Assembled: the two gears in mesh, the small one turned to fit between // the large one's teeth step("Put the large gear on its axle") part("large gear", at = [0, 0, 0]) spur_gear(teeth_large, tooth, bore, pressure, spokes = teeth_large >= spokes_from ? 5 : 0); step("Put the small gear on its axle, in mesh") part("small gear", at = [gear_distance(teeth_large, teeth_small, tooth), 0, 0], rotate = [0, 0, gear_phase(teeth_small)]) spur_gear(teeth_small, tooth, bore, pressure, spokes = teeth_small >= spokes_from ? 5 : 0); for (x = [0, gear_distance(teeth_large, teeth_small, tooth)]) hardware("dowel", [x, 0, -10], [x, 0, $thickness + 10], bore, label = "axle"); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/jigsaw/ # Jigsaw puzzle A jigsaw puzzle with interlocking pieces. Choose the size and piece count, or change the seed for a new pattern. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=jigsaw). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/jigsaw/jigsaw.kerf) - [SVG](https://kerfscript.mylasertools.com/models/jigsaw/jigsaw.svg) - [DXF](https://kerfscript.mylasertools.com/models/jigsaw/jigsaw.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 150 × 120 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `cols` | `5` | 2 to 12 | | `rows` | `4` | 2 to 12 | | `piece` | `30` | 15 to 60 | | `knob` | `0.22` | 0.15 to 0.3 | | `seed` | `3` | 1 to 99 | ## Source ```kerf // A jigsaw puzzle with interlocking pieces. // Choose the size and piece count, or change the seed for a new pattern. /* [Puzzle] */ // Pieces across cols = 5; // [2:1:12] // Pieces down rows = 4; // [2:1:12] // Size of one piece piece = 30; // [15:1:60] // Knob size, as a share of the edge knob = 0.22; // [0.15:0.01:0.3] // Different seeds give different knob directions seed = 3; // [1:1:99] /* [Hidden] */ w = cols * piece; h = rows * piece; flips = rands(0, 1, 2 * cols * rows, seed); // One edge from [0, 0] to [len, 0] with a knob on side s (1 or -1). function edge(len, s) = let ( rr = knob * len / 2, c = [len / 2, s * (rr * 1.65)], n = 24 ) concat( [[0, 0], [len / 2 - rr * 1.1, 0], [len / 2 - rr * 0.75, s * rr * 0.55]], [for (i = [0:n]) let (a = 235 - i * 290 / n) [c[0] + rr * cos(a), c[1] + s * rr * sin(a)]], [[len / 2 + rr * 0.75, s * rr * 0.55], [len / 2 + rr * 1.1, 0], [len, 0]] ); function side(i) = flips[i] < 0.5 ? -1 : 1; rect([w, h], r = 3); // Edges between columns. for (c = [1:cols - 1], r = [0:rows - 1]) translate([c * piece, r * piece]) rotate(90) polyline(edge(piece, side(c * rows + r))); // Edges between rows. for (r = [1:rows - 1], c = [0:cols - 1]) translate([c * piece, r * piece]) polyline(edge(piece, side(cols * rows + r * cols + c))); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/clock-face/ # Clock face A clock face: cut outline, scored minute marks, engraved hours. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=clock-face). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/clock-face/clock-face.kerf) - [SVG](https://kerfscript.mylasertools.com/models/clock-face/clock-face.svg) - [DXF](https://kerfscript.mylasertools.com/models/clock-face/clock-face.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 150 × 150 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `150` | 100 to 300 | | `marks` | `60` | 12 to 120 | | `hole` | `8` | 5 to 12 | ## Source ```kerf // A clock face: cut outline, scored minute marks, engraved hours. size = 150; // [100:5:300] marks = 60; // [12:12:120] hole = 8; // [5:0.5:12] r = size / 2; difference() { circle(r = r, $fn = 180); circle(d = hole); } score() for (i = [0:marks - 1]) { long = i % (marks / 12) == 0; rotate(i * 360 / marks) line([r - (long ? 12 : 7), 0], [r - 4, 0]); } engrave() for (h = [1:12]) rotate(90 - h * 30) translate([r - 22, 0]) rotate(h * 30 - 90) text(str(h), r / 7, halign = "center", valign = "center"); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/hadal/ # Hadal skeleton sculpture Hadal: the skeleton of an imagined deep-sea ray on a display stand. Nine ribs slot onto a spine, which sits in two cradles on a plinth. The ribs lean more along the body, so the flat pieces look like a turning animal. This design has 13 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=hadal). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/hadal/hadal.kerf) - [SVG](https://kerfscript.mylasertools.com/models/hadal/hadal.svg) - [DXF](https://kerfscript.mylasertools.com/models/hadal/hadal.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Spine | 1 | 270 × 68.3 mm | | Rib 1 | 1 | 59.4 × 34.2 mm | | Rib 2 | 1 | 100.4 × 46.1 mm | | Rib 3 | 1 | 151.6 × 53.7 mm | | Rib 4 | 1 | 192.6 × 56.1 mm | | Rib 5 | 1 | 180.3 × 53.3 mm | | Rib 6 | 1 | 141.4 × 48.9 mm | | Rib 7 | 1 | 100.4 × 42.1 mm | | Rib 8 | 1 | 65.6 × 35.6 mm | | Rib 9 | 1 | 41 × 29.3 mm | | Cradle A | 1 | 64 × 82.9 mm | | Cradle B | 1 | 64 × 80.9 mm | | Plinth | 1 | 190 × 106 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `length` | `270` | 250 to 300 | | `output` | `"sculpture"` | Sculpture, Fit test | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.5 to 4 | | `clearance` | `0.15` | 0.05 to 0.25 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Hadal: the skeleton of an imagined deep-sea ray on a display stand. Nine // ribs slot onto a spine, which sits in two cradles on a plinth. The ribs // lean more along the body, so the flat pieces look like a turning animal. /* [Specimen] */ // Length from nose to tail length = 270; // [250:10:300] // The sculpture, or two pieces to test the slot fit first output = "sculpture"; // [sculpture:Sculpture, fit:Fit test] // Scored veins, rib numbers and plinth markings score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.5:0.1:4] // Added to the slot width clearance = 0.15; // [0.05:0.05:0.25] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 96; s = length / 270; t = $thickness; sw = t + clearance; // Per rib: position on the spine, slot depth, half width, height and lean xs = [35, 55, 75, 95, 115, 135, 155, 175, 195]; zs = [75, 79, 83, 85, 86, 85, 82, 78, 73]; ws = [29, 49, 74, 94, 88, 69, 49, 32, 20]; hs = [20, 27, 32, 34, 31, 27, 23, 20, 17]; leans = [-0.22, -0.17, -0.09, 0, 0.12, 0.21, 0.27, 0.30, 0.30]; cradle_x = [65, 165]; cradle_z = [74, 72]; module rib_envelope(i) multmatrix([[1, 0, 0], [leans[i], 1, 0]]) scale([ws[i] * s, hs[i] * s]) path("M 0 1 C 0.42 1 0.65 0.55 1 0.25 Q 1.07 0.11 0.94 -0.04 C 0.55 -0.28 0.38 -0.67 0 -0.65 C -0.38 -0.67 -0.55 -0.28 -0.94 -0.04 Q -1.07 0.11 -1 0.25 C -0.65 0.55 -0.42 1 0 1 Z"); module rib_bone(i) difference() { intersection() { rib_envelope(i); union() { if (i == 8) rib_envelope(i); else difference() { rib_envelope(i); offset(delta = -6 * s) rib_envelope(i); } translate([-7 * s, -100 * s]) rect([14 * s, 200 * s]); // Cross braces run past the shell before clipping, so no thin tips are left if (ws[i] >= 45) for (side = [-1, 1]) stroke(5 * s) line([side * 4 * s, 0], [side * ws[i] * 1.10 * s, (hs[i] * 0.12 + leans[i] * side * ws[i] * 1.10) * s]); } } // Slot from below, onto the spine translate([-sw / 2, -200 * s]) rect([sw, 200 * s]); } module rib(i) { rib_bone(i); if (score_detail) score() { outline() offset(delta = -3 * s) rib_envelope(i); for (side = [-1, 1]) line([side * 9 * s, 0], [side * ws[i] * 0.97 * s, (hs[i] * 0.105 + leans[i] * side * ws[i] * 0.97) * s]); translate([0, hs[i] * 0.62 * s]) text(str(i + 1), size = 3.5 * s, halign = "center", valign = "center"); } } module spine_envelope() scale(s) path("M 0 77 Q 7 89 16 93 L 13 111 Q 13 118 18 113 L 30 91 C 77 103 137 106 195 87 C 218 84 238 104 263 124 Q 270 128 270 122 C 253 97 232 73 210 63 Q 200 59 190 59 C 136 78 72 75 31 60 Q 15 53 6 64 Q 0 68 0 77 Z"); module spine() { difference() { spine_envelope(); // Rib slots from above, cradle slots from below for (i = [0:8]) translate([xs[i] * s - sw / 2, zs[i] * s]) rect([sw, 100 * s]); for (j = [0:1]) translate([cradle_x[j] * s - sw / 2, -10 * s]) rect([sw, (cradle_z[j] + 10) * s]); translate([15 * s, 76 * s]) ellipse([5.5 * s, 4 * s]); scale(s) path("M 215 74 Q 236 84 253 108 Q 233 91 215 81 Z"); } if (score_detail) score() scale(s) { path("M 6 78 Q 12 88 23 86"); path("M 20 66 C 70 82 139 85 188 68"); path("M 213 69 Q 238 81 260 115"); for (j = [0:1]) translate([cradle_x[j] - 7, cradle_z[j] + 9]) text(j == 0 ? "A" : "B", size = 3, halign = "center"); for (i = [0:8]) translate([xs[i] + 5, zs[i] - 7]) text(str(i + 1), size = 2.7, halign = "center"); } } module cradle_envelope(j) { h = cradle_z[j] * s; union() { hull() { translate([0, h - 3 * s]) circle(r = 9 * s); translate([0, 48 * s]) circle(r = 8 * s); } for (side = [-1, 1]) hull() { translate([0, 52 * s]) circle(r = 8 * s); translate([side * 24 * s, 5 * s]) circle(r = 8 * s); } for (side = [-1, 1]) translate([side * 24 * s - 8 * s, 0]) rect([16 * s, 10 * s]); } } module cradle(j) { difference() { union() { intersection() { cradle_envelope(j); translate([-45 * s, 0]) rect([90 * s, 120 * s]); } // Tabs into the plinth for (side = [-1, 1]) translate([side * 24 * s - 6 * s, -t]) rect([12 * s, t + 0.5]); } translate([-sw / 2, cradle_z[j] * s]) rect([sw, 100 * s]); for (side = [-1, 1]) hull() { translate([side * 22 * s, 12 * s]) circle(r = 2.2 * s); translate([side * 8 * s, 40 * s]) circle(r = 2.2 * s); } } if (score_detail) score() { translate([0, 61 * s]) text(j == 0 ? "A" : "B", size = 4 * s, halign = "center", valign = "center"); for (side = [-1, 1]) polyline([[side * 27 * s, 7 * s], [side * 18 * s, 28 * s], [side * 4 * s, 51 * s]]); } } module plinth() { difference() { scale(s) polygon([[20, -35], [38, -53], [192, -53], [210, -35], [210, 35], [192, 53], [38, 53], [20, 35]]); for (j = [0:1], side = [-1, 1]) translate([cradle_x[j] * s, side * 24 * s]) rect([sw, 12 * s + clearance], center = true); } if (score_detail) score() scale(s) { outline() polygon([[25, -33], [40, -48], [190, -48], [205, -33], [205, 33], [190, 48], [40, 48], [25, 33]]); translate([115, -32]) text("H A D A L", size = 7, halign = "center", valign = "center"); translate([115, -43]) text("SPECIMEN 09 / ABYSSAL ARCHIVE", size = 2.6, halign = "center", valign = "center"); for (x = [75:5:155]) line([x, 39], [x, x % 10 == 0 ? 44 : 42]); for (j = [0:1]) translate([cradle_x[j], 0]) text(j == 0 ? "A" : "B", size = 3.5, halign = "center"); } } module fit_test() { for (j = [0:1]) part(j == 0 ? "Fit test, lower" : "Fit test, upper") { difference() { rect([35, 30], r = 2); translate([17.5 - sw / 2, j == 0 ? -1 : 15]) rect([sw, 16]); } if (score_detail) score() translate([7, 12]) text(j == 0 ? "L" : "U", size = 4); } } if (output == "fit") fit_test(); // Assembled: the spine stands in the two cradles on the plinth, the ribs // across it in their slots across = [90, 0, 90]; if (output == "sculpture") { // The assembly steps, listed first so they number in build order step("Glue cradles A and B into the plinth"); step("Lower the spine into the cradles"); step("Drop ribs 1 to 9 into the numbered slots on top of the spine"); step("Lower the spine into the cradles") part("Spine", at = [0, t / 2, 0], rotate = [90, 0, 0]) spine(); step("Drop ribs 1 to 9 into the numbered slots on top of the spine") for (i = [0:8]) part(str("Rib ", i + 1), at = [xs[i] * s - t / 2, 0, zs[i] * s], rotate = across) rib(i); step("Glue cradles A and B into the plinth") { part("Cradle A", at = [cradle_x[0] * s - t / 2, 0, 0], rotate = across) cradle(0); part("Cradle B", at = [cradle_x[1] * s - t / 2, 0, 0], rotate = across) cradle(1); part("Plinth", at = [0, 0, -t]) plinth(); } } // Assembly: glue cradles A and B into the plinth, lower the spine into // them, then drop ribs 1 to 9 into the numbered slots on top of the spine. // Dry fit everything before gluing the joints. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/xenospora/ # Xenospora seed pod Xenospora: a seed pod from another planet. Twelve flat crescent fins stand around a crossed core between a base and a crown. The fins only look twisted: each one is flat, set at a tangent to the circle. This design has 16 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=xenospora). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/xenospora/xenospora.kerf) - [xenospora-sheet-1.svg](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-1.svg) - [xenospora-sheet-1.dxf](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-1.dxf) - [xenospora-sheet-2.svg](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-2.svg) - [xenospora-sheet-2.dxf](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-2.dxf) - [xenospora-sheet-3.svg](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-3.svg) - [xenospora-sheet-3.dxf](https://kerfscript.mylasertools.com/models/xenospora/xenospora-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Fin 1 | 1 | 85.1 × 226 mm | | Fin 2 | 1 | 85.1 × 226 mm | | Fin 3 | 1 | 85.1 × 226 mm | | Fin 4 | 1 | 85.1 × 226 mm | | Fin 5 | 1 | 85.1 × 226 mm | | Fin 6 | 1 | 85.1 × 226 mm | | Fin 7 | 1 | 85.1 × 226 mm | | Fin 8 | 1 | 85.1 × 226 mm | | Fin 9 | 1 | 85.1 × 226 mm | | Fin 10 | 1 | 85.1 × 226 mm | | Fin 11 | 1 | 85.1 × 226 mm | | Fin 12 | 1 | 85.1 × 226 mm | | Root | 1 | 120 × 120 mm | | Crown | 1 | 86 × 86 mm | | Core A | 1 | 42 × 226 mm | | Core B | 1 | 42 × 226 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `height` | `220` | 210 to 250 | | `fullness` | `60` | 54 to 66 | | `output` | `"all"` | All parts, Fins 1 to 4, Fins 5 to 8, Fins 9 to 12, Base crown and core, Fit test | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.8 to 3.2 | | `clearance` | `0.15` | 0.1 to 0.3 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Xenospora: a seed pod from another planet. Twelve flat crescent fins stand // around a crossed core between a base and a crown. The fins only look // twisted: each one is flat, set at a tangent to the circle. /* [Specimen] */ // Height between base and crown height = 220; // [210:5:250] // How far the crescents bow out fullness = 60; // [54:2:66] // All parts, one group at a time, or two pieces to test the slot fit output = "all"; // [all:All parts, fins_a:Fins 1 to 4, fins_b:Fins 5 to 8, fins_c:Fins 9 to 12, structure:Base crown and core, fit:Fit test] // Scored veins and assembly numbers score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to the slot width clearance = 0.15; // [0.1:0.05:0.3] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 144; t = $thickness; s = height / 220; R = 30 * s; sw = t + clearance; fin_tab = 8; core_tab = 12; // Centre line and width of a fin, and width of the core, at height v (0 to 1) function cc(v) = s * (6 + 12 * v + fullness * pow(sin(180 * v), 1.3)); function ww(v) = s * (12 + 14 * pow(sin(180 * v), 1.2)); function cp(v) = [cc(v), height * v]; function polar(r, a) = [r * cos(a), r * sin(a)]; function core_w(v) = s * (8 + 13 * pow(sin(180 * clamp((v - 0.18) / 0.64, 0, 1)), 1.4)); module fin_blank() polygon(concat( [for (j = [0:160]) let (v = j / 160) [cc(v) - ww(v) / 2, height * v]], [for (j = [160:-1:0]) let (v = j / 160) [cc(v) + ww(v) / 2, height * v]] )); function pore_normal(v) = let ( dx = s * (12 + fullness * 1.3 * PI * pow(sin(180 * v), 0.3) * cos(180 * v)), dy = height, n = sqrt(dx * dx + dy * dy) ) [-dy / n, dx / n]; // A slot that follows the centre line of the fin from a to b, with round ends module pore(a, b, w) { r = w * s / 2; na = pore_normal(a); nb = pore_normal(b); aa = atan2(na[1], na[0]); ab = atan2(nb[1], nb[0]); polygon(concat( [for (j = [0:72]) let (v = a + (b - a) * j / 72) cp(v) + pore_normal(v) * r], [for (j = [1:36]) cp(b) + polar(r, ab - j * 180 / 36)], [for (j = [1:72]) let (v = b - (b - a) * j / 72) cp(v) - pore_normal(v) * r], [for (j = [1:36]) cp(a) + polar(r, aa - 180 - j * 180 / 36)] )); } module fin(i) { difference() { union() { fin_blank(); translate([6 * s - fin_tab / 2, -t]) rect([fin_tab, t + 0.3]); translate([18 * s - fin_tab / 2, height - 0.3]) rect([fin_tab, t + 0.3]); } pore(0.16, 0.31, 4); pore(0.37, 0.58, 6); pore(0.64, 0.82, 4); } if (score_detail) score() { for (side = [-1, 1]) polyline([for (j = [6:154]) let (v = j / 160) [cc(v) + side * (ww(v) / 2 - 2.3 * s), height * v]]); for (v = [0.2:0.06:0.8]) line([cc(v) + 4 * s, height * v], [cc(v + 0.014) + ww(v + 0.014) / 2 - 2.3 * s, height * (v + 0.014)]); translate([cc(0.055), height * 0.055]) text(str(i), size = 3.5 * s, halign = "center", valign = "center"); translate([cc(0.95), height * 0.95]) text(str(i), size = 3 * s, halign = "center", valign = "center"); } } module cross_socket() { rect([core_tab + clearance, sw], center = true); rect([sw, core_tab + clearance], center = true); } module sockets(top = false) { for (i = [0:11]) rotate(i * 30) translate([R, top ? 18 * s : 6 * s]) rect([sw, fin_tab + clearance], center = true); } // The base (root) and the crown module disc(top = false) { difference() { circle(r = (top ? 43 : 60) * s); sockets(top); cross_socket(); } if (score_detail) score() { outline() circle(r = (top ? 40 : 57) * s); for (i = [0:11]) rotate(i * 30) { translate([top ? 33 * s : 40 * s, top ? 18 * s : 6 * s]) text(str(i + 1), size = 2.8 * s, halign = "center", valign = "center"); line([R - 5 * s, top ? 18 * s : 6 * s], [R - 8 * s, top ? 18 * s : 6 * s]); } translate([0, -15 * s]) text(top ? "CROWN" : "ROOT", size = 3 * s, halign = "center", valign = "center"); if (!top) { translate([0, -48 * s]) text("X E N O S P O R A", size = 4 * s, halign = "center", valign = "center"); translate([0, -54 * s]) text("SPECIMEN 03 / EXOBOTANICA", size = 2.3 * s, halign = "center", valign = "center"); // Core A lines up with this mark line([-18 * s, 0], [-10 * s, 0]); translate([-21 * s, 0]) text("A", size = 3 * s, halign = "center", valign = "center"); } } } module core_blank() polygon(concat( [for (j = [0:160]) let (v = j / 160) [-core_w(v), height * v]], [for (j = [160:-1:0]) let (v = j / 160) [core_w(v), height * v]] )); // Two core halves that cross: A is slotted from the top, B from the bottom module core(a = true) { difference() { union() { core_blank(); translate([-core_tab / 2, -t]) rect([core_tab, t + 0.3]); translate([-core_tab / 2, height - 0.3]) rect([core_tab, t + 0.3]); } translate([-sw / 2, a ? height / 2 : -t - 1]) rect([sw, height / 2 + t + 1]); for (side = [-1, 1], v = [0.36, 0.48, 0.60]) translate([side * (core_w(v) - 6 * s), height * v]) ellipse([2.2 * s, 5 * s]); } if (score_detail) score() { for (side = [-1, 1]) polyline([for (j = [35:125]) let (v = j / 160) [side * (core_w(v) - 2 * s), height * v]]); translate([-4.6 * s, height * 0.1]) text(a ? "A" : "B", size = 3 * s, halign = "center", valign = "center"); translate([-4.6 * s, height * 0.9]) text(a ? "A" : "B", size = 3 * s, halign = "center", valign = "center"); for (side = [-1, 1], v = [0.29:0.06:0.71]) line([side * 3 * s, height * v], [side * (core_w(v + 0.025) - 2 * s), height * (v + 0.025)]); } } module fit_test() { part("Fit test, sockets") { difference() { rect([66, 32], r = 2); translate([17, 16]) rect([sw, fin_tab + clearance], center = true); translate([45, 16]) cross_socket(); } if (score_detail) score() { translate([17, 5]) text("FIN", size = 3, halign = "center"); translate([45, 5]) text("CORE", size = 3, halign = "center"); } } for (a = [true, false]) part(a ? "Fit test, A" : "Fit test, B") { difference() { union() { rect([24, 32]); translate([6, 32]) rect([12, t]); translate([8, -t]) rect([8, t + 0.2]); } translate([12 - sw / 2, a ? 16 : -t - 1]) rect([sw, a ? 20 + t : 17 + t]); } if (score_detail) score() translate([4, 25]) text(a ? "A" : "B", size = 3); } } if (output == "fit") fit_test(); // Assembled: the two cores cross in the root, the twelve fins stand round // them every 30 degrees, and the crown caps them function fin_at(i) = concat([(R - t / 2) * cos(30 * (i - 1)), (R - t / 2) * sin(30 * (i - 1))], [0]); module placed_fin(i) step("Set the twelve fins into their numbered sockets, all bowing the same way") part(str("Fin ", i), at = fin_at(i), rotate = [90, 0, 90 + 30 * (i - 1)]) fin(i); // Steps are numbered in the order they first run, so name them here in build order step("Cross the two cores and glue them into the root") {} step("Set the twelve fins into their numbered sockets, all bowing the same way") {} step("Lower the crown over the top tabs and glue it") {} if (output == "all" || output == "fins_a") for (i = [1:4]) placed_fin(i); if (output == "all" || output == "fins_b") for (i = [5:8]) placed_fin(i); if (output == "all" || output == "fins_c") for (i = [9:12]) placed_fin(i); if (output == "all" || output == "structure") { step("Cross the two cores and glue them into the root") part("Root", at = [0, 0, -t]) disc(false); step("Lower the crown over the top tabs and glue it") part("Crown", at = [0, 0, height]) disc(true); step("Cross the two cores and glue them into the root") { part("Core A", at = [0, t / 2, 0], rotate = [90, 0, 0]) core(true); part("Core B", at = [-t / 2, 0, 0], rotate = [90, 0, 90]) core(false); } } // Assembly: // 1. Slide cores A and B together at right angles and glue them into the // cross socket in the root, with core A on the scored A mark. // 2. Set fins 1 to 12 into the numbered sockets, all bowing the same way. // 3. Lower the crown over the top tabs, numbers matching the root, and glue. // Paint the core a contrasting colour after cutting if you like, keeping // the joints bare. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/umbra/ # Umbra shadow machine Umbra: a shadow machine. Six flat fins stand on a base at different depths and angles. Lit by a small LED through a 1 mm pinhole, their shadows line up on a wall 900 mm away into one dragon standing on a rock. Each fin outline is the dragon's shadow projected back onto that fin's plane. This design has 9 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=umbra). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/umbra/umbra.kerf) - [umbra-sheet-1.svg](https://kerfscript.mylasertools.com/models/umbra/umbra-sheet-1.svg) - [umbra-sheet-1.dxf](https://kerfscript.mylasertools.com/models/umbra/umbra-sheet-1.dxf) - [umbra-sheet-2.svg](https://kerfscript.mylasertools.com/models/umbra/umbra-sheet-2.svg) - [umbra-sheet-2.dxf](https://kerfscript.mylasertools.com/models/umbra/umbra-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Fin 1 | 1 | 74.2 × 123 mm | | Fin 2 | 1 | 89.5 × 155.1 mm | | Fin 3 | 1 | 42.6 × 94.5 mm | | Fin 4 | 1 | 56.7 × 56.2 mm | | Fin 5 | 1 | 28 × 57.1 mm | | Fin 6 | 1 | 52.9 × 100.1 mm | | Base | 1 | 200 × 350 mm | | Stone | 1 | 60.7 × 42.7 mm | | Light holder | 1 | 50 × 48 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `output` | `"all"` | All parts, The six fins, Base stone and light holder, Fit test | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.8 to 3.2 | | `clearance` | `0.15` | 0.1 to 0.3 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Umbra: a shadow machine. Six flat fins stand on a base at different // depths and angles. Lit by a small LED through a 1 mm pinhole, their shadows // line up on a wall 900 mm away into one dragon standing on a rock. Each fin // outline is the dragon's shadow projected back onto that fin's plane. /* [Output] */ // All parts, one group, or two pieces to test the slot fit output = "all"; // [all:All parts, fins:The six fins, structure:Base stone and light holder, fit:Fit test] // Scored numbers and alignment marks score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to every slot, for a glued slip fit clearance = 0.15; // [0.10:0.05:0.30] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 96; t = $thickness; sw = t + clearance; // The wall is L mm in front of the pinhole, which is sz mm above the base L = 900; sz = 25; // Each fin stands depth mm from the pinhole, turned by its yaw in degrees depths = [245, 275, 185, 155, 125, 215]; yaws = [9, 15, -3, -9, -15, 3]; // A point [x, z] of the picture on the wall, projected onto fin i: the fin is // a vertical plane through [0, depth] turned by its yaw. function warp(p, i) = let (a = yaws[i], d = depths[i], f = d / (L - p[0] * tan(a))) [f * p[0] / cos(a), sz + f * (p[1] - sz)]; // The dragon on the wall, in six fragments, one per fin T0 = [[-202.5366, 337.6753], [-227, 388], [-177.4432, 362.1121], [-169.1793, 369.0774], [-154.699, 361.5131], [-149.2495, 357.4991], [-127.6525, 340.5957], [-106.6053, 322.4979], [-86.2426, 303.3208], [-66.6998, 283.1791], [-48.1136, 262.1864], [-30.6214, 240.4557], [-14.3616, 218.0987], [-1.1879, 198], [28, 198], [28, 141], [23.9744, 141.9472], [18, 129.6], [18, 97.2632], [28, 82], [28, 57.8], [27, 57], [18, 62.7857], [18, 35], [-18, 35], [-18, 59.5], [-28, 57], [-28, 88.8889], [-18.123, 92.6202], [-23, 104], [-28, 110.6094], [-28, 172.7213], [-42.4058, 188.2796], [-42.2033, 189.1382], [-44.3083, 190.3343], [-70.2036, 218.3012], [-69.9296, 230.7208], [-70.5118, 235.8344], [-71.5792, 240.8117], [-75.1955, 250.4647], [-80.893, 259.8491], [-89.115, 270.1244], [-92.811, 273.5871], [-97.5446, 276.3256], [-101.5494, 276.9722], [-106.1357, 275.9018], [-109.9794, 273.855], [-128.295, 259.1001], [-130.5146, 260.9069], [-131.2948, 283.0353], [-132.7841, 289.986], [-135.5295, 296.3848], [-139.6256, 302.4154], [-145.2288, 308.1677], [-152.4931, 313.6416], [-158.7674, 316.9745], [-162.5038, 317.9852], [-166.1084, 318.2274], [-169.8032, 317.7325], [-173.7782, 316.4109], [-180.4189, 312.5686], [-195.3017, 299.9189], [-209.5861, 329.304]]; T1 = [[-18, 175.3864], [-49, 193], [-54.23, 198.15], [-59.52, 202.4], [-64.87, 205.75], [-70.28, 208.2], [-78.5075, 210.1875], [-86.87, 210.15], [-95.3675, 208.0875], [-104, 204], [-103.4048, 208.5952], [-103.8097, 215.955], [-104.9446, 221.173], [-106.7716, 226.6401], [-109.2907, 232.3564], [-114.3668, 241.3979], [-121, 251], [-125.7751, 256.6886], [-130.6886, 261.0484], [-135.7405, 264.0796], [-140.9308, 265.782], [-146.2595, 266.1557], [-148.9758, 265.8443], [-154.5121, 264.2249], [-160.1869, 261.2768], [-166, 257], [-166.2344, 263.0469], [-167.4375, 268.6875], [-169.6094, 273.9219], [-172.75, 278.75], [-176.8594, 283.1719], [-181.9375, 287.1875], [-187.9844, 290.7969], [-195, 294], [-202.2769, 295.2583], [-209.9256, 295.2149], [-217.9463, 293.8698], [-223.5, 292.25], [-232.1405, 288.7355], [-241.1529, 283.9194], [-250.5372, 277.8017], [-257, 273], [-193, 349], [-123, 408], [-66, 449], [-40, 328], [-33.9104, 312.3875], [-27.5473, 293.4314], [-22.7288, 276.5478], [-18.4971, 259.0774], [-15.4187, 244.0706], [-12.2626, 225.5247], [-10.0806, 209.6215], [-7.7273, 188], [18, 188], [18, 35], [-18, 35]]; T2 = [[-0.0622, 204.5378], [6.7844, 208.3344], [13.1111, 213.1111], [18.9178, 218.8678], [24.2044, 225.6044], [28.9711, 233.3211], [34.5178, 245.1344], [39.14, 258.69], [44.543, 279.5977], [48.4219, 288.1406], [51.75, 293.25], [57.8555, 300.0352], [62.668, 303.9727], [71, 309], [60, 342], [84, 330], [88, 350], [102, 330], [120, 332], [130, 324.5], [106.6667, 300], [122.7692, 300], [112, 290], [140, 290], [133, 277], [110, 275], [104.4379, 270.7396], [98.8284, 267.8047], [96.0059, 266.8343], [90.3254, 265.8876], [87.4675, 265.9112], [81.716, 266.9527], [76.978, 268.8269], [73, 271], [70.9438, 254.2101], [67.1095, 235.429], [61.784, 218.1391], [56.2071, 204.8698], [51, 195], [18, 154], [18, 35], [-18, 35], [-18, 188], [-8.0882, 188], [-10, 201]]; T3 = [[-18, 67.4286], [-61, 49], [-70.43, 46.6214], [-79.7431, 44.788], [-88.9394, 43.4997], [-98.0189, 42.7566], [-106.9816, 42.5587], [-118.7501, 43.1428], [-127.4402, 44.2169], [-136.0135, 45.8361], [-144.47, 48.0005], [-152.8096, 50.7101], [-161.0324, 53.9648], [-171.8145, 59.1525], [-185, 67], [-192.7158, 72.7788], [-199.7253, 78.9429], [-206.0285, 85.4923], [-211.6254, 92.4269], [-214.9643, 97.264], [-219.3841, 104.8407], [-223.0975, 112.8026], [-225.1807, 118.3246], [-227.717, 126.9287], [-229.0155, 132.8787], [-235, 182], [-223, 163], [-192, 182], [-195.6747, 172.564], [-197.5017, 166.3772], [-199.308, 157.2526], [-199.9931, 148.3149], [-199.827, 142.4602], [-198.6436, 133.8339], [-197.0631, 128.0754], [-193.6763, 119.7627], [-190.9383, 114.6049], [-186.1111, 107.4444], [-180.4198, 100.9753], [-176.1454, 97.0466], [-169.0137, 91.7298], [-161.0178, 87.1043], [-152.1578, 83.1701], [-145.7709, 80.9314], [-126.037, 75.7407], [-116.5093, 73.9769], [-107.1481, 72.7963], [-91.9167, 72.125], [-83, 72.5], [-74.25, 73.4583], [-62.8426, 75.6435], [-51.7315, 78.8657], [-43.5926, 81.963], [-33, 87], [12, 104], [2.8235, 91], [18, 91], [18, 35], [-18, 35]]; T4 = [[-17.4286, 91], [-23, 104], [-30.5022, 114.3111], [-35.1256, 122.2778], [-38.8889, 130.4444], [-41.7922, 138.8111], [-43.8356, 147.3778], [-45.2222, 159.1111], [-45.2589, 168.1444], [-43.97, 180.5], [-42, 190], [-36.3253, 203.0657], [-31.4152, 211.4844], [-27.8304, 216.4394], [-23.9965, 220.8685], [-19.9135, 224.7716], [-15.5813, 228.1488], [-7.9689, 232.4356], [-2.12, 234.32], [0.6978, 234.7689], [6.12, 234.68], [8.7244, 234.1422], [13.72, 232.08], [18.4311, 228.7022], [22.8578, 224.0089], [28.5488, 215.8166], [32.3964, 208.9112], [35.0459, 201.4734], [36.497, 193.503], [36.7988, 187.8935], [36.568, 182.0473], [35.2234, 172.8343], [31.5666, 159.7219], [25.7796, 145.6627], [9, 111], [28, 82], [57, 78], [68, 65], [51, 67], [53, 56], [37, 65], [27, 57], [18, 62.7857], [18, 35], [-18, 35], [-18, 59.5], [-36, 55], [-51, 64], [-66, 61], [-61, 75], [-45, 79], [-18, 76.6176], [-18, 91]]; T5 = [[35.2181, 175.3922], [50, 169], [75, 185], [89, 204], [96, 200], [91, 180], [111, 191], [114, 184], [101, 167], [120, 171], [120, 163], [90, 151], [76, 153], [45, 137], [18, 143.3529], [18, 35], [-18, 35], [-18, 188], [-8.0882, 188], [-10, 201], [-0.0622, 204.5378], [8.9511, 209.8178], [17.04, 216.84], [24.2044, 225.6044], [30.4444, 236.1111], [35.76, 248.36], [39.14, 258.69], [43.5469, 276.5156], [45.6875, 282.5625], [50.0117, 290.7539], [53.6367, 295.6289], [57.8197, 300], [106.6667, 300], [130, 324.5], [132, 323], [161, 319], [151, 303], [126, 303], [112, 290], [140, 290], [133, 277], [110, 275], [104.4379, 270.7396], [98.8284, 267.8047], [96.0059, 266.8343], [90.3254, 265.8876], [87.4675, 265.9112], [81.716, 266.9527], [76.978, 268.8269], [73, 271], [71.4379, 257.4852], [69.213, 244.6331], [66.3254, 232.4438], [62.7751, 220.9172], [57.4053, 207.4408], [51, 195]]; targets = [T0, T1, T2, T3, T4, T5]; rock = [[-94, 25], [-84, 43], [-49, 49], [-34, 66], [-12, 60], [6, 69], [32, 52], [68, 43], [88, 25]]; // The foot of a fin: two tabs into the base module foot() { polygon([[-14, 0], [14, 0], [14, 10], [3, 25], [3, 32], [-3, 32], [-3, 25], [-14, 10]]); for (u = [-8, 8]) translate([u - 3, -t]) rect([6, t + 0.3]); } module fin(i) { union() { polygon([for (p = targets[i]) warp(p, i)]); foot(); } if (score_detail) score() { translate([0, 5]) text(str(i + 1), size = 3, halign = "center", valign = "center"); line([-13, 0], [13, 0]); // Points the same way as the arrow at this fin's slots in the base polyline([[-5, 10], [5, 10], [2, 12]]); } } // The rock under the dragon, projected onto a plane 300 mm from the light module stone() { union() { polygon([for (p = rock) [p[0] * 300 / L, sz + (p[1] - sz) * 300 / L]]); polygon([[-94 * 300 / L, 0], [88 * 300 / L, 0], [88 * 300 / L, 25], [-94 * 300 / L, 25]]); for (u = [-20, 20]) translate([u - 4, -t]) rect([8, t + 0.3]); } if (score_detail) score() { translate([0, 8]) text("STONE", size = 3, halign = "center"); polyline([[-24, 28], [-10, 32], [0, 29], [11, 32], [22, 27]]); } } // Holds the pinhole: tape thin card over the 10 mm hole and prick a 1 mm // hole in its centre, with a battery LED close behind it module light_holder() { difference() { union() { translate([-25, 0]) rect([50, 45], r = 3); for (u = [-15, 15]) translate([u - 4, -t]) rect([8, t + 3.3]); } translate([0, 25]) circle(d = 10); } if (score_detail) score() { translate([0, 39]) text("SOURCE", size = 3, halign = "center"); translate([0, 10]) text("1 mm PINHOLE", size = 2.4, halign = "center"); line([-9, 25], [-6, 25]); line([6, 25], [9, 25]); line([0, 31], [0, 34]); line([0, 16], [0, 19]); line([-10, 0], [10, 0]); } } module base() { difference() { polygon([[-100, -25], [-90, -35], [90, -35], [100, -25], [100, 305], [90, 315], [-90, 315], [-100, 305]]); for (i = [0:5]) translate([0, depths[i]]) rotate(yaws[i]) for (u = [-8, 8]) translate([u, 0]) rect([6 + clearance, sw], center = true); translate([0, 300]) for (u = [-20, 20]) translate([u, 0]) rect([8 + clearance, sw], center = true); // The holder sits just behind y = 0, so its front face is at the origin translate([0, -t / 2]) for (u = [-15, 15]) translate([u, 0]) rect([8 + clearance, sw], center = true); } if (score_detail) score() { line([-4, 0], [4, 0]); line([0, -4], [0, 4]); translate([8, -2]) text("O", size = 3); line([0, 12], [0, 115]); polyline([[-3, 109], [0, 115], [3, 109]]); translate([0, 49]) text("U M B R A", size = 5, halign = "center"); translate([0, 61]) text("A DRAGON FROM FRAGMENTS", size = 2.6, halign = "center"); translate([0, 75]) text("WALL Y=900 / PINHOLE Z=25", size = 2.5, halign = "center"); translate([0, 87]) text("BATTERY LED ONLY", size = 3, halign = "center"); for (i = [0:5]) translate([0, depths[i]]) rotate(yaws[i]) { translate([21, 0]) text(str(i + 1), size = 3, halign = "center", valign = "center"); polyline([[14, 0], [18, 0], [16, 2]]); } translate([42, 300]) text("STONE", size = 2.8, halign = "center", valign = "center"); translate([35, -1.5]) text("S", size = 3, halign = "center"); line([-82, 0], [-82, 300]); for (y = [0:25:300]) { line([-82, y], [-78, y]); if (y % 50 == 0) translate([-85, y]) text(str(y), size = 2.5, halign = "right", valign = "center"); } translate([64, 150]) rotate(90) text("+Y TO WALL / 900 mm FROM O", size = 3, halign = "center"); } } module fit_test() { part("Fit test, slots") { difference() { rect([68, 32], r = 2); for (x = [18, 50]) translate([x, 16]) rect([6 + clearance, sw], center = true); } if (score_detail) score() translate([34, 6]) text("GENTLE SLIP FIT", size = 3, halign = "center"); } part("Fit test, tab") { union() { rect([24, 28]); translate([9, -t]) rect([6, t + 0.2]); } if (score_detail) score() { translate([12, 14]) text("6 mm TAB", size = 3, halign = "center"); line([0, 0], [24, 0]); } } } // Assembled: every piece stands in its slots in the base, the fins turned // to their yaws, the stone at the back and the holder at the origin function fin_at(i) = [-t / 2 * sin(yaws[i]), depths[i] + t / 2 * cos(yaws[i]), 0]; if (output == "all" || output == "fins") step("Glue the fins into their numbered slots, arrows matching") for (i = [0:5]) part(str("Fin ", i + 1), at = fin_at(i), rotate = [90, 0, yaws[i]]) fin(i); if (output == "all" || output == "structure") { part("Base", at = [0, 0, -t]) base(); step("Glue the stone and the light holder into their slots") { part("Stone", at = [0, 300 + t / 2, 0], rotate = [90, 0, 0]) stone(); part("Light holder", at = [0, 0, 0], rotate = [90, 0, 0]) light_holder(); } } if (output == "fit") fit_test(); // Setting up: glue the fins, stone and holder into their numbered slots, the // arrows on the fins matching the arrows on the base. In a dark room, stand // the base square to a flat wall with the pinhole 900 mm from it, and light // it with a cool battery LED close behind the pinhole. Never a flame or laser. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/wave-cathedral/ # Wave Cathedral Wave Cathedral: a vault made of tides. Seventeen flat arches, each a little different, slot onto two notched rails on a base. Two crests run along the roof and the crown drifts from side to side, so the flat ribs read as a rolling wave. This design has 20 parts on 5 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=wave-cathedral). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral.kerf) - [wave-cathedral-sheet-1.svg](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-1.svg) - [wave-cathedral-sheet-1.dxf](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-1.dxf) - [wave-cathedral-sheet-2.svg](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-2.svg) - [wave-cathedral-sheet-2.dxf](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-2.dxf) - [wave-cathedral-sheet-3.svg](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-3.svg) - [wave-cathedral-sheet-3.dxf](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-3.dxf) - [wave-cathedral-sheet-4.svg](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-4.svg) - [wave-cathedral-sheet-4.dxf](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-4.dxf) - [wave-cathedral-sheet-5.svg](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-5.svg) - [wave-cathedral-sheet-5.dxf](https://kerfscript.mylasertools.com/models/wave-cathedral/wave-cathedral-sheet-5.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Rib 1 | 1 | 224 × 113 mm | | Rib 2 | 1 | 224 × 127 mm | | Rib 3 | 1 | 224 × 143.9 mm | | Rib 4 | 1 | 224 × 156.1 mm | | Rib 5 | 1 | 224 × 156.4 mm | | Rib 6 | 1 | 224 × 144.6 mm | | Rib 7 | 1 | 224 × 127.9 mm | | Rib 8 | 1 | 224 × 114.4 mm | | Rib 9 | 1 | 224 × 107.9 mm | | Rib 10 | 1 | 224 × 108.4 mm | | Rib 11 | 1 | 224 × 114.3 mm | | Rib 12 | 1 | 224 × 123.3 mm | | Rib 13 | 1 | 224 × 131.9 mm | | Rib 14 | 1 | 224 × 136 mm | | Rib 15 | 1 | 224 × 133.4 mm | | Rib 16 | 1 | 224 × 125.5 mm | | Rib 17 | 1 | 224 × 116 mm | | Rail L | 1 | 304 × 27 mm | | Rail R | 1 | 304 × 27 mm | | Base | 1 | 252 × 320 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `length` | `320` | 280 to 340 | | `wave` | `1` | 0.8 to 1.2 | | `output` | `"all"` | All parts, Ribs 1 to 4, Ribs 5 to 8, Ribs 9 to 12, Ribs 13 to 16, Rib 17, Base, Rails, Fit test | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.8 to 3.2 | | `clearance` | `0.15` | 0.1 to 0.3 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Wave Cathedral: a vault made of tides. Seventeen flat arches, each a little // different, slot onto two notched rails on a base. Two crests run along the // roof and the crown drifts from side to side, so the flat ribs read as a // rolling wave. /* [Pavilion] */ // Length of the base length = 320; // [280:10:340] // Height of the roof waves and how far the crown drifts wave = 1; // [0.8:0.1:1.2] // All parts, one group at a time, or three pieces to test the slot fit output = "all"; // [all:All parts, ribs_a:Ribs 1 to 4, ribs_b:Ribs 5 to 8, ribs_c:Ribs 9 to 12, ribs_d:Ribs 13 to 16, rib_e:Rib 17, base:Base, rails:Rails, fit:Fit test] // Scored contour lines, rib numbers and base markings score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to every slot, for a glued slip fit clearance = 0.15; // [0.10:0.05:0.30] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 120; s = length / 320; t = $thickness; sw = t + clearance; rail_x = 100 * s; rail_h = 24 * s; seat = 12 * s; // Where rib i stands along the base function station(i) = s * (16 + 18 * i); // Roof height along the nave (v from 0 at the front to 1 at the back): two crests function crest(v) = s * (100 + wave * (58 * exp(-pow((v - 0.22) / 0.18, 2)) + 36 * exp(-pow((v - 0.82) / 0.20, 2)))); // How far the crown leans sideways function drift(v) = s * wave * 29 * sin(360 * v - 32); // A point on rib i at angle a: every rib has the same feet, its own crown function arch(i, a, inner = false) = let (v = i / 16, r = (inner ? 88 : 112) * s, h = crest(v) - (inner ? 18 * s : 0), q = max(0, sin(a))) [r * cos(a) + drift(v) * q * q, h * pow(q, 0.86)]; function trace(i, a) = let (v = i / 16, q = max(0, sin(a))) [100 * s * cos(a) + drift(v) * q * q, (crest(v) - 9 * s) * pow(q, 0.86)]; module rib(i) { difference() { polygon(concat([for (j = [0:240]) arch(i, j * 180 / 240)], [for (j = [240:-1:0]) arch(i, j * 180 / 240, true)])); // Slots from below, over the slots from above in the rails for (side = [-1, 1]) translate([side * rail_x - sw / 2, -1]) rect([sw, seat + 1]); } if (score_detail) score() { polyline([for (j = [10:230]) trace(i, j * 180 / 240)]); translate([-100 * s, 26 * s]) text(str(i + 1), size = 3 * s, halign = "center", valign = "center"); polyline([[95 * s, 25 * s], [103 * s, 25 * s], [100 * s, 28 * s]]); } } module rail(label) { difference() { union() { translate([8 * s, 0]) rect([length - 16 * s, rail_h]); for (y = [40 * s, 160 * s, 280 * s]) translate([y - 6 * s, -t]) rect([12 * s, t + 0.3]); } for (i = [0:16]) translate([station(i) - sw / 2, seat]) rect([sw, rail_h - seat + 1]); } if (score_detail) score() { for (i = [0:16]) translate([station(i), 6 * s]) text(str(i + 1), size = 2.5 * s, halign = "center", valign = "center"); translate([151 * s, 18 * s]) text(label, size = 3 * s, halign = "center", valign = "center"); line([20 * s, 3 * s], [length - 20 * s, 3 * s]); } } module base() { difference() { translate([-126 * s, 0]) rect([252 * s, length], r = 8 * s); for (side = [-1, 1], y = [40 * s, 160 * s, 280 * s]) translate([side * rail_x, y]) rect([sw, 12 * s + clearance], center = true); } if (score_detail) score() { translate([0, 7.5 * s]) text("W A V E C A T H E D R A L", size = 3.4 * s, halign = "center", valign = "center"); translate([0, length - 7 * s]) text("A VAULT MADE OF TIDES / XVII", size = 2.7 * s, halign = "center", valign = "center"); for (side = [-1, 1]) polyline([for (j = [0:100]) let (v = j / 100) [side * (54 + 5 * sin(360 * v)) * s, (20 + 280 * v) * s]]); for (i = [0:16], side = [-1, 1]) { translate([side * 119 * s, station(i)]) text(str(i + 1), size = 2.4 * s, halign = "center", valign = "center"); line([side * 114 * s, station(i)], [side * 116 * s, station(i)]); } translate([-100 * s, 7 * s]) text("L", size = 3 * s, halign = "center", valign = "center"); translate([100 * s, 7 * s]) text("R", size = 3 * s, halign = "center", valign = "center"); polyline([[-12 * s, 22 * s], [12 * s, 22 * s], [8 * s, 25 * s]]); translate([0, 28 * s]) text("+X / FRONT", size = 2.5 * s, halign = "center", valign = "center"); } } module fit_test() { part("Fit test, base") { difference() { rect([48, 30], r = 2); translate([24, 17]) rect([sw, 12 * s + clearance], center = true); } if (score_detail) score() translate([24, 6]) text("BASE", size = 3, halign = "center"); } part("Fit test, rail") { difference() { union() { rect([36, rail_h]); translate([18 - 6 * s, -t]) rect([12 * s, t + 0.3]); } translate([18 - sw / 2, seat]) rect([sw, rail_h - seat + 1]); } if (score_detail) score() translate([7, 6]) text("R", size = 3); } part("Fit test, rib") { difference() { rect([30, 30 * s]); translate([15 - sw / 2, -1]) rect([sw, seat + 1]); } if (score_detail) score() translate([6, 21 * s]) text("V", size = 3); } } // Steps are numbered in the order they first run, so name them here in build order step("Glue the rails into the base, numbers running from the front") {} step("Lower the ribs into their numbered notches, scored faces to the front") {} // Assembled: the rails stand in the base, the ribs across them at their // stations module placed_rib(i) step("Lower the ribs into their numbered notches, scored faces to the front") part(str("Rib ", i + 1), at = [0, station(i) + t / 2, 0], rotate = [90, 0, 0]) rib(i); if (output == "all" || output == "ribs_a") for (i = [0:3]) placed_rib(i); if (output == "all" || output == "ribs_b") for (i = [4:7]) placed_rib(i); if (output == "all" || output == "ribs_c") for (i = [8:11]) placed_rib(i); if (output == "all" || output == "ribs_d") for (i = [12:15]) placed_rib(i); if (output == "all" || output == "rib_e") placed_rib(16); if (output == "all" || output == "rails") { step("Glue the rails into the base, numbers running from the front") { part("Rail L", at = [-rail_x - t / 2, 0, 0], rotate = [90, 0, 90]) rail("L"); part("Rail R", at = [rail_x - t / 2, 0, 0], rotate = [90, 0, 90]) rail("R"); } } if (output == "all" || output == "base") step("Glue the rails into the base, numbers running from the front") part("Base", at = [0, 0, -t]) base(); if (output == "fit") fit_test(); // Assembly: glue rail L and rail R into the base, numbers running from the // front. Lower ribs 1 to 17 into the matching notches, scored faces to the // front, then glue all the joints after a dry fit. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/morph/ # Morph contour sculptures Morph: one program, five sculptures. Flat contour layers stack on three dowels, with spacer plates between them; only each layer's outline turns and swells, so the stack becomes a bowl, an urn, a twisted tower, a lotus or a saddle. Pick a form, or choose custom and shape your own. This design has 83 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=morph). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/morph/morph.kerf) - [morph-sheet-1.svg](https://kerfscript.mylasertools.com/models/morph/morph-sheet-1.svg) - [morph-sheet-1.dxf](https://kerfscript.mylasertools.com/models/morph/morph-sheet-1.dxf) - [morph-sheet-2.svg](https://kerfscript.mylasertools.com/models/morph/morph-sheet-2.svg) - [morph-sheet-2.dxf](https://kerfscript.mylasertools.com/models/morph/morph-sheet-2.dxf) - [morph-sheet-3.svg](https://kerfscript.mylasertools.com/models/morph/morph-sheet-3.svg) - [morph-sheet-3.dxf](https://kerfscript.mylasertools.com/models/morph/morph-sheet-3.dxf) - [morph-sheet-4.svg](https://kerfscript.mylasertools.com/models/morph/morph-sheet-4.svg) - [morph-sheet-4.dxf](https://kerfscript.mylasertools.com/models/morph/morph-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Layer 1 | 1 | 107.5 × 96 mm | | Layer 2 | 1 | 110.4 × 99.5 mm | | Layer 3 | 1 | 114.4 × 104.6 mm | | Layer 4 | 1 | 118.8 × 110.2 mm | | Layer 5 | 1 | 123.2 × 116 mm | | Layer 6 | 1 | 127.3 × 121.7 mm | | Layer 7 | 1 | 130.9 × 126.9 mm | | Layer 8 | 1 | 133.7 × 131.5 mm | | Layer 9 | 1 | 135.3 × 135.1 mm | | Layer 10 | 1 | 135.7 × 137.5 mm | | Layer 11 | 1 | 134.7 × 138.5 mm | | Layer 12 | 1 | 132.2 × 138 mm | | Layer 13 | 1 | 128.4 × 136 mm | | Layer 14 | 1 | 123.3 × 132.6 mm | | Layer 15 | 1 | 117.3 × 128 mm | | Layer 16 | 1 | 110.9 × 122.7 mm | | Layer 17 | 1 | 105 × 117.4 mm | | Layer 18 | 1 | 105.1 × 116.9 mm | | Layer 19 | 1 | 107.4 × 117.8 mm | | Layer 20 | 1 | 109.8 × 118.8 mm | | Layer 21 | 1 | 112.2 × 119.6 mm | | Layer 22 | 1 | 114.6 × 120.4 mm | | Layer 23 | 1 | 117.1 × 121.1 mm | | Layer 24 | 1 | 120 × 122.3 mm | | Layer 25 | 1 | 124 × 124.5 mm | | Layer 26 | 1 | 128 × 126.7 mm | | Layer 27 | 1 | 130.6 × 127.4 mm | | Layer 28 | 1 | 130.8 × 125.7 mm | | Spacer | 54 | 62 × 62 mm | | Bottom cover | 1 | 139.4 × 139.4 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `preset` | `"helix"` | Tide bowl, Seed urn, Helix tower, Lotus bloom, Saddle vessel, Custom | | `output` | `"all"` | All parts, Two layers, Spacer plates, Bottom cover, Dowel fit test, One layer | | `batch` | `1` | 1 to 28 | | `score_detail` | `true` | on or off | | `profile` | `[48,55,48,52]` | | | `belly_at` | `0.42` | 0.25 to 0.65 | | `layer_count` | `28` | 10 to 28 | | `spacers_per_gap` | `2` | 0, 1, 2 | | `section_power` | `4` | 2 to 5 | | `stretch` | `1.12` | 1 to 1.2 | | `lobe_count` | `6` | 3 to 10 | | `lobe_depth` | `0` | 0 to 0.3 | | `twist` | `150` | -180 to 180 | | `rim_width` | `11` | 9 to 16 | | `spoke_count` | `3` | 3, 6, 9 | | `open_windows` | `true` | on or off | | `bore` | `32` | 20 to 32 | | `$thickness` | `3` | 2.8 to 3.2 | | `dowel_diameter` | `6` | 5.8 to 6.2 | | `dowel_clearance` | `0.25` | 0.15 to 0.35 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Morph: one program, five sculptures. Flat contour layers stack on three // dowels, with spacer plates between them; only each layer's outline turns // and swells, so the stack becomes a bowl, an urn, a twisted tower, a lotus // or a saddle. Pick a form, or choose custom and shape your own. /* [Form] */ // A ready recipe, or custom to use the settings below preset = "helix"; // [bowl:Tide bowl, urn:Seed urn, helix:Helix tower, lotus:Lotus bloom, saddle:Saddle vessel, custom:Custom] // All parts, or one group at a time output = "all"; // [all:All parts, layers:Two layers, spacers:Spacer plates, base:Bottom cover, fit:Dowel fit test, single:One layer] // Which pair of layers (Two layers), or which layer (One layer) batch = 1; // [1:1:28] // Scored layer numbers and the arrow that lines the layers up score_detail = true; /* [Custom profile] */ // Outer radius at the bottom, belly, neck and mouth profile = [48, 55, 48, 52]; // Height of the belly, as a share of the total belly_at = 0.42; // [0.25:0.01:0.65] // Contour layers layer_count = 28; // [10:1:28] // Spacer plates between two layers: 0 dense, 1 airy, 2 very open spacers_per_gap = 2; // [0, 1, 2] /* [Custom section] */ // Shape of each layer: 2 circle, 4 rounded square, 5 squarer section_power = 4; // [2:0.1:5] // Stretch one way before turning stretch = 1.12; // [1:0.01:1.2] // Petals around each layer lobe_count = 6; // [3:1:10] // Petal depth, as a share of the radius (0 for none) lobe_depth = 0; // [0:0.01:0.3] // Total turn from bottom to top, in degrees twist = 150; // [-180:1:180] // Width of the outer rim rim_width = 11; // [9:1:16] // Spokes between the rim and the hub spoke_count = 3; // [3, 6, 9] // Open windows between hub and rim, or solid layers open_windows = true; // Hole down the middle bore = 32; // [20:1:32] /* [Material] */ // Sheet thickness; also sets the height of the stack $thickness = 3; // [2.8:0.1:3.2] // Measured diameter of the three wooden dowels dowel_diameter = 6; // [5.8:0.05:6.2] // Added to the dowel holes, for a glued fit dowel_clearance = 0.25; // [0.15:0.05:0.35] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 120; // Each recipe: [profile, belly_at, layers, spacers, section, stretch, lobes, // lobe depth, twist, rim, spokes, windows, bore] recipes = [ [[52, 82, 95, 96], 0.40, 12, 1, 2, 1, 8, 0.10, 0, 11, 3, true, 32], [[52, 90, 55, 46], 0.46, 24, 1, 2, 1, 6, 0, 0, 12, 6, true, 28], [[48, 55, 48, 52], 0.42, 28, 2, 4, 1.12, 6, 0, 150, 11, 3, true, 32], [[48, 64, 88, 95], 0.35, 16, 1, 2, 1, 6, 0.28, -18, 10, 6, true, 32], [[78, 46, 53, 80], 0.48, 22, 1, 5, 1, 4, 0, -70, 12, 3, false, 30] ]; keys = ["bowl", "urn", "helix", "lotus", "saddle"]; p = preset == "custom" ? [profile, belly_at, layer_count, spacers_per_gap, section_power, stretch, lobe_count, lobe_depth, twist, rim_width, spoke_count, open_windows, bore] : recipes[search(preset, keys)]; pr = p[0]; peak = p[1]; N = p[2]; S = p[3]; power = p[4]; elong = p[5]; lobes = p[6]; depth = p[7]; turn = p[8]; rim = p[9]; spokes = p[10]; windows = p[11]; opening = p[12]; t = $thickness; hd = dowel_diameter + dowel_clearance; // Three dowels at unequal angles, so a layer only fits one way round pin_angles = [0, 120, 235]; pin_r = 24; hub = 31; stack_h = (N + (N - 1) * S) * t; foot_r = max(55, 0.28 * (stack_h + t)); echo("Three dowels, each", stack_h + t, "mm long"); function ease(a, b, v) = lerp(a, b, (1 - cos(180 * v)) / 2); // Outer radius at height v (0 to 1), through the four profile radii function radius(v) = v <= peak ? ease(pr[0], pr[1], v / peak) : v <= 0.82 ? ease(pr[1], pr[2], (v - peak) / (0.82 - peak)) : ease(pr[2], pr[3], (v - 0.82) / 0.18); function polar(r, a) = [r * cos(a), r * sin(a)]; // The outline at height v, angle a: a superellipse with petals, stretched and turned function point(v, a) = let ( r = radius(v) * (1 + depth * (1 + cos(lobes * a)) / 2) / pow(pow(abs(cos(a)), power) + pow(abs(sin(a)), power), 1 / power), x = elong * r * cos(a), y = r * sin(a), q = turn * v ) [x * cos(q) - y * sin(q), x * sin(q) + y * cos(q)]; module contour(i) polygon([for (j = [0:359]) point(i / (N - 1), j)]); module holes() for (a = pin_angles) translate(polar(pin_r, a)) circle(d = hd); module hub_blank() difference() { circle(r = hub); circle(d = opening); } // Two-digit numbers in single strokes, so the file needs no font segments = [[[0, 4], [2, 4]], [[2, 4], [2, 2]], [[2, 2], [2, 0]], [[0, 0], [2, 0]], [[0, 0], [0, 2]], [[0, 2], [0, 4]], [[0, 2], [2, 2]]]; digits = [[0, 1, 2, 3, 4, 5], [1, 2], [0, 1, 6, 4, 3], [0, 1, 6, 2, 3], [5, 6, 1, 2], [0, 5, 6, 2, 3], [0, 5, 6, 4, 2, 3], [0, 1, 2], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 5, 6]]; module number(n) for (k = [0:1]) translate([k * 3, 0]) for (s = digits[k == 0 ? floor(n / 10) : n % 10]) line(segments[s][0], segments[s][1]); module slab(i) { difference() { if (i == 0 || !windows) contour(i); else union() { difference() { contour(i); offset(delta = -rim) contour(i); } hub_blank(); intersection() { contour(i); for (a = [0:360 / spokes:359]) rotate(a) translate([22, -4]) rect([180, 8]); } } holes(); if (i > 0) circle(d = opening); } if (score_detail) score() { translate([18, 7]) number(i + 1); // Points at the dowel at 0 degrees on every layer polyline([[17, -7], [28, -7], [25, -9]]); } } module spacer() difference() { hub_blank(); holes(); } module foot() { circle(r = foot_r); if (score_detail) score() { // Targets that show through layer 1's holes, to centre it for (a = pin_angles) translate(polar(pin_r, a)) { line([-2, 0], [2, 0]); line([0, -2], [0, 2]); } line([-4, 0], [4, 0]); line([0, -4], [0, 4]); } } module fit_test() { part("Fit test, dowel holes") { difference() { rect([100, 30], r = 2); for (j = [0:3]) translate([14 + 24 * j, 15]) circle(d = dowel_diameter + 0.1 + j * 0.1); } if (score_detail) score() for (j = [0:3]) translate([11 + 24 * j, 23]) number(j + 1); } part("Fit test, hub") spacer(); } // Assembled: the bottom cover, then each layer with its spacers above it pitch = (1 + S) * t; function layer_z(i) = t + i * pitch; // Name the bottom cover's step first, since it goes on first if (output == "all") step("Glue the bottom cover onto the three dowels") {} step("Stack each layer and its spacers on the dowels, scored faces up and arrows lined up") { if (output == "all" || output == "layers") for (i = [0:N - 1]) if (output == "all" || floor(i / 2) == batch - 1) part(str("Layer ", i + 1), at = [0, 0, layer_z(i)]) slab(i); if (output == "single" && batch <= N) part(str("Layer ", batch), at = [0, 0, layer_z(batch - 1)]) slab(batch - 1); if ((output == "all" || output == "spacers") && S > 0) part("Spacer", qty = (N - 1) * S, at = [for (g = [0:N - 2], j = [0:S - 1]) [0, 0, layer_z(g) + t + j * t]]) spacer(); } if (output == "all" || output == "base") step("Glue the bottom cover onto the three dowels") part("Bottom cover", at = [0, 0, 0]) foot(); if (output == "fit") fit_test(); // Assembly: cut the fit test first and set dowel_clearance from it. Stack // the bottom cover, layer 1, the spacers, layer 2 and so on, all scored faces // up and the arrows lined up, on the three dowels. Glue as you go and trim // the dowels flush with the top layer. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/orbit/ # Orbit planetary gears Orbit: a planetary gear demonstrator. A crank turns the sun gear in the middle; three planets roll around it inside a fixed 72-tooth ring and carry the carrier arms with them. The carrier turns once for every four turns of the crank, in the same direction, while the planets spin the other way. This design has 69 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=orbit). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/orbit/orbit.kerf) - [orbit-sheet-1.svg](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-1.svg) - [orbit-sheet-1.dxf](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-1.dxf) - [orbit-sheet-2.svg](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-2.svg) - [orbit-sheet-2.dxf](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-2.dxf) - [orbit-sheet-3.svg](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-3.svg) - [orbit-sheet-3.dxf](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-3.dxf) - [orbit-sheet-4.svg](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-4.svg) - [orbit-sheet-4.dxf](https://kerfscript.mylasertools.com/models/orbit/orbit-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Sun gear | 2 | 78 × 78 mm | | Planet gear | 6 | 78 × 78 mm | | Ring gear | 3 | 260 × 260 mm | | Base | 2 | 260 × 260 mm | | Spindle pedestal | 2 | 34 × 34 mm | | Ring standoff | 12 | 18 × 18 mm | | Foot | 21 | 18 × 18 mm | | Carrier | 2 | 130 × 146.7 mm | | Hub sleeve | 8 | 22 × 22 mm | | Carrier spacer | 1 | 32 × 32 mm | | Crank | 2 | 36 × 73 mm | | Handle | 6 | 20 × 20 mm | | Fit test | 1 | 110 × 48 mm | | Timing card | 1 | 190 × 80 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `planet_teeth` | `24` | 24, 30, 36 | | `gear_module` | `3` | 2.5 to 3 | | `backlash` | `0.3` | 0.2 to 0.5 | | `crank_angle` | `0` | 0 to 1440 | | `output` | `"all"` | All parts, Gears, Base standoffs and feet, Carrier, Hub sleeves, Crank and handle, Fit test, Timing card | | `$thickness` | `3` | 2.8 to 3.2 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Orbit: a planetary gear demonstrator. A crank turns the sun gear in the // middle; three planets roll around it inside a fixed 72-tooth ring and carry // the carrier arms with them. The carrier turns once for every four turns of // the crank, in the same direction, while the planets spin the other way. /* [Gearing] */ // Teeth on each planet; the ring has 24 + 2 x this many planet_teeth = 24; // [24, 30, 36] // Tooth size: pitch diameter = teeth x module gear_module = 3; // [2.5:0.1:3] // Total play between two meshing gears, at the pitch circle backlash = 0.3; // [0.2:0.05:0.5] /* [Assembly] */ // Crank angle in the assembled view; the carrier turns a quarter as far crank_angle = 0; // [0:5:1440] /* [Output] */ // All parts, or one group at a time output = "all"; // [all:All parts, gears:Gears, frame:Base standoffs and feet, carrier:Carrier, hub:Hub sleeves, crank:Crank and handle, coupon:Fit test, timing:Timing card] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to the 6 mm shaft holes and the 22 mm hub hole bearing_clearance = 0.3; // [0.2:0.05:0.4] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 160; t = $thickness; ns = 24; np = planet_teeth; nr = ns + 2 * np; m = gear_module; rs = ns * m / 2; rp = np * m / 2; rr = nr * m / 2; // Distance from the centre to each planet orbit = rs + rp; outer = rr + 22; mounting = rr + 13; hub = 22; ratio = 1 + nr / ns; assert(contains([24, 30, 36], np), "Planet teeth: 24, 30 or 36"); assert(m >= 2.5 && m <= 3, "Module: 2.5 to 3 mm"); assert(backlash >= 0.2 && backlash <= 0.5, "Backlash: 0.2 to 0.5 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 1440, "Crank angle: 0 to 1440 degrees"); assert(contains(["all", "gears", "frame", "carrier", "hub", "crank", "coupon", "timing"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; module mounting_holes() { for (a = [30, 150, 270]) translate(polar(mounting, a)) circle(d = 4.5); } // n scored tally marks, to number the laminates module tally(n, p) { score() for (j = [0:n - 1]) translate(p + [2 * j, 0]) rect([0.3, 4]); } // Involute teeth with a 20 degree pressure angle. backlash is shared between // the two gears of a mesh: every tooth is thinned, and every ring space // widened, by backlash / 2. The roots get a plain radial relief. function gear_inv(a) = tan(a) * 180 / PI - a; function gear_polar(r, a) = [r * cos(a), r * sin(a)]; // Half the angular width of a tooth (or of a ring space) at radius r function gear_half(z, m, r, backlash, internal = false) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) 90 / z + gear_inv(20) - gear_inv(ar) + (internal ? 1 : -1) * backlash / (4 * rp) * 180 / PI; function gear_outline(z, m, backlash, internal = false, steps = 24) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - (internal ? 1 : 1.25) * m, hi = rp + (internal ? 1.25 : 1) * m, start = max(lo, rb), hs = gear_half(z, m, start, backlash, internal), ht = gear_half(z, m, hi, backlash, internal)) [for (k = [0:z - 1]) each concat( [gear_polar(lo, 360 * k / z - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z - gear_half(z, m, r, backlash, internal))], [for (j = [1:8]) gear_polar(hi, 360 * k / z - ht + 2 * ht * j / 8)], [for (j = [steps - 1:-1:0]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z + gear_half(z, m, r, backlash, internal))], [if (start > lo) gear_polar(lo, 360 * k / z + hs)], [for (j = [1:7]) gear_polar(lo, 360 * k / z + hs + (360 / z - 2 * hs) * j / 8)] )]; module externalgear(z, m, backlash) { polygon(gear_outline(z, m, backlash, false)); } module internalhole(z, m, backlash) { polygon(gear_outline(z, m, backlash, true)); } // Timing, with the crank at 0: the sun has a tooth centred on +X and the ring // hole is turned half a tooth. Planet k sits at beta = 120 k degrees, turned // by (1 + 24 / planet_teeth) beta + 180 - 180 / planet_teeth. module sun() { difference() { externalgear(ns, m, backlash); circle(d = 6 + bearing_clearance); } score() line([12, 0], [rs - 5, 0]); } module planet() { difference() { externalgear(np, m, backlash); circle(d = 6 + bearing_clearance); } score() line([9, 0], [rp - 5, 0]); } module ring() { difference() { circle(r = outer); rotate(180 / nr) internalhole(nr, m, backlash); mounting_holes(); } score() line([rr + 5, 0], [outer - 4, 0]); } module carrier() { difference() { union() { circle(r = 18); for (a = [0, 120, 240]) hull() { circle(r = 12); translate(polar(orbit, a)) circle(r = 11); } } circle(d = hub + bearing_clearance); for (a = [0, 120, 240]) translate(polar(orbit, a)) circle(d = 4.4); } for (i = [0:2]) tally(i + 1, polar(orbit - 18, 120 * i)); } module base() { difference() { circle(r = outer); circle(d = 6.2); mounting_holes(); } score() outline() circle(r = orbit); for (a = [0, 120, 240]) score() line(polar(orbit - 4, a), polar(orbit + 4, a)); } module ring_spacer() { difference() { circle(d = 18); circle(d = 4.5); } } module foot() { difference() { circle(d = 18); circle(d = 12); } } module hub_ring() { difference() { circle(d = hub); circle(d = 6 + bearing_clearance); } } module crank() { difference() { hull() { circle(r = 18); translate([0, 44]) circle(r = 11); } circle(d = 6 + bearing_clearance); translate([0, 44]) circle(d = 4.5); } score() line([0, 10], [0, 36]); } module handle() { difference() { circle(d = 20); circle(d = 6 + bearing_clearance); } } // The assembled view, base bottom at z = 0. The crank turns the sun; the // carrier follows at 1 / ratio, and each planet spins as it rolls round // inside the ring. a = crank_angle; carrier_turn = a / ratio; crank_turn = a - 125; function at(p, z) = concat(p, [z]); function layers(z, n) = [for (k = [0:n - 1]) [0, 0, z + k * t]]; function planet_spin(beta) = (1 + ns / np) * beta - ns / np * a + 180 - 180 / np; // Each planet is two layers, one planet after another planets = [for (j = [0:2], k = [0:1]) [carrier_turn + 120 * j, k]]; // The assembly steps in build order, named first so they number that way build_steps = [ "Glue the pedestal and the feet under the base", "Put the hub sleeves and the sun gear on the spindle", "Bolt the ring gear on its standoffs", "Set each planet by the timing card and lower the carrier into mesh", "Add the carrier spacer, then the crank and its handle" ]; if (output == "all") for (k = build_steps) step(k) {} if (output == "all" || output == "gears") { step(build_steps[1]) part("Sun gear", qty = 2, at = layers(6 * t + 0.5, 2), rotate = [0, 0, a]) sun(); step(build_steps[3]) part("Planet gear", qty = 6, at = [for (q = planets) at(polar(orbit, q[0]), 6 * t + 0.5 + q[1] * t)], rotate = [for (q = planets) [0, 0, planet_spin(q[0])]]) planet(); step(build_steps[2]) part("Ring gear", qty = 3, at = layers(6 * t + 0.5, 3)) ring(); } if (output == "all" || output == "frame") { step(build_steps[0]) part("Base", qty = 2, at = layers(0, 2)) base(); step(build_steps[0]) part("Spindle pedestal", qty = 2, at = [[0, 0, -t], [0, 0, -2 * t]]) difference() { circle(d = 34); circle(d = 6.1); } // A stack of four under each mounting hole, and a foot of seven below it step(build_steps[2]) part("Ring standoff", qty = 12, at = [for (j = [0:2], k = [0:3]) at(polar(mounting, 30 + 120 * j), 2 * t + k * t)]) ring_spacer(); step(build_steps[0]) part("Foot", qty = 21, at = [for (j = [0:2], k = [0:6]) at(polar(mounting, 30 + 120 * j), -7 * t + k * t)]) foot(); } if (output == "all" || output == "carrier") step(build_steps[3]) part("Carrier", qty = 2, at = layers(8 * t + 1.2, 2), rotate = [0, 0, carrier_turn]) carrier(); if (output == "all" || output == "hub") { // Four below the sun and four above it step(build_steps[1]) part("Hub sleeve", qty = 8, at = concat(layers(2 * t + 0.5, 4), layers(8 * t + 0.5, 4))) hub_ring(); step(build_steps[4]) part("Carrier spacer", at = [0, 0, 10 * t + 1.2]) difference() { circle(d = 32); circle(d = hub + bearing_clearance); } } if (output == "all" || output == "crank") { step(build_steps[4]) part("Crank", qty = 2, at = layers(12 * t + 0.5, 2), rotate = [0, 0, crank_turn]) crank(); step(build_steps[4]) part("Handle", qty = 6, at = [for (k = [0:5]) at(polar(44, 90 + crank_turn), 14 * t + 1 + k * t)], rotate = [0, 0, crank_turn]) handle(); } // The fit test and the timing card lie on the table beside the model if (output == "all" || output == "coupon") part("Fit test", at = [outer + 20, -outer, -7 * t]) { difference() { rect([110, 48], r = 3); for (i = [0:3]) translate([15 + 25 * i, 16]) circle(d = 6.1 + i * 0.1); translate([28, 35]) circle(d = hub + bearing_clearance); translate([70, 35]) circle(d = 4.4); } for (i = [0:3]) tally(i + 1, [12 + 25 * i, 4]); } // One dial per planet: the long line points to the planet, the short one shows how far to turn it if (output == "all" || output == "timing") part("Timing card", at = [outer + 20, -outer + 60, -7 * t]) { rect([190, 80], r = 3); for (i = [0:2]) { p = [35 + 60 * i, 40]; a = 120 * i; phi = (1 + ns / np) * a + 180 - 180 / np; score() translate(p) outline() circle(r = 24); score() line(p, p + polar(22, phi)); score() line(p, p + polar(24, a)); tally(i + 1, p + [-7, -32]); } } echo(str("Orbit: ", ns, " / ", np, " / ", nr, " teeth, ", ratio, " crank turns per carrier turn, ", 2 * outer, " mm across")); // Assembly, with base bottom at z = 0 and t the sheet thickness: glue the // laminates first (base, pedestal, ring, sun, each planet, carrier, crank, // three standoffs of 4 discs, three feet of 7, a handle of 6). Glue the // pedestal under the base and fix a 6 mm steel spindle through both, and the // feet under the mounting holes. On the spindle: a 0.5 mm washer, 4 hub // sleeves, the sun, 4 more hub sleeves, all glued to the sun only. Bolt the // ring on its standoffs (6t to 9t), score line to +X like the sun's. Planets // turn on brass bushes bolted to the carrier (8t to 10t); set each planet by // the timing card and lower the carrier into mesh. Add the carrier spacer and // shims, glue the crank on top of the upper hub, then the handle on its bush. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/trace/ # Trace drawing machine Trace: a drawing machine. A crank turns the carrier, which rolls a 24-tooth gear around the inside of a fixed ring. A pencil held in that gear, off its centre, draws a hypotrochoid on paper laid on the base: with the 80-tooth ring, ten lobes that close after three turns of the crank. This design has 56 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=trace). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/trace/trace.kerf) - [trace-sheet-1.svg](https://kerfscript.mylasertools.com/models/trace/trace-sheet-1.svg) - [trace-sheet-1.dxf](https://kerfscript.mylasertools.com/models/trace/trace-sheet-1.dxf) - [trace-sheet-2.svg](https://kerfscript.mylasertools.com/models/trace/trace-sheet-2.svg) - [trace-sheet-2.dxf](https://kerfscript.mylasertools.com/models/trace/trace-sheet-2.dxf) - [trace-sheet-3.svg](https://kerfscript.mylasertools.com/models/trace/trace-sheet-3.svg) - [trace-sheet-3.dxf](https://kerfscript.mylasertools.com/models/trace/trace-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Ring gear | 2 | 280 × 280 mm | | Rolling gear | 2 | 78 × 78 mm | | Base | 2 | 280 × 280 mm | | Ring standoff | 12 | 16 × 16 mm | | Pedestal | 9 | 22 × 22 mm | | Foot | 15 | 18 × 18 mm | | Carrier | 2 | 141 × 30 mm | | Planet spacer | 3 | 14 × 14 mm | | Handle | 5 | 18 × 18 mm | | Pencil sleeve | 3 | 14 × 14 mm | | Fit test | 1 | 130 × 45 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `ring_teeth` | `80` | 72, 80, 84 | | `pen_offset` | `22` | 18 to 24 | | `backlash` | `0.3` | 0.2 to 0.5 | | `carrier_angle` | `0` | 0 to 1080 | | `output` | `"all"` | All parts, Gears, Base standoffs and feet, Carrier and handle, Pencil sleeve, Fit test, Paper template | | `$thickness` | `3` | 2.8 to 3.2 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Trace: a drawing machine. A crank turns the carrier, which rolls a 24-tooth // gear around the inside of a fixed ring. A pencil held in that gear, off its // centre, draws a hypotrochoid on paper laid on the base: with the 80-tooth // ring, ten lobes that close after three turns of the crank. /* [Drawing] */ // Teeth on the fixed ring: 72 draws 3 lobes, 80 draws 10, 84 draws 7 ring_teeth = 80; // [72, 80, 84] // Distance from the rolling gear's centre to the pencil pen_offset = 22; // [18:1:24] // Total play between the two gears, at the pitch circle backlash = 0.3; // [0.2:0.05:0.5] /* [Assembly] */ // Carrier angle in the assembled view; the drawing closes after three turns carrier_angle = 0; // [0:5:1080] /* [Output] */ // All parts, one group at a time, or a stencil for cutting the paper output = "all"; // [all:All parts, gears:Gears, frame:Base standoffs and feet, drive:Carrier and handle, holder:Pencil sleeve, coupon:Fit test, paper:Paper template] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to the 6 mm shaft holes bearing_clearance = 0.3; // [0.2:0.05:0.4] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 160; t = $thickness; nr = ring_teeth; np = 24; m = 3; R = nr * m / 2; r = np * m / 2; // Distance from the centre to the rolling gear's axle a = R - r; d = pen_offset; outer = R + 20; mount = R + 12; // Crank turns until the drawing closes turns = np / gcd(nr, np); assert(contains([72, 80, 84], nr), "Ring teeth: 72, 80 or 84"); assert(d >= 18 && d <= 24, "Pen offset: 18 to 24 mm"); assert(backlash >= 0.2 && backlash <= 0.5, "Backlash: 0.2 to 0.5 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "gears", "frame", "drive", "holder", "coupon", "paper"], output), "Unknown output"); function gcd(a, b) = b == 0 ? a : gcd(b, a % b); function polar(r, a) = [r * cos(a), r * sin(a)]; // Where the pencil is when the carrier has turned theta degrees function pen(theta) = polar(a, theta) + polar(d, (1 - nr / np) * theta); module mounts() { for (q = [30, 150, 270]) translate(polar(mount, q)) circle(d = 4.5); } // n scored tally marks, to number the parts module tally(n, p) { score() for (j = [0:n - 1]) translate(p + [2 * j, 0]) rect([0.3, 4]); } // Involute teeth with a 20 degree pressure angle. backlash is shared between // the two gears: every tooth is thinned, and every ring space widened, by // backlash / 2. The roots get a plain radial relief. function gear_inv(a) = tan(a) * 180 / PI - a; function gear_polar(r, a) = [r * cos(a), r * sin(a)]; // Half the angular width of a tooth (or of a ring space) at radius r function gear_half(z, m, r, backlash, internal = false) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) 90 / z + gear_inv(20) - gear_inv(ar) + (internal ? 1 : -1) * backlash / (4 * rp) * 180 / PI; function gear_outline(z, m, backlash, internal = false, steps = 24) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - (internal ? 1 : 1.25) * m, hi = rp + (internal ? 1.25 : 1) * m, start = max(lo, rb), hs = gear_half(z, m, start, backlash, internal), ht = gear_half(z, m, hi, backlash, internal)) [for (k = [0:z - 1]) each concat( [gear_polar(lo, 360 * k / z - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z - gear_half(z, m, r, backlash, internal))], [for (j = [1:8]) gear_polar(hi, 360 * k / z - ht + 2 * ht * j / 8)], [for (j = [steps - 1:-1:0]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z + gear_half(z, m, r, backlash, internal))], [if (start > lo) gear_polar(lo, 360 * k / z + hs)], [for (j = [1:7]) gear_polar(lo, 360 * k / z + hs + (360 / z - 2 * hs) * j / 8)] )]; module externalgear(z, m, backlash) { polygon(gear_outline(z, m, backlash, false)); } module internalhole(z, m, backlash) { polygon(gear_outline(z, m, backlash, true)); } // Timing: a ring space and a rolling-gear tooth are both centred on +X. The // rolling gear turns (1 - ring_teeth / 24) times the carrier angle. module ring() { difference() { circle(r = outer); internalhole(nr, m, backlash); mounts(); } score() line([R + 5, 0], [outer - 3, 0]); } module planet() { difference() { externalgear(np, m, backlash); circle(d = 6 + bearing_clearance); translate([d, 0]) circle(d = 8.4); } score() line([7, 0], [d - 6, 0]); } module base() { difference() { circle(r = outer); circle(d = 6.2); mounts(); } score() outline() circle(r = R - 6); } module carrier() { difference() { hull() { translate([-35, 0]) circle(r = 11); circle(r = 15); translate([a, 0]) circle(r = 11); } circle(d = 6 + bearing_clearance); translate([a, 0]) circle(d = 4.4); translate([-35, 0]) circle(d = 4.4); } score() line([15, 0], [a - 14, 0]); } module washer(od, id) { difference() { circle(d = od); circle(d = id); } } module paper_template() { difference() { circle(r = R - 6); circle(d = 26); } score() { outline() circle(r = a); line([-14, 0], [-22, 0]); } } // The assembled view, base bottom at z = 0. The carrier turns the rolling // gear round inside the ring, and it spins the other way as it goes. c_turn = carrier_angle; spin = (1 - nr / np) * c_turn; gear_at = polar(a, c_turn); function at(p, z) = concat(p, [z]); function layers(p, z, n) = [for (k = [0:n - 1]) at(p, z + k * t)]; // Standoffs and feet: a stack under each of the three mounting holes function stacks(z, n) = [for (j = [0:2], k = [0:n - 1]) at(polar(mount, 30 + 120 * j), z + k * t)]; // Steps are numbered in the order they first run, so name them here in build order step("Glue up the base and stand it on its feet, with the standoffs and the pedestal") {} step("Bolt the ring gear on its standoffs") {} step("Fit the rolling gear under the carrier and mesh it with the ring") {} step("Add the handle stack at the carrier's short end") {} if (output == "all" || output == "gears") { step("Bolt the ring gear on its standoffs") part("Ring gear", qty = 2, at = layers([0, 0], 6 * t, 2)) ring(); step("Fit the rolling gear under the carrier and mesh it with the ring") part("Rolling gear", qty = 2, at = layers(gear_at, 6 * t + 0.15, 2), rotate = [0, 0, spin]) planet(); } if (output == "all" || output == "frame") step("Glue up the base and stand it on its feet, with the standoffs and the pedestal") { part("Base", qty = 2, at = layers([0, 0], 0, 2)) base(); part("Ring standoff", qty = 12, at = stacks(2 * t, 4)) washer(16, 4.5); part("Pedestal", qty = 9, at = layers([0, 0], 2 * t, 9)) washer(22, 6.2); part("Foot", qty = 15, at = stacks(-5 * t, 5)) washer(18, 12); } if (output == "all" || output == "drive") { step("Fit the rolling gear under the carrier and mesh it with the ring") part("Carrier", qty = 2, at = layers([0, 0], 11 * t + 0.35, 2), rotate = [0, 0, c_turn]) carrier(); step("Fit the rolling gear under the carrier and mesh it with the ring") part("Planet spacer", qty = 3, at = layers(gear_at, 8 * t + 0.35, 3)) washer(14, 6.2); step("Add the handle stack at the carrier's short end") part("Handle", qty = 5, at = layers(polar(35, 180 + c_turn), 13 * t + 0.85, 5)) washer(18, 6 + bearing_clearance); } if (output == "all" || output == "holder") step("Fit the rolling gear under the carrier and mesh it with the ring") part("Pencil sleeve", qty = 3, at = layers(pen(c_turn), 3 * t + 0.15, 3)) washer(14, 8.4); // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [outer + 20, -outer, -5 * t]) { difference() { rect([130, 45], r = 3); for (i = [0:3]) translate([15 + 25 * i, 15]) circle(d = 6.1 + i * 0.1); translate([113, 15]) circle(d = 8.4); translate([65, 34]) circle(d = 4.4); } for (i = [0:3]) tally(i + 1, [12 + 25 * i, 29]); } // Trace this onto paper and cut the paper by hand; it is not part of the kit if (output == "paper") part("Paper template") paper_template(); echo(str("Trace: ", turns, " crank turns per drawing, pencil ", a - d, " to ", a + d, " mm from the centre, ", 2 * outer, " mm across")); // Assembly, with base bottom at z = 0 and t the sheet thickness: glue the // two-layer ring, rolling gear, base and carrier, then three standoffs of 4 // discs, three feet of 5 under the mounting holes and a pedestal of 9 at the // centre of the base. Fix a 6 mm steel spindle in the pedestal. Bolt the ring // on its standoffs (6t to 8t). Glue the 3 pencil sleeves under the rolling // gear's pencil hole and the 3 planet spacers under the carrier's axle hole; // the gear turns on a brass bush bolted to the carrier. Lay the paper on the // base around the pedestal, put the carrier on the spindle (11t up), mesh the // gear with its tooth and score line at +X, and add the handle stack on a // bush at the carrier's short end. A short pencil stub, wrapped in paper for // a snug fit, goes up through the sleeve with its tip just on the paper. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/stride/ # Stride walking linkage Stride: Theo Jansen's walking linkage on a standing board. Seven flat links turn on brass bushes in front of the board; turn the crank and the foot traces the scored path, lifting and stepping like a Strandbeest leg. This design has 56 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=stride). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/stride/stride.kerf) - [stride-sheet-1.svg](https://kerfscript.mylasertools.com/models/stride/stride-sheet-1.svg) - [stride-sheet-1.dxf](https://kerfscript.mylasertools.com/models/stride/stride-sheet-1.dxf) - [stride-sheet-2.svg](https://kerfscript.mylasertools.com/models/stride/stride-sheet-2.svg) - [stride-sheet-2.dxf](https://kerfscript.mylasertools.com/models/stride/stride-sheet-2.dxf) - [stride-sheet-3.svg](https://kerfscript.mylasertools.com/models/stride/stride-sheet-3.svg) - [stride-sheet-3.dxf](https://kerfscript.mylasertools.com/models/stride/stride-sheet-3.dxf) - [stride-sheet-4.svg](https://kerfscript.mylasertools.com/models/stride/stride-sheet-4.svg) - [stride-sheet-4.dxf](https://kerfscript.mylasertools.com/models/stride/stride-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | C crank | 1 | 46 × 16 mm | | A upper link | 1 | 114 × 14 mm | | B lower link | 1 | 137.8 × 14 mm | | R rocker | 1 | 115.6 × 92.1 mm | | D anchor link | 1 | 92.6 × 14 mm | | E coupler | 1 | 92.8 × 14 mm | | F foot | 1 | 78.6 × 141.4 mm | | Backboard | 3 | 360 × 340 mm | | Stand foot | 4 | 130 × 35 mm | | Spacer Z, C to A | 1 | 12 × 12 mm | | Spacer Z, A to B | 7 | 12 × 12 mm | | Spacer X, A to R | 1 | 12 × 12 mm | | Spacer W, D to B | 1 | 12 × 12 mm | | Spacer W, B to F | 4 | 12 × 12 mm | | Spacer U, E to F | 1 | 12 × 12 mm | | Spacer V, R to E | 7 | 12 × 12 mm | | Spacer Y, R to D | 1 | 12 × 12 mm | | Spacer Y, board to R | 8 | 12 × 12 mm | | Handle grip | 6 | 18 × 18 mm | | Rear crank | 2 | 60 × 20 mm | | Hex socket | 2 | 18 × 18 mm | | Fit test | 1 | 130 × 50 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `unit_scale` | `2` | 1.6 to 2.2 | | `crank_radius` | `15` | 14 to 15 | | `link_width` | `10` | 8 to 12 | | `pivot_clearance` | `0.3` | 0.2 to 0.4 | | `crank_angle` | `0` | 0 to 360 | | `output` | `"all"` | All parts, Links, Backboard, Stand feet, Spacers and handle, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Stride: Theo Jansen's walking linkage on a standing board. Seven flat // links turn on brass bushes in front of the board; turn the crank and the // foot traces the scored path, lifting and stepping like a Strandbeest leg. /* [Mechanism] */ // Overall size: every Jansen length is multiplied by this unit_scale = 2; // [1.6:0.1:2.2] // Crank radius in Jansen units; changes the shape of the foot path crank_radius = 15; // [14:0.5:15] // Width of the links in mm, whatever the scale link_width = 10; // [8:1:12] /* [Fit] */ // Added to the 6 mm holes the brass bushes turn in pivot_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // Crank angle in the assembled view crank_angle = 0; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, links:Links, frame:Backboard, feet:Stand feet, accessories:Spacers and handle, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; s = unit_scale; t = $thickness; r = link_width / 2; hole = 6 + pivot_clearance; assert(s >= 1.6 && s <= 2.2, "Scale: 1.6 to 2.2"); assert(crank_radius >= 14 && crank_radius <= 15, "Crank radius: 14 to 15"); assert(link_width >= 8 && link_width <= 12, "Link width: 8 to 12 mm"); assert(pivot_clearance >= 0.2 && pivot_clearance <= 0.4, "Pivot clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "links", "frame", "feet", "accessories", "coupon"], output), "Unknown output"); function polar(rad, a) = [rad * cos(a), rad * sin(a)]; // Where circles round p (radius rad) and q (radius rad2) cross; sgn picks the side function meet(p, rad, q, rad2, sgn) = let (v = q - p, d = norm(v), a = (rad * rad - rad2 * rad2 + d * d) / (2 * d), h = sqrt(max(0, rad * rad - a * a))) p + v * a / d + [-v[1], v[0]] * h / d * sgn; // Joints O, Y, Z, X, W, V, U, T at crank angle theta: Jansen's lengths, scaled function joints(theta) = let ( O = [0, 0], Y = [-38, -7.8], Z = polar(crank_radius, theta), X = meet(Z, 50, Y, 41.5, -1), W = meet(Z, 61.9, Y, 39.3, 1), V = meet(Y, 40.1, X, 55.8, 1), U = meet(V, 39.4, W, 36.7, -1), T = meet(W, 49, U, 65.7, 1)) [for (p = [O, Y, Z, X, W, V, U, T]) p * s]; J = joints(0); FW = 180 * s; FH = 170 * s; // Where the crank axle O sits on the board origin = [125 * s, 133 * s]; module capsule(a, b, rr) { hull() { translate(a) circle(r = rr); translate(b) circle(r = rr); } } module rod(a, b, n) { difference() { union() { capsule(a, b, r); translate(a) circle(r = 7); translate(b) circle(r = 7); } translate(a) circle(d = hole); translate(b) circle(d = hole); } score() line(a, b); score() translate((a + b) / 2 + [0, -1.5]) text(n, size = 3, halign = "center"); } module triangle(a, b, c, n) { difference() { union() { capsule(a, b, r); capsule(b, c, r); capsule(c, a, r); for (p = [a, b, c]) translate(p) circle(r = 7); } for (p = [a, b, c]) translate(p) circle(d = hole); } score() translate((a + b) / 2 + [0, -1.5]) text(n, size = 3, halign = "center"); } module crank() { difference() { capsule([0, 0], [crank_radius * s, 0], 8); circle(d = 4.4); translate([crank_radius * s, 0]) circle(d = hole); } score() line([0, 0], [crank_radius * s, 0]); } module frame() { difference() { rect([FW, FH], r = 8); translate(origin) circle(d = 6.2); translate(origin + J[1]) circle(d = 4.4); // Slots for the two stand feet for (x = [25 * s, 155 * s]) translate([x - t, 0]) rect([2 * t, 15]); } // The path the foot T follows through a full turn score() polyline([for (a = [0:2:360]) origin + joints(a)[7]]); score() translate([FW / 2, FH - 18]) text("STRIDE", size = 12, halign = "center"); score() translate([FW / 2, FH - 30]) text("JANSEN LINKAGE / TURN SLOWLY", size = 4, halign = "center"); score() translate(origin + [9, 0]) text("O", size = 4); score() translate(origin + J[1] + [-9, 0]) text("Y", size = 4); } module stand_foot() { difference() { rect([130, 35], r = 3); translate([65 - 1.5 * t, 15]) rect([3 * t, 22]); } } module disc(d, inner) { difference() { circle(d = d); circle(d = inner); } } // Spacer rings on the bushes: the joints and layers they sit between, the gap // in mm, then the joint (O Y Z X W V U T) and the layer they start from spacers = [ ["Z, C to A", 5, 2, "C"], ["Z, A to B", 2 * t + 15, 2, "A"], ["X, A to R", 5, 3, "A"], ["W, D to B", 5, 4, "D"], ["W, B to F", t + 10, 4, "B"], ["U, E to F", 5, 6, "E"], ["V, R to E", 2 * t + 15, 5, "R"], ["Y, R to D", 5, 1, "R"], ["Y, board to R", 10 + 2 * (t + 5), 1, ""] ]; // The assembled view, z up from the table, the board's front face at y = 0 // and the links in front of it (towards -y), back to front C A R D B E F, // each one t + 5 mm further out. A link drawn along x from its first joint // turns to point at its second; the rocker and foot, drawn where they sit at // crank angle 0, turn about their first joint. P = joints(crank_angle); function depth(layer) = layer == "" ? 0 : 10 + search(layer, "CARDBEF") * (t + 5) + t; function front(j, d) = [origin.x + P[j].x, -d, origin.y + P[j].y]; function heading(i, j) = let (v = P[j] - P[i]) atan2(v[1], v[0]); function face(a) = [90, -a, 0]; function link_at(layer, i) = front(i, depth(layer) - t); // The rocker or foot, turned about joint i as it swings from J to P function swing(i, j) = let (v = J[j] - J[i]) heading(i, j) - atan2(v[1], v[0]); function pivoted(layer, i, j) = let (b = swing(i, j), w = [-J[i].x * cos(b) + J[i].y * sin(b), -J[i].x * sin(b) - J[i].y * cos(b)]) link_at(layer, i) + [w.x, 0, w.y]; // Behind the board: the hex socket, two rear cranks and the handle grip turn = crank_angle - 70; behind = -3 * t - 1; function on_axle(d) = [origin.x, -d, origin.y]; // Steps are numbered in the order they first run, so name them here in build order step("Glue the three backboards together and stand them in the feet") {} step("Fit the links on brass bushes, back to front, with the spacer rings between them") {} step("Fit the hex sockets, the rear cranks and the handle grip on the spindle") {} if (output == "all" || output == "links") step("Fit the links on brass bushes, back to front, with the spacer rings between them") { part("C crank", at = link_at("C", 0), rotate = face(crank_angle)) crank(); part("A upper link", at = link_at("A", 2), rotate = face(heading(2, 3))) rod([0, 0], [50 * s, 0], "A"); part("B lower link", at = link_at("B", 2), rotate = face(heading(2, 4))) rod([0, 0], [61.9 * s, 0], "B"); part("R rocker", at = pivoted("R", 1, 3), rotate = face(swing(1, 3))) triangle(J[1], J[3], J[5], "R"); part("D anchor link", at = link_at("D", 1), rotate = face(heading(1, 4))) rod([0, 0], [39.3 * s, 0], "D"); part("E coupler", at = link_at("E", 5), rotate = face(heading(5, 6))) rod([0, 0], [39.4 * s, 0], "E"); part("F foot", at = pivoted("F", 4, 6), rotate = face(swing(4, 6))) triangle(J[4], J[6], J[7], "F"); } if (output == "all" || output == "frame") step("Glue the three backboards together and stand them in the feet") part("Backboard", qty = 3, at = [for (k = [1:3]) [0, k * t, 0]], rotate = face(0)) frame(); // Two plies each, standing across the board under its ends if (output == "all" || output == "feet") step("Glue the three backboards together and stand them in the feet") part("Stand foot", qty = 4, at = [for (x = [25, 155], k = [0:1]) [x * s - t + k * t, 1.5 * t - 65, 0]], rotate = [90, 0, 90]) stand_foot(); if (output == "all" || output == "accessories") { // As many rings as fit each gap; make up the rest with metal shims step("Fit the links on brass bushes, back to front, with the spacer rings between them") for (sp = spacers) let (n = floor(sp[1] / t)) part(str("Spacer ", sp[0]), qty = n, at = [for (k = [0:n - 1]) front(sp[2], depth(sp[3]) + k * t)], rotate = face(0)) disc(12, 6.4); step("Fit the hex sockets, the rear cranks and the handle grip on the spindle") { part("Handle grip", qty = 6, at = [for (k = [1:6]) on_axle(behind - 3 * t - k * t) + [40 * cos(turn), 0, 40 * sin(turn)]], rotate = face(turn)) disc(18, hole); part("Rear crank", qty = 2, at = [on_axle(behind - t), on_axle(behind - 2 * t)], rotate = face(turn)) difference() { capsule([0, 0], [40, 0], 10); circle(d = 4.4); translate([40, 0]) circle(d = 4.4); } // One in front of the crank C, one behind the board under the rear cranks part("Hex socket", qty = 2, at = [on_axle(depth("C")), on_axle(behind - 3 * t)], rotate = [face(crank_angle), face(turn)]) difference() { circle(d = 18); circle(d = 8.31, $fn = 6); } } } // The fit test lies on the table beside the board if (output == "all" || output == "coupon") part("Fit test", at = [FW + 20, -60, 0]) { difference() { rect([130, 50], r = 3); for (i = [0:3]) translate([18 + 28 * i, 18]) circle(d = 6.1 + 0.1 * i); translate([20, 35]) circle(d = 4.4); translate([45, 32]) rect([3 * t, 12]); translate([80, 32]) rect([2 * t, 12]); } for (i = [0:3]) score() translate([18 + 28 * i, 5]) text(str(6.1 + 0.1 * i), size = 3, halign = "center"); } echo(str("Board ", FW, " x ", FH, " mm; crank radius ", crank_radius * s, " mm")); // Assembly: glue the three backboards together and stand them in the two // feet (two plies each). The links go in front of the board on 6 mm brass // bushes, back to front in this order: C, A, R, D, B, E, F, with a rear face // every t + 5 mm from 10 mm out. Thread the spacer rings onto the bushes // between the layers they are named for. The M4 spindle at O turns the front // crank C and, behind the board, the rear crank with its handle grip. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tidal/ # Tidal wave automaton Tidal: a hand-cranked wave automaton. Round cams sit off-centre on a square shaft, each turned a little further than the last, so as you turn the crank they lift seven fins one after another and the fins ripple like a wave. This design has 120 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tidal). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tidal/tidal.kerf) - [tidal-sheet-1.svg](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-1.svg) - [tidal-sheet-1.dxf](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-1.dxf) - [tidal-sheet-2.svg](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-2.svg) - [tidal-sheet-2.dxf](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-2.dxf) - [tidal-sheet-3.svg](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-3.svg) - [tidal-sheet-3.dxf](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-3.dxf) - [tidal-sheet-4.svg](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-4.svg) - [tidal-sheet-4.dxf](https://kerfscript.mylasertools.com/models/tidal/tidal-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 237.8 × 110 mm | | End wall | 6 | 110 × 145 mm | | Guide half | 4 | 232.8 × 45 mm | | Journal | 6 | 18 × 18 mm | | Thrust washer | 4 | 26 × 26 mm | | Cam 1 | 3 | 44 × 44 mm | | Cam 2 | 3 | 44 × 44 mm | | Cam 3 | 3 | 44 × 44 mm | | Cam 4 | 3 | 44 × 44 mm | | Cam 5 | 3 | 44 × 44 mm | | Cam 6 | 3 | 44 × 44 mm | | Cam 7 | 3 | 44 × 44 mm | | Fin 1 | 2 | 73.9 × 165.7 mm | | Fin 2 | 2 | 73.9 × 165.7 mm | | Fin 3 | 2 | 73.9 × 165.7 mm | | Fin 4 | 2 | 73.9 × 165.7 mm | | Fin 5 | 2 | 73.9 × 165.7 mm | | Fin 6 | 2 | 73.9 × 165.7 mm | | Fin 7 | 2 | 73.9 × 165.7 mm | | Spacer ring | 52 | 14 × 14 mm | | Crank | 3 | 32 × 56 mm | | Handle | 6 | 18 × 18 mm | | Fit test | 1 | 110 × 48 mm | | Timing card | 1 | 336 × 56 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `fins` | `7` | 5 to 9 | | `lift` | `8` | 5 to 10 | | `phase` | `45` | 15 to 180 | | `pitch_layers` | `9` | 8 to 11 | | `clearance` | `0.4` | 0.2 to 0.6 | | `shaft_fit` | `0.15` | 0.05 to 0.3 | | `crank_angle` | `0` | 0 to 360 | | `output` | `"all"` | All parts, Frame, Cams, Fins, Spacer rings, Crank, Fit test, Timing card | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Tidal: a hand-cranked wave automaton. Round cams sit off-centre on a // square shaft, each turned a little further than the last, so as you turn // the crank they lift seven fins one after another and the fins ripple // like a wave. /* [Motion] */ // Number of fins fins = 7; // [5:1:9] // How far each cam sits off the shaft; the fins rise and fall twice this lift = 8; // [5:1:10] // How much further each cam is turned than the one before, in degrees phase = 45; // [15:5:180] /* [Structure] */ // Distance between fins, in sheet thicknesses pitch_layers = 9; // [8:1:11] // Total play for the parts that slide or turn clearance = 0.4; // [0.2:0.1:0.6] // Added to the square holes for the 6 mm shaft shaft_fit = 0.15; // [0.05:0.05:0.3] /* [Assembly] */ // Crank angle in the assembled view crank_angle = 0; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Frame, cams:Cams, followers:Fins, spacers:Spacer rings, crank:Crank, coupon:Fit test, timing:Timing card] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 120; t = $thickness; pitch = pitch_layers * t; camT = 3 * t; followerT = 2 * t; margin = 8 * t; inner = 2 * margin + camT + (fins - 1) * pitch + 0.8; overall = inner + 6 * t; shaft_z = 45; R = 22; guides = [90, 126]; stem = 12; foot = 38; assert(fins == floor(fins) && fins >= 5 && fins <= 9, "Fins: 5 to 9"); assert(lift >= 5 && lift <= 10, "Lift: 5 to 10 mm"); assert(phase >= 15 && phase <= 180, "Phase: 15 to 180 degrees"); assert(pitch_layers == floor(pitch_layers) && pitch_layers >= 8 && pitch_layers <= 11, "Pitch: 8 to 11 layers"); assert(clearance >= 0.2 && clearance <= 0.6, "Clearance: 0.2 to 0.6 mm"); assert(shaft_fit >= 0.05 && shaft_fit <= 0.3, "Shaft fit: 0.05 to 0.3 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "frame", "cams", "followers", "spacers", "crank", "coupon", "timing"], output), "Unknown output"); // Where fin i stands along the shaft function station(i) = 3 * t + margin + camT / 2 + i * pitch; module square_bore(a = 0) { rotate(a) rect([6 + shaft_fit, 6 + shaft_fit], center = true); } // Tally marks: i short scored bars module mark(i, p) { score() for (j = [0:i - 1]) translate([p[0] + 2 * j, p[1]]) rect([0.3, 4]); } module base() { difference() { rect([overall, 110], r = 4); for (x = [1.5 * t, overall - 1.5 * t], y = [20, 90]) translate([x, y]) rect([3 * t + 0.2, 14.2], center = true); } } module end_wall() { difference() { union() { translate([-55, 2 * t]) rect([110, 145 - 2 * t], r = 4); for (y = [-35, 35]) translate([y - 7, 0]) rect([14, 2 * t + 5]); } translate([0, shaft_z]) circle(d = 18 + clearance); for (z = guides, y = [-30, 30]) translate([y, z + t / 2]) rect([12.2, t + 0.2], center = true); // Windows below the guides for (y = [-35, 35]) translate([y - 9, 55]) rect([18, 24], r = 5); } } // Half a guide: two halves close around the fin stems module guide_half() { difference() { union() { rect([inner, 45]); for (x = [-2 * t, inner]) translate([x, 9]) rect([2 * t + 1, 12]); } for (i = [0:fins - 1]) translate([margin + camT / 2 + i * pitch, 45]) rect([followerT + clearance, stem + clearance], center = true); } } // Cam i: its square hole is turned back by i * phase, so on the shaft its // offset points i * phase further round than cam 1 module cam(i) { difference() { translate([lift, 0]) circle(r = R); square_bore(-i * phase); for (y = [-11, 11]) translate([lift, y]) circle(d = 3.1); } } // A fin with its straight stem and the flat foot that rides on the cam module follower() { union() { translate([-foot / 2, 0]) rect([foot, 6], r = 1); // Round the fin's outer corners, then fillet where it meets the stem offset(r = -6) offset(delta = 6) union() { translate([-stem / 2, 4]) rect([stem, 108]); offset(r = 14) offset(delta = -14) polygon([[-6, 108], [-17, 118], [-34, 133], [-40, 145], [-30, 155], [-10, 164], [0, 166], [10, 164], [30, 155], [40, 145], [34, 133], [17, 118], [6, 108]]); } } } module crank() { difference() { hull() { circle(r = 16); translate([0, 30]) circle(r = 10); } square_bore(); translate([0, 30]) circle(d = 4.5); } } // The assembled view, z up from the table, the shaft along x. Parts on the // shaft stand across it and turn with the crank; each fin rides its cam. a = crank_angle; mid = 55; crank_turn = 160 + a; function on_shaft(x) = [x, mid, shaft_z]; function shaft_turn(b) = [90, -b, 90]; across = [90, 0, 90]; // The three plies of each end wall, left and right side = [for (k = [0:5]) k < 3 ? k * t : overall - 3 * t + (k - 3) * t]; // Spacer rings: eight at each end and pitch_layers - 3 between the cams ring_x = concat([for (k = [0:7]) 3 * t + k * t], [for (i = [0:fins - 2], k = [0:pitch_layers - 4]) station(i) + camT / 2 + k * t], [for (k = [0:7]) station(fins - 1) + camT / 2 + k * t]); function fin_z(i) = shaft_z + R + lift * sin(i * phase + a); // Steps are numbered in the order they first run, so name them here in build order step("Thread the journals, cams and spacer rings onto the square shaft, set to the timing card") {} step("Stand the end walls in the base, the journals in their holes") {} step("Set each fin on its cam and close the guide halves round the stems") {} step("Add the thrust washers and the crank with its handle") {} if (output == "all" || output == "frame") { step("Stand the end walls in the base, the journals in their holes") { part("Base", qty = 2, at = [[0, 0, 0], [0, 0, t]]) base(); part("End wall", qty = 6, at = [for (x = side) [x, mid, 0]], rotate = across) end_wall(); } // The two guides, each two halves face to face step("Set each fin on its cam and close the guide halves round the stems") part("Guide half", qty = 4, at = [for (z = guides, k = [0:1]) k == 0 ? [3 * t, mid - 45, z] : [3 * t, mid + 45, z + t]], rotate = [for (z = guides, k = [0:1]) [180 * k, 0, 0]]) guide_half(); step("Thread the journals, cams and spacer rings onto the square shaft, set to the timing card") part("Journal", qty = 6, at = [for (x = side) on_shaft(x)], rotate = shaft_turn(a)) difference() { circle(d = 18); square_bore(); } step("Add the thrust washers and the crank with its handle") part("Thrust washer", qty = 4, at = [on_shaft(-t), on_shaft(-2 * t), on_shaft(overall), on_shaft(overall + t)], rotate = across) difference() { circle(d = 26); circle(d = 9.2); } } if (output == "all" || output == "cams") step("Thread the journals, cams and spacer rings onto the square shaft, set to the timing card") for (i = [0:fins - 1]) part(str("Cam ", i + 1), qty = 3, at = [for (k = [0:2]) on_shaft(station(i) - camT / 2 + k * t)], rotate = shaft_turn(i * phase + a)) { cam(i); mark(i + 1, [lift - 7, 12]); score() translate([lift, 0]) outline() circle(r = 16); } if (output == "all" || output == "followers") step("Set each fin on its cam and close the guide halves round the stems") for (i = [0:fins - 1]) part(str("Fin ", i + 1), qty = 2, at = [for (k = [0:1]) [station(i) - t + k * t, mid, fin_z(i)]], rotate = across) { follower(); mark(i + 1, [-8, 145]); score() translate([0, 150]) outline() circle(d = 12); } if (output == "all" || output == "spacers") step("Thread the journals, cams and spacer rings onto the square shaft, set to the timing card") part("Spacer ring", qty = 16 + (fins - 1) * (pitch_layers - 3), at = [for (x = ring_x) on_shaft(x)], rotate = across) difference() { circle(d = 14); circle(d = 9.2); } if (output == "all" || output == "crank") step("Add the thrust washers and the crank with its handle") { part("Crank", qty = 3, at = [for (k = [0:2]) on_shaft(-5 * t + k * t)], rotate = shaft_turn(crank_turn)) crank(); // The grips turn on the handle bolt, 30 mm out on the crank part("Handle", qty = 6, at = [for (k = [1:6]) on_shaft(-5 * t - k * t) + [0, -30 * sin(crank_turn), 30 * cos(crank_turn)]], rotate = shaft_turn(crank_turn)) difference() { circle(d = 18); circle(d = 4.5); } } // The fit test and the timing card lie on the table beside the model if (output == "all" || output == "coupon") part("Fit test", at = [overall + 20, 0, 0]) { difference() { rect([110, 48], r = 3); for (j = [0:3]) translate([15 + 25 * j, 16]) rect([6.05 + 0.1 * j, 6.05 + 0.1 * j], center = true); translate([25, 34]) circle(d = 18 + clearance); translate([64, 34]) rect([followerT + clearance, stem + clearance], center = true); } for (j = [0:3]) mark(j + 1, [12 + 25 * j, 4]); } // Which way each cam's offset points, seen from the crank side if (output == "all" || output == "timing") part("Timing card", at = [0, -70, 0]) { rect([48 * fins, 56], r = 3); for (i = [0:fins - 1]) { p = [24 + 48 * i, 28]; a = i * phase; score() translate(p) outline() circle(r = 20); score() translate(p) outline() rect([6, 6], center = true); score() hull() { translate(p) circle(d = 0.4); translate(p + [14 * cos(a), 14 * sin(a)]) circle(d = 0.4); } score() translate(p + [14 * cos(a), 14 * sin(a)]) outline() circle(d = 3); mark(i + 1, [p[0] - 8, 4]); } } echo(str("Body ", overall, " x 110 mm; square shaft at least ", overall + 75, " mm long")); // Assembly: glue the laminations (base 2, each end wall 3, each cam 3, each // fin 2, each journal 3, crank 3, handle 6). Thread the rotor onto the square // shaft: journal, 8 spacer rings, cam 1, 6 rings, cam 2 ... cam 7, 8 rings, // journal, every cam set to the timing card. Stand the end walls in the base // with the journals in their round holes, set each fin's foot on its cam and // close the two guide halves around the stems at each level. Add two thrust // washers outside each wall, the crank with its handle and a collar on one // end, a collar on the other. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ascent/ # Ascent marble lift Ascent: a hand-cranked marble lift. Turn the crank clockwise and a pocket in the laminated wheel carries one marble up the left side, over the top and down to an opening in the shroud on the right, where it drops out and rolls down a sloping chute into a covered catch box. This design has 40 parts on 10 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=ascent). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/ascent/ascent.kerf) - [ascent-sheet-1.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-1.svg) - [ascent-sheet-1.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-1.dxf) - [ascent-sheet-2.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-2.svg) - [ascent-sheet-2.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-2.dxf) - [ascent-sheet-3.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-3.svg) - [ascent-sheet-3.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-3.dxf) - [ascent-sheet-4.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-4.svg) - [ascent-sheet-4.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-4.dxf) - [ascent-sheet-5.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-5.svg) - [ascent-sheet-5.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-5.dxf) - [ascent-sheet-6.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-6.svg) - [ascent-sheet-6.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-6.dxf) - [ascent-sheet-7.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-7.svg) - [ascent-sheet-7.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-7.dxf) - [ascent-sheet-8.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-8.svg) - [ascent-sheet-8.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-8.dxf) - [ascent-sheet-9.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-9.svg) - [ascent-sheet-9.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-9.dxf) - [ascent-sheet-10.svg](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-10.svg) - [ascent-sheet-10.dxf](https://kerfscript.mylasertools.com/models/ascent/ascent-sheet-10.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Wheel | 6 | 220 × 219.2 mm | | Crank | 2 | 59 × 24 mm | | Hub ring | 4 | 28 × 28 mm | | Grip | 4 | 18 × 18 mm | | Rear cheek | 1 | 388 × 350 mm | | Front cheek | 1 | 388 × 350 mm | | Shroud | 7 | 260 × 260 mm | | Base | 1 | 450 × 110 mm | | Loading gate | 1 | 41 × 28 mm | | Gusset | 4 | 40 × 55 mm | | Chute floor | 1 | 198.9 × 21 mm | | Catch floor | 1 | 50 × 50 mm | | Catch lid | 1 | 50 × 50 mm | | Catch side wall | 2 | 50 × 22 mm | | Catch end wall | 1 | 44 × 22 mm | | Catch pedestal | 2 | 40 × 58.8 mm | | Fit test | 1 | 90 × 40 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `marble_diameter` | `16` | 14 to 18 | | `wheel_radius` | `110` | 105 to 115 | | `pockets` | `6` | 5, 6 | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | | `crank_angle` | `0` | 0 to 360 | | `output` | `"all"` | All parts, Wheel and crank, Frame, Chute and catch, Fit test | ## Source ```kerf // Ascent: a hand-cranked marble lift. Turn the crank clockwise and a pocket // in the laminated wheel carries one marble up the left side, over the top // and down to an opening in the shroud on the right, where it drops out and // rolls down a sloping chute into a covered catch box. /* [Mechanism] */ // Measured diameter of the marble; sets the pockets and the wheel thickness marble_diameter = 16; // [14:1:18] // Radius of the pocket wheel wheel_radius = 110; // [105:5:115] // Number of pockets round the wheel pockets = 6; // [5, 6] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Assembly] */ // Crank angle in the assembled view, clockwise seen from the front crank_angle = 0; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, wheel:Wheel and crank, frame:Frame, chute:Chute and catch, coupon:Fit test] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 144; t = $thickness; D = marble_diameter; R = wheel_radius; // Centre of the round end of each pocket pocket_r = R - D - 4; pocket_w = D + 2; // Wheel layers: at least the marble plus 2 mm thick. The shroud has one // more, so the wheel has half a sheet to spare on each side. layers = ceil((D + 2) / t); cheek_gap = (layers + 1) * t; outer = R + 20; // Shaft height above the base axle_height = R + 110; assert(D >= 14 && D <= 18, "Marble: 14 to 18 mm"); assert(R >= 105 && R <= 115, "Wheel radius: 105 to 115 mm"); assert(pockets == 5 || pockets == 6, "Pockets: 5 or 6"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "wheel", "frame", "chute", "coupon"], output), "Unknown output"); module hole(p, d = 4.4) { translate(p) circle(d = d, $fn = 48); } module ring(d, inner) { difference() { circle(d = d); circle(d = inner, $fn = 48); } } module capsule(a, b, r) { hull() { translate(a) circle(r = r); translate(b) circle(r = r); } } // Part of an annulus between radii r1 and r2, from angle a to angle b module sector(a, b, r1, r2) { polygon(concat( [for (i = [0:40]) let (q = lerp(a, b, i / 40)) [r2 * cos(q), r2 * sin(q)]], [for (i = [40:-1:0]) let (q = lerp(a, b, i / 40)) [r1 * cos(q), r1 * sin(q)]] )); } // Six M4 bolts through the cheeks and shroud, none in the release opening module bolt_holes() { for (a = [0, 60, 120, 180, 240, 285]) hole([(R + 13) * cos(a), (R + 13) * sin(a)]); } // Each pocket is a slot open at the rim: the marble rests in its round end // on the way up and slides out against the shroud on the way down. The // rounded slots between the pockets only take weight off. module wheel() { difference() { circle(r = R); hole([0, 0], 6.5); for (i = [0:pockets - 1]) rotate(-90 + i * 360 / pockets) union() { translate([pocket_r, 0]) circle(d = pocket_w); translate([pocket_r, -pocket_w / 2]) rect([R - pocket_r + 3, pocket_w]); } for (a = [0:360 / pockets:359]) rotate(a) capsule([30, 0], [pocket_r - 24, 0], 6); } score() translate([-16, 0]) text("CW", size = 6); } // A ring 2 mm clear of the wheel keeps the marble in its pocket, except // between -65 and -25 degrees, where it lets the marble out to the chute module shroud() { difference() { circle(r = outer); circle(r = R + 2); sector(-65, -25, R + 1, outer + 2); bolt_holes(); } } // The chute's side walls are part of the cheeks: 24 mm high above the floor // line y = -110 - 0.25 (x - 65), a 1 in 4 slope that stays clear of the wheel module chute_side() { polygon([[65, -110], [258, -158.25], [258, -134.25], [65, -86]]); } // Both cheeks, seen from the front with the shaft at the origin: the round // cover, the stand down to the base, the chute wall and a leg under it. The // front one has windows onto the wheel, slots too narrow for the marble to // pass, and the loading port under the gate. module cheek(front = false) { difference() { union() { circle(r = outer); translate([-75, -axle_height]) rect([150, axle_height - outer + 12]); chute_side(); translate([194, -axle_height]) rect([12, axle_height - 130]); } // For an 8 mm bush hole([0, 0], 8.2); bolt_holes(); if (front) { for (a = [0:60:300]) sector(a + 12, a + 48, 24, pocket_r - D / 2 - 6); // Viewing slots round the rim, none at the bottom where the loading // port and the gate's pivot are for (a = [-180, -60, 0, 60, 120]) sector(a + 8, a + 48, R - D / 2 - 4, R - D / 2 + 2); // Loading port, in line with a pocket pointing straight down hole([0, -(R + 2 - D / 2)], D + 2); hole([D / 2 + 13, -(R + 2 - D / 2)]); } } if (front) score() translate([0, -170]) text("ASCENT", size = 8, halign = "center"); if (front) score() translate([65, -56]) text("PAUSE", size = 4); } module base() { difference() { translate([90, 0]) rect([450, 110], center = true, r = 4); // Optional holes to clamp the base down; the gussets are glued for (x = [-60, 60]) for (y = [-35, 35]) hole([x, y]); for (x = [125, 185]) for (y = [-35, 35]) hole([x, y]); } score() translate([-120, 40]) text("ONE MARBLE / CLOCKWISE / MANUAL RELOAD", size = 4); } module catch_square() { rect([50, 50], center = true); } module catch_wall(w) { rect([w, 22], center = true); } module gusset() { difference() { polygon([[0, 0], [40, 0], [0, 55]]); hole([10, 12]); } } // Swings on an M4 pivot to uncover the loading port; a wingnut holds it shut module gate() { difference() { hull() { circle(d = D + 12); translate([D / 2 + 13, 0]) circle(d = 12); } hole([D / 2 + 13, 0]); } score() translate([-6, -2]) text("LOAD", size = 3); } module crank() { difference() { capsule([0, 0], [35, 0], 12); hole([0, 0], 6.5); hole([35, 0]); } } // Test holes for the 6 mm shaft and grip sleeve, and a slot for the sheet module coupon() { difference() { rect([90, 40], center = true); for (i = [0:3]) hole([-30 + i * 20, 0], 6 + i * 0.2); translate([-20, 12]) rect([40, t + 0.2], center = true); } score() translate([-35, -15]) text("6.0 / 6.2 / 6.4 / 6.6", size = 3); } // The assembled view, z up from the table, the shaft along y with the front // towards -y. Parts drawn in the front view stand up with their back face // at depth z0 (front is +z0). At crank angle 0 a pocket points 45 degrees // down to the right, into the release opening. turn = -15 - crank_angle; crank_z = -40; slope = atan2(158.25 - 110, 258 - 65); // The axle stands axle_height above the base centre = axle_height + t; floor_top = centre - 158.25; function depth(z0, x = 0, y = 0) = [x, -z0, centre + y]; function facing(b = 0) = [90, -b, 0]; across = [90, 0, 90]; // Hub rings: two behind the crank and two in front of it hub_z = [crank_z - 3 * t, crank_z - 2 * t, crank_z + t, crank_z + 2 * t]; // The assembly steps, listed first so they number in build order step("Glue the wheel layers on the shaft"); step("Bolt the rear cheek, the shroud rings and the front cheek round the wheel, and pivot the gate on the front"); step("Glue the cheeks onto the base with the gussets"); step("Glue in the chute floor and build the catch box on its pedestals"); step("Glue the hub rings and the crank on the shaft and bolt on the grip"); if (output == "all" || output == "wheel") { step("Glue the wheel layers on the shaft") part("Wheel", qty = layers, at = [for (k = [0:layers - 1]) depth(-layers * t / 2 + k * t)], rotate = facing(turn)) wheel(); step("Glue the hub rings and the crank on the shaft and bolt on the grip") { part("Crank", qty = 2, at = [depth(crank_z - t), depth(crank_z)], rotate = facing(turn)) crank(); part("Hub ring", qty = 4, at = [for (z = hub_z) depth(z)], rotate = facing(turn)) ring(28, 6.5); // The grip turns with the crank, 35 mm out part("Grip", qty = 4, at = [for (k = [1:4]) depth(crank_z - t - 0.8 - k * t) + [35 * cos(turn), 0, 35 * sin(turn)]], rotate = facing(turn)) ring(18, 6.4); } } if (output == "all" || output == "frame") { step("Bolt the rear cheek, the shroud rings and the front cheek round the wheel, and pivot the gate on the front") { part("Rear cheek", at = depth(-cheek_gap / 2 - t), rotate = facing()) cheek(false); part("Front cheek", at = depth(cheek_gap / 2), rotate = facing()) cheek(true); part("Shroud", qty = layers + 1, at = [for (k = [0:layers]) depth(-cheek_gap / 2 + k * t)], rotate = facing()) shroud(); } step("Glue the cheeks onto the base with the gussets") part("Base", at = [0, 0, 0]) base(); step("Bolt the rear cheek, the shroud rings and the front cheek round the wheel, and pivot the gate on the front") part("Loading gate", at = depth(cheek_gap / 2 + t, 0, -(R + 2 - D / 2)), rotate = facing()) gate(); // Two behind the stand and two in front, at x = -50 and 50 step("Glue the cheeks onto the base with the gussets") part("Gusset", qty = 4, at = [[-50 + t / 2, -cheek_gap / 2 - t, t], [50 + t / 2, -cheek_gap / 2 - t, t], [-50 - t / 2, cheek_gap / 2 + t, t], [50 - t / 2, cheek_gap / 2 + t, t]], rotate = [[90, 0, 270], [90, 0, 270], across, across]) gusset(); } if (output == "all" || output == "chute") step("Glue in the chute floor and build the catch box on its pedestals") { // Its top face on the floor line, from (65, -110) to (258, -158.25) part("Chute floor", at = [(65 + 258) / 2 - t * sin(slope), 0, centre - (110 + 158.25) / 2 - t * cos(slope)], rotate = [0, slope, 0]) rect([198.94, cheek_gap], center = true); part("Catch floor", at = [283, 0, floor_top - t]) catch_square(); part("Catch lid", at = [283, 0, floor_top + 22]) catch_square(); part("Catch side wall", qty = 2, at = [[283, 25, floor_top + 11], [283, 3 - 25, floor_top + 11]], rotate = facing()) catch_wall(50); part("Catch end wall", at = [308 - t, 0, floor_top + 11], rotate = across) catch_wall(50 - 2 * t); // From the base up to the underside of the catch floor part("Catch pedestal", qty = 2, at = [[283, 20, t + (floor_top - 2 * t) / 2], [283, t - 20, t + (floor_top - 2 * t) / 2]], rotate = facing()) rect([40, axle_height - 158.25 - t], center = true); } // The fit test lies on the table in front of the stand if (output == "all" || output == "coupon") part("Fit test", at = [0, -110, 0]) coupon(); echo(str("Ascent: ", layers, " wheel layers in a ", cheek_gap, " mm cheek gap, ", 2 * R, " mm wheel, ", D, " mm marble")); // Assembly, front view, shaft at the origin: glue the wheel layers on a rod // and the shroud rings on the six bolts, and sand the pockets smooth. Epoxy // a 6 mm steel shaft (80 mm) into the wheel, centred on it, and an 8 mm brass // bush into each cheek. Bolt rear cheek, shroud and front cheek with six M4 // x 40 bolts, the front nuts only finger tight, with a shaft collar outside // each cheek. Pivot the gate on a countersunk M4 bolt, its head flush inside // the front cheek. Glue the cheeks onto the base with the gussets, the chute // floor between the cheek walls, and the catch floor on its pedestals with // two side walls and the end wall on top; tape the lid on. On the shaft // behind the rear collar glue two hub rings, the two crank layers and two // more rings, then bolt the grip (four rings on a 6 mm sleeve) to the crank. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/expanse/ # Expanse sliding organizer Expanse: a split-top organizer. Turn the disc rim at the front and the two trays on top glide apart, sideways and in step, to uncover a well in the middle; turn it back and they close over it again. Two pins on the disc push the trays' followers along fixed guide rails. This design has 34 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=expanse). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/expanse/expanse.kerf) - [expanse-sheet-1.svg](https://kerfscript.mylasertools.com/models/expanse/expanse-sheet-1.svg) - [expanse-sheet-1.dxf](https://kerfscript.mylasertools.com/models/expanse/expanse-sheet-1.dxf) - [expanse-sheet-2.svg](https://kerfscript.mylasertools.com/models/expanse/expanse-sheet-2.svg) - [expanse-sheet-2.dxf](https://kerfscript.mylasertools.com/models/expanse/expanse-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 330.6 × 190 mm | | Deck | 1 | 330.6 × 190 mm | | Foot | 12 | 20 × 20 mm | | Guide rail | 2 | 290.6 × 21 mm | | Right follower | 1 | 100 × 160 mm | | Left follower | 1 | 100 × 160 mm | | Tray floor | 2 | 100 × 140 mm | | Tray side wall | 4 | 134 × 33 mm | | Tray end wall | 4 | 100 × 33 mm | | Drive disc | 1 | 176 × 176 mm | | Well side wall | 2 | 87 × 20 mm | | Well end wall | 2 | 93 × 20 mm | | Fit test | 1 | 120 × 45 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `tray_width` | `100` | 96 to 108 | | `tray_depth` | `140` | 136 to 150 | | `tray_height` | `30` | 25 to 35 | | `crank_radius` | `70` | 66 to 72 | | `open_angle` | `15` | 12 to 20 | | `closed_angle` | `74` | 72 to 74 | | `slide_clearance` | `0.4` | 0.3 to 0.5 | | `disc_angle` | `25` | 12 to 74 | | `output` | `"all"` | All parts, Base deck feet and rails, Followers and trays, Drive disc, Well walls, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Expanse: a split-top organizer. Turn the disc rim at the front and the two // trays on top glide apart, sideways and in step, to uncover a well in the // middle; turn it back and they close over it again. Two pins on the disc // push the trays' followers along fixed guide rails. /* [Box] */ // Width of each tray, in the direction it slides tray_width = 100; // [96:2:108] // Depth of each tray, front to back tray_depth = 140; // [136:2:150] // Height of the tray walls tray_height = 30; // [25:1:35] /* [Motion] */ // Distance from the disc centre to each drive pin crank_radius = 70; // [66:1:72] // Disc angle with the trays fully open open_angle = 15; // [12:1:20] // Disc angle with the trays closed closed_angle = 74; // [72:1:74] /* [Fit] */ // Added to the slots the 6 mm sleeves run in, and the play between the followers and the rails slide_clearance = 0.4; // [0.3:0.05:0.5] /* [Assembly] */ // Disc angle in the assembled view, from the closed angle (trays in) down // to the open angle (trays out) disc_angle = 25; // [12:1:74] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, chassis:Base deck feet and rails, trays:Followers and trays, drive:Drive disc, well:Well walls, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; W = tray_width; D = tray_depth; H = tray_height; R = crank_radius; // Each tray centre sits closed_x from the middle when closed and moves out // by R x (cos(angle) - cos(closed_angle)) as the disc turns closed_x = W / 2 + 2; travel = R * (cos(open_angle) - cos(closed_angle)); // Where the drive slot sits across each follower; the same at every angle slot_x = R * cos(closed_angle) - closed_x; // The four fixed guide bolts guide_x = 86; guide_y = D / 2 - 18; // The tray posts post_y = D / 2 - 8; base_w = 2 * (closed_x + travel + W / 2 + 15); base_d = D + 50; well_w = W - 10; well_d = D - 50; rail_len = base_w - 40; // The rails' inner faces are the follower depth plus slide_clearance apart rail_y = D / 2 + 10 + slide_clearance / 2 + t / 2; leg_x = rail_len / 2 - 10; arc_steps = ceil((closed_angle - open_angle) / 2); assert(W >= 96 && W <= 108, "Tray width: 96 to 108 mm"); assert(D >= 136 && D <= 150, "Tray depth: 136 to 150 mm"); assert(H >= 25 && H <= 35, "Tray height: 25 to 35 mm"); assert(R >= 66 && R <= 72, "Crank radius: 66 to 72 mm"); // Leaves wood between each drive slot and the tray post holes beside it assert(R * cos(closed_angle) >= 19, "Crank radius x cos(closed angle): at least 19 mm; use a larger radius or a smaller closed angle"); assert(open_angle >= 12 && open_angle <= 20, "Open angle: 12 to 20 degrees"); assert(closed_angle >= 72 && closed_angle <= 74, "Closed angle: 72 to 74 degrees"); assert(slide_clearance >= 0.3 && slide_clearance <= 0.5, "Slide clearance: 0.3 to 0.5 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(disc_angle >= 12 && disc_angle <= 74, "Disc angle: 12 to 74 degrees"); assert(contains(["all", "chassis", "trays", "drive", "well", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; module capsule(a, b, r) { hull() { translate(a) circle(r = r); translate(b) circle(r = r); } } // M4 holes by default module hole(p, d = 4.4) { translate(p) circle(d = d, $fn = 48); } module ring(d, inner) { difference() { circle(d = d); circle(d = inner, $fn = 48); } } // Holes for the centre axle, the four deck standoffs, the four guide bolts // and the two stop pins, and slots for the rail legs module base() { difference() { rect([base_w, base_d], center = true, r = 5); hole([0, 0]); for (x = [-base_w / 2 + 8, base_w / 2 - 8]) for (y = [-base_d / 2 + 8, base_d / 2 - 8]) hole([x, y]); for (s = [-1, 1]) for (y = [-guide_y, guide_y]) hole([s * guide_x, y]); for (a = [100, 280]) hole([35 * cos(a), 35 * sin(a)]); for (x = [-leg_x, leg_x]) for (y = [-rail_y, rail_y]) translate([x, y]) rect([12.2, t + 0.2], center = true); } score() translate([0, -base_d / 2 + 10]) text("EXPANSE", size = 8, halign = "center"); } // The rails stop the followers from turning. The bar stands 9 to 18 mm above // the base, level with the followers' edges and clear of the disc below; the // legs stand outside the disc and glue into the base. module rail() { union() { translate([-rail_len / 2, 9]) rect([rail_len, 9]); for (x = [-leg_x, leg_x]) translate([x - 6, -t]) rect([12, 9 + t + 0.2]); } } module deck() { difference() { rect([base_w, base_d], center = true, r = 5); for (x = [-base_w / 2 + 8, base_w / 2 - 8]) for (y = [-base_d / 2 + 8, base_d / 2 - 8]) hole([x, y]); // The front notch shows the disc rim translate([-50, -base_d / 2 - 1]) rect([100, 31], r = 3); // Long slots for the tray posts, the length of the travel for (s = [-1, 1]) for (px = [-W / 2 + 8, W / 2 - 8]) for (y = [-post_y, post_y]) capsule([s * (closed_x - 1) + px, y], [s * (closed_x + travel + 1) + px, y], 5); // Slots for the tabs of the four well walls for (x = [-well_w / 2, well_w / 2]) for (y = [-well_d / 4, well_d / 4]) translate([x, y]) rect([t + 0.2, 10.2], center = true); for (y = [-well_d / 2, well_d / 2]) for (x = [-well_w / 4, well_w / 4]) translate([x, y]) rect([10.2, t + 0.2], center = true); } score() text("CENTRAL WELL", size = 4, halign = "center"); } // s = 1 for the right follower, -1 for the left, which is the same part // turned half round. A drive pin at angle a is at R cos(a) across and // R sin(a) along: it slides along the drive slot and pushes the follower // across. The guide bolts run in the two slots that follow the travel. module follower(s) { difference() { rect([W, D + 20], center = true, r = 3); capsule([s * slot_x, s * (R * sin(open_angle) - 1)], [s * slot_x, s * (R * sin(closed_angle) + 1)], (6 + slide_clearance) / 2); for (y = [-guide_y, guide_y]) capsule([s * (guide_x - closed_x + 1), y], [s * (guide_x - closed_x - travel - 1), y], (6 + slide_clearance) / 2); for (x = [-W / 2 + 8, W / 2 - 8]) for (y = [-post_y, post_y]) hole([x, y]); } score() text(s == 1 ? "RIGHT" : "LEFT", size = 5, halign = "center"); } // Post holes, and slots for the end walls (across the ends) and the side // walls (along the sides) module tray_floor() { difference() { rect([W, D], center = true, r = 2); for (x = [-W / 2 + 8, W / 2 - 8]) for (y = [-post_y, post_y]) hole([x, y]); for (y = [-D / 2 + t / 2, D / 2 - t / 2]) for (x = [-W / 4, W / 4]) translate([x, y]) rect([12.2, t + 0.2], center = true); for (x = [-W / 2 + t / 2, W / 2 - t / 2]) for (y = [-D / 4, D / 4]) translate([x, y]) rect([t + 0.2, 12.2], center = true); } } // A wall of length len with two tabs, sep either side of its middle, that // go down through the floor or the deck module wall(len, sep) { union() { translate([-len / 2, 0]) rect([len, H], r = 1); for (x = [-sep, sep]) translate([x - 6, -t]) rect([12, t + 0.2]); } } module well_wall(len, sep) { union() { translate([-len / 2, 0]) rect([len, 17], r = 1); for (x = [-sep, sep]) translate([x - 5, -t]) rect([10, t + 0.2]); } } module drive_disc() { difference() { circle(r = R + 18); hole([0, 0], 6.3); for (s = [-1, 1]) hole([s * R, 0]); // The two fixed stop pins each ride in an arc slot, which ends where the // trays are fully open or closed for (a0 = [100, 280]) for (i = [0:arc_steps - 1]) let (a = lerp(a0 - closed_angle, a0 - open_angle, i / arc_steps), b = lerp(a0 - closed_angle, a0 - open_angle, (i + 1) / arc_steps)) capsule(polar(35, a), polar(35, b), 3.2); } // Grip marks round the rim for (a = [0:30:330]) score() rotate(a) line([R + 10, 0], [R + 15, 0]); } // The assembled view, base underside at z = 0: feet under the base, the // rails standing in it, the disc (t + 2), the followers (t + 10), the deck on // its corner standoffs (t + 20) with the well walls, and the trays on their // posts (2t + 40), slid out as far as the disc angle takes them turn = max(open_angle, min(closed_angle, disc_angle)); tray_x = closed_x + R * (cos(turn) - cos(closed_angle)); tray_z = 2 * t + 40; upright = [90, 0, 0]; across = [90, 0, 90]; corners = [for (sx = [-1, 1], sy = [-1, 1]) [sx * (base_w / 2 - 8), sy * (base_d / 2 - 8)]]; // Right tray first, then left; in each, the front wall or side first trays = [for (side = [1, -1], k = [1, -1]) [side, k]]; // The assembly steps in build order, named first so they number that way build_steps = [ "Glue the feet under the base and the guide rails into its slots", "Bolt the drive disc on the centre axle, scored face up", "Lower the followers between the rails and nut them on", "Bolt the deck on its corner standoffs and glue the well walls in", "Screw the tray floors on the posts and glue their walls on" ]; if (output == "all") for (k = build_steps) step(k) {} if (output == "all" || output == "chassis") { step(build_steps[0]) part("Base", at = [0, 0, 0]) base(); step(build_steps[3]) part("Deck", at = [0, 0, t + 20]) deck(); step(build_steps[0]) part("Foot", qty = 12, at = [for (c = corners, k = [0:2]) concat(c, [-3 * t + k * t])]) ring(20, 10); step(build_steps[0]) part("Guide rail", qty = 2, at = [[0, rail_y + t / 2, t], [0, -rail_y + t / 2, t]], rotate = upright) rail(); } if (output == "all" || output == "trays") { step(build_steps[2]) part("Right follower", at = [tray_x, 0, t + 10]) follower(1); step(build_steps[2]) part("Left follower", at = [-tray_x, 0, t + 10]) follower(-1); step(build_steps[4]) part("Tray floor", qty = 2, at = [[tray_x, 0, tray_z], [-tray_x, 0, tray_z]]) tray_floor(); step(build_steps[4]) part("Tray side wall", qty = 4, at = [for (q = trays) [q[0] * tray_x + q[1] * (W / 2 - t / 2) - t / 2, 0, tray_z + t]], rotate = across) wall(D - 2 * t, D / 4); step(build_steps[4]) part("Tray end wall", qty = 4, at = [for (q = trays) [q[0] * tray_x, q[1] * (D / 2 - t / 2) + t / 2, tray_z + t]], rotate = upright) wall(W, W / 4); } if (output == "all" || output == "drive") step(build_steps[1]) part("Drive disc", at = [0, 0, t + 2], rotate = [0, 0, turn]) drive_disc(); if (output == "all" || output == "well") { step(build_steps[3]) part("Well side wall", qty = 2, at = [[well_w / 2 - t / 2, 0, 2 * t + 20], [-well_w / 2 - t / 2, 0, 2 * t + 20]], rotate = across) well_wall(well_d - t, well_d / 4); step(build_steps[3]) part("Well end wall", qty = 2, at = [[0, well_w / 2 + t / 2, 2 * t + 20], [0, -well_w / 2 + t / 2, 2 * t + 20]], rotate = upright) well_wall(well_w + t, well_w / 4); } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [base_w / 2 + 20, -base_d / 2, -3 * t]) { difference() { rect([120, 45], r = 3); for (i = [0:3]) translate([18 + 27 * i, 14]) circle(d = 6.2 + 0.1 * i, $fn = 48); translate([18, 31]) rect([12.2, t + 0.2], center = true); hole([60, 31]); } for (i = [0:3]) score() translate([18 + 27 * i, 4]) text(str(6.2 + 0.1 * i), size = 3, halign = "center"); } echo(str("Expanse: two ", W, " x ", D, " x ", H, " mm trays, ", round(travel * 10) / 10, " mm travel each, base ", round(base_w * 10) / 10, " x ", base_d, " mm")); // Assembly, with base bottom at z = 0, t the sheet thickness and M4 hardware // throughout: glue three feet under each corner of the base and the two rails // into its slots, with a follower as the spacing gauge. Fit the countersunk // drive bolts in the disc, the centre axle and two stop bolts up through the // base with 5.2 mm sleeves and a 2 mm thrust washer at the centre, then the // disc (t + 2 to 2t + 2), scored face up with the stop pins in its arcs, nuts // against the sleeves. Add 8.2 mm sleeves on the drive bolts and stand the // four guide bolts in 10 mm spacers with 13.2 mm sleeves. Screw four 30 mm // standoffs to each follower, lower RIGHT and LEFT (t + 10) over the sleeves // between the rails and nut them. Bolt the deck (t + 20) on four 20 mm corner // standoffs, the posts through its slots, and screw the tray floors on the // posts (2t + 40). Glue the end walls across each floor's ends, the side walls // between them, and the four well walls into the deck. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/celestial/ # Celestial geared orrery Celestial: a hand-cranked geared orrery. The crank turns three drive gears pinned together on one axle; each meshes with an orbit gear on a fixed journal beside it, so the three planets circle a fixed sun at half, once and twice the crank speed, the other way. Stylized speeds, not astronomical. This design has 49 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=celestial). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/celestial/celestial.kerf) - [celestial-sheet-1.svg](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-1.svg) - [celestial-sheet-1.dxf](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-1.dxf) - [celestial-sheet-2.svg](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-2.svg) - [celestial-sheet-2.dxf](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-2.dxf) - [celestial-sheet-3.svg](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-3.svg) - [celestial-sheet-3.dxf](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-3.dxf) - [celestial-sheet-4.svg](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-4.svg) - [celestial-sheet-4.dxf](https://kerfscript.mylasertools.com/models/celestial/celestial-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Lower orbit gear | 2 | 186 × 186 mm | | Lower drive gear | 2 | 95.7 × 96 mm | | Middle orbit gear | 2 | 140.9 × 141 mm | | Middle drive gear | 2 | 140.9 × 141 mm | | Upper orbit gear | 2 | 96 × 95.7 mm | | Upper drive gear | 2 | 185.8 × 185.8 mm | | Base | 2 | 357 × 222 mm | | Foot | 12 | 22 × 22 mm | | Journal pedestal | 4 | 16 × 16 mm | | Planet | 9 | 12 × 12 mm | | Sun | 2 | 28 × 28 mm | | Crank | 2 | 40 × 70 mm | | Handle | 5 | 18 × 18 mm | | Fit test | 1 | 110 × 48 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `gear_module` | `3` | 2.8 to 3.2 | | `backlash` | `0.55` | 0.5 to 0.7 | | `marker_size` | `12` | 10 to 14 | | `bearing_clearance` | `0.15` | 0.1 to 0.2 | | `crank_angle` | `432` | 0 to 720 | | `output` | `"all"` | All parts, Gears, Base pedestals and feet, Planets and sun, Crank and handle, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Celestial: a hand-cranked geared orrery. The crank turns three drive gears // pinned together on one axle; each meshes with an orbit gear on a fixed // journal beside it, so the three planets circle a fixed sun at half, once // and twice the crank speed, the other way. Stylized speeds, not astronomical. /* [Mechanism] */ // Tooth size: pitch diameter = teeth x module. Also sets the axle spacing and the orbits gear_module = 3; // [2.8:0.1:3.2] // Total play between two meshing gears, at the pitch circle backlash = 0.55; // [0.5:0.05:0.7] // Diameter of the planets marker_size = 12; // [10:1:14] /* [Fit] */ // Added to the 8 mm holes the gears and crank turn on bearing_clearance = 0.15; // [0.1:0.05:0.2] /* [Assembly] */ // Crank angle in the assembled view; the outer orbit takes two turns to go // round once crank_angle = 432; // [0:6:720] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, gears:Gears, frame:Base pedestals and feet, markers:Planets and sun, crank:Crank and handle, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 128; t = $thickness; m = gear_module; // Distance between the two axles: every pair has 90 teeth between them spacing = 45 * m; // Height between the gear levels, and the underside of the lowest pair pitch = 20; z0 = 15; assert(m >= 2.8 && m <= 3.2, "Module: 2.8 to 3.2 mm"); assert(backlash >= 0.5 && backlash <= 0.7, "Backlash: 0.5 to 0.7 mm"); assert(marker_size >= 10 && marker_size <= 14, "Marker size: 10 to 14 mm"); assert(bearing_clearance >= 0.1 && bearing_clearance <= 0.2, "Bearing clearance: 0.1 to 0.2 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 720, "Crank angle: 0 to 720 degrees"); assert(contains(["all", "gears", "frame", "markers", "crank", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; // n scored tally marks, to number the holes on the fit test module tally(n, p) { score() for (j = [0:n - 1]) translate(p + [2 * j, 0]) rect([0.3, 4]); } // Involute teeth with a 20 degree pressure angle. backlash is shared between // the two gears of a mesh: every tooth is thinned by backlash / 2. The roots // get a plain radial relief. function gear_inv(a) = tan(a) * 180 / PI - a; function gear_polar(r, a) = [r * cos(a), r * sin(a)]; // Half the angular width of a tooth (or of a ring space) at radius r function gear_half(z, m, r, backlash, internal = false) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) 90 / z + gear_inv(20) - gear_inv(ar) + (internal ? 1 : -1) * backlash / (4 * rp) * 180 / PI; function gear_outline(z, m, backlash, internal = false, steps = 24) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - (internal ? 1 : 1.25) * m, hi = rp + (internal ? 1.25 : 1) * m, start = max(lo, rb), hs = gear_half(z, m, start, backlash, internal), ht = gear_half(z, m, hi, backlash, internal)) [for (k = [0:z - 1]) each concat( [gear_polar(lo, 360 * k / z - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z - gear_half(z, m, r, backlash, internal))], [for (j = [1:8]) gear_polar(hi, 360 * k / z - ht + 2 * ht * j / 8)], [for (j = [steps - 1:-1:0]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z + gear_half(z, m, r, backlash, internal))], [if (start > lo) gear_polar(lo, 360 * k / z + hs)], [for (j = [1:7]) gear_polar(lo, 360 * k / z + hs + (360 / z - 2 * hs) * j / 8)] )]; module externalgear(z, m, backlash) { polygon(gear_outline(z, m, backlash, false)); } // The orbit gears turn freely, each on its own, around the fixed journal at // x = 0. The drive gears and the crank sit on the other journal and are tied // together by two M3 rods, so turning the crank by theta turns orbit gear z // by -theta (90 - z) / z: half, once and twice as far. // Timing, with the crank at 0: each orbit gear has a tooth centred on +X, its // score line pointing at the drive axle, and the drive gears are turned so a // space faces back at it. module orbit_gear(z) { difference() { externalgear(z, m, backlash); circle(d = 8 + bearing_clearance); } // The planet's path, and the line to glue it on score() outline() circle(r = z * m / 2 - 12); score() line([8, 0], [z * m / 2 - 5, 0]); } module drive_gear(z) { difference() { rotate(180 - 180 / z) externalgear(z, m, backlash); circle(d = 8 + bearing_clearance); for (x = [-12, 12]) translate([x, 0]) circle(d = 3.4); } score() line([0, 9], [0, z * m / 2 - 5]); } module base() { difference() { translate([-32 * m - 15, -32 * m - 15]) rect([spacing + 64 * m + 30, 64 * m + 30], r = 12); // The M6 rods that hold the two journals for (x = [0, spacing]) translate([x, 0]) circle(d = 6.5); // Hold-down bolts above the feet for (x = [-27 * m, spacing + 27 * m]) for (y = [-27 * m, 27 * m]) translate([x, y]) circle(d = 4.5); } score() translate([spacing / 2, -26 * m]) text("CELESTIAL", size = 9, halign = "center"); score() translate([spacing / 2, -29 * m]) text("STYLIZED ORBITS / 1 : 2 : 4", size = 3.5, halign = "center"); } module foot() { difference() { circle(d = 22); circle(d = 12); } } module pedestal() { difference() { circle(d = 16); circle(d = 8.1); } } module marker() { circle(d = marker_size); score() outline() circle(d = marker_size - 4); } module sun() { difference() { circle(d = 28); circle(d = 6.5); } for (a = [0:30:330]) score() line(polar(9, a), polar(12, a)); } module crank() { difference() { hull() { circle(r = 20); translate([0, 40]) circle(r = 10); } circle(d = 8 + bearing_clearance); for (x = [-12, 12]) translate([x, 0]) circle(d = 3.4); translate([0, 40]) circle(d = 4.5); } } module handle() { difference() { circle(d = 18); circle(d = 6.3); } } // The assembled view, base underside at z = 0: the orbit gears on the left // journal and the drive gears on the right, at z0, z0 + pitch and z0 + 2 // pitch, the crank above them and the sun on its rod. Each orbit gear turns // the other way from the crank, by the teeth of its pair. a = crank_angle; teeth = [60, 45, 30]; function level_z(i) = z0 + i * pitch; function orbit_turn(i) = -a * (90 - teeth[i]) / teeth[i]; function layers(x, z, n) = [for (k = [0:n - 1]) [x, 0, z + k * t]]; corners = [for (x = [-27 * m, spacing + 27 * m], y = [-27 * m, 27 * m]) [x, y]]; // Bottom to top: the outer orbit has the largest gear and the smallest drive levels = ["Lower", "Middle", "Upper"]; // The assembly steps, listed first so they number in build order step("Glue the feet under the base and a pedestal round the foot of each journal"); step("Stack the orbit gears on the left journal and the drive gears on the right"); step("Pin the crank on top of the drive gears"); step("Glue the planets on the orbit gears, fix the sun on its rod and bolt on the handle"); if (output == "all" || output == "gears") step("Stack the orbit gears on the left journal and the drive gears on the right") for (i = [0:2]) { z = teeth[i]; part(str(levels[i], " orbit gear"), qty = 2, at = layers(0, level_z(i), 2), rotate = [0, 0, orbit_turn(i)]) orbit_gear(z); part(str(levels[i], " drive gear"), qty = 2, at = layers(spacing, level_z(i), 2), rotate = [0, 0, a]) drive_gear(90 - z); } if (output == "all" || output == "frame") step("Glue the feet under the base and a pedestal round the foot of each journal") { part("Base", qty = 2, at = layers(0, 0, 2)) base(); part("Foot", qty = 12, at = [for (c = corners, k = [0:2]) concat(c, [-3 * t + k * t])]) foot(); part("Journal pedestal", qty = 4, at = [for (x = [0, spacing], k = [0:1]) [x, 0, 2 * t + k * t]]) pedestal(); } if (output == "all" || output == "markers") step("Glue the planets on the orbit gears, fix the sun on its rod and bolt on the handle") { // Three layers on each orbit gear, 12 mm in from its pitch circle part("Planet", qty = 9, at = [for (i = [0:2], k = [0:2]) concat(polar(teeth[i] * m / 2 - 12, orbit_turn(i)), [level_z(i) + 2 * t + k * t])], rotate = [for (i = [0:2], k = [0:2]) [0, 0, orbit_turn(i)]]) marker(); part("Sun", qty = 2, at = layers(0, 85, 2)) sun(); } if (output == "all" || output == "crank") { step("Pin the crank on top of the drive gears") part("Crank", qty = 2, at = layers(spacing, 75, 2), rotate = [0, 0, a]) crank(); step("Glue the planets on the orbit gears, fix the sun on its rod and bolt on the handle") part("Handle", qty = 5, at = [for (k = [0:4]) concat([spacing, 0] + polar(40, 90 + a), [82 + k * t])], rotate = [0, 0, a]) handle(); } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [spacing + 27 * m + 30, -27 * m, -3 * t]) { difference() { rect([110, 48], r = 3); for (i = [0:3]) translate([15 + 25 * i, 15]) circle(d = 8.1 + 0.1 * i); translate([25, 35]) circle(d = 6.3); translate([55, 35]) circle(d = 3.4); translate([85, 35]) circle(d = 4.5); } for (i = [0:3]) tally(i + 1, [12 + 25 * i, 3]); } echo(str("Celestial: axles ", spacing, " mm apart, gear levels at ", z0, ", ", z0 + pitch, " and ", z0 + 2 * pitch, " mm; the orbits turn 1/2, 1 and 2 times per crank turn")); // Assembly, with base bottom at z = 0 (assumes 3 mm stock and 1 mm washers): // glue the laminates first (base, each gear, each pedestal and the crank of // 2, the sun of 2, four feet and three planets of 3, a handle of 5), the // gears on an 8 mm mandrel. Glue the feet under the corner holes. Bolt an // M6 rod up through each axle hole and clamp an 8 mm steel journal tube on // it, a pedestal glued round its foot, shimmed so the lowest gears sit on a // washer at z = 15. On the left journal the orbit gears go 60, 45, 30 at // z = 15, 35 and 55, with washers and 12 mm sleeves between them, each free // to turn alone. On the right the drive gears go 30, 45, 60 at the same // heights, then the crank at 75, threaded on two M3 rods with 14 mm spacers // between them. With the crank at 0, point every orbit gear's score line at // the drive axle and every drive gear's at +Y. Leave 0.25 mm of float under // each retaining washer. Glue the planets where the score lines cross the // orbits, fix the sun on its rod at z = 85 and bolt the handle on its sleeve. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/flight/ # Flight moth automaton Flight: a moth automaton on a standing board. Turn the crank behind the board and a pin on the front disc pushes a slider up and down between two guide posts; two links from the slider rock the wings on their fixed roots, so both wings beat together. This design has 88 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=flight). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/flight/flight.kerf) - [flight-sheet-1.svg](https://kerfscript.mylasertools.com/models/flight/flight-sheet-1.svg) - [flight-sheet-1.dxf](https://kerfscript.mylasertools.com/models/flight/flight-sheet-1.dxf) - [flight-sheet-2.svg](https://kerfscript.mylasertools.com/models/flight/flight-sheet-2.svg) - [flight-sheet-2.dxf](https://kerfscript.mylasertools.com/models/flight/flight-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Backboard | 3 | 130 × 230 mm | | Stand foot | 4 | 130 × 42 mm | | Right wing | 1 | 182 × 94 mm | | Left wing | 1 | 182 × 94 mm | | Crank disc | 1 | 46 × 46 mm | | Rear crank | 3 | 20 × 49 mm | | Drive rod | 1 | 14 × 64 mm | | Wing link | 2 | 14 × 64 mm | | Slider | 1 | 16 × 90 mm | | Handle grip | 5 | 16 × 16 mm | | Root standoff | 32 | 16 × 16 mm | | Guide standoff | 14 | 14 × 14 mm | | Spare standoff | 1 | 14 × 14 mm | | Spacer ring | 18 | 12 × 12 mm | | Fit test | 1 | 110 × 44 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `crank_radius` | `12` | 10 to 12 | | `wing_span` | `150` | 140 to 165 | | `slide_clearance` | `0.4` | 0.3 to 0.6 | | `crank_angle` | `0` | 0 to 360 | | `output` | `"all"` | All parts, Backboard and feet, Wings, Crank and links, Standoffs and spacer rings, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Flight: a moth automaton on a standing board. Turn the crank behind the // board and a pin on the front disc pushes a slider up and down between two // guide posts; two links from the slider rock the wings on their fixed // roots, so both wings beat together. /* [Motion] */ // Crank pin radius in mm; a bigger radius gives a bigger wingbeat crank_radius = 12; // [10:1:12] // Length of each wing beyond its root, in mm; the linkage stays the same wing_span = 150; // [140:5:165] /* [Fit] */ // Total play between the slider's slot and its two guide sleeves slide_clearance = 0.4; // [0.3:0.1:0.6] /* [Assembly] */ // Crank angle in the assembled view crank_angle = 0; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Backboard and feet, wings:Wings, linkage:Crank and links, spacers:Standoffs and spacer rings, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 120; t = $thickness; w = wing_span; // Front view, x = 0 down the middle of the body, y = 0 at the foot bottoms axle = [0, 85]; guides = [[0, 165], [0, 185]]; roots = [[-35, 180], [35, 180]]; // Rods and wing links are 50 mm between holes; each link pin sits 25 mm // inboard of its wing root link = 50; arm = 25; assert(crank_radius >= 10 && crank_radius <= 12, "Crank radius: 10 to 12 mm"); assert(w >= 140 && w <= 165, "Wing span: 140 to 165 mm"); assert(slide_clearance >= 0.3 && slide_clearance <= 0.6, "Slide clearance: 0.3 to 0.6 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "frame", "wings", "linkage", "spacers", "coupon"], output), "Unknown output"); // Right wing angle, 0 = level, with the slider pin at height y function wing_angle(y) = let (d = norm([roots[1][0], roots[1][1] - y])) atan2(roots[1][1] - y, roots[1][0]) - acos((arm * arm + d * d - link * link) / (2 * arm * d)); // The slider pin is highest and lowest with the crank pin straight up or down low = wing_angle(axle[1] + crank_radius + link); high = wing_angle(axle[1] - crank_radius + link); // Holes default to the 6.3 mm moving fit on a 6 mm brass sleeve module bore(p, d = 6.3) { translate(p) circle(d = d); } module rod(L) { difference() { hull() { circle(r = 7); translate([0, L]) circle(r = 7); } bore([0, 0]); bore([0, L]); } } module frame() { difference() { union() { translate([-65, 0]) rect([130, 205], r = 14); // The moth's head translate([0, 203]) circle(r = 27); } // M4 bolts through the fixed guide posts and wing roots for (p = concat(guides, roots)) bore(p, 4.4); // The brass journal the crank spindle turns in bore(axle, 6.3); // Open slots for the two stand feet for (x = [-45, 45]) translate([x - t - 0.125, -1]) rect([2 * t + 0.25, 25.25]); for (x = [-47, 47]) translate([x, 70]) rect([18, 65], r = 8, center = true); } } module foot() { difference() { polygon([[-65, 0], [65, 0], [65, 15], [30, 42], [-5, 30], [-65, 15]]); translate([-1.5 * t - 0.15, 24]) rect([3 * t + 0.3, 60]); } } // The right wing, root pivot at the origin and link pin 25 mm inboard module wing() { difference() { union() { polygon([[0, 0], [0.13 * w, 35], [0.38 * w, 62], [0.70 * w, 69], [w, 47], [0.97 * w, 31], [0.86 * w, 28], [0.92 * w, 15], [0.80 * w, 12], [0.84 * w, -1], [0.70 * w, -5], [0.69 * w, -18], [0.52 * w, -25], [0.32 * w, -20], [0.12 * w, -10]]); hull() { circle(r = 10); translate([-arm, 0]) circle(r = 7); } } bore([0, 0]); bore([-arm, 0]); // Four windows in the wing for (i = [0:3]) hull() { translate([w * (0.25 + i * 0.12), 18 + i * 5]) circle(r = 3); translate([w * (0.32 + i * 0.13), 32 + i * 4]) circle(r = 3); } } // Veins fanning out from the root score() for (i = [0:4]) hull() { translate([12, 3]) circle(d = 0.35); translate([w * (0.38 + i * 0.12), -8 + i * 12]) circle(d = 0.35); } } // Pivot S at the origin; the slot rides on both guide sleeves module slider() { difference() { translate([-8, -10]) rect([16, 90], r = 5); bore([0, 0]); hull() { bore([0, 12], 6 + slide_clearance); bore([0, 68], 6 + slide_clearance); } } } module crank() { difference() { circle(r = 23); bore([0, 0], 4.4); bore([0, crank_radius]); } } module rear_crank() { difference() { hull() { circle(r = 10); translate([0, 30]) circle(r = 9); } bore([0, 0], 4.4); bore([0, 30], 4.4); } } module disc(d = 16, hole = 4.4) { difference() { circle(d = d); circle(d = hole); } } // The assembled view, z up from the table, the board's front face at y = 0 // and the moving layers in front of it at depth d (towards -y). The crank // starts at 240 degrees, where the wings are near the top of their beat; the // slider rides straight up and down between the guides. function polar(r, a) = [r * cos(a), r * sin(a)]; function meet(p, r1, q, r2, sgn) = let (v = q - p, d = norm(v), a = (r1 * r1 - r2 * r2 + d * d) / (2 * d), h = sqrt(max(0, r1 * r1 - a * a))) p + v * a / d + [-v[1], v[0]] * h / d * sgn; crank = 240 + crank_angle; pin = axle + polar(crank_radius, crank); slide = [0, pin.y + sqrt(link * link - pin.x * pin.x)]; right_pin = meet(slide, link, roots[1], arm, 1); left_pin = [-right_pin.x, right_pin.y]; beat = atan2(roots[1].y - right_pin.y, roots[1].x - right_pin.x); depth = [t + 1, 5 * t + 1, 7 * t + 1, 11 * t + 1, 13 * t + 1, 16 * t + 4]; function front(p, d) = [p.x, -d, p.y]; function face(b = 0) = [90, -b, 0]; // A rod drawn up its y axis, turned to run from p to q function towards(p, q) = face(atan2(q.y - p.y, q.x - p.x) - 90); // Spacer rings on the crank pin, the slider pivot and the two link pins rings = concat([for (k = [1:3]) front(pin, depth[0] + k * t)], [for (d = [6 * t + 1, 8 * t + 1, 9 * t + 1, 10 * t + 1, 12 * t + 1]) front(slide, d)], [for (k = [1:4]) front(left_pin, depth[3] + k * t)], [for (k = [1:2]) front(right_pin, depth[4] + k * t)]); // Spares lie on the table to the right function spare(k) = [200 + 18 * k, -40, 0]; // The assembly steps, listed first so they number in build order step("Glue the backboard layers and the feet, and slide the board down into the feet"); step("Bolt the root and guide standoffs through the board on brass sleeves"); step("Clamp the crank disc in front and the rear crank, with its grip, behind"); step("Put the drive rod, the slider and the wing links on their sleeves, with spacer rings in the gaps"); step("Join the links to the wings, with the wings level"); if (output == "all" || output == "frame") step("Glue the backboard layers and the feet, and slide the board down into the feet") { part("Backboard", qty = 3, at = [for (k = [1:3]) [0, k * t, 0]], rotate = face()) { frame(); score() translate([-22, 110]) text("FLIGHT", size = 9); } part("Stand foot", qty = 4, at = [for (x = [-45, 45], k = [0:1]) [x - t + k * t, 1.5 * t, 0]], rotate = [90, 0, 90]) foot(); } if (output == "all" || output == "wings") step("Join the links to the wings, with the wings level") { part("Right wing", at = front(roots[1], depth[5]), rotate = face(beat)) wing(); part("Left wing", at = front(roots[0], depth[5]), rotate = face(-beat)) mirror([1, 0]) wing(); } if (output == "all" || output == "linkage") { step("Clamp the crank disc in front and the rear crank, with its grip, behind") { part("Crank disc", at = front(axle, depth[0]), rotate = face(crank - 90)) crank(); part("Rear crank", qty = 3, at = [for (k = [1:3]) front(axle, -3 * t - 4 - k * t)], rotate = face(crank - 90)) rear_crank(); } step("Put the drive rod, the slider and the wing links on their sleeves, with spacer rings in the gaps") { part("Drive rod", at = front(pin, depth[1]), rotate = towards(pin, slide)) rod(link); part("Wing link", qty = 2, at = [front(slide, depth[3]), front(slide, depth[4])], rotate = [towards(slide, left_pin), towards(slide, right_pin)]) rod(link); part("Slider", at = front(slide, depth[2]), rotate = face()) slider(); } step("Clamp the crank disc in front and the rear crank, with its grip, behind") { // The grip turns with the rear crank, 30 mm out part("Handle grip", qty = 5, at = [for (k = [1:5]) front(axle + polar(30, crank), -6 * t - 4 - k * t)], rotate = face(crank - 90)) disc(16, 6.3); } } if (output == "all" || output == "spacers") { step("Bolt the root and guide standoffs through the board on brass sleeves") { // 16 under each wing root and 7 under each guide post, glued part("Root standoff", qty = 32, at = [for (r = roots, k = [0:15]) front(r, k * t)], rotate = face()) disc(16, 6.3); part("Guide standoff", qty = 14, at = [for (g = guides, k = [0:6]) front(g, k * t)], rotate = face()) disc(14, 6.3); } // Not fitted: the clamp nut on the spindle turns where it would go part("Spare standoff", at = spare(0)) disc(14, 6.3); // 14 between the moving layers, and four spares step("Put the drive rod, the slider and the wing links on their sleeves, with spacer rings in the gaps") part("Spacer ring", qty = 18, at = concat(rings, [for (k = [1:4]) spare(k)]), rotate = concat([for (r = rings) face()], [for (k = [1:4]) [0, 0, 0]])) disc(12, 6.3); } // The fit test lies on the table beside the board if (output == "all" || output == "coupon") part("Fit test", at = [200, -110, 0]) { difference() { rect([110, 44], r = 3); for (i = [0:3]) bore([15 + 25 * i, 15], 6.1 + 0.1 * i); // The cross-lap slot of a foot and the slider's slot width translate([20, 33]) rect([3 * t + 0.3, 8], center = true); translate([65, 33]) rect([6 + slide_clearance, 8], center = true); } score() translate([5, 3]) text("6.1 6.2 6.3 6.4", size = 4); } echo(str("Flight: wings beat from ", round(low * 10) / 10, " to ", round(high * 10) / 10, " degrees; ", 2 * crank_radius, " mm slider stroke; ", 2 * (roots[1][0] + w), " mm across the wings")); // Assembly, with depths d measured forward from the front of the board: glue // the three backboards and the feet (two plies each) and slide the board down // into the feet. Bolt the fixed posts through the board on 6 mm brass sleeves: // 16 root standoffs and a 4 mm shim at each wing root, 7 guide standoffs and // a 1 mm shim at each guide. The M4 spindle turns in a brass journal in the // board, clamped by nuts to the crank disc in front and the rear crank (three // plies, with the handle grip on its bush) behind. The moving parts go on // sleeves, rear faces at: crank disc t + 1, drive rod 5t + 1, slider 7t + 1 // (on both guide sleeves), left wing link 11t + 1, right wing link 13t + 1, // wings 16t + 4, with spacer rings and 3 mm shims filling the gaps. Join the // links to the wings with the wings level, not hanging down. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/bloom/ # Bloom expanding flower Bloom: a mechanical flower that opens. Turn the drive ring in the middle and six couplers push six sculpted petals straight out along their radial guides, all together, so the flower grows from 34 to 42 cm across; turn it back and the petals close again. This design has 95 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=bloom). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/bloom/bloom.kerf) - [bloom-sheet-1.svg](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-1.svg) - [bloom-sheet-1.dxf](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-1.dxf) - [bloom-sheet-2.svg](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-2.svg) - [bloom-sheet-2.dxf](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-2.dxf) - [bloom-sheet-3.svg](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-3.svg) - [bloom-sheet-3.dxf](https://kerfscript.mylasertools.com/models/bloom/bloom-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 356 × 311.5 mm | | Hub | 1 | 50 × 50 mm | | Retainer | 1 | 62 × 62 mm | | Foot cap | 6 | 18 × 18 mm | | Foot ring | 30 | 18 × 18 mm | | Drive ring | 1 | 140 × 140 mm | | Coupler | 6 | 83 × 13 mm | | Petal | 6 | 92 × 34 mm | | Grip ring | 5 | 16 × 16 mm | | Ring pivot spacer | 6 | 12 × 12 mm | | Inner pin spacer | 12 | 12 × 12 mm | | Outer pin spacer | 18 | 12 × 12 mm | | Fit test | 1 | 150 × 65 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `petals` | `6` | 5, 6 | | `leaf_length` | `85` | 70 to 90 | | `leaf_width` | `34` | 26 to 38 | | `closing_angle` | `55` | 45 to 55 | | `guide_clearance` | `0.4` | 0.3 to 0.5 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `ring_angle` | `35` | 15 to 55 | | `output` | `"all"` | All parts, Base hub and feet, Ring couplers and petals, Spacers, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Bloom: a mechanical flower that opens. Turn the drive ring in the middle // and six couplers push six sculpted petals straight out along their radial // guides, all together, so the flower grows from 34 to 42 cm across; turn it // back and the petals close again. /* [Flower] */ // Number of petals petals = 6; // [5, 6] // Petal length, from the inner pin to the tip leaf_length = 85; // [70:5:90] // Petal width at its widest leaf_width = 34; // [26:2:38] // Ring angle when the flower is closed; more gives more travel closing_angle = 55; // [45:5:55] /* [Fit] */ // Added to the 6 mm guide slots and arc slots the sleeves slide in guide_clearance = 0.4; // [0.3:0.05:0.5] // Added to the 6 mm holes that turn on sleeves bearing_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // Drive ring angle in the assembled view, from the opening angle (open) to // the closing angle (closed) ring_angle = 35; // [15:1:55] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Base hub and feet, motion:Ring couplers and petals, spacers:Spacers, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 180; t = $thickness; n = petals; // Radius of the coupler pins on the drive ring, and the coupler length pivot_radius = 60; coupler_length = 70; // Ring angle when the flower is fully open opening_angle = 15; // Distance between the two pins under each petal pin_spacing = 30; // The two stop bolts stand at this radius stop_radius = 42; bush = 6 + bearing_clearance; slot = 6 + guide_clearance; function polar(r, a) = [r * cos(a), r * sin(a)]; // A slider-crank: with the ring turned a degrees past a petal's guide, its // coupler pin is a degrees off the guide line, and the coupler holds the // petal's inner pin this far from the centre function petal_radius(a) = pivot_radius * cos(a) + sqrt(coupler_length * coupler_length - pivot_radius * pivot_radius * sin(a) * sin(a)); closed = petal_radius(closing_angle); open = petal_radius(opening_angle); assert(contains([5, 6], n), "Petals: 5 or 6"); assert(leaf_length >= 70 && leaf_length <= 90, "Leaf length: 70 to 90 mm"); assert(leaf_width >= 26 && leaf_width <= 38, "Leaf width: 26 to 38 mm"); assert(closing_angle >= 45 && closing_angle <= 55, "Closing angle: 45 to 55 degrees"); assert(guide_clearance >= 0.3 && guide_clearance <= 0.5, "Guide clearance: 0.3 to 0.5 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(ring_angle >= 15 && ring_angle <= 55, "Ring angle: 15 to 55 degrees"); assert(contains(["all", "frame", "motion", "spacers", "coupon"], output), "Unknown output"); module label(s, xy, size = 4) { score() translate(xy) text(s, size = size, halign = "center", valign = "center"); } module washer(od, id) { difference() { circle(d = od); circle(d = id, $fn = 64); } } module bolt_hole(p = [0, 0]) { translate(p) circle(d = 4.4, $fn = 64); } module capsule(a, b, d) { hull() { translate(a) circle(d = d, $fn = 64); translate(b) circle(d = d, $fn = 64); } } // An arc slot, as a chain of straight capsules about 2 degrees long module arc_slot(rad, a, b, d) { for (j = [0:ceil((b - a) / 2) - 1]) let (c = ceil((b - a) / 2)) capsule(polar(rad, a + (b - a) * j / c), polar(rad, a + (b - a) * (j + 1) / c), d); } // Two laminates: a disc with an arm under each petal. The petal's two // sleeves run in the arm's slot, which keeps the petal pointing outward. module base() { difference() { union() { circle(r = 75); for (k = [0:n - 1]) rotate(360 * k / n) hull() { translate([64, 0]) circle(r = 12); translate([166, 0]) circle(r = 12); } } bolt_hole(); for (a = [0, 180]) bolt_hole(polar(stop_radius, a)); for (k = [0:n - 1]) { rotate(360 * k / n) capsule([81, 0], [160, 0], slot); // Foot bolts, between the arms, countersunk flush under the ring bolt_hole(polar(65, 360 * (k + 0.5) / n)); } } for (k = [0:n - 1]) label(str(k + 1), polar(170, 360 * k / n), 3); label("BLOOM", [0, -18], 6); label("ROTATE / EXPAND", [0, -27], 3); } // The ring turns on the hub. Pin k sits on guide k when the ring is at 0, so // at ring angle a it is a degrees ahead of its petal. The fixed stop bolts // run in the two arc slots and hold the ring between the opening and // closing angles. module drive_ring() { difference() { washer(140, 50 + guide_clearance); for (k = [0:n - 1]) bolt_hole(polar(pivot_radius, 360 * k / n)); for (a = [0, 180]) arc_slot(stop_radius, a - closing_angle, a - opening_angle, slot); // The grip bolt_hole(polar(43, 90)); } for (k = [0:n - 1]) score() line(polar(64, 360 * k / n), polar(68, 360 * k / n)); label("TURN", polar(43, 90), 3); } module coupler() { difference() { capsule([0, 0], [coupler_length, 0], 13); circle(d = bush, $fn = 64); translate([coupler_length, 0]) circle(d = bush, $fn = 64); } score() line([11, 0], [coupler_length - 11, 0]); } // The inner pin is at the origin and the petal points along +x module petal() { difference() { union() { capsule([0, 0], [30, 0], 14); polygon([[24, -6], [0.45 * leaf_length, -0.38 * leaf_width], [0.68 * leaf_length, -0.5 * leaf_width], [0.87 * leaf_length, -0.28 * leaf_width], [leaf_length, 0], [0.87 * leaf_length, 0.28 * leaf_width], [0.68 * leaf_length, 0.5 * leaf_width], [0.45 * leaf_length, 0.38 * leaf_width], [24, 6]]); } bolt_hole(); bolt_hole([pin_spacing, 0]); } // Veins score() { line([37, 0], [leaf_length - 8, 0]); for (f = [0.5, 0.65, 0.8]) { line([f * leaf_length, 0], [(f - 0.08) * leaf_length, leaf_width * 0.22]); line([f * leaf_length, 0], [(f - 0.08) * leaf_length, -leaf_width * 0.22]); } } } // Holes for the 6 mm sleeves, then slots for the guides module fit_test() { difference() { rect([150, 65], r = 3); for (j = [0:3]) translate([20 + 35 * j, 18]) circle(d = 6.1 + 0.1 * j, $fn = 64); for (j = [0:2]) translate([25 + 50 * j, 44]) capsule([-8, 0], [8, 0], 6.2 + 0.2 * j); } for (j = [0:3]) label(str(6.1 + 0.1 * j), [20 + 35 * j, 29], 3); for (j = [0:2]) label(str(6.2 + 0.2 * j), [25 + 50 * j, 57], 3); } // The assembled view, z = 0 on top of the base: the feet below it, the hub // and the drive ring, the retainer and grip, then on each ring pin a spacer // and the coupler, and each petal on its two pins in its slot turn = max(opening_angle, min(closing_angle, ring_angle)); reach = petal_radius(turn); function guide(k) = 360 * k / n; function ring_pin(k) = polar(pivot_radius, guide(k) + turn); function foot_at(k, z) = concat(polar(65, guide(k + 0.5)), [z]); if (output == "all" || output == "frame") { step("Glue the two bases together and bolt a foot under each hole between the arms") part("Base", qty = 2, at = [[0, 0, -2 * t], [0, 0, -t]]) base(); step("Drop the drive ring round the hub and bolt the retainer on") { part("Hub", at = [0, 0, 0.4]) washer(50, 4.4); part("Retainer", at = [0, 0, t + 0.4]) washer(62, 4.4); } // A foot between each pair of petals: the cap under the base, then five rings step("Glue the two bases together and bolt a foot under each hole between the arms") { part("Foot cap", qty = n, at = [for (k = [0:n - 1]) foot_at(k, -3 * t)]) washer(18, 4.4); part("Foot ring", qty = 5 * n, at = [for (k = [0:n - 1], j = [0:4]) foot_at(k, -4 * t - j * t)]) washer(18, 10); } } if (output == "all" || output == "motion") { step("Drop the drive ring round the hub and bolt the retainer on") part("Drive ring", at = [0, 0, 0.2], rotate = [0, 0, turn]) drive_ring(); step("Put a pivot spacer and a coupler on each ring pin") part("Coupler", qty = n, at = [for (k = [0:n - 1]) concat(ring_pin(k), [2 * t + 1])], rotate = [for (k = [0:n - 1]) let (v = polar(reach, guide(k)) - ring_pin(k)) [0, 0, atan2(v[1], v[0])]]) coupler(); step("Hang each petal on its two pins, with the spacers, through its slot") part("Petal", qty = n, at = [for (k = [0:n - 1]) concat(polar(reach, guide(k)), [3 * t + 1.5])], rotate = [for (k = [0:n - 1]) [0, 0, guide(k)]]) petal(); step("Stack the grip rings on the grip sleeve") part("Grip ring", qty = 5, at = [for (j = [0:4]) concat(polar(43, 90 + turn), [t + 0.4 + j * t])]) washer(16, bush); } if (output == "all" || output == "spacers") { step("Put a pivot spacer and a coupler on each ring pin") part("Ring pivot spacer", qty = n, at = [for (k = [0:n - 1]) concat(ring_pin(k), [t + 0.2])]) washer(12, bush); step("Hang each petal on its two pins, with the spacers, through its slot") { part("Inner pin spacer", qty = 2 * n, at = [for (k = [0:n - 1], j = [0:1]) concat(polar(reach, guide(k)), [j * t])]) washer(12, bush); part("Outer pin spacer", qty = 3 * n, at = [for (k = [0:n - 1], j = [0:2]) concat(polar(reach + pin_spacing, guide(k)), [j * t])]) washer(12, bush); } } // The fit test lies on the table beside the flower if (output == "all" || output == "coupon") part("Fit test", at = [110, -40, -8 * t]) fit_test(); echo(str("Bloom: ", n, " petals travel ", round((open - closed) * 10) / 10, " mm; the tips span ", round(2 * (closed + leaf_length)), " mm closed and ", round(2 * (open + leaf_length)), " mm open")); // Assembly, with z = 0 the top of the base and t the sheet thickness: glue // the two bases with their slots aligned, and each foot from a cap and five // rings; bolt the feet under the holes between the arms, heads countersunk // flush. Countersink the six pivot bolts and the grip bolt up through the // drive ring. Shim the hub on the base to t + 0.4, lay 0.2 mm thrust film, // drop the ring round the hub and bolt the stop sleeves (t + 0.4 long) // through the arc slots to the base. Bolt the retainer through hub and base; // the ring should turn freely. On each ring pin: sleeve (2t + 1), a pivot // spacer and 0.8 mm of shims, then the coupler (2t + 1 to 3t + 1). Each // petal hangs on two bolts from above with sleeves (5t + 1.8) through the // slot, nuts below the base: two inner pin spacers, 1 mm of shims and the // coupler on the inner pin, three outer pin spacers and 1.5 mm on the outer // one, so the petal sits at 3t + 1.5. Stack the five grip rings on the grip // sleeve (5t + 0.4), then move the ring slowly through its range. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/index-sequencer/ # Index Geneva sequencer Index: a hand-cranked Geneva sequencer. Turn the grip continuously and a pin on the driver arm drops into a slot of the wheel, turns it on by one station and slides out again. The wheel then stays locked while the driver goes round, so the numbered dial advances one step per turn, then waits. This design has 40 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=index-sequencer). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/index-sequencer/index-sequencer.kerf) - [SVG](https://kerfscript.mylasertools.com/models/index-sequencer/index-sequencer.svg) - [DXF](https://kerfscript.mylasertools.com/models/index-sequencer/index-sequencer.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Driver arm | 2 | 112.2 × 26 mm | | Locking disc | 2 | 78.1 × 91.6 mm | | Geneva wheel | 2 | 127.3 × 127.3 mm | | Grip ring | 5 | 16 × 16 mm | | Output hub | 4 | 28 × 28 mm | | Base | 2 | 257.4 × 167.4 mm | | Output pedestal | 2 | 20 × 20 mm | | Foot ring | 12 | 18 × 18 mm | | Foot cap | 4 | 18 × 18 mm | | Station dial | 1 | 46 × 46 mm | | Pointer | 1 | 35 × 35 mm | | Pointer spacer | 2 | 12 × 12 mm | | Fit test | 1 | 140 × 55 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `stations` | `4` | 4, 5, 6 | | `center_distance` | `90` | 85 to 100 | | `clearance` | `0.4` | 0.35 to 0.6 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `driver_angle` | `-18` | -180 to 1260 | | `output` | `"all"` | All parts, Wheel arm and lock, Base and feet, Dial and pointer, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Index: a hand-cranked Geneva sequencer. Turn the grip continuously and a // pin on the driver arm drops into a slot of the wheel, turns it on by one // station and slides out again. The wheel then stays locked while the driver // goes round, so the numbered dial advances one step per turn, then waits. /* [Mechanism] */ // Slots in the wheel and numbers on the dial stations = 4; // [4, 5, 6] // Distance between the driver and wheel shafts center_distance = 90; // [85:5:100] /* [Fit] */ // Play of the pin across its slot, and gap around the locking disc clearance = 0.4; // [0.35:0.05:0.6] // Added to the 6 mm holes that turn on brass sleeves bearing_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // Driver angle in the assembled view, from the pin pointing at the wheel; // each turn indexes the wheel one station on driver_angle = -18; // [-180:2:1260] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, motion:Wheel arm and lock, frame:Base and feet, display:Dial and pointer, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 240; t = $thickness; n = stations; c = center_distance; // The driver turns at A = [0, 0], the wheel at B = [c, 0]. Slots are // 2 alpha apart; the pin runs on a radius r = c sin(alpha), so it meets // each slot square-on and the wheel starts and stops without a knock. alpha = 180 / n; r = c * sin(alpha); pin_radius = 3; lock_radius = 0.72 * r; // The wheel ends where the pin enters, measured at the edge of the slot rim = sqrt(pow(c * cos(alpha), 2) + pow(pin_radius, 2)); // The grip sits opposite the pin grip_offset = 0.48 * r; // The relief in the locking disc is a circle crossing its edge at right // angles, centred out along the pin: it opens 90 - alpha either side of the // pin, exactly while the pin is in a slot and the wheel has to turn relief_center = lock_radius / sin(alpha); relief_radius = lock_radius / tan(alpha); assert(contains([4, 5, 6], n), "Stations: 4, 5 or 6"); assert(c >= 85 && c <= 100, "Center distance: 85 to 100 mm"); assert(clearance >= 0.35 && clearance <= 0.6, "Clearance: 0.35 to 0.6 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(driver_angle >= -180 && driver_angle <= 1260, "Driver angle: -180 to 1260 degrees"); assert(contains(["all", "motion", "frame", "display", "coupon"], output), "Unknown output"); function polar(d, a) = [d * cos(a), d * sin(a)]; module label(s, p, sz = 4) { score() translate(p) text(s, size = sz, halign = "center", valign = "center"); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } module bolt() { circle(d = 4.5); } // The 6 mm sleeve hole, and a 2 mm pin hole that keeps glued layers in line module register() { circle(d = 6 + bearing_clearance); translate([-10, 0]) circle(d = 2.1, $fn = 64); } // Drawn mid-index, with a slot facing the driver at -X. Between the slots, // concave locking arcs centred c away: while the wheel waits, one of them // wraps the locking disc and holds it still. module wheel() { difference() { circle(r = rim); for (k = [0:n - 1]) { rotate(180 + 360 * k / n) translate([c - r - pin_radius - 1, -pin_radius - clearance / 2]) rect([2 * rim, 2 * pin_radius + clearance]); translate(polar(c, 180 + alpha + 360 * k / n)) circle(r = lock_radius + clearance); } register(); } score() line([9, 0], [17, 0]); } // The relief is 0.2 mm larger than the exact circle, for real-world play module locking_disc() { difference() { circle(r = lock_radius); translate([relief_center, 0]) circle(r = relief_radius + 0.2); register(); translate([-grip_offset, 0]) bolt(); } score() line([-16, 0], [-23, 0]); } // The pin's bolt at +X, the grip's at -X module arm() { difference() { hull() { circle(r = 13); translate([r, 0]) circle(r = 9); translate([-grip_offset, 0]) circle(r = 9); } register(); translate([r, 0]) bolt(); translate([-grip_offset, 0]) bolt(); } score() line([15, 0], [r - 9, 0]); } module dial() { difference() { circle(r = 23); register(); } for (k = [0:n - 1]) label(str(k + 1), polar(16, alpha + 360 * k / n), 6); score() outline() circle(r = 21); } // A square hook from the output bolt, its arrow pointing back at the dial module pointer() { difference() { union() { circle(r = 6); hull() { circle(r = 2); translate([0, 27]) circle(r = 2); } hull() { translate([0, 27]) circle(r = 2); translate([27, 27]) circle(r = 2); } hull() { translate([27, 27]) circle(r = 2); translate([27, 0]) circle(r = 2); } polygon([[22, 0], [29, 4], [29, -4]]); } circle(d = 4.4); } } module base() { difference() { translate([-r - 20, -rim - 20]) rect([c + rim + r + 40, 2 * rim + 40], r = 8); bolt(); translate([c, 0]) bolt(); // Corner holes, to line up the feet for (x = [-r - 8, c + rim + 8]) for (y = [-rim - 8, rim + 8]) translate([x, y]) bolt(); } label("INDEX", [c / 2, -rim - 9], 9); label(str(n, " STATIONS / STEP ", 360 / n, " DEG"), [c / 2, rim + 10], 4); score() { line([0, 0], [c, 0]); translate([c, 0]) outline() circle(r = 23); } } module fit_test() { difference() { rect([140, 55], r = 3); for (i = [0:3]) translate([18 + 32 * i, 17]) circle(d = 6.1 + 0.1 * i); for (i = [0:2]) translate([25 + 42 * i, 39]) rect([18, 6.2 + 0.2 * i], center = true); } for (i = [0:3]) label(str(6.1 + 0.1 * i), [18 + 32 * i, 29], 3); } // The assembled view, base underside at z = 0: the driver (arm, then // locking disc) at the origin, the wheel block (wheel, hubs, dial) on its // pedestal at [c, 0], the pointer on spacers above it. While the pin is in // a slot, the slot points from the wheel's centre at the pin; for the rest // of the turn the wheel waits, locked, half a station either side. a = driver_angle; turns = floor((a + 180) / 360); local = a - 360 * turns; wheel_turn = (abs(local) <= 90 - alpha ? let (p = polar(r, local)) atan2(p[1], p[0] - c) % 360 - 180 : local > 0 ? -alpha : alpha) - 360 / n * turns; function stack(x, z, k) = [for (i = [0:k - 1]) [x, 0, z + i * t]]; feet = [for (x = [-r - 8, c + rim + 8], y = [-rim - 8, rim + 8]) [x, y]]; // The assembly steps, listed first so they number in build order step("Glue the feet under the base and the pedestal over the wheel's hole"); step("Glue the locking disc on the driver arm and bolt the arm on its shaft"); step("Glue the wheel, the hubs and the dial into one block and set it on its shaft over the pedestal"); step("Bolt the pointer on its two spacers above the wheel"); step("Put the grip rings on their sleeve"); if (output == "all" || output == "motion") { step("Glue the locking disc on the driver arm and bolt the arm on its shaft") { part("Driver arm", qty = 2, at = stack(0, 2 * t + 0.5, 2), rotate = [0, 0, a]) arm(); part("Locking disc", qty = 2, at = stack(0, 4 * t + 0.5, 2), rotate = [0, 0, a]) locking_disc(); } step("Glue the wheel, the hubs and the dial into one block and set it on its shaft over the pedestal") part("Geneva wheel", qty = 2, at = stack(c, 4 * t + 1, 2), rotate = [0, 0, wheel_turn]) wheel(); // The grip turns with the driver, opposite the pin step("Put the grip rings on their sleeve") part("Grip ring", qty = 5, at = [for (i = [0:4]) concat(polar(grip_offset, a + 180), [6 * t + 1.5 + i * t])], rotate = [0, 0, a]) ring(16, 6 + bearing_clearance); step("Glue the wheel, the hubs and the dial into one block and set it on its shaft over the pedestal") part("Output hub", qty = 4, at = stack(c, 6 * t + 1, 4), rotate = [0, 0, wheel_turn]) difference() { circle(d = 28); register(); } } if (output == "all" || output == "frame") step("Glue the feet under the base and the pedestal over the wheel's hole") { part("Base", qty = 2, at = stack(0, 0, 2)) base(); // Takes the M4 shaft only, so the output sleeve stands on it part("Output pedestal", qty = 2, at = stack(c, 2 * t, 2)) ring(20, 4.5); part("Foot ring", qty = 12, at = [for (f = feet, i = [0:2]) concat(f, [-3 * t + i * t])]) ring(18, 12); part("Foot cap", qty = 4, at = [for (f = feet) concat(f, [-4 * t])]) ring(18, 4.5); } if (output == "all" || output == "display") { step("Glue the wheel, the hubs and the dial into one block and set it on its shaft over the pedestal") part("Station dial", at = [c, 0, 10 * t + 1], rotate = [0, 0, wheel_turn]) dial(); step("Bolt the pointer on its two spacers above the wheel") { part("Pointer", at = [c, 0, 13 * t + 2.5]) pointer(); part("Pointer spacer", qty = 2, at = stack(c, 11 * t + 2.5, 2)) ring(12, 4.5); } } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [c + rim + 30, -rim, -4 * t]) fit_test(); echo(str("Index: ", n, " stations, ", 360 / n, " degrees a step, waits ", 100 * (0.5 + 1 / n), "% of each turn; pin on a ", round(r * 10) / 10, " mm radius")); // Assembly, with base bottom at z = 0 and t the sheet thickness: glue the // laminates first (base, pedestal, driver arm, locking disc, wheel, a grip // of five rings, four feet of three rings over a cap). Glue the feet under // the corner holes and the pedestal over the wheel's hole. Countersink the // two outer holes under the driver arm until M4 flat heads sit flush, fit // the pin and grip bolts from below, then glue the locking disc on the arm // with its relief towards the pin. Glue the wheel, four hubs and the dial // into one block, lined up by a 2 mm pin. Both shafts are M4 bolts up // through the base, each with a 6 mm brass sleeve to turn on: the driver on // a 4t + 1.2 sleeve, the wheel block 0.5 mm above the arm on a 7t + 1.7 // sleeve over the pedestal. With the pin pointing away from the wheel, set // dial 1 facing right so a locking arc wraps the disc. Shim each to 0.2 mm // endplay under a washer and nut. Bolt the pointer on two spacers above the // wheel's nut, arrow at the right-hand number. Last, a 2t + 0.8 sleeve and // small nut on the pin, clear of the wheel, and the grip rings on a 5t + 1.7 // sleeve. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/vault/ # Vault three-dial puzzle box Vault: a puzzle box (not a safe) with three dials on its lid. Each dial turns a notched gate wheel under the lid; dial in the code and the three notches line up with the fingers of a sliding fence, so the knob slides the fence off its catch in the front wall and the lid lifts out. This design has 63 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=vault). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/vault/vault.kerf) - [vault-sheet-1.svg](https://kerfscript.mylasertools.com/models/vault/vault-sheet-1.svg) - [vault-sheet-1.dxf](https://kerfscript.mylasertools.com/models/vault/vault-sheet-1.dxf) - [vault-sheet-2.svg](https://kerfscript.mylasertools.com/models/vault/vault-sheet-2.svg) - [vault-sheet-2.dxf](https://kerfscript.mylasertools.com/models/vault/vault-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 260 × 180 mm | | Front wall | 1 | 260 × 90 mm | | Front inner wall | 1 | 260 × 90 mm | | Rear wall | 1 | 260 × 90 mm | | Rear inner wall | 1 | 260 × 90 mm | | Side wall | 2 | 168 × 90 mm | | Corner cleat | 8 | 12 × 66 mm | | Floor cleat | 4 | 100 × 12 mm | | Lid ledge | 4 | 100 × 12 mm | | Lid | 1 | 252.8 × 166.8 mm | | Bearing pad | 6 | 32 × 32 mm | | Rear tongue | 2 | 20 × 22 mm | | Tongue spacer | 4 | 20 × 12 mm | | Gate wheel 1, code 3 | 1 | 48 × 48 mm | | Gate wheel 2, code 1 | 1 | 48 × 48 mm | | Gate wheel 3, code 7 | 1 | 48 × 48 mm | | Dial | 3 | 48 × 48 mm | | Hub disc | 9 | 18 × 18 mm | | Fence | 1 | 210 × 46.5 mm | | Fence spacer | 4 | 14 × 14 mm | | Fence keeper | 2 | 16 × 16 mm | | Knob stem | 3 | 8 × 8 mm | | Knob | 1 | 24 × 24 mm | | Fit test | 1 | 140 × 65 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `code_1` | `3` | 0 to 9 | | `code_2` | `1` | 0 to 9 | | `code_3` | `7` | 0 to 9 | | `box_height` | `90` | 80 to 120 | | `rotor_clearance` | `0.5` | 0.4 to 0.6 | | `fence_slide` | `10` | 0 to 10 | | `lid_lift` | `45` | 0 to 80 | | `output` | `"all"` | All parts, Box, Lid and its fittings, Dials and gate wheels, Fence and knob, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Vault: a puzzle box (not a safe) with three dials on its lid. Each dial // turns a notched gate wheel under the lid; dial in the code and the three // notches line up with the fingers of a sliding fence, so the knob slides // the fence off its catch in the front wall and the lid lifts out. /* [Combination] */ // First digit; the gate wheels are cut to suit the code code_1 = 3; // [0:1:9] // Second digit code_2 = 1; // [0:1:9] // Third digit code_3 = 7; // [0:1:9] /* [Enclosure] */ // Height of the walls, without the base box_height = 90; // [80:5:120] /* [Fit] */ // Added to the 18 mm bores the dial hubs turn in rotor_clearance = 0.5; // [0.4:0.1:0.6] /* [Assembly] */ // How far the fence is slid back in the assembled view; 10 mm frees the // lid, and only with the right code fence_slide = 10; // [0:0.5:10] // How high the lid is lifted out lid_lift = 45; // [0:1:80] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, enclosure:Box, lid:Lid and its fittings, rotors:Dials and gate wheels, fence:Fence and knob, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 144; t = $thickness; W = 260; D = 180; H = box_height; // Gap between the lid and the walls, all round lid_gap = 0.6; // Where the three dials sit along the lid, left to right dial_x = [70, 130, 190]; codes = [code_1, code_2, code_3]; assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(H >= 80 && H <= 120, "Box height: 80 to 120 mm"); assert(rotor_clearance >= 0.4 && rotor_clearance <= 0.6, "Rotor clearance: 0.4 to 0.6 mm"); for (k = codes) assert(k >= 0 && k <= 9 && floor(k) == k, "Code digits: whole numbers 0 to 9"); assert(fence_slide >= 0 && fence_slide <= 10, "Fence slide: 0 to 10 mm"); assert(lid_lift >= 0 && lid_lift <= 80, "Lid lift: 0 to 80 mm"); assert(contains(["all", "enclosure", "lid", "rotors", "fence", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; module label(s, p, size = 5) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module slot(a, b, d) { hull() { translate(a) circle(d = d); translate(b) circle(d = d); } } module annulus(od, id) { difference() { circle(d = od); circle(d = id); } } // The M4 bolt hole and two 2 mm pin holes, the same in every layer of a // rotor, so the gate wheel can only go on one way round module pin_holes() { circle(d = 4.4); translate([6, 0]) circle(d = 2.2); translate([0, 6]) circle(d = 2.2); } // The front and rear walls are doubled. The inner one has windows at fence // height, one in front for the fence's catch and two at the back for the // lid's tongues; the outer one closes them off. module wall(front = true, inner = true) { difference() { rect([W, H]); if (inner) { if (front) translate([125, H - 4 * t - 0.7]) rect([10, t + 1.4]); else for (x = [45, 195]) translate([x - 1, H - 4 * t - 0.7]) rect([22, t + 1.4]); } } if (!inner && front) { label("VAULT", [W / 2, H * 0.58], 18); label("THREE DIAL PUZZLE", [W / 2, H * 0.32], 5); } } // Score lines mark the inner faces of the doubled walls module base() { rect([W, D]); score() { line([t, 2 * t], [W - t, 2 * t]); line([t, D - 2 * t], [W - t, D - 2 * t]); } } // Drawn where it sits in the box, front at the bottom: the dial bores, the // two fence guide holes and the slot the knob moves in, with a pointer under // each dial module lid() { difference() { translate([t + lid_gap, 2 * t + lid_gap]) rect([W - 2 * (t + lid_gap), D - 2 * (2 * t + lid_gap)]); for (x = dial_x) translate([x, 75]) circle(d = 18 + rotor_clearance); for (x = [35, 225]) translate([x, 29]) circle(d = 3.4); slot([130, 20], [130, 30], 8.6); } for (i = [0:2]) { score() polygon([[dial_x[i] - 2, 45], [dial_x[i] + 2, 45], [dial_x[i], 49]]); label(str(i + 1), [dial_x[i], 39], 4); } label("VAULT", [130, 123], 12); label("SET / SLIDE / LIFT", [130, 142], 4); // Arrow: slide the knob this way to open score() { line([148, 20], [148, 30]); line([146, 28], [148, 30]); line([150, 28], [148, 30]); } } // Digit k is drawn 36k degrees clockwise of the front, and the gate wheel's // notch is turned the same way for its code. Turning the rotor to bring the // code under the pointer brings the notch round to face the fence. module gate(k) { difference() { circle(r = 24); rotate(-36 * k) translate([-3.5, -25]) rect([7, 13]); pin_holes(); } score() line([9, 0], [19, 0]); } module dial() { difference() { circle(r = 24); pin_holes(); } for (k = [0:9]) { label(str(k), polar(18, -90 - 36 * k), 4); score() line(polar(21.5, -90 - 36 * k), polar(23, -90 - 36 * k)); } score() line([9, 0], [13, 0]); } module hub() { difference() { circle(d = 18); pin_holes(); } } // Drawn where it sits under the lid, locked. Its fingers stop 1 mm short of // the gate wheels; the tab in front reaches into the front wall's window. // With every notch facing it the fence slides 10 mm back on its two guide // slots, the fingers enter the notches and the tab leaves the window. One // wrong dial stops the whole fence after about 1 mm. module fence() { difference() { union() { translate([25, 13]) rect([210, 26], r = 2); for (x = dial_x) translate([x - 2.75, 38]) rect([5.5, 12]); translate([126, t + 0.5]) rect([8, 39 - (t + 0.5)]); } for (x = [35, 225]) slot([x, 19], [x, 29], 6.6); translate([130, 20]) circle(d = 4.4); } score() line([130, 34], [130, 39]); } module coupon() { difference() { rect([140, 65], r = 3); for (i = [0:3]) translate([19 + 30 * i, 18]) circle(d = 18.2 + i * 0.1); for (i = [0:2]) translate([25 + 35 * i, 47]) circle(d = 6.2 + i * 0.2); } for (i = [0:3]) label(str(18.2 + i * 0.1), [19 + 30 * i, 32], 3); for (i = [0:2]) label(str(6.2 + i * 0.2), [25 + 35 * i, 59], 3); } // The assembled view, front at y = 0 and the base on the table. The front // and rear walls stand on the base; everything under the lid hangs from it // and lifts out with it. The rotors are turned to the code, the fence slid // back with its knob. guide_x = [35, 225]; tongue_x = [45, 195]; upright = [90, 0, 0]; along = [0, 0, 90]; function wall_at(y) = [0, y, t]; function on_lid(x, y, z) = [x, y, z + lid_lift]; function code_turn(i) = [0, 0, 36 * codes[i]]; cleats = [for (j = [0:3], k = [0:1]) [j % 2 == 0 ? t : W - t - 12, j < 2 ? 3 * t + k * t : D - 2 * t - k * t, t]]; if (output == "all" || output == "enclosure") step("Glue up the box: the walls on the base, then the cleats and ledges") { part("Base", at = [0, 0, 0]) base(); part("Front wall", at = wall_at(t), rotate = upright) wall(true, false); part("Front inner wall", at = wall_at(2 * t), rotate = upright) wall(true, true); part("Rear wall", at = wall_at(D), rotate = upright) wall(false, false); part("Rear inner wall", at = wall_at(D - t), rotate = upright) wall(false, true); part("Side wall", qty = 2, at = [[0, 2 * t, t], [W - t, 2 * t, t]], rotate = [90, 0, 90]) rect([D - 4 * t, H]); // Pairs of strips in the four corners, against the inner walls part("Corner cleat", qty = 8, at = cleats, rotate = upright) rect([12, H - 8 * t]); part("Floor cleat", qty = 4, at = [[t + 12, 40, t], [W - t, 40, t], [80, 2 * t, t], [80, D - 2 * t - 12, t]], rotate = [along, along, [0, 0, 0], [0, 0, 0]]) rect([100, 12]); part("Lid ledge", qty = 4, at = [for (x = [t + 12, W - t], k = [0:1]) [x, 50, H - 2 * t + k * t]], rotate = along) rect([100, 12]); } if (output == "all" || output == "lid") step("Glue the bearing pads and the rear tongues under the lid") { part("Lid", at = on_lid(0, 0, H)) lid(); part("Bearing pad", qty = 6, at = [for (x = dial_x, k = [0:1]) on_lid(x, 75, H - 2 * t + k * t)]) annulus(32, 18 + rotor_clearance); part("Rear tongue", qty = 2, at = [for (x = tongue_x) on_lid(x, D - t - 22.5, H - 3 * t)]) rect([20, 22]); part("Tongue spacer", qty = 4, at = [for (x = tongue_x, k = [0:1]) on_lid(x, D - t - 22.5, H - 2 * t + k * t)]) rect([20, 12]); } if (output == "all" || output == "rotors") step("Build the three rotors through the lid, each set to its code") { for (i = [0:2]) part(str("Gate wheel ", i + 1, ", code ", codes[i]), at = on_lid(dial_x[i], 75, H - 3 * t - 0.5), rotate = code_turn(i)) gate(codes[i]); part("Dial", qty = 3, at = [for (x = dial_x) on_lid(x, 75, H + t)], rotate = [for (i = [0:2]) code_turn(i)]) dial(); part("Hub disc", qty = 9, at = [for (x = dial_x, k = [0:2]) on_lid(x, 75, H - 2 * t - 0.5 + k * t)], rotate = [for (i = [0:2], k = [0:2]) code_turn(i)]) hub(); } if (output == "all" || output == "fence") step("Hang the fence under the lid and bolt the knob on through the slot") { part("Fence", at = on_lid(0, fence_slide, H - 3 * t)) fence(); part("Fence spacer", qty = 4, at = [for (x = guide_x, k = [0:1]) on_lid(x, 29, H - 2 * t + k * t)]) annulus(14, 6.2); part("Fence keeper", qty = 2, at = [for (x = guide_x) on_lid(x, 29, H - 4 * t - 0.4)]) annulus(16, 3.4); part("Knob stem", qty = 3, at = [for (k = [0:2]) on_lid(130, 20 + fence_slide, H - 2 * t + 1 + k * t)]) annulus(8, 4.4); part("Knob", at = on_lid(130, 20 + fence_slide, H + t + 1)) annulus(24, 4.4); } // The fit test lies on the table beside the box if (output == "all" || output == "coupon") part("Fit test", at = [W + 20, 0, 0]) coupon(); echo(str("Vault: code ", code_1, "-", code_2, "-", code_3, ", ", W, " x ", D, " x ", H + t, " mm; fence guide sleeves ", 3 * t + 0.4, " mm long")); // Assembly, with z up from the bottom of the base: glue each inner wall to // its outer wall, stand them and the side walls on the base, add the corner // cleats in pairs at the four corners (up to H - 7t), the floor cleats and // a ledge of two strips each side (y 50 to 150) with its top at H. Under // the lid glue two bearing pads round each bore. Each rotor is a gate wheel, // three hub discs through the bore, a 0.5 mm shim and the dial on top, on // an M4 bolt with two 2 mm pins, glued to each other but not to the lid. // The fence hangs on two M3 bolts: lid, fence spacers, a 6 mm brass sleeve // 3t + 0.4 long through the fence slot, keeper, nut. Bolt the 1 mm washer, // the stems and the knob to the fence through the lid slot. Glue each rear // tongue under its two spacers, its end 3 mm past the back of the lid. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/lambda/ # Lambda Chebyshev walker Lambda: a four-legged walker on Chebyshev's lambda linkage. One square shaft turns four eccentrics a quarter turn apart; each drives a lambda whose far end follows an approximately level path before lifting and swinging back. The legs hang from those ends on flat soles. Turn the handle anticlockwise, seen from the handle side, to follow the walking motion. This design has 62 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=lambda). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/lambda/lambda.kerf) - [lambda-sheet-1.svg](https://kerfscript.mylasertools.com/models/lambda/lambda-sheet-1.svg) - [lambda-sheet-1.dxf](https://kerfscript.mylasertools.com/models/lambda/lambda-sheet-1.dxf) - [lambda-sheet-2.svg](https://kerfscript.mylasertools.com/models/lambda/lambda-sheet-2.svg) - [lambda-sheet-2.dxf](https://kerfscript.mylasertools.com/models/lambda/lambda-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Right frame | 2 | 173 × 104 mm | | Left frame | 2 | 173 × 104 mm | | Front plate | 1 | 104 × 104 mm | | Rear plate | 1 | 104 × 104 mm | | Journal | 4 | 18 × 18 mm | | Eccentric | 8 | 46 × 46 mm | | Coupler | 8 | 113.2 × 60.4 mm | | Rocker | 4 | 51.5 × 14 mm | | Leg | 8 | 74 × 124 mm | | Thrust collar | 4 | 26 × 26 mm | | Spacer ring | 12 | 12 × 12 mm | | Handle | 2 | 53 × 20 mm | | Handle grip | 5 | 18 × 18 mm | | Fit test | 1 | 150 × 58 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `sole_length` | `60` | 50 to 70 | | `frame_gap` | `92` | 84 to 108 | | `pivot_clearance` | `0.3` | 0.2 to 0.4 | | `shaft_fit` | `0.15` | 0.05 to 0.3 | | `turn_clearance` | `0.4` | 0.3 to 0.6 | | `slot_fit` | `0.1` | 0 to 0.2 | | `crank_angle` | `0` | 0 to 360 | | `output` | `"all"` | All parts, Frames and end plates, Eccentrics couplers and rockers, Legs, Collars spacers and handle, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Lambda: a four-legged walker on Chebyshev's lambda linkage. One square // shaft turns four eccentrics a quarter turn apart; each drives a lambda whose // far end follows an approximately level path before lifting and swinging // back. The legs hang from those ends on flat soles. Turn the handle // anticlockwise, seen from the handle side, to follow the walking motion. /* [Walker] */ // Length of each sole; longer soles stand steadier sole_length = 60; // [50:5:70] // Space between the two side frames, inside frame_gap = 92; // [84:4:108] /* [Fit] */ // Added to the 6 mm holes the brass bushes turn in pivot_clearance = 0.3; // [0.2:0.05:0.4] // Added to the 6 mm square holes for the shaft shaft_fit = 0.15; // [0.05:0.05:0.3] // Total play of the journals in the frames and the couplers on their eccentrics turn_clearance = 0.4; // [0.3:0.1:0.6] // Added to the tab slots in the frames slot_fit = 0.1; // [0:0.05:0.2] /* [Assembly] */ // Shaft angle in the assembled view; only moves the parts crank_angle = 0; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Frames and end plates, linkage:Eccentrics couplers and rockers, legs:Legs, accessories:Collars spacers and handle, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 120; t = $thickness; // Chebyshev's proportions: crank 1, frame 2, rocker 2.5, coupler 2.5 to // the rocker and as far again to the foot point P r = 15; L = 2.5 * r; // Side view, x forward, y up, floor at y = 0: the shaft A and the rocker // pivot D sit level, H above the floor H = 55; A = [0, H]; D = [2 * r, H]; // The eccentric that stands in for the crank, and the coupler's ring round it ecc = 23; ring = ecc + turn_clearance / 2 + 7; // Each leg hangs from P down to the floor; the shin passes in front of the // shaft, the thigh reaches back far enough to hang the leg plumb drop = H + 4 * r; shin = [12, 26]; thigh = 44; // Frame outline and where the end plates stand rear = -56; front = ceil(4.36 * r + sole_length / 2 + 6); x0 = rear - 6; x1 = front + t + 6; y0 = 36; y1 = 140; tabs = [[42, 58], [110, 130]]; bush = 6 + pivot_clearance; assert(sole_length >= 50 && sole_length <= 70, "Sole length: 50 to 70 mm"); assert(frame_gap >= 84 && frame_gap <= 108, "Frame gap: 84 to 108 mm"); assert(pivot_clearance >= 0.2 && pivot_clearance <= 0.4, "Pivot clearance: 0.2 to 0.4 mm"); assert(shaft_fit >= 0.05 && shaft_fit <= 0.3, "Shaft fit: 0.05 to 0.3 mm"); assert(turn_clearance >= 0.3 && turn_clearance <= 0.6, "Turn clearance: 0.3 to 0.6 mm"); assert(slot_fit >= 0 && slot_fit <= 0.2, "Slot fit: 0 to 0.2 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "frame", "linkage", "legs", "accessories", "coupon"], output), "Unknown output"); function polar(rad, a) = [rad * cos(a), rad * sin(a)]; // Where circles round p (radius rad) and q (radius rad2) cross; sgn picks the side function meet(p, rad, q, rad2, sgn) = let (v = q - p, d = norm(v), a = (rad * rad - rad2 * rad2 + d * d) / (2 * d), h = sqrt(max(0, rad * rad - a * a))) p + v * a / d + [-v[1], v[0]] * h / d * sgn; // Crank pin B, rocker joint C and foot point P at crank angle a, relative to A function joints(a) = let (B = polar(r, a), C = meet(B, L, D - A, L, 1)) [B, C, 2 * C - B]; path = [for (a = [0:2:358]) joints(a)[2]]; low = min([for (p = path) p[1]]); flat = [for (p = path) if (p[1] - low < 0.2) p[0]]; high = max([for (p = path) p[1]]); // Assembled view: x forward, y to the left, z up, the shaft along y. The // side-view parts stand up with rotate = [90, -a, 0]: their x stays x, their // y becomes z, a turns them like the shaft, and their thickness runs towards // -y from the y given. s counts out from a frame face, outside or inside. out_face = frame_gap / 2 + 2 * t; in_face = frame_gap / 2; // Each lambda: side (-1 right, 1 left), outside the frame or inside, and how // far its eccentric is turned on the shaft legs = [[-1, true, 0], [-1, false, 180], [1, true, 90], [1, false, 270]]; // Where the layers start, in s layer = [["collar", 0.5], ["rocker", 4], ["ring D", 0.5], ["ring C", 7.5], ["eccentric", 8 + t], ["ring P", 17.5], ["leg", 12 + 3 * t], ["handle", 17 + 5 * t]]; function s_of(name) = layer[search(name, [for (l = layer) l[0]])][1]; // World y of the face a part from s to s + t hangs from (its largest y), on // lambda k's side function face(k, s) = let (l = legs[k], near = l[1] ? out_face + s : in_face - s, far = l[1] ? near + t : near - t) l[0] > 0 ? max(near, far) : -min(near, far); function turn(k) = crank_angle + legs[k][2]; function world(k) = [for (p = joints(turn(k))) A + p]; function at_side(p, y) = [p[0], y, p[1]]; function upright(a = 0) = [90, -a, 0]; function heading(p, q) = atan2(q[1] - p[1], q[0] - p[0]); handle_turn = crank_angle + 200; grip = A + polar(35, handle_turn); module square_bore() { rect([6 + shaft_fit, 6 + shaft_fit], center = true); } module label(s, p, size = 4) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } // The right frame as seen from outside; the left one is its mirror image module frame_body() { difference() { translate([x0, y0]) rect([x1 - x0, y1 - y0], r = 8); translate(A) circle(d = 18 + turn_clearance); translate(D) circle(d = bush); translate([-38, 80]) rect([126, 26], r = 8); for (x = [rear, front], y = tabs) translate([x - slot_fit / 2, y[0]]) rect([t + slot_fit, y[1] - y[0]]); } // The path every point of a leg follows, drawn where the leg pivot P runs score() polyline([for (p = path) A + p], closed = true); } module frame_text(m) { label("FOOT PATH", [m * 2 * r, H + 4 * r - 6], 3.5); } // Seen from the front: tabs at both ends go through the frames module end_plate() { difference() { translate([-frame_gap / 2, y0]) rect([frame_gap, y1 - y0]); translate([8 - frame_gap / 2, 62]) rect([frame_gap - 16, 42], r = 6); } for (s = [-1, 1], y = tabs) translate([s > 0 ? frame_gap / 2 : -frame_gap / 2 - 2 * t, y[0]]) rect([2 * t, y[1] - y[0]]); } // Bore on the shaft at the origin, the crank pin B on +x module eccentric() { difference() { translate([r, 0]) circle(r = ecc); square_bore(); } score() line([5, 0], [r + ecc - 4, 0]); } // B at the origin, C and P along +x module coupler() { difference() { hull() { circle(r = ring); translate([2 * L, 0]) circle(r = 8); } circle(d = 2 * ecc + turn_clearance); translate([L, 0]) circle(d = bush); translate([2 * L, 0]) circle(d = bush); } score() line([ring + 2, 0], [L - 9, 0]); label("C", [L, 10], 4); label("P", [2 * L - 13, 0], 4); } module rocker() { difference() { hull() { circle(r = 7); translate([L, 0]) circle(r = 7); } circle(d = bush); translate([L, 0]) circle(d = bush); } } // The pivot P at the origin, the sole on the floor at y = -drop module leg() { difference() { offset(r = 4) offset(delta = -4) offset(r = -6) offset(delta = 6) union() { circle(r = 9); polygon([[-thigh, -50], [shin[1], -50], [shin[1], -4], [12, 9], [-12, 9], [-thigh, -28]]); translate([shin[0], -drop + 6]) rect([shin[1] - shin[0], drop - 56]); translate([-sole_length / 2, -drop]) rect([sole_length, 8], r = 3); } circle(d = bush); } score() line([-sole_length / 2 + 4, -drop + 4], [sole_length / 2 - 4, -drop + 4]); } module disc(d, hole) { difference() { circle(d = d); circle(d = hole); } } module handle() { difference() { hull() { circle(r = 10); translate([35, 0]) circle(r = 8); } square_bore(); translate([35, 0]) circle(d = 4.4); } } module coupon() { difference() { rect([150, 58], r = 3); for (i = [0:3]) translate([15 + 22 * i, 15]) circle(d = 6.1 + 0.1 * i); for (i = [0:2]) translate([105 + 17 * i, 15]) rect([6.05 + 0.1 * i, 6.05 + 0.1 * i], center = true); translate([20, 42]) circle(d = 18 + turn_clearance); translate([50, 34]) rect([t + slot_fit, 16]); translate([70, 34]) rect([2 * t + slot_fit, 16]); } for (i = [0:3]) label(str(6.1 + 0.1 * i), [15 + 22 * i, 5], 3); for (i = [0:2]) label(str(6.05 + 0.1 * i), [105 + 17 * i, 5], 3); } // The assembly steps in build order, named first so they number that way build_steps = [ "Glue up the frames with their journals and slot in the end plates", "Put the square shaft through, with a thrust collar on each frame face", "Bolt a rocker each side of the frames at D, with the spacer rings", "Slide the eccentrics on the shaft and bolt each coupler to its rocker", "Hang the legs from the couplers", "Fit the handle and its grip on the right end of the shaft" ]; if (output == "all") for (k = build_steps) step(k) {} if (output == "all" || output == "frame") { step(build_steps[0]) part("Right frame", qty = 2, at = [[0, -in_face - t, 0], [0, -in_face, 0]], rotate = upright()) { frame_body(); frame_text(1); label("TURN ANTICLOCKWISE", [2 * r, y0 + 8], 3.5); } step(build_steps[0]) part("Left frame", qty = 2, at = [[0, in_face + t, 0], [0, in_face, 0]], rotate = [90, 0, 180]) { mirror([1, 0]) frame_body(); frame_text(-1); } step(build_steps[0]) part("Front plate", at = [front, 0, 0], rotate = [90, 0, 90]) { end_plate(); label("LAMBDA", [0, 122], 14); label("CHEBYSHEV WALKER", [0, 49], 5); } step(build_steps[0]) part("Rear plate", at = [rear, 0, 0], rotate = [90, 0, 90]) end_plate(); // Two plies in each frame step(build_steps[0]) part("Journal", qty = 4, at = [for (y = [-in_face - t, -in_face, in_face + t, out_face]) [0, y, H]], rotate = upright(crank_angle)) difference() { circle(d = 18); square_bore(); } } if (output == "all" || output == "linkage") { // Two plies for each lambda step(build_steps[3]) part("Eccentric", qty = 8, at = [for (k = [0:3], i = [0:1]) [0, face(k, s_of("eccentric") + i * t), H]], rotate = [for (k = [0:3], i = [0:1]) upright(turn(k))]) eccentric(); step(build_steps[3]) part("Coupler", qty = 8, at = [for (k = [0:3], i = [0:1]) at_side(world(k)[0], face(k, s_of("eccentric") + i * t))], rotate = [for (k = [0:3], i = [0:1]) let (w = world(k)) upright(heading(w[0], w[2]))]) coupler(); step(build_steps[2]) part("Rocker", qty = 4, at = [for (k = [0:3]) at_side(D, face(k, s_of("rocker")))], rotate = [for (k = [0:3]) upright(heading(D, world(k)[1]))]) rocker(); } if (output == "all" || output == "legs") step(build_steps[4]) part("Leg", qty = 8, at = [for (k = [0:3], i = [0:1]) at_side(world(k)[2], face(k, s_of("leg") + i * t))], rotate = upright()) leg(); if (output == "all" || output == "accessories") { step(build_steps[1]) part("Thrust collar", qty = 4, at = [for (k = [0:3]) [0, face(k, s_of("collar")), H]], rotate = [for (k = [0:3]) upright(turn(k))]) difference() { circle(d = 26); square_bore(); } // At each D, C and P, between the layers step(build_steps[2]) part("Spacer ring", qty = 12, at = [for (k = [0:3], j = [0:2]) at_side([D, world(k)[1], world(k)[2]][j], face(k, s_of(["ring D", "ring C", "ring P"][j])))], rotate = upright()) disc(12, bush); // On the right end of the shaft, outside the right leg step(build_steps[5]) part("Handle", qty = 2, at = [for (i = [0:1]) [0, face(0, s_of("handle") + i * t), H]], rotate = upright(handle_turn)) handle(); step(build_steps[5]) part("Handle grip", qty = 5, at = [for (i = [0:4]) at_side(grip, face(0, s_of("handle") + 2 * t + 0.5 + i * t))], rotate = upright()) disc(18, bush); } if (output == "all" || output == "coupon") part("Fit test", at = [-50, 100, 0]) coupon(); echo(str("Lambda: feet move ", round(max(flat) - min(flat)), " mm level to 0.2 mm for ", len(flat) * 2, " degrees of each turn and lift ", round((high - low) * 10) / 10, " mm; square shaft ", frame_gap + 14 * t + 25, " mm, body ", x1 - x0, " mm long")); // Assembly, with s the distance out from a frame face (outside or inside): // glue each frame from two plies, the eccentrics, couplers and legs in pairs, // the handle from two. Slot the end plates into the frames, front plate // forward. Each frame carries two journals in its round hole and a thrust // collar on each face, all on the 6 mm square steel shaft. A 6 mm brass bush // on an M4 bolt at D carries a rocker on each side of the frame, s 4 to 7, // a spacer ring under each. The eccentrics sit at s 8 + t to 8 + 3t: right // inside half a turn from right outside, left outside a quarter turn on from // right outside, left inside half a turn from that. Each coupler rides on its // eccentric and is bolted to its rocker at C over a spacer ring. The legs // hang at s 12 + 3t to 12 + 5t from P, on an M4 bolt, brass bush and spacer // ring. The handle goes on the right end of the shaft at s 17 + 5t, its grip // discs on a bush. Thin washers take up the last millimetre in each gap. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/crawl/ # Crawl spider walker Crawl: an eight-legged walker on Klann linkages. Each leg is a six-bar linkage: the crank drives a connecting arm held by a lower rocker, and the arm swings the leg, held at the top by an upper rocker. Turn the handle and each foot follows a walking path, then lifts and reaches forward. The legs are phased to support the body through the crank cycle. This design has 121 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=crawl). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/crawl/crawl.kerf) - [crawl-sheet-1.svg](https://kerfscript.mylasertools.com/models/crawl/crawl-sheet-1.svg) - [crawl-sheet-1.dxf](https://kerfscript.mylasertools.com/models/crawl/crawl-sheet-1.dxf) - [crawl-sheet-2.svg](https://kerfscript.mylasertools.com/models/crawl/crawl-sheet-2.svg) - [crawl-sheet-2.dxf](https://kerfscript.mylasertools.com/models/crawl/crawl-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Side plate | 2 | 122.4 × 91.4 mm | | Outer plate | 2 | 116.4 × 85.4 mm | | Crank disc | 12 | 58.9 × 58.9 mm | | Journal | 4 | 18 × 18 mm | | Hub ring | 15 | 14 × 14 mm | | Handle crank | 2 | 55 × 22 mm | | Handle grip | 6 | 18 × 18 mm | | End cap | 1 | 24 × 24 mm | | Connecting arm | 8 | 101.9 × 23.3 mm | | Leg | 8 | 147.8 × 49.9 mm | | Upper rocker | 8 | 55.4 × 14 mm | | Lower rocker | 8 | 39.7 × 14 mm | | Spacer ring | 44 | 11 × 11 mm | | Fit test | 1 | 120 × 48 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `unit` | `80` | 76 to 84 | | `link_width` | `10` | 9 to 10 | | `pivot_clearance` | `0.3` | 0.2 to 0.4 | | `shaft_fit` | `0.15` | 0.05 to 0.3 | | `journal_clearance` | `0.3` | 0.2 to 0.5 | | `crank_angle` | `90` | 0 to 360 | | `output` | `"all"` | All parts, Plates, Crank discs journals and handle, Legs and rockers, Spacer rings, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Crawl: an eight-legged walker on Klann linkages. Each leg is a six-bar // linkage: the crank drives a connecting arm held by a lower rocker, and the // arm swings the leg, held at the top by an upper rocker. Turn the handle and // each foot follows a walking path, then lifts and reaches forward. // The legs are phased to support the body through the crank cycle. /* [Legs] */ // Size: every length of the Klann linkage is this many mm per patent unit unit = 80; // [76:1:84] // Width of the links in mm link_width = 10; // [9:0.5:10] /* [Fit] */ // Added to the 6 mm holes the brass sleeves turn in pivot_clearance = 0.3; // [0.2:0.05:0.4] // Added to the square holes for the 6 mm square bar shaft_fit = 0.15; // [0.05:0.05:0.3] // Added to the 18 mm bores the journals turn in journal_clearance = 0.3; // [0.2:0.05:0.5] /* [Assembly] */ // Crank angle in the assembled view: 0 to 180 the outer feet are down, 180 to 360 the inner ones crank_angle = 90; // [0:15:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Plates, crank:Crank discs journals and handle, legs:Legs and rockers, spacers:Spacer rings, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 72; t = $thickness; u = unit; w = link_width / 2; boss = 7; hole = 6 + pivot_clearance; assert(u >= 76 && u <= 84, "Size: 76 to 84 mm per unit"); assert(link_width >= 9 && link_width <= 10, "Link width: 9 to 10 mm"); assert(pivot_clearance >= 0.2 && pivot_clearance <= 0.4, "Pivot clearance: 0.2 to 0.4 mm"); assert(shaft_fit >= 0.05 && shaft_fit <= 0.3, "Shaft fit: 0.05 to 0.3 mm"); assert(journal_clearance >= 0.2 && journal_clearance <= 0.5, "Journal clearance: 0.2 to 0.5 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "frame", "crank", "legs", "spacers", "coupon"], output), "Unknown output"); // Klann's leg in the patent's units, in its fully extended pose: crank // axle O, the two rocker axles, then the crank pin P, elbow E, knee K, hip H // and foot F pO = [1.599, 0.75]; pA = [1.366, 1.366]; pB = [1.009, 0.574]; pP = [1.331, 0.75]; pE = [0.741, 0.75]; pK = [0.232, 0.866]; pH = [0.866, 1.5]; pF = [0, 0]; // In mm about the crank axle, turned to face forward (+x) function mm(p) = [pO[0] - p[0], p[1] - pO[1]] * u; A = mm(pA); B = mm(pB); crank = norm(mm(pP)); function polar(rad, a) = [rad * cos(a), rad * sin(a)]; function meet(p, rad, q, rad2, sgn) = let (v = q - p, d = norm(v), a = (rad * rad - rad2 * rad2 + d * d) / (2 * d), h = sqrt(max(0, rad * rad - a * a))) p + v * a / d + [-v[1], v[0]] * h / d * sgn; // Where c sits seen from a, with b straight along +x function canon(a, b, c) = let (v = b - a, g = atan2(v[1], v[0]), q = c - a) [q[0] * cos(g) + q[1] * sin(g), q[1] * cos(g) - q[0] * sin(g)]; // The joints with the crank pin at angle a: E, K, H, F function leg(a) = let ( P = polar(crank, a), E = meet(P, norm(mm(pE) - mm(pP)), B, norm(mm(pE) - B), 1), K = meet(P, norm(mm(pK) - mm(pP)), E, norm(mm(pK) - mm(pE)), 1), H = meet(K, norm(mm(pH) - mm(pK)), A, norm(mm(pH) - A), -1), F = meet(K, norm(mm(pF) - mm(pK)), H, norm(mm(pF) - mm(pH)), 1)) [E, K, H, F]; // P, E, K, H, F, A, B of a front leg, or of a rear leg (its mirror image on // the same crank pin) function joints(a, rear = false) = let (j = concat([polar(crank, rear ? 180 - a : a)], leg(rear ? 180 - a : a), [A, B])) rear ? [for (p = j) [-p[0], p[1]]] : j; iP = 0; iE = 1; iK = 2; iH = 3; iF = 4; iA = 5; iB = 6; path = [for (a = [0:3:357]) leg(a)[3]]; stance = [for (i = [0:60]) path[i]]; low = min([for (p = path) p[1]]); stride = max([for (p = stance) p[0]]) - min([for (p = stance) p[0]]); lift = max([for (p = path) p[1]]) - max([for (p = stance) p[1]]); module bar(a, b) { hull() { translate(a) circle(r = w); translate(b) circle(r = w); } } // A link through its joints, with a hole at each joint in holes module link(pts, holes, name) { difference() { union() { for (i = [0:len(pts) - 2]) bar(pts[i], pts[i + 1]); for (p = pts) translate(p) circle(r = boss); } for (p = holes) translate(p) circle(d = hole); } // Along the first bar, never upside down score() let (a = pts[0], b = pts[1], g = atan2(b[1] - a[1], b[0] - a[0])) translate((a + b) / 2) rotate(abs(g) > 90 ? g + 180 : g) text(name, size = 3.2, halign = "center", valign = "center"); } // Each drawn with its first joint at the origin and its second along +x cP = [0, 0]; cE = [norm(mm(pE) - mm(pP)), 0]; cK = canon(mm(pP), mm(pE), mm(pK)); gK = [0, 0]; gH = [norm(mm(pH) - mm(pK)), 0]; gF = canon(mm(pK), mm(pH), mm(pF)); module square_bore() { rect([6 + shaft_fit, 6 + shaft_fit], center = true); } module plate(r) { difference() { hull() { for (s = [-1, 1]) { translate([s * A[0], A[1]]) circle(r = r); translate([s * B[0], B[1]]) circle(r = r); } circle(r = 16); } for (s = [-1, 1]) { translate([s * A[0], A[1]]) circle(d = 4.4); translate([s * B[0], B[1]]) circle(d = 4.4); } circle(d = 18 + journal_clearance); } } module ring(od, inner) { difference() { circle(d = od); circle(d = inner); } } module hub(od) { difference() { circle(d = od); square_bore(); } } // Assembled view: x forward, z up, y across, seen from the right (-y) as // the parts are drawn. Four groups of a front and a rear leg, each on one // crank pin: outside and inside each side plate. Across (y) a group is its // crank disc, a 5 mm gap, then four layers with 4 mm gaps for the nuts: // front rockers; front leg and rear arm; rear leg and front arm; rear // rockers; and 5 mm to the next part. lay is each layer's offset from the disc. lay = [5, 9 + t, 13 + 2 * t, 17 + 3 * t]; depth = 22 + 4 * t; d_in = 10 + depth; d_out = d_in + 9 * t; top = d_out + depth + 5 * t; // Handle radius: its bolt head turns just inside the tie rod nuts handle = 36; // Disc face, which way the group runs from it, and the crank phase: the // inner groups are half a turn behind the outer ones groups = [[d_out, 1, 0], [d_in, -1, 180], [-d_out, -1, 0], [-d_in, 1, 180]]; function gj(g, rear) = joints(crank_angle + groups[g][2], rear); // Height of the crank axle: the lowest foot pads stand on the floor ride = boss - min([for (g = [0:3], r = [false, true]) gj(g, r)[iF][1]]); // Low side of the layer s mm from group g's disc function lo(g, s) = groups[g][1] > 0 ? groups[g][0] + s : groups[g][0] - s - t; // A part lying in the side view, its point [0, 0] at p, turned a degrees, // its sheet from y to y + t; flipped over, it is its own mirror image function side_at(p, y, flipped = false) = [p[0], flipped ? y : y + t, ride + p[1]]; function side_rot(a, flipped = false) = [flipped ? -90 : 90, -a, 0]; // Eight links, two per group: [at, rotate] drawn from joint i1 towards i2, // on layer lf for the front leg and lr for the rear one function links(i1, i2, lf, lr) = [for (c = [0:7]) let (g = floor(c / 2), r = c % 2 == 1, j = gj(g, r), a = j[i1], b = j[i2]) [side_at(a, lo(g, lay[r ? lr : lf]), r), side_rot(atan2(b[1] - a[1], b[0] - a[0]), r)]]; // Spacer rings on the joints: joint, rear leg, offset of the ring's near face rings = [for (g = [0:3], q = [[iE, false, lay[0] + t], [iE, false, lay[1]], [iE, false, lay[1] + t], [iE, true, lay[1] + t], [iE, true, lay[2]], [iE, true, lay[2] + t], [iH, false, lay[0] + t], [iK, false, lay[1] + t], [iK, true, lay[1] + t], [iH, true, lay[2] + t], [iP, false, lay[1] + t]]) [side_at(gj(g, q[1])[q[0]], lo(g, q[2])), side_rot(0)]]; // Turning with the crank: y of each crank disc ply and its phase, the hub rings discs = [for (s = [1, -1], q = [[d_in, 180], [d_in + 7 * t, 0], [d_out + depth, 0]], k = [0, 1]) [s * (q[0] + k * t) - (s < 0 ? t : 0), q[1]]]; hubs = concat([for (s = [1, -1], y = [d_in + 2 * t, d_in + 5 * t, d_out + depth + 2 * t], k = [0, 1]) s * (y + k * t) - (s < 0 ? t : 0)], [top, -top - t, -top - 2 * t]); O = [0, 0]; plates = [d_in + 4 * t, -d_in - 5 * t]; outers = [d_out + depth + 4 * t, -d_out - depth - 5 * t]; // On the left (+y) a plate is turned round so its face looks outward function plate_at(y) = [0, y > 0 ? y : y + t, ride]; function plate_rot(y) = y > 0 ? [90, 0, 180] : [90, 0, 0]; if (output == "all" || output == "frame") step("Bolt the side plates and the outer plates on the four tie rods") { part("Side plate", qty = 2, at = [for (y = plates) plate_at(y)], rotate = [for (y = plates) plate_rot(y)]) { plate(14); score() translate([0, -26]) text("SIDE PLATE", size = 4, halign = "center"); } // Both faces outward, the name the right way round on each side part("Outer plate", qty = 2, at = [for (y = outers) plate_at(y)], rotate = [for (y = outers) plate_rot(y)]) { plate(11); score() translate([0, 30]) text("CRAWL", size = 9, halign = "center"); score() translate([0, -21]) text("KLANN LINKAGE", size = 3.5, halign = "center"); } } if (output == "all" || output == "crank") step("Build the crank on the square bar between the plates, with the end cap on the left and the handle on the right") { // Glued in pairs; the crank pin hole is for the M4 rod part("Crank disc", qty = 12, at = [for (q = discs) side_at(O, q[0])], rotate = [for (q = discs) side_rot(crank_angle + q[1])]) { difference() { circle(r = crank + 8); square_bore(); translate([crank, 0]) circle(d = 4.4); } score() line([5, 0], [crank - 5, 0]); } part("Journal", qty = 4, at = [for (y = [plates[0], outers[0], plates[1], outers[1]]) side_at(O, y)], rotate = side_rot(crank_angle)) hub(18); part("Hub ring", qty = 15, at = [for (y = hubs) side_at(O, y)], rotate = side_rot(crank_angle)) hub(14); // The handle is on the right, outside the hub rings and the tie rod nuts, // keyed a quarter turn behind the outer crank pins part("Handle crank", qty = 2, at = [for (k = [1, 2]) side_at(O, -top - (2 + k) * t)], rotate = side_rot(crank_angle - 90)) difference() { hull() { circle(r = 11); translate([handle, 0]) circle(r = 8); } square_bore(); translate([handle, 0]) circle(d = 4.4); } part("Handle grip", qty = 6, at = [for (k = [1:6]) side_at(polar(handle, crank_angle - 90), -top - (4 + k) * t)], rotate = side_rot(crank_angle - 90)) ring(18, hole); part("End cap", at = side_at(O, top + t), rotate = side_rot(crank_angle)) hub(24); } if (output == "all" || output == "legs") step("Hang the rockers, the legs and the arms beside each crank disc, with spacer rings in the gaps") { arms = links(iP, iE, 2, 1); part("Connecting arm", qty = 8, at = [for (q = arms) q[0]], rotate = [for (q = arms) q[1]]) link([cP, cE, cK], [cP, cE, cK], "ARM"); legs = links(iK, iH, 1, 2); part("Leg", qty = 8, at = [for (q = legs) q[0]], rotate = [for (q = legs) q[1]]) link([gH, gK, gF], [gH, gK], "LEG"); uppers = links(iA, iH, 0, 3); part("Upper rocker", qty = 8, at = [for (q = uppers) q[0]], rotate = [for (q = uppers) q[1]]) link([[0, 0], [norm(mm(pH) - A), 0]], [[0, 0], [norm(mm(pH) - A), 0]], "UPPER"); lowers = links(iB, iE, 0, 3); part("Lower rocker", qty = 8, at = [for (q = lowers) q[0]], rotate = [for (q = lowers) q[1]]) link([[0, 0], [norm(mm(pE) - B), 0]], [[0, 0], [norm(mm(pE) - B), 0]], "LOW"); } if (output == "all" || output == "spacers") step("Hang the rockers, the legs and the arms beside each crank disc, with spacer rings in the gaps") part("Spacer ring", qty = 44, at = [for (q = rings) q[0]], rotate = [for (q = rings) q[1]]) ring(11, hole); if (output == "all" || output == "coupon") part("Fit test", at = [150, -24, 0], rotate = [0, 0, 0]) { difference() { rect([120, 48], r = 3); for (i = [0:3]) translate([14 + 20 * i, 14]) circle(d = 6.1 + 0.1 * i); translate([96, 14]) circle(d = 4.4); translate([18, 34]) square_bore(); translate([45, 32]) circle(d = 18 + journal_clearance); } for (i = [0:3]) score() translate([14 + 20 * i, 3]) text(str(6.1 + 0.1 * i), size = 3, halign = "center"); } echo(str("Crawl: stride ", round(stride), " mm, lift ", round(lift), " mm, crank ", round(crank * 10) / 10, " mm; crank axle ", round(boss - low), " mm up at most; ", round(2 * (max([for (p = path) p[0]]) + boss)), " mm long, ", round(2 * top), " mm across the outer plates")); // Assembly, across the walker from the middle (3 mm sheet): side plates at // 56 mm and outer plates at 117 mm on four M4 threaded rods 250 mm long at // the rocker axles, a washer and nut each side of every plate. The crank is // built up from parts on 6 mm square bar. On each side a 27 mm bar turns in // a journal in the side plate, two hub rings each side, with a crank disc of // two glued plies on each end; a short bar through each outer plate's journal // carries the outer disc and, outside, a hub ring and the end cap on the // left, or two hub rings and the handle crank on the right. M4 rods through // the discs, nutted each side of each disc, are the crank pins: one 106 mm // across the inside, half a turn round from the two outer ones. Beside each // disc is a group of two legs: a 5 mm gap, the front upper and lower rockers, // front leg and rear arm, rear leg and front arm, the rear rockers, 5 mm to // the next part, with 4 mm gaps between layers for the nuts. Each rocker // turns on a brass sleeve t + 0.2 long clamped by washers and nuts on its // tie rod; both arms turn on one sleeve on the crank pin, nutted each side, // with a spacer ring between. The hips and knees are 6 mm brass sleeves on // M4 x 20 bolts, the elbows on M4 x 25 bolts with their heads towards the // disc; fill each 4 mm gap a joint crosses with a spacer ring and a 1 mm // washer, and the layer an elbow crosses with a ring. Turn the handle with // its top going backwards and Crawl walks forwards. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/inversor/ # Inversor straight-line linkage Inversor: Peaucellier and Lipkin's linkage of 1864, the first to turn a circle into an exactly straight line. Two long arms from a fixed pivot hold a rhombus of four short links; swing the handle on the input link and the pencil at the far corner draws a ruler-straight line along the scored scale. This design has 53 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=inversor). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/inversor/inversor.kerf) - [SVG](https://kerfscript.mylasertools.com/models/inversor/inversor.svg) - [DXF](https://kerfscript.mylasertools.com/models/inversor/inversor.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 255 × 240 mm | | Foot | 8 | 20 × 20 mm | | Input link | 1 | 69 × 14 mm | | Arm | 2 | 164 × 14 mm | | Short link | 2 | 74 × 14 mm | | Pen link | 2 | 74 × 14 mm | | Post ring | 8 | 16 × 16 mm | | Spacer ring | 9 | 12 × 12 mm | | Glide | 3 | 14 × 14 mm | | Glide ring | 4 | 14 × 14 mm | | Pencil sleeve | 7 | 14 × 14 mm | | Handle grip | 4 | 16 × 16 mm | | Fit test | 1 | 140 × 48 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `rhombus_side` | `60` | 55 to 65 | | `line_length` | `140` | 100 to 160 | | `pivot_clearance` | `0.3` | 0.2 to 0.4 | | `input_angle` | `30` | -44 to 44 | | `output` | `"all"` | All parts, Base and feet, Links, Posts glides and spacers, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Inversor: Peaucellier and Lipkin's linkage of 1864, the first to turn a // circle into an exactly straight line. Two long arms from a fixed pivot hold // a rhombus of four short links; swing the handle on the input link and the // pencil at the far corner draws a ruler-straight line along the scored scale. /* [Linkage] */ // Side of the rhombus; the arms stay 150 mm and the line moves to suit rhombus_side = 60; // [55:1:65] // Length of the straight line between the two stops line_length = 140; // [100:10:160] /* [Fit] */ // Added to the 6 mm holes the brass bushes turn in pivot_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // The input link's angle in the assembled view, from the line O to Q; it // stops at the ends of the line input_angle = 30; // [-44:1:44] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, base:Base and feet, links:Links, spacers:Posts glides and spacers, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; s = rhombus_side; // Arms from O, and the input link, which turns about Q with OQ = QA arm = 150; crank = 55; hole = 6 + pivot_clearance; pencil = 8.4; // OA x OP = arm^2 - s^2 whatever the pose, so A's circle through O becomes // the line x = line_x line_x = (arm * arm - s * s) / (2 * crank); half = line_length / 2; // The input link's swing either side of the axis, and the dowel stops swing = 2 * atan(half / line_x); stop_r = 35; stop_a = swing + asin(10 / stop_r); // The base, with the fixed pivot O on it W = 255; D = 240; O = [35, D / 2]; Q = O + [crank, 0]; assert(s >= 55 && s <= 65, "Rhombus side: 55 to 65 mm"); assert(line_length >= 100 && line_length <= 160, "Line length: 100 to 160 mm"); assert(pivot_clearance >= 0.2 && pivot_clearance <= 0.4, "Pivot clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(input_angle >= -44 && input_angle <= 44, "Input angle: -44 to 44 degrees"); assert(contains(["all", "base", "links", "spacers", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; // Where circles round p (radius r1) and q (radius r2) cross; sgn picks the side function meet(p, r1, q, r2, sgn) = let (v = q - p, d = norm(v), a = (r1 * r1 - r2 * r2 + d * d) / (2 * d), h = sqrt(max(0, r1 * r1 - a * a))) p + v * a / d + [-v[1], v[0]] * h / d * sgn; // Joints A, B, D, P with the input link at angle phi, relative to O. P is A // reflected across BD, the far corner of the rhombus. function joints(phi) = let ( A = [crank, 0] + polar(crank, phi), B = meet([0, 0], arm, A, s, 1), Dj = meet([0, 0], arm, A, s, -1)) [A, B, Dj, B + Dj - A]; module label(txt, p, size = 4, h = "center") { score() translate(p) text(txt, size = size, halign = h, valign = "center"); } // A link drawn along x, holes at 0 and len; the second hole can be the pencil's module link(len, name, end = hole) { difference() { hull() { circle(r = 7); translate([len, 0]) circle(r = 7); } circle(d = hole); translate([len, 0]) circle(d = end); } label(name, [len / 2, 0], 4); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } module base() { difference() { rect([W, D], r = 8); // M4 bolts up through the fixed pivots, 6 mm dowels for the stops for (p = [O, Q]) translate(p) circle(d = 4.4); for (a = [-stop_a, stop_a]) translate(Q + polar(stop_r, a)) circle(d = 6); } // The scale: the line the pencil draws, a tick every 5 mm, numbered every 20 X = O[0] + line_x; score() { line([X, O[1] - half], [X, O[1] + half]); for (y = [-half, half]) line([X - 6, O[1] + y], [X + 6, O[1] + y]); for (i = [0:5:line_length]) line([X + 12, O[1] - half + i], [X + (i % 10 == 0 ? 17 : 14.5), O[1] - half + i]); // The circle the input corner A follows, through O polyline([for (a = [-swing:2:swing]) Q + polar(crank, a)]); } for (i = [0:20:line_length]) label(str(i), [X + 19, O[1] - half + i], 3.5, "left"); label("O", O + [-15, 0], 5); label("Q", Q + [0, -14], 5); label("INVERSOR", [52, 34], 14); label("PEAUCELLIER AND LIPKIN, 1864", [52, 20], 4); label("A CIRCLE THROUGH O DRAWS A STRAIGHT LINE", [52, 13], 3); } // Spacer stacks on each joint, bottom to top, from the base spacer_rings = 9; glide_rings = 4; sleeves = 7; post_rings = 8; // The assembled view. Heights h are above the base, whose top is at 2t; the // links sit on five layers: input link (h t), A-B and D-P (3t), A-D and B-P // (5t), arm O-B (7t), arm O-D (9t). pose = joints(max(-swing, min(swing, input_angle))); function joint(k) = k == "O" ? O : k == "Q" ? Q : O + pose[search(k, ["A", "B", "D", "P"])]; function lift(k, h) = concat(joint(k), [2 * t + h]); // A link lies from joint a towards joint b function heading(a, b) = let (v = joint(b) - joint(a)) [0, 0, atan2(v[1], v[0])]; corners = [[14, 14], [W - 14, 14], [14, D - 14], [W - 14, D - 14]]; handle_h = 6 * t + 3.7; // The assembly steps in build order, named first so they number that way build_steps = [ "Glue the two base plies together and the feet under the corners", "Stand the glides and post rings on the base", "Stack the links on their joints, with a spacer ring between each pair", "Glue the pencil sleeves on at P and the handle grips on at A" ]; if (output == "all") for (k = build_steps) step(k) {} if (output == "all" || output == "base") { step(build_steps[0]) part("Base", qty = 2, at = [[0, 0, 0], [0, 0, t]]) base(); // Two under each corner step(build_steps[0]) part("Foot", qty = 8, at = [for (c = corners, k = [1:2]) concat(c, [-k * t])]) circle(d = 20); } if (output == "all" || output == "links") { step(build_steps[2]) part("Input link", at = lift("Q", t), rotate = heading("Q", "A")) link(crank, "INPUT Q-A"); step(build_steps[2]) part("Arm", qty = 2, at = [lift("O", 7 * t), lift("O", 9 * t)], rotate = [heading("O", "B"), heading("O", "D")]) link(arm, "ARM O"); step(build_steps[2]) part("Short link", qty = 2, at = [lift("A", 3 * t), lift("A", 5 * t)], rotate = [heading("A", "B"), heading("A", "D")]) link(s, "A"); step(build_steps[2]) part("Pen link", qty = 2, at = [lift("D", 3 * t), lift("B", 5 * t)], rotate = [heading("D", "P"), heading("B", "P")]) link(s, "PEN", pencil); } if (output == "all" || output == "spacers") { step(build_steps[1]) part("Post ring", qty = post_rings, at = concat([for (h = [0:6]) lift("O", h * t)], [lift("Q", 0)])) ring(16, hole); step(build_steps[2]) part("Spacer ring", qty = spacer_rings, at = [lift("A", 2 * t), lift("A", 4 * t), lift("B", 4 * t), lift("B", 6 * t), lift("D", 4 * t), lift("D", 6 * t), lift("D", 7 * t), lift("D", 8 * t), lift("O", 8 * t)]) ring(12, hole); // The bottom glide under A, B and D holds the bolt head in its bigger hole step(build_steps[1]) part("Glide", qty = 3, at = [lift("A", 0), lift("B", 0), lift("D", 0)]) ring(14, 8.4); step(build_steps[1]) part("Glide ring", qty = glide_rings, at = [lift("B", t), lift("B", 2 * t), lift("D", t), lift("D", 2 * t)]) ring(14, hole); step(build_steps[3]) part("Pencil sleeve", qty = sleeves, at = [for (h = [0, 1, 2, 4, 6, 7, 8]) lift("P", h * t)]) ring(14, pencil); step(build_steps[3]) part("Handle grip", qty = 4, at = [for (k = [0:3]) lift("A", handle_h + k * t)]) ring(16, hole); } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [W + 20, 0, -2 * t]) { difference() { rect([140, 48], r = 3); for (i = [0:3]) translate([15 + 25 * i, 16]) circle(d = 6.1 + 0.1 * i); translate([118, 16]) circle(d = pencil); translate([20, 36]) circle(d = 4.4); for (i = [0:1]) translate([45 + 20 * i, 36]) circle(d = 5.9 + 0.1 * i); } for (i = [0:3]) label(str(6.1 + 0.1 * i), [15 + 25 * i, 5], 3); label("PEN", [118, 5], 3); label("DOWEL", [55, 44], 3); } echo(str("Inversor: ", line_length, " mm line ", round(line_x * 10) / 10, " mm from O; input link swings ", round(swing * 10) / 10, " degrees each way; base ", W, " x ", D, " mm")); // Assembly, with heights h above the base: glue the two base plies and the // feet in pairs under the corners. Fixed pivots are M4 bolts up through the // base on 6 mm brass bushes: at Q one post ring under the input link (h t), at // O seven post rings under the two arms (h 7t and 9t). Glue the 6 mm dowel // stops in, t + 1 mm proud. The moving joints are M4 bolts in brass bushes, // heads in the glides, washer and nut on top; every glide stands on the base. // A: glide, input link, ring, short link (h 3t), ring, short link (h 5t), nut, // then a bush with the four handle grips and a second nut on a 50 mm bolt. B: // glide and two glide rings, short link, ring, pen link (h 5t), ring, arm. D: // glide and two glide rings, pen link (h 3t), ring, short link, three rings, // arm (h 9t). At P a pencil stub is the pin: three sleeves under the lower pen // link, one between the pen links and three on top, glued to their links, tip // on the paper. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/echo/ # Echo pantograph Echo: a pantograph for copying a drawing larger. Four arms make a hinged parallelogram anchored to the board; trace the original with the stylus and the pencil draws it again, 1.5 to 4 times the size. Move the stylus and the pencil to the holes engraved with the same ratio. To copy smaller, swap them. This design has 19 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=echo). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/echo/echo.kerf) - [echo-sheet-1.svg](https://kerfscript.mylasertools.com/models/echo/echo-sheet-1.svg) - [echo-sheet-1.dxf](https://kerfscript.mylasertools.com/models/echo/echo-sheet-1.dxf) - [echo-sheet-2.svg](https://kerfscript.mylasertools.com/models/echo/echo-sheet-2.svg) - [echo-sheet-2.dxf](https://kerfscript.mylasertools.com/models/echo/echo-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Anchor arm | 1 | 264 × 24 mm | | Pencil arm | 1 | 324 × 24 mm | | Stylus arm | 1 | 264 × 24 mm | | Link | 1 | 96 × 24 mm | | Base board | 1 | 560 × 380 mm | | Nut block | 1 | 44 × 44 mm | | Anchor block | 1 | 44 × 44 mm | | Glide foot | 6 | 20 × 20 mm | | Stylus collar | 2 | 14 × 14 mm | | Pencil guide | 3 | 14 × 14 mm | | Fit test | 1 | 130 × 42 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `ratio` | `2.5` | 1.5x, 2x, 2.5x, 3x, 4x | | `pivot_clearance` | `0.3` | 0.2 to 0.4 | | `pencil_hole` | `8.2` | 7.8 to 8.6 | | `stylus_x` | `0.5` | 0 to 1 | | `stylus_y` | `0.8` | 0 to 1 | | `output` | `"all"` | All parts, Arms and link, Base board and anchor, Feet and collars, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Echo: a pantograph for copying a drawing larger. Four arms make a hinged // parallelogram anchored to the board; trace the original with the stylus and // the pencil draws it again, 1.5 to 4 times the size. Move the stylus and the // pencil to the holes engraved with the same ratio. To copy smaller, swap them. /* [Scale] */ // The ratio the board marks are drawn for; every arm has holes for all five ratio = 2.5; // [1.5:1.5x, 2:2x, 2.5:2.5x, 3:3x, 4:4x] /* [Fit] */ // Added to the 6 mm holes the brass sleeves of the joints turn in pivot_clearance = 0.3; // [0.2:0.05:0.4] // Holes for the pencil and the 8 mm dowel stylus; the pencil should drop freely pencil_hole = 8.2; // [7.8:0.1:8.6] /* [Assembly] */ // Where the stylus is in the assembled view, across the original's square // from left to right stylus_x = 0.5; // [0:0.05:1] // And from front to back stylus_y = 0.8; // [0:0.05:1] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, arms:Arms and link, board:Base board and anchor, small:Feet and collars, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; // Anchor arm and stylus arm, joint to joint; link, and the pencil arm from // the elbow to the joint with the stylus arm a = 240; n = 72; // Arm width w = 24; hole = 6 + pivot_clearance; ratios = [1.5, 2, 2.5, 3, 4]; // Base board, and the anchor on it board = [560, 380]; anchor = [40, 115]; // The original's square for each ratio, [ratio, centre x, centre y, side]: // the largest found where the parallelogram never closes past 30 degrees, // every joint stays on the board and the copy clears the original zones = [[1.5, 230, 112, 64], [2, 180, 115, 82], [2.5, 152, 119, 86], [3, 138, 122, 80], [4, 132, 120, 58]]; assert(contains(ratios, ratio), "Ratio: 1.5, 2, 2.5, 3 or 4"); assert(pivot_clearance >= 0.2 && pivot_clearance <= 0.4, "Pivot clearance: 0.2 to 0.4 mm"); assert(pencil_hole >= 7.8 && pencil_hole <= 8.6, "Pencil hole: 7.8 to 8.6 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(stylus_x >= 0 && stylus_x <= 1 && stylus_y >= 0 && stylus_y <= 1, "Stylus position: 0 to 1 each way"); assert(contains(["all", "arms", "board", "small", "coupon"], output), "Unknown output"); // With the link n long, the anchor O, the elbow B, the joints C and X make a // parallelogram. Put the pencil k n from B and the stylus a / k from X: // then stylus = O + (pencil - O) / k, on the line from the anchor to the // pencil and k times nearer, at every angle of the arms. function pencil_at(k) = k * n; function stylus_at(k) = a / k; function ratio_name(k) = str(k, "x"); zone = zones[search(ratio, ratios)]; copy_side = ratio * zone[3]; copy_centre = anchor + ([zone[1], zone[2]] - anchor) * ratio; // The assembled view, z up from the top of the board. The stylus arm (a / k // from X to the stylus) and the link (n) are two links from the anchor to // the stylus; their angles give the whole parallelogram. stylus = [zone[1], zone[2]] + ([stylus_x, stylus_y] - [0.5, 0.5]) * zone[3]; ik = let (p = a / ratio, d = stylus - anchor, r = norm(d), phi = acos((r * r - p * p - n * n) / (2 * p * n)), ua = atan2(d[1], d[0]) - atan2(-n * sin(phi), p + n * cos(phi))) [ua, ua - phi]; u = [cos(ik[0]), sin(ik[0])]; v = [cos(ik[1]), sin(ik[1])]; elbow = anchor + a * u; joint_x = anchor + n * v; joint_c = elbow + n * v; pencil = elbow + ratio * n * v; // The anchor arm and stylus arm run on the glide feet, the link and the // pencil arm above them, over the M6 washers low = 2 * t; high = 3 * t + 0.5; function at(p, z) = concat(p, [z]); module label(s, p, size = 3.5) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module annulus(od, id) { difference() { circle(d = od); circle(d = id); } } // An arm along x from x0 to x1, joint holes at joints, pencil or stylus holes // at rows with their ratio engraved beside each module arm(x0, x1, joints, rows = [], name = "", name_at = 0) { difference() { hull() { translate([x0, 0]) circle(d = w); translate([x1, 0]) circle(d = w); } for (x = joints) translate([x, 0]) circle(d = hole); for (r = rows) translate([r[1], 0]) circle(d = pencil_hole); } for (r = rows) label(ratio_name(r[0]), [r[1], 8.6]); if (name != "") label(name, [name_at, 0], 4.5); } module anchor_arm() { arm(0, a, [0, a]); label("ECHO", [a / 2, 0], 11); label("ANCHOR", [a / 2 + 54, 0], 4); } module pencil_arm() arm(0, pencil_at(4) + 12, [0, n], [for (k = ratios) [k, pencil_at(k)]], "PENCIL", (pencil_at(3) + pencil_at(4)) / 2); module stylus_arm() arm(0, a, [0, a], [for (k = ratios) [k, stylus_at(k)]], "STYLUS", (stylus_at(1.5) + a) / 2); module link() arm(0, n, [0, n], [], "LINK", n / 2); // L marks just outside the corners of a square of side s centred at c module corners(c, s) { score() for (i = [-1, 1], j = [-1, 1]) let (p = c + [i, j] * (s / 2 + 2)) { line(p, p + [-8 * i, 0]); line(p, p + [0, -8 * j]); } } // The board is drawn as seen from above. The anchor nut sits in a hex hole // through the board and the lower anchor block; the marks show where to tape // the original and the copy paper for the chosen ratio. module base_board() { difference() { rect(board, r = 10); translate(anchor) circle(d = 8.2, $fn = 6); } corners([zone[1], zone[2]], zone[3]); label("ORIGINAL", [zone[1], zone[2] + zone[3] / 2 + 9], 4); corners(copy_centre, copy_side); label(str("COPY ", ratio_name(ratio)), [copy_centre.x, copy_centre.y + copy_side / 2 + 9], 4); label("ECHO", [440, 338], 24); label("PANTOGRAPH 1.5x TO 4x", [440, 314], 6); } module coupon() { difference() { rect([130, 42], r = 3); for (i = [0:2]) translate([15 + 20 * i, 14]) circle(d = 6.2 + 0.1 * i); for (i = [0:2]) translate([75 + 20 * i, 14]) circle(d = 8.1 + 0.1 * i); translate([15, 32]) circle(d = 4.4); translate([40, 32]) circle(d = 8.2, $fn = 6); } for (i = [0:2]) label(str(6.2 + 0.1 * i), [15 + 20 * i, 4], 3); for (i = [0:2]) label(str(8.1 + 0.1 * i), [75 + 20 * i, 4], 3); } // The assembly steps, listed first so they number in build order step("Glue the nut block and the anchor block on the board, with an M4 nut in the hex hole"); step("Bolt the four arms together at their joints, the anchor arm and the link onto the anchor"); step("Glue the glide feet under the joints, the stylus collars on the dowel and the pencil guides over the pencil hole"); if (output == "all" || output == "arms") step("Bolt the four arms together at their joints, the anchor arm and the link onto the anchor") { part("Anchor arm", at = at(anchor, low), rotate = [0, 0, ik[0]]) anchor_arm(); part("Pencil arm", at = at(elbow, high), rotate = [0, 0, ik[1]]) pencil_arm(); part("Stylus arm", at = at(joint_x, low), rotate = [0, 0, ik[0]]) stylus_arm(); part("Link", at = at(anchor, high), rotate = [0, 0, ik[1]]) link(); } if (output == "all" || output == "board") step("Glue the nut block and the anchor block on the board, with an M4 nut in the hex hole") { part("Base board", at = [0, 0, -t]) base_board(); part("Nut block", at = at(anchor, 0)) difference() { circle(d = 44); circle(d = 8.2, $fn = 6); } part("Anchor block", at = at(anchor, t)) difference() { circle(d = 44); circle(d = 4.4); } } if (output == "all" || output == "small") step("Glue the glide feet under the joints, the stylus collars on the dowel and the pencil guides over the pencil hole") { // Two under each of the three joints that move over the paper part("Glide foot", qty = 6, at = [for (q = [elbow, joint_c, joint_x], k = [0:1]) at(q, k * t)]) annulus(20, 10); // Glued to the stylus dowel so its tip just reaches the paper part("Stylus collar", qty = 2, at = [for (k = [0:1]) at(stylus, 3 * t + k * t)]) annulus(14, pencil_hole - 0.2); part("Pencil guide", qty = 3, at = [for (k = [0:2]) at(pencil, 4 * t + 0.5 + k * t)]) annulus(14, pencil_hole); } // The fit test lies on the table beside the board if (output == "all" || output == "coupon") part("Fit test", at = [board[0] + 20, 0, -t]) coupon(); echo(str("Echo: ", ratio_name(ratio), " copy; stylus ", round(stylus_at(ratio) * 10) / 10, " mm from the link joint, pencil ", pencil_at(ratio), " mm from the elbow; original up to ", zone[3], " mm square, copy ", copy_side, " mm")); // Assembly, z up from the top of the base board: glue the nut block and the // anchor block on the board over the hex hole, with an M4 nut in the hole. // The four joints are M4 bolts on 6 mm brass sleeves 2t + 0.7 long, with an // M6 washer between the arms: anchor arm under the link at the anchor (the // bolt from above into the nut), anchor arm under the pencil arm at the // elbow, stylus arm under the pencil arm and under the link at the other // two. Glue two glide feet under the anchor arm at the elbow and under the // stylus arm at both its joints; the bolt heads sit inside them. Glue the // stylus collars on an 8 mm dowel with a rounded tip, so the tip just // touches the paper, and stack the pencil guides over the pencil hole. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/weave/ # Weave pintograph Weave: a pintograph, a drawing machine with two turning discs. The crank turns a small drive gear that meshes with both discs; the right one has a few more teeth than the left, so it slowly falls behind. An arm on a pin in each disc carries the pencil where the two arms cross, and it draws loop after loop, each a little different, weaving a cushion of lines. This design has 36 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=weave). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/weave/weave.kerf) - [weave-sheet-1.svg](https://kerfscript.mylasertools.com/models/weave/weave-sheet-1.svg) - [weave-sheet-1.dxf](https://kerfscript.mylasertools.com/models/weave/weave-sheet-1.dxf) - [weave-sheet-2.svg](https://kerfscript.mylasertools.com/models/weave/weave-sheet-2.svg) - [weave-sheet-2.dxf](https://kerfscript.mylasertools.com/models/weave/weave-sheet-2.dxf) - [weave-sheet-3.svg](https://kerfscript.mylasertools.com/models/weave/weave-sheet-3.svg) - [weave-sheet-3.dxf](https://kerfscript.mylasertools.com/models/weave/weave-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 375 × 375 mm | | Foot | 8 | 30 × 30 mm | | Left disc | 2 | 155 × 155 mm | | Right disc | 2 | 162.5 × 162.5 mm | | Drive gear | 2 | 80 × 79.7 mm | | Arm | 2 | 229 × 18 mm | | Pen sleeve | 3 | 16 × 16 mm | | Axle spacer | 6 | 16 × 16 mm | | Pin spacer | 3 | 14 × 14 mm | | Handle | 5 | 18 × 18 mm | | Fit test | 1 | 130 × 45 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `right_teeth` | `63` | 61 teeth / finest, 62 teeth / fine, 63 teeth / open, 64 teeth / widest | | `left_pin` | `40` | 16 mm, 24 mm, 32 mm, 40 mm | | `right_pin` | `32` | 16 mm, 24 mm, 32 mm, 40 mm | | `arm_length` | `200` | 180 to 210 | | `backlash` | `0.4` | 0.3 to 0.6 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `crank_angle` | `340` | 0 to 3600 | | `output` | `"all"` | All parts, Base and feet, Discs and drive gear, Arms and pen sleeve, Spacers and handle, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Weave: a pintograph, a drawing machine with two turning discs. The crank // turns a small drive gear that meshes with both discs; the right one has a // few more teeth than the left, so it slowly falls behind. An arm on a pin in // each disc carries the pencil where the two arms cross, and it draws loop // after loop, each a little different, weaving a cushion of lines. /* [Drawing] */ // Teeth on the right disc, against 60 on the left: fewer teeth make a finer weave and more crank turns right_teeth = 63; // [61:61 teeth / finest, 62:62 teeth / fine, 63:63 teeth / open, 64:64 teeth / widest] // Hole the left arm's pin goes in, as its distance from the disc centre left_pin = 40; // [16:16 mm, 24:24 mm, 32:32 mm, 40:40 mm] // Hole the right arm's pin goes in right_pin = 32; // [16:16 mm, 24:24 mm, 32:32 mm, 40:40 mm] // Arm length, pin to pencil: longer arms draw higher up the paper and flatter arm_length = 200; // [180:10:210] /* [Fit] */ // Total play between two meshing gears, at the pitch circle backlash = 0.4; // [0.3:0.05:0.6] // Added to the 6 mm holes that turn on brass bushes bearing_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // Crank angle in the assembled view; the discs turn more slowly, so it // goes past one turn crank_angle = 340; // [0:10:3600] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, base:Base and feet, gears:Discs and drive gear, arms:Arms and pen sleeve, spacers:Spacers and handle, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; m = 2.5; z0 = 30; z1 = 60; z2 = right_teeth; L = arm_length; hole = 6 + bearing_clearance; // The discs turn on axles 190 mm apart; the drive gear sits below them, at // the pitch distance from both, so it moves a little with the tooth count s = 95; c1 = (z0 + z1) * m / 2; c2 = (z0 + z2) * m / 2; gx = (c1 * c1 - c2 * c2) / (4 * s); drive = [gx, -sqrt(c1 * c1 - (gx + s) * (gx + s))]; discs = [[-s, 0], [s, 0]]; holes = [16, 24, 32, 40]; // The handle stands on the drive gear, inside its teeth handle_r = 24; // A5 paper, landscape paper = [210, 148]; assert(contains([61, 62, 63, 64], z2), "Right disc: 61 to 64 teeth"); assert(contains(holes, left_pin), "Left pin: 16, 24, 32 or 40 mm"); assert(contains(holes, right_pin), "Right pin: 16, 24, 32 or 40 mm"); assert(L >= 180 && L <= 210, "Arm length: 180 to 210 mm"); assert(backlash >= 0.3 && backlash <= 0.6, "Backlash: 0.3 to 0.6 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 3600, "Crank angle: 0 to 3600 degrees"); assert(contains(["all", "base", "gears", "arms", "spacers", "coupon"], output), "Unknown output"); function gcd(a, b) = b == 0 ? a : gcd(b, a % b); function polar(r, a) = [r * cos(a), r * sin(a)]; // The pins when the crank has turned a degrees: both discs turn the other // way, the left at z0 / z1 and the right at z0 / z2 of the crank's speed. // Both rows of holes point up at a = 0. function pins(a) = [discs[0] + polar(left_pin, 90 - a * z0 / z1), discs[1] + polar(right_pin, 90 - a * z0 / z2)]; // The pencil, where two circles of the arm length round the pins cross, // on the paper side function pen(a) = let (p = pins(a), v = p[1] - p[0], d = norm(v), h = sqrt(L * L - d * d / 4)) p[0] + v / 2 + [-v[1], v[0]] * h / d; // The drawing closes when the left disc has made z2 / gcd turns loops = z2 / gcd(z1, z2); turns = loops * z1 / z0; drawing = [for (i = [0:turns * 72]) pen(i * 5)]; xs = [for (p = drawing) p.x]; ys = [for (p = drawing) p.y]; // The arms always meet: the pins are at most this far apart, under 2 L reach = 2 * s + left_pin + right_pin; assert(reach < 2 * L - 60, "The arms must be long enough to meet"); // The angle between the arms at the pencil, widest and narrowest widest = 2 * asin(reach / 2 / L); narrowest = 2 * asin((2 * s - left_pin - right_pin) / 2 / L); // The paper sits as low as it can without going under the discs box = [max(xs) - min(xs), max(ys) - min(ys)]; paper_y = min(min(ys) - 8, max(88, (min(ys) + max(ys)) / 2 - paper[1] / 2)); assert(box[0] < paper[0] - 16 && max(ys) < paper_y + paper[1] - 8, "The drawing must fit on the paper"); // Base outline bx = 187.5; by0 = -116; by1 = ceil(paper_y + paper[1] + 10); // Involute teeth with a 20 degree pressure angle. backlash is shared between // the two gears of a mesh: every tooth is thinned by backlash / 2. The roots // get a plain radial relief. function gear_inv(a) = tan(a) * 180 / PI - a; function gear_polar(r, a) = [r * cos(a), r * sin(a)]; // Half the angular width of a tooth at radius r function gear_half(z, m, r, backlash) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) 90 / z + gear_inv(20) - gear_inv(ar) - backlash / (4 * rp) * 180 / PI; function gear_outline(z, m, backlash, steps = 12) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - 1.25 * m, hi = rp + m, start = max(lo, rb), hs = gear_half(z, m, start, backlash), ht = gear_half(z, m, hi, backlash)) [for (k = [0:z - 1]) each concat( [gear_polar(lo, 360 * k / z - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z - gear_half(z, m, r, backlash))], [for (j = [1:4]) gear_polar(hi, 360 * k / z - ht + 2 * ht * j / 4)], [for (j = [steps - 1:-1:0]) let (r = start + (hi - start) * j / steps) gear_polar(r, 360 * k / z + gear_half(z, m, r, backlash))], [if (start > lo) gear_polar(lo, 360 * k / z + hs)], [for (j = [1:5]) gear_polar(lo, 360 * k / z + hs + (360 / z - 2 * hs) * j / 6)] )]; module externalgear(z) { polygon(gear_outline(z, m, backlash)); } // Timing, with the crank at 0: the drive gear has a tooth on +X. Seen along // the line between the axles, a tooth on one side of the drive gear faces a // space on the disc, so each disc's teeth are turned by rho to match. function rho(c, z) = let (a = atan2(c[1] - drive[1], c[0] - drive[0]), f = a * z0 / 360 - floor(a * z0 / 360)) a + 180 - (0.5 - f) * 360 / z; // n scored tally marks module tally(n, p) { score() for (j = [0:n - 1]) translate(p + [2 * j, 0]) rect([0.3, 4]); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } // A disc: the pin holes run up from the centre, numbered, and the one the // settings use is ringed module disc(z, c, pin, label) { difference() { rotate(rho(c, z)) externalgear(z); circle(d = hole); for (r = holes) translate([0, r]) circle(d = 4.4); } score() { for (r = holes) translate([5, r - 1.5]) text(str(r), size = 3); translate([0, pin]) outline() circle(d = 8.5); translate([0, -30]) text(label, size = 5, halign = "center"); translate([0, -38]) text(str(z, " TEETH"), size = 3.5, halign = "center"); } } module drive_gear() { difference() { externalgear(z0); circle(d = hole); translate([handle_r, 0]) circle(d = 4.4); } score() line([7, 0], [handle_r - 6, 0]); } // Pin hole at the origin, pencil hole at L along +X, and a short tail // beyond it so the arms cross at the pencil module arm() { difference() { hull() { circle(r = 8); translate([L, 0]) circle(r = 9); translate([L + 14, 0]) circle(r = 7); } circle(d = hole); translate([L, 0]) circle(d = 8.4); } score() translate([L / 2, 0]) text("WEAVE", size = 6, halign = "center", valign = "center"); score() for (x = [14, L - 15]) translate([x, 0]) rect([6, 0.3], center = true); } module base() { difference() { translate([-bx, by0]) rect([2 * bx, by1 - by0], r = 12); // M4 axle bolts for (c = concat(discs, [drive])) translate(c) circle(d = 4.4); } score() { // Where the paper goes, with the drawing's corners marked inside it translate([-paper[0] / 2, paper_y]) outline() rect(paper); for (p = [[min(xs), min(ys)], [max(xs), min(ys)], [min(xs), max(ys)], [max(xs), max(ys)]]) translate(p) { rect([6, 0.3], center = true); rect([0.3, 6], center = true); } translate([-146, by1 - 28]) text("WEAVE", size = 13, halign = "center"); // The whole drawing at a third of its size translate([-146, by1 - 52]) scale(1 / 3) translate([-(min(xs) + max(xs)) / 2, -(min(ys) + max(ys)) / 2]) polyline(drawing); translate([-146, by1 - 80]) text(str(z1, " : ", z2, " TEETH"), size = 4, halign = "center"); translate([-146, by1 - 87]) text(str("PINS ", left_pin, " / ", right_pin), size = 4, halign = "center"); translate([-146, by1 - 94]) text(str("ARMS ", L), size = 4, halign = "center"); translate([146, by1 - 28]) text("PINTOGRAPH", size = 6, halign = "center"); translate([146, by1 - 38]) text(str(turns, " TURNS TO CLOSE"), size = 4, halign = "center"); } } feet = [[-bx + 20, by0 + 20], [bx - 20, by0 + 20], [-bx + 20, by1 - 20], [bx - 20, by1 - 20]]; // The assembled view, base bottom at z = 0: gears on two axle spacers, the // nut on each crank pin, then the left arm on one pin spacer and the right // arm on two, crossing at the pencil a = crank_angle; P = pins(a); pencil = pen(a); zg = 4 * t; zn = 6 * t + 3.2; arm_z = [zn + t, zn + 2 * t]; function heading(p) = let (v = pencil - p) [0, 0, atan2(v[1], v[0])]; function at(p, z) = concat(p, [z]); if (output == "all" || output == "base") step("Glue up the base and the feet, with two spacers round each axle hole") { part("Base", qty = 2, at = [[0, 0, 0], [0, 0, t]]) base(); part("Foot", qty = 8, at = [for (f = feet, k = [0:1]) at(f, -2 * t + k * t)]) circle(d = 30); } if (output == "all" || output == "gears") step("Mesh the two discs and the drive gear on their axles") { // Both discs turn the other way from the crank, more slowly part("Left disc", qty = 2, at = [for (k = [0:1]) at(discs[0], zg + k * t)], rotate = [0, 0, -a * z0 / z1]) disc(z1, discs[0], left_pin, "LEFT"); part("Right disc", qty = 2, at = [for (k = [0:1]) at(discs[1], zg + k * t)], rotate = [0, 0, -a * z0 / z2]) disc(z2, discs[1], right_pin, "RIGHT"); part("Drive gear", qty = 2, at = [for (k = [0:1]) at(drive, zg + k * t)], rotate = [0, 0, a]) drive_gear(); } if (output == "all" || output == "arms") step("Bolt the arms on the crank pins and push the pencil through") { part("Arm", qty = 2, at = [at(P[0], arm_z[0]), at(P[1], arm_z[1])], rotate = [heading(P[0]), heading(P[1])]) arm(); // Under the left arm, down to the paper part("Pen sleeve", qty = 3, at = [for (k = [3:-1:1]) at(pencil, arm_z[0] - k * t)]) ring(16, 8.4); } if (output == "all" || output == "spacers") { step("Glue up the base and the feet, with two spacers round each axle hole") part("Axle spacer", qty = 6, at = [for (c = concat(discs, [drive]), k = [0:1]) at(c, 2 * t + k * t)]) ring(16, hole); step("Bolt the arms on the crank pins and push the pencil through") part("Pin spacer", qty = 3, at = [at(P[0], zn), at(P[1], zn), at(P[1], zn + t)]) ring(14, hole); step("Bolt the handle stack on the drive gear") part("Handle", qty = 5, at = [for (k = [0:4]) at(drive + polar(handle_r, a), 6 * t + 4 + k * t)]) ring(18, hole); } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [bx + 20, by0, -2 * t]) { difference() { rect([130, 45], r = 3); for (i = [0:3]) translate([15 + 25 * i, 15]) circle(d = 6.1 + 0.1 * i); translate([113, 15]) circle(d = 8.4); translate([113, 34]) circle(d = 4.4); } for (i = [0:3]) tally(i + 1, [12 + 25 * i, 29]); } echo(str("Weave: the drawing closes after ", turns, " crank turns (", loops, " loops), ", round(box[0]), " x ", round(box[1]), " mm, on A5 paper ", round(paper_y), " mm above the disc axles; the arms meet at ", round(narrowest), " to ", round(widest), " degrees")); // Assembly, with base bottom at z = 0: glue the two plies of the base, each // disc and the drive gear, then the feet in pairs under the corners and two // axle spacers round each axle hole. Each axle is an M4 bolt up from below // with a 6 mm brass bush over it standing on the base; the gear turns on the // bush, clamped from above by a washer and nut. Mesh the gears with both rows // of holes pointing at the paper. Each crank pin is an M4 bolt up through the // ringed hole, nutted on the disc, with a brass bush: the left arm on one pin // spacer, the right arm on two, each under a washer and nut. Glue the three // pen sleeves under the left arm's pencil hole, lay the right arm on top and // push a pencil down through both until it rests on the paper. Bolt the // handle stack on a bush on the drive gear and tape A5 paper in its outline. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tempo/ # Tempo anchor escapement Tempo: a model of a Graham deadbeat escapement. A falling weight turns the escape wheel; its teeth push the two pallets of the anchor in turn, and the anchor swings the pendulum and lets the wheel on by half a tooth each beat. The hand stands dead still between beats. Wind the knob behind, start the pendulum swinging and set the beat with the crutch bolt. This design has 75 parts on 5 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tempo). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tempo/tempo.kerf) - [tempo-sheet-1.svg](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-1.svg) - [tempo-sheet-1.dxf](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-1.dxf) - [tempo-sheet-2.svg](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-2.svg) - [tempo-sheet-2.dxf](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-2.dxf) - [tempo-sheet-3.svg](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-3.svg) - [tempo-sheet-3.dxf](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-3.dxf) - [tempo-sheet-4.svg](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-4.svg) - [tempo-sheet-4.dxf](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-4.dxf) - [tempo-sheet-5.svg](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-5.svg) - [tempo-sheet-5.dxf](https://kerfscript.mylasertools.com/models/tempo/tempo-sheet-5.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Front plate | 1 | 223.2 × 380 mm | | Front plate inner | 1 | 223.2 × 380 mm | | Back plate | 2 | 223.2 × 380 mm | | Base | 1 | 243.2 × 155.5 mm | | Base lower | 1 | 243.2 × 155.5 mm | | Pillar ring | 16 | 14 × 14 mm | | Escape wheel | 2 | 142.6 × 143.2 mm | | Anchor | 2 | 151.3 × 65.7 mm | | Hand | 1 | 14 × 40.2 mm | | Arbor spacer | 6 | 12 × 12 mm | | Crutch | 2 | 22 × 78 mm | | Pendulum rod | 2 | 14 × 255 mm | | Bob face | 1 | 90 × 90 mm | | Bob disc | 5 | 90 × 90 mm | | Bob cheek | 4 | 37.7 × 88.8 mm | | Click disc | 1 | 64 × 64 mm | | Click ring | 1 | 64 × 64 mm | | Ratchet wheel | 1 | 30 × 30 mm | | Drum core | 3 | 10 × 10 mm | | Drum flange | 1 | 44 × 44 mm | | Winding knob | 2 | 34 × 34 mm | | End collar | 1 | 16 × 16 mm | | Weight base | 1 | 70 × 70 mm | | Weight ring | 15 | 70 × 70 mm | | Weight bail | 1 | 70 × 70 mm | | Fit test | 1 | 130 × 52 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `pendulum_length` | `250` | 180 to 320 | | `teeth` | `30` | 24 to 40 | | `lift` | `3` | 2 to 4 | | `bush_clearance` | `0.1` | 0 to 0.3 | | `output` | `"all"` | All parts, Plates and base, Escape wheel and anchor, Crutch rod and bob, Drum ratchet and weight, Fit test | | `amplitude` | `4` | 3 to 5.5 | | `swing_phase` | `15` | 0 to 345 | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Tempo: a model of a Graham deadbeat escapement. A falling weight // turns the escape wheel; its teeth push the two pallets of the anchor in // turn, and the anchor swings the pendulum and lets the wheel on by half a // tooth each beat. The hand stands dead still between beats. Wind the knob // behind, start the pendulum swinging and set the beat with the crutch bolt. /* [Clock] */ // Pendulum length in mm, from the anchor arbor to the centre of the bob pendulum_length = 250; // [180:10:320] // Teeth on the escape wheel; the wheel grows with them teeth = 30; // [24:1:40] // How far the impulse faces turn the anchor, in degrees lift = 3; // [2:0.5:4] /* [Fit] */ // Added to the 6 mm holes the brass bushes are glued into bush_clearance = 0.1; // [0:0.05:0.3] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Plates and base, escapement:Escape wheel and anchor, pendulum:Crutch rod and bob, drive:Drum ratchet and weight, coupon:Fit test] /* [Assembly] */ // How far the pendulum swings each side in the assembled view, in degrees amplitude = 4; // [3:0.5:5.5] // Where the pendulum is in its swing, in degrees: 0 is hanging straight and moving right, 90 at the right end swing_phase = 15; // [0:15:345] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; L = pendulum_length; N = teeth; assert(L >= 180 && L <= 320, "Pendulum length: 180 to 320 mm"); assert(N == floor(N) && N >= 24 && N <= 40, "Teeth: whole numbers 24 to 40"); assert(lift >= 2 && lift <= 4, "Lift: 2 to 4 degrees"); assert(amplitude >= 3 && amplitude <= 5.5, "Amplitude: 3 to 5.5 degrees"); assert(bush_clearance >= 0 && bush_clearance <= 0.3, "Bush clearance: 0 to 0.3 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "frame", "escapement", "pendulum", "drive", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; function rad(a) = a * PI / 180; // Escapement, all angles in degrees. Every tooth is 15 mm from the next at // the tips, so the wheel grows with its tooth count. R = N * 15 / (2 * PI); pitch_a = 360 / N; // The anchor spans a whole number of teeth and a half: when one pallet holds // a tooth, the other sits in a gap span = floor(N / 4) + 0.5; hs = span * pitch_a / 2; // Anchor arbor above the wheel, where the lines to the two pallets touch // the tip circle, so a tooth on a locking face pushes straight at the arbor D = R / cos(hs); rho = R * tan(hs); // Lock: how far the anchor turns past a tooth before it can escape. Drop: // how far the wheel turns free after each impulse. Swing: the largest // pendulum swing the pallets are cut for. lock = 1.5; drop = 1.5; swing = 5.5; tip_w = 0.6; tau = tip_w / R * 180 / PI; // Pallet width in wheel degrees: half a tooth, less the drop w = pitch_a / 2 - drop; // The entry pallet unlocks at +c, the exit pallet at -c c = (lift - lock) / 2; // Tooth height: room for the deepest pallet at full swing h = rad(swing - c + lift) * rho + 2; Rr = R - h; lean = 8; d1 = h * tan(lean) / Rr * 180 / PI; d2 = d1 + 0.33 * 15 / Rr * 180 / PI; // Wheel tip positions, seen from the anchor arbor: where a tip locks and // where it leaves each pallet function tip(a) = polar(R, a) - [0, D]; function ang(p) = atan2(p[1], p[0]); r_out = norm(tip(90 + hs + w / 2)); r_in = norm(tip(90 + hs - w / 2)); // Corners in the anchor's own frame: the anchor turns by +alpha, so a tip at // world angle a meets the anchor at a - alpha a_lock_e = ang(tip(90 + hs + w / 2)) - c; a_end_e = ang(tip(90 + hs - w / 2)) - (c - lift); a_lock_x = ang(tip(90 - hs + w / 2)) + c; a_end_x = ang(tip(90 - hs - w / 2)) - (lift - c); // The anchor's bow stays clear of the tips at full swing; the pallets reach // up into it band = R + rho * sin(swing) + 2; ext = (band - R + 4) / rad(1) / rho - c; // Frame: heights above the base top, depths forward from the back of the // back plate. The plates are two plies, 4t + 1 apart. gap = 4 * t + 1; front_d = 2 * t + gap; // Anchor arbor height: room for the bob below it, and for the wheel zP = max(L + 70, D + R + 45); zW = zP - D; PW = 2 * (R + 40); PH = zP + 54; island = 0.45 * R; pillars = [[-(PW / 2 - 12), zP + 34], [PW / 2 - 12, zP + 34], [-(PW / 2 - 12), zW - R - 20], [PW / 2 - 12, zW - R - 20]]; tabs = [-(PW / 2 - 32), PW / 2 - 32]; // Behind the back plate: a washer, a brass sleeve, the click disc and ring, // then the drum, far enough back that the weight pot clears the plate sleeve = 28; core_r = 5; core_d = -(1 + sleeve + 2 * t + 1.5 * t); bob_d = 90; period = 2 * PI * sqrt(L / 1000 / 9.81); // How far the weight pot can fall, and how long that keeps it going fall = zW - 40 - 17 * t; run = fall / (2 * PI * core_r) * N * period; // In front: the front face of the front plate, the hand on a washer, the // crutch on two spacers and two washers, the rod and the bob round it fp = front_d + 2 * t; hand_d = fp + 1; crutch_d = fp + 2 * t + 2; rod_d = crutch_d + 2 * t; bob_back = rod_d - 3 * t; base_front = rod_d + 5 * t + 15; base_back = -core_d + 35 + 12; function fix2(v) = let (n = round(v * 100)) str(floor(n / 100), ".", n % 100 < 10 ? "0" : "", n % 100); module label(s, p, size = 5) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } // Graham teeth: a straight front face leaning forward, so only the tip // touches a locking face, and a curved back that leaves room for the // pallets. The wheel turns clockwise from the front. function tooth(k) = let (f = 90 + k * pitch_a) concat( [for (j = [1:5]) polar(Rr, f - pitch_a + d2 + j * (pitch_a + d1 - d2) / 6)], [polar(Rr, f + d1), polar(R, f), polar(R, f + tau)], [for (j = [1:8]) let (s = j / 8) polar(R - h * s, f + tau + d1 * s + (d2 - d1 - tau) * pow(s, 1.6))] ); module escape_wheel() { difference() { polygon([for (k = [0:N - 1]) each tooth(k)]); circle(d = 4.1); // Six spokes for (k = [0:5]) offset(r = 3) offset(delta = -3) polygon(concat( [for (j = [0:6]) polar(15, k * 60 + 8 + j * 44 / 6)], [for (j = [6:-1:0]) polar(Rr - 7, k * 60 + 5 + j * 50 / 6)])); } } // Drawn with the anchor arbor at the origin and the wheel centre at // [0, -D]. Each locking face is an arc round the arbor; each impulse face // runs from the locking corner to a corner `lift` degrees deeper. module anchor() { difference() { union() { circle(r = 11); translate([-8, -(D - band - 11)]) rect([16, D - band - 11]); polygon(concat( [for (j = [0:16]) [0, -D] + polar(band, 90 - hs - 9 + j * (2 * hs + 18) / 16)], [for (j = [16:-1:0]) [0, -D] + polar(band + 13, 90 - hs - 9 + j * (2 * hs + 18) / 16)])); // Entry pallet, on the left polygon(concat( [for (j = [0:8]) polar(r_out, a_lock_e - ext + j * ext / 8)], [polar(r_in, a_end_e)], [for (j = [0:8]) polar(r_in, a_end_e - j * (a_end_e - a_lock_e + ext) / 8)])); // Exit pallet, on the right polygon(concat( [for (j = [0:8]) polar(r_in, a_lock_x + j * ext / 8)], [for (j = [0:8]) polar(r_out, a_lock_x + ext - j * (a_lock_x + ext - a_end_x) / 8)])); } circle(d = 4.1); } label("IN", [0, -D] + polar(band + 6.5, 90 + hs - 2), 4); label("OUT", [0, -D] + polar(band + 6.5, 90 - hs + 2), 4); } module plate(front = false, outer = false) { difference() { union() { translate([-PW / 2, 0]) rect([PW, PH], r = 10); for (x = tabs) translate([x - 12, -2 * t]) rect([24, 2 * t + 1]); } for (p = [[0, zP], [0, zW]]) translate(p) circle(d = 6 + bush_clearance); for (p = pillars) translate(p) circle(d = 4.4); // Two windows onto the wheel and the pallets if (front) difference() { union() { translate([0, zW]) circle(r = R + 25); translate([0, zP]) circle(r = 30); } translate([0, zW]) circle(r = island); translate([0, zP]) circle(r = 15); translate([-9, 0]) rect([18, PH]); } } if (outer) { label("TEMPO", [0, zP + 42], 11); // One mark for every beat: the hand moves half a tooth each time for (k = [0:2 * N - 1]) score() hull() { translate([0, zW] + polar(island - 3, 90 - k * 180 / N)) circle(d = 0.4); translate([0, zW] + polar(island - (k % 5 == 0 ? 9 : 6), 90 - k * 180 / N)) circle(d = 0.4); } } } module base(top = false) { difference() { translate([-PW / 2 - 10, -base_front]) rect([PW + 20, base_front + base_back], r = 8); for (x = tabs) for (d = [0, front_d]) translate([x - 12.1, -d - 2 * t - 0.1]) rect([24.2, 2 * t + 0.2]); } if (top) { label("GRAHAM DEADBEAT ESCAPEMENT", [0, -base_front + 9], 5); label(str("WIND THE KNOB BEHIND, THEN SWING THE BOB ", round(swing - 2), " DEGREES"), [0, base_back - 9], 3.5); } } module hand() { difference() { union() { circle(r = 7); polygon([[-3, 0], [3, 0], [0.8, island - 6], [-0.8, island - 6]]); polygon([[-4, 0], [4, 0], [0, -14]]); } circle(d = 4.1); } } // The crutch is glued on the front of the anchor arbor. The rod hangs on // bolt A and is held by bolt B in the curved slot: loosen B and swing the // rod a little to put the clock in beat. module crutch() { difference() { hull() { circle(r = 11); translate([0, -58]) circle(r = 9); } circle(d = 4.1); translate([0, -40]) circle(d = 3.4); for (a = [-10:2:10]) hull() { translate([0, -40] + polar(16, -90 + a)) circle(d = 3.4); translate([0, -40] + polar(16, -90 + a + 2)) circle(d = 3.4); } } } module rod() { difference() { translate([-7, -(L + 37)]) rect([14, L + 5], r = 3); translate([0, -40]) circle(d = 3.4); translate([0, -56]) circle(d = 3.4); hull() { translate([0, -(L - 27)]) circle(d = 4.4); translate([0, -(L + 27)]) circle(d = 4.4); } } // The top of the bob reads against these: higher is faster for (k = [-3:3]) score() hull() { translate([-6, -(L - bob_d / 2) + 5 * k]) circle(d = 0.4); translate([k == 0 ? 3 : -2, -(L - bob_d / 2) + 5 * k]) circle(d = 0.4); } label("F", [3.5, -(L - bob_d / 2) + 12], 3.5); label("S", [3.5, -(L - bob_d / 2) - 12], 3.5); } module bob_disc() { difference() { circle(d = bob_d); for (y = [-12, 12]) translate([0, y]) circle(d = 4.4); } } // Ratchet: the drum turns free on the arbor when you wind, and drives the // click disc, glued to the arbor, when the weight pulls. Two flexible // fingers in the click ring drop into the ratchet teeth. rt = 12; module ratchet() { difference() { polygon([for (k = [0:rt - 1]) each [polar(12.8, k * 30 + 2), polar(15, k * 30), polar(15, k * 30 + 3), polar(12.8, k * 30 + 26)]]); circle(d = 4.4); } } module click_ring() { difference() { union() { ring(64, 47); for (a = [0, 180]) { // Root block, finger and hook polygon(concat([for (j = [0:4]) polar(23.6, a - 14 + j * 3.5)], [for (j = [4:-1:0]) polar(17, a - 14 + j * 3.5)])); polygon(concat([for (j = [0:30]) polar(20, a + j * 5)], [for (j = [30:-1:0]) polar(17, a + j * 5)])); // The hook's end face lies on the line of a ratchet tooth's front polygon([polar(17.5, a + 150 - 2 * 2.5 / 2.2), polar(13.1, a + 150 + 2 * 1.9 / 2.2), polar(13.1, a + 147), polar(17.5, a + 132)]); } } } } // The assembled view. The pendulum and anchor turn alpha anticlockwise (seen // from the front); the wheel turns theta clockwise from where it is drawn. // With the exit pallet locked the wheel stands at theta_x; each impulse turns // it on by the pallet width w while the anchor turns through the lift, then // it drops onto the other pallet, half a tooth on. alpha = amplitude * sin(swing_phase); theta_x = (hs - w / 2) % pitch_a; // Wheel turn after a pallet unlocks, u = 0 to 1 through the lift, kept a // hair behind the impulse face function impulse(u) = max(0, w * u - 0.05); cycle = floor((swing_phase + 90) / 360); psi = swing_phase - 360 * cycle; theta = theta_x + pitch_a * cycle + (psi <= 90 ? (alpha < -c ? 0 : alpha < lift - c ? impulse((alpha + c) / lift) : pitch_a / 2) : (alpha > c ? pitch_a / 2 : alpha > c - lift ? pitch_a / 2 + impulse((c - alpha) / lift) : pitch_a)); // A part in the plane of the plates, its origin at p (x, height above the // base top) and its back face at depth d; world y points to the back function at_front(p, d) = [p[0], -d, p[1] + 2 * t]; // Turned a degrees anticlockwise as seen from the front function turned(a) = [90, -a, 0]; // A point of the pendulum, drawn with the anchor arbor at the origin function swung(v) = [0, zP] + [v[0] * cos(alpha) - v[1] * sin(alpha), v[0] * sin(alpha) + v[1] * cos(alpha)]; click_d = -(1 + sleeve + t); bob_at = swung([0, -L]); // The weight pot hangs from the right of the drum, lower as the wheel turns pot_top = zW + 2 * t - 40 - core_r * rad(theta); if (output == "all" || output == "frame") step("Stand the plates in the base on the four pillar bolts") { part("Front plate", at = at_front([0, 0], fp - t), rotate = turned(0)) plate(true, true); part("Front plate inner", at = at_front([0, 0], front_d), rotate = turned(0)) plate(true, false); part("Back plate", qty = 2, at = [at_front([0, 0], 0), at_front([0, 0], t)], rotate = turned(0)) plate(false, false); part("Base", at = [0, 0, t]) base(true); part("Base lower", at = [0, 0, 0]) base(false); part("Pillar ring", qty = 16, at = [for (p = pillars, k = [0:3]) at_front(p, (2 + k) * t)], rotate = turned(0)) ring(14, 4.4); } if (output == "all" || output == "escapement") step("Fit the escape wheel and the anchor between the plates, and the hand in front") { part("Escape wheel", qty = 2, at = [for (k = [0:1]) at_front([0, zW], (3 + k) * t + 0.5)], rotate = turned(-theta)) escape_wheel(); part("Anchor", qty = 2, at = [for (k = [0:1]) at_front([0, zP], (3 + k) * t + 0.5)], rotate = turned(alpha)) anchor(); // The hand can go on the arbor at any angle; shown where the rod hides it least part("Hand", at = at_front([0, zW], hand_d), rotate = turned(-theta - 120)) hand(); // Each side of the wheel and the anchor, and two under the crutch part("Arbor spacer", qty = 6, rotate = turned(0), at = [at_front([0, zW], 2 * t), at_front([0, zW], front_d - t), at_front([0, zP], 2 * t), at_front([0, zP], front_d - t), at_front([0, zP], fp), at_front([0, zP], fp + t)]) ring(12, 4.4); } if (output == "all" || output == "pendulum") step("Bolt the pendulum rod to the crutch and clamp the bob on") { part("Crutch", qty = 2, at = [for (k = [0:1]) at_front([0, zP], crutch_d + k * t)], rotate = turned(alpha)) crutch(); part("Pendulum rod", qty = 2, at = [for (k = [0:1]) at_front([0, zP], rod_d + k * t)], rotate = turned(alpha)) rod(); part("Bob face", at = at_front(bob_at, rod_d + 4 * t), rotate = turned(alpha)) { bob_disc(); label(str("L ", L, " MM"), [0, 26], 5); label(str("T ", fix2(period), " S"), [0, -26], 5); } // Three behind the rod, two in front of it under the face part("Bob disc", qty = 5, at = [for (d = [0, 1, 2, 5, 6]) at_front(bob_at, bob_back + d * t)], rotate = turned(alpha)) bob_disc(); // Either side of the rod, in its two layers part("Bob cheek", qty = 4, at = [for (k = [0:3]) at_front(bob_at, rod_d + floor(k / 2) * t)], rotate = [for (k = [0:3]) turned(alpha + 180 * (k % 2))]) intersection() { circle(d = bob_d); translate([7.3, -bob_d / 2]) rect([bob_d, bob_d]); } } if (output == "all" || output == "drive") step("Fit the winding drum behind and hang the weight pot on its string") { part("Click disc", at = at_front([0, zW], click_d), rotate = turned(-theta)) ring(64, 4.1); part("Click ring", at = at_front([0, zW], click_d - t), rotate = turned(-theta)) click_ring(); part("Ratchet wheel", at = at_front([0, zW], click_d - t), rotate = turned(-theta)) ratchet(); part("Drum core", qty = 3, at = [for (k = [1:3]) at_front([0, zW], click_d - (k + 1) * t)], rotate = turned(-theta)) ring(2 * core_r, 4.4); // Tie the string through the small hole part("Drum flange", at = at_front([0, zW], click_d - 5 * t), rotate = turned(-theta)) difference() { ring(44, 4.4); translate([0, core_r + 3]) circle(d = 1.6); } part("Winding knob", qty = 2, at = [for (k = [6:7]) at_front([0, zW], click_d - k * t)], rotate = turned(-theta)) difference() { union() { circle(r = 14); for (k = [0:11]) translate(polar(14, k * 30)) circle(r = 3); } circle(d = 4.4); } part("End collar", at = at_front([0, zW], click_d - 8 * t), rotate = turned(-theta)) ring(16, 4.1); part("Weight base", at = [core_r, -core_d, pot_top - 17 * t]) circle(d = 70); part("Weight ring", qty = 15, at = [for (k = [0:14]) [core_r, -core_d, pot_top - (16 - k) * t]]) ring(70, 54); part("Weight bail", at = [core_r, -core_d, pot_top - t]) difference() { union() { ring(70, 54); translate([-32, -5]) rect([64, 10]); } circle(d = 3); } } if (output == "all" || output == "coupon") part("Fit test", at = [PW / 2 + 40, -base_front, 0]) { difference() { rect([130, 52], r = 3); for (i = [0:3]) translate([16 + 26 * i, 16]) circle(d = 6 + 0.1 * i); for (i = [0:2]) translate([16 + 26 * i, 38]) circle(d = 4 + 0.1 * i); translate([92, 38]) circle(d = 4.4); translate([106, 38]) circle(d = 3.4); translate([118, 30]) rect([2 * t + 0.2, 16], center = true); } for (i = [0:3]) score() translate([16 + 26 * i, 4]) text(str(6 + 0.1 * i), size = 3, halign = "center"); for (i = [0:2]) score() translate([16 + 26 * i, 26]) text(str(4 + 0.1 * i), size = 3, halign = "center"); } echo(str("Tempo: pendulum ", L, " mm, period ", fix2(period), " s (2 pi root L/g), a tick every ", fix2(period / 2), " s; ", N, " teeth on a ", round(2 * R), " mm wheel, once round in ", round(N * period), " s; anchor spans ", span, " teeth, lift ", lift, ", lock ", lock, ", drop ", drop, " degrees; about ", round(run), " s per wind")); // Assembly, depths forward from the back of the back plate: glue the // laminates (each plate, the base, wheel, anchor, crutch and rod of 2; the // bob of 3 discs, the 4 cheeks in two layers, 2 discs and the face; the knob // of 2; the weight pot of base, 15 rings and bail). Glue 6 mm brass tube // bushes into the arbor holes and stand both plates in the base on four M4 x // 35 pillar bolts, 4 rings and a 1 mm washer on each. Epoxy the wheel and the // anchor to 4 mm steel rod arbors between the plates, a spacer each side. In // front go the hand on a washer and, on the anchor arbor, two spacers, two // washers and the crutch. Behind: a washer, a 28 mm brass tube sleeve, the // click disc and ring (epoxied to the arbor), then ratchet, three cores, // flange and knob glued together and free to turn, and the end collar. Bolt // the rod to the crutch with two M3 bolts, clamp the bob on with two M4 bolts // and wing nuts, and hang the pot, with 200 to 500 g of sand or nuts in it, // on string tied to the flange and wound on the cores. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/hours/ # Hours wooden gear clock Hours: a weight-driven wall clock with a wooden gear train between open skeleton plates. The weight turns the drum, the train turns the minute arbor once an hour, and a deadbeat anchor on the pendulum lets the escape wheel through half a tooth a beat. Wind it by turning the knob below the centre clockwise; set the time by turning the hands, which are a push fit. This design has 52 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=hours). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/hours/hours.kerf) - [hours-sheet-1.svg](https://kerfscript.mylasertools.com/models/hours/hours-sheet-1.svg) - [hours-sheet-1.dxf](https://kerfscript.mylasertools.com/models/hours/hours-sheet-1.dxf) - [hours-sheet-2.svg](https://kerfscript.mylasertools.com/models/hours/hours-sheet-2.svg) - [hours-sheet-2.dxf](https://kerfscript.mylasertools.com/models/hours/hours-sheet-2.dxf) - [hours-sheet-3.svg](https://kerfscript.mylasertools.com/models/hours/hours-sheet-3.svg) - [hours-sheet-3.dxf](https://kerfscript.mylasertools.com/models/hours/hours-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Back plate | 2 | 176 × 323 mm | | Front plate | 2 | 176 × 323 mm | | Wall bar | 1 | 142 × 22 mm | | Wall bar, nut layer | 1 | 142 × 22 mm | | Wall pad | 2 | 20 × 20 mm | | Wall pad, nut layer | 2 | 20 × 20 mm | | Great wheel | 2 | 129.8 × 129.8 mm | | Centre pinion | 3 | 20.8 × 20.8 mm | | Centre wheel | 1 | 129.8 × 129.8 mm | | Third pinion | 2 | 20.8 × 20.8 mm | | Third wheel | 1 | 121.8 × 121.8 mm | | Escape pinion | 2 | 20.8 × 20.8 mm | | Escape wheel | 2 | 90 × 90 mm | | Anchor | 2 | 83 × 46.2 mm | | Drum | 4 | 36 × 36 mm | | Drum flange | 1 | 56 × 56 mm | | Ratchet wheel | 1 | 50 × 50 mm | | Click | 1 | 43.3 × 34.7 mm | | Winding knob | 2 | 21.7 × 21.7 mm | | Cannon pinion | 2 | 24.9 × 24 mm | | Minute wheel | 1 | 61.8 × 61.6 mm | | Minute pinion | 2 | 20.8 × 20.8 mm | | Hour wheel | 1 | 65.8 × 65.8 mm | | Dial | 1 | 176 × 176 mm | | Hour hand | 1 | 13 × 61.5 mm | | Minute hand | 1 | 11 × 102.8 mm | | Pendulum top | 2 | 18 × 73 mm | | Pendulum top, channel | 2 | 18 × 73 mm | | Bob | 1 | 90 × 90 mm | | Bob back | 1 | 90 × 90 mm | | Bob, channel | 2 | 90 × 89.9 mm | | Fit test | 1 | 130 × 40 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `beat` | `0.75` | 0.75, 1 | | `drop` | `1000` | 500 to 1500 | | `backlash` | `0.4` | 0.3 to 0.6 | | `rod_fit` | `0.05` | 0 to 0.2 | | `tube_fit` | `0.1` | 0 to 0.3 | | `running_clearance` | `0.3` | 0.2 to 0.5 | | `hour` | `10` | 1 to 12 | | `minute` | `10` | 0 to 59 | | `swing` | `3` | -4 to 4 | | `output` | `"all"` | All parts, Plates and wall fittings, Wheels and pinions, Escape wheel and anchor, Drum ratchet and knob, Motion works dial and hands, Pendulum, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Hours: a weight-driven wall clock with a wooden gear train between open // skeleton plates. The weight turns the drum, the train turns the minute // arbor once an hour, and a deadbeat anchor on the pendulum lets the escape // wheel through half a tooth a beat. Wind it by turning the knob below the // centre clockwise; set the time by turning the hands, which are a push fit. /* [Clock] */ // Time for one swing of the pendulum, in seconds; sets the escape wheel's teeth and the pendulum length beat = 0.75; // [0.75, 1] // How far the weight can fall, in mm, for the running time drop = 1000; // [500:50:1500] // Total play between two meshing gears, at the pitch circle backlash = 0.4; // [0.3:0.05:0.6] /* [Fit] */ // Added to the 4 mm holes that grip a steel rod: wheels, pinions, anchor, hands, knob rod_fit = 0.05; // [0:0.05:0.2] // Added to the 6 mm holes that grip a brass tube: bushes, hour wheel and hour hand tube_fit = 0.1; // [0:0.05:0.3] // Added to holes that turn on a rod or tube, and to the pendulum's dowel channel running_clearance = 0.3; // [0.2:0.05:0.5] /* [Assembly] */ // Hour shown in the assembled view hour = 10; // [1:1:12] // Minute shown in the assembled view minute = 10; // [0:1:59] // Pendulum angle in the assembled view, degrees, positive to the right swing = 3; // [-4:0.5:4] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, frame:Plates and wall fittings, train:Wheels and pinions, escapement:Escape wheel and anchor, winding:Drum ratchet and knob, dial:Motion works dial and hands, pendulum:Pendulum, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; // The click's spring finger is 1.4 mm wide on purpose, and the hands taper $min_width = 1.3; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; m = 2; // Profile shift: the 8 and 10 leaf pinions would be undercut without it, // and the wheels give it back so every centre distance stays m (z1 + z2) / 2 shift = 0.55; // The train, from the drum to the escape wheel, and the motion works z_great = 64; z_centre = 64; z_third = 60; z_leaf = 8; z_cannon = 10; z_minute = 30; z_hour = 32; // Escape wheel: once round a minute, half a tooth a beat z_escape = round(30 / beat); // Arbors, with the minute arbor (the dial centre) at the origin M = [0, 0]; G = [0, -m * (z_great + z_leaf) / 2]; T = [m * (z_centre + z_leaf) / 2 * cos(70), m * (z_centre + z_leaf) / 2 * sin(70)]; E = [0, T.y + sqrt(pow(m * (z_third + z_leaf) / 2, 2) - T.x * T.x)]; // Anchor pivot above the escape wheel, and the escape wheel's tip radius anchor_height = 65; A = E + [0, anchor_height]; R = 45; // Minute wheel stud, the four pillars, the dial posts S = [-40, 0]; pillars = [[-80, -110], [80, -110], [-60, A.y], [60, A.y]]; dial_r = 76; posts = [for (a = [45:90:315]) [dial_r * cos(a), dial_r * sin(a)]]; drum_r = 18; // Pendulum length from the beat: L = g (beat / pi)^2 L = 9806.65 * beat * beat / (PI * PI); assert(beat == 0.75 || beat == 1, "Beat: 0.75 or 1 s"); assert(drop >= 500 && drop <= 1500, "Drop: 500 to 1500 mm"); assert(backlash >= 0.3 && backlash <= 0.6, "Backlash: 0.3 to 0.6 mm"); assert(rod_fit >= 0 && rod_fit <= 0.2, "Rod fit: 0 to 0.2 mm"); assert(tube_fit >= 0 && tube_fit <= 0.3, "Tube fit: 0 to 0.3 mm"); assert(running_clearance >= 0.2 && running_clearance <= 0.5, "Running clearance: 0.2 to 0.5 mm"); assert(hour >= 1 && hour <= 12 && floor(hour) == hour, "Hour: 1 to 12"); assert(minute >= 0 && minute <= 59 && floor(minute) == minute, "Minute: 0 to 59"); assert(swing >= -4 && swing <= 4, "Swing: -4 to 4 degrees"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "frame", "train", "escapement", "winding", "dial", "pendulum", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; module label(s, p, size = 4) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module bar(a, b, w) { hull() { translate(a) circle(d = w); translate(b) circle(d = w); } } rod_hole = 4 + rod_fit; tube_hole = 6 + tube_fit; // Involute teeth, 20 degree pressure angle, with a profile shift x. backlash // is shared between the two gears of a mesh. Pinion tips are cut back where // the tooth would come to a point. function gear_inv(a) = tan(a) * 180 / PI - a; function gear_half(z, r, x) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) (90 + 360 * x * tan(20) / PI) / z + gear_inv(20) - gear_inv(ar) - backlash / (4 * rp) * 180 / PI; function gear_tip(z, x, lo, hi, k = 0) = k == 20 ? lo : let (mid = (lo + hi) / 2) gear_half(z, mid, x) * PI / 180 * mid > 0.3 * m ? gear_tip(z, x, mid, hi, k + 1) : gear_tip(z, x, lo, mid, k + 1); function gear_outline(z, x, steps) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - m * (1.25 - x), hi = gear_tip(z, x, rp, rp + m * (1 + x)), start = max(lo, rb), hs = gear_half(z, start, x), ht = gear_half(z, hi, x)) [for (k = [0:z - 1]) each concat( [polar(lo, 360 * k / z - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) polar(r, 360 * k / z - gear_half(z, r, x))], [for (j = [1:3]) polar(hi, 360 * k / z - ht + 2 * ht * j / 4)], [for (j = [steps:-1:0]) let (r = start + (hi - start) * j / steps) polar(r, 360 * k / z + gear_half(z, r, x))], [if (start > lo) polar(lo, 360 * k / z + hs)], [for (j = [1:3]) polar(lo, 360 * k / z + hs + (360 / z - 2 * hs) * j / 4)] )]; // Spoked wheels show the train behind them module spokes(rim, hub, n = 5, w = 6) { difference() { circle(r = rim); circle(r = hub); for (i = [0:n - 1]) rotate(90 + 360 * i / n) translate([0, -w / 2]) rect([rim + 1, w]); } } module wheel(z, hole, hub = 10, n = 5) { rim = m * z / 2 - m * (1.25 + shift) - 5; difference() { polygon(gear_outline(z, -shift, 5)); circle(d = hole); if (rim > hub + 6) spokes(rim, hub, n); } label(str(z), [0, (hub + rim) / 2], 4); } module pinion(z, hole) { difference() { polygon(gear_outline(z, shift, 8)); circle(d = hole); } } // Escape wheel, turning clockwise seen from the front. Each tooth leans // forward: its leading tip corner (on the tip circle, at 360 k / N) does the // locking on both pallets. The back is hollowed to clear the pallets. tooth_h = 5.5; module escape_wheel() { pa = 360 / z_escape; rr = R - tooth_h; rake = tooth_h * tan(12) / rr * 180 / PI; tw = 0.7 / R * 180 / PI; bw = 0.42 * pa; difference() { polygon([for (k = [0:z_escape - 1]) let (a = k * pa) each concat( [polar(rr, a + rake), polar(R, a), polar(R, a + tw), polar(R - tooth_h * 0.6, a + tw + (rake + bw - tw) * 0.25), polar(rr, a + rake + bw)], [for (j = [1:3]) polar(rr, a + rake + bw + (pa - bw) * j / 4)] )]); circle(d = rod_hole); spokes(rr - 5, 9, 5, 5); } } // Graham deadbeat anchor, drawn round its pivot with the escape wheel's // centre at [0, -anchor_height]. The pallets span half a tooth more than // a whole number of teeth. Locking faces are arcs round the pivot, so a // locked tooth cannot push the pendulum; the entry pallet locks on its // outer face, the exit pallet on its inner face. Impulse runs from +-s0 to // -+(lift - s0) degrees of swing, leaving lift - 2 s0 of lock. span = floor(z_escape / 4) + 0.5; phi = span * 180 / z_escape; L0 = sqrt(R * R + anchor_height * anchor_height - 2 * anchor_height * R * cos(phi)); // Drop: the free turn of the wheel between pallets, more than the tooth tip is wide drop_e = 1.1; pallet_w = PI * R / z_escape - drop_e; lift = 2; s0 = 0.6; lock_ext = 10; ro = L0 + pallet_w / 2; ri = L0 - pallet_w / 2; // Where the tip circle crosses the circle of radius r round the pivot; side -1 left, 1 right function tip_at(r, side) = let (qy = (R * R - r * r - anchor_height * anchor_height) / (2 * anchor_height)) atan2(qy, side * sqrt(r * r - qy * qy)); // Pallet corners, as angles round the pivot. Entry: deeper into the wheel as // the anchor turns anticlockwise. Exit: deeper as it turns clockwise. eo = tip_at(ro, -1) - s0; ei = tip_at(ri, -1) - s0 + lift; er = eo - lock_ext; xi = tip_at(ri, 1) + s0; xo = tip_at(ro, 1) + s0 - lift; xr = xi + lock_ext; module anchor() { difference() { union() { circle(r = 10); polygon(concat([for (j = [0:24]) polar(ro, er + (eo - er) * j / 24)], [polar(ri, ei)], [for (j = [24:-1:0]) polar(ri, er + (ei - er) * j / 24)])); polygon(concat([for (j = [0:24]) polar(ri, xr + (xi - xr) * j / 24)], [polar(ro, xo)], [for (j = [24:-1:0]) polar(ro, xr + (xo - xr) * j / 24)])); for (a = [er - 1, xr + 1]) hull() { circle(r = 4); translate(polar(L0, a)) circle(r = pallet_w / 2 + 1); } } circle(d = rod_hole); } } // Ratchet and click, drawn round the great wheel's centre. The weight turns // the drum anticlockwise; each tooth's steep face (at 90 + 15 k degrees) // pushes the click and so the great wheel. Winding turns the drum the other // way: the teeth lift the click and bend its long spring finger against a pin. ratchet_n = 24; module ratchet() { difference() { polygon([for (k = [0:ratchet_n - 1]) let (a = 90 + 360 * k / ratchet_n) each [polar(22.5, a), for (j = [1:5]) polar(22.5 + 2.5 * j / 5, a + 360 / ratchet_n * j / 5 - 0.05)]]); circle(d = rod_hole); } } click_pin = polar(36, 128); finger_end = click_pin + polar(36, 235); spring_pin = finger_end + polar(2.7, 325); module click() { difference() { union() { translate(click_pin) circle(r = 4.5); polygon([polar(22.9, 90.6), polar(25.8, 97), polar(26.6, 105), polar(28, 116), click_pin + polar(4, 250), click_pin + polar(4, 60), polar(31, 104), polar(25.8, 90.6)]); hull() { translate(click_pin + polar(4, 235)) circle(r = 0.7); translate(finger_end) circle(r = 0.7); } } translate(click_pin) circle(d = 4 + running_clearance); } } module disc(r, hole = rod_hole) { difference() { circle(r = r); circle(d = hole); } } module knob() { difference() { circle(r = 11); for (i = [0:11]) translate(polar(11.5, 30 * i)) circle(r = 2); circle(d = rod_hole); } } // The plates: bosses round the arbors and pillars, joined by bars. Both // are drawn as seen from the front. function side_x(y, s) = s * (80 - 20 * (y + 110) / (A.y + 110)); module plate(front) { bars = [[pillars[0], G], [pillars[1], G], [pillars[0], pillars[2]], [pillars[1], pillars[3]], [pillars[2], A], [pillars[3], A], [E, A], [M, [side_x(0, -1), 0]], [M, [side_x(0, 1), 0]], [T, [side_x(T.y, 1), T.y]], [G, M]]; difference() { union() { for (b = bars) bar(b[0], b[1], 10); for (p = [M, G, T, E, A]) translate(p) circle(r = 9); for (p = pillars) translate(p) circle(r = 8); if (front) { translate(S) circle(r = 8); for (p = posts) { translate(p) circle(r = 7); bar(p, [side_x(p.y, sign(p.x)), p.y], 8); } } } for (p = [M, G, T, E, A]) translate(p) circle(d = tube_hole); for (p = pillars) translate(p) circle(d = 4.4); if (front) { translate(S) circle(d = 4.4); for (p = posts) translate(p) circle(d = 4.4); } } } // The wall bar hangs on a screw through its keyhole; its nut layer hides // the pillar nuts so it lies flat on the wall module wall_bar(nut_layer) { difference() { hull() for (p = [pillars[2], pillars[3]]) translate(p) circle(r = 11); for (p = [pillars[2], pillars[3]]) translate(p) circle(d = nut_layer ? 8.4 : 4.4); translate([0, A.y - 4]) circle(d = 9); bar([0, A.y - 4], [0, A.y + 5], 4.4); } } // Dial: a chapter ring, so the train shows through it. 6 is left out for // the winding hole. module dial() { difference() { circle(r = 88); circle(r = 64); translate(G) circle(d = 8); for (p = posts) translate(p) circle(d = 4.4); } for (h = [1:12]) { a = 90 - 30 * h; engrave() translate(polar(84.5, a)) rotate(a) rect([5, h % 3 == 0 ? 3 : 1.6], center = true); if (h != 6) engrave() translate(polar(74, a)) text(str(h), size = 8, halign = "center", valign = "center"); } for (i = [0:59]) if (i % 5 != 0) score() line(polar(85, 6 * i), polar(87, 6 * i)); } module hand(len, tail, w, hole, boss) { difference() { union() { circle(r = boss); polygon([[-w / 2, 0], [w / 2, 0], [0.6, len], [-0.6, len]]); translate([0, len * 0.72]) rotate(45) rect(w * 1.3, center = true); hull() { circle(r = w / 2); translate([0, -tail]) circle(r = w * 0.7); } } circle(d = hole); translate([0, len * 0.72]) circle(d = w * 0.7); } } // Pendulum: a 6 mm dowel held between laminations. The two middle layers // have a channel, the outer two close it. module pendulum_top(channel) { difference() { translate([-9, -60]) rect([18, 73], r = 4); circle(d = rod_hole); if (channel) translate([-(6 + running_clearance) / 2, -61]) rect([6 + running_clearance, 51]); } } module bob(channel) { difference() { circle(r = 45); if (channel) translate([-(6 + running_clearance) / 2, -25]) rect([6 + running_clearance, 71]); } } module coupon() { holes = [rod_hole, 4 + running_clearance, tube_hole, 6 + running_clearance, 4.4]; names = ["ROD", "TURN", "TUBE", "TURN", "M4"]; difference() { rect([130, 40], r = 3); for (i = [0:4]) translate([15 + 25 * i, 25]) circle(d = holes[i]); } for (i = [0:4]) label(names[i], [15 + 25 * i, 10], 3.5); } // Assembled view. The clock hangs facing -y with its back plate's front face // at y = 0 and the wall at y = 8 t + 4; x and z are the plates' x and y, the // dial centre raised so the bob clears the floor. Depths d are measured // forward from the back plate, so a part's back face sits at y = -d. Z0 = L + 60 - A.y; function at_d(p, d) = [p.x, -d, Z0 + p.y]; function up(a) = [90, -a, 0]; function stack(p, d, n) = [for (i = [0:n - 1]) at_d(p, d + i * t)]; // Between the plates, back to front: escape wheel and anchor, the escape // pinion with the third wheel in the middle of its face, the third pinion // and centre wheel, the centre pinion and great wheel, then the ratchet and // click, the drum and its flange. 1 mm or half a layer between neighbours. d_escape = 1; d_epinion = 1 + 2 * t; d_third = 1 + 2.5 * t; d_tpinion = 1 + 3.5 * t; d_centre = 1 + 4 * t; d_cpinion = 1 + 5 * t; d_great = 1 + 5.5 * t; d_ratchet = 1 + 7.5 * t; d_drum = 1 + 8.5 * t; d_flange = 1 + 12.5 * t; d_front = 2 + 13.5 * t; // In front of the front plate: the motion works, the dial, the hands f0 = d_front + 2 * t; d_cannon = f0 + 1; d_minute = f0 + 1 + 0.5 * t; d_mpinion = f0 + 1 + 1.5 * t; d_hour = f0 + 1 + 2.5 * t; d_dial = f0 + 2 + 3.5 * t; d_hhand = f0 + 3 + 4.5 * t; d_mhand = f0 + 4 + 6.5 * t; // Behind the back plate: the pendulum, then the wall bar d_pend = -(2 * t + 1) - 4 * t; d_wall = -(6 * t + 4); // The time and the swing set every wheel. Locked on a pallet (or riding its // impulse face near the middle of the swing), the escape wheel's angle comes // from the anchor; the train follows it back to the minute arbor, which is // within 0.1 degree of the time shown. // Where the segment p to q crosses the circle of radius r round the origin function seg_circle(p, q, r) = let (v = q - p, a = v * v, b = 2 * (p * v), c = p * p - r * r, s = sqrt(max(0, b * b - 4 * a * c)), u = (-b - s) / (2 * a)) p + v * (u >= 0 && u <= 1 ? u : (-b + s) / (2 * a)); pivot = [0, anchor_height]; // The leading corner of the engaged tooth, round the escape wheel's centre contact = swing >= s0 ? pivot + polar(ro, tip_at(ro, -1)) : swing >= 0 ? seg_circle(pivot + polar(ro, eo + swing), pivot + polar(ri, ei + swing), R) : swing > -s0 ? seg_circle(pivot + polar(ri, xi + swing), pivot + polar(ro, xo + swing), R) : pivot + polar(ri, tip_at(ri, 1)); gamma = atan2(contact.y, contact.x); pa = 360 / z_escape; time_angle = -6 * (60 * (hour % 12) + minute); a_escape = gamma + pa * round((60 * time_angle - gamma) / pa); a_minute = a_escape / 60; a_third = -a_minute * z_centre / z_leaf; a_great = -a_minute * z_leaf / z_great; a_stud = -a_minute * z_cannon / z_minute; a_hour = a_minute / 12; // A driven gear's turn so its teeth fall between the driver's: driver at c1 // turned r1, driven at c2 function mesh(c1, r1, z1, c2, z2) = let (b = atan2(c2.y - c1.y, c2.x - c1.x)) b + 180 + 180 / z2 + (b - r1) * z1 / z2; // Each wheel's turn when the minute arbor is at 0, its pinion at 0 o_third = mesh(E, 0, z_leaf, T, z_third); o_centre = mesh(T, 0, z_leaf, M, z_centre); o_great = mesh(M, 0, z_leaf, G, z_great); o_minute = mesh(M, 0, z_cannon, S, z_minute); o_hour = mesh(S, 0, z_leaf, M, z_hour); r_great = a_great + o_great; // The bob's centre, L below the anchor arbor bob_at = A + polar(L, swing - 90); // The assembly steps in build order, named first so they number that way build_steps = [ "Bolt the wall bar and pads behind the back plate", "Set the escapement and the gear train on the back plate", "Add the ratchet, click and drum to the great wheel", "Close the movement with the front plate on its pillars", "Fit the motion works, the dial, the hands and the winding knob in front", "Hang the pendulum on the anchor arbor behind the back plate" ]; if (output == "all") for (k = build_steps) step(k) {} if (output == "all" || output == "frame") { step(build_steps[0]) part("Back plate", qty = 2, at = stack(M, -2 * t, 2), rotate = up(0)) plate(false); step(build_steps[3]) part("Front plate", qty = 2, at = stack(M, d_front, 2), rotate = up(0)) plate(true); step(build_steps[0]) part("Wall bar", at = at_d(M, d_wall - t), rotate = up(0)) wall_bar(false); step(build_steps[0]) part("Wall bar, nut layer", at = at_d(M, d_wall - 2 * t), rotate = up(0)) wall_bar(true); step(build_steps[0]) part("Wall pad", qty = 2, at = [at_d(pillars[0], d_wall - t), at_d(pillars[1], d_wall - t)], rotate = up(0)) disc(10, 4.4); step(build_steps[0]) part("Wall pad, nut layer", qty = 2, at = [at_d(pillars[0], d_wall - 2 * t), at_d(pillars[1], d_wall - 2 * t)], rotate = up(0)) disc(10, 8.4); } if (output == "all" || output == "train") { step(build_steps[1]) part("Great wheel", qty = 2, at = stack(G, d_great, 2), rotate = up(r_great)) difference() { wheel(z_great, 4 + running_clearance, 47, 6); // Holes for the click's pivot and spring pins translate(click_pin) circle(d = rod_hole); translate(spring_pin) circle(d = rod_hole); } step(build_steps[1]) part("Centre pinion", qty = 3, at = stack(M, d_cpinion, 3), rotate = up(a_minute)) pinion(z_leaf, rod_hole); step(build_steps[1]) part("Centre wheel", at = at_d(M, d_centre), rotate = up(a_minute + o_centre)) wheel(z_centre, rod_hole); step(build_steps[1]) part("Third pinion", qty = 2, at = stack(T, d_tpinion, 2), rotate = up(a_third)) pinion(z_leaf, rod_hole); step(build_steps[1]) part("Third wheel", at = at_d(T, d_third), rotate = up(a_third + o_third)) wheel(z_third, rod_hole); step(build_steps[1]) part("Escape pinion", qty = 2, at = stack(E, d_epinion, 2), rotate = up(a_escape)) pinion(z_leaf, rod_hole); } if (output == "all" || output == "escapement") { step(build_steps[1]) part("Escape wheel", qty = 2, at = stack(E, d_escape, 2), rotate = up(a_escape)) escape_wheel(); step(build_steps[1]) part("Anchor", qty = 2, at = stack(A, d_escape, 2), rotate = up(swing)) anchor(); } if (output == "all" || output == "winding") { step(build_steps[2]) part("Drum", qty = 4, at = stack(G, d_drum, 4), rotate = up(r_great)) disc(drum_r); step(build_steps[2]) part("Drum flange", at = at_d(G, d_flange), rotate = up(r_great)) disc(28); step(build_steps[2]) part("Ratchet wheel", at = at_d(G, d_ratchet), rotate = up(r_great)) ratchet(); step(build_steps[2]) part("Click", at = at_d(G, d_ratchet), rotate = up(r_great)) click(); step(build_steps[4]) part("Winding knob", qty = 2, at = stack(G, d_hhand, 2), rotate = up(r_great)) knob(); } if (output == "all" || output == "dial") { step(build_steps[4]) part("Cannon pinion", qty = 2, at = stack(M, d_cannon, 2), rotate = up(a_minute)) pinion(z_cannon, rod_hole); step(build_steps[4]) part("Minute wheel", at = at_d(S, d_minute), rotate = up(a_stud + o_minute)) wheel(z_minute, 6 + running_clearance, 8, 4); step(build_steps[4]) part("Minute pinion", qty = 2, at = stack(S, d_mpinion, 2), rotate = up(a_stud)) pinion(z_leaf, 6 + running_clearance); step(build_steps[4]) part("Hour wheel", at = at_d(M, d_hour), rotate = up(a_hour + o_hour)) wheel(z_hour, tube_hole, 8, 4); step(build_steps[4]) part("Dial", at = at_d(M, d_dial), rotate = up(0)) dial(); step(build_steps[4]) part("Hour hand", at = at_d(M, d_hhand), rotate = up(a_hour)) hand(48, 10, 5, tube_hole, 6.5); step(build_steps[4]) part("Minute hand", at = at_d(M, d_mhand), rotate = up(a_minute)) hand(84, 16, 4, rod_hole, 5.5); } if (output == "all" || output == "pendulum") { // Front to back: the engraved layer, two channel layers, the back step(build_steps[5]) part("Pendulum top", qty = 2, at = [at_d(A, d_pend + 3 * t), at_d(A, d_pend)], rotate = up(swing)) pendulum_top(false); step(build_steps[5]) part("Pendulum top, channel", qty = 2, at = stack(A, d_pend + t, 2), rotate = up(swing)) pendulum_top(true); step(build_steps[5]) part("Bob", at = at_d(bob_at, d_pend + 3 * t), rotate = up(swing)) { bob(false); engrave() translate([0, 8]) text("HOURS", size = 13, halign = "center", valign = "center"); label(str(beat, " s BEAT"), [0, -10], 5); label("UP: FASTER", [0, -24], 4); } step(build_steps[5]) part("Bob back", at = at_d(bob_at, d_pend), rotate = up(swing)) bob(false); step(build_steps[5]) part("Bob, channel", qty = 2, at = stack(bob_at, d_pend + t, 2), rotate = up(swing)) bob(true); } if (output == "all" || output == "coupon") part("Fit test", at = [150, -80, 0]) coupon(); echo(str("Hours: ", beat, " s beat, pendulum ", round(L), " mm from arbor to bob centre; ", z_escape, "-tooth escape wheel turns once a minute; train ", z_centre, "/", z_leaf, " x ", z_third, "/", z_leaf, " = ", z_centre * z_third / (z_leaf * z_leaf), " from minute arbor to escape wheel; motion works ", z_minute, "/", z_cannon, " x ", z_hour, "/", z_leaf, " = ", z_minute * z_hour / (z_cannon * z_leaf), ":1; great wheel turns once in ", z_great / z_leaf, " h, so ", round(drop / (PI * (2 * drum_r + 1)) * z_great / z_leaf), " h on a ", drop, " mm drop; dowel ", round(L + 8), " mm")); // Assembly, depths forward from the back plate's front face (3 mm stock): // glue the plate pairs, the laminated wheels and pinions on a 4 mm rod, the // drum (ratchet wheel, four drums, flange) and the pendulum layers round a // 6 mm dowel. The arbors are 4 mm steel rod turning in 6 mm brass tube bushes // (4 mm bore) glued in both plates, with brass tube collars between the // wheels. Back to front: escape wheel and anchor at 1, escape pinion 7 with // the third wheel 8.5, third pinion 11.5 with the centre wheel 13, centre // pinion 16 with the great wheel 17.5 (free on its arbor, the click on two // rod pins in front of it), ratchet 23.5, drum to 41.5, front plate 42.5. // The pillars are four M4 bolts in 42.5 mm tubes; 16 mm tubes behind carry // the wall bar and pads, nuts sunk in their nut layers. In front go the cannon // pinion (glued), the minute wheel and pinion on a brass sleeve on an M4 stud, // the hour wheel on a 6 mm brass hour pipe over the arbor, the dial on four // M4 posts, the hands and the knob. Glue the pendulum top on the anchor arbor // behind the back plate, the bob's centre L below it. Hang about 2 kg on a // string wound on the left of the drum, and level the clock until it ticks // evenly. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tally/ # Tally odometer counter Tally: a hand-cranked counter with an odometer carry. Each turn of the crank moves the units drum on one digit; as any drum rolls from 9 to 0, two teeth on its side turn a pinion a quarter turn, and the pinion moves the next drum on one digit. In between, the pinions are locked, so the count in the lid's window can't slip. This design has 108 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tally). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tally/tally.kerf) - [tally-sheet-1.svg](https://kerfscript.mylasertools.com/models/tally/tally-sheet-1.svg) - [tally-sheet-1.dxf](https://kerfscript.mylasertools.com/models/tally/tally-sheet-1.dxf) - [tally-sheet-2.svg](https://kerfscript.mylasertools.com/models/tally/tally-sheet-2.svg) - [tally-sheet-2.dxf](https://kerfscript.mylasertools.com/models/tally/tally-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Digit ring | 4 | 66 × 66 mm | | Carry gear | 4 | 58.5 × 52.5 mm | | Lock disc | 4 | 55.7 × 56 mm | | Drum disc | 20 | 62.3 × 62.3 mm | | Tile 0 | 4 | 15 × 19.2 mm | | Tile 1 | 4 | 15 × 19.2 mm | | Tile 2 | 4 | 15 × 19.2 mm | | Tile 3 | 4 | 15 × 19.2 mm | | Tile 4 | 4 | 15 × 19.2 mm | | Tile 5 | 4 | 15 × 19.2 mm | | Tile 6 | 4 | 15 × 19.2 mm | | Tile 7 | 4 | 15 × 19.2 mm | | Tile 8 | 4 | 15 × 19.2 mm | | Tile 9 | 4 | 15 × 19.2 mm | | Shaft spacer | 1 | 16 × 16 mm | | Pinion gear | 8 | 30 × 30 mm | | Pinion lock | 4 | 39 × 39 mm | | Crank hub | 3 | 18 × 18 mm | | Crank inner | 1 | 35.1 × 48.2 mm | | Crank outer | 1 | 35.1 × 48.2 mm | | Handle ring | 4 | 16 × 16 mm | | Cap ring | 2 | 16 × 16 mm | | Cap | 2 | 16 × 16 mm | | Base | 1 | 105.5 × 106 mm | | Left wall | 1 | 106 × 75 mm | | Right wall | 1 | 106 × 75 mm | | Bearing pad | 1 | 40 × 40 mm | | Front | 1 | 105.5 × 75 mm | | Back | 1 | 105.5 × 75 mm | | Lid | 1 | 105.5 × 106 mm | | Lid locator | 2 | 79.5 × 8 mm | | Fit test | 1 | 118 × 50 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `wheels` | `4` | 3 to 5 | | `backlash` | `0.4` | 0.3 to 0.6 | | `lock_clearance` | `0.3` | 0.2 to 0.4 | | `bearing_clearance` | `0.3` | 0.2 to 0.4 | | `count` | `2026` | 0 to 99999 | | `crank_angle` | `100` | 0 to 355 | | `output` | `"all"` | All parts, Digit drums, Carry pinions, Crank, Box and lid, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Tally: a hand-cranked counter with an odometer carry. Each turn of the // crank moves the units drum on one digit; as any drum rolls from 9 to 0, // two teeth on its side turn a pinion a quarter turn, and the pinion moves // the next drum on one digit. In between, the pinions are locked, so the // count in the lid's window can't slip. /* [Counter] */ // Number of digit drums wheels = 4; // [3:1:5] /* [Fit] */ // Total play between two meshing gears, at the pitch circle backlash = 0.4; // [0.3:0.05:0.6] // Gap between a lock disc and the pinion that rides on it lock_clearance = 0.3; // [0.2:0.05:0.4] // Added to the 6 mm and 4 mm holes that turn on the shafts bearing_clearance = 0.3; // [0.2:0.05:0.4] /* [Assembly] */ // Count shown in the window of the assembled view count = 2026; // [0:1:99999] // Crank angle from straight up; the count moves on between 324 and 360 crank_angle = 100; // [0:5:355] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, drums:Digit drums, carry:Carry pinions, crank:Crank, box:Box and lid, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 144; t = $thickness; n = wheels; // Gears: module 3, a 20 tooth ring on every drum, 8 tooth pinions, so the // pinion shaft is (20 + 8) / 2 x 3 = 42 mm from the drum shaft m = 3; a = 14 * m; // A drum turns from 9 to 0 while its angle goes from -9 to 27 degrees; that // is the window where its two teeth turn the pinion. Digit d shows at 27 + 36d. tc = -9; // Lock discs, and the pinion's lock star with lobes on four of its teeth lock_radius = 28; star_radius = 19.5; // Digit drums: decagons with a tile glued on each face drum_face = 30; tile_width = 2 * drum_face * tan(18) - 0.3; drum_layers = 5; // The drum shaft runs 38 mm above the base, the pinion shaft 42 mm behind it shaft_z = 38; box_w = t * (1 + 8 * n) + 0.5; box_front = 36; box_back = a + 22; box_d = box_front + box_back; box_h = 75; crank_radius = 30; assert(n >= 3 && n <= 5 && floor(n) == n, "Wheels: 3, 4 or 5"); assert(backlash >= 0.3 && backlash <= 0.6, "Backlash: 0.3 to 0.6 mm"); assert(lock_clearance >= 0.2 && lock_clearance <= 0.4, "Lock clearance: 0.2 to 0.4 mm"); assert(bearing_clearance >= 0.2 && bearing_clearance <= 0.4, "Bearing clearance: 0.2 to 0.4 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(count >= 0 && count <= 99999 && floor(count) == count, "Count: whole numbers 0 to 99999"); assert(crank_angle >= 0 && crank_angle < 360, "Crank angle: 0 to 360"); assert(contains(["all", "drums", "carry", "crank", "box", "coupon"], output), "Unknown output"); function polar(r, ang) = [r * cos(ang), r * sin(ang)]; module label(s, p, size = 4) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } // Involute teeth with a 20 degree pressure angle, as in Celestial. Teeth not // in the list are left out down to the root circle; tip can be raised for // the cutting copy of a pinion. function gear_inv(ang) = tan(ang) * 180 / PI - ang; function gear_half(z, r, bl) = let (rp = m * z / 2, rb = rp * cos(20), ar = acos(min(1, rb / r))) 90 / z + gear_inv(20) - gear_inv(ar) - bl / (4 * rp) * 180 / PI; function gear_outline(z, bl, teeth, tip, steps = 12) = let (rp = m * z / 2, rb = rp * cos(20), lo = rp - 1.25 * m, hi = is_undef(tip) ? rp + m : tip, start = max(lo, rb), hs = gear_half(z, start, bl), ht = gear_half(z, hi, bl)) [for (k = [0:z - 1]) let (c = 360 * k / z) each ((is_undef(teeth) || contains(teeth, k)) ? concat( [polar(lo, c - hs)], [for (j = [(start > lo ? 0 : 1):steps]) let (r = start + (hi - start) * j / steps) polar(r, c - gear_half(z, r, bl))], [for (j = [1:5]) polar(hi, c - ht + 2 * ht * j / 6)], [for (j = [steps:-1:(start > lo ? 0 : 1)]) let (r = start + (hi - start) * j / steps) polar(r, c + gear_half(z, r, bl))], [if (start > lo) polar(lo, c + hs)], [for (j = [1:5]) polar(lo, c + hs + (360 / z - 2 * hs) * j / 6)] ) : [for (j = [0:11]) polar(lo, c - 180 / z + 30 * j / z)])]; // The pinion as cut, and a fatter copy (teeth 0.15 mm fuller each side, // tips 0.3 mm longer) that trims the two carry teeth, for clearance pinion_pts = gear_outline(8, backlash); pinion_cutter = gear_outline(8, -0.3, undef, 4 * m + m + 0.3); two_teeth = gear_outline(20, backlash, [0, 19]); ring_pts = gear_outline(20, backlash); // The lock star: a disc of star_radius with four concave arcs of the lock // disc's radius (plus clearance) cut from it, centred a away at 45, 135, 225 // and 315 degrees. What is left are four lobes on the teeth at 0, 90, 180 // and 270. grow makes the slightly larger copy that cuts the lock disc. function star_pts(grow) = let (r1 = star_radius + grow, rho = lock_radius + lock_clearance - grow, g = acos((r1 * r1 + a * a - rho * rho) / (2 * r1 * a))) [for (k = [0:3]) let (c = 45 + 90 * k, C = polar(a, c), pa = polar(r1, c - g), pb = polar(r1, c + g), sa = atan2(pa[1] - C[1], pa[0] - C[0]), sb = atan2(pb[1] - C[1], pb[0] - C[0]), ua = sa - 360 * round((sa - c - 180) / 360), ub = sb - 360 * round((sb - c - 180) / 360)) each concat( [for (j = [0:8]) polar(r1, c - 90 + g + (90 - 2 * g) * j / 8)], [for (j = [0:16]) C + polar(rho, ua + (ub - ua) * j / 16)])]; star_shape = star_pts(0); star_cutter = star_pts(lock_clearance); // Pinion angle while a drum turns from theta = tc to tc + 36: 2.5 times as // fast as the drum (20 teeth to 8), the other way, a quarter turn in all. // Before and after, it stands still with a tooth pointing at the drum shaft. function pinion_at(th) = 225 - 2.5 * clamp(th - tc, 0, 36); // Drum angles where the pinion is still close enough to matter before = [for (th = [tc - 20:0.5:tc]) th]; after = [for (th = [tc + 36:0.5:tc + 56]) th]; during = [for (th = [tc:0.5:tc + 36]) th]; // The bore every layer of a drum turns on, and two 2 mm pins that keep its // glued layers in line module register() { circle(d = 6 + bearing_clearance); translate([6.5, 0]) circle(d = 2.1, $fn = 32); translate([0, 6.5]) circle(d = 2.1, $fn = 32); } module mark() { score() line([9, 0], [14, 0]); } module digit_ring() { difference() { polygon(ring_pts); register(); } mark(); } // Two teeth of the 20 tooth gear. The pinion only moves in the window, so // whatever its standing teeth would touch before and after is trimmed off. module carry_gear() { difference() { // Opening by 0.1 mm drops the slivers the trimming leaves offset(r = 0.1) offset(delta = -0.1) difference() { polygon(two_teeth); for (th = concat(before, after)) rotate(-th) translate([a, 0]) rotate(pinion_at(th)) polygon(pinion_cutter); } register(); } mark(); } // The pinion's lobes ride on this disc. The notch is cut by the lobe that // swings through it in the window; nowhere else can the pinion turn. module lock_disc() { difference() { circle(r = lock_radius); for (th = during) rotate(-th) translate([a, 0]) rotate(pinion_at(th)) polygon(star_cutter); register(); } mark(); } // Faces centred at 27 + 36k degrees; digit d's tile goes on the face at // 63 - 36d, so it faces up into the lid's window when the drum is at 27 + 36d module drum_disc() { difference() { polygon([for (k = [0:9]) polar(drum_face / cos(18), 9 + 36 * k)]); register(); } mark(); } module tile(d) { rect([drum_layers * t, tile_width]); engrave() translate([drum_layers * t / 2, tile_width / 2]) text(str(d), size = 13, halign = "center", valign = "center"); } module pinion_gear() { difference() { polygon(pinion_pts); circle(d = 4 + bearing_clearance); translate([6, 0]) circle(d = 2.1, $fn = 32); } } module pinion_lock() { difference() { polygon(star_shape); circle(d = 4 + bearing_clearance); translate([6, 0]) circle(d = 2.1, $fn = 32); } score() line([9, 0], [star_radius - 3, 0]); } // The crank: hub discs turn in the right wall's bore; the outer crank layer // traps the handle bolt's nut. The handle is at 63 degrees, so it points up // once a count is done. module crank_hub() { difference() { circle(d = 18); register(); } } module crank(trap) { difference() { hull() { circle(r = 13); translate(polar(crank_radius, 63)) circle(r = 8.5); } register(); translate(polar(crank_radius, 63)) if (trap) circle(r = 4.2, $fn = 6); else circle(d = 4.4); } if (!trap) label("TALLY", polar(18, 63) + [-7, 3], 3.5); } // Box panels, with the drum shaft at x = 0 of the inside, y = 0 of the depth module side_wall(right) { difference() { finger_rect([box_d + 2 * t, box_h], ["flat", "out", "flat", "out"]); translate([t + box_front, shaft_z]) circle(d = right ? 18.5 : 6 + bearing_clearance); translate([t + box_front + a, shaft_z]) circle(d = 4); } // Which way to turn, round the bearing pad if (right) score() translate([t + box_front, shaft_z]) { arc(r = 25, start = 200, end = 340); line(polar(25, 340), polar(25, 340) + polar(4, 220)); line(polar(25, 340), polar(25, 340) + polar(4, 280)); } } module front_panel() { finger_rect([box_w + 2 * t, box_h], ["flat", "in", "flat", "in"]); label("TALLY", [t + box_w / 2, 42], 16); label("ONE TURN, ONE COUNT", [t + box_w / 2, 24], 4.5); } module back_panel() { finger_rect([box_w + 2 * t, box_h], ["flat", "in", "flat", "in"]); } // Drawn as it sits on the box: a window over the top tile of each drum module lid() { difference() { rect([box_w + 2 * t, box_d + 2 * t], r = 1); for (i = [0:n - 1]) translate([2 * t + 8 * t * i + 1, t + box_front - 8]) rect([drum_layers * t - 2, 16], r = 1); } score() translate([2 * t - 2, t + box_front - 10]) rect([8 * t * (n - 1) + drum_layers * t + 4, 20], r = 2); label("TALLY", [t + box_w / 2, t + box_front + 30], 12); label(str(n, " DIGIT COUNTER"), [t + box_w / 2, t + box_front + 19], 4); } module coupon() { difference() { rect([118, 50], r = 3); for (i = [0:2]) translate([12 + 14 * i, 14]) circle(d = 6.2 + 0.1 * i); for (i = [0:2]) translate([12 + 14 * i, 36]) circle(d = 4.2 + 0.1 * i); translate([54, 36]) circle(d = 4); for (i = [0:2]) translate([66 + 8 * i, 36]) circle(d = 2 + 0.1 * i, $fn = 32); translate([98, 25]) circle(d = 18.5); } label("6.2 6.3 6.4", [26, 4], 3); label("4.2 4.3 4.4 4.0", [33, 46], 3); label("2.0 2.1 2.2", [74, 46], 3); label("HUB 18.5", [98, 7], 3); } // The assembled view: x along the drum shaft from the inside of the left // wall, y = 0 under the drum shaft, z = 0 on the table. A part drawn round // a shaft and stood across x with on_shaft(angle) turns by that angle. shaft_at = t + shaft_z; shown = count % pow(10, n); // How far the units drum (and each drum above a run of 9s) has rolled on rolled = clamp((crank_angle - 324) / 36, 0, 1); function rolling(j) = shown % pow(10, j) == pow(10, j) - 1 ? rolled : 0; function digit(j) = floor(shown / pow(10, j)) % 10; // Drum j's angle; drum -1 is the crank function drum_angle(j) = j < 0 ? 27 + crank_angle : 27 + 36 * (digit(j) + rolling(j)); // Pinion j turns a quarter for every step drum j + 1 has made function pinion_angle(j) = 225 - 90 * (floor(shown / pow(10, j + 1)) + rolling(j + 1)); function on_shaft(ang) = [90, -ang, 90]; // Where drum j's layers start along x: carry gear, lock disc, five drum // discs, digit ring. Its pinion's first gear is one layer before. function layer_x(j) = 7 * t + 8 * t * (n - 2 - j); function drum_at(j, x) = [x, 0, shaft_at]; // A tile on the drum face at ang (as turned): it stands at 30 mm, its middle // on the face, its x along the shaft function tile_at(j, ang) = [layer_x(j) + 2 * t, drum_face * cos(ang) - tile_width / 2 * sin(ang), shaft_at + drum_face * sin(ang) + tile_width / 2 * cos(ang)]; crank_x = layer_x(-1); lid_x = -box_w - 3 * t; // Steps are numbered in the order they first run, so name them here in build order step("Glue the walls on the base and the bearing pad on the right wall") {} step("Glue up the drums and the crank's gear, with the tiles round each drum") {} step("Thread the drum shaft through the drums and the crank, and bolt on the handle") {} step("Fit the carry pinions on their rod, each with a tooth at the drum shaft") {} step("Glue the locators under the lid") {} if (output == "all" || output == "drums") { step("Glue up the drums and the crank's gear, with the tiles round each drum") { part("Digit ring", qty = n, at = [for (j = [0:n - 1]) drum_at(j, layer_x(j) + 7 * t)], rotate = [for (j = [0:n - 1]) on_shaft(drum_angle(j))]) digit_ring(); // Copy 1 is the crank's, then drums 0 to n - 2 part("Carry gear", qty = n, at = [for (j = [-1:n - 2]) drum_at(j, layer_x(j))], rotate = [for (j = [-1:n - 2]) on_shaft(drum_angle(j))]) carry_gear(); part("Lock disc", qty = n, at = [for (j = [-1:n - 2]) drum_at(j, layer_x(j) + t)], rotate = [for (j = [-1:n - 2]) on_shaft(drum_angle(j))]) lock_disc(); part("Drum disc", qty = drum_layers * n, at = [for (j = [0:n - 1], k = [0:drum_layers - 1]) drum_at(j, layer_x(j) + (2 + k) * t)], rotate = [for (j = [0:n - 1], k = [0:drum_layers - 1]) on_shaft(drum_angle(j))]) drum_disc(); for (d = [0:9]) part(str("Tile ", d), qty = n, at = [for (j = [0:n - 1]) tile_at(j, drum_angle(j) + 63 - 36 * d)], rotate = [for (j = [0:n - 1]) [drum_angle(j) + 63 - 36 * d - 90, 0, 0]]) tile(d); } step("Thread the drum shaft through the drums and the crank, and bolt on the handle") part("Shaft spacer", at = drum_at(n - 1, 0), rotate = on_shaft(drum_angle(n - 1))) difference() { circle(d = 16); circle(d = 6 + bearing_clearance); } } if (output == "all" || output == "carry") step("Fit the carry pinions on their rod, each with a tooth at the drum shaft") { // Two gears each: one in the upper drum's ring, one in the lower drum's carry gear part("Pinion gear", qty = 2 * n, at = [for (j = [-1:n - 2], k = [0:1]) [layer_x(j) - t + k * t, a, shaft_at]], rotate = [for (j = [-1:n - 2], k = [0:1]) on_shaft(pinion_angle(j))]) pinion_gear(); part("Pinion lock", qty = n, at = [for (j = [-1:n - 2]) [layer_x(j) + t, a, shaft_at]], rotate = [for (j = [-1:n - 2]) on_shaft(pinion_angle(j))]) pinion_lock(); } if (output == "all" || output == "crank") step("Thread the drum shaft through the drums and the crank, and bolt on the handle") { crank_turn = on_shaft(drum_angle(-1)); handle = polar(crank_radius, 63 + drum_angle(-1)); part("Crank hub", qty = 3, at = [for (k = [2:4]) drum_at(-1, crank_x + k * t)], rotate = crank_turn) crank_hub(); part("Crank inner", at = drum_at(-1, crank_x + 5 * t), rotate = crank_turn) crank(true); part("Crank outer", at = drum_at(-1, crank_x + 6 * t), rotate = crank_turn) crank(false); // On a brass tube over the handle bolt, past a 0.8 mm washer part("Handle ring", qty = 4, at = [for (k = [0:3]) [crank_x + 7 * t + 0.8 + k * t, handle[0], shaft_at + handle[1]]], rotate = crank_turn) ring(16, 6.2); part("Cap ring", qty = 2, at = [drum_at(-1, -2 * t), drum_at(-1, crank_x + 7 * t + 0.5)], rotate = crank_turn) ring(16, 6); part("Cap", qty = 2, at = [drum_at(-1, -3 * t), drum_at(-1, crank_x + 8 * t + 0.5)], rotate = crank_turn) circle(d = 16); } if (output == "all" || output == "box") { step("Glue the walls on the base and the bearing pad on the right wall") { part("Base", at = [-t, -box_front - t, 0]) rect([box_w + 2 * t, box_d + 2 * t], r = 1); part("Left wall", at = [-t, -box_front - t, t], rotate = [90, 0, 90]) side_wall(false); part("Right wall", at = [box_w, -box_front - t, t], rotate = [90, 0, 90]) side_wall(true); part("Bearing pad", at = [box_w + t, 0, shaft_at], rotate = [90, 0, 90]) ring(40, 18.5); part("Front", at = [-t, -box_front, t], rotate = [90, 0, 0]) front_panel(); part("Back", at = [-t, box_back + t, t], rotate = [90, 0, 0]) back_panel(); } step("Glue the locators under the lid") { // The lid is off, lying on the table against the left wall, pulled // forward 50 mm so its name shows part("Lid", at = [lid_x, -box_front - t - 50, t]) lid(); part("Lid locator", qty = 2, at = [[lid_x + t + 10, -box_front - 50, 0], [lid_x + t + 10, box_back - 58, 0]]) rect([box_w - 20, 8]); } } if (output == "all" || output == "coupon") part("Fit test", at = [0, -box_front - t - 70, 0]) coupon(); echo(str("Tally: ", n, " drums, counts to ", pow(10, n) - 1, "; pinion shaft ", a, " mm behind the drum shaft; box ", box_w + 2 * t, " x ", box_d + 2 * t, " x ", box_h + 2 * t, " mm; drum shaft ", 8 * n * t + 9 * t + 0.5, " mm long, pinion shaft ", box_w + 2 * t, " mm")); // Assembly, x along the drum shaft from the inside of the left wall: glue // the finger-jointed walls on the base, the bearing pad round the right // wall's bore, the locators under the lid. Each drum, left to right, is a // carry gear and lock disc (not on the leftmost), five drum discs and a digit // ring on two 2 mm steel pins, tiles 0 to 9 round its faces; the crank is a // carry gear, lock disc, three hubs and the two crank layers. Thread the 6 mm // steel rod through the shaft spacer, the drums and the crank, a plywood cap // glued on each end. Each carry pinion is two gears and a lock on a 2 mm pin, // on the 4 mm steel rod (glued in the walls) between 6 mm brass tubes 6t and // 5t long. Set the drums to 0000 and every pinion with a tooth at the drum // shaft. The handle rings turn on a 6 mm brass tube over an M4 bolt, washers // each end, the nut trapped in the inner crank. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/cascade/ # Cascade binary marble adder Cascade: a marble computer that adds in binary. A column of rockers on a leaning board holds a number, one bit each: 1, 2, 4, 8 and 16 from the top. A marble that meets a 0 sets it to 1 and drops into the return channel; one that meets a 1 sets it back to 0 and rolls on to the next bit, the carry. Drop a marble into the slot marked 4 to add 4; lift the bar behind to reset. This design has 75 parts on 4 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=cascade). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/cascade/cascade.kerf) - [cascade-sheet-1.svg](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-1.svg) - [cascade-sheet-1.dxf](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-1.dxf) - [cascade-sheet-2.svg](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-2.svg) - [cascade-sheet-2.dxf](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-2.dxf) - [cascade-sheet-3.svg](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-3.svg) - [cascade-sheet-3.dxf](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-3.dxf) - [cascade-sheet-4.svg](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-4.svg) - [cascade-sheet-4.dxf](https://kerfscript.mylasertools.com/models/cascade/cascade-sheet-4.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Board face | 1 | 184 × 476 mm | | Board back | 1 | 184 × 476 mm | | Stand foot | 4 | 205 × 103.1 mm | | Rocker face | 5 | 68 × 26.6 mm | | Rocker | 10 | 68 × 26.6 mm | | Reset lever | 10 | 60 × 10 mm | | Reset bar face | 1 | 44 × 493 mm | | Reset bar | 1 | 44 × 493 mm | | Chute rail | 15 | 95 × 32.2 mm | | Stop rail | 12 | 51 × 47.6 mm | | Last stop rail | 3 | 51 × 30.6 mm | | Hopper | 3 | 119 × 90.2 mm | | Return wall | 3 | 6 × 382 mm | | Tray floor | 1 | 152 × 80 mm | | Tray side | 2 | 80 × 25 mm | | Tray end | 2 | 146 × 25 mm | | Fit test | 1 | 120 × 44 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `bits` | `5` | 3 to 6 | | `marble` | `16` | 16 mm glass marbles, 12.7 mm steel balls | | `lean` | `20` | 16 to 24 | | `bush_bore` | `6.3` | 6.1 to 6.5 | | `value` | `13` | 0 to 63 | | `output` | `"all"` | All parts, Board and stand, Rockers and reset, Rails, Return tray, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Cascade: a marble computer that adds in binary. A column of rockers on a // leaning board holds a number, one bit each: 1, 2, 4, 8 and 16 from the top. // A marble that meets a 0 sets it to 1 and drops into the return channel; one // that meets a 1 sets it back to 0 and rolls on to the next bit, the carry. // Drop a marble into the slot marked 4 to add 4; lift the bar behind to reset. /* [Counter] */ // Number of bits; the board gets taller with each one bits = 5; // [3:1:6] // What the channels are sized for marble = 16; // [16:16 mm glass marbles, 12.7:12.7 mm steel balls] // How far the board leans back from upright, in degrees lean = 20; // [16:1:24] /* [Fit] */ // Hole the brass bush of each rocker turns in, in the board bush_bore = 6.3; // [6.1:0.1:6.5] /* [Assembly] */ // The number the rockers hold in the assembled view, 0 to 2^bits - 1 value = 13; // [0:1:63] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, board:Board and stand, rockers:Rockers and reset, rails:Rails, tray:Return tray, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; D = marble; n = bits; // Board coordinates, seen from the front: x = 0 down the column of pivots, // y = 0 at the bottom edge. Bit k (value 2^k) pivots at pivot(k), the // lowest bit at the top. pitch = 78; low_pivot = 40; function pivot(k) = [0, low_pivot + (n - 1 - k) * pitch]; top = pivot(0)[1]; board_x = [-104, 80]; board_h = top + 124; // Rocker: tips 20 degrees either way. The apex sits 17 mm above the pivot, // so whichever way it leans the marble lands on the raised side; each side // slopes 30 degrees down to a cup 20 mm out, then rises 14 degrees to the tip. tip = 20; apex = 17; cup_x = 20; end_x = 34; // The underside is a V that lies flat on the stop below in either position keel = 10; cup_y = apex - cup_x * tan(30); end_y = cup_y + (end_x - cup_x) * tan(14); under_y = -keel + end_x * tan(tip); // Top of the stop under each rocker stop_y = -keel * cos(tip); // Channels: one marble wide with 1 mm to spare each side, 6 mm walls half = D / 2 + 1; wall = 6; // The throat above each rocker that centres the marble on the pivot line throat = [30.5, 42.5]; // Chutes slope 12 degrees down from the left edge to the throat function chute_y(x) = throat[1] + (-half - x) * tan(12); // The return channel's left wall, just clear of the rocker tips ret_x = 36; ret_w = D + 2; // Feet: cross-halved with the board 60 mm up from its bottom edge, which // stands 45 mm above the table feet_x = [-92, 66]; lap = 60; rise = 45; // The reset bar's two guide bolts and its travel guide_x = 43; guides = [pivot(n - 1)[1] + 30, pivot(1)[1] + 30]; stroke = 12; lever = 25; tray_w = feet_x[1] - feet_x[0] - 2 * t; assert(n >= 3 && n <= 6 && floor(n) == n, "Bits: whole numbers 3 to 6"); assert(D == 16 || D == 12.7, "Marble: 16 or 12.7 mm"); assert(lean >= 16 && lean <= 24, "Lean: 16 to 24 degrees"); assert(value >= 0 && value < pow(2, n) && floor(value) == value, "Value: whole numbers 0 to 2^bits - 1"); assert(bush_bore >= 6.1 && bush_bore <= 6.5, "Bush bore: 6.1 to 6.5 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "board", "rockers", "rails", "tray", "coupon"], output), "Unknown output"); module hole(p, d = 4.4) { translate(p) circle(d = d); } module label(s, p, size = 5) { engrave() translate(p) text(s, size = size, halign = "center", valign = "center"); } // Pivot at the origin, level. Rounded corners so the marble rolls off the // tips cleanly. module rocker(face = false) { difference() { offset(r = 2) offset(r = -2) polygon([[-end_x, end_y], [-cup_x, cup_y], [0, apex], [cup_x, cup_y], [end_x, end_y], [end_x, under_y], [0, -keel], [-end_x, under_y]]); hole([0, 0]); } // Whichever end is down shows the bit if (face) { label("0", [-24.5, 2.4], 6); label("1", [24.5, 2.4], 6); } } // Drawn about its own pivot: the floor of the chute into this bit, from the // board's left edge to the throat, and the throat's left wall module chute_rail() { x0 = board_x[0]; x1 = -half - wall; drop = 7 / cos(12); polygon([[x0, chute_y(x0)], [-half, throat[1]], [-half, throat[0]], [x1, throat[0]], [x1, chute_y(x1) - drop], [x0, chute_y(x0) - drop]]); } // Drawn about its own pivot: the stop the rocker rests on, a slope that // sends a marble off the right tip into the return channel, the channel's // left wall down to the next bit, and the right wall of the next throat. // Its underside clears the next rocker and a marble riding in its cup. module stop_rail(last = false) { slope = stop_y - 1.5 - (ret_x - 15) * tan(30); // The V's point swings below the stop's top as the rocker passes level notch = keel * sin(tip) + 1.6; below = -pitch + throat[0]; if (last) polygon([[-15, stop_y], [-notch, stop_y], [-notch, stop_y - 1.5], [notch, stop_y - 1.5], [notch, stop_y], [15, stop_y], [15, stop_y - 1.5], [ret_x, slope], [ret_x, -low_pivot], [ret_x - wall, -low_pivot], [ret_x - wall, slope - 7], [half, stop_y - 6.1], [-15, stop_y - 6.1]]); else polygon([[-15, stop_y], [-notch, stop_y], [-notch, stop_y - 1.5], [notch, stop_y - 1.5], [notch, stop_y], [15, stop_y], [15, stop_y - 1.5], [ret_x, slope], [ret_x, -pitch + 21], [ret_x - wall, -pitch + 21], [21, below], [half, below], [half, stop_y - 6.1], [-15, stop_y - 6.1]]); } // Drawn about the top pivot: the right wall of the first throat and a roof // over the first chute, opening into a funnel at the left edge module hopper() { x0 = board_x[0]; polygon([[half, throat[0]], [half + wall, throat[0]], [half + wall, chute_y(half + wall) + 40], [x0, chute_y(x0) + 58], [x0, chute_y(x0) + 50], [x0 + 30, chute_y(x0 + 30) + 32], [half, chute_y(half) + 32]]); } module board(face = false) { difference() { translate([board_x[0], 0]) rect([board_x[1] - board_x[0], board_h], r = 4); for (k = [0:n - 1]) hole(pivot(k), bush_bore); // Countersunk from the front, flush with the floor of the return channel for (y = guides) hole([guide_x, y]); for (x = feet_x) translate([x - t - 0.125, -1]) rect([2 * t + 0.25, lap / 2 + 1.25]); } if (face) { for (k = [0:n - 1]) label(str(pow(2, k)), [-86, pivot(k)[1] + chute_y(board_x[0]) + 16], 9); label("CASCADE", [50, top + 84], 9); label("BINARY MARBLE ADDER", [50, top + 72], 3.6); label("LOW END = 1", [50, top + 64], 3.6); } } module lever() { difference() { hull() { translate([-lever, 0]) circle(r = 5); translate([lever, 0]) circle(r = 5); } hole([0, 0]); } } // Drawn where it hangs behind the board, at rest. A tab under the right end // of every lever lifts any rocker that shows 1 back to 0. module reset_bar(face = false) { difference() { union() { translate([33, 15]) rect([20, board_h - 7]); for (k = [0:n - 1]) translate([19, pivot(k)[1] - 20.5]) rect([15, 6]); translate([23, board_h + 6]) rect([40, 26], r = 4); } for (y = guides) hull() { hole([guide_x, y - stroke], 6.4); hole([guide_x, y], 6.4); } hole([43, board_h + 25], 10); } if (face) label("RESET", [43, board_h + 13], 6); } // Side view, the table at v = 0 and the board's back face rising from // (0, rise) at the lean angle. The slot from the top takes the board. function back_at(s) = [s * sin(lean), rise + s * cos(lean)]; module foot() { n_ = [-cos(lean), sin(lean)]; a = back_at(lap); difference() { polygon([[-55, 0], [150, 0], [150, 10], a, a + 2 * t * n_, [-55, 18]]); let (e = 0.125, p = back_at(lap / 2) - e * n_, q = back_at(lap + 20) - e * n_, w = (2 * t + 2 * e) * n_) polygon([p, q, q + w, p + w]); } } module coupon() { difference() { rect([120, 44], r = 3); for (i = [0:3]) hole([14 + 18 * i, 30], 6.1 + 0.1 * i); hole([86, 30]); // The feet's slot in the board and the reset bar's guide slot translate([104, 22]) rect([2 * t + 0.25, 16], center = true); hull() { hole([86, 10], 6.4); hole([104, 10], 6.4); } } engrave() translate([5, 15]) text("6.1 6.2 6.3 6.4", size = 3.5); } // Assembled view: the board's back face rises from y = 0, z = rise, leaning // back. A part drawn in board coordinates with its back face d mm in front of // the board's front face goes to board_at(p, d), turned by board_turn(a) for // a part also turned a degrees in the board's plane (a rocker's tilt). function board_at(p, d) = [p.x, -(2 * t + d) * cos(lean) + p.y * sin(lean), rise + (2 * t + d) * sin(lean) + p.y * cos(lean)]; // Standing the part up (90 - lean about x) after turning it a about z, as // the x, y, z angles that rotate takes function board_turn(a = 0) = let (b = 90 - lean) [atan2(sin(b) * cos(a), cos(b)), -asin(sin(b) * sin(a)), atan2(cos(b) * sin(a), cos(a))]; // A 1 tips its rocker's right end down function tilt(k) = floor(value / pow(2, k)) % 2 == 1 ? -tip : tip; // The return tray stands between the feet, in front of the board's foot tray_at = [feet_x[0] + t, -62, 0]; // The assembly steps, listed first so they number in build order step("Glue the two board layers together"); step("Glue the rails, the hopper and the return wall on the front, three layers each"); step("Bolt each rocker on its pivot, with its two reset levers behind the board"); step("Hang the reset bar on its two guide bolts"); step("Slide the board into the feet and stand the return tray between them"); if (output == "all" || output == "board") { step("Glue the two board layers together") { part("Board face", at = board_at([0, 0], -t), rotate = board_turn()) board(true); part("Board back", at = board_at([0, 0], -2 * t), rotate = board_turn()) board(false); } step("Slide the board into the feet and stand the return tray between them") part("Stand foot", qty = 4, rotate = [90, 0, 90], at = [for (x = feet_x, k = [0:1]) [x - t + k * t, 0, 0]]) foot(); } if (output == "all" || output == "rockers") { step("Bolt each rocker on its pivot, with its two reset levers behind the board") { // Rockers 1 mm off the board on their washers, three layers each part("Rocker face", qty = n, at = [for (k = [0:n - 1]) board_at(pivot(k), 1 + 2 * t)], rotate = [for (k = [0:n - 1]) board_turn(tilt(k))]) rocker(true); part("Rocker", qty = 2 * n, at = [for (k = [0:n - 1], j = [0:1]) board_at(pivot(k), 1 + j * t)], rotate = [for (k = [0:n - 1], j = [0:1]) board_turn(tilt(k))]) rocker(false); // Levers and bar 1 mm behind the board, two layers each part("Reset lever", qty = 2 * n, at = [for (k = [0:n - 1], j = [0:1]) board_at(pivot(k), -3 * t - 1 - j * t)], rotate = [for (k = [0:n - 1], j = [0:1]) board_turn(tilt(k))]) lever(); } step("Hang the reset bar on its two guide bolts") { part("Reset bar face", at = board_at([0, 0], -3 * t - 1), rotate = board_turn()) reset_bar(true); part("Reset bar", at = board_at([0, 0], -4 * t - 1), rotate = board_turn()) reset_bar(false); } } if (output == "all" || output == "rails") step("Glue the rails, the hopper and the return wall on the front, three layers each") { // Three layers of every rail on the board's front face part("Chute rail", qty = 3 * n, at = [for (k = [0:n - 1], j = [0:2]) board_at(pivot(k), j * t)], rotate = board_turn()) chute_rail(); part("Stop rail", qty = 3 * (n - 1), at = [for (k = [0:n - 2], j = [0:2]) board_at(pivot(k), j * t)], rotate = board_turn()) stop_rail(); part("Last stop rail", qty = 3, at = [for (j = [0:2]) board_at(pivot(n - 1), j * t)], rotate = board_turn()) stop_rail(true); part("Hopper", qty = 3, at = [for (j = [0:2]) board_at(pivot(0), j * t)], rotate = board_turn()) hopper(); part("Return wall", qty = 3, at = [for (j = [0:2]) board_at([0, 0], j * t)], rotate = board_turn()) translate([ret_x + ret_w, 0]) rect([wall, top + 30]); } if (output == "all" || output == "tray") step("Slide the board into the feet and stand the return tray between them") { part("Tray floor", at = tray_at) rect([tray_w, 80]); part("Tray side", qty = 2, rotate = [90, 0, 90], at = [tray_at + [0, 0, t], tray_at + [tray_w - t, 0, t]]) rect([80, 25]); part("Tray end", qty = 2, rotate = [90, 0, 0], at = [tray_at + [t, 3, t], tray_at + [t, 80, t]]) rect([tray_w - 2 * t, 25]); } if (output == "all" || output == "coupon") part("Fit test", at = [110, -110, 0]) coupon(); echo(str("Cascade: ", n, " bits, counts 0 to ", pow(2, n) - 1, "; board ", board_x[1] - board_x[0], " x ", board_h, " mm leaning ", lean, " degrees; rockers tip ", tip, " degrees, cups ", cup_x, " mm out")); // Assembly, front view: glue the two board layers. Glue every rail as three // layers on the front, each piece where it is drawn (chute and stop rails at // each pivot). Glue each rocker face on two plain layers. Each rocker turns // with its pivot: an M4 x 30 bolt through the rocker, an M6 washer, a 6 mm // brass tube 2t + 2 long through the board's bush hole, a washer, two glued // reset levers level across the back and a nyloc nut, tight enough to clamp // the tube, not the board. Hang the reset bar (two glued layers) on two // countersunk M4 bolts with 6 mm brass sleeves 2t + 1.4 long through its // slots, a washer each side and a nut. Slide the board into the feet (two // plies each) and stand the return tray box between them. Use 16 mm glass // marbles or 12.7 mm steel balls, one at a time. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/helix/ # Helix Archimedes screw marble lift Helix: an Archimedes screw marble lift. Turn the crank and a screw of 120 stacked plates, each a step round from the last on a square bar, pushes 16 mm marbles up a trough of dowels. At the top they drop onto a chute and roll back down a zig-zag track behind the screw to a ramp that feeds the screw again near the bottom. This design has 159 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=helix). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/helix/helix.kerf) - [helix-sheet-1.svg](https://kerfscript.mylasertools.com/models/helix/helix-sheet-1.svg) - [helix-sheet-1.dxf](https://kerfscript.mylasertools.com/models/helix/helix-sheet-1.dxf) - [helix-sheet-2.svg](https://kerfscript.mylasertools.com/models/helix/helix-sheet-2.svg) - [helix-sheet-2.dxf](https://kerfscript.mylasertools.com/models/helix/helix-sheet-2.dxf) - [helix-sheet-3.svg](https://kerfscript.mylasertools.com/models/helix/helix-sheet-3.svg) - [helix-sheet-3.dxf](https://kerfscript.mylasertools.com/models/helix/helix-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Screw plate | 120 | 39.5 × 25.9 mm | | Journal | 4 | 20 × 20 mm | | Collar | 4 | 32 × 32 mm | | Crank | 2 | 55 × 22 mm | | Grip ring | 4 | 18 × 18 mm | | Board | 1 | 312 × 348 mm | | Base | 1 | 380 × 150 mm | | Lower end plate | 2 | 100 × 58 mm | | Upper end plate | 2 | 100 × 58 mm | | Exit rib | 1 | 51.2 × 36.8 mm | | Track floor 1 | 1 | 244.9 × 27 mm | | Track rail 1 | 1 | 241.9 × 10 mm | | Track floor 2 | 1 | 249.9 × 27 mm | | Track rail 2 | 1 | 246.9 × 10 mm | | Track floor 3 | 1 | 199.7 × 27 mm | | Track rail 3 | 1 | 196.7 × 10 mm | | End wall 1 | 1 | 27 × 13 mm | | End wall 2 | 1 | 27 × 37.2 mm | | End wall 3 | 1 | 27 × 37.5 mm | | Exit floor | 1 | 32 × 79.3 mm | | Exit wall | 2 | 79 × 17 mm | | Exit front wall | 1 | 26 × 10 mm | | Intake floor | 1 | 26 × 29.8 mm | | Intake wall | 1 | 30 × 14.7 mm | | Intake back wall | 1 | 30 × 39.9 mm | | Ramp | 1 | 70 × 60.1 mm | | Fit test | 1 | 150 × 60 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `flight` | `2` | Two plates, Three plates | | `crank_radius` | `35` | 30 to 40 | | `bar_clearance` | `0.2` | 0.1 to 0.3 | | `bearing_clearance` | `0.4` | 0.3 to 0.6 | | `crank_angle` | `90` | 0 to 360 | | `output` | `"all"` | All parts, Screw and crank, Board base and end plates, Track chutes and ramp, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Helix: an Archimedes screw marble lift. Turn the crank and a screw of 120 // stacked plates, each a step round from the last on a square bar, pushes // 16 mm marbles up a trough of dowels. At the top they drop onto a chute and // roll back down a zig-zag track behind the screw to a ramp that feeds the // screw again near the bottom. /* [Screw] */ // Plates the flight tooth spans: two leaves a longer pocket, three is stronger flight = 2; // [2:Two plates, 3:Three plates] // Crank handle radius crank_radius = 35; // [30:5:40] /* [Fit] */ // Added to the 10 mm square bar in every star-shaped hole bar_clearance = 0.2; // [0.1:0.05:0.3] // Added to the 20 mm journals in the end plate bearings bearing_clearance = 0.4; // [0.3:0.1:0.6] /* [Assembly] */ // Crank angle in the assembled view, anticlockwise seen from the crank; one // turn lifts every marble one pitch crank_angle = 90; // [0:15:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, screw:Screw and crank, frame:Board base and end plates, track:Track chutes and ramp, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; D = 16; assert(flight == 2 || flight == 3, "Flight: 2 or 3 plates"); assert(crank_radius >= 30 && crank_radius <= 40, "Crank radius: 30 to 40 mm"); assert(bar_clearance >= 0.1 && bar_clearance <= 0.3, "Bar clearance: 0.1 to 0.3 mm"); assert(bearing_clearance >= 0.3 && bearing_clearance <= 0.6, "Bearing clearance: 0.3 to 0.6 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(contains(["all", "screw", "frame", "track", "coupon"], output), "Unknown output"); // The screw: plates 3 mm apart, each turned 30 degrees on from the last, so // the flight climbs a 36 mm pitch in 12 plates. Tip radius 27.5, core 12. plates = 120; step = 30; pitch = 12 * t; tip = 27.5; core = 12; // Four 6 mm dowels on a 34 mm radius, at these angles from the bottom of // the screw towards the front. The marble rides between the first two; // the others only keep it in. rod_r = 34; rods = [0, 20, 45, 70]; // Assembly coordinates: x along the base, y towards the board, z up. The // screw axis rises at 45 degrees from s = 0, the inner face of the lower end // plate, 70 mm above the table. s is measured along the axis. tilt = 45; z0 = 70; ct = cos(tilt); st = sin(tilt); avec = [ct, 0, st]; // Down the face of the end plates, and up it dvec = [st, 0, -ct]; vvec = [-st, 0, ct]; function ax(s) = [s * ct, 0, z0 + s * st]; // D below the axis along the end plate, Y towards the board function pt(s, Dn, Y) = ax(s) + dvec * Dn + [0, Y, 0]; // Along the axis: collars and screw, the end plates and the exit gap s_screw = 0.5 + t; s_top = s_screw + plates * t; s_upper = s_top + t + 0.5; // The two trough dowels stop at the exit rib; past it the marble falls s_exit = s_upper - 33; // The board's front face, and where the track runs in front of it board_y = 58; track_y = 34; // Rotations as 3 x 3 matrices, turned into rotate = [x, y, z] by euler() function mm(A, B) = [for (i = [0:2]) [for (j = [0:2]) A[i][0] * B[0][j] + A[i][1] * B[1][j] + A[i][2] * B[2][j]]]; function rz(r) = [[cos(r), -sin(r), 0], [sin(r), cos(r), 0], [0, 0, 1]]; // Parts square to the screw: drawn x towards the board, y up the end plate, // thickness up the axis. The lower end's parts face down the axis instead, // so their engraved side shows. up_axis = [[0, vvec[0], avec[0]], [1, 0, 0], [0, vvec[2], avec[2]]]; down_axis = [[0, vvec[0], -avec[0]], [-1, 0, 0], [0, vvec[2], -avec[2]]]; // A feature drawn along +x lands at angle a round the screw, measured from // the bottom towards the front, the way the screw turns function turned_up(a) = euler(mm(up_axis, rz(-90 - a))); function turned_down(a) = euler(mm(down_axis, rz(a - 90))); theta = crank_angle; // The ramp beside the trough: parallel to the axis, sloping 20 degrees down // to the lowest dowel and just clear of the flight kappa = 20; ramp_d = 29 / cos(kappa); ramp_e = [-sin(kappa) * st, cos(kappa), sin(kappa) * ct]; ramp_n = [-cos(kappa) * st, -sin(kappa), cos(kappa) * ct]; ramp_frame = [[avec[0], ramp_e[0], ramp_n[0]], [0, ramp_e[1], ramp_n[1]], [avec[2], ramp_e[2], ramp_n[2]]]; function ramp_l(Y) = Y / cos(kappa); // Chutes: floors level along x and sloping 1 in 10 across. The exit chute // catches the marble under the top of the screw; the intake chute takes it // off the track to the ramp. beta = atan(0.1); function ze(y) = 262 - 0.1 * (y + 15); function zi(y) = 120 + 0.1 * (y - 31); hole_x = 271; hole_y = 47.5; // Track: three legs at a 0.085 slope. At each turn a marble on the lower // leg passes 2 mm under the floor of the one above. slope = 0.085; sigma = atan(slope); drop = t + D + 2; l1 = ze(hole_y) - t / cos(beta) - D - 1; function top1(x) = l1 + slope * (x - hole_x); function top2(x) = top1(40) - drop - slope * (x - 40); function top3(x) = top2(270) - drop + slope * (x - 270); // [left end, right end, top at the left, rising to the right]. Each leg // stops 17 mm short of the end wall below, room for a marble to drop past. // The last stops over the intake's front wall, so a marble rolls off its // end into the chute and the back wall stops it. legs = [[40, 284, top1(40), 1], [19, 268, top2(19), -1], [89, 288, top3(89), 1]]; module ring(d, inner) { difference() { circle(d = d); circle(d = inner, $fn = 48); } } // Twelve-pointed hole: three squares 30 degrees apart, so a plate fits the // square bar in any of twelve positions 30 degrees apart module star_hole() { for (a = [0, 30, 60]) rotate(a) rect(10 + bar_clearance, center = true); } // One plate of the screw, its tooth along +x module screw_plate() { g = flight * step; difference() { union() { circle(r = core); offset(r = 2) offset(delta = -2) polygon(concat( [for (i = [0:12]) let (a = -g / 2 + g * i / 12) [tip * cos(a), tip * sin(a)]], [for (i = [12:-1:0]) let (a = -g / 2 + g * i / 12) [9 * cos(a), 9 * sin(a)]])); } star_hole(); } score() line([core + 2, 0], [tip - 3, 0]); } module round_part(d) { difference() { circle(d = d); star_hole(); } } // Drawn in the down-axis frame; the arrow shows the way to turn it module crank() { difference() { hull() { circle(r = 11); translate([crank_radius, 0]) circle(r = 9); } star_hole(); translate([crank_radius, 0]) circle(d = 4.4, $fn = 32); } score() { translate([13, -3.5]) text("TURN", size = 4); line([crank_radius - 4, -6], [crank_radius - 4, 6]); line([crank_radius - 4, 6], [crank_radius - 6, 3]); line([crank_radius - 4, 6], [crank_radius - 2, 3]); } } // x towards the board, y up the plate, the axis at the origin function rod_uv(a) = [-rod_r * sin(a), -rod_r * cos(a)]; module end_plate() { difference() { union() { hull() { circle(r = 17); for (a = rods) translate(rod_uv(a)) circle(r = 7); translate([0, -38]) rect([board_y, 52]); } for (v = [[-34, -20], [-6, 8]]) translate([board_y - 1, v[0]]) rect([t + 1, v[1] - v[0]]); } circle(d = 20 + bearing_clearance); for (a = rods) translate(rod_uv(a)) circle(d = 6.2, $fn = 32); } } // Holds the ends of the trough dowels. Its edge stays clear of the marble // over the trough and of the flight elsewhere. module exit_rib() { function arc(r, a0, a1) = [for (i = [0:16]) let (a = lerp(a0, a1, i / 16)) [-r * sin(a), -r * cos(a)]]; difference() { polygon(concat(arc(41, -15, 82), arc(29.5, 82, 30), arc(33.5, 30, -15))); for (a = rods) translate(rod_uv(a)) circle(d = 6.2, $fn = 32); } } // x along the axis from s = 30, y up the slope from the axis' plane. Wide // at the bottom, where a tab reaches the board; narrow beside the intake. // A marble landing on it rolls down until the flight below catches it. module ramp() { // The back edge stops where its lower corner meets the board lb = (board_y - t * sin(kappa)) / cos(kappa); le = (board_y + t - 0.05 - t * sin(kappa)) / cos(kappa); polygon([[0, ramp_l(3.5)], [70, ramp_l(3.5)], [70, ramp_l(29.5)], [32, ramp_l(29.5)], [32, lb], [28, lb], [28, le], [14, le], [14, lb], [0, lb]]); } // A track floor, length along the slope, 24 wide from the rail to the board module leg_floor(L) { rect([L, board_y - track_y]); for (x = [12, L / 2 - 10, L - 32]) translate([x, board_y - track_y - 1]) rect([20, t + 1]); } // Chute side wall, drawn in y and z where it stands: on the floor line z(y) // from y0 to the board, h tall, with a tab into the board module chute_wall(z, y0, h, chamfer = 0) { polygon(concat([for (y = [y0, board_y]) [y, z(y)]], [[board_y, z(board_y) + 3], [board_y + t, z(board_y) + 3], [board_y + t, z(board_y) + 9.5], [board_y, z(board_y) + 9.5]], chamfer > 0 ? [[board_y, z(board_y) + h], [y0 + chamfer, z(y0 + chamfer) + h], [y0, z(y0) + h - chamfer]] : [[board_y, z(board_y) + h], [y0, z(y0) + h]])); } // A chute floor, x from x0 to x1, l up its slope; the tab reaches y = 61 module chute_floor(x0, x1, L, le, tab0, tab1) { translate([x0, 0]) rect([x1 - x0, L]); translate([tab0, L - 1]) rect([tab1 - tab0, le - L + 1]); } // End wall of a leg, standing across the channel on the floor at zb module end_wall(zb, zt) { polygon([[track_y, zb], [board_y, zb], [board_y, zb + 3], [board_y + t, zb + 3], [board_y + t, zt - 3], [board_y, zt - 3], [board_y, zt], [track_y, zt]]); } // Slots, in the board's x and z, for a tab drawn in a turned frame module slot_in(p, a, x0, x1, y0, y1, c = 0.12) { translate([p[0], p[2]]) rotate(a) translate([x0 - c, y0 - c]) rect([x1 - x0 + 2 * c, y1 - y0 + 2 * c]); } function leg_theta(g) = -g[3] * sigma; function leg_origin(g) = [g[0] - t * sin(leg_theta(g)), track_y, g[2] - t * cos(leg_theta(g))]; function leg_len(g) = (g[1] - g[0]) / cos(sigma); function leg_top(i, x) = i == 0 ? top1(x) : i == 1 ? top2(x) : top3(x); // The backboard, drawn in x and z, with every slot that holds the frame module board() { difference() { union() { translate([-20, t]) rect([312, 348 - t], r = 6); for (x = [-10, 120, 250]) translate([x, 0]) rect([30, t + 0.5]); } // End plates, both plies for (s0 = [-2 * t, s_upper]) for (v = [[-34, -20], [-6, 8]]) slot_in(ax(s0), tilt, 0, 2 * t, v[0], v[1]); // The ramp's tab passes the board at 20 degrees let (dmin = ramp_d - ((board_y + t - 0.05 - t * sin(kappa)) / cos(kappa)) * sin(kappa), dmax = ramp_d - board_y * tan(kappa) + t / cos(kappa)) slot_in(ax(44), tilt, 0, 14, -dmax, -dmin); // Track floors and the end walls on them for (i = [0:2]) let (g = legs[i], L = leg_len(g)) for (x = [12, L / 2 - 10, L - 32]) slot_in(leg_origin(g), -leg_theta(g), x, x + 20, 0, t); for (w = [[284 - t, top1(284), 13], [19, top2(19), top1(40) + 18 - top2(19)], [288 - t, top3(288), top2(270) + 18 - top3(288)]]) slot_in([w[0], 0, w[1]], 0, 0, t, 3, w[2] - 3); // Chute floors and walls slot_in([262, 0, ze(board_y + t) - t / cos(beta)], 0, 0, 18, 0, ze(board_y) - ze(board_y + t) + t / cos(beta), 0.15); slot_in([68, 0, zi(board_y) - t / cos(beta)], 0, 0, 16, 0, zi(board_y + t) - zi(board_y) + t / cos(beta), 0.15); for (x = [255, 287 - t]) slot_in([x, 0, ze(board_y) + 3], 0, 0, t, 0, 6.5); for (x = [63, 89 - t]) slot_in([x, 0, zi(board_y) + 3], 0, 0, t, 0, 6.5); } engrave() translate([86, 290]) text("HELIX", size = 34, halign = "center"); score() translate([86, 272]) text("ARCHIMEDES SCREW MARBLE LIFT", size = 6, halign = "center"); } module base() { difference() { translate([-75, -75]) rect([380, 150], r = 6); for (x = [-10, 120, 250]) translate([x - 0.12, board_y - 0.12]) rect([30.24, t + 0.24]); } score() translate([-60, -64]) text("16 MM MARBLES / TURN THE CRANK ANTICLOCKWISE", size = 5); } module coupon() { difference() { rect([150, 60], r = 3); for (i = [0:2]) translate([20 + 25 * i, 42]) for (a = [0, 30, 60]) rotate(a) rect(10.1 + 0.1 * i, center = true); for (i = [0:3]) translate([18 + 30 * i, 16]) circle(d = 20.3 + 0.1 * i); for (i = [0:2]) translate([97 + 12 * i, 44]) circle(d = 6.1 + 0.1 * i, $fn = 32); translate([138, 40]) rect([t + 0.24, 14], center = true); } score() { translate([33, 52]) text("BAR +0.1 +0.2 +0.3", size = 3, halign = "center"); translate([109, 52]) text("6.1 6.2 6.3", size = 3, halign = "center"); translate([63, 2]) text("20.3 20.4 20.5 20.6", size = 3, halign = "center"); } } // Placements of the parts that turn with the crank, at angle theta ramp_at = ax(30) + dvec * (ramp_d + t * cos(kappa)) + [0, t * sin(kappa), 0]; // Outside face of the lower outer collar; the crank plies go on below it crank_s = -3 * t - 0.5; handle = dvec * (crank_radius * cos(theta)) + [0, -crank_radius * sin(theta), 0]; // The assembly steps, listed first so they number in build order step("Glue the screw plates on the square bar, each turned one point on from the last"); step("Slide on the end plates, glue the journals in with a collar each side, and the crank plies below"); step("Push the trough dowels through the end plates, the lower two into the exit rib"); step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base"); step("Glue the rails and the chute walls onto the floors"); step("Bolt the grip on the crank"); if (output == "all" || output == "screw") { step("Glue the screw plates on the square bar, each turned one point on from the last") part("Screw plate", qty = plates, at = [for (j = [0:plates - 1]) ax(s_screw + j * t)], rotate = [for (j = [0:plates - 1]) turned_up(theta - step * j)]) screw_plate(); step("Slide on the end plates, glue the journals in with a collar each side, and the crank plies below") { // Two in each bearing; the lower pair face down the axis part("Journal", qty = 4, at = [ax(0), ax(-t), ax(s_upper), ax(s_upper + t)], rotate = [turned_down(theta), turned_down(theta), turned_up(theta), turned_up(theta)]) round_part(20); // Inside and outside each end plate part("Collar", qty = 4, at = [ax(0.5), ax(-2 * t - 0.5), ax(s_top), ax(s_upper + 2 * t + 0.5)], rotate = [turned_up(theta), turned_down(theta), turned_up(theta), turned_up(theta)]) round_part(32); part("Crank", qty = 2, at = [ax(crank_s), ax(crank_s - t)], rotate = turned_down(theta)) crank(); } // On a 6 mm sleeve over the handle bolt, below the crank and a washer step("Bolt the grip on the crank") part("Grip ring", qty = 4, at = [for (k = [0:3]) ax(crank_s - 2 * t - 0.8 - k * t) + handle], rotate = turned_down(theta)) ring(18, 6.4); } if (output == "all" || output == "frame") { step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base") { part("Board", at = [0, board_y + t, 0], rotate = [90, 0, 0]) board(); part("Base", at = [0, 0, 0]) base(); } step("Slide on the end plates, glue the journals in with a collar each side, and the crank plies below") { part("Lower end plate", qty = 2, at = [ax(0), ax(-t)], rotate = euler(down_axis)) mirror([1, 0]) end_plate(); part("Upper end plate", qty = 2, at = [ax(s_upper), ax(s_upper + t)], rotate = euler(up_axis)) end_plate(); } step("Push the trough dowels through the end plates, the lower two into the exit rib") part("Exit rib", at = ax(s_exit - t), rotate = euler(up_axis)) exit_rib(); } if (output == "all" || output == "track") { for (i = [0:2]) let (g = legs[i], L = leg_len(g)) { step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base") part(str("Track floor ", i + 1), at = leg_origin(g), rotate = [0, leg_theta(g), 0]) leg_floor(L); // Rails stop at the end walls step("Glue the rails and the chute walls onto the floors") let (x0 = i == 1 ? g[0] + t : g[0], x1 = i == 1 ? g[1] : g[1] - t) part(str("Track rail ", i + 1), at = [x0, track_y + t, leg_top(i, x0)], rotate = [90, leg_theta(g), 0]) rect([(x1 - x0) / cos(sigma), 10]); } step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base") { part("End wall 1", at = [284 - t, 0, 0], rotate = [90, 0, 90]) end_wall(top1(284), top1(284) + 13); part("End wall 2", at = [19, 0, 0], rotate = [90, 0, 90]) end_wall(top2(19), top1(40) + 18); part("End wall 3", at = [288 - t, 0, 0], rotate = [90, 0, 90]) end_wall(top3(288), top2(270) + 18); // The exit chute runs from in front of the trough to the board; the // marble rolls back and drops through the hole onto the first leg let (L = (board_y + 18) / cos(beta), le = (board_y + t - 0.1 + 18) / cos(beta)) part("Exit floor", at = [0, -18 - t * sin(beta), ze(-18) - t * cos(beta)], rotate = [-beta, 0, 0]) difference() { chute_floor(255, 287, L, le, 262, 280); translate([hole_x, (hole_y + 18) / cos(beta)]) circle(d = 20); } } step("Glue the rails and the chute walls onto the floors") { part("Exit wall", qty = 2, at = [[255, 0, 0], [287 - t, 0, 0]], rotate = [90, 0, 90]) chute_wall(function(y) ze(y), -18, 10, 6); part("Exit front wall", at = [0, -15, 0], rotate = [90, 0, 0]) translate([255 + t, ze(-18)]) rect([32 - 2 * t, 10]); } step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base") let (L = (board_y - 31 - t * sin(beta)) / cos(beta), le = (board_y + t - 0.4 - 31) / cos(beta)) part("Intake floor", at = [0, 31 + t * sin(beta), zi(31) - t * cos(beta)], rotate = [beta, 0, 0]) chute_floor(63, 89, L, le, 68, 84); step("Glue the rails and the chute walls onto the floors") { part("Intake wall", at = [89 - t, 0, 0], rotate = [90, 0, 90]) chute_wall(function(y) zi(y), 31, 12); // Tall enough to catch a marble flying off the last leg part("Intake back wall", at = [63, 0, 0], rotate = [90, 0, 90]) chute_wall(function(y) zi(y), 31, top3(89) + 18 - zi(31)); } step("Glue the end plates, the floors, the end walls and the ramp into the board, and the board into the base") part("Ramp", at = ramp_at, rotate = euler(ramp_frame)) ramp(); } // The fit test lies on the table beside the base if (output == "all" || output == "coupon") part("Fit test", at = [330, -60, 0]) coupon(); echo(str("Helix: ", plates, " plates on a 10 mm square bar, ", pitch, " mm pitch, ", plates / 12, " turns; lifts ", round(((s_exit - 55) * st) * 10) / 10, " mm; track legs at ", round(sigma * 10) / 10, " degrees")); // Assembly: thread the screw plates onto a 10 mm square bar (aluminium or // hardwood, 395 mm), each turned one point of its star (30 degrees) on from // the last, always the same way, and glue them; the scored lines spiral // round. Glue a collar at each end of the stack. Glue each pair of end // plates, slide them on, then glue two journals into each bearing (free to // turn in it), a collar outside each, and the two crank plies outside the // lower collar. Push four 6 mm dowels through both end plates; the two // lowest end in the exit rib, 33 mm short of the upper plate. Glue the end // plates, chutes, track floors, end walls and ramp into the board, the board // into the base, and the rails and walls onto the floors. The grip is four // rings on a 6 mm brass sleeve, on an M4 x 25 bolt through the crank with a // washer each side and a nyloc nut. Turn the crank anticlockwise as you face // it, and drop 16 mm glass marbles into the intake chute. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/peck/ # Peck pecking hens Peck: the folk toy of hens pecking grain on a round paddle. Each hen's head rocks on a bolt through her body, and a string from her neck runs down through the paddle to a weight hanging below. Hold the handle and swing the paddle in small circles: the weight swings round, pulls each string in turn and the hens peck one after another, their tails bobbing up behind. This design has 48 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=peck). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/peck/peck.kerf) - [peck-sheet-1.svg](https://kerfscript.mylasertools.com/models/peck/peck-sheet-1.svg) - [peck-sheet-1.dxf](https://kerfscript.mylasertools.com/models/peck/peck-sheet-1.dxf) - [peck-sheet-2.svg](https://kerfscript.mylasertools.com/models/peck/peck-sheet-2.svg) - [peck-sheet-2.dxf](https://kerfscript.mylasertools.com/models/peck/peck-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Paddle top | 1 | 240 × 350 mm | | Paddle base | 1 | 240 × 350 mm | | Right body | 6 | 40 × 39.5 mm | | Left body | 6 | 40 × 39.5 mm | | Body spacer | 6 | 19.6 × 20.5 mm | | Head | 6 | 87 × 50.5 mm | | Right tail | 6 | 27.6 × 43 mm | | Left tail | 6 | 27.6 × 43 mm | | Weight disc | 7 | 44 × 44 mm | | Knot ring | 1 | 44 × 44 mm | | String plate | 1 | 26 × 26 mm | | Fit test | 1 | 96 × 40 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `hens` | `6` | 3 to 8 | | `paddle_diameter` | `240` | 220 to 300 | | `neck_clearance` | `0.5` | 0.3 to 0.7 | | `swing` | `165` | 0 to 360 | | `swing_reach` | `30` | 0 to 45 | | `output` | `"all"` | All parts, Paddle, Hens, Weight, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Peck: the folk toy of hens pecking grain on a round paddle. Each hen's head // rocks on a bolt through her body, and a string from her neck runs down // through the paddle to a weight hanging below. Hold the handle and swing the // paddle in small circles: the weight swings round, pulls each string in turn // and the hens peck one after another, their tails bobbing up behind. /* [Toy] */ // Number of hens round the paddle hens = 6; // [3:1:8] // Diameter of the round paddle in mm; the hens stand at the same distance from its rim paddle_diameter = 240; // [220:10:300] /* [Fit] */ // Added to the M3 bolt for the hole each head rocks on neck_clearance = 0.5; // [0.3:0.1:0.7] /* [Assembly] */ // Where the swing pulls in the assembled view, in degrees round the paddle: // the hen nearest it pecks and the weight swings out the other way swing = 165; // [0:5:360] // How far the weight swings out, in mm swing_reach = 30; // [0:1:45] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, paddle:Paddle, hens:Hens, weight:Weight, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; n = hens; D = paddle_diameter; assert(n == floor(n) && n >= 3 && n <= 8, "Hens: 3 to 8"); assert(D >= 220 && D <= 300, "Paddle diameter: 220 to 300 mm"); assert(neck_clearance >= 0.3 && neck_clearance <= 0.7, "Neck clearance: 0.3 to 0.7 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(swing >= 0 && swing <= 360, "Swing: 0 to 360 degrees"); assert(swing_reach >= 0 && swing_reach <= 45, "Swing reach: 0 to 45 mm"); assert(contains(["all", "paddle", "hens", "weight", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; // A hen is drawn from her right side, facing +x, with y up from the paddle // top. Her head rocks on the bolt at P, straight above the origin. P = [0, 18]; // The head and neck, drawn up. The beak is the lowest point when she pecks. neck_pts = [[0, 18], [13.9, 16.1], [16.5, 21], [21, 28], [23, 30], [24.5, 33], [28, 35.5], [33, 38], [28, 40], [27, 43], [24, 46.5], [23, 49], [21, 48], [19.5, 50.5], [17.5, 49], [15.5, 50], [15, 46.5], [13.5, 42], [10, 36], [5, 29], [0, 24]]; // The arm back through the body and the tail, the counterweight. With the // head up the tail's lowest corner rests on the paddle: that is the stop. tail_pts = [[0, 18], [2, 23], [-20, 26], [-27, 26], [-29, 29], [-33, 36], [-38, 41], [-43, 43], [-46, 40], [-45, 35], [-50, 37], [-53, 31], [-50, 26], [-54, 20], [-51, 14], [-52, 8], [-48, 3], [-44, 0]]; beak = [33, 38]; // The string ties through the neck at A and drops through the paddle at H A = P + polar(11, 15); H = [7, 0]; // The fixed body stays inside this radius round P and the tail plates // outside the larger one, so they never meet however the head rocks body_r = 27; tail_r = 28.5; // The head tips down until the beak touches the paddle reach = norm(beak - P); tip = atan2(beak.y - P.y, beak.x - P.x) + asin(P.y / reach); // Where the beak lands, ahead of the hen's origin peck_x = sqrt(reach * reach - P.y * P.y); // Hens stand facing the centre, their origins on this circle; the handle // points down the page, between two hens ring = D / 2 - 60; function hen_angle(i) = 90 + 180 / n + 360 * i / n; // Below the paddle: from its underside down to the string plate, with the // heads up and the weight hanging still, and the holes the strings end in drop = 110; plate_r = 8; // String length from the neck to the plate, and its travel, head up to down function rotp(p, a) = P + [[cos(a), -sin(a)], [sin(a), cos(a)]] * (p - P); above_up = norm(A - H); above_down = norm(rotp(A, -tip) - H); below = norm([ring - H.x - plate_r, drop]); string = above_up + 2 * t + below; // The assembled view, paddle top at z = 0. The far string pulls its head // right down and the weight hangs as low as that lets it; each other head // tips until its string is just long enough, or stays up on a slack string. function s_of(a) = norm(rotp(A, a) - H); function bisect(s, lo, hi, k = 0) = let (mid = (lo + hi) / 2) k == 40 ? mid : s_of(mid) > s ? bisect(s, lo, mid, k + 1) : bisect(s, mid, hi, k + 1); function head_for(s) = s >= above_up ? 0 : bisect(s, -tip, 0); longest = below + above_up - above_down; knot = polar(swing_reach, swing + 180); flat = [for (i = [0:n - 1]) norm(knot + polar(plate_r, hen_angle(i)) - polar(ring - H.x, hen_angle(i)))]; hang = min([for (d = flat) sqrt(longest * longest - d * d)]); heads = [for (d = flat) head_for(above_up - max(0, sqrt(d * d + hang * hang) - below))]; plate_top = -2 * t - hang; // A point in hen i's frame (x forward, y to her left, z up) on the paddle. // Her layers stand up through P's plane; a head turned h about P moves its // origin to rotp([0, 0], h). function on_hen(i, q) = let (a = hen_angle(i) + 180) concat(polar(ring, hen_angle(i)), [0]) + [q.x * cos(a) - q.y * sin(a), q.x * sin(a) + q.y * cos(a), q.z]; function turned(h, y) = let (o = rotp([0, 0], h)) [o.x, y, o.y]; function mirrored(h, y) = let (o = rotp([0, 0], h)) [-o.x, y, o.y]; function facing(i, h = 0) = [90, -h, hen_angle(i) + 180]; function left_facing(i, h = 0) = [90, -h, hen_angle(i)]; // The body: a round hen on short legs, with a tab for the paddle slot module body_shape() { intersection() { union() { translate([-6, 20]) ellipse([20, 13.5]); polygon([[-13, 0], [1, 0], [4, 10], [-16, 10]]); } translate(P) circle(r = body_r); } } module tab() { translate([-13, -2 * t]) rect([12, 2 * t + 0.01]); } module body_half() { difference() { union() { body_shape(); tab(); } translate(P) circle(d = 3.2); } // Wing, feathers and toes on the outside face score() { hull() { translate([-1, 24]) circle(r = 6); translate([-21, 25]) circle(r = 1); } for (k = [0:2]) line([-17 + 3 * k, 25 - 2 * k], [-5, 25 - 2 * k]); line([-6, 0], [-6, 7]); } } // The middle layer under the rocking parts: a wedge below P that the head // and the arm clear by a few degrees at both ends of their swing module spacer() { difference() { union() { intersection() { body_shape(); polygon([P, P + polar(80, 208), P + polar(80, 244), P + polar(80, 280)]); } tab(); } translate(P) circle(r = 7.5); } } module head() { difference() { union() { translate(P) circle(r = 6); polygon(neck_pts); polygon(tail_pts); } translate(P) circle(d = 3 + neck_clearance); translate(A) circle(d = 1.6); // The eye translate([22.5, 43]) circle(d = 2.2); } } // Glued either side of the tail, all outside tail_r module tail_plate() { difference() { polygon(tail_pts); translate(P) circle(r = tail_r); } score() for (a = [160, 176, 192]) line(P + polar(31, a), P + polar(56, a)); } module paddle(top) { difference() { union() { circle(d = D); translate([-16, -D / 2 - 110]) rect([32, 120], r = 16); } for (i = [0:n - 1]) { a = hen_angle(i); // The hen's tab slot and her string hole translate(polar(ring, a)) rotate(a + 180) translate([-13.1, -1.5 * t - 0.1]) rect([12.2, 3 * t + 0.2]); translate(polar(ring - H.x, a)) circle(d = 4); } // Hang it up by the handle translate([0, -D / 2 - 94]) circle(d = 8); } if (top) { score() circle(d = D - 8); score() translate([0, -D / 2 - 50]) rotate(-90) text("PECK", size = 12, halign = "center", valign = "center"); // Grain where each beak lands, and a little more in the middle engrave() for (i = [0:n - 1]) { a = hen_angle(i); g = rands(0, 1, 18, i + 3); for (k = [0:5]) translate(polar(ring - peck_x, a) + polar(1 + 6 * g[3 * k], 360 * g[3 * k + 1])) rotate(180 * g[3 * k + 2]) ellipse([1.5, 0.8]); } engrave() let (g = rands(0, 1, 30, 1)) for (k = [0:9]) translate(polar(3 + (ring - peck_x - 10) * g[3 * k], 360 * g[3 * k + 1])) rotate(180 * g[3 * k + 2]) ellipse([1.5, 0.8]); } } module ring_disc(d, hole) { difference() { circle(d = d); circle(d = hole); } } if (output == "all" || output == "paddle") { step("Glue the paddle top onto the paddle base") { part("Paddle top", at = [0, 0, -t]) paddle(true); part("Paddle base", at = [0, 0, -2 * t]) paddle(false); } } if (output == "all" || output == "hens") { hen = [0:n - 1]; step("Bolt each hen's body halves round the spacer and the rocking head") { part("Right body", qty = n, at = [for (i = hen) on_hen(i, [0, -t / 2, 0])], rotate = [for (i = hen) facing(i)]) body_half(); part("Left body", qty = n, at = [for (i = hen) on_hen(i, [0, t / 2, 0])], rotate = [for (i = hen) left_facing(i)]) mirror([1, 0]) body_half(); part("Body spacer", qty = n, at = [for (i = hen) on_hen(i, [0, t / 2, 0])], rotate = [for (i = hen) facing(i)]) spacer(); part("Head", qty = n, at = [for (i = hen) on_hen(i, turned(heads[i], t / 2))], rotate = [for (i = hen) facing(i, heads[i])]) head(); } step("Glue a tail plate either side of each tail") { part("Right tail", qty = n, at = [for (i = hen) on_hen(i, turned(heads[i], -t / 2))], rotate = [for (i = hen) facing(i, heads[i])]) tail_plate(); part("Left tail", qty = n, at = [for (i = hen) on_hen(i, mirrored(-heads[i], t / 2))], rotate = [for (i = hen) left_facing(i, -heads[i])]) mirror([1, 0]) tail_plate(); } } if (output == "all" || output == "weight") step("Stack the weight discs, knot ring and string plate on a bolt and tie on the strings") { part("Weight disc", qty = 7, at = [for (k = [1:7]) concat(knot, [plate_top - 2 * t - k * t])]) { ring_disc(44, 4.4); score() circle(d = 36); } // Leaves room under the plate for the knots part("Knot ring", at = concat(knot, [plate_top - 2 * t])) ring_disc(44, 20); part("String plate", at = concat(knot, [plate_top - t])) difference() { circle(d = 26); circle(d = 4.4); for (i = [0:n - 1]) translate(polar(plate_r, hen_angle(i))) circle(d = 1.6); } } // The fit test lies beside the weight, at its foot if (output == "all" || output == "coupon") part("Fit test", at = [D / 2 + 30, -D / 2, plate_top - 9 * t]) { difference() { rect([96, 40], r = 3); translate([12, 28]) circle(d = 3.2); translate([26, 28]) circle(d = 3 + neck_clearance); translate([40, 28]) circle(d = 1.6); translate([54, 28]) circle(d = 4); translate([70, 22]) rect([12.2, 3 * t + 0.2]); } score() translate([5, 6]) text("3.2 NECK 1.6 4 TAB", size = 3.5); } echo(str("Peck: ", n, " hens, heads tip ", round(tip), " degrees to peck, ", round((above_up - above_down) * 10) / 10, " mm of string travel; tie each string ", round(string), " mm from neck to plate; weight ", drop, " mm below the paddle")); // Assembly: glue the paddle top onto the paddle base, holes and slots lined // up. Each hen is three layers: right body, then the spacer with the head // beside it, then left body. Bolt them through P with an M3 x 12 bolt and // nut, snug but not tight, sanding the head's faces until it rocks freely; // glue the spacer between the bodies, and a tail plate either side of the // tail. Glue the tab into its slot, the hen facing the middle. Tie a string // through each neck at A, pass it down its hole and through its hole in the // string plate, and knot it under the plate. Stack the weight discs, knot // ring and plate on an M4 x 35 bolt with a nut on top. Hang the weight with // every head up and adjust each knot until its string is just taut. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/dabble/ # Dabble duck automaton Dabble: a hand-cranked duck automaton on an open cam box. Three cams on a square shaft lift three dowel pushrods through the deck: as you turn the crank the duck dips its head to the engraved pond while its tail tips up, lifts its head again, then raises its wings. The pushrods are only lifted; the duck's own weight keeps them on the cams, so turn it slowly. This design has 106 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=dabble). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/dabble/dabble.kerf) - [dabble-sheet-1.svg](https://kerfscript.mylasertools.com/models/dabble/dabble-sheet-1.svg) - [dabble-sheet-1.dxf](https://kerfscript.mylasertools.com/models/dabble/dabble-sheet-1.dxf) - [dabble-sheet-2.svg](https://kerfscript.mylasertools.com/models/dabble/dabble-sheet-2.svg) - [dabble-sheet-2.dxf](https://kerfscript.mylasertools.com/models/dabble/dabble-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 210 × 90 mm | | End wall | 4 | 90 × 131 mm | | Guide rail | 2 | 198 × 30 mm | | Deck | 2 | 210 × 90 mm | | Neck cam | 3 | 62 × 73.5 mm | | Wing cam | 3 | 60 × 50 mm | | Tail cam | 3 | 58 × 50.3 mm | | Journal | 4 | 18 × 18 mm | | Thrust washer | 2 | 26 × 26 mm | | Shaft collar | 2 | 18 × 18 mm | | Spacer ring | 50 | 14 × 14 mm | | Follower disc | 6 | 34 × 34 mm | | Left side | 2 | 114 × 54 mm | | Right side | 2 | 114 × 54 mm | | Body core | 3 | 78.8 × 48 mm | | Neck front | 1 | 45.8 × 80 mm | | Neck back | 1 | 45.8 × 80 mm | | Tail front | 1 | 54.2 × 29.6 mm | | Tail back | 1 | 54.2 × 29.6 mm | | Left wing | 1 | 90.6 × 25 mm | | Right wing | 1 | 90.6 × 25 mm | | Crank | 3 | 32 × 56 mm | | Handle | 6 | 18 × 18 mm | | Fit test | 1 | 120 × 52 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `neck_lift` | `12` | 10 to 12 | | `wing_lift` | `10` | 8 to 10 | | `tail_lift` | `8` | 6 to 8 | | `clearance` | `0.4` | 0.2 to 0.6 | | `shaft_fit` | `0.15` | 0.05 to 0.3 | | `dowel_fit` | `0.4` | 0.2 to 0.6 | | `crank_angle` | `95` | 0 to 360 | | `output` | `"all"` | All parts, Box, Cams and shaft parts, Follower discs, Duck, Crank, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Dabble: a hand-cranked duck automaton on an open cam box. Three cams on a // square shaft lift three dowel pushrods through the deck: as you turn the // crank the duck dips its head to the engraved pond while its tail tips up, // lifts its head again, then raises its wings. The pushrods are only lifted; // the duck's own weight keeps them on the cams, so turn it slowly. /* [Motion] */ // How far the neck cam drops the head pushrod; more lift, a deeper dip neck_lift = 12; // [10:1:12] // How far the wing cam lifts the wing pushrod wing_lift = 10; // [8:1:10] // How far the tail cam lifts the tail pushrod tail_lift = 8; // [6:1:8] /* [Fit] */ // Total play for the parts that turn or slide clearance = 0.4; // [0.2:0.1:0.6] // Added to the square holes for the 6 mm shaft shaft_fit = 0.15; // [0.05:0.05:0.3] // Added to the holes the 6 mm dowel pushrods slide in dowel_fit = 0.4; // [0.2:0.1:0.6] /* [Assembly] */ // Crank angle in the assembled view: 0 to 190 the head dips, 190 to 350 the // wings rise and fall crank_angle = 95; // [0:5:360] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, box:Box, rotor:Cams and shaft parts, followers:Follower discs, duck:Duck, crank:Crank, coupon:Fit test] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; // The box: length along the shaft, depth, and how far the walls sit in from // the ends of the base and deck BL = 210; BW = 90; W0 = 6; // Cams: smallest radius, and the follower disc that rides on them rb = 25; disc = 34; lmax = max(neck_lift, wing_lift, tail_lift); // Heights from the bottom of the base: shaft, guide rail, top of the deck z_s = 2 * t + rb + lmax + 8; rail_z = z_s + rb + lmax + 2 * t + 3; z_d = rail_z + 2 * t + 22 + 2 * t; // The duck is drawn side on, facing +x, with y = 0 on the deck; x0 is where // its origin sits along the box. Pivots of the neck, wings and tail, and how // far from each pivot its pushrod bears. x0 = 92; pn = [28, 16]; rn = 11; pw = [10, 32]; rw = 18; pt = [-36, 26]; rt = 14; boss = 4; // Pushrods bear midway along the little arc their boss or pin sweeps un = pn[0] + rn * (1 + cos(asin(neck_lift / 2 / rn))) / 2; uw = pw[0] - rw * (1 + cos(asin(wing_lift / 2 / rw))) / 2; ut = pt[0] - rt * (1 + cos(asin(tail_lift / 2 / rt))) / 2; // Cam timing in crank degrees: the head goes down, rests, comes up; then // the wings rise and fall dip = [80, 30, 80]; flap_at = 190; flap = [80, 0, 80]; // Spacer rings fill the shaft between the journals and the cams gaps = [x0 + ut - 1.5 * t - W0 - 2 * t, uw - ut - 3 * t, un - uw - 3 * t, BL - W0 - 2 * t - x0 - un - 1.5 * t]; rings = sum([for (g = gaps) floor((g - 0.4) / t)]); assert(neck_lift >= 10 && neck_lift <= 12, "Neck lift: 10 to 12 mm"); assert(wing_lift >= 8 && wing_lift <= 10, "Wing lift: 8 to 10 mm"); assert(tail_lift >= 6 && tail_lift <= 8, "Tail lift: 6 to 8 mm"); assert(clearance >= 0.2 && clearance <= 0.6, "Clearance: 0.2 to 0.6 mm"); assert(shaft_fit >= 0.05 && shaft_fit <= 0.3, "Shaft fit: 0.05 to 0.3 mm"); assert(dowel_fit >= 0.2 && dowel_fit <= 0.6, "Dowel fit: 0.2 to 0.6 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(crank_angle >= 0 && crank_angle <= 360, "Crank angle: 0 to 360 degrees"); assert(contains(["all", "box", "rotor", "followers", "duck", "crank", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; function wrap(a) = ((a % 360) + 360) % 360; // A smooth bump of height L: rise over p[0] degrees, stay up p[1], fall // over p[2]; d is its slope in mm per radian function bump(a, L, p) = let (x = wrap(a)) x < p[0] ? L / 2 * (1 - cos(180 * x / p[0])) : x < p[0] + p[1] ? L : x < p[0] + p[1] + p[2] ? L / 2 * (1 + cos(180 * (x - p[0] - p[1]) / p[2])) : 0; function d_bump(a, L, p) = let (x = wrap(a)) x < p[0] ? L / 2 * 180 / p[0] * sin(180 * x / p[0]) : x < p[0] + p[1] ? 0 : x < p[0] + p[1] + p[2] ? -L / 2 * 180 / p[2] * sin(180 * (x - p[0] - p[1]) / p[2]) : 0; // Pushrod lift for cam k (0 neck, 1 wings, 2 tail) at crank angle a function lift(k, a) = k == 0 ? neck_lift - bump(a, neck_lift, dip) : k == 1 ? bump(a - flap_at, wing_lift, flap) : bump(a, tail_lift, dip); function d_lift(k, a) = k == 0 ? -d_bump(a, neck_lift, dip) : k == 1 ? d_bump(a - flap_at, wing_lift, flap) : d_bump(a, tail_lift, dip); // The cam is the envelope of the flat follower's face: at crank angle a the // face is rb + lift above the shaft, and it touches the cam d_lift to one side function cam_point(k, q) = let (a = 90 - q, h = rb + lift(k, a), dh = -d_lift(k, a)) h * [cos(q), sin(q)] + dh * [-sin(q), cos(q)]; module label(s, p, size = 4, a = 0) { score() translate(p) rotate(a) text(s, size = size, halign = "center", valign = "center"); } module square_bore() { rect([6 + shaft_fit, 6 + shaft_fit], center = true); } module ring(od, id) { difference() { circle(d = od); circle(d = id); } } module capsule(a, b, r1, r2) { hull() { translate(a) circle(r = r1); translate(b) circle(r = r2); } } // ---- The box ---- module base() { difference() { translate([0, -BW / 2]) rect([BL, BW], r = 4); for (x = [W0 + t, BL - W0 - t], y = [-30, 30]) translate([x, y]) rect([2 * t + 0.2, 14.2], center = true); } } // Drawn across the box: x is depth, y height from the bottom of the base module end_wall() { difference() { union() { translate([-BW / 2, 2 * t]) rect([BW, z_d - 4 * t]); for (x = [-30, 30]) translate([x - 7, 0]) rect([14, z_d]); } translate([0, z_s]) circle(d = 18 + clearance); translate([0, rail_z + t]) rect([12.2, 2 * t + 0.2], center = true); // Windows, so the cams show from the ends too translate([-32, 2 * t + 8]) rect([64, z_s - 2 * t - 22], r = 6); translate([-32, z_s + 14]) rect([64, rail_z - z_s - 20], r = 6); } } // The lower guide for the pushrods, between the walls module rail() { difference() { union() { translate([W0 + 2 * t, -15]) rect([BL - 2 * W0 - 4 * t, 30], r = 3); translate([W0, -6]) rect([BL - 2 * W0, 12]); } for (u = [un, uw, ut]) translate([x0 + u, 0]) circle(d = 6 + dowel_fit); } } // The deck the duck floats on: pushrod holes, the slots for the duck's tabs // and the walls, and the pond the beak dips into module deck() { difference() { translate([0, -BW / 2]) rect([BL, BW], r = 4); for (x = [W0 + t, BL - W0 - t], y = [-30, 30]) translate([x, y]) rect([2 * t + 0.2, 14.2], center = true); for (u = [un, uw, ut]) translate([x0 + u, 0]) circle(d = 6 + dowel_fit); for (u = [-38, 4], s = [-1, 1]) translate([x0 + u - 0.1, s > 0 ? 1.5 * t - 0.1 : -3.5 * t - 0.1]) rect([14.2, 2 * t + 0.2]); } score() for (i = [1:3]) translate([x0 + 101, 0]) outline() ellipse([4.5 * i, 6.5 * i]); label("DABBLE", [BL / 2, -35], 12); label("TURN SLOWLY", [42, -35], 4); } // ---- The rotor ---- module cam(k, name) { difference() { polygon([for (q = [0:2:358]) cam_point(k, q)]); square_bore(); } // Timing mark: on the shaft, every cam's mark points the same way score() line([0, 6], [0, 15]); label(name, [0, -15], 4); } module crank() { difference() { hull() { circle(r = 16); translate([0, 30]) circle(r = 10); } square_bore(); translate([0, 30]) circle(d = 4.5); } } // ---- The duck, side on, at rest: head up, wings and tail down ---- module body() { hull() { translate([28, 22]) circle(r = 22); translate([-15, 24]) circle(r = 24); translate([-50, 28]) circle(r = 14); translate([-54, 0]) rect([82, 1]); } } // The neck turns on a brass tube at pn; its boss rests on the pushrod module neck() { fu = asin(neck_lift / 2 / rn); union() { translate(pn) circle(r = 6); translate(pn + polar(rn, fu)) circle(r = boss); capsule(pn, pn + [5, 18], 6, 6.5); capsule(pn + [5, 18], pn + [9, 36], 6.5, 6); capsule(pn + [9, 36], pn + [10, 50], 6, 6.5); capsule(pn + [10, 50], pn + [15, 62], 6.5, 12); offset(r = 1) offset(delta = -1) polygon([pn + [20, 70], pn + [40, 62], pn + [39, 58], pn + [24, 55]]); } } module neck_marks() { score() { translate(pn + [19, 66]) outline() circle(d = 3.6); line(pn + [26, 60], pn + [39.5, 60]); } score() translate(pn + [12, 44]) rotate(10) line([-7, 0], [7, 0]); } // The tail turns on a tube at pt, its boss resting on the pushrod behind module tail() { f0 = -asin(tail_lift / 2 / rt); union() { translate(pt) circle(r = 6); translate(pt + [-rt * cos(f0), rt * sin(f0)]) circle(r = boss); capsule(pt, pt + [-20, 8], 5, 5); offset(r = 2) offset(delta = -2) polygon([pt + [-16, 3], pt + [-44, 12], pt + [-50, 22], pt + [-40, 21], pt + [-20, 15]]); } } module tail_marks() { score() for (i = [0:2]) line(pt + [-22, 9 + 2 * i], pt + [-44 + 2 * i, 14 + 3 * i]); } // Wings turn on the long tube at pw; the steel pin through both rides on // the middle pushrod module wing() { difference() { union() { translate(pw) circle(r = 8); offset(r = 3) offset(delta = -3) polygon([pw + [6, 4], pw + [-4, 11], pw + [-28, 13], pw + [-55, 10], pw + [-80, 3], pw + [-90, 0], pw + [-70, -6], pw + [-45, -10], pw + [-20, -12], pw + [0, -9]]); } translate(pw) circle(d = 6 + clearance); translate(pw + [-rw * cos(asin(wing_lift / 2 / rw)), -wing_lift / 2]) circle(d = 4.2); } score() { for (i = [0:3]) line(pw + [-30 - 4 * i, 8 - 4 * i], pw + [-84 + 3 * i, 1 - 2 * i]); for (i = [0:3]) translate(pw + [-6 - 7 * i, 2]) arc(r = 5, start = 200, end = 340); for (i = [0:2]) translate(pw + [-10 - 7 * i, -5]) arc(r = 5, start = 200, end = 340); } } // The arc the wing pin moves along, with room around it module pin_slot(w) { a0 = asin(wing_lift / 2 / rw) + 4; for (a = [-a0:2:a0 - 2]) hull() { translate(pw + [-rw * cos(a), rw * sin(a)]) circle(d = w); translate(pw + [-rw * cos(a + 2), rw * sin(a + 2)]) circle(d = w); } } module side() { difference() { union() { body(); for (u = [-38, 4]) translate([u, -2 * t]) rect([14, 2 * t + 1]); } for (p = [pn, pw, pt]) translate(p) circle(d = 6.3); pin_slot(4 + 2 * clearance); } score() { translate([30, 22]) arc(r = 16, start = 200, end = 280); for (i = [0:4], j = [0:1]) translate([-46 + 9 * i + 4.5 * j, 12 + 7 * j]) arc(r = 5, start = 200, end = 340); } } // What the neck, tail, wing pin and pushrods sweep through, with room to spare module cavity() { offset(r = 1.5) { for (a = [0:-6:-70]) rotate(a, center = pn) neck(); for (a = [0:6:36]) rotate(-a, center = pt) tail(); } pin_slot(7); for (c = [[un, pn[1] + neck_lift / 2 - boss], [uw, pw[1] + wing_lift / 2 - 2], [ut, pt[1] + tail_lift / 2 - boss]]) translate([c[0] - 4.5, -1]) rect([9, c[1] + 2]); } // Glued between the two sides, three layers thick module core() { difference() { body(); cavity(); translate(pw) circle(d = 6.3); // Only the part between the tail and the neck is worth gluing in translate([ut + 4.5 - 30, -1]) rect([30, 60]); translate([40, -1]) rect([30, 60]); } } // The assembled view, z up from the bottom of the base, the shaft along x // at y = 0. Parts on the shaft stand across it and turn with the crank. a = crank_angle; function on_shaft(x) = [x, 0, z_s]; shaft_turn = [90, -a, 90]; across = [90, 0, 90]; wall_x = [W0, W0 + t, BL - W0 - 2 * t, BL - W0 - t]; // Pushrod lifts and the turns they give the neck (head down is negative), // wings and tail (tip up is negative), each about its pivot s_n = lift(0, a); s_w = lift(1, a); s_t = lift(2, a); neck_turn = asin((s_n - neck_lift / 2) / rn) - asin(neck_lift / 2 / rn); wing_turn = -asin((s_w - wing_lift / 2) / rw) - asin(wing_lift / 2 / rw); tail_turn = -asin((s_t - tail_lift / 2) / rt) - asin(tail_lift / 2 / rt); cam_x = [x0 + un, x0 + uw, x0 + ut]; ring_x = let (starts = [W0 + 2 * t, cam_x[2] + 1.5 * t, cam_x[1] + 1.5 * t, cam_x[0] + 1.5 * t]) [for (g = [0:3]) for (k = [0:floor((gaps[g] - 0.4) / t) - 1]) starts[g] + k * t]; // Duck layers are drawn side on: facing front, back face at y, or mirrored // and facing back, front face at y. A piece turned b about its pivot q // moves its origin to q - rotate(b) q. function swung(q, b) = q - [q.x * cos(b) - q.y * sin(b), q.x * sin(b) + q.y * cos(b)]; function front_at(y, q = [0, 0], b = 0) = let (o = swung(q, b)) [x0 + o.x, y, z_d + o.y]; function back_at(y, q = [0, 0], b = 0) = let (o = swung([-q.x, q.y], -b)) [x0 - o.x, y, z_d + o.y]; function front_turn(b = 0) = [90, -b, 0]; function back_turn(b = 0) = [90, b, 180]; // ---- Parts ---- if (output == "all" || output == "box") step("Stand the end walls in the base with the guide rail between them, and glue the deck on") { part("Base", qty = 2, at = [[0, 0, 0], [0, 0, t]]) base(); part("End wall", qty = 4, at = [for (x = wall_x) [x, 0, 0]], rotate = across) end_wall(); part("Guide rail", qty = 2, at = [[0, 0, rail_z], [0, 0, rail_z + t]]) rail(); part("Deck", qty = 2, at = [[0, 0, z_d - 2 * t], [0, 0, z_d - t]]) deck(); } if (output == "all" || output == "rotor") step("Thread the square shaft through the walls: the crank, the journals, the cams, the rings and the collar") { part("Neck cam", qty = 3, at = [for (k = [0:2]) on_shaft(cam_x[0] + (k - 1.5) * t)], rotate = shaft_turn) cam(0, "NECK"); part("Wing cam", qty = 3, at = [for (k = [0:2]) on_shaft(cam_x[1] + (k - 1.5) * t)], rotate = shaft_turn) cam(1, "WING"); part("Tail cam", qty = 3, at = [for (k = [0:2]) on_shaft(cam_x[2] + (k - 1.5) * t)], rotate = shaft_turn) cam(2, "TAIL"); part("Journal", qty = 4, at = [for (x = wall_x) on_shaft(x)], rotate = shaft_turn) difference() { circle(d = 18); square_bore(); } part("Thrust washer", qty = 2, at = [on_shaft(W0 - t), on_shaft(BL - W0)], rotate = across) ring(26, 9.2); part("Shaft collar", qty = 2, at = [on_shaft(BL - W0 + t), on_shaft(BL - W0 + 2 * t)], rotate = shaft_turn) difference() { circle(d = 18); square_bore(); } part("Spacer ring", qty = rings, at = [for (x = ring_x) on_shaft(x)], rotate = across) ring(14, 9.2); } // Two on each pushrod, riding on its cam if (output == "all" || output == "followers") step("Drop each pushrod, with its two follower discs, down through the rail and the deck onto its cam") part("Follower disc", qty = 6, at = [for (k = [0:2], j = [0:1]) [cam_x[k], 0, z_s + rb + [s_n, s_w, s_t][k] + j * t]]) ring(disc, 6.2); if (output == "all" || output == "duck") step("Build the duck and glue the tabs of its sides into the deck") { part("Left side", qty = 2, at = [front_at(-1.5 * t), front_at(-2.5 * t)], rotate = front_turn()) side(); part("Right side", qty = 2, at = [back_at(1.5 * t), back_at(2.5 * t)], rotate = back_turn()) mirror([1, 0]) side(); part("Body core", qty = 3, at = [for (k = [1:3]) front_at(-1.5 * t + k * t)], rotate = front_turn()) core(); part("Neck front", at = front_at(0, pn, neck_turn), rotate = front_turn(neck_turn)) difference() { union() { neck(); neck_marks(); } translate(pn) circle(d = 6 + clearance); } part("Neck back", at = back_at(0, pn, neck_turn), rotate = back_turn(neck_turn)) mirror([1, 0]) difference() { union() { neck(); neck_marks(); } translate(pn) circle(d = 6 + clearance); } part("Tail front", at = front_at(0, pt, tail_turn), rotate = front_turn(tail_turn)) difference() { union() { tail(); tail_marks(); } translate(pt) circle(d = 6 + clearance); } part("Tail back", at = back_at(0, pt, tail_turn), rotate = back_turn(tail_turn)) mirror([1, 0]) difference() { union() { tail(); tail_marks(); } translate(pt) circle(d = 6 + clearance); } part("Left wing", at = front_at(-3.5 * t - 0.8, pw, wing_turn), rotate = front_turn(wing_turn)) wing(); part("Right wing", at = back_at(3.5 * t + 0.8, pw, wing_turn), rotate = back_turn(wing_turn)) mirror([1, 0]) wing(); } if (output == "all" || output == "crank") { step("Thread the square shaft through the walls: the crank, the journals, the cams, the rings and the collar") part("Crank", qty = 3, at = [for (k = [0:2]) on_shaft(W0 - 4 * t + k * t)], rotate = shaft_turn) crank(); // The grips turn on the handle bolt, 30 mm out on the crank step("Bolt the handle on the crank") part("Handle", qty = 6, at = [for (k = [1:6]) [W0 - 4 * t - k * t, -30 * sin(a), z_s + 30 * cos(a)]], rotate = shaft_turn) ring(18, 4.5); } // The fit test lies on the table beside the box if (output == "all" || output == "coupon") part("Fit test", at = [BL + 20, -BW / 2, 0]) { difference() { rect([120, 52], r = 3); for (j = [0:3]) translate([14 + 18 * j, 14]) rect([6.05 + 0.1 * j, 6.05 + 0.1 * j], center = true); translate([96, 16]) circle(d = 18 + clearance); for (j = [0:2]) translate([14 + 18 * j, 38]) circle(d = 6.2 + 0.2 * j); translate([68, 38]) circle(d = 6.3); translate([96, 38]) rect([2 * t + 0.2, 14.2], center = true); } for (j = [0:3]) label(str(6.05 + 0.1 * j), [14 + 18 * j, 5], 3); for (j = [0:2]) label(str(6.2 + 0.2 * j), [14 + 18 * j, 47], 3); label("TUBE", [68, 47], 3); } echo(str("Dabble: head dips ", round(2 * asin(neck_lift / 2 / rn)), " degrees, wings lift ", round(2 * asin(wing_lift / 2 / rw)), ", tail ", round(2 * asin(tail_lift / 2 / rt)), "; box ", BL, " x ", BW, " x ", z_d, " mm; shaft ", round(BL - 2 * W0 + 7 * t + 4), " mm, ", rings, " spacer rings; dowels ", round(z_d + pn[1] - boss - neck_lift / 2 - z_s - rb), ", ", round(z_d + pw[1] - 2 - wing_lift / 2 - z_s - rb), " and ", round(z_d + pt[1] - boss - tail_lift / 2 - z_s - rb), " mm")); // Assembly: glue the laminations (base, deck, guide rail, each end wall, // side and follower 2, core and each cam 3, crank 3, handle 6). Stand the // walls in the base with the rail between them and glue the deck on. Thread // the 6 mm square steel shaft: crank, thrust washer, journal, rings, tail cam, // rings, wing cam, rings, neck cam, rings, journal, washer, collar; every // cam's mark the same way and its name away from the crank, glued under its // pushrod. Glue each 6 mm dowel through two follower discs, flush beneath, // and drop it through rail and deck. The duck: core between the sides, 21 mm // lengths of 6 mm brass tube glued through the neck and tail pivots, the neck // and tail turning on them with an M4 washer each side; a 29 mm tube through // the wing pivot carries the wings, held by an M4 bolt, washers and nut. Glue // a 30 mm length of 4 mm steel rod through both wings to rest on the middle // pushrod, then glue the sides' tabs into the deck. An M4 bolt and nut carry // the handle on the crank. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/wellspring/ # Wellspring saqiya water wheel Wellspring: a saqiya, the animal-driven water wheel al-Jazari drew in the 13th century. Turn the ox round its walk and the draw-bar turns a peg wheel above the gear pit; its dowel pegs drive a lantern pinion at right angles, and the wheel on the same axle lifts a ring of pots out of the well and tips them into the trough at the top. This design has 108 parts on 5 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=wellspring). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/wellspring/wellspring.kerf) - [wellspring-sheet-1.svg](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-1.svg) - [wellspring-sheet-1.dxf](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-1.dxf) - [wellspring-sheet-2.svg](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-2.svg) - [wellspring-sheet-2.dxf](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-2.dxf) - [wellspring-sheet-3.svg](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-3.svg) - [wellspring-sheet-3.dxf](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-3.dxf) - [wellspring-sheet-4.svg](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-4.svg) - [wellspring-sheet-4.dxf](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-4.dxf) - [wellspring-sheet-5.svg](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-5.svg) - [wellspring-sheet-5.dxf](https://kerfscript.mylasertools.com/models/wellspring/wellspring-sheet-5.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 2 | 454 × 345.5 mm | | Walkway | 1 | 329.5 × 329.5 mm | | Walkway post | 5 | 48 × 115 mm | | Tower disc | 40 | 24 × 24 mm | | Crown wheel | 2 | 155.5 × 155.5 mm | | Shaft collar | 11 | 26 × 26 mm | | Draw-bar | 2 | 161.8 × 26 mm | | Lantern plate | 4 | 59.8 × 59.8 mm | | Lantern bearing | 2 | 36 × 118 mm | | Water wheel | 2 | 150 × 150 mm | | Wheel hub | 2 | 30 × 30 mm | | Pot | 20 | 32 × 20 mm | | Inner upright | 2 | 146 × 112 mm | | Outer upright | 2 | 146 × 138 mm | | Well wall | 2 | 20.4 × 46 mm | | Trough floor | 1 | 100 × 34 mm | | Trough side | 2 | 100 × 14 mm | | Trough end | 1 | 28 × 14 mm | | Trough post | 2 | 34 × 138 mm | | Ox | 2 | 108.5 × 60.9 mm | | Fit test | 1 | 150 × 50 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `crown_pegs` | `24` | 20 to 28 | | `pots` | `10` | 8 to 12 | | `bush_fit` | `0.1` | 0 to 0.3 | | `dowel_fit` | `0.05` | -0.1 to 0.2 | | `tab_fit` | `0.15` | 0 to 0.3 | | `output` | `"all"` | All parts, Base, deck and posts, Tower, crown, draw-bar and lantern, Wheel and pots, Well walls and trough, Ox, Fit test | | `draw_bar_angle` | `195` | 0 to 360 | | `$thickness` | `3` | 2.8 to 3.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Wellspring: a saqiya, the animal-driven water wheel al-Jazari drew in the // 13th century. Turn the ox round its walk and the draw-bar turns a peg wheel // above the gear pit; its dowel pegs drive a lantern pinion at right angles, // and the wheel on the same axle lifts a ring of pots out of the well and // tips them into the trough at the top. /* [Mechanism] */ // Pegs on the horizontal crown wheel; the lantern has 8 staves, so this sets the ratio crown_pegs = 24; // [20:1:28] // Pots around the water wheel pots = 10; // [8:1:12] /* [Fit] */ // Added to the 6 mm holes the brass bushes are glued into bush_fit = 0.1; // [0:0.05:0.3] // Added to the 6 mm holes the dowel pegs and staves are glued into dowel_fit = 0.05; // [-0.1:0.05:0.2] // Added to the slots the tabs fit into tab_fit = 0.15; // [0:0.05:0.3] /* [Export] */ // All parts, or one group at a time output = "all"; // [all:All parts, base:Base, deck and posts, gearing:Tower, crown, draw-bar and lantern, wheel:Wheel and pots, well:Well walls and trough, ox:Ox, coupon:Fit test] /* [Assembly] */ // Draw-bar angle in the assembled view, counterclockwise from the axle draw_bar_angle = 195; // [0:5:360] /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; t = $thickness; // Peg gearing: 6 mm dowels at an 18 mm pitch on both gears, three dowel // widths, which leaves room for each peg to enter and leave between staves pitch = 18; staves = 8; Rc = pitch * crown_pegs / (2 * PI); rl = pitch * staves / (2 * PI); ratio = crown_pegs / staves; // The pegs reach 11 mm past the lantern's top staves engage = 11; // Heights from the underside of the base. The axle sits so the pots clear // the well floor, the walkway just above the bearings. zL = 100; Rw = 75; plate_r = rl + 7; zc = zL + rl + 9; peg_tip = zL + rl - engage; deck_z = zL + 12; tower_n = floor((zc - 4.2 - 2 * t) / t); // Enough collars to lift the draw-bar clear of the ox's hump collar_n = ceil((deck_z + t + 1 + 53 - zc - 2 * t) / t); zd = zc + 2 * t + collar_n * t; // The walkway ring, the ox's path and the wheel's inner face Ri = Rc + 32; Ro = Ri + 64; L = Ri + 32; wx = Ro + 2; // Along the axle: the lantern's plates, its bearing and the well uprights lx_in = Rc - 12.5; lx_out = Rc + 4.5; lbx = lx_out + 2 * t + 4.2; wix = wx - 4.2 - 2 * t; wox = wx + 4 * t + 4.2; // The well's long walls, and the trough inside the ring of pots Yw = 70; wall_h = 40; trough_y = [-12, 22]; trough_z = zL + 38; trough_x = wx + 2 * t + 0.5; post_x = trough_x + 86; base_x = [-(Ro + 8), trough_x + 108]; assert(crown_pegs == floor(crown_pegs) && crown_pegs >= 20 && crown_pegs <= 28, "Crown pegs: 20 to 28"); assert(pots == floor(pots) && pots >= 8 && pots <= 12, "Pots: 8 to 12"); assert(bush_fit >= 0 && bush_fit <= 0.3, "Bush fit: 0 to 0.3 mm"); assert(dowel_fit >= -0.1 && dowel_fit <= 0.2, "Dowel fit: -0.1 to 0.2 mm"); assert(tab_fit >= 0 && tab_fit <= 0.3, "Tab fit: 0 to 0.3 mm"); assert(t >= 2.8 && t <= 3.2, "Thickness: 2.8 to 3.2 mm"); assert($kerf >= 0 && $kerf <= 0.25, "Kerf: 0 to 0.25 mm"); assert(draw_bar_angle >= 0 && draw_bar_angle <= 360, "Draw-bar angle: 0 to 360 degrees"); assert(contains(["all", "base", "gearing", "wheel", "well", "ox", "coupon"], output), "Unknown output"); function polar(r, a) = [r * cos(a), r * sin(a)]; function r2(x) = round(x * 100) / 100; module label(s, p, size = 4) { score() translate(p) text(s, size = size, halign = "center", valign = "center"); } module bush() { circle(d = 6 + bush_fit); } module dowel() { circle(d = 6 + dowel_fit); } module annulus(od, id) { difference() { circle(d = od); circle(d = id); } } // A slot of length len along x for a tab n sheets thick, centred on y = 0 module slot(x0, len, n = 1) { translate([x0, -(n * t + tab_fit) / 2]) rect([len, n * t + tab_fit]); } // Tabs under a standing part, through both base layers, at these x ranges module tabs(ranges, depth = 2 * t) { for (r = ranges) translate([r[0], -depth]) rect([r[1] - r[0], depth + 0.01]); } // Windows between six spokes, from radius r0 to r1 module windows(r0, r1, spoke = 10) { difference() { circle(r = r1); circle(r = r0); for (a = [0:60:120]) rotate(a) rect([2 * r1 + 2, spoke], center = true); } } // Drawn in place, seen from above: the walkway posts, the bearings and the // well walls stand in slots through both layers; water ripples in the well module base() { difference() { translate([base_x[0], -(Ro + 8)]) rect([base_x[1] - base_x[0], 2 * (Ro + 8)], r = 10); for (a = [60:60:300]) rotate(a) { slot(Ri + 12, 10); slot(Ro - 22, 10); } for (y = [-16, 6]) translate([lbx - tab_fit / 2, y]) rect([2 * t + tab_fit, 10]); for (x = [wix, wox], y = [-66, 50]) translate([x - tab_fit / 2, y]) rect([2 * t + tab_fit, 16]); translate([wox - tab_fit / 2, 0]) rect([2 * t + tab_fit, 12]); for (s = [-1, 1]) translate([wx - 4.2 + 6, s * (Yw + t / 2)]) slot(0, 8.4 + 4 * t - 12); translate([post_x - tab_fit / 2, -6]) rect([2 * t + tab_fit, 22]); } score() for (i = [0:5]) polyline([for (y = [-Yw + 4:4:Yw - 4]) [wx - 2 + 3.2 * i + 1.2 * sin(y * 12), y]]); } // The ring the ox walks on, with the post slots and the name at the front module deck() { difference() { annulus(2 * Ro, 2 * Ri); for (a = [60:60:300]) rotate(a) { slot(Ri + 12, 10); slot(Ro - 22, 10); } } score() outline() circle(r = L - 12); score() outline() circle(r = L + 12); engrave() translate([0, -(Ri + 22)]) text("WELLSPRING", size = 10, halign = "center", valign = "center"); label(str("SAQIYA AFTER AL-JAZARI / ", crown_pegs, " : ", staves), [0, -(Ri + 50)], 4); } // A radial post under the walkway: x is the distance from the shaft module post() { h = deck_z - 2 * t; difference() { translate([Ri + 8, 0]) rect([Ro - Ri - 16, h]); translate([Ri + 20, 16]) rect([Ro - Ri - 40, h - 32], r = 8); } tabs([[Ri + 12, Ri + 22], [Ro - 22, Ro - 12]]); translate([0, h - 0.01]) tabs([[Ri + 12, Ri + 22], [Ro - 22, Ro - 12]], -t - 0.01); } // The horizontal peg wheel: pegs at the pitch radius, six spokes. Peg 0 // sits on +x, under the lantern when the draw-bar points that way. module crown() { difference() { circle(r = Rc + 9); circle(d = 4.4); for (i = [0:crown_pegs - 1]) translate(polar(Rc, 360 * i / crown_pegs)) dowel(); windows(22, Rc - 10); } score() outline() circle(r = Rc); } // From the shaft out past the ox; the ox's yoke post fits the slot module draw_bar() { difference() { hull() { circle(r = 13); translate([L + 6, 0]) circle(r = 10); } circle(d = 4.4); translate([L, 0]) rect([2 * t + tab_fit, 10 + tab_fit], center = true); } score() line([18, 0], [L - 10, 0]); } // One of the lantern's two end plates: 8 stave holes on the pitch circle module lantern_plate() { difference() { circle(r = plate_r); circle(d = 4.4); for (j = [0:staves - 1]) translate(polar(rl, 22.5 + 360 * j / staves)) dowel(); } } // Bearing plates stand across the axle: x along the axle's side (world y), // y up from the top of the base, the bush at the axle height module lantern_bearing() { difference() { union() { translate([-18, 0]) rect([36, zL - 2 * t]); translate([0, zL - 2 * t]) circle(r = 18); } translate([0, zL - 2 * t]) bush(); } tabs([[-16, -6], [6, 16]]); } module well_upright(outer) { top = outer ? trough_z - 2 * t : deck_z - 2 * t; span = outer ? trough_y : [-16, 16]; difference() { union() { translate([-(Yw + t), 0]) rect([2 * (Yw + t), wall_h]); translate([span[0], 0]) rect([span[1] - span[0], top]); } translate([0, zL - 2 * t]) bush(); for (s = [-1, 1]) translate([s * 38, wall_h / 2 - 2]) rect([28, wall_h - 16], center = true); } tabs(concat([[-66, -50], [50, 66]], outer ? [[0, 12]] : [])); label(outer ? "OUTER" : "INNER", [(span[0] + span[1]) / 2, 10], 4); } module well_wall() { len = 8.4 + 4 * t; rect([len, wall_h]); tabs([[6, len - 6]]); } module trough_post() { h = trough_z - 2 * t; difference() { translate([trough_y[0], 0]) rect([trough_y[1] - trough_y[0], h]); translate([trough_y[0] + 8, 14]) rect([trough_y[1] - trough_y[0] - 16, h - 34], r = 6); } tabs([[-6, 16]]); } // The wheel, seen from the trough side, where the pots are glued module wheel() { difference() { circle(r = Rw); circle(d = 4.4); windows(20, Rw - 14); } for (i = [0:pots - 1]) score() translate(polar(Rw - 1, 360 * i / pots)) rotate(360 * i / pots - 90) outline() pot_shape(); } // A pot, its mouth at +x. On the wheel it leads the way round, so it fills // at the bottom, rises upright and pours over the top. function pot_half() = [[-16, 4], [-13, 8], [-6, 10], [2, 9.2], [8, 6], [11, 4.5], [14, 4.5], [15, 6], [16, 6]]; module pot_shape() { polygon(concat(pot_half(), [for (p = reverse(pot_half())) [p[0], -p[1]]])); } module pot() { pot_shape(); score() { line([-9, -9.4], [-9, 9.4]); line([4, -8.2], [4, 8.2]); line([14, -4.5], [14, 4.5]); } } // The ox, facing +x with the yoke post at x = 0 and its hooves at y = 0; the // post reaches up through the draw-bar ox_top = zd + 2 * t - (deck_z + t + 1); module ox() { // A humped ox: tail, hind legs, belly, forelegs, dewlap, head and horn, hump offset(r = -0.8) offset(delta = 0.8) polygon([[-44, 43], [-47, 41], [-49, 36], [-50, 26], [-50.5, 18], [-52, 14], [-49, 13], [-48, 18], [-47.5, 28], [-46, 33], [-45, 27], [-43, 20], [-41, 13], [-41, 3], [-42, 0], [-35, 0], [-35.5, 4], [-36, 13], [-33.5, 19], [-32, 14], [-31, 4], [-32, 0], [-25, 0], [-25.5, 5], [-25, 15], [-22, 20], [-12, 18], [0, 17.5], [10, 18.5], [13, 15], [13.5, 4], [12.5, 0], [19.5, 0], [19.5, 5], [20, 14], [21.5, 16], [23, 14], [23.5, 4], [22.5, 0], [29.5, 0], [29.5, 5], [30, 16], [33, 21], [37, 22], [41, 26], [45, 28], [52, 28], [55.5, 30], [56.5, 34], [55, 37], [50, 42], [47, 45], [49, 50], [52, 54], [50, 54.5], [46, 50], [43, 46.5], [40, 45], [30, 45], [12, 46], [7, 50.5], [2, 52], [-4, 50], [-8, 46], [-20, 43], [-34, 42.5]]); translate([-5, 45]) rect([10, ox_top - 45]); score() { // Eye, ear, nostril and mouth translate([49, 38]) outline() circle(d = 2.6); polyline([[44, 45], [39, 43], [42, 42.5]]); line([54.5, 33.5], [55.5, 33]); line([52, 29.5], [55, 30.5]); // The breast strap from the yoke, and a saddle cloth with a zigzag hem polyline([[5, 47], [20, 34], [34, 24]]); polyline([[-10, 45], [-11, 26], [-29, 27], [-31, 42.5]]); polyline([for (i = [0:9]) [-29 + 2 * i, 25 + 2 * (i % 2)]]); // Cloven hooves and the knees for (x = [-41.5, -31.5, 13, 23]) { line([x, 3.5], [x + 6.5, 3.5]); line([x + 3.2, 0], [x + 3.2, 3.5]); } for (p = [[-38, 13], [-28, 13], [16.5, 10], [26.5, 10]]) line(p - [2, 0], p + [2, 0]); } } module coupon() { difference() { rect([150, 50], r = 3); for (i = [0:3]) translate([15 + 22 * i, 16]) circle(d = 5.9 + bush_fit + 0.1 * i); for (i = [0:2]) translate([105 + 18 * i, 16]) circle(d = 5.9 + dowel_fit + 0.1 * i); translate([15, 38]) circle(d = 4.4); translate([40, 32]) rect([t + tab_fit, 12]); translate([60, 32]) rect([2 * t + tab_fit, 12]); } for (i = [0:3]) label(str(r2(5.9 + bush_fit + 0.1 * i)), [15 + 22 * i, 6], 3); for (i = [0:2]) label(str(r2(5.9 + dowel_fit + 0.1 * i)), [105 + 18 * i, 6], 3); label("BUSH", [48, 45], 3); label("DOWEL", [123, 30], 3); } // Where the parts go in the assembled view. The lantern does not follow the // exact ratio, peg gearing never does: step up from 10 degrees behind it to // the first angle where no peg overlaps a stave, then bisect back to where // the driving peg just touches its stave. Staves at mesh angle a sit at // y = rl sin a, z = rl cos a about the axle; pegs hang from the crown. theta = draw_bar_angle; function peg_gap(a) = min([1e9, for (i = [0:crown_pegs - 1]) let (ph = theta + 360 * i / crown_pegs, px = Rc * cos(ph), py = Rc * sin(ph)) if (px > 0 && abs(py) < rl + 9) for (j = [0:staves - 1]) let (b = a + 360 * j / staves, sy = rl * sin(b), sz = rl * cos(b), dz = abs(min(max(sz, peg_tip - zL), zc - zL) - sz)) if (dz < 3) sqrt(max(0, lx_in - px, px - lx_out) ^ 2 + max(0, abs(py - sy) - sqrt(9 - dz * dz)) ^ 2) - 3]); function first_free(a, k = 0) = peg_gap(a) >= 0 || k > 60 ? a : first_free(a + 0.5, k + 1); function contact(lo, hi, k = 0) = k >= 40 ? hi : let (m = (lo + hi) / 2) peg_gap(m) >= 0 ? contact(lo, m, k + 1) : contact(m, hi, k + 1); mesh_a = let (f = first_free(ratio * theta + 180 / staves - 10)) contact(f - 0.5, f); // The lantern and wheel turn this far about the axle, clockwise seen from // the trough end as the draw-bar goes counterclockwise beta = 67.5 - mesh_a; // A plate standing across the axle (thickness along +x), and one on the // axle turned with it across = [90, 0, 90]; function on_axle(b) = [90, -b, 90]; // A point at radius r along the draw-bar, height z function round_at(r, z) = [r * cos(theta), r * sin(theta), z]; if (output == "all" || output == "base") step("Glue the base layers and stand the walkway on its five posts") { part("Base", qty = 2, at = [[0, 0, 0], [0, 0, t]]) base(); part("Walkway", at = [0, 0, deck_z]) deck(); part("Walkway post", qty = 5, at = [for (a = [60:60:300]) [-t / 2 * sin(a), t / 2 * cos(a), 2 * t]], rotate = [for (a = [60:60:300]) [90, 0, a]]) post(); } if (output == "all" || output == "gearing") { step("Build the tower and clamp the crown wheel and draw-bar on its shaft") { part("Tower disc", qty = tower_n, at = [for (i = [0:tower_n - 1]) [0, 0, 2 * t + i * t]]) difference() { circle(d = 24); bush(); } part("Crown wheel", qty = 2, at = [[0, 0, zc], [0, 0, zc + t]], rotate = [0, 0, theta]) crown(); part("Shaft collar", qty = collar_n, at = [for (i = [0:collar_n - 1]) [0, 0, zc + 2 * t + i * t]]) annulus(26, 4.4); part("Draw-bar", qty = 2, at = [[0, 0, zd], [0, 0, zd + t]], rotate = [0, 0, theta]) draw_bar(); } step("Thread the lantern, its bearing, the uprights and the water wheel onto the axle") { part("Lantern plate", qty = 4, at = [for (x = [lx_in - 2 * t, lx_in - t, lx_out, lx_out + t]) [x, 0, zL]], rotate = on_axle(beta)) lantern_plate(); part("Lantern bearing", qty = 2, at = [[lbx, 0, 2 * t], [lbx + t, 0, 2 * t]], rotate = across) lantern_bearing(); } } if (output == "all" || output == "wheel") { step("Thread the lantern, its bearing, the uprights and the water wheel onto the axle") { part("Water wheel", qty = 2, at = [[wx, 0, zL], [wx + t, 0, zL]], rotate = on_axle(beta)) wheel(); part("Wheel hub", qty = 2, at = [[wx + 2 * t, 0, zL], [wx + 3 * t, 0, zL]], rotate = on_axle(beta)) annulus(30, 4.4); } // Each pot in two layers on the wheel's outer face, turned with the wheel step("Glue the pots on the wheel, the well walls between the uprights and the trough on top") part("Pot", qty = 2 * pots, at = [for (i = [0:pots - 1], k = [0, 1]) let (g = 360 * i / pots + beta) [wx + 2 * t + k * t, (Rw - 1) * cos(g), zL + (Rw - 1) * sin(g)]], rotate = [for (i = [0:pots - 1], k = [0, 1]) on_axle(beta + 360 * i / pots - 90)]) pot(); } if (output == "all" || output == "well") { step("Thread the lantern, its bearing, the uprights and the water wheel onto the axle") { part("Inner upright", qty = 2, at = [[wix, 0, 2 * t], [wix + t, 0, 2 * t]], rotate = across) well_upright(false); part("Outer upright", qty = 2, at = [[wox, 0, 2 * t], [wox + t, 0, 2 * t]], rotate = across) well_upright(true); } step("Glue the pots on the wheel, the well walls between the uprights and the trough on top") { part("Well wall", qty = 2, at = [[wx - 4.2, Yw + t, 2 * t], [wx - 4.2, -Yw, 2 * t]], rotate = [90, 0, 0]) well_wall(); part("Trough floor", at = [trough_x, 0, trough_z]) translate([0, trough_y[0]]) rect([100, trough_y[1] - trough_y[0]]); part("Trough side", qty = 2, at = [[trough_x, trough_y[1], trough_z + t], [trough_x, trough_y[0] + t, trough_z + t]], rotate = [90, 0, 0]) rect([100, 14]); part("Trough end", at = [trough_x, 0, trough_z + t], rotate = across) translate([trough_y[0] + t, 0]) rect([trough_y[1] - trough_y[0] - 2 * t, 14]); part("Trough post", qty = 2, at = [[post_x, 0, 2 * t], [post_x + t, 0, 2 * t]], rotate = across) trough_post(); } } if (output == "all" || output == "ox") step("Set the ox on the walkway, its post up through the draw-bar") part("Ox", qty = 2, at = [round_at(L - t, deck_z + t + 1), round_at(L, deck_z + t + 1)], rotate = [90, 0, theta + 90]) ox(); // The fit test lies on the table in front of the model if (output == "all" || output == "coupon") part("Fit test", at = [base_x[0], -(Ro + 8) - 70, 0]) coupon(); echo(str("Wellspring: ", crown_pegs, " pegs to ", staves, " staves, so the wheel turns ", ratio, " times for each round of the ox; pitch radii ", r2(Rc), " and ", r2(rl), " mm at ", pitch, " mm pitch; axle ", zL, " mm up; base ", r2(base_x[1] - base_x[0]), " x ", r2(2 * Ro + 16), " mm")); // Assembly: glue the two base layers and stand the five posts in their // slots under the walkway. Glue the tower discs into a post, a 6 mm brass // tube bush (4 mm bore) in its top and bottom four discs. Glue the crown // wheel's two layers with the 26 mm dowel pegs pointing down, then the // collars and the draw-bar on top, and clamp it between nuts on a 175 mm M4 // threaded rod that turns in the tower, washers under the lower nut. Glue // each pair of lantern plates and the eight 29 mm dowel staves between them // round a brass spacer. On a 150 mm M4 rod: nut, lantern, nut and washer, // the lantern bearing, the inner upright, washer and nut, the wheel and hub, // nut and washer, the outer upright, washer and nut, a bush in every // bearing. Glue the pots on the wheel's outer face, mouths leading, the // well walls between the uprights, the trough on the outer upright and its // post, and the ox's two layers with its post up through the draw-bar's // slot; its hooves clear the walkway by 1 mm. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/fracture/ # Fracture recursive relief Fracture: a recursive vortex in relief. Each triangle drops its middle triangle and repeats in its three corners, then every point turns by an angle that grows with its distance from the centre. Deeper layers inset each opening a step further, so small holes stay shallow and large channels run down to a solid backer. This design has 6 parts on 6 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=fracture). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/fracture/fracture.kerf) - [fracture-sheet-1.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-1.svg) - [fracture-sheet-1.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-1.dxf) - [fracture-sheet-2.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-2.svg) - [fracture-sheet-2.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-2.dxf) - [fracture-sheet-3.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-3.svg) - [fracture-sheet-3.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-3.dxf) - [fracture-sheet-4.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-4.svg) - [fracture-sheet-4.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-4.dxf) - [fracture-sheet-5.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-5.svg) - [fracture-sheet-5.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-5.dxf) - [fracture-sheet-6.svg](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-6.svg) - [fracture-sheet-6.dxf](https://kerfscript.mylasertools.com/models/fracture/fracture-sheet-6.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Layer 0 | 1 | 400 × 400 mm | | Layer 1 | 1 | 400 × 400 mm | | Layer 2 | 1 | 400 × 400 mm | | Layer 3 | 1 | 400 × 400 mm | | Layer 4 | 1 | 400 × 400 mm | | Backer | 1 | 400 × 400 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `preset` | `"vortex"` | Vortex, Counterflow, Crystalline, Custom | | `output` | `"all"` | All panels, Layer 0 (front), Layer 1, Layer 2, Layer 3, Layer 4, Layer 5, Backer | | `size` | `400` | 360 to 420 | | `depth` | `4` | 2 to 5 | | `sectors` | `6` | 5 to 8 | | `twist` | `38` | -45 to 45 | | `layers` | `5` | 3 to 6 | | `terrace` | `0.85` | 0.6 to 1.2 | | `web` | `3.2` | 3 to 5 | | `min_opening` | `2.5` | 2.5 to 5 | | `$thickness` | `3` | 2 to 4 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Fracture: a recursive vortex in relief. Each triangle drops its middle // triangle and repeats in its three corners, then every point turns by an // angle that grows with its distance from the centre. Deeper layers inset // each opening a step further, so small holes stay shallow and large channels // run down to a solid backer. /* [Composition] */ // A ready recipe, or custom to use the settings below preset = "vortex"; // [vortex:Vortex, counterflow:Counterflow, crystalline:Crystalline, custom:Custom] // All panels, or one panel at a time output = "all"; // [all:All panels, layer0:Layer 0 (front), layer1:Layer 1, layer2:Layer 2, layer3:Layer 3, layer4:Layer 4, layer5:Layer 5, backer:Backer] // Panel diameter size = 400; // [360:10:420] // How many times the triangles split depth = 4; // [2:1:5] /* [Custom] */ // Triangular sectors around the centre sectors = 6; // [5:1:8] // Turn at the rim, in degrees; 0 for straight triangles, negative to reverse twist = 38; // [-45:1:45] /* [Relief] */ // Relief layers, plus the backer layers = 5; // [3:1:6] // How much each deeper layer insets the openings terrace = 0.85; // [0.6:0.05:1.2] // Wood left between openings, before the turn web = 3.2; // [3:0.2:5] // Smallest opening kept, as the diameter of its inscribed circle min_opening = 2.5; // [2.5:0.5:5] /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:4] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [450, 450]; $gap = 6; $fn = 180; // Each recipe: [sectors, twist] recipes = [[6, 38], [7, -35], [6, 0]]; keys = ["vortex", "counterflow", "crystalline"]; p = preset == "custom" ? [sectors, twist] : recipes[search(preset, keys)]; n = p[0]; turn = p[1]; R = size / 2; reach = R - 18; border = 2; samples = 24; assert(size >= 360 && size <= 420, "Size: 360 to 420 mm"); assert(sectors >= 5 && sectors <= 8 && sectors == floor(sectors), "Sectors: a whole number from 5 to 8"); assert(depth >= 2 && depth <= 5 && depth == floor(depth), "Depth: a whole number from 2 to 5"); assert(twist >= -45 && twist <= 45, "Twist: -45 to 45 degrees"); assert(layers >= 3 && layers <= 6 && layers == floor(layers), "Layers: a whole number from 3 to 6"); assert(terrace >= 0.6 && terrace <= 1.2, "Terrace: 0.6 to 1.2 mm"); assert(web >= 3 && web <= 5, "Web: 3 to 5 mm"); assert(min_opening >= 2.5 && min_opening <= 5, "Smallest opening: 2.5 to 5 mm"); assert($thickness >= 2 && $thickness <= 4 && $kerf >= 0 && $kerf <= 0.25, "Thickness: 2 to 4 mm; kerf: 0 to 0.25 mm"); assert(contains(["all", "layer0", "layer1", "layer2", "layer3", "layer4", "layer5", "backer"], output), "Unknown output"); assert(output != "layer5" || layers >= 6, "Layer 5 needs six layers"); assert(output != "layer4" || layers >= 5, "Layer 4 needs five or more layers"); assert(output != "layer3" || layers >= 4, "Layer 3 needs four or more layers"); function polar(r, a) = [r * cos(a), r * sin(a)]; function side(t, i) = norm(t[(i + 1) % 3] - t[(i + 2) % 3]); function perimeter(t) = side(t, 0) + side(t, 1) + side(t, 2); // Incentre and inscribed radius of a triangle function center(t) = let (a = side(t, 0), b = side(t, 1), c = side(t, 2)) (t[0] * a + t[1] * b + t[2] * c) / (a + b + c); function area(t) = abs((t[1][0] - t[0][0]) * (t[2][1] - t[0][1]) - (t[1][1] - t[0][1]) * (t[2][0] - t[0][0])) / 2; function radius(t) = 2 * area(t) / perimeter(t); // Move every side of a triangle inwards by d function inset(t, d) = let (c = center(t), k = 1 - d / radius(t)) [for (p = t) c + (p - c) * k]; // Each point turns by its own angle, growing with the square of its radius function warp(p) = polar(norm(p), atan2(p[1], p[0]) + turn * pow(norm(p) / reach, 2)); // The sides sampled, so straight edges bend into curves function curved(t) = [for (e = [0:2]) let (a = t[e], b = t[(e + 1) % 3]) for (i = [0:samples - 1]) let (f = i / samples) warp([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f])]; function hole_count(k, r, z) = k < 1 || r / 2 - web / 2 - z * terrace < min_opening / 2 ? 0 : 1 + 3 * hole_count(k - 1, r / 2, z); // Cut the middle triangle, then repeat in the three corners module cutouts(t, k, z) { if (k > 0 && radius(t) / 2 - web / 2 - z * terrace >= min_opening / 2) { ab = (t[0] + t[1]) / 2; bc = (t[1] + t[2]) / 2; ca = (t[2] + t[0]) / 2; polygon(curved(inset([ab, bc, ca], web / 2 + z * terrace))); cutouts([t[0], ab, ca], k - 1, z); cutouts([ab, t[1], bc], k - 1, z); cutouts([ca, bc, t[2]], k - 1, z); } } sector = inset([[0, 0], polar(reach, -180 / n), polar(reach, 180 / n)], border); // A round panel with three bolt holes module blank() { difference() { circle(r = R); for (a = [30:120:270]) translate(polar(R - 8, a)) circle(d = 4.5, $fn = 48); } } module relief(z) { // Assembled: on the backer, the deepest layer first and layer 0 at the front part(str("Layer ", z), at = [0, 0, (layers - z) * $thickness]) difference() { blank(); for (a = [0:n - 1]) rotate(a * 360 / n) cutouts(sector, depth, z); } echo("Layer", z, "openings", n * hole_count(depth, radius(sector), z)); } function layer_step(z) = str("Lay layer ", z, z == 0 ? " on top" : " on", ", turned until the channels line up"); if (output == "all") { // Name the steps in build order first: the backer, then the deepest layer up step("Lay the backer down") {} for (z = [layers - 1:-1:0]) step(layer_step(z)) {} for (z = [0:layers - 1]) step(layer_step(z)) relief(z); step("Lay the backer down") part("Backer", at = [0, 0, 0]) blank(); } else if (output == "backer") part("Backer") blank(); else for (z = [0:layers - 1]) if (output == "layer" + str(z)) relief(z); // Assembly: lay the backer down, then the layers from the deepest up to // layer 0 on top, all face up and none flipped. The bolt holes are // symmetric, so turn each layer until the channels line up. Join the stack // with three M4 bolts, a washer on each side and locknuts; six 3 mm panels // take M4 x 30 mm bolts. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/morphogen/ # Morphogen terrain relief Morphogen: a living-looking terrain drawn by a field. Two waves cross in a space that is twisted around an off-centre point and folded by two shears; each layer cuts away where the field rises above a level. Deeper layers use higher levels, so channels narrow and split into pockets towards the backer. This design has 5 parts on 5 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=morphogen). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/morphogen/morphogen.kerf) - [morphogen-sheet-1.svg](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-1.svg) - [morphogen-sheet-1.dxf](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-1.dxf) - [morphogen-sheet-2.svg](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-2.svg) - [morphogen-sheet-2.dxf](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-2.dxf) - [morphogen-sheet-3.svg](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-3.svg) - [morphogen-sheet-3.dxf](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-3.dxf) - [morphogen-sheet-4.svg](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-4.svg) - [morphogen-sheet-4.dxf](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-4.dxf) - [morphogen-sheet-5.svg](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-5.svg) - [morphogen-sheet-5.dxf](https://kerfscript.mylasertools.com/models/morphogen/morphogen-sheet-5.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Layer 0 | 1 | 360 × 360 mm | | Layer 1 | 1 | 360 × 360 mm | | Layer 2 | 1 | 360 × 360 mm | | Layer 3 | 1 | 360 × 360 mm | | Backer | 1 | 360 × 360 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `preset` | `"undercurrent"` | Undercurrent, Delta, Cellular, Custom | | `output` | `"all"` | All panels, Layer 0 (front), Layer 1, Layer 2, Layer 3, Layer 4, Backer | | `size` | `360` | 340 to 400 | | `wavelength` | `44` | 40 to 58 | | `focus_y` | `15` | -40 to 40 | | `warp` | `24` | 15 to 32 | | `vortex` | `90` | -100 to 100 | | `phase` | `28` | 0 to 180 | | `focus_x` | `-35` | -50 to 50 | | `threshold` | `0.22` | 0.2 to 0.7 | | `terrace` | `0.23` | 0.15 to 0.25 | | `layers` | `4` | 3 to 5 | | `smooth` | `2` | 1.5 to 3 | | `$thickness` | `3` | 2 to 4 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // Morphogen: a living-looking terrain drawn by a field. Two waves cross in a // space that is twisted around an off-centre point and folded by two shears; // each layer cuts away where the field rises above a level. Deeper layers use // higher levels, so channels narrow and split into pockets towards the backer. /* [Composition] */ // A ready recipe, or custom to use the settings below preset = "undercurrent"; // [undercurrent:Undercurrent, delta:Delta, cellular:Cellular, custom:Custom] // All panels, or one panel at a time output = "all"; // [all:All panels, layer0:Layer 0 (front), layer1:Layer 1, layer2:Layer 2, layer3:Layer 3, layer4:Layer 4, backer:Backer] // Panel diameter size = 360; // [340:10:400] // Spacing of the waves wavelength = 44; // [40:1:58] // Up and down position of the twist centre focus_y = 15; // [-40:5:40] /* [Custom] */ // How far the shears fold the waves warp = 24; // [15:1:32] // Twist at the centre, in degrees; negative turns the other way vortex = 90; // [-100:5:100] // Shifts where the waves meet, in degrees phase = 28; // [0:1:180] // Side to side position of the twist centre focus_x = -35; // [-50:5:50] // Field level of the front layer: low for broad channels, high for pockets threshold = 0.22; // [0.2:0.02:0.7] // Field level added for each deeper layer terrace = 0.23; // [0.15:0.01:0.25] /* [Relief] */ // Relief layers, plus the backer layers = 4; // [3:1:5] // Rounding of the openings, and the smallest sliver removed smooth = 2; // [1.5:0.25:3] /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:4] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [440, 440]; $gap = 6; $fn = 192; // Each recipe: [warp, vortex, phase, focus_x, threshold, terrace] recipes = [ [24, 90, 28, -35, 0.22, 0.23], [32, 35, 110, 25, 0.22, 0.23], [20, -65, 65, -35, 0.62, 0.18] ]; keys = ["undercurrent", "delta", "cellular"]; p = preset == "custom" ? [warp, vortex, phase, focus_x, threshold, terrace] : recipes[search(preset, keys)]; fold = p[0]; spin = p[1]; shift = p[2]; fx = p[3]; level = p[4]; step = p[5]; // Grid squares across the panel N = 120; R = size / 2; h = size / N; assert(size >= 340 && size <= 400, "Size: 340 to 400 mm"); assert(wavelength >= 40 && wavelength <= 58, "Wavelength: 40 to 58 mm"); assert(warp >= 15 && warp <= 32, "Warp: 15 to 32 mm"); assert(vortex >= -100 && vortex <= 100, "Vortex: -100 to 100 degrees"); assert(phase >= 0 && phase <= 180, "Phase: 0 to 180 degrees"); assert(focus_x >= -50 && focus_x <= 50 && focus_y >= -40 && focus_y <= 40, "Twist centre: x -50 to 50, y -40 to 40 mm"); assert(threshold >= 0.2 && threshold <= 0.7, "Threshold: 0.2 to 0.7"); assert(terrace >= 0.15 && terrace <= 0.25, "Terrace: 0.15 to 0.25"); assert(layers >= 3 && layers <= 5 && layers == floor(layers), "Layers: a whole number from 3 to 5"); assert(smooth >= 1.5 && smooth <= 3, "Smoothing: 1.5 to 3 mm"); assert($thickness >= 2 && $thickness <= 4 && $kerf >= 0 && $kerf <= 0.25, "Thickness: 2 to 4 mm; kerf: 0 to 0.25 mm"); assert(contains(["all", "layer0", "layer1", "layer2", "layer3", "layer4", "backer"], output), "Unknown output"); assert(output != "layer3" || layers >= 4, "Layer 3 needs four or more layers"); assert(output != "layer4" || layers >= 5, "Layer 4 needs five layers"); // A twist that fades with distance from the focus, two sine shears, then a // main wave crossed by a weaker one and a slight left to right slope function field(x, y) = let ( dx = x - fx, dy = y - focus_y, r = sqrt(dx * dx + dy * dy), t = atan2(dy, dx) + spin * exp(-r * r / (0.14 * size * size)), xx = r * cos(t) + fx, yy = r * sin(t) + focus_y, u = xx + fold * sin(yy * 360 / 170 + shift) + 8 * sin(yy * 360 / 65), v = yy + 15 * sin(u * 360 / 140), a = u * 360 / wavelength, b = v * 360 / (wavelength * 1.35)) sin(a) + 0.55 * cos(b) + 0.10 * x / R; // The field sampled once at every grid point, and the grid points in rows: // point (i, j) is GP[at(i, j)] V = [for (j = [0:N]) [for (i = [0:N]) field(-R + i * h, -R + j * h)]]; GP = [for (j = [0:N], i = [0:N]) [-R + i * h, -R + j * h]]; function at(i, j) = j * (N + 1) + i; // Grid squares that reach into the circle the openings are clipped to function near(i, j) = let (x = -R + (i + 0.5) * h, y = -R + (j + 0.5) * h) sqrt(x * x + y * y) < R - 16 + h; // Where the field crosses the level between grid points p and q function cut(p, q, fp, fq, v) = let (s = (v - fp) / (fq - fp)) [p[0] + (q[0] - p[0]) * s, p[1] + (q[1] - p[1]) * s]; // The part above the level of a triangle p, q, r whose corner p is the odd // one out, always as four points function corner(p, q, r, fp, fq, fr, v) = let (m = cut(p, q, fp, fq, v), n = cut(r, p, fr, fp, v)) fp >= v ? [p, m, n, n] : [q, r, n, m]; function piece(a, b, c, fa, fb, fc, v) = let (A = fa >= v, B = fb >= v, C = fc >= v) A == B ? corner(c, a, b, fc, fa, fb, v) : A == C ? corner(b, c, a, fb, fc, fa, v) : corner(a, b, c, fa, fb, fc, v); // Every grid square split into two triangles (k = 0, 1): no ambiguous cases. // Triangles wholly above the level are outlines through the grid points, the // ones the level crosses get their own points. All of them tile without // overlapping, so even-odd polygons of them are their union. module raw_field(v) { F = [for (j = [0:N - 1], i = [0:N - 1], k = [0:1]) let (bi = i + 1, bj = j + k, ci = i + 1 - k, cj = j + 1) if (near(i, j) && V[j][i] >= v && V[bj][bi] >= v && V[cj][ci] >= v) [at(i, j), at(bi, bj), at(ci, cj)]]; X = [for (j = [0:N - 1], i = [0:N - 1], k = [0:1]) let (bi = i + 1, bj = j + k, ci = i + 1 - k, cj = j + 1, fa = V[j][i], fb = V[bj][bi], fc = V[cj][ci], n = (fa >= v ? 1 : 0) + (fb >= v ? 1 : 0) + (fc >= v ? 1 : 0)) if (near(i, j) && n > 0 && n < 3) piece(GP[at(i, j)], GP[at(bi, bj)], GP[at(ci, cj)], fa, fb, fc, v)]; union() { if (len(F) > 0) polygon(GP, F); if (len(X) > 0) polygon([for (p = X) each p], [for (k = [0:len(X) - 1]) [4 * k, 4 * k + 1, 4 * k + 2, 4 * k + 3]]); } } // Close the seams between triangles, round the tips and drop slivers, then // close any gaps the rounding left module holes(z) { offset(r = -smooth) offset(r = smooth) offset(r = smooth) offset(r = -2 * smooth) offset(r = smooth) intersection() { circle(r = R - 16); raw_field(level + z * step); } } // A round panel with three bolt holes, spaced unevenly so it only fits one way module blank() { difference() { circle(r = R); for (a = [20, 142, 268]) translate([(R - 8) * cos(a), (R - 8) * sin(a)]) circle(d = 4.5, $fn = 48); } } module panel(z) { // Assembled: on the backer, the deepest layer first and layer 0 at the front part(str("Layer ", z), at = [0, 0, (layers - z) * $thickness]) difference() { blank(); holes(z); } } function layer_step(z) = str("Lay layer ", z, z == 0 ? " on top" : " on", ", face up"); if (output == "all") { // Name the steps in build order first: the backer, then the deepest layer up step("Lay the backer down") {} for (z = [layers - 1:-1:0]) step(layer_step(z)) {} for (z = [0:layers - 1]) step(layer_step(z)) panel(z); step("Lay the backer down") part("Backer", at = [0, 0, 0]) blank(); } else if (output == "backer") part("Backer") blank(); else for (z = [0:layers - 1]) if (output == "layer" + str(z)) panel(z); // Assembly: lay the backer down, then the layers from the deepest up to // layer 0 on top, all face up and none flipped; the uneven bolt holes line // them up. Join the stack with three M4 bolts, a washer on each side and // locknuts; five 3 mm panels take M4 x 25 mm bolts. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tilt-maze/ # Tilt maze Tilt maze: roll a ball bearing from the dot to the star. Every seed draws a new maze, and there is always exactly one way through. The maze layer is stacked as many times as the ball needs, under a clear cover. This design has 5 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tilt-maze). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tilt-maze/tilt-maze.kerf) - [tilt-maze-sheet-1.svg](https://kerfscript.mylasertools.com/models/tilt-maze/tilt-maze-sheet-1.svg) - [tilt-maze-sheet-1.dxf](https://kerfscript.mylasertools.com/models/tilt-maze/tilt-maze-sheet-1.dxf) - [tilt-maze-sheet-2.svg](https://kerfscript.mylasertools.com/models/tilt-maze/tilt-maze-sheet-2.svg) - [tilt-maze-sheet-2.dxf](https://kerfscript.mylasertools.com/models/tilt-maze/tilt-maze-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 144 × 144 mm | | Maze layer | 3 | 144 × 144 mm | | Cover | 1 | 144 × 144 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `cols` | `12` | 4 to 20 | | `rows` | `12` | 4 to 20 | | `seed` | `7` | 1 to 999 | | `ball` | `6` | 4 to 12 | | `wall` | `3` | 2 to 6 | | `cover` | `true` | on or off | | `$thickness` | `3` | 2 to 6 | | `cover_thickness` | `2` | 1.5 to 3 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Tilt maze: roll a ball bearing from the dot to the star. Every seed draws // a new maze, and there is always exactly one way through. The maze layer // is stacked as many times as the ball needs, under a clear cover. /* [Maze] */ // Cells across cols = 12; // [4:1:20] // Cells down rows = 12; // [4:1:20] // Different seeds draw different mazes seed = 7; // [1:1:999] /* [Ball and walls] */ // Ball bearing diameter ball = 6; // [4:0.5:12] // Width of the walls between channels wall = 3; // [2:0.5:6] // Add a clear cover to keep the ball in cover = true; /* [Material] */ // Sheet thickness of the wood layers $thickness = 3; // [2:0.5:6] // Thickness of the clear cover cover_thickness = 2; // [1.5:0.5:3] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 32; channel = ball + 1.5; cell = channel + wall; border = 9; w = cols * cell + 2 * border; h = rows * cell + 2 * border; // Enough maze layers to sink the ball below the cover layers = ceil((ball + 0.5) / $thickness); rnd = rands(0, 0.999, cols * rows * 2, seed); // The centre of cell [x, y] function at(c) = [border + (c.x + 0.5) * cell, border + (c.y + 0.5) * cell]; // Depth-first search: walk to a random unvisited neighbour, back up when // there is none. Returns the passages as pairs of cells. function carve(stack, seen, passages, k) = len(stack) == 0 ? passages : let ( here = stack[len(stack) - 1], next = [for (d = [[1, 0], [-1, 0], [0, 1], [0, -1]]) let (n = here + d) if (n.x >= 0 && n.x < cols && n.y >= 0 && n.y < rows && !seen[n.y * cols + n.x]) n] ) len(next) == 0 ? carve(slice(stack, 0, len(stack) - 1), seen, passages, k) : let (n = next[floor(rnd[k % len(rnd)] * len(next))], id = n.y * cols + n.x) carve(concat(stack, [n]), [for (i = [0:len(seen) - 1]) i == id || seen[i]], concat(passages, [[here, n]]), k + 1); passages = carve([[0, 0]], [for (i = [0:cols * rows - 1]) i == 0], [], 0); start = [0, 0]; goal = [cols - 1, rows - 1]; module plate() rect([w, h], r = 5); module screw_holes() for (x = [border / 2, w - border / 2], y = [border / 2, h - border / 2]) translate([x, y]) circle(d = 3.4); module channels() for (p = passages) hull() { translate(at(p[0])) rect(channel, center = true); translate(at(p[1])) rect(channel, center = true); } // Assembled: the base, the maze layers and the cover, stacked; the cover's // own material block changes $thickness, so keep the plywood's ply = $thickness; step("Glue the maze layers onto the base, lining up the screw holes") { part("Base", at = [0, 0, 0]) { difference() { plate(); screw_holes(); } engrave() { translate(at(start)) circle(d = channel * 0.55); translate(at(goal)) star(5, channel * 0.42, channel * 0.18); } } part("Maze layer", qty = layers, at = [for (k = [1:layers]) [0, 0, k * ply]]) difference() { plate(); screw_holes(); channels(); } } if (cover) step("Drop the ball in and screw the clear cover on") material(thickness = cover_thickness, name = "clear acrylic") part("Cover", at = [0, 0, (layers + 1) * ply]) difference() { plate(); screw_holes(); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/topo-island/ # Layered island Layered island: a made-up mountain as a stack of contour layers, like a topographic map you can touch. The height of the land is a formula, and each layer is traced where the land crosses its height. Every layer is scored with the outline of the one above, so gluing up is easy. This design has 9 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=topo-island). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/topo-island/topo-island.kerf) - [SVG](https://kerfscript.mylasertools.com/models/topo-island/topo-island.svg) - [DXF](https://kerfscript.mylasertools.com/models/topo-island/topo-island.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 200 × 150 mm | | Layer 1 | 1 | 80.8 × 140.1 mm | | Layer 2 | 1 | 68.3 × 132 mm | | Layer 3 | 1 | 56.3 × 124.1 mm | | Layer 4 | 1 | 49.6 × 117.4 mm | | Layer 5 | 1 | 43 × 67.7 mm | | Layer 6 | 1 | 36 × 56.3 mm | | Layer 7 | 1 | 30.4 × 38.5 mm | | Layer 8 | 1 | 23.1 × 28.3 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `200` | 120 to 400 | | `layers` | `8` | 3 to 14 | | `roughness` | `1` | 0 to 2 | | `seed` | `9` | 1 to 999 | | `name` | `"ISLA KERF"` | | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Layered island: a made-up mountain as a stack of contour layers, like a // topographic map you can touch. The height of the land is a formula, and // each layer is traced where the land crosses its height. Every layer is // scored with the outline of the one above, so gluing up is easy. /* [Island] */ // Width of the base plate size = 200; // [120:10:400] // Contour layers above the sea layers = 8; // [3:1:14] // How rough the land is roughness = 1; // [0:0.1:2] // Different seeds raise different islands seed = 9; // [1:1:999] // Engraved on the base plate, leave empty for none name = "ISLA KERF"; // 20 /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; plate = [size, size * 0.75]; peak = [size * 0.45, plate[1] * 0.5]; reach = plate[1] * 0.48; rnd = rands(0, 1, 16, seed); // Two smaller hills, somewhere around the main peak hills = [for (i = [0:1]) let (a = rnd[0] * 360 + i * (110 + rnd[1] * 100), d = reach * (0.55 + rnd[i + 2] * 0.25)) [peak + d * [cos(a), sin(a)], reach * (0.2 + rnd[i + 4] * 0.08), 0.5 + rnd[i + 6] * 0.25]]; function bump(p, c, w) = exp(-((p - c) * (p - c)) / (2 * w * w)); // Height of the land at point p function land(p) = bump(p, peak, reach * 0.26) + hills[0][2] * bump(p, hills[0][0], hills[0][1]) + hills[1][2] * bump(p, hills[1][0], hills[1][1]) + roughness * (0.07 * sin(p.x * 360 / (reach * 0.9) + rnd[8] * 360) * sin(p.y * 360 / (reach * 1.1) + rnd[9] * 360) + 0.03 * sin((p.x + p.y) * 360 / (reach * 0.45) + rnd[10] * 360) + 0.01 * sin((p.x - 2 * p.y) * 360 / (reach * 0.22) + rnd[11] * 360)); // Sample the land along rays from the peak once; every layer reuses them rays = 144; samples = 48; dr = reach / samples; profiles = [for (i = [0:rays - 1]) let (a = i * 360 / rays) [for (j = [0:samples]) land(peak + j * dr * [cos(a), sin(a)])]]; coast = 0.08; top = land(peak); function level(k) = coast + (top - coast) * k / (layers + 1); // Distance along a ray where the land first drops below height h function crossing(prof, h, j = 1) = j > samples ? reach : prof[j] < h ? dr * (j - 1 + (prof[j - 1] - h) / (prof[j - 1] - prof[j])) : crossing(prof, h, j + 1); function contour(k) = [for (i = [0:rays - 1]) let (a = i * 360 / rays) peak + max(crossing(profiles[i], level(k)), 3) * [cos(a), sin(a)]]; // Smooth the traced outline and keep it free of slivers module contour_layer(k) offset(r = 1.2) offset(r = -1.2) polygon(contour(k)); // Assembled: the layers stacked on the base, lowest first part("Base", at = [0, 0, 0]) { rect(plate, r = 4); score() contour_layer(1); engrave() if (name != "") translate([plate[0] - 10, 10]) text(name, size = plate[1] * 0.045, halign = "right", spacing = 1.2); score() translate([plate[0] - 18, plate[1] - 20]) { for (a = [0:90:270]) rotate(a) polygon([[0, 10], [2, 0], [-2, 0]]); circle(r = 4, $fn = 32); } } for (k = [1:layers]) step(str("Glue layer ", k, " on its scored outline")) part(str("Layer ", k), at = [0, 0, k * $thickness]) { contour_layer(k); if (k < layers) { score() contour_layer(k + 1); // The number hides under the next layer engrave() translate(peak) text(str(k), size = 4, halign = "center", valign = "center"); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/fractal-tree/ # Fractal tree ornament Fractal tree: a module that calls itself. Each branch splits in two, a little shorter and thinner, until the twigs end in leaves. The seed bends every branch differently, so no two trees are the same. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=fractal-tree). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/fractal-tree/fractal-tree.kerf) - [SVG](https://kerfscript.mylasertools.com/models/fractal-tree/fractal-tree.svg) - [DXF](https://kerfscript.mylasertools.com/models/fractal-tree/fractal-tree.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 120 × 126 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `120` | 80 to 240 | | `depth` | `6` | 4 to 8 | | `spread` | `56` | 20 to 80 | | `ratio` | `0.76` | 0.6 to 0.82 | | `wildness` | `14` | 0 to 30 | | `seed` | `12` | 1 to 999 | | `leaves` | `true` | on or off | | `label` | `"2026"` | | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Fractal tree: a module that calls itself. Each branch splits in two, // a little shorter and thinner, until the twigs end in leaves. The seed // bends every branch differently, so no two trees are the same. /* [Tree] */ // Diameter of the ring size = 120; // [80:5:240] // How many times the branches split depth = 6; // [4:1:8] // Angle between two branches, in degrees spread = 56; // [20:1:80] // Length of a branch compared with the one before ratio = 0.76; // [0.6:0.01:0.82] // How much the branches wander wildness = 14; // [0:1:30] // Different seeds grow different trees seed = 12; // [1:1:999] // Round leaves at the ends of the twigs leaves = true; /* [Text] */ // Engraved on the ground, leave empty for none label = "2026"; // 16 /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 20; r = size / 2; rim = max(3, size * 0.03); ground = -r * 0.62; leaf = size / 120; thinnest = 1.6; rnd = rands(-1, 1, pow(2, depth + 1), seed); base = [0, ground + r * 0.05]; // The trunk length that would make the tree about as tall as the ring tall = r * 1.45 * (1 - ratio) / (1 - pow(ratio, depth + 1)); // Where every branch ends, from a trunk of this length standing at [0, 0]: // the same steps as branch() below, as numbers instead of shapes function tips(length, level, k, pos = [0, 0], a = 0) = let ( a2 = a + rnd[k] * wildness, end = pos + length * [-sin(a2), cos(a2)] ) level == 0 ? [end] : concat([end], [for (side = [0, 1]) each tips( length * ratio * (1 + rnd[(2 * k + 1 + side) % len(rnd)] * 0.12), level - 1, 2 * k + 1 + side, end, a2 + (side == 0 ? 1 : -1) * spread / 2)]); // How far a tip may go: inside the ring, leaving room for its leaf inner = r - rim - 3.6 * leaf - 1.5; // How much of direction d fits inside the ring from the base function fit(d) = let (bd = base * d, dd = d * d, c = base * base - inner * inner) dd == 0 ? 1 : (-bd + sqrt(bd * bd - dd * c)) / dd; // Shorten the tree just enough to keep every leaf inside the ring trunk = tall * min(1, min([for (d = tips(tall, depth, 0)) fit(d)])); // One branch of this length and width w, then two smaller ones on top. // k numbers the branches so each one gets its own random bend. module branch(length, w, level, k) { bend = rnd[k] * wildness; rotate(bend) { hull() { circle(d = w); translate([0, length]) circle(d = max(thinnest, w * 0.7)); } if (level > 0) translate([0, length]) for (side = [0, 1]) rotate((side == 0 ? 1 : -1) * spread / 2) branch(length * ratio * (1 + rnd[(2 * k + 1 + side) % len(rnd)] * 0.12), max(thinnest, w * 0.7), level - 1, 2 * k + 1 + side); else if (leaves) translate([0, length + leaf]) ellipse([1.5 * leaf, 2.6 * leaf]); } } module ground_shape() intersection() { circle(r = r - rim / 2, $fn = 180); translate([0, ground - r * 0.9]) ellipse([r * 1.3, r], $fn = 180); } difference() { union() { difference() { circle(r = r, $fn = 180); circle(r = r - rim, $fn = 180); } ground_shape(); // Closing: grow then shrink, so gaps too small to cut are filled in offset(r = -0.3) offset(r = 0.3) translate(base) branch(trunk, max(5, size * 0.055), depth, 0); // Hanging loop translate([0, r]) circle(d = 12); } translate([0, r + 1.5]) circle(d = 5); } if (label != "") engrave() translate([0, ground - r * 0.2]) text(label, size = r * 0.1, halign = "center", valign = "center"); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/round-badge/ # Round badge Round badge: text set around a circle, letter by letter. Each letter is measured, then placed and turned to follow the ring, so any words fit. A start for logos, coasters, seals and patches. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=round-badge). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/round-badge/round-badge.kerf) - [SVG](https://kerfscript.mylasertools.com/models/round-badge/round-badge.svg) - [DXF](https://kerfscript.mylasertools.com/models/round-badge/round-badge.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 90 × 90 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `top_text` | `"HANDMADE WITH CARE"` | | | `bottom_text` | `"EST. 2026"` | | | `centre_text` | `"KS"` | | | `size` | `90` | 50 to 200 | | `edge` | `"scalloped"` | Round, Scalloped | | `style` | `"text"` | Engraved text, Engraved ring | | `hole` | `false` | on or off | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Round badge: text set around a circle, letter by letter. Each letter is // measured, then placed and turned to follow the ring, so any words fit. // A start for logos, coasters, seals and patches. /* [Text] */ // Around the top top_text = "HANDMADE WITH CARE"; // 32 // Around the bottom bottom_text = "EST. 2026"; // 24 // In the middle centre_text = "KS"; // 4 /* [Badge] */ // Diameter size = 90; // [50:5:200] // Edge of the badge edge = "scalloped"; // [round:Round, scalloped:Scalloped] // Engraved text on plain wood, or plain text on an engraved ring style = "text"; // [text:Engraved text, band:Engraved ring] // A hole to hang it from hole = false; /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 120; r = size / 2; band_out = r * 0.86; band_in = r * 0.62; letters = (band_out - band_in) * 0.5; mid = (band_out + band_in) / 2; // Width of the first i letters of s function before(s, i) = i == 0 ? 0 : text_width(slice(s, 0, i), letters); // Angle, in degrees, of the middle of letter i on a circle of radius rr function angle_of(s, i, rr) = (before(s, i) + text_width(s[i], letters) / 2) * 180 / (PI * rr); // Top text runs clockwise with the letters standing on the ring module top_arc(s) { rr = mid - letters * 0.36; span = text_width(s, letters) * 180 / (PI * rr); for (i = [0:len(s) - 1]) rotate(90 + span / 2 - angle_of(s, i, rr)) translate([rr, 0]) rotate(-90) text(s[i], size = letters, halign = "center"); } // Bottom text runs anticlockwise, so it reads left to right the right way up module bottom_arc(s) { rr = mid + letters * 0.36; span = text_width(s, letters) * 180 / (PI * rr); for (i = [0:len(s) - 1]) rotate(270 - span / 2 + angle_of(s, i, rr)) translate([rr, 0]) rotate(90) text(s[i], size = letters, halign = "center"); } module words() { if (top_text != "") top_arc(top_text); if (bottom_text != "") bottom_arc(bottom_text); // Stars between the two lines of text for (x = [-1, 1]) translate([x * mid, 0]) star(5, letters * 0.4, letters * 0.17); } module outline_shape() { if (edge == "round") circle(r = r); else union() { circle(r = r * 0.95); for (a = [0:10:350]) rotate(a) translate([r * 0.95, 0]) circle(r = r * 0.05, $fn = 24); } } difference() { union() { outline_shape(); if (hole) translate([0, r]) circle(d = 12); } if (hole) translate([0, r + 1]) circle(d = 5); } engrave() { if (style == "band") difference() { circle(r = band_out); circle(r = band_in); words(); } else words(); if (centre_text != "") text(centre_text, size = band_in * 0.62, halign = "center", valign = "center"); } // outline() keeps each circle a line of its own; filled circles would merge score() for (rr = [band_out + 1.2, band_in - 1.2, band_in - 3]) outline() circle(r = rr); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/gift-tags/ # Gift tags from a list Gift tags from a list: type the names, get one tag each. Every name is measured and sized to fill its tag, short or long. Use it for place cards, keyrings or labels by changing the shape and the second line. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=gift-tags). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/gift-tags/gift-tags.kerf) - [SVG](https://kerfscript.mylasertools.com/models/gift-tags/gift-tags.svg) - [DXF](https://kerfscript.mylasertools.com/models/gift-tags/gift-tags.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Mila | 1 | 72 × 31.7 mm | | Theo | 1 | 72 × 31.7 mm | | Grandma | 1 | 72 × 31.7 mm | | Oliver | 1 | 72 × 31.7 mm | | Nora | 1 | 72 × 31.7 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `names` | `"Mila, Theo, Grandma, Oliver, Nora"` | | | `line2` | `"with love"` | | | `shape` | `"tag"` | Luggage tag, Round | | `size` | `72` | 40 to 120 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Gift tags from a list: type the names, get one tag each. Every name is // measured and sized to fill its tag, short or long. Use it for place // cards, keyrings or labels by changing the shape and the second line. /* [Tags] */ // Names, separated by commas names = "Mila, Theo, Grandma, Oliver, Nora"; // Small text under each name, leave empty for none line2 = "with love"; // 24 // Tag shape shape = "tag"; // [tag:Luggage tag, round:Round] // Width of a luggage tag, or diameter of a round one size = 72; // [40:2:120] /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 64; hole = 5; function trim(s) = len(s) == 0 ? s : s[0] == " " ? trim(slice(s, 1)) : s[len(s) - 1] == " " ? trim(slice(s, 0, len(s) - 1)) : s; list = [for (n = split(names, ",")) if (trim(n) != "") trim(n)]; // Text area [x, y, width, height] and hole position for each shape tag_h = size * 0.44; area = shape == "tag" ? [tag_h * 0.62, 0, size - tag_h * 0.62 - 4, tag_h] : [-size * 0.38, -size * 0.3, size * 0.76, size * 0.52]; hole_at = shape == "tag" ? [tag_h * 0.3, tag_h / 2] : [0, size / 2 - 8]; module blank() { if (shape == "tag") offset(r = 2) offset(delta = -2) polygon([ [0, tag_h * 0.28], [tag_h * 0.28, 0], [size, 0], [size, tag_h], [tag_h * 0.28, tag_h], [0, tag_h * 0.72] ]); else circle(d = size); } // The largest text size at which the name still fits the area function fit(name, width, height) = min(height, width / text_width(name, 1)); module tag(name) { small = line2 == "" ? 0 : area[3] * 0.16; big = fit(name, area[2] * 0.86, area[3] * (line2 == "" ? 0.5 : 0.4)); cx = area[0] + area[2] / 2; cy = area[1] + area[3] / 2 + (line2 == "" ? 0 : small * 0.75); difference() { blank(); translate(hole_at) circle(d = hole); } engrave() { translate([cx, cy]) text(name, size = big, halign = "center", valign = "center"); if (line2 != "") translate([cx, cy - big * 0.6 - small * 1.1]) text(line2, size = small, halign = "center", valign = "center"); } score() { translate(hole_at) circle(d = hole + 4); if (shape == "tag") translate([area[0], 3]) rect([area[2], tag_h - 6], r = 2); else circle(d = size - 4); } } for (name = list) part(name) tag(name); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/slot-desk-sign/ # SLOT desk sign SLOT, a first project: a desk sign in two pieces. A panel with a tab slips into a slot in a base. Change the word, the outline, the motif and the size; the tab, the slot and the base follow. The word and the motif are scored, so nothing falls out of the letters. This design has 2 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=slot-desk-sign). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/slot-desk-sign/slot-desk-sign.kerf) - [SVG](https://kerfscript.mylasertools.com/models/slot-desk-sign/slot-desk-sign.svg) - [DXF](https://kerfscript.mylasertools.com/models/slot-desk-sign/slot-desk-sign.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Panel | 1 | 120 × 112.9 mm | | Base | 1 | 108 × 66 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `label` | `"HELLO"` | | | `shape` | `"arch"` | Arch, Hexagon, Round | | `pattern` | `"sun"` | Sunburst, Honeycomb, Moon and stars, Leaf sprig | | `width` | `120` | 100 to 160 | | `height` | `110` | 90 to 140 | | `output` | `"sign"` | Desk sign, Fit test | | `$thickness` | `3` | 2.8 to 3.2 | | `clearance` | `0.1` | 0.1 to 0.2 | | `$kerf` | `0.15` | 0 to 0.25 | ## Source ```kerf // SLOT, a first project: a desk sign in two pieces. A panel with a tab slips // into a slot in a base. Change the word, the outline, the motif and the // size; the tab, the slot and the base follow. The word and the motif are // scored, so nothing falls out of the letters. /* [Make it yours] */ // One short word or name, shown in capitals label = "HELLO"; // 10 // Outline of the panel shape = "arch"; // [arch:Arch, hex:Hexagon, round:Round] // Drawing above the word pattern = "sun"; // [sun:Sunburst, tiles:Honeycomb, stars:Moon and stars, leaves:Leaf sprig] // Panel width width = 120; // [100:5:160] // Panel height above the base height = 110; // [90:5:140] // The sign, or two scraps to test the slot fit first output = "sign"; // [sign:Desk sign, fit:Fit test] /* [Material] */ // Sheet thickness, measured $thickness = 3; // [2.8:0.1:3.2] // Added to the slot width, in total clearance = 0.1; // [0.1:0.05:0.2] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [360, 240]; $gap = 6; $fn = 96; t = $thickness; tab_width = width * 0.36; // A little shorter than the base is thick, so the sign never rocks on its tab tab_depth = t - 0.15; base_width = width * 0.9; base_depth = max(60, height * 0.6); word = upper(label); motif_scale = min(width, height) / 110; // The outline has a flat bottom: its shoulders sit on the base module face() { if (shape == "arch") polygon(concat([[-width / 2, 0], [width / 2, 0]], [for (a = [0:3:180]) [width / 2 * cos(a), height * (0.55 + 0.45 * sin(a))]])); if (shape == "hex") polygon([[-0.32 * width, 0], [0.32 * width, 0], [width / 2, height / 2], [0.32 * width, height], [-0.32 * width, height], [-width / 2, height / 2]]); if (shape == "round") intersection() { translate([0, height * 0.45]) ellipse([width / 2, height * 0.55]); translate([-width / 2, 0]) rect([width, height]); } } module motif() { if (pattern == "sun") { outline() circle(r = 10); for (a = [0:30:330]) rotate(a) line([14, 0], [22, 0]); } if (pattern == "tiles") { outline() regular_polygon(6, 7); for (a = [30:60:330]) translate([13.5 * cos(a), 13.5 * sin(a)]) outline() regular_polygon(6, 7); } if (pattern == "stars") { outline() difference() { circle(r = 14); translate([7, 4]) circle(r = 12); } for (p = [[-22, 12, 3], [-20, -12, 2.5], [19, 13, 3.5], [23, -10, 2.5]]) translate([p[0], p[1]]) outline() star(4, p[2], p[2] * 0.35); } if (pattern == "leaves") { line([0, -21], [0, 22]); for (y = [-12, 0, 12], side = [-1, 1]) { line([0, y - 3], [side * 11, y + 5]); translate([side * 8, y + 3]) rotate(side * 45) outline() ellipse([3.5, 8]); } } } module slot(w) rect([w + clearance, t + clearance], center = true); // Assembled: the panel's tab stands in the base's slot if (output == "sign") { part("Panel", at = [0, t / 2, t], rotate = [90, 0, 0]) { union() { face(); translate([-tab_width / 2, -tab_depth]) rect([tab_width, tab_depth + 0.5]); } score() { outline() offset(delta = -4) face(); translate([0, height * 0.67]) scale(motif_scale) motif(); translate([0, height * 0.31]) text(word, size = 18, fit = width * 0.7, halign = "center", valign = "center"); } } part("Base", at = [0, 0, 0]) difference() { rect([base_width, base_depth], center = true, r = 8); slot(tab_width); } } // The same slot with a 30 mm tab, on two small scraps if (output == "fit") { part("Fit test, base") difference() { rect([50, 28], center = true, r = 3); slot(30); } part("Fit test, tab") union() { translate([-20, 0]) rect([40, 20], r = 2); translate([-15, -tab_depth]) rect([30, tab_depth + 0.5]); } } // Assembly: score first, then cut the slot, then the outlines. Lower the // panel's tab straight into the slot until its shoulders sit on the base. // If the fit is loose, or for a sign that stays put, add a little wood glue. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/pocket-owl/ # Pocket Wildlife owl card Pocket Wildlife owl: a perched owl with raised wings in six slot-together pieces, on a 150 x 100 mm card. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=pocket-owl). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/pocket-owl/pocket-owl.kerf) - [SVG](https://kerfscript.mylasertools.com/models/pocket-owl/pocket-owl.svg) - [DXF](https://kerfscript.mylasertools.com/models/pocket-owl/pocket-owl.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Owl card | 1 | 150 × 100 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `output` | `"card"` | card, loose, coupon | | `stock` | `3` | 2.8 to 3.2 | | `beam` | `0.15` | 0.08 to 0.25 | | `fit` | `0.08` | 0 to 0.18 | | `bridge` | `0.9` | 0.6 to 1.4 | ## Source ```kerf // Pocket Wildlife owl: a perched owl with raised wings in six slot-together // pieces, on a 150 x 100 mm card. The pieces stay in the card on small tabs // until an adult releases them and sands the nubs. Shapes are drawn at their // finished size and KerfScript moves the cut paths out by half the beam, so // leave kerf offsets off in your laser software. /* [Card] */ // The card, the pieces cut free, or a slot fit test output = "card"; // [card, loose, coupon] /* [Material] */ // Measured thickness of the plywood stock = 3; // [2.8:0.1:3.2] // Width of the laser cut beam = 0.15; // [0.08:0.01:0.25] // Slots are this much wider than the stock fit = 0.08; // [0:0.02:0.18] // Width of the tabs that hold pieces in the card bridge = 0.9; // [0.6:0.1:1.4] /* [Hidden] */ $thickness = stock; $kerf = beam; $fn = 48; $sheet = output == "loose" ? [300, 200] : [160, 110]; card = [150, 100]; s = stock + fit; // Drawn larger and scaled to fit the card ko = 0.8; names = ["Body", "Wings", "Face", "Chest", "Feet", "Tail"]; // Piece 0 is the body. Each joint is [piece, place along the body, height, // true when the crosspiece slot opens upward]. The body is notched to match. joints = [for (j = [[1, -9, 44, true], [2, 10, 78, false], [3, 8, 36, true], [4, 0, 27, true], [5, -22, 27, true]]) [j[0], j[1] * ko, j[2] * ko, j[3]]]; module both() { children(); mirror([1, 0]) children(); } // Rounds the corners of a shape, inside and out module soft(r = 0.8) offset(r = -r) offset(r = 2 * r) offset(r = -r) children(); module slot_up(x, y) translate([x - s / 2, y]) square([s, 200]); module slot_down(x, y) translate([x - s / 2, y - 200]) square([s, 200]); function joint(i) = [for (j = joints) if (j[0] == i) j][0]; module shape(i) { if (i == 0) difference() { body(); for (j = joints) if (j[3]) slot_down(j[1], j[2]); else slot_up(j[1], j[2]); } else if (joint(i) == undef) flat(i); else difference() { cross(i); if (joint(i)[3]) slot_up(0, joint(i)[2]); else slot_down(0, joint(i)[2]); } } // x runs from the tail to the beak, y is height above the table module body() scale(ko) path("M16 20 C21 22 24 34 24 46 C24 54 21 60 17 64 L17 68 L23 72 L17 77 C20 84 18 92 10 95 C2 98 -10 98 -18 94 C-26 89 -28 78 -26 68 C-24 60 -26 50 -30 42 C-33 34 -36 26 -40 18 C-41.5 15 -39 12.5 -36 14 L-30 20 Z"); // Crosspieces: x runs across the owl, y is height module cross(i) scale(ko) { if (i == 1) soft(0.6) both() path("M0 20 L7 21 C10 30 12 38 15 46 C22 50 30 52 38 56 Q44 57 46 62 Q51 64 53 70 Q58 73 59 79 Q63 83 63 89 L67 93 L61 96 L62 101 L56 100 L55 104 L50 99 C40 92 26 78 14 66 L8 62 L0 62 Z"); if (i == 2) soft() both() path("M0 58 C8 58 16 60 22 66 C27 72 28 82 25 90 L28 100 L26 102.5 L18 96 C12 98 6 98 0 97 Z"); if (i == 3) translate([0, 36]) ellipse([17, 16]); // Feet gripping a branch if (i == 4) soft() union() { path("M-34 4 C-34 1 -32 0 -30 0 L30 0 C34 0 36 3 35 6 C34 9 31 10 28 10 L-30 10 C-33 10 -34 7 -34 4 Z"); both() path("M3 10 L3 16 C3 18 2 20 0 20 L0 34 C6 34 11 32 12 26 L12 16 L16 9 L13 9 L11 12 L9 9 L7 12 L5 9 Z"); } if (i == 5) soft() both() path("M0 14 L4 14 C5 10 6 4 7 0 L15 0 C14 6 14 12 13 18 C12 26 8 34 0 34 Z"); } module flat(i) {} // Engraved eyes on the face module eyes(i) if (i == 2) scale(ko) both() translate([11, 80]) { score() { outline() circle(r = 9); outline() circle(r = 5.5); } engrave() circle(r = 3); } // Where each piece goes when the model is built: the body stands on edge // along x and the crosspieces stand across it function placement(i) = i == 0 ? [[0, stock / 2, 0], [90, 0, 0]] : [[joint(i)[1] - stock / 2, 0, 0], [90, 0, 90]]; // Where each piece sits on the card [x, y, turn], the tabs that hold it // [x, y, edge direction] in its own coordinates, and the title's box layout = [[38.04, -1.51, 10], [112.09, 99.05, 130], [131.34, -8.49, 10], [61.48, 25.08, 0], [118.18, 29.37, 210], [61.48, 5.84, 350]]; tabs = [[[-2.16, 77.71, -0.3], [-3.19, 16.0, 180.0], [18.65, 41.91, -77.2], [-32.01, 14.38, 65.6]], [[42.43, 81.63, 45.0], [-46.82, 61.57, 107.4], [8.89, 27.4, -106.9], [-3.31, 49.6, 0.0]], [[-19.23, 80.72, -39.1], [5.01, 46.76, -171.4], [-7.44, 47.24, 166.3]], [[3.54, 41.16, -14.3], [-12.76, 24.45, 109.8]], [[-3.35, 26.89, 11.7], [9.48, 0.0, 180.0], [-27.16, 4.11, 84.7]], [[3.42, 26.5, -23.8], [-4.4, 5.61, -101.3]]]; title = [105.48, 78.28, 34, 13]; // Cut each outline with gaps at the tabs module piece(i, tabs) { difference() { outline() offset(delta = beam / 2) shape(i); for (t = tabs) translate([t[0], t[1]]) rotate(t[2]) square([bridge + beam, 4], center = true); } eyes(i); } if (output == "card") part("Owl card") { rect(card, r = 5); for (i = [0:len(names) - 1]) translate([layout[i][0], layout[i][1]]) rotate(layout[i][2]) piece(i, tabs[i]); engrave() { translate([title[0] + title[2] / 2, title[1] + title[3] * 0.62]) text("OWL", size = title[3] * 0.42, halign = "center", valign = "center"); translate([title[0] + title[2] / 2, title[1] + title[3] * 0.16]) text("POCKET WILDLIFE", size = title[3] * 0.15, halign = "center", valign = "center"); } } if (output == "loose") for (i = [0:len(names) - 1]) step(i == 0 ? "Stand the body on edge" : "Slide each crosspiece onto the body at its notch") part(names[i], at = placement(i)[0], rotate = placement(i)[1]) { shape(i); eyes(i); } // Slots from 0.08 mm narrower to 0.12 mm wider than the stock, and three tabs if (output == "coupon") part("Fit coupon") { deltas = [-0.08, -0.04, 0, 0.04, 0.08, 0.12]; difference() { rect([130, 44], r = 3); for (k = [0:5]) translate([12 + k * 21, 26]) square([stock + deltas[k], 20]); } engrave() for (k = [0:5]) translate([12 + k * 21 + stock / 2, 21]) text(str(deltas[k] > 0 ? "+" : "", deltas[k]), size = 2.6, halign = "center", valign = "center"); for (k = [0:2]) translate([22 + k * 43, 9]) difference() { outline() offset(delta = beam / 2) square([24, 8], center = true); for (x = [-12, 12]) translate([x, 0]) square([4, bridge + beam], center = true); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/pocket-dinosaur/ # Pocket Wildlife dinosaur card Pocket Wildlife dinosaur: a long-necked dinosaur with a ribbed body in six slot-together pieces, on a 150 x 100 mm card. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=pocket-dinosaur). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/pocket-dinosaur/pocket-dinosaur.kerf) - [SVG](https://kerfscript.mylasertools.com/models/pocket-dinosaur/pocket-dinosaur.svg) - [DXF](https://kerfscript.mylasertools.com/models/pocket-dinosaur/pocket-dinosaur.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Dinosaur card | 1 | 150 × 100 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `output` | `"card"` | card, loose, coupon | | `stock` | `3` | 2.8 to 3.2 | | `beam` | `0.15` | 0.08 to 0.25 | | `fit` | `0.08` | 0 to 0.18 | | `bridge` | `0.9` | 0.6 to 1.4 | ## Source ```kerf // Pocket Wildlife dinosaur: a long-necked dinosaur with a ribbed body in six // slot-together pieces, on a 150 x 100 mm card. The pieces stay in the card // on small tabs until an adult releases them and sands the nubs. Shapes are // drawn at their finished size and KerfScript moves the cut paths out by // half the beam, so leave kerf offsets off in your laser software. /* [Card] */ // The card, the pieces cut free, or a slot fit test output = "card"; // [card, loose, coupon] /* [Material] */ // Measured thickness of the plywood stock = 3; // [2.8:0.1:3.2] // Width of the laser cut beam = 0.15; // [0.08:0.01:0.25] // Slots are this much wider than the stock fit = 0.08; // [0:0.02:0.18] // Width of the tabs that hold pieces in the card bridge = 0.9; // [0.6:0.1:1.4] /* [Hidden] */ $thickness = stock; $kerf = beam; $fn = 48; $sheet = output == "loose" ? [300, 200] : [160, 110]; card = [150, 100]; s = stock + fit; names = ["Body", "Back rib", "Middle rib", "Front rib", "Back legs", "Front legs"]; // Piece 0 is the body. Each joint is [piece, place along the body, height, // true when the crosspiece slot opens upward]. The body is notched to match. joints = [[1, -11, 43, true], [2, 0, 43, true], [3, 11, 43, true], [4, -22, 40, true], [5, 22, 40, true]]; module both() { children(); mirror([1, 0]) children(); } // Rounds the corners of a shape, inside and out module soft(r = 0.8) offset(r = -r) offset(r = 2 * r) offset(r = -r) children(); module slot_up(x, y) translate([x - s / 2, y]) square([s, 200]); module slot_down(x, y) translate([x - s / 2, y - 200]) square([s, 200]); function joint(i) = [for (j = joints) if (j[0] == i) j][0]; module shape(i) { if (i == 0) difference() { body(); for (j = joints) if (j[3]) slot_down(j[1], j[2]); else slot_up(j[1], j[2]); } else if (joint(i) == undef) flat(i); else difference() { cross(i); if (joint(i)[3]) slot_up(0, joint(i)[2]); else slot_down(0, joint(i)[2]); } } // x runs from the tail to the head, y is height above the table module body() path("M-28 30 L28 30 C34 34 40 48 42 62 C43 70 46 74 51 75 C57 75 62 76 64 79 C66 83 64 87 58 88 C52 89 49 88 46 85 C41 80 36 70 31 62 C26 57 13 61 0 61 C-12 61 -24 57 -31 50 C-42 41 -54 37 -66 37 C-69.5 37 -69.5 33 -66 33 C-54 33 -40 32 -28 30 Z"); // Crosspieces: x runs across the dinosaur, y is height module cross(i) { if (i <= 3) translate([0, 43]) ellipse(i == 2 ? [18, 14] : [16, 13]); else soft() both() path("M0 18 L6 18 C7 12 7 6 7 3 L6 0 L17 0 L16 3 C16 12 17 20 17 30 C17 40 10 47 0 47 Z"); } module flat(i) {} // The engraved eye and a scored mouth module eyes(i) if (i == 0) { engrave() translate([57, 83]) circle(r = 1.6); score() polyline([[64, 80], [55, 79.5]]); } // Where each piece goes when the model is built: the body stands on edge // along x and the crosspieces stand across it function placement(i) = i == 0 ? [[0, stock / 2, 0], [90, 0, 0]] : [[joint(i)[1] - stock / 2, 0, 0], [90, 0, 90]]; // Where each piece sits on the card [x, y, turn], the tabs that hold it // [x, y, edge direction] in its own coordinates, and the title's box layout = [[56.50, 30.86, 340], [56.28, 3.08, 0], [82.71, -8.74, 350], [52.68, -25.32, 0], [129.28, 26.28, 0], [20.88, 26.28, 0]]; tabs = [[[54.2, 88.37, -0.6], [-14.03, 30.0, 180.0], [18.52, 30.0, 180.0], [-37.53, 45.31, 32.0]], [[-3.17, 55.71, 9.2], [13.98, 36.7, -123.4]], [[-3.19, 56.75, 8.8], [15.38, 35.76, -129.1]], [[-3.17, 55.71, 9.2], [13.98, 36.7, -123.4]], [[3.17, 46.75, -9.1], [-6.57, 13.65, -95.6], [16.02, 5.27, -91.0]], [[3.17, 46.75, -9.1], [-6.57, 13.65, -95.6], [16.02, 5.27, -91.0]]]; title = [107.88, 10.68, 34, 13]; // Cut each outline with gaps at the tabs module piece(i, tabs) { difference() { outline() offset(delta = beam / 2) shape(i); for (t = tabs) translate([t[0], t[1]]) rotate(t[2]) square([bridge + beam, 4], center = true); } eyes(i); } if (output == "card") part("Dinosaur card") { rect(card, r = 5); for (i = [0:len(names) - 1]) translate([layout[i][0], layout[i][1]]) rotate(layout[i][2]) piece(i, tabs[i]); engrave() { translate([title[0] + title[2] / 2, title[1] + title[3] * 0.62]) text("DINOSAUR", size = title[3] * 0.42, halign = "center", valign = "center"); translate([title[0] + title[2] / 2, title[1] + title[3] * 0.16]) text("POCKET WILDLIFE", size = title[3] * 0.15, halign = "center", valign = "center"); } } if (output == "loose") for (i = [0:len(names) - 1]) step(i == 0 ? "Stand the body on edge" : "Slide each crosspiece onto the body at its notch") part(names[i], at = placement(i)[0], rotate = placement(i)[1]) { shape(i); eyes(i); } // Slots from 0.08 mm narrower to 0.12 mm wider than the stock, and three tabs if (output == "coupon") part("Fit coupon") { deltas = [-0.08, -0.04, 0, 0.04, 0.08, 0.12]; difference() { rect([130, 44], r = 3); for (k = [0:5]) translate([12 + k * 21, 26]) square([stock + deltas[k], 20]); } engrave() for (k = [0:5]) translate([12 + k * 21 + stock / 2, 21]) text(str(deltas[k] > 0 ? "+" : "", deltas[k]), size = 2.6, halign = "center", valign = "center"); for (k = [0:2]) translate([22 + k * 43, 9]) difference() { outline() offset(delta = beam / 2) square([24, 8], center = true); for (x = [-12, 12]) translate([x, 0]) square([4, bridge + beam], center = true); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/pocket-dragonfly/ # Pocket Wildlife dragonfly card Pocket Wildlife dragonfly: six legs, big eyes and two pairs of wings in seven slot-together pieces, on a 150 x 100 mm card. Each pair of wings drops over a tongue on the thorax. The pieces stay in the card on small tabs until an adult releases them and sands the nubs. Shapes are drawn at their finished size and KerfScript moves the cut paths out by half the beam, so leave kerf offsets off in your laser software. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=pocket-dragonfly). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/pocket-dragonfly/pocket-dragonfly.kerf) - [SVG](https://kerfscript.mylasertools.com/models/pocket-dragonfly/pocket-dragonfly.svg) - [DXF](https://kerfscript.mylasertools.com/models/pocket-dragonfly/pocket-dragonfly.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Dragonfly card | 1 | 150 × 100 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `output` | `"card"` | card, loose, coupon | | `stock` | `3` | 2.8 to 3.2 | | `beam` | `0.15` | 0.08 to 0.25 | | `fit` | `0.08` | 0 to 0.18 | | `bridge` | `0.9` | 0.6 to 1.4 | ## Source ```kerf // Pocket Wildlife dragonfly: six legs, big eyes and two pairs of wings in // seven slot-together pieces, on a 150 x 100 mm card. Each pair of wings // drops over a tongue on the thorax. The pieces stay in the card on small // tabs until an adult releases them and sands the nubs. Shapes are drawn at // their finished size and KerfScript moves the cut paths out by half the // beam, so leave kerf offsets off in your laser software. /* [Card] */ // The card, the pieces cut free, or a slot fit test output = "card"; // [card, loose, coupon] /* [Material] */ // Measured thickness of the plywood stock = 3; // [2.8:0.1:3.2] // Width of the laser cut beam = 0.15; // [0.08:0.01:0.25] // Slots are this much wider than the stock fit = 0.08; // [0:0.02:0.18] // Width of the tabs that hold pieces in the card bridge = 0.9; // [0.6:0.1:1.4] /* [Hidden] */ $thickness = stock; $kerf = beam; $fn = 48; $sheet = output == "loose" ? [300, 200] : [160, 110]; card = [150, 100]; s = stock + fit; // Drawn with the thorax 20 mm up, then lowered fy = -6; names = ["Body", "Eyes", "Front legs", "Middle legs", "Back legs", "Forewings", "Hindwings"]; // Piece 0 is the body. Each joint is [piece, place along the body, height, // true when the crosspiece slot opens upward]. The body is notched to match. // The wings, pieces 5 and 6, sit on tongues instead. joints = [[1, 58, 27, false], [2, 36, 20, true], [3, 28, 20, true], [4, 20, 20, true]]; module both() { children(); mirror([1, 0]) children(); } // Rounds the corners of a shape, inside and out module soft(r = 0.8) offset(r = -r) offset(r = 2 * r) offset(r = -r) children(); module slot_up(x, y) translate([x - s / 2, y]) square([s, 200]); module slot_down(x, y) translate([x - s / 2, y - 200]) square([s, 200]); function joint(i) = [for (j = joints) if (j[0] == i) j][0]; module shape(i) { if (i == 0) difference() { body(); for (j = joints) if (j[3]) slot_down(j[1], j[2]); else slot_up(j[1], j[2]); } else if (joint(i) == undef) flat(i); else difference() { cross(i); if (joint(i)[3]) slot_up(0, joint(i)[2]); else slot_down(0, joint(i)[2]); } } // x runs from the tail to the head, y is height above the table. Two // tongues on the thorax carry the wings. module body() translate([0, fy]) union() { path("M50 28 C50 22 46 20 40 20 L18 20 C14 20 11 23 9 26 C-20 28.5 -45 31 -62 32.5 C-66 33 -66 38 -62 38 C-45 38 -20 37 9 36.5 C11 38 13 38 15 38 L45 38 C48 38 50 35 50 32 Z"); translate([58, 31]) circle(r = 9); translate([47, 24]) square([6, 12]); for (x = [16, 31]) translate([x, 37]) square([8, 1 + stock]); } // Crosspieces: x runs across the dragonfly, y is height module cross(i) { if (i == 1) translate([0, fy]) both() { translate([7.5, 34]) circle(r = 7.5); translate([0, 27]) square([3, 12]); } // Legs that reach further out towards the back else let (k = [0, 0, 4, 6, 8][i]) soft(0.6) both() polygon([[0, 25], [5, 25], [10, 23], [10 + k, 28], [13.5 + k, 28], [18 + k, 2], [20.5 + k, 0], [15.5 + k, 0], [10 + k, 23], [9, 18], [6, 11], [0, 11]]); } // Wings lie flat on the thorax: x across the body, y along it, with a slot // over their tongue module flat(i) difference() { scale([0.92, 1]) both() { if (i == 5) path("M0 42 L5 42 C20 43 40 46 58 48 C63 48.5 65 46 63 43.5 C58 39 45 35 30 32 C20 30 10 28 5 28 L0 28 Z"); if (i == 6) path("M0 27 L5 27 C20 27.5 40 27 56 24 C61 23 62 19 58 17.5 C46 14 30 9 16 10 C10 10.5 6 12 5 13 L0 13 Z"); } translate([-s / 2, (i == 5 ? 31 : 16) - fit / 2]) square([s, 8 + fit]); } // Scored rings on the eyes module eyes(i) if (i == 1) translate([0, fy]) both() translate([7.5, 34]) score() for (r = [2.5, 5]) outline() circle(r = r); // Where each piece goes when the model is built: the body stands on edge // along x, crosspieces stand across it and the wings lie on the thorax function placement(i) = i == 0 ? [[0, stock / 2, 0], [90, 0, 0]] : i < 5 ? [[joint(i)[1] - stock / 2, 0, 0], [90, 0, 90]] : [[0, 0, 38 + fy], [0, 0, -90]]; // Where each piece sits on the card [x, y, turn], the tabs that hold it // [x, y, edge direction] in its own coordinates, and the title's box layout = [[79.28, 61.48, 0], [131.28, -13.82, 0], [84.46, 11.48, 0], [115.39, 48.19, 260], [29.44, 21.07, 340], [53.81, 15.58, 350], [56.78, 52.59, 350]]; tabs = [[[17.74, 35.0, 0.0], [16.46, 14.15, 168.8], [64.06, 18.37, -138.0], [-61.99, 32.0, 1.6]], [[7.52, 35.5, -0.1], [-4.5, 21.13, -156.4]], [[-15.53, 28.0, 0.0], [14.8, 19.66, 103.4], [-15.69, 15.92, -103.4]], [[-17.61, 28.0, 0.0], [16.75, 19.88, 103.4], [-17.35, 17.35, -103.4]], [[-19.53, 28.0, 0.0], [18.75, 19.86, 103.4], [-18.88, 19.33, -103.4]], [[54.61, 48.06, -0.8], [-53.24, 40.24, 151.9], [-6.6, 42.16, -4.8]], [[-14.09, 27.17, 0.0], [13.78, 10.1, 173.4], [-42.7, 14.18, 163.7]]]; title = [15.08, 13.88, 34, 13]; // Cut each outline with gaps at the tabs module piece(i, tabs) { difference() { outline() offset(delta = beam / 2) shape(i); for (t = tabs) translate([t[0], t[1]]) rotate(t[2]) square([bridge + beam, 4], center = true); } eyes(i); } if (output == "card") part("Dragonfly card") { rect(card, r = 5); for (i = [0:len(names) - 1]) translate([layout[i][0], layout[i][1]]) rotate(layout[i][2]) piece(i, tabs[i]); engrave() { translate([title[0] + title[2] / 2, title[1] + title[3] * 0.62]) text("DRAGONFLY", size = title[3] * 0.42, halign = "center", valign = "center"); translate([title[0] + title[2] / 2, title[1] + title[3] * 0.16]) text("POCKET WILDLIFE", size = title[3] * 0.15, halign = "center", valign = "center"); } } if (output == "loose") for (i = [0:len(names) - 1]) step(i == 0 ? "Stand the body on edge" : i < 5 ? "Slide the eyes and legs onto the body at their notches" : "Drop each pair of wings over the tongue on the thorax") part(names[i], at = placement(i)[0], rotate = placement(i)[1]) { shape(i); eyes(i); } // Slots from 0.08 mm narrower to 0.12 mm wider than the stock, and three tabs if (output == "coupon") part("Fit coupon") { deltas = [-0.08, -0.04, 0, 0.04, 0.08, 0.12]; difference() { rect([130, 44], r = 3); for (k = [0:5]) translate([12 + k * 21, 26]) square([stock + deltas[k], 20]); } engrave() for (k = [0:5]) translate([12 + k * 21 + stock / 2, 21]) text(str(deltas[k] > 0 ? "+" : "", deltas[k]), size = 2.6, halign = "center", valign = "center"); for (k = [0:2]) translate([22 + k * 43, 9]) difference() { outline() offset(delta = beam / 2) square([24, 8], center = true); for (x = [-12, 12]) translate([x, 0]) square([4, bridge + beam], center = true); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/drawer-organizer/ # Drawer organizer Drawer organizer: a finger-jointed tray from the box library, and a grid of dividers from the dividers library that slot into each other like an egg crate. Measure your drawer, then pick how many compartments you want. This design has 9 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=drawer-organizer). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/drawer-organizer/drawer-organizer.kerf) - [SVG](https://kerfscript.mylasertools.com/models/drawer-organizer/drawer-organizer.svg) - [DXF](https://kerfscript.mylasertools.com/models/drawer-organizer/drawer-organizer.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | bottom | 1 | 240 × 160 mm | | front | 1 | 240 × 50 mm | | back | 1 | 240 × 50 mm | | left | 1 | 160 × 50 mm | | right | 1 | 160 × 50 mm | | long divider | 1 | 233 × 45 mm | | cross divider | 3 | 153 × 45 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `240` | 80 to 560 | | `depth` | `160` | 60 to 380 | | `height` | `50` | 25 to 120 | | `cols` | `4` | 1 to 10 | | `rows` | `2` | 1 to 8 | | `scoops` | `true` | on or off | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Drawer organizer: a finger-jointed tray from the box library, and a grid // of dividers from the dividers library that slot into each other like an // egg crate. Measure your drawer, then pick how many compartments you want. use use /* [Tray] */ // Outside width width = 240; // [80:5:560] // Outside depth, front to back depth = 160; // [60:5:380] // Outside height height = 50; // [25:5:120] /* [Compartments] */ // Compartments across cols = 4; // [1:1:10] // Compartments front to back rows = 2; // [1:1:8] // Dips in the divider tops, to reach in with a finger scoops = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] // Added to every slot, for an easy fit clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; box([width, depth, height]); // Inside the tray, standing on its floor, a little lower than the walls grid_dividers([width - 2 * t, depth - 2 * t, height - t - 2], cols, rows, clearance = clearance, scoops = scoops, at = [t, t, t]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/spice-shelf/ # Tab-and-slot spice shelf Spice shelf: a wall shelf held together by tabs and slots, no glue or screws in the joints. The shelves and their front rails tab into the sides, and the sides tab into the back, which hangs on two screws by its keyholes. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=spice-shelf). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/spice-shelf/spice-shelf.kerf) - [SVG](https://kerfscript.mylasertools.com/models/spice-shelf/spice-shelf.svg) - [DXF](https://kerfscript.mylasertools.com/models/spice-shelf/spice-shelf.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | back | 1 | 260 × 240 mm | | side | 2 | 70 × 240 mm | | shelf | 2 | 248 × 67 mm | | rail | 2 | 248 × 22 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `260` | 120 to 500 | | `height` | `240` | 120 to 500 | | `depth` | `70` | 40 to 150 | | `shelves` | `2` | 1 to 5 | | `rail` | `22` | 0 to 40 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.1` | 0 to 0.3 | ## Source ```kerf // Spice shelf: a wall shelf held together by tabs and slots, no glue or // screws in the joints. The shelves and their front rails tab into the // sides, and the sides tab into the back, which hangs on two screws by // its keyholes. use use /* [Shelf] */ // Outside width width = 260; // [120:10:500] // Outside height height = 240; // [120:10:500] // Depth from the wall depth = 70; // [40:5:150] // Number of shelves shelves = 2; // [1:1:5] // Height of the rail in front of each shelf rail = 22; // [0:1:40] /* [Material] */ // Sheet thickness, measured $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] // Added to every slot, for an easy fit clearance = 0.1; // [0:0.05:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; // The back reaches past the sides, so their slots are closed all round inset = 6; // Between the sides, and out from the back span = width - 2 * inset - 2 * t; reach = depth - t; // Where each shelf's underside is, the lowest one 10 mm up levels = [for (i = [0:shelves - 1]) 10 + i * (height - 40) / max(1, shelves)]; // Where the rails stand, measured on a side from the back rail_at = reach - 1.5 * t; // Steps are numbered in the order they first run, so name them here in build order step("Tab the shelves and rails into the sides") {} step("Tab the sides into the back and hang it on two screws") part("back", at = [0, t, 0], rotate = [90, 0, 0]) keyholes(span / 2, at = [width / 2, height - 18]) difference() { rect([width, height], r = 4); for (x = [inset + t / 2, width - inset - t / 2]) translate([x, 0]) rotate(90) slots(height, clearance = clearance); } step("Tab the shelves and rails into the sides") { part("side", qty = 2, rotate = [90, 0, 90], at = [[inset, t, 0], [width - inset - t, t, 0]]) difference() { tab_rect([reach, height], [false, false, false, true]); for (z = levels) { translate([0, z + t / 2]) slots(reach, clearance = clearance); if (rail > 0) translate([rail_at, z + t]) rotate(90) slots(rail, clearance = clearance); } } part("shelf", qty = shelves, at = [for (z = levels) [inset + t, t, z]]) tab_rect([span, reach], [false, true, false, true]); if (rail > 0) part("rail", qty = shelves, rotate = [90, 0, 0], at = [for (z = levels) [inset + t, t + rail_at + t / 2, z + t]]) tab_rect([span, rail], [false, true, false, true]); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/project-enclosure/ # Project enclosure Project enclosure: a box for a small electronics project, its lid held by four M3 screws and nuts so it comes off again. The front has a switch and a power jack, the back a USB port, and the lid and the sides vents. Change the cutouts to suit your board. This design has 6 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=project-enclosure). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/project-enclosure/project-enclosure.kerf) - [SVG](https://kerfscript.mylasertools.com/models/project-enclosure/project-enclosure.svg) - [DXF](https://kerfscript.mylasertools.com/models/project-enclosure/project-enclosure.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | bottom | 1 | 144 × 94 mm | | lid | 1 | 150 × 100 mm | | front | 1 | 144 × 57 mm | | back | 1 | 144 × 57 mm | | left | 1 | 94 × 57 mm | | right | 1 | 94 × 57 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `150` | 80 to 300 | | `depth` | `100` | 60 to 250 | | `height` | `60` | 40 to 150 | | `vents` | `true` | on or off | | `screw` | `3` | 2.5, 3, 4 | | `screw_length` | `12` | 10 to 25 | | `$thickness` | `3` | 3 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Project enclosure: a box for a small electronics project, its lid held // by four M3 screws and nuts so it comes off again. The front has a // switch and a power jack, the back a USB port, and the lid and the // sides vents. Change the cutouts to suit your board. use /* [Box] */ // Outside width, lid included width = 150; // [80:5:300] // Outside depth depth = 100; // [60:5:250] // Outside height height = 60; // [40:5:150] // Vents in the lid and the sides vents = true; /* [Screws] */ // Screw diameter screw = 3; // [2.5, 3, 4] // Screw length under the head screw_length = 12; // [10:1:25] /* [Material] */ // Sheet thickness $thickness = 3; // [3:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 48; // The walls stand in from the lid's edge by a screw's width w = width - 2 * screw; d = depth - 2 * screw; h = height - $thickness; ports = [ ["front", [w * 0.25, h / 2], 12.5], ["front", [w * 0.75, h / 2], 8], ["back", [w / 2, 14], [13, 6.5]], ["bottom", [12, 12], 3.2], ["bottom", [w - 12, 12], 3.2], ["bottom", [12, d - 12], 3.2], ["bottom", [w - 12, d - 12], 3.2], ]; air = [ ["lid", [width / 2, depth / 2], [width * 0.5, depth * 0.35], "vents"], ["left", [d / 2, h / 2], [d * 0.5, h * 0.4], "vents"], ["right", [d / 2, h / 2], [d * 0.5, h * 0.4], "vents"], ]; enclosure([width, depth, height], screw = screw, screw_length = screw_length, cutouts = vents ? concat(ports, air) : ports); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/solstice/ # Solstice lantern Solstice: a lantern with two sun walls and two moon walls, held between a base and an open crown by tabs. For a battery LED, not a candle. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=solstice). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/solstice/solstice.kerf) - [SVG](https://kerfscript.mylasertools.com/models/solstice/solstice.svg) - [DXF](https://kerfscript.mylasertools.com/models/solstice/solstice.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Sun wall | 2 | 94 × 176 mm | | Moon wall | 2 | 94 × 176 mm | | Base | 1 | 116 × 116 mm | | Crown | 1 | 116 × 116 mm | | Fit test | 1 | 68 × 26 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `height` | `170` | 160 to 210 | | `fit_coupon` | `true` | on or off | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.5 to 4 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Solstice: a lantern with two sun walls and two moon walls, held between a // base and an open crown by tabs. For a battery LED, not a candle. /* [Lantern] */ // Wall height between base and crown height = 170; // [160:5:210] // Add a small test piece with three slot widths fit_coupon = true; // Scored rings and lines score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.5:0.1:4] // Beam width $kerf = 0.15; // [0:0.01:0.3] // Added to slot width and length, for a slip fit clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "birch plywood"; $sheet = [600, 400]; $gap = 5; $fn = 96; w = 94; span = 100; foot = 116; tab_w = 10; tab_x = 28; t = $thickness; cy = height * 0.53; module kite(rx, ry) polygon([[0, ry], [rx, 0], [0, -ry], [-rx, 0]]); module rosette(moon = false) { if (moon) difference() { circle(r = 10); translate([4, 2]) circle(r = 9); } else star(8, 10, 5.6); for (a = [0:45:315]) rotate(a + (moon ? 22.5 : 0)) { polygon([[14, 0], [19, -5], [24, 0], [19, 5]]); polygon([[28, 0], [32, -5.8], [37, 0], [32, 5.8]]); } } module wall_blank() union() { translate([-w / 2, 0]) rect([w, height]); for (x = [-tab_x, tab_x]) { translate([x - tab_w / 2, -t]) rect([tab_w, t + 0.5]); translate([x - tab_w / 2, height - 0.5]) rect([tab_w, t + 0.5]); } } module wall(moon = false) { difference() { wall_blank(); translate([0, cy]) rosette(moon); // A fan of five kites at the top and bottom for (x = [-28, -14, 0, 14, 28]) { translate([x, height - 20 - abs(x) * 0.12]) kite(3.1, 10 - abs(x) * 0.1); translate([x, 21 + abs(x) * 0.12]) kite(3.1, 10 - abs(x) * 0.1); } // Pinhole stars for (x = [-39, 39], y = [25, 45, height - 45, height - 25]) translate([x, y]) circle(d = 2.4); } if (score_detail) score() { translate([-42, 6]) outline() rect([84, height - 12]); translate([0, cy]) { outline() circle(r = 11.8); outline() circle(r = 26); outline() circle(r = 39.5); for (a = [0:45:315]) rotate(a + (moon ? 22.5 : 0)) line([10.8, 0], [39.5, 0]); } for (s = [-1, 1]) polyline([[-34, cy + s * 44], [-20, cy + s * 52], [0, cy + s * 45], [20, cy + s * 52], [34, cy + s * 44]]); } } module sockets(extra = clearance) { for (a = [0:90:270]) rotate(a) for (x = [-tab_x, tab_x]) translate([x, span / 2]) rect([tab_w + extra, t + extra], center = true); } module plate(top = false) { difference() { rect([foot, foot], center = true, r = 6); sockets(); if (top) rect([80, 80], center = true, r = 10); } if (score_detail) score() { outline() rect([108, 108], center = true, r = 3); if (top) outline() rect([86, 86], center = true, r = 12); else { outline() circle(d = 48); outline() circle(d = 54); for (a = [0:30:330]) rotate(a) line([29, 0], [34, 0]); } } if (!top) engrave() { translate([0, -37]) text("SOLSTICE", size = 5, halign = "center", valign = "center"); translate([0, 37]) text("LED ONLY", size = 3.4, halign = "center", valign = "center"); } } // Assembled: the walls stand in the base round the middle, sun walls facing // each other, and the crown sits on their top tabs function wall_at(a) = [(span / 2 - t / 2) * sin(a), -(span / 2 - t / 2) * cos(a), t]; step("Plug the four walls into the base, sun walls facing each other") { part("Sun wall", qty = 2, at = [wall_at(0), wall_at(180)], rotate = [[90, 0, 0], [90, 0, 180]]) wall(false); part("Moon wall", qty = 2, at = [wall_at(90), wall_at(270)], rotate = [[90, 0, 90], [90, 0, 270]]) wall(true); part("Base", at = [0, 0, 0]) plate(false); } step("Lower the crown onto the top tabs and leave it loose") part("Crown", at = [0, 0, t + height]) plate(true); // The fit test lies on the table beside the lantern if (fit_coupon) part("Fit test", at = [foot / 2 + 20, -foot / 2, 0]) { difference() { rect([68, 26], r = 3); for (i = [0:2]) translate([13 + i * 21, 13]) rect([tab_w + clearance, t + clearance + (i - 1) * 0.1], center = true); } engrave() for (i = [0:2]) translate([13 + i * 21, 21]) text(str(round(100 * (t + clearance + (i - 1) * 0.1)) / 100), size = 2.8, halign = "center", valign = "center"); score() translate([34, 5]) text("SLOT WIDTH / mm", size = 2.5, halign = "center", valign = "center"); } // Assembly: plug the four walls into the base, sun walls facing each other, // then lower the crown onto the top tabs. Leave the crown loose so the LED // can be reached; it sits on the scored circle in the base. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/winterlight/ # Winterlight ornaments Winterlight: three Christmas ornaments, a snow crystal, a fir grove under the moon and an eight-point star. Each is one piece with its own cord loop. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=winterlight). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/winterlight/winterlight.kerf) - [SVG](https://kerfscript.mylasertools.com/models/winterlight/winterlight.svg) - [DXF](https://kerfscript.mylasertools.com/models/winterlight/winterlight.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Frost | 1 | 84 × 95.5 mm | | Grove | 1 | 84 × 95.5 mm | | Nova | 1 | 73.8 × 92 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `selection` | `"all"` | All three, Frost, Grove, Nova | | `body_size` | `84` | 76 to 100 | | `score_detail` | `true` | on or off | | `$thickness` | `3` | 2.5 to 4 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Winterlight: three Christmas ornaments, a snow crystal, a fir grove under // the moon and an eight-point star. Each is one piece with its own cord loop. /* [Ornaments] */ // Make all three or just one selection = "all"; // [all:All three, frost:Frost, grove:Grove, nova:Nova] // Diameter of the round ornaments; everything scales with it body_size = 84; // [76:2:100] // Scored frost, branch and facet lines score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.5:0.1:4] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "birch plywood"; $sheet = [340, 150]; $gap = 6; $fn = 144; s = body_size / 84; cord_hole = 4.5; module round_blank() union() { circle(r = 42); translate([-6.5, 38]) rect([13, 9]); translate([0, 47]) circle(r = 6.5); } module round_frame() difference() { round_blank(); circle(r = 36); } module hanger_hole(y = 47) translate([0, y * s]) circle(d = cord_hole); module rim_detail() { outline() circle(r = 39); for (a = [0:30:330]) rotate(a) line([37.5, 0], [40.5, 0]); translate([0, 47]) outline() circle(r = 4.5); } // Frost: six spines with two pairs of branches each, inside a ring. module frost() { difference() { scale(s) union() { round_frame(); circle(r = 7.5); for (a = [30:60:330]) rotate(a) { stroke(3.8) line([0, 0], [38, 0]); for (side = [-1, 1]) { stroke(3.4) line([16, 0], [25, side * 8.7]); stroke(3.4) line([26, 0], [37, side * 7.5]); } } } hanger_hole(); scale(s) regular_polygon(6, 3.4); } if (score_detail) scale(s) score() { rim_detail(); outline() regular_polygon(6, 5.5); for (a = [30:60:330]) rotate(a) { line([7.8, 0], [37.3, 0]); for (side = [-1, 1]) { line([16, 0], [24.3, side * 8]); line([26, 0], [36.4, side * 6.9]); } } } } // Grove: three firs on a snowy hill, with a crescent moon joined to the rim. module fir(w, h) offset(r = 0.6) offset(delta = -0.6) polygon([ [0, h], [w * 0.52, h * 0.71], [w * 0.26, h * 0.71], [w * 0.82, h * 0.43], [w * 0.46, h * 0.43], [w, h * 0.13], [2.4, h * 0.13], [2.4, 0], [-2.4, 0], [-2.4, h * 0.13], [-w, h * 0.13], [-w * 0.46, h * 0.43], [-w * 0.82, h * 0.43], [-w * 0.26, h * 0.71], [-w * 0.52, h * 0.71] ]); module fir_lines(w, h) { line([0, 2], [0, h - 5]); for (p = [[0.71, 0.39], [0.43, 0.67], [0.13, 0.84]]) polyline([[-w * p[1], h * p[0] + 1.4], [0, h * p[0] + 3.4], [w * p[1], h * p[0] + 1.4]]); } module grove() { difference() { scale(s) union() { round_frame(); intersection() { circle(r = 42); union() { path("M -44 -25 Q -22 -14 0 -25 Q 22 -20 44 -23 L 44 -44 L -44 -44 Z"); translate([-20, -29]) fir(10, 38); translate([1, -32]) fir(13.4, 59); translate([22, -33]) fir(10, 44); translate([-17, 23]) difference() { circle(r = 12); translate([4.5, 2.2]) circle(r = 10.2); } } } } hanger_hole(); } if (score_detail) scale(s) score() { rim_detail(); translate([-20, -29]) fir_lines(10, 38); translate([1, -32]) fir_lines(13.4, 59); translate([22, -33]) fir_lines(10, 44); path("M -24 -25 Q -12 -23 0 -29 Q 12 -33 24 -25"); path("M -17 -31 Q 0 -39 17 -31"); // Small stars scored on the rim for (a = [35, 55, 75, 105, 155, 175]) rotate(a) { line([37.8, -0.9], [40.2, 0.9]); line([37.8, 0.9], [40.2, -0.9]); } translate([-17, 23]) arc(r = 9, start = 130, end = 270); } } // Nova: an eight-point star, four long rays and four short ones. module nova_blank() union() { offset(r = 0.65) offset(delta = -0.65) polygon([ [0, 44], [8, 18], [24, 24], [18, 8], [38, 0], [18, -8], [24, -24], [8, -18], [0, -43], [-8, -18], [-24, -24], [-18, -8], [-38, 0], [-18, 8], [-24, 24], [-8, 18] ]); hull() { translate([0, 34]) circle(r = 5.3); translate([0, 44]) circle(r = 6.5); } } module nova_windows() { for (a = [0, 180]) rotate(a) polygon([[0, 12], [3.5, 20], [0, 32], [-3.5, 20]]); for (a = [90, 270]) rotate(a) polygon([[0, 12], [3.2, 19], [0, 28], [-3.2, 19]]); for (a = [0:90:270]) rotate(a) polygon([[10.8, 10.8], [16.5, 13.3], [19, 19], [13.3, 16.5]]); star(8, 5.6, 3.1); } module nova() { difference() { scale(s) difference() { nova_blank(); nova_windows(); } hanger_hole(44); } if (score_detail) scale(s) score() { outline() star(8, 9.1, 6.8); for (a = [0:90:270]) rotate(a) { line([0, 9.3], [0, 40]); line([7, 7], [22, 22]); polyline([[6.6, 15], [9, 9], [15, 6.6]]); } translate([0, 44]) outline() circle(r = 4.5); } } if (selection == "all" || selection == "frost") part("Frost") frost(); if (selection == "all" || selection == "grove") part("Grove") grove(); if (selection == "all" || selection == "nova") part("Nova") nova(); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/snowflake/ # Snowflake ornament A six-armed snowflake ornament. Each arm is one branch, repeated. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=snowflake). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/snowflake/snowflake.kerf) - [SVG](https://kerfscript.mylasertools.com/models/snowflake/snowflake.svg) - [DXF](https://kerfscript.mylasertools.com/models/snowflake/snowflake.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 76.1 × 90.7 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `90` | 40 to 200 | | `branches` | `3` | 1 to 5 | | `arm` | `3.2` | 2 to 6 | | `spread` | `52` | 30 to 70 | | `seed` | `7` | 1 to 99 | | `loop` | `true` | on or off | | `hole` | `3.5` | 2 to 6 | ## Source ```kerf // A six-armed snowflake ornament. Each arm is one branch, repeated. /* [Snowflake] */ // Width across the arms size = 90; // [40:5:200] // Side branches on each arm branches = 3; // [1:1:5] // Arm thickness arm = 3.2; // [2:0.2:6] // Angle of the side branches spread = 52; // [30:1:70] // Different seeds give different snowflakes seed = 7; // [1:1:99] /* [Hanging] */ loop = true; // Hole diameter hole = 3.5; // [2:0.5:6] /* [Hidden] */ $fn = 48; r = size / 2; lengths = rands(0.35, 0.8, branches, seed); spots = rands(0.25, 0.85, branches, seed + 1); module bar(a, b, w) hull() { translate(a) circle(d = w); translate(b) circle(d = w); } module arm() { bar([0, 0], [r - arm, 0], arm); translate([r - arm * 1.6, 0]) rotate(45) rect(arm * 1.8, center = true); for (i = [0:branches - 1]) { x = r * spots[i]; len = (r - x) * lengths[i]; for (s = [-1, 1]) bar([x, 0], [x + len * cos(spread), s * len * sin(spread)], arm * 0.8); } } difference() { union() { for (k = [0:5]) rotate(90 + k * 60) arm(); regular_polygon(6, r * 0.22); if (loop) translate([0, r - arm]) circle(d = hole + 2 * arm); } regular_polygon(6, r * 0.1); if (loop) translate([0, r - arm]) circle(d = hole); } engrave() rotate(30) for (k = [0:5]) rotate(k * 60) translate([r * 0.16, 0]) circle(d = r * 0.035); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/phone-stand/ # Slot-together phone stand A phone stand made from three slotted parts. Adjust the width, angle and material thickness to suit your phone. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=phone-stand). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/phone-stand/phone-stand.kerf) - [SVG](https://kerfscript.mylasertools.com/models/phone-stand/phone-stand.svg) - [DXF](https://kerfscript.mylasertools.com/models/phone-stand/phone-stand.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | side | 2 | 91.6 × 103.4 mm | | brace | 1 | 96 × 30 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `span` | `64` | 40 to 120 | | `lean` | `66` | 50 to 80 | | `lip` | `20` | 10 to 40 | | `label` | `"KerfScript"` | | | `thickness` | `4` | 2 to 8 | | `clearance` | `0.1` | 0 to 0.4 | ## Source ```kerf // A phone stand made from three slotted parts. // Adjust the width, angle and material thickness to suit your phone. /* [Stand] */ // Gap between the two sides span = 64; // [40:1:120] // Lean of the phone, in degrees from flat lean = 66; // [50:1:80] // Height of the front lip lip = 20; // [10:1:40] // Engraved text on the brace label = "KerfScript"; // 16 /* [Material] */ // Sheet thickness thickness = 4; // [2:0.5:8] // Slot clearance, added to the thickness clearance = 0.1; // [0:0.05:0.4] /* [Hidden] */ $fn = 40; slot = thickness + clearance; rest = 100; top = [14 + rest * cos(lean), 12 + rest * sin(lean)]; foot = top[0] + top[1] / tan(70); brace_x = foot * 0.62; brace_h = 30; module rounded(r) offset(r = r) offset(delta = -r) children(); module side() difference() { rounded(2) polygon([ [0, 0], [foot, 0], [top[0] + 6, top[1]], [top[0] - 2, top[1]], [14, 12], [14, lip], [6, lip], [0, lip - 6] ]); translate([brace_x - slot / 2, -1]) rect([slot, brace_h / 2 + 1]); } module brace() difference() { rect([span + 2 * thickness + 24, brace_h], r = 3); for (x = [12, 12 + span + thickness]) translate([x - clearance / 2, brace_h / 2]) rect([slot, brace_h / 2 + 1]); } // Assembled: the sides stand span apart, the brace across them in its slots material(thickness = thickness, kerf = 0.1, name = "plywood") { step("Stand the two sides up") part("side", qty = 2, at = [[0, 12 + thickness, 0], [0, 12 + 2 * thickness + span, 0]], rotate = [90, 0, 0]) side(); step("Slide the brace down into the slots in both sides") part("brace", at = [brace_x - thickness / 2, 0, 0], rotate = [90, 0, 90]) { brace(); engrave() translate([(span + 2 * thickness + 24) / 2, brace_h * 0.28]) text(label, 6, halign = "center", valign = "center"); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/guilloche-coaster/ # Guilloche coaster A round coaster with an interlaced guilloche pattern. Adjust the diameter, wave count and number of scored rings. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=guilloche-coaster). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/guilloche-coaster/guilloche-coaster.kerf) - [SVG](https://kerfscript.mylasertools.com/models/guilloche-coaster/guilloche-coaster.svg) - [DXF](https://kerfscript.mylasertools.com/models/guilloche-coaster/guilloche-coaster.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 95 × 95 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `95` | 60 to 140 | | `waves` | `18` | 6 to 40 | | `copies` | `7` | 2 to 14 | | `petals` | `9` | 3 to 24 | ## Source ```kerf // A round coaster with an interlaced guilloche pattern. // Adjust the diameter, wave count and number of scored rings. /* [Coaster] */ // Diameter size = 95; // [60:1:140] // Waves around the outer band waves = 18; // [6:1:40] // Copies of each band copies = 7; // [2:1:14] // Petals in the centre rosette petals = 9; // [3:1:24] /* [Hidden] */ $fn = 120; R = size / 2; steps = 720; function band(r0, amp, k, phase) = [for (i = [0:steps - 1]) let (a = i * 360 / steps, r = r0 + amp * sin(k * a + phase)) [r * cos(a), r * sin(a)]]; function rosette(r0, k, phase) = [for (i = [0:steps - 1]) let (a = i * 360 / steps, r = r0 * (0.55 + 0.45 * pow(cos(k * a / 2 + phase), 2))) [r * cos(a), r * sin(a)]]; circle(r = R); score() { for (j = [0:copies - 1]) polyline(band(R * 0.76, R * 0.09, waves, j * 360 / copies), closed = true); for (j = [0:copies - 1]) polyline(rosette(R * 0.5, petals, j * 180 / (copies * petals)), closed = true); circle(r = R * 0.9); circle(r = R * 0.6); } engrave() circle(r = R * 0.06); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/sunflower-panel/ # Sunflower lamp panel A lamp panel with a spiral of holes inspired by sunflower seeds. Adjust the panel size, hole count and hole sizes. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=sunflower-panel). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/sunflower-panel/sunflower-panel.kerf) - [SVG](https://kerfscript.mylasertools.com/models/sunflower-panel/sunflower-panel.svg) - [DXF](https://kerfscript.mylasertools.com/models/sunflower-panel/sunflower-panel.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 150 × 150 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `150` | 80 to 300 | | `height` | `150` | 80 to 300 | | `count` | `260` | 50 to 800 | | `min_hole` | `1.6` | 1 to 5 | | `max_hole` | `6.5` | 3 to 12 | | `margin` | `9` | 4 to 30 | ## Source ```kerf // A lamp panel with a spiral of holes inspired by sunflower seeds. // Adjust the panel size, hole count and hole sizes. /* [Panel] */ // Panel width width = 150; // [80:5:300] // Panel height height = 150; // [80:5:300] // Number of holes count = 260; // [50:10:800] // Smallest hole min_hole = 1.6; // [1:0.1:5] // Largest hole max_hole = 6.5; // [3:0.5:12] // Margin without holes margin = 9; // [4:1:30] /* [Hidden] */ $fn = 24; golden = 180 * (3 - sqrt(5)); reach = min(width, height) / 2 - margin - max_hole / 2; spacing = reach / sqrt(count); difference() { rect([width, height], r = 4, center = true); for (i = [1:count]) { rr = spacing * sqrt(i); d = lerp(min_hole, max_hole, i / count); rotate(i * golden) translate([rr, 0]) circle(d = d); } } score() rect([width - 2 * margin + 4, height - 2 * margin + 4], r = 3, center = true); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/honeycomb-trivet/ # Honeycomb trivet A hexagonal trivet with a honeycomb pattern of cutouts. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=honeycomb-trivet). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/honeycomb-trivet/honeycomb-trivet.kerf) - [SVG](https://kerfscript.mylasertools.com/models/honeycomb-trivet/honeycomb-trivet.svg) - [DXF](https://kerfscript.mylasertools.com/models/honeycomb-trivet/honeycomb-trivet.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 155.9 × 177.5 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `180` | 100 to 300 | | `cell` | `16` | 8 to 40 | | `wall` | `3` | 2 to 8 | | `border` | `10` | 5 to 30 | | `round` | `8` | 0 to 20 | ## Source ```kerf // A hexagonal trivet with a honeycomb pattern of cutouts. /* [Trivet] */ // Width across the corners size = 180; // [100:5:300] // Hole size, corner to corner cell = 16; // [8:1:40] // Wall between holes wall = 3; // [2:0.5:8] // Solid border border = 10; // [5:1:30] // Corner rounding round = 8; // [0:1:20] /* [Hidden] */ R = size / 2; r = cell / 2; dx = 1.5 * r + wall * sqrt(3) / 2; dy = sqrt(3) * r + wall; n = ceil(R / dx) + 1; apothem = R * sqrt(3) / 2 - border; // Distance from the centre to p, measured across the flats of the outer hexagon. function flat_distance(p) = max([for (k = [0:2]) abs(p[0] * cos(k * 60) + p[1] * sin(k * 60))]); difference() { offset(r = round) offset(delta = -round) rotate(30) regular_polygon(6, R); for (i = [-n:n], j = [-n:n]) { p = [i * dx, j * dy + (abs(i) % 2) * dy / 2]; if (flat_distance(p) + r <= apothem) translate(p) regular_polygon(6, r); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ruler/ # Engraved ruler A ruler with scored millimetre ticks and engraved numbers. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=ruler). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/ruler/ruler.kerf) - [SVG](https://kerfscript.mylasertools.com/models/ruler/ruler.svg) - [DXF](https://kerfscript.mylasertools.com/models/ruler/ruler.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 176 × 28 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `length_cm` | `15` | 5 to 30 | | `width` | `28` | 18 to 50 | | `label` | `"KERF"` | | | `hole` | `true` | on or off | ## Source ```kerf // A ruler with scored millimetre ticks and engraved numbers. /* [Ruler] */ // Measuring length in cm length_cm = 15; // [5:1:30] // Ruler width width = 28; // [18:1:50] // Engraved label at the right end label = "KERF"; // 12 // Hole for hanging hole = true; /* [Hidden] */ $fn = 40; lead = 8; len = length_cm * 10; total = len + 2 * lead + (hole ? 10 : 0); difference() { rect([total, width], r = 3); if (hole) translate([total - 7, width / 2]) circle(d = 5); } score() for (mm = [0:len]) let ( tick = mm % 10 == 0 ? width * 0.42 : mm % 5 == 0 ? width * 0.3 : width * 0.2 ) line([lead + mm, width], [lead + mm, width - tick]); engrave() { for (cm = [0:length_cm]) translate([lead + cm * 10, width * 0.5 - 1]) text(str(cm), 4.2, halign = "center", valign = "top"); translate([lead + len - 2, 3]) text(label, 3.4, halign = "right", spacing = 1.3); translate([lead, 3]) text("cm", 3.4); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/hinge-card/ # Living hinge card Card with a living hinge down the middle, from the hinge library. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=hinge-card). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/hinge-card/hinge-card.kerf) - [SVG](https://kerfscript.mylasertools.com/models/hinge-card/hinge-card.svg) - [DXF](https://kerfscript.mylasertools.com/models/hinge-card/hinge-card.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 160 × 100 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `160` | 80 to 300 | | `height` | `100` | 50 to 200 | | `hinge` | `24` | 10 to 60 | ## Source ```kerf // Card with a living hinge down the middle, from the hinge library. use width = 160; // [80:5:300] height = 100; // [50:5:200] hinge = 24; // [10:1:60] flex_rect([width, height], bends = [width / 2], width = hinge, r = 4); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/flex-box/ # Flex box with round corners Flex box: a box with round corners, its four sides one strip of plywood bent by living hinges. The bottom's tabs lock into slots along the strip, and the lid sits on top, kept in place by an insert glued under it. Bend the strip slowly; warm, slightly damp plywood bends best. This design has 4 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=flex-box). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/flex-box/flex-box.kerf) - [SVG](https://kerfscript.mylasertools.com/models/flex-box/flex-box.svg) - [DXF](https://kerfscript.mylasertools.com/models/flex-box/flex-box.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | side | 1 | 359.7 × 57 mm | | bottom | 1 | 120 × 80 mm | | lid | 1 | 120 × 80 mm | | lid insert | 1 | 113.8 × 73.8 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `120` | 70 to 250 | | `depth` | `80` | 70 to 250 | | `height` | `60` | 30 to 150 | | `radius` | `18` | 10 to 35 | | `lid` | `true` | on or off | | `$thickness` | `3` | 1.5 to 4 | | `$kerf` | `0.15` | 0 to 0.3 | | `spacing` | `1.5` | 1 to 3 | ## Source ```kerf // Flex box: a box with round corners, its four sides one strip of // plywood bent by living hinges. The bottom's tabs lock into slots along // the strip, and the lid sits on top, kept in place by an insert glued // under it. Bend the strip slowly; warm, slightly damp plywood bends best. use /* [Box] */ // Outside width width = 120; // [70:5:250] // Outside depth depth = 80; // [70:5:250] // Outside height, lid included height = 60; // [30:5:150] // Outside corner radius radius = 18; // [10:1:35] // With a lid, or an open tray lid = true; /* [Material] */ // Sheet thickness; living hinges suit 3 mm plywood or thinner $thickness = 3; // [1.5:0.5:4] // Beam width $kerf = 0.15; // [0:0.01:0.3] // Distance between the hinge cuts: closer bends easier spacing = 1.5; // [1:0.25:3] /* [Hidden] */ $material = "plywood"; $fn = 64; flex_box([width, depth, height], r = radius, lid = lid, spacing = spacing); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/name-sign/ # Name sign Name sign: plate sized to the text, with two hanging holes. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=name-sign). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/name-sign/name-sign.kerf) - [SVG](https://kerfscript.mylasertools.com/models/name-sign/name-sign.svg) - [DXF](https://kerfscript.mylasertools.com/models/name-sign/name-sign.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 99.7 × 46.8 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `name` | `"Atkinson"` | | | `size` | `22` | 10 to 60 | | `margin` | `8` | 4 to 30 | ## Source ```kerf // Name sign: plate sized to the text, with two hanging holes. name = "Atkinson"; // Name on the sign size = 22; // [10:1:60] margin = 8; // [4:1:30] w = text_width(name, size) + 2 * margin; h = size * 1.4 + 2 * margin; difference() { rect([w, h], r = 6, center = true); for (x = [-1, 1]) translate([x * (w / 2 - margin / 2 - 2), h / 2 - margin / 2 - 2]) circle(d = 4); } engrave() text(name, size, halign = "center", valign = "center"); score() offset(delta = -2) rect([w, h], r = 6, center = true); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/wall-sign/ # Hanging wall sign Wall sign: a name or a word on a panel that hangs on screws, a nail or a cord, from the hanging library. The keyholes take a screw head; measure yours and set the sizes to match. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=wall-sign). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/wall-sign/wall-sign.kerf) - [SVG](https://kerfscript.mylasertools.com/models/wall-sign/wall-sign.svg) - [DXF](https://kerfscript.mylasertools.com/models/wall-sign/wall-sign.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | sign | 1 | 200 × 70 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `label` | `"Workshop"` | | | `width` | `200` | 100 to 400 | | `height` | `70` | 40 to 200 | | `hang` | `"keyholes"` | Two keyholes, Two holes for a cord, A loop on top | | `head` | `9` | 6 to 14 | | `shank` | `4.5` | 2.5 to 6 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Wall sign: a name or a word on a panel that hangs on screws, a nail // or a cord, from the hanging library. The keyholes take a screw head; // measure yours and set the sizes to match. use /* [Sign] */ // Text, engraved label = "Workshop"; // 16 // Panel width width = 200; // [100:10:400] // Panel height height = 70; // [40:5:200] // How it hangs hang = "keyholes"; // [keyholes:Two keyholes, holes:Two holes for a cord, loop:A loop on top] /* [Screws] */ // Diameter of the screw head head = 9; // [6:0.5:14] // Diameter of the screw's shank shank = 4.5; // [2.5:0.5:6] /* [Material] */ $thickness = 3; // [2:0.5:6] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $fn = 64; module panel() rect([width, height], r = 6); module hung() { if (hang == "keyholes") keyholes(width - 30, at = [width / 2, height - 10], head = head, shank = shank) panel(); if (hang == "holes") hanging_hole(at = [width / 2, height - 8], d = 5, spacing = width - 20) panel(); if (hang == "loop") hanging_loop(at = [width / 2, height + 3], d = 6) panel(); } part("sign") { hung(); engrave() translate([width / 2, height * 0.42]) text(label, size = height * 0.4, fit = width * 0.7, halign = "center", valign = "center"); score() outline() offset(delta = -4) panel(); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/calibration-square/ # Calibration square A 100 mm square with a 20 mm hole for checking dimensions and kerf. Measure the outside and the hole after cutting to check compensation. See the kerf guide for the calibration steps. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=calibration-square). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/calibration-square/calibration-square.kerf) - [SVG](https://kerfscript.mylasertools.com/models/calibration-square/calibration-square.svg) - [DXF](https://kerfscript.mylasertools.com/models/calibration-square/calibration-square.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 100 × 100 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `100` | 20 to 300 | | `hole` | `20` | 2 to 100 | | `$kerf` | `0.15` | 0 to 0.5 | ## Source ```kerf // A 100 mm square with a 20 mm hole for checking dimensions and kerf. // Measure the outside and the hole after cutting to check compensation. // See the kerf guide for the calibration steps. /* [Size] */ // Outside of the square size = 100; // [20:5:300] // Diameter of the hole hole = 20; // [2:1:100] /* [Material] */ $kerf = 0.15; // [0:0.01:0.5] difference() { rect(size, center = true); circle(d = hole); } score() rect(size / 2, center = true); engrave() { translate([0, -size / 2 + 6]) text(str(size, " mm"), size = 4, halign = "center"); translate([0, hole / 2 + 4]) text(str("hole ", hole, " mm, kerf ", $kerf), size = 3, halign = "center"); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/kerf-test/ # Kerf and fit test A row of slots for testing the fit of your material. Try a scrap from the same sheet in each slot. Positive labels mean wider slots; negative labels mean narrower ones. Measure both kerf and sheet thickness before adjusting the design. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=kerf-test). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/kerf-test/kerf-test.kerf) - [SVG](https://kerfscript.mylasertools.com/models/kerf-test/kerf-test.svg) - [DXF](https://kerfscript.mylasertools.com/models/kerf-test/kerf-test.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | part | 1 | 81 × 27 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `$thickness` | `3` | 1 to 10 | | `$kerf` | `0.15` | 0 to 0.5 | | `steps` | `7` | 3 to 11 | | `step` | `0.1` | 0.05 to 0.3 | | `depth` | `15` | 8 to 30 | ## Source ```kerf // A row of slots for testing the fit of your material. // Try a scrap from the same sheet in each slot. Positive labels mean // wider slots; negative labels mean narrower ones. Measure both kerf // and sheet thickness before adjusting the design. use /* [Material] */ $thickness = 3; // [1:0.5:10] $kerf = 0.15; // [0:0.01:0.5] /* [Slots] */ // How many slots steps = 7; // [3:2:11] // Change in width from one slot to the next step = 0.1; // [0.05:0.05:0.3] // How deep the slots go depth = 15; // [8:1:30] slot_gauge(steps, step, depth); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/calibration-set/ # Calibration set Calibration set: cut it once for every new material, before anything that has to fit. The kerf gauge measures the beam, the slot gauge how wide a slot your sheet needs, and the joint pairs which kerf setting makes finger joints that hold. This design has 8 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=calibration-set). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/calibration-set/calibration-set.kerf) - [SVG](https://kerfscript.mylasertools.com/models/calibration-set/calibration-set.svg) - [DXF](https://kerfscript.mylasertools.com/models/calibration-set/calibration-set.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | kerf gauge | 1 | 72 × 43 mm | | slot gauge | 1 | 81 × 27 mm | | joint 0.1 out | 1 | 40 × 20 mm | | joint 0.1 in | 1 | 40 × 20 mm | | joint 0.15 out | 1 | 40 × 20 mm | | joint 0.15 in | 1 | 40 × 20 mm | | joint 0.2 out | 1 | 40 × 20 mm | | joint 0.2 in | 1 | 40 × 20 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `pieces` | `"all"` | All three, Kerf gauge, Slot gauge, Joint pairs | | `strips` | `10` | 5 to 20 | | `kerfs` | `[0.1,0.15,0.2]` | | | `$thickness` | `3` | 1 to 10 | ## Source ```kerf // Calibration set: cut it once for every new material, before anything // that has to fit. The kerf gauge measures the beam, the slot gauge how // wide a slot your sheet needs, and the joint pairs which kerf setting // makes finger joints that hold. use /* [Tests] */ // Which pieces to cut pieces = "all"; // [all:All three, kerf:Kerf gauge, slots:Slot gauge, joints:Joint pairs] // Strips in the kerf gauge strips = 10; // [5:1:20] // Kerf settings to try on the joint pairs kerfs = [0.1, 0.15, 0.2]; /* [Material] */ // Sheet thickness, as you measured it $thickness = 3; // [1:0.5:10] /* [Hidden] */ $material = "plywood"; if (pieces == "all" || pieces == "kerf") part("kerf gauge") kerf_gauge(strips); if (pieces == "all" || pieces == "slots") part("slot gauge") slot_gauge(); if (pieces == "all" || pieces == "joints") joint_test(kerfs); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tea-pavilion/ # Tea pavilion dispenser Tea pavilion: a packet dispenser with a plain gable roof and a front lip. Packet width, depth and stack height generate the enclosure dimensions. Assembly: glue the wall and floor joints, then glue one roof slope in place. Leave the other slope loose for refilling from above. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tea-pavilion). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tea-pavilion/tea-pavilion.kerf) - [SVG](https://kerfscript.mylasertools.com/models/tea-pavilion/tea-pavilion.svg) - [DXF](https://kerfscript.mylasertools.com/models/tea-pavilion/tea-pavilion.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 79 × 74 mm | | Dispensing end | 1 | 79 × 197 mm | | Rear end | 1 | 79 × 197 mm | | Side | 2 | 74 × 163 mm | | Fixed roof | 1 | 62.7 × 90 mm | | Lift-off roof | 1 | 62.7 × 90 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `packet_width` | `65` | 50 to 95 | | `packet_depth` | `60` | 45 to 90 | | `stack_height` | `160` | 90 to 250 | | `ease` | `4` | 2 to 8 | | `opening_height` | `24` | 18 to 36 | | `roof_rise` | `34` | 22 to 60 | | `overhang` | `8` | 4 to 15 | | `window_detail` | `true` | on or off | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Tea pavilion: a packet dispenser with a plain gable roof and a front lip. // Packet width, depth and stack height generate the enclosure dimensions. // Assembly: glue the wall and floor joints, then glue one roof slope in // place. Leave the other slope loose for refilling from above. /* [Packets] */ packet_width = 65; // [50:1:95] packet_depth = 60; // [45:1:90] stack_height = 160; // [90:5:250] ease = 4; // [2:0.5:8] opening_height = 24; // [18:1:36] /* [Roof] */ roof_rise = 34; // [22:2:60] overhang = 8; // [4:1:15] window_detail = true; /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; w = packet_width + 2 * ease + 2 * t; d = packet_depth + 2 * ease + 2 * t; h = stack_height + t; angle = atan(2 * roof_rise / w); slant = w / 2 / cos(angle); extra = overhang / cos(angle); assert(opening_height < h / 2, "Dispensing opening must stay below mid-height"); module end(front = false) { difference() { union() { finger_rect([w, h], ["in", "out", "flat", "out"]); polygon([[0, h - 0.1], [w, h - 0.1], [w / 2, h + roof_rise]]); } if (front) translate([w * 0.16, t + 6]) rect([w * 0.68, opening_height], r = 5); } if (window_detail) score() translate([w / 2, h * 0.65]) { outline() rect([w * 0.34, h * 0.22], center = true, r = 3); line([-w * 0.17, 0], [w * 0.17, 0]); line([0, -h * 0.11], [0, h * 0.11]); } } step("Glue the ends and sides into the floor") { part("Floor", at = [0, 0, 0]) finger_rect([w, d], "out"); part("Dispensing end", at = [0, t, 0], rotate = [90, 0, 0]) end(true); part("Rear end", at = [0, d, 0], rotate = [90, 0, 0]) end(false); part("Side", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([d, h], ["in", "in", "flat", "in"]); } step("Glue one roof slope in place") part("Fixed roof", at = [-overhang, -overhang, h - overhang * tan(angle)], rotate = [0, -angle, 0]) rect([slant + extra, d + 2 * overhang]); step("Lay the other slope on top, loose for refilling") part("Lift-off roof", at = [w / 2, -overhang, h + roof_rise], rotate = [0, angle, 0]) rect([slant + extra, d + 2 * overhang]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/jewel-tray-box/ # Jewellery tray box Jewel tray box: an open box, lift-off lid and removable compartment grid. The lid locates on a glued inset plate; it is not a friction-fit hinge. Assembly: glue the box joints and lid layers. Leave the grid removable. This design has 11 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=jewel-tray-box). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/jewel-tray-box/jewel-tray-box.kerf) - [jewel-tray-box-sheet-1.svg](https://kerfscript.mylasertools.com/models/jewel-tray-box/jewel-tray-box-sheet-1.svg) - [jewel-tray-box-sheet-1.dxf](https://kerfscript.mylasertools.com/models/jewel-tray-box/jewel-tray-box-sheet-1.dxf) - [jewel-tray-box-sheet-2.svg](https://kerfscript.mylasertools.com/models/jewel-tray-box/jewel-tray-box-sheet-2.svg) - [jewel-tray-box-sheet-2.dxf](https://kerfscript.mylasertools.com/models/jewel-tray-box/jewel-tray-box-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | bottom | 1 | 210 × 145 mm | | front | 1 | 210 × 55 mm | | back | 1 | 210 × 55 mm | | left | 1 | 145 × 55 mm | | right | 1 | 145 × 55 mm | | long divider | 1 | 203 × 42 mm | | cross divider | 3 | 138 × 42 mm | | Lid | 1 | 218 × 153 mm | | Lid locator | 1 | 202.6 × 137.6 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `210` | 100 to 320 | | `depth` | `145` | 80 to 240 | | `height` | `55` | 35 to 100 | | `columns` | `4` | 1 to 7 | | `rows` | `2` | 1 to 5 | | `lid_overhang` | `4` | 2 to 8 | | `lid_ease` | `0.7` | 0.4 to 1.5 | | `lid_lift` | `45` | 0 to 100 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Jewel tray box: an open box, lift-off lid and removable compartment grid. // The lid locates on a glued inset plate; it is not a friction-fit hinge. // Assembly: glue the box joints and lid layers. Leave the grid removable. use use /* [Box] */ width = 210; // [100:5:320] depth = 145; // [80:5:240] height = 55; // [35:5:100] columns = 4; // [1:1:7] rows = 2; // [1:1:5] lid_overhang = 4; // [2:1:8] lid_ease = 0.7; // [0.4:0.1:1.5] /* [Assembled view] */ // Lift the lid to see the compartments. Cut parts keep their dimensions. lid_lift = 45; // [0:5:100] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; iw = width - 2 * t; id = depth - 2 * t; assert(iw / columns > 18 && id / rows > 18, "Compartments must be wider than 18 mm"); assert(height > 5 * t + 8, "Increase height for the lid locator and dividers"); step("Glue the box together") box([width, depth, height]); step("Drop the compartment grid in, without glue") grid_dividers([iw, id, height - 3 * t - 4], columns, rows, clearance = clearance, scoops = true, at = [t, t, t]); step("Glue the lid locator under the lid") { part("Lid", at = [-lid_overhang, -lid_overhang, height + lid_lift]) { rect([width + 2 * lid_overhang, depth + 2 * lid_overhang], r = 5); score() translate([lid_overhang + width / 2, lid_overhang + depth / 2]) { outline() rect([width * 0.6, depth * 0.54], center = true, r = 8); for (x = [-width * 0.18, 0, width * 0.18]) translate([x, 0]) outline() circle(d = 10); } } part("Lid locator", at = [t + lid_ease, t + lid_ease, height - t + lid_lift]) rect([iw - 2 * lid_ease, id - 2 * lid_ease], r = 3); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/earring-screen/ # Earring screen Earring screen: a plain perforated panel plugs into a broad foot. Hole pairs, row spacing and sheet thickness are independent controls. Assembly: test the slot fit, then glue the panel tabs into the foot. This design has 2 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=earring-screen). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/earring-screen/earring-screen.kerf) - [SVG](https://kerfscript.mylasertools.com/models/earring-screen/earring-screen.svg) - [DXF](https://kerfscript.mylasertools.com/models/earring-screen/earring-screen.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 200 × 80 mm | | Screen | 1 | 180 × 203 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `180` | 120 to 300 | | `height` | `200` | 120 to 320 | | `pairs` | `5` | 2 to 8 | | `rows` | `6` | 2 to 10 | | `hole` | `2.5` | 2 to 4 | | `pair_spacing` | `9` | 6 to 12 | | `foot_depth` | `80` | 60 to 130 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Earring screen: a plain perforated panel plugs into a broad foot. // Hole pairs, row spacing and sheet thickness are independent controls. // Assembly: test the slot fit, then glue the panel tabs into the foot. use /* [Display] */ width = 180; // [120:5:300] height = 200; // [120:5:320] pairs = 5; // [2:1:8] rows = 6; // [2:1:10] hole = 2.5; // [2:0.1:4] pair_spacing = 9; // [6:1:12] foot_depth = 80; // [60:5:130] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; assert(width / pairs > pair_spacing + hole + 5, "Reduce pairs or increase width"); assert((height - 40) / max(1, rows - 1) > hole + 5, "Reduce rows or increase height"); step("Lay the foot flat") part("Foot", at = [-10, 0, 0]) difference() { rect([width + 20, foot_depth], r = 5); translate([10, foot_depth / 2]) slots(width, clearance = clearance); } step("Test the slot fit, then glue the screen tabs into the foot") part("Screen", at = [0, foot_depth / 2 + t / 2, t], rotate = [90, 0, 0]) difference() { union() { rect([width, height], r = 5); tabs(width); } for (i = [0:pairs - 1], j = [0:rows - 1], s = [-1, 1]) translate([width * (i + 0.5) / pairs + s * pair_spacing / 2, 25 + j * (height - 40) / (rows - 1)]) circle(d = hole); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/napkin-bridge/ # Napkin bridge Napkin bridge: two scooped cheeks stand in a slotted rectangular foot. The clear gap follows the napkin stack, rather than a fixed sheet size. Assembly: glue both cheeks into the foot after checking the slot fit. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=napkin-bridge). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/napkin-bridge/napkin-bridge.kerf) - [SVG](https://kerfscript.mylasertools.com/models/napkin-bridge/napkin-bridge.svg) - [DXF](https://kerfscript.mylasertools.com/models/napkin-bridge/napkin-bridge.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 170 × 70 mm | | Cheek | 2 | 154 × 91 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `170` | 100 to 260 | | `height` | `88` | 55 to 140 | | `clear_gap` | `40` | 20 to 90 | | `scoop` | `18` | 0 to 30 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Napkin bridge: two scooped cheeks stand in a slotted rectangular foot. // The clear gap follows the napkin stack, rather than a fixed sheet size. // Assembly: glue both cheeks into the foot after checking the slot fit. use /* [Holder] */ width = 170; // [100:5:260] height = 88; // [55:5:140] clear_gap = 40; // [20:5:90] scoop = 18; // [0:1:30] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; depth = clear_gap + 2 * t + 24; centres = [12 + t / 2, 12 + 1.5 * t + clear_gap]; step("Lay the foot flat and check the slot fit") part("Foot", at = [0, 0, 0]) difference() { rect([width, depth], r = 5); for (y = centres) translate([8, y]) slots(width - 16, clearance = clearance); } step("Glue both cheeks into the foot") part("Cheek", qty = 2, at = [for (y = centres) [8, y + t / 2, t]], rotate = [90, 0, 0]) difference() { union() { rect([width - 16, height], r = 4); tabs(width - 16); } if (scoop > 0) translate([(width - 16) / 2, height]) ellipse([(width - 16) * 0.3, scoop]); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/post-sorter/ # Post sorter Post sorter: a row of tabbed dividers with a rising top line. Bay clearance and bay count set the base depth automatically. Assembly: glue each divider into the matching slots in the base. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=post-sorter). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/post-sorter/post-sorter.kerf) - [SVG](https://kerfscript.mylasertools.com/models/post-sorter/post-sorter.svg) - [DXF](https://kerfscript.mylasertools.com/models/post-sorter/post-sorter.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 220 × 137 mm | | Divider 1 | 1 | 200 × 68 mm | | Divider 2 | 1 | 200 × 90 mm | | Divider 3 | 1 | 200 × 112 mm | | Divider 4 | 1 | 200 × 134 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `220` | 150 to 340 | | `bays` | `3` | 1 to 6 | | `bay_gap` | `35` | 20 to 65 | | `front_height` | `65` | 45 to 100 | | `rise` | `22` | 0 to 30 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Post sorter: a row of tabbed dividers with a rising top line. // Bay clearance and bay count set the base depth automatically. // Assembly: glue each divider into the matching slots in the base. use /* [Storage] */ width = 220; // [150:5:340] bays = 3; // [1:1:6] bay_gap = 35; // [20:5:65] front_height = 65; // [45:5:100] rise = 22; // [0:1:30] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; margin = 10; depth = 2 * margin + (bays + 1) * t + bays * bay_gap; span = width - 2 * margin; step("Lay the base flat") part("Base", at = [0, 0, 0]) difference() { rect([width, depth], r = 4); for (i = [0:bays]) translate([margin, margin + t / 2 + i * (bay_gap + t)]) slots(span, clearance = clearance); } step("Glue each divider into its slots in the base") for (i = [0:bays]) part(str("Divider ", i + 1), at = [margin, margin + t + i * (bay_gap + t), t], rotate = [90, 0, 0]) difference() { union() { rect([span, front_height + rise * i], r = 4); tabs(span); } translate([span / 2, front_height + rise * i]) ellipse([span * 0.25, 13]); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/coaster-caddy/ # Coaster caddy Coaster caddy: a small finger-jointed bin with an open front scoop. The interior follows the coaster diameter and stacked thickness. Assembly: glue the finger joints. Coasters are not included in the file. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=coaster-caddy). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/coaster-caddy/coaster-caddy.kerf) - [SVG](https://kerfscript.mylasertools.com/models/coaster-caddy/coaster-caddy.svg) - [DXF](https://kerfscript.mylasertools.com/models/coaster-caddy/coaster-caddy.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Bottom | 1 | 107 × 36 mm | | Front | 1 | 107 × 61.9 mm | | Back | 1 | 107 × 61.9 mm | | End | 2 | 36 × 61.9 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `diameter` | `95` | 60 to 140 | | `coaster_thickness` | `4` | 2 to 8 | | `capacity` | `6` | 3 to 12 | | `ease` | `3` | 2 to 6 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Coaster caddy: a small finger-jointed bin with an open front scoop. // The interior follows the coaster diameter and stacked thickness. // Assembly: glue the finger joints. Coasters are not included in the file. /* [Coasters] */ diameter = 95; // [60:5:140] coaster_thickness = 4; // [2:0.5:8] capacity = 6; // [3:1:12] ease = 3; // [2:0.5:6] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; w = diameter + 2 * ease + 2 * t; d = capacity * coaster_thickness + 2 * ease + 2 * t; h = diameter * 0.62 + t; part("Bottom", at = [0, 0, 0]) finger_rect([w, d], "out"); step("Glue the front and back to the bottom") { part("Front", at = [0, t, 0], rotate = [90, 0, 0]) difference() { finger_rect([w, h], ["in", "out", "flat", "out"]); translate([w / 2, h + 5]) circle(d = diameter * 0.68); } part("Back", at = [0, d, 0], rotate = [90, 0, 0]) finger_rect([w, h], ["in", "out", "flat", "out"]); } step("Glue the ends in") part("End", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([d, h], ["in", "in", "flat", "in"]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/tissue-cover/ # Tissue carton cover Tissue cover: an open-bottom sleeve sized around a refill carton. A rounded dispensing slot is cut into the finger-jointed top panel. Assembly: glue the top and four walls; slide it over the carton. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=tissue-cover). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/tissue-cover/tissue-cover.kerf) - [SVG](https://kerfscript.mylasertools.com/models/tissue-cover/tissue-cover.svg) - [DXF](https://kerfscript.mylasertools.com/models/tissue-cover/tissue-cover.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Top | 1 | 235 × 125 mm | | Long wall | 2 | 235 × 95 mm | | End wall | 2 | 125 × 95 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `carton_width` | `225` | 120 to 280 | | `carton_depth` | `115` | 80 to 170 | | `carton_height` | `90` | 50 to 150 | | `ease` | `2` | 1 to 5 | | `opening` | `100` | 50 to 150 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Tissue cover: an open-bottom sleeve sized around a refill carton. // A rounded dispensing slot is cut into the finger-jointed top panel. // Assembly: glue the top and four walls; slide it over the carton. /* [Refill] */ carton_width = 225; // [120:5:280] carton_depth = 115; // [80:5:170] carton_height = 90; // [50:5:150] ease = 2; // [1:0.5:5] opening = 100; // [50:5:150] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; w = carton_width + 2 * ease + 2 * t; d = carton_depth + 2 * ease + 2 * t; h = carton_height + ease + t; assert(opening < w - 8 * t, "Opening leaves too little material at its ends"); part("Top", at = [0, 0, h - t]) difference() { finger_rect([w, d], "out"); translate([w / 2, d / 2]) rect([opening, 28], center = true, r = 14); } part("Long wall", qty = 2, at = [[0, t, 0], [0, d, 0]], rotate = [90, 0, 0]) finger_rect([w, h], ["flat", "out", "in", "out"]); part("End wall", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([d, h], ["flat", "in", "in", "in"]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/stem-lab/ # Stem lab tube stand Stem lab: two perforated decks hold removable glass tubes above a base. Tube diameter and pitch define the deck size and the side supports. Assembly: glue decks into side slots. Glass tubes are separate hardware. Water belongs inside the tubes; the plywood is not a watertight vase. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=stem-lab). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/stem-lab/stem-lab.kerf) - [SVG](https://kerfscript.mylasertools.com/models/stem-lab/stem-lab.svg) - [DXF](https://kerfscript.mylasertools.com/models/stem-lab/stem-lab.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 166 × 66 mm | | Side | 2 | 50 × 118 mm | | Tube deck | 2 | 150 × 50 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `tubes` | `4` | 1 to 8 | | `tube_diameter` | `18` | 10 to 30 | | `pitch` | `34` | 24 to 55 | | `height` | `115` | 70 to 170 | | `depth` | `50` | 40 to 90 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Stem lab: two perforated decks hold removable glass tubes above a base. // Tube diameter and pitch define the deck size and the side supports. // Assembly: glue decks into side slots. Glass tubes are separate hardware. // Water belongs inside the tubes; the plywood is not a watertight vase. use /* [Tubes] */ tubes = 4; // [1:1:8] tube_diameter = 18; // [10:1:30] pitch = 34; // [24:2:55] height = 115; // [70:5:170] depth = 50; // [40:5:90] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; span = (tubes - 1) * pitch + tube_diameter + 24; levels = [height * 0.35, height - 12]; assert(pitch >= tube_diameter + 6, "Increase pitch to leave a web between tubes"); // Steps are numbered in the order they first run, so name them here in build order step("Glue the tube decks into the side slots") {} step("Glue the sides into the base") part("Base", at = [-t - 8, -8, 0]) difference() { rect([span + 2 * t + 16, depth + 16], r = 5); for (x = [8 + t / 2, 8 + 1.5 * t + span]) translate([x, 8]) rotate(90) slots(depth, clearance = clearance); } step("Glue the tube decks into the side slots") { part("Side", qty = 2, at = [[-t, 0, t], [span, 0, t]], rotate = [90, 0, 90]) difference() { union() { rect([depth, height], r = 4); tabs(depth); } for (z = levels) translate([0, z + t / 2]) slots(depth, clearance = clearance); } part("Tube deck", qty = 2, at = [for (z = levels) [0, 0, t + z]]) difference() { tab_rect([span, depth], [false, true, false, true]); for (i = [0:tubes - 1]) translate([12 + tube_diameter / 2 + i * pitch, depth / 2]) circle(d = tube_diameter + 0.6); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/paint-deck/ # Perforated display deck Perforated deck: a raised shelf locates bottles or display eggs over a foot. Hole pitch, diameter and the grid count determine the footprint. An egg display is a parameter preset of this construction, not another model. Assembly: glue the two side supports into the foot, then add the deck. This design has 4 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=paint-deck). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/paint-deck/paint-deck.kerf) - [SVG](https://kerfscript.mylasertools.com/models/paint-deck/paint-deck.svg) - [DXF](https://kerfscript.mylasertools.com/models/paint-deck/paint-deck.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 237 × 126 mm | | Side | 2 | 114 × 46 mm | | Bottle deck | 1 | 225 × 114 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `columns` | `6` | 2 to 10 | | `rows` | `3` | 1 to 5 | | `diameter` | `26` | 16 to 42 | | `pitch` | `35` | 24 to 55 | | `deck_height` | `32` | 20 to 70 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Perforated deck: a raised shelf locates bottles or display eggs over a foot. // Hole pitch, diameter and the grid count determine the footprint. An egg // display is a parameter preset of this construction, not another model. // Assembly: glue the two side supports into the foot, then add the deck. use /* [Hole grid] */ columns = 6; // [2:1:10] rows = 3; // [1:1:5] diameter = 26; // [16:1:42] pitch = 35; // [24:1:55] deck_height = 32; // [20:2:70] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; span = (columns - 1) * pitch + diameter + 18; depth = (rows - 1) * pitch + diameter + 18; assert(pitch >= diameter + 5, "Increase pitch to leave 5 mm between holes"); step("Lay the foot flat") part("Foot", at = [-t - 6, -6, 0]) difference() { rect([span + 2 * t + 12, depth + 12], r = 4); for (x = [6 + t / 2, 6 + 1.5 * t + span]) translate([x, 6]) rotate(90) slots(depth, clearance = clearance); } step("Glue the two side supports into the foot") part("Side", qty = 2, at = [[-t, 0, t], [span, 0, t]], rotate = [90, 0, 90]) difference() { union() { rect([depth, deck_height + t + 8], r = 2); tabs(depth); } translate([0, deck_height + t / 2]) slots(depth, clearance = clearance); } step("Glue the deck onto the side supports") part("Bottle deck", at = [0, 0, t + deck_height]) difference() { tab_rect([span, depth], [false, true, false, true]); for (i = [0:columns - 1], j = [0:rows - 1]) translate([9 + diameter / 2 + i * pitch, 9 + diameter / 2 + j * pitch]) circle(d = diameter); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/miniature-bench/ # Miniature bench Miniature bench: a small seat and back tab into two shaped side supports. This is a display miniature. Dimensions are not a human seating design. Assembly: fit the seat and back between the sides, then glue the joints. This design has 4 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=miniature-bench). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/miniature-bench/miniature-bench.kerf) - [SVG](https://kerfscript.mylasertools.com/models/miniature-bench/miniature-bench.svg) - [DXF](https://kerfscript.mylasertools.com/models/miniature-bench/miniature-bench.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Side support | 2 | 58 × 97 mm | | Seat | 1 | 150 × 58 mm | | Back | 1 | 150 × 47 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `150` | 80 to 240 | | `depth` | `58` | 40 to 90 | | `seat_height` | `40` | 25 to 65 | | `back_height` | `47` | 25 to 75 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Miniature bench: a small seat and back tab into two shaped side supports. // This is a display miniature. Dimensions are not a human seating design. // Assembly: fit the seat and back between the sides, then glue the joints. use /* [Bench] */ width = 150; // [80:5:240] depth = 58; // [40:2:90] seat_height = 40; // [25:2:65] back_height = 47; // [25:2:75] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; span = width - 2 * t; back_at = depth - 1.5 * t; total = seat_height + t + back_height + 7; step("Stand the two side supports up") part("Side support", qty = 2, at = [[0, 0, 0], [width - t, 0, 0]], rotate = [90, 0, 90]) difference() { polygon([[0, 0], [depth, 0], [depth, total], [depth - 12, total], [depth - 12, seat_height + 14], [0, seat_height + 14]]); translate([0, seat_height + t / 2]) slots(depth, clearance = clearance); translate([back_at, seat_height + t]) rotate(90) slots(back_height, clearance = clearance); translate([depth / 2, 8]) rect([depth - 24, 16], center = true, r = 3); } step("Fit the seat and back between the sides and glue the joints") { part("Seat", at = [t, 0, seat_height]) tab_rect([span, depth], [false, true, false, true]); part("Back", at = [t, back_at + t / 2, seat_height + t], rotate = [90, 0, 0]) difference() { tab_rect([span, back_height], [false, true, false, true]); for (j = [1:2]) translate([10, back_height * j / 3 - 1.5]) rect([span - 20, 3], r = 1.5); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/book-brace/ # Book brace pair Book brace: a bookend pair with tabbed uprights and triangular gussets. The foot extends beneath the books so their weight helps stabilize it. Assembly: glue each upright and gusset into its foot. Add non-slip pads. This design has 6 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=book-brace). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/book-brace/book-brace.kerf) - [SVG](https://kerfscript.mylasertools.com/models/book-brace/book-brace.svg) - [DXF](https://kerfscript.mylasertools.com/models/book-brace/book-brace.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot 1 | 1 | 110 × 120 mm | | Upright 1 | 1 | 98 × 158 mm | | Gusset 1 | 1 | 96 × 83 mm | | Foot 2 | 1 | 110 × 120 mm | | Upright 2 | 1 | 98 × 158 mm | | Gusset 2 | 1 | 96 × 83 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `height` | `155` | 100 to 230 | | `width` | `110` | 80 to 180 | | `foot_length` | `120` | 90 to 190 | | `brace_height` | `80` | 50 to 100 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Book brace: a bookend pair with tabbed uprights and triangular gussets. // The foot extends beneath the books so their weight helps stabilize it. // Assembly: glue each upright and gusset into its foot. Add non-slip pads. use /* [Bookends] */ height = 155; // [100:5:230] width = 110; // [80:5:180] foot_length = 120; // [90:5:190] brace_height = 80; // [50:5:100] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; assert(brace_height < height - 12, "Gusset must be lower than the upright"); for (k = [0:1]) { ox = k * (width + 28); part(str("Foot ", k + 1), at = [ox, 0, 0]) difference() { rect([width, foot_length], r = 3); translate([6, 10 + t / 2]) slots(width - 12, clearance = clearance); translate([width / 2, 10 + t]) rotate(90) slots(foot_length - 24, count = 2, clearance = clearance); } step("Glue each upright into its foot") part(str("Upright ", k + 1), at = [ox + 6, 10 + t, t], rotate = [90, 0, 0]) union() { rect([width - 12, height], r = 4); tabs(width - 12); } step("Glue a gusset into each foot, behind its upright") part(str("Gusset ", k + 1), at = [ox + width / 2 - t / 2, 10 + t, t], rotate = [90, 0, 90]) union() { polygon([[0, 0], [foot_length - 24, 0], [0, brace_height]]); tabs(foot_length - 24, count = 2); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/cable-harbour/ # Cable harbour device tray Cable harbour: a divided device tray with cable exits through its back. Bay width, count and cable diameter generate the internal layout. Assembly: glue the box, then fit the dividers without glue for cleaning. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=cable-harbour). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/cable-harbour/cable-harbour.kerf) - [SVG](https://kerfscript.mylasertools.com/models/cable-harbour/cable-harbour.svg) - [DXF](https://kerfscript.mylasertools.com/models/cable-harbour/cable-harbour.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 93 × 145 mm | | Front | 1 | 93 × 44 mm | | Cable wall | 1 | 93 × 44 mm | | End | 2 | 145 × 44 mm | | Device divider | 2 | 138 × 68 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `bays` | `3` | 1 to 6 | | `bay_width` | `27` | 20 to 45 | | `depth` | `145` | 90 to 230 | | `wall_height` | `44` | 30 to 70 | | `divider_height` | `65` | 45 to 100 | | `cable_diameter` | `8` | 5 to 14 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Cable harbour: a divided device tray with cable exits through its back. // Bay width, count and cable diameter generate the internal layout. // Assembly: glue the box, then fit the dividers without glue for cleaning. use use /* [Devices] */ bays = 3; // [1:1:6] bay_width = 27; // [20:1:45] depth = 145; // [90:5:230] wall_height = 44; // [30:2:70] divider_height = 65; // [45:5:100] cable_diameter = 8; // [5:1:14] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; w = bays * bay_width + (bays + 1) * t; part("Floor", at = [0, 0, 0]) difference() { finger_rect([w, depth], "out"); if (bays > 1) for (i = [1:bays - 1]) translate([i * (bay_width + t) + t / 2, t + 0.5]) rotate(90) slots(depth - 2 * t - 1, clearance = clearance); } step("Glue the front, the cable wall and the ends to the floor") { part("Front", at = [0, t, 0], rotate = [90, 0, 0]) finger_rect([w, wall_height], ["in", "out", "flat", "out"]); part("Cable wall", at = [0, depth, 0], rotate = [90, 0, 0]) difference() { finger_rect([w, wall_height], ["in", "out", "flat", "out"]); for (i = [0:bays - 1]) translate([t + bay_width / 2 + i * (bay_width + t), wall_height]) rect([cable_diameter, wall_height * 0.66], center = true, r = cable_diameter / 2); } part("End", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([depth, wall_height], ["in", "in", "flat", "in"]); } step("Fit the device dividers into the floor slots, without glue") if (bays > 1) part("Device divider", qty = bays - 1, at = [for (i = [1:bays - 1]) [i * (bay_width + t), t + 0.5, t]], rotate = [90, 0, 90]) union() { rect([depth - 2 * t - 1, divider_height], r = 4); tabs(depth - 2 * t - 1); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ribbon-rail/ # Ribbon spool rail Ribbon rail: a pair of uprights hold a removable wooden dowel over a base. Spool count and width set the space between uprights. Dowel not included. Assembly: glue the uprights into the base; pass the dowel through both. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=ribbon-rail). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/ribbon-rail/ribbon-rail.kerf) - [SVG](https://kerfscript.mylasertools.com/models/ribbon-rail/ribbon-rail.svg) - [DXF](https://kerfscript.mylasertools.com/models/ribbon-rail/ribbon-rail.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 180 × 115 mm | | Axle support | 2 | 99 × 70.5 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `spools` | `5` | 1 to 10 | | `spool_width` | `28` | 15 to 55 | | `spool_diameter` | `75` | 40 to 120 | | `dowel_diameter` | `8` | 6 to 14 | | `spool_gap` | `3` | 2 to 8 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Ribbon rail: a pair of uprights hold a removable wooden dowel over a base. // Spool count and width set the space between uprights. Dowel not included. // Assembly: glue the uprights into the base; pass the dowel through both. use /* [Spools] */ spools = 5; // [1:1:10] spool_width = 28; // [15:1:55] spool_diameter = 75; // [40:5:120] dowel_diameter = 8; // [6:1:14] spool_gap = 3; // [2:1:8] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; span = spools * (spool_width + spool_gap) + spool_gap; depth = spool_diameter + 24; axle_height = spool_diameter / 2 + 12; height = axle_height + dowel_diameter / 2 + 14; part("Base", at = [-8, -8, 0]) difference() { rect([span + 2 * t + 16, depth + 16], r = 5); for (x = [8 + t / 2, 8 + span + 1.5 * t]) translate([x, 8]) rotate(90) slots(depth, clearance = clearance); } part("Axle support", qty = 2, at = [[0, 0, t], [span + t, 0, t]], rotate = [90, 0, 90]) difference() { union() { rect([depth, height], r = 5); tabs(depth); } translate([depth / 2, axle_height]) circle(d = dowel_diameter + 0.6); } // Cut a dowel to span + 2 * thickness + 20 mm. A collar at each end // prevents it sliding out. The 3D model contains the cut sheet parts only. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/bottle-tote/ # Bottle display tote Bottle tote: a compartment tray with a tall central handle partition. Bottle diameter and column count generate the footprint and handle span. Assembly: glue all joints and the handle partition to the floor. Use for light display bottles until the chosen material and joints are load tested. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=bottle-tote). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/bottle-tote/bottle-tote.kerf) - [SVG](https://kerfscript.mylasertools.com/models/bottle-tote/bottle-tote.svg) - [DXF](https://kerfscript.mylasertools.com/models/bottle-tote/bottle-tote.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 158 × 158 mm | | Long wall | 2 | 158 × 65 mm | | End wall | 2 | 158 × 65 mm | | Handle partition | 1 | 150 × 189 mm | | Cross divider | 1 | 149 × 58 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `bottle_diameter` | `65` | 45 to 90 | | `columns` | `2` | 2 to 4 | | `ease` | `4` | 2 to 8 | | `wall_height` | `65` | 45 to 100 | | `handle_height` | `185` | 140 to 240 | | `$thickness` | `4` | 3 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Bottle tote: a compartment tray with a tall central handle partition. // Bottle diameter and column count generate the footprint and handle span. // Assembly: glue all joints and the handle partition to the floor. Use for // light display bottles until the chosen material and joints are load tested. use /* [Bottles] */ bottle_diameter = 65; // [45:1:90] columns = 2; // [2:1:4] ease = 4; // [2:1:8] wall_height = 65; // [45:5:100] handle_height = 185; // [140:5:240] /* [Material] */ $thickness = 4; // [3:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; pitch = bottle_diameter + 2 * ease + t; w = columns * pitch + t; d = 2 * pitch + t; inner = w - 2 * t; slot_h = wall_height - t - 3; assert(handle_height > wall_height + 55, "Handle needs room above the compartments"); part("Floor", at = [0, 0, 0]) difference() { finger_rect([w, d], "out"); translate([t, d / 2]) slots_for_handle(); } // Two short tabs locate the handle in the floor, avoiding its cross slots. module slots_for_handle() { for (x = [inner * 0.2, inner * 0.8]) translate([x, 0]) rect([14 + clearance, t + clearance], center = true); } step("Glue the long walls and end walls to the floor") { part("Long wall", qty = 2, at = [[0, t, 0], [0, d, 0]], rotate = [90, 0, 0]) finger_rect([w, wall_height], ["in", "out", "flat", "out"]); part("End wall", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([d, wall_height], ["in", "in", "flat", "in"]); } step("Stand the cross dividers on the floor and lower the handle partition over them, tabs into the floor") { part("Handle partition", at = [t, d / 2 + t / 2, t], rotate = [90, 0, 0]) difference() { union() { rect([inner, handle_height], r = 8); for (x = [inner * 0.2, inner * 0.8]) translate([x - 7, -t]) rect([14, t + 0.1]); } translate([inner / 2, handle_height - 25]) rect([min(95, inner - 30), 25], center = true, r = 10); for (i = [1:columns - 1]) translate([i * pitch - t / 2 - (t + clearance) / 2, -1]) rect([t + clearance, slot_h / 2 + 1]); } part("Cross divider", qty = columns - 1, at = [for (i = [1:columns - 1]) [i * pitch, t + 0.5, t]], rotate = [90, 0, 90]) slotted_divider(d - 2 * t - 1, slot_h, [d / 2 - t - 0.5], "top", clearance); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/pocket-valet/ # Pocket valet tray Pocket valet: a shallow tray with a removable key and watch perch. A tabbed upright holds a horizontal shelf with open key-ring notches. Assembly: glue the tray and upright joints; add felt if desired. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=pocket-valet). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/pocket-valet/pocket-valet.kerf) - [SVG](https://kerfscript.mylasertools.com/models/pocket-valet/pocket-valet.svg) - [DXF](https://kerfscript.mylasertools.com/models/pocket-valet/pocket-valet.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 220 × 140 mm | | Long wall | 2 | 220 × 28 mm | | End wall | 2 | 140 × 28 mm | | Perch upright | 1 | 196 × 123 mm | | Key perch | 1 | 196 × 31 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `220` | 150 to 320 | | `depth` | `140` | 100 to 220 | | `tray_height` | `28` | 20 to 45 | | `perch_height` | `110` | 80 to 160 | | `keys` | `4` | 2 to 7 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Pocket valet: a shallow tray with a removable key and watch perch. // A tabbed upright holds a horizontal shelf with open key-ring notches. // Assembly: glue the tray and upright joints; add felt if desired. use /* [Valet] */ width = 220; // [150:5:320] depth = 140; // [100:5:220] tray_height = 28; // [20:2:45] perch_height = 110; // [80:5:160] keys = 4; // [2:1:7] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; span = width - 24; wall_at = depth - 24; reach = 28; step("Glue the walls into the floor") { part("Floor", at = [0, 0, 0]) difference() { finger_rect([width, depth], "out"); translate([12, wall_at]) slots(span, clearance = clearance); } part("Long wall", qty = 2, at = [[0, t, 0], [0, depth, 0]], rotate = [90, 0, 0]) finger_rect([width, tray_height], ["in", "out", "flat", "out"]); part("End wall", qty = 2, at = [[0, 0, 0], [width - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([depth, tray_height], ["in", "in", "flat", "in"]); } step("Glue the upright into the floor") part("Perch upright", at = [12, wall_at + t / 2, t], rotate = [90, 0, 0]) difference() { union() { rect([span, perch_height + 10], r = 3); tabs(span); } translate([0, perch_height + t / 2]) slots(span, clearance = clearance); } step("Glue the key perch into the upright") part("Key perch", at = [12, wall_at - t / 2 - reach, t + perch_height]) difference() { tab_rect([span, reach], [false, false, true, false]); for (i = [0:keys - 1]) translate([span * (i + 0.5) / keys, 0]) rect([5, 18], center = true, r = 2); } // Rear-edge tabs pass through the upright into free space behind it. ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/card-pocket/ # Business card pocket Card pocket: an open-top card tray with a scooped front and a taller back. Interior width and depth follow the cards and stack, with editable ease. Assembly: glue the five finger-jointed panels. Cards are not supplied. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=card-pocket). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/card-pocket/card-pocket.kerf) - [SVG](https://kerfscript.mylasertools.com/models/card-pocket/card-pocket.svg) - [DXF](https://kerfscript.mylasertools.com/models/card-pocket/card-pocket.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 100 × 32 mm | | Front | 1 | 100 × 24 mm | | Back | 1 | 100 × 55 mm | | Side | 2 | 32 × 24 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `card_width` | `90` | 70 to 160 | | `stack_depth` | `22` | 12 to 60 | | `back_height` | `55` | 40 to 90 | | `front_height` | `24` | 18 to 38 | | `ease` | `2` | 1 to 4 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Card pocket: an open-top card tray with a scooped front and a taller back. // Interior width and depth follow the cards and stack, with editable ease. // Assembly: glue the five finger-jointed panels. Cards are not supplied. /* [Cards] */ card_width = 90; // [70:1:160] stack_depth = 22; // [12:1:60] back_height = 55; // [40:1:90] front_height = 24; // [18:1:38] ease = 2; // [1:0.5:4] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; w = card_width + 2 * ease + 2 * t; d = stack_depth + 2 * ease + 2 * t; assert(back_height > front_height + 8, "Back must rise above the front lip"); assert(front_height >= 3 * t, "Front lip must be at least three sheet thicknesses high"); // Side corners only meet the common lower wall height. The back rises // above those joints, without fingers exposed along its upper sides. part("Floor", at = [0, 0, 0]) finger_rect([w, d], "out"); step("Glue the front and back to the floor") { part("Front", at = [0, t, 0], rotate = [90, 0, 0]) difference() { finger_rect([w, front_height], ["in", "out", "flat", "out"]); translate([w / 2, front_height + 5]) ellipse([w * 0.26, 10]); } part("Back", at = [0, d, 0], rotate = [90, 0, 0]) union() { finger_rect([w, front_height], ["in", "out", "flat", "out"]); translate([t, front_height - 0.1]) rect([w - 2 * t, back_height - front_height + 0.1], r = 2); } } step("Glue the sides in") part("Side", qty = 2, at = [[0, 0, 0], [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([d, front_height], ["in", "in", "flat", "in"]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/plant-plinth/ # Plant display plinth Plant plinth: two crossing half-lapped frames support a circular top. Pot diameter sets the top and footprint; height sets the support frames. Assembly: cross the frames at their half-depth slots and glue on the top. Use as a small display stand after checking the material's load capacity. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=plant-plinth). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/plant-plinth/plant-plinth.kerf) - [SVG](https://kerfscript.mylasertools.com/models/plant-plinth/plant-plinth.svg) - [DXF](https://kerfscript.mylasertools.com/models/plant-plinth/plant-plinth.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Frame A | 1 | 107.2 × 85 mm | | Frame B | 1 | 107.2 × 85 mm | | Top | 1 | 134 × 134 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `pot_diameter` | `110` | 70 to 180 | | `height` | `85` | 50 to 150 | | `rim` | `12` | 8 to 20 | | `leg_width` | `22` | 16 to 35 | | `$thickness` | `4` | 3 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Plant plinth: two crossing half-lapped frames support a circular top. // Pot diameter sets the top and footprint; height sets the support frames. // Assembly: cross the frames at their half-depth slots and glue on the top. // Use as a small display stand after checking the material's load capacity. /* [Stand] */ pot_diameter = 110; // [70:5:180] height = 85; // [50:5:150] rim = 12; // [8:1:20] leg_width = 22; // [16:1:35] /* [Material] */ $thickness = 4; // [3:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; diameter = pot_diameter + 2 * rim; span = diameter * 0.8; assert(span > 3 * leg_width + 8, "Reduce leg width or enlarge the top"); assert(height > leg_width + 12, "Increase height to leave a clear opening"); module frame(top_slot = false) difference() { rect([span, height]); window = (span - 3 * leg_width) / 2; for (x = [leg_width, 2 * leg_width + window]) translate([x, height * 0.2]) rect([window, height * 0.5], r = 3); translate([span / 2 - (t + clearance) / 2, top_slot ? height / 2 : -1]) rect([t + clearance, height / 2 + 1]); } step("Cross the two frames at their half-depth slots") { part("Frame A", at = [-span / 2, t / 2, 0], rotate = [90, 0, 0]) frame(true); part("Frame B", at = [-t / 2, -span / 2, 0], rotate = [90, 0, 90]) frame(false); } step("Glue the top on") part("Top", at = [0, 0, height]) circle(d = diameter); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/drawer-stack/ # Small-parts drawer stack Drawer stack: a finger-jointed cabinet with separate sliding tray drawers. Drawer count and clearances generate the shelf levels and outer height. Assembly: glue the carcass and shelf tabs, then glue each drawer square. Sand the runners and test the slide clearance before applying a finish. This design has 22 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=drawer-stack). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/drawer-stack/drawer-stack.kerf) - [drawer-stack-sheet-1.svg](https://kerfscript.mylasertools.com/models/drawer-stack/drawer-stack-sheet-1.svg) - [drawer-stack-sheet-1.dxf](https://kerfscript.mylasertools.com/models/drawer-stack/drawer-stack-sheet-1.dxf) - [drawer-stack-sheet-2.svg](https://kerfscript.mylasertools.com/models/drawer-stack/drawer-stack-sheet-2.svg) - [drawer-stack-sheet-2.dxf](https://kerfscript.mylasertools.com/models/drawer-stack/drawer-stack-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Carcass floor | 1 | 190 × 140 mm | | Carcass top | 1 | 190 × 140 mm | | Carcass back | 1 | 190 × 115.2 mm | | Carcass side | 2 | 140 × 115.2 mm | | Runner shelf | 2 | 190 × 137 mm | | Drawer 1 floor | 1 | 181.6 × 133 mm | | Drawer 1 front | 1 | 181.6 × 32 mm | | Drawer 1 back | 1 | 181.6 × 32 mm | | Drawer 1 side | 2 | 133 × 32 mm | | Drawer 2 floor | 1 | 181.6 × 133 mm | | Drawer 2 front | 1 | 181.6 × 32 mm | | Drawer 2 back | 1 | 181.6 × 32 mm | | Drawer 2 side | 2 | 133 × 32 mm | | Drawer 3 floor | 1 | 181.6 × 133 mm | | Drawer 3 front | 1 | 181.6 × 32 mm | | Drawer 3 back | 1 | 181.6 × 32 mm | | Drawer 3 side | 2 | 133 × 32 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `190` | 120 to 300 | | `depth` | `140` | 90 to 240 | | `drawers` | `3` | 1 to 5 | | `drawer_height` | `32` | 25 to 65 | | `slide_ease` | `1.2` | 0.6 to 2 | | `extension` | `0` | 0 to 0.6 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Drawer stack: a finger-jointed cabinet with separate sliding tray drawers. // Drawer count and clearances generate the shelf levels and outer height. // Assembly: glue the carcass and shelf tabs, then glue each drawer square. // Sand the runners and test the slide clearance before applying a finish. use /* [Cabinet] */ width = 190; // [120:5:300] depth = 140; // [90:5:240] drawers = 3; // [1:1:5] drawer_height = 32; // [25:1:65] slide_ease = 1.2; // [0.6:0.1:2] /* [Assembled view] */ // Keep at least 40 percent of each drawer supported inside the carcass. extension = 0; // [0:0.05:0.6] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; t = $thickness; step = drawer_height + 2 * slide_ease + t; height = drawers * step + t; span = width - 2 * t; reach = depth - t; dw = span - 2 * slide_ease; dd = reach - 4; assert(extension >= 0 && extension <= 0.6, "Keep at least 40 percent of the drawer supported"); assert(drawer_height > 3 * t + 8, "Drawer is too shallow for this sheet thickness"); step("Glue up the carcass, with the runner shelves in the side slots") { part("Carcass floor", at = [0, 0, 0]) finger_rect([width, depth], ["flat", "out", "out", "out"]); part("Carcass top", at = [0, 0, height - t]) finger_rect([width, depth], ["flat", "out", "out", "out"]); part("Carcass back", at = [0, depth, 0], rotate = [90, 0, 0]) finger_rect([width, height], ["in", "out", "in", "out"]); part("Carcass side", qty = 2, at = [[0, 0, 0], [width - t, 0, 0]], rotate = [90, 0, 90]) difference() { finger_rect([depth, height], ["in", "in", "in", "flat"]); if (drawers > 1) for (i = [1:drawers - 1]) translate([0, i * step + t / 2]) slots(reach, clearance = clearance); } if (drawers > 1) part("Runner shelf", qty = drawers - 1, at = [for (i = [1:drawers - 1]) [t, 0, i * step]]) tab_rect([span, reach], [false, true, false, true]); } step("Glue up each drawer and slide it in") for (i = [0:drawers - 1]) { x = t + slide_ease; y = 2 - extension * dd; z = t + slide_ease + i * step; part(str("Drawer ", i + 1, " floor"), at = [x, y, z]) finger_rect([dw, dd], "out"); part(str("Drawer ", i + 1, " front"), at = [x, y + t, z], rotate = [90, 0, 0]) difference() { finger_rect([dw, drawer_height], ["in", "out", "flat", "out"]); translate([dw / 2, drawer_height]) circle(d = min(24, drawer_height - 2 * t - 4)); } part(str("Drawer ", i + 1, " back"), at = [x, y + dd, z], rotate = [90, 0, 0]) finger_rect([dw, drawer_height], ["in", "out", "flat", "out"]); part(str("Drawer ", i + 1, " side"), qty = 2, at = [[x, y, z], [x + dw - t, y, z]], rotate = [90, 0, 90]) finger_rect([dd, drawer_height], ["in", "in", "flat", "in"]); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/loom-card/ # Weaving loom and shuttle Loom card: a reusable small weaving frame with rounded yarn notches. Warp count and pitch generate equal notches along both short edges. Assembly: wind a continuous warp between opposite notches. The second part is a blunt shuttle with two open yarn pockets, not a cutting tool. This design has 2 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=loom-card). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/loom-card/loom-card.kerf) - [SVG](https://kerfscript.mylasertools.com/models/loom-card/loom-card.svg) - [DXF](https://kerfscript.mylasertools.com/models/loom-card/loom-card.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Loom | 1 | 132 × 180 mm | | Shuttle | 1 | 22 × 159.8 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `warp_count` | `17` | 7 to 31 | | `pitch` | `6` | 4 to 10 | | `length` | `180` | 100 to 280 | | `border` | `18` | 12 to 28 | | `notch_width` | `2` | 1.5 to 3 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Loom card: a reusable small weaving frame with rounded yarn notches. // Warp count and pitch generate equal notches along both short edges. // Assembly: wind a continuous warp between opposite notches. The second // part is a blunt shuttle with two open yarn pockets, not a cutting tool. /* [Weaving] */ warp_count = 17; // [7:1:31] pitch = 6; // [4:0.5:10] length = 180; // [100:5:280] border = 18; // [12:1:28] notch_width = 2; // [1.5:0.1:3] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; width = (warp_count - 1) * pitch + 2 * border; assert(pitch >= notch_width + 2, "Leave at least 2 mm between yarn notches"); assert(length > 2 * border + 35, "Increase frame length or reduce border"); part("Loom", at = [0, 0, 0]) difference() { rect([width, length], r = 6); translate([border, border]) rect([width - 2 * border, length - 2 * border], r = 5); for (i = [0:warp_count - 1], y = [0, length]) translate([border + i * pitch, y]) rect([notch_width, 12], center = true, r = notch_width / 2); } part("Shuttle", at = [width + 15, 0, 0]) difference() { rect([22, min(length, 160)], r = 9); for (y = [0, min(length, 160)]) translate([11, y]) rect([7, 28], center = true, r = 3.5); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ribbed-lantern/ # Ribbed lantern shade Ribbed lantern shade: curved radial fins locate in two notched rings. Ring tabs are cut into each fin, so changes to thickness update both joints. Assembly: glue the fins into both rings. The shade stands on the fin ends. Shade only: place a separate cool battery LED inside, with no flame. This design has 18 parts on 3 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=ribbed-lantern). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern.kerf) - [ribbed-lantern-sheet-1.svg](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-1.svg) - [ribbed-lantern-sheet-1.dxf](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-1.dxf) - [ribbed-lantern-sheet-2.svg](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-2.svg) - [ribbed-lantern-sheet-2.dxf](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-2.dxf) - [ribbed-lantern-sheet-3.svg](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-3.svg) - [ribbed-lantern-sheet-3.dxf](https://kerfscript.mylasertools.com/models/ribbed-lantern/ribbed-lantern-sheet-3.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Radial fin | 16 | 75 × 200 mm | | Collar ring | 2 | 105.9 × 105.9 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `diameter` | `220` | 140 to 340 | | `height` | `200` | 140 to 300 | | `ribs` | `16` | 8 to 24 | | `collar_diameter` | `70` | 50 to 110 | | `belly` | `1` | 0.6 to 1.8 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | ## Source ```kerf // Ribbed lantern shade: curved radial fins locate in two notched rings. // Ring tabs are cut into each fin, so changes to thickness update both joints. // Assembly: glue the fins into both rings. The shade stands on the fin ends. // Shade only: place a separate cool battery LED inside, with no flame. /* [Shade] */ diameter = 220; // [140:5:340] height = 200; // [140:5:300] ribs = 16; // [8:1:24] collar_diameter = 70; // [50:5:110] belly = 1; // [0.6:0.1:1.8] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] /* [Hidden] */ $material = "plywood"; $fn = 128; t = $thickness; ri = collar_diameter / 2; bulge = diameter / 2 - ri - 18; levels = [20, height - 20]; tab_start = ri + 4; tab_end = ri + 14; assert(bulge >= 10, "Increase diameter or reduce the collar"); assert(2 * (ri + 4) * sin(180 / ribs) > t + clearance + 4, "Reduce ribs or increase the collar diameter"); function outer(z) = ri + 18 + bulge * pow(sin(180 * z / height), belly); module fin() difference() { polygon(concat([[ri, 0]], [for (i = [0:60]) [outer(height * i / 60), height * i / 60]], [[ri, height]])); for (z = levels) { translate([ri - 1, z - clearance / 2]) rect([tab_start - ri + 1, t + clearance]); translate([tab_end, z - clearance / 2]) rect([diameter, t + clearance]); } } step("Set the fins out in a circle, tabs facing in") part("Radial fin", qty = ribs, at = [for (i = [0:ribs - 1]) [-t / 2 * sin(360 * i / ribs), t / 2 * cos(360 * i / ribs), 0]], rotate = [for (i = [0:ribs - 1]) [90, 0, 360 * i / ribs]]) fin(); step("Slide the fin tabs into both collar rings and glue them") part("Collar ring", qty = 2, at = [for (z = levels) [0, 0, z]]) difference() { circle(r = ri + 18); circle(r = ri - 5); // Open each slot through the outside rim so fins can slide in radially. for (i = [0:ribs - 1]) rotate(360 * i / ribs) translate([tab_start - clearance / 2, -(t + clearance) / 2]) rect([ri + 19 - tab_start + clearance, t + clearance]); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/slide-grid/ # Captive sliding tile puzzle Slide grid: a solvably shuffled sliding puzzle with a removable clear retainer. Circular finger ports let tiles slide beneath the cover while keeping them in. Assembly: glue the plywood spacer to the floor. Add tiles, then bolt the acrylic cover on with thin washers for vertical play. Undo it to change tiles. This design has 18 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=slide-grid). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/slide-grid/slide-grid.kerf) - [slide-grid-sheet-1.svg](https://kerfscript.mylasertools.com/models/slide-grid/slide-grid-sheet-1.svg) - [slide-grid-sheet-1.dxf](https://kerfscript.mylasertools.com/models/slide-grid/slide-grid-sheet-1.dxf) - [slide-grid-sheet-2.svg](https://kerfscript.mylasertools.com/models/slide-grid/slide-grid-sheet-2.svg) - [slide-grid-sheet-2.dxf](https://kerfscript.mylasertools.com/models/slide-grid/slide-grid-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Floor | 1 | 146 × 146 mm | | Cavity spacer | 1 | 146 × 146 mm | | Tile 1 | 1 | 30 × 30 mm | | Tile 2 | 1 | 30 × 30 mm | | Tile 3 | 1 | 30 × 30 mm | | Tile 5 | 1 | 30 × 30 mm | | Tile 6 | 1 | 30 × 30 mm | | Tile 7 | 1 | 30 × 30 mm | | Tile 4 | 1 | 30 × 30 mm | | Tile 9 | 1 | 30 × 30 mm | | Tile 10 | 1 | 30 × 30 mm | | Tile 11 | 1 | 30 × 30 mm | | Tile 8 | 1 | 30 × 30 mm | | Tile 13 | 1 | 30 × 30 mm | | Tile 14 | 1 | 30 × 30 mm | | Tile 15 | 1 | 30 × 30 mm | | Tile 12 | 1 | 30 × 30 mm | | Clear retainer | 1 | 146 × 146 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `columns` | `4` | 3 to 6 | | `rows` | `4` | 3 to 6 | | `tile_size` | `30` | 24 to 45 | | `shuffle_steps` | `32` | 0 to 80 | | `seed` | `7` | 1 to 99 | | `numbers` | `true` | on or off | | `tile_slide` | `0` | 0 to 1 | | `slide_gap` | `0.4` | 0.2 to 0.8 | | `vertical_play` | `0.3` | 0.2 to 0.6 | | `cover_thickness` | `2` | 1.5 to 3 | | `bolt_diameter` | `3` | 3 to 4 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Slide grid: a solvably shuffled sliding puzzle with a removable clear retainer. // Circular finger ports let tiles slide beneath the cover while keeping them in. // Assembly: glue the plywood spacer to the floor. Add tiles, then bolt the // acrylic cover on with thin washers for vertical play. Undo it to change tiles. /* [Puzzle] */ columns = 4; // [3:1:6] rows = 4; // [3:1:6] tile_size = 30; // [24:1:45] shuffle_steps = 32; // [0:1:80] seed = 7; // [1:1:99] numbers = true; /* [Assembled view] */ // Slide one adjacent tile into the blank, and back, without changing the cut. tile_slide = 0; // [0:0.05:1] /* [Fit] */ slide_gap = 0.4; // [0.2:0.05:0.8] vertical_play = 0.3; // [0.2:0.05:0.6] cover_thickness = 2; // [1.5:0.5:3] bolt_diameter = 3; // [3:0.5:4] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; border = 12; pitch = tile_size + slide_gap; iw = columns * pitch + slide_gap; id = rows * pitch + slide_gap; w = iw + 2 * border; d = id + 2 * border; cells = columns * rows; assert(tile_slide >= 0 && tile_slide <= 1, "Tile slide must stay between neighbouring cells"); function swapped(v, a, b) = [for (i = [0:len(v) - 1]) i == a ? v[b] : i == b ? v[a] : v[i]]; function shuffled(v, empty, n) = n <= 0 ? v : let ( choices = [for (i = [0:cells - 1]) if (abs(i % columns - empty % columns) + abs(floor(i / columns) - floor(empty / columns)) == 1) i], pick = choices[min(len(choices) - 1, floor(rands(0, len(choices), 1, seed + n)[0]))]) shuffled(swapped(v, empty, pick), pick, n - 1); values = shuffled(concat([for (i = [1:cells - 1]) i], [0]), cells - 1, shuffle_steps); empty = [for (i = [0:cells - 1]) if (values[i] == 0) i][0]; moving = [for (i = [0:cells - 1]) if (abs(i % columns - empty % columns) + abs(floor(i / columns) - floor(empty / columns)) == 1) i][0]; function location(i) = [border + slide_gap + (i % columns) * pitch, border + slide_gap + floor(i / columns) * pitch, t]; module screw_holes() for (x = [border / 2, w - border / 2], y = [border / 2, d - border / 2]) translate([x, y]) circle(d = bolt_diameter + 0.3); module plate() difference() { rect([w, d], r = 4); screw_holes(); } module numeral(n, size) { digits = [[0, 1, 2, 3, 4, 5], [1, 2], [0, 1, 6, 4, 3], [0, 1, 6, 2, 3], [5, 6, 1, 2], [0, 5, 6, 2, 3], [0, 5, 4, 3, 2, 6], [0, 1, 2], [0, 1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 5, 6]]; strokes = [[[0, 2], [1, 2]], [[1, 2], [1, 1]], [[1, 1], [1, 0]], [[0, 0], [1, 0]], [[0, 1], [0, 0]], [[0, 2], [0, 1]], [[0, 1], [1, 1]]]; scale(size / 2) for (k = digits[n]) line(strokes[k][0], strokes[k][1]); } step("Glue the spacer onto the floor") { part("Floor", at = [0, 0, 0]) plate(); part("Cavity spacer", at = [0, 0, t]) difference() { plate(); translate([border, border]) rect([iw, id], r = 1); } } step("Set the tiles in the tray") for (i = [0:cells - 1]) if (values[i] > 0) part(str("Tile ", values[i]), at = location(i) + (i == moving ? (location(empty) - location(i)) * tile_slide : [0, 0, 0])) { rect([tile_size, tile_size], r = 1); if (numbers) score() translate([tile_size / 2, tile_size / 2]) { if (values[i] >= 10) { translate([-7, -5]) numeral(floor(values[i] / 10), 10); translate([1, -5]) numeral(values[i] % 10, 10); } else translate([-2.5, -5]) numeral(values[i], 10); } } step("Bolt the clear retainer on, with thin washers for a little play") material(thickness = cover_thickness, name = "clear acrylic") part("Clear retainer", at = [0, 0, 2 * t + vertical_play]) difference() { plate(); for (i = [0:cells - 1]) translate([location(i)[0] + tile_size / 2, location(i)[1] + tile_size / 2]) circle(d = tile_size * 0.64); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/game-insert/ # Lift-out game box insert Game insert: lift-out card trays and token bins fill a measured game box. Card dimensions constrain the front bank; token bins fill the remaining area. Assembly: glue each bin separately. Leave all bins loose on the carrier floor. No game-specific artwork or component shapes are used. This design has 46 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=game-insert). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/game-insert/game-insert.kerf) - [game-insert-sheet-1.svg](https://kerfscript.mylasertools.com/models/game-insert/game-insert-sheet-1.svg) - [game-insert-sheet-1.dxf](https://kerfscript.mylasertools.com/models/game-insert/game-insert-sheet-1.dxf) - [game-insert-sheet-2.svg](https://kerfscript.mylasertools.com/models/game-insert/game-insert-sheet-2.svg) - [game-insert-sheet-2.dxf](https://kerfscript.mylasertools.com/models/game-insert/game-insert-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Carrier floor | 1 | 297 × 247 mm | | Card tray 1 floor | 1 | 97.4 × 102 mm | | Card tray 1 front | 1 | 97.4 × 38 mm | | Card tray 1 back | 1 | 97.4 × 38 mm | | Card tray 1 side | 2 | 102 × 38 mm | | Card tray 2 floor | 1 | 97.4 × 102 mm | | Card tray 2 front | 1 | 97.4 × 38 mm | | Card tray 2 back | 1 | 97.4 × 38 mm | | Card tray 2 side | 2 | 102 × 38 mm | | Card tray 3 floor | 1 | 97.4 × 102 mm | | Card tray 3 front | 1 | 97.4 × 38 mm | | Card tray 3 back | 1 | 97.4 × 38 mm | | Card tray 3 side | 2 | 102 × 38 mm | | Token bin 1 floor | 1 | 97.4 × 70.1 mm | | Token bin 1 front | 1 | 97.4 × 28 mm | | Token bin 1 back | 1 | 97.4 × 28 mm | | Token bin 1 side | 2 | 70.1 × 28 mm | | Token bin 4 floor | 1 | 97.4 × 70.1 mm | | Token bin 4 front | 1 | 97.4 × 28 mm | | Token bin 4 back | 1 | 97.4 × 28 mm | | Token bin 4 side | 2 | 70.1 × 28 mm | | Token bin 2 floor | 1 | 97.4 × 70.1 mm | | Token bin 2 front | 1 | 97.4 × 28 mm | | Token bin 2 back | 1 | 97.4 × 28 mm | | Token bin 2 side | 2 | 70.1 × 28 mm | | Token bin 5 floor | 1 | 97.4 × 70.1 mm | | Token bin 5 front | 1 | 97.4 × 28 mm | | Token bin 5 back | 1 | 97.4 × 28 mm | | Token bin 5 side | 2 | 70.1 × 28 mm | | Token bin 3 floor | 1 | 97.4 × 70.1 mm | | Token bin 3 front | 1 | 97.4 × 28 mm | | Token bin 3 back | 1 | 97.4 × 28 mm | | Token bin 3 side | 2 | 70.1 × 28 mm | | Token bin 6 floor | 1 | 97.4 × 70.1 mm | | Token bin 6 front | 1 | 97.4 × 28 mm | | Token bin 6 back | 1 | 97.4 × 28 mm | | Token bin 6 side | 2 | 70.1 × 28 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `box_width` | `300` | 180 to 420 | | `box_depth` | `250` | 180 to 380 | | `box_height` | `65` | 40 to 100 | | `box_ease` | `1.5` | 1 to 3 | | `card_width` | `66` | 45 to 90 | | `card_length` | `92` | 65 to 130 | | `deck_count` | `3` | 1 to 5 | | `stack_height` | `35` | 20 to 65 | | `card_ease` | `2` | 1 to 4 | | `token_columns` | `3` | 1 to 6 | | `token_rows` | `2` | 1 to 4 | | `token_height` | `28` | 18 to 50 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Game insert: lift-out card trays and token bins fill a measured game box. // Card dimensions constrain the front bank; token bins fill the remaining area. // Assembly: glue each bin separately. Leave all bins loose on the carrier floor. // No game-specific artwork or component shapes are used. /* [Game box interior] */ box_width = 300; // [180:5:420] box_depth = 250; // [180:5:380] box_height = 65; // [40:5:100] box_ease = 1.5; // [1:0.5:3] /* [Cards] */ card_width = 66; // [45:1:90] card_length = 92; // [65:1:130] deck_count = 3; // [1:1:5] stack_height = 35; // [20:1:65] card_ease = 2; // [1:0.5:4] /* [Tokens] */ token_columns = 3; // [1:1:6] token_rows = 2; // [1:1:4] token_height = 28; // [18:1:50] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; gap = 1.2; w = box_width - 2 * box_ease; d = box_depth - 2 * box_ease; cw = (w - (deck_count + 1) * gap) / deck_count; cd = card_length + 2 * card_ease + 2 * t; ch = stack_height + t; token_zone = d - cd - 3 * gap; tw = (w - (token_columns + 1) * gap) / token_columns; td = (token_zone - (token_rows - 1) * gap) / token_rows; assert(cw - 2 * t >= card_width + 2 * card_ease, "Card trays are too narrow: reduce decks or increase box width"); assert(tw - 2 * t >= 25 && td - 2 * t >= 25, "Token bins need at least 25 mm of clear space"); assert(t + max(ch, token_height) < box_height - 1, "Trays exceed the box height"); module bin(name, width, depth, height, at) { part(str(name, " floor"), at = at) finger_rect([width, depth], "out"); part(str(name, " front"), at = at + [0, t, 0], rotate = [90, 0, 0]) difference() { finger_rect([width, height], ["in", "out", "flat", "out"]); translate([width / 2, height]) ellipse([min(width * 0.24, 22), min(height * 0.3, 10)]); } part(str(name, " back"), at = at + [0, depth, 0], rotate = [90, 0, 0]) finger_rect([width, height], ["in", "out", "flat", "out"]); part(str(name, " side"), qty = 2, at = [at, at + [width - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([depth, height], ["in", "in", "flat", "in"]); } part("Carrier floor", at = [0, 0, 0]) { rect([w, d], r = 3); score() { for (i = [0:deck_count - 1]) translate([gap + i * (cw + gap), gap]) outline() rect([cw, cd]); for (i = [0:token_columns - 1], j = [0:token_rows - 1]) translate([gap + i * (tw + gap), cd + 2 * gap + j * (td + gap)]) outline() rect([tw, td]); } } step("Glue up each card tray and set it loose on the carrier floor") for (i = [0:deck_count - 1]) bin(str("Card tray ", i + 1), cw, cd, ch, [gap + i * (cw + gap), gap, t]); step("Glue up each token bin and set it loose behind the card trays") for (i = [0:token_columns - 1], j = [0:token_rows - 1]) bin(str("Token bin ", j * token_columns + i + 1), tw, td, token_height, [gap + i * (tw + gap), cd + 2 * gap + j * (td + gap), t]); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/cable-chain/ # Articulated cable chain Cable chain: alternating wide and narrow U-links pivot on bought side bolts. Each link has two cheeks and one tabbed floor bridge. The channel stays open. Assembly: glue each U-link square, then connect adjacent cheeks with short bolts, washers and locknuts. Keep the pivots loose enough to turn freely. This free-pivot prototype has no mechanical bend stops or machine rating. This design has 24 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=cable-chain). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/cable-chain/cable-chain.kerf) - [SVG](https://kerfscript.mylasertools.com/models/cable-chain/cable-chain.svg) - [DXF](https://kerfscript.mylasertools.com/models/cable-chain/cable-chain.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Link 1 left cheek | 1 | 58 × 22 mm | | Link 1 right cheek | 1 | 58 × 22 mm | | Link 1 bridge | 1 | 34 × 10 mm | | Link 2 left cheek | 1 | 58 × 22 mm | | Link 2 right cheek | 1 | 58 × 22 mm | | Link 2 bridge | 1 | 41 × 10 mm | | Link 3 left cheek | 1 | 58 × 22 mm | | Link 3 right cheek | 1 | 58 × 22 mm | | Link 3 bridge | 1 | 34 × 10 mm | | Link 4 left cheek | 1 | 58 × 22 mm | | Link 4 right cheek | 1 | 58 × 22 mm | | Link 4 bridge | 1 | 41 × 10 mm | | Link 5 left cheek | 1 | 58 × 22 mm | | Link 5 right cheek | 1 | 58 × 22 mm | | Link 5 bridge | 1 | 34 × 10 mm | | Link 6 left cheek | 1 | 58 × 22 mm | | Link 6 right cheek | 1 | 58 × 22 mm | | Link 6 bridge | 1 | 41 × 10 mm | | Link 7 left cheek | 1 | 58 × 22 mm | | Link 7 right cheek | 1 | 58 × 22 mm | | Link 7 bridge | 1 | 34 × 10 mm | | Link 8 left cheek | 1 | 58 × 22 mm | | Link 8 right cheek | 1 | 58 × 22 mm | | Link 8 bridge | 1 | 41 × 10 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `links` | `8` | 3 to 14 | | `channel_width` | `28` | 18 to 60 | | `link_height` | `22` | 20 to 40 | | `pitch` | `36` | 32 to 65 | | `pin_diameter` | `3` | 3 to 5 | | `joint_angle` | `8` | 0 to 12 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | | `pivot_clearance` | `0.3` | 0.2 to 0.6 | | `axial_gap` | `0.5` | 0.3 to 1 | ## Source ```kerf // Cable chain: alternating wide and narrow U-links pivot on bought side bolts. // Each link has two cheeks and one tabbed floor bridge. The channel stays open. // Assembly: glue each U-link square, then connect adjacent cheeks with short // bolts, washers and locknuts. Keep the pivots loose enough to turn freely. // This free-pivot prototype has no mechanical bend stops or machine rating. use /* [Chain] */ links = 8; // [3:1:14] channel_width = 28; // [18:1:60] link_height = 22; // [20:1:40] pitch = 36; // [32:1:65] pin_diameter = 3; // [3:0.5:5] /* [Assembled view] */ // Angle between successive links; this does not add a physical stop. joint_angle = 8; // [0:1:12] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] pivot_clearance = 0.3; // [0.2:0.05:0.6] axial_gap = 0.5; // [0.3:0.1:1] /* [Hidden] */ $material = "plywood"; t = $thickness; r = link_height / 2; bridge_length = pitch - 2 * (r + 2); floor_z = -r + 3; assert(bridge_length >= 8, "Increase pitch or reduce link height"); assert(link_height > 2 * t + 10, "Increase link height for this thickness"); assert(pin_diameter + pivot_clearance < link_height - 10, "Pivot holes leave too little material"); assert((links - 1) * joint_angle <= 150, "Reduce bend or link count to avoid curling back"); function joint(i) = [i == 0 ? 0 : sum([for (j = [0:i - 1]) pitch * cos(j * joint_angle)]), 0, r + (i == 0 ? 0 : sum([for (j = [0:i - 1]) pitch * sin(j * joint_angle)]))]; function direction(a) = [cos(a), 0, sin(a)]; function normal(a) = [-sin(a), 0, cos(a)]; module cheek() difference() { hull() { circle(r = r); translate([pitch, 0]) circle(r = r); } for (x = [0, pitch]) translate([x, 0]) circle(d = pin_diameter + pivot_clearance); translate([pitch / 2, floor_z + t / 2]) rect([bridge_length / 2 + clearance, t + clearance], center = true); } for (i = [0:links - 1]) { a = i * joint_angle; span = channel_width + (i % 2 == 0 ? 0 : 2 * (t + axial_gap)); for (side = [-1, 1]) part(str("Link ", i + 1, side < 0 ? " left cheek" : " right cheek"), at = joint(i) + [0, side * (span / 2 + t / 2) + t / 2, 0], rotate = [90, -a, 0]) cheek(); part(str("Link ", i + 1, " bridge"), at = joint(i) + direction(a) * (pitch / 2 + bridge_length / 2) + normal(a) * floor_z + [0, -span / 2, 0], rotate = [-a, 0, 90]) tab_rect([span, bridge_length], [false, true, false, true], count = 1, width = bridge_length / 2); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/cantilever-case/ # Cantilever sewing and tool case Cantilever case: two banks of trays rise outward on parallel pivot links. The pivot offsets and link lengths are computed from the tray dimensions. Assembly: glue the base and each tray square. Bolt the links outside the front and back walls, using washers and standoffs to separate link layers. Link pivots must turn freely. Do not glue the links to the trays. This design has 41 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=cantilever-case). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/cantilever-case/cantilever-case.kerf) - [cantilever-case-sheet-1.svg](https://kerfscript.mylasertools.com/models/cantilever-case/cantilever-case-sheet-1.svg) - [cantilever-case-sheet-1.dxf](https://kerfscript.mylasertools.com/models/cantilever-case/cantilever-case-sheet-1.dxf) - [cantilever-case-sheet-2.svg](https://kerfscript.mylasertools.com/models/cantilever-case/cantilever-case-sheet-2.svg) - [cantilever-case-sheet-2.dxf](https://kerfscript.mylasertools.com/models/cantilever-case/cantilever-case-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base well floor | 1 | 226 × 140 mm | | Base well front | 1 | 226 × 44 mm | | Base well back | 1 | 226 × 44 mm | | Base well end | 2 | 140 × 44 mm | | Left tray 1 floor | 1 | 110 × 140 mm | | Left tray 1 front | 1 | 110 × 32 mm | | Left tray 1 back | 1 | 110 × 32 mm | | Left tray 1 end | 2 | 140 × 32 mm | | Left link 1 front 1 | 1 | 70.5 × 9 mm | | Left link 1 front 2 | 1 | 70.5 × 9 mm | | Left link 1 back 1 | 1 | 70.5 × 9 mm | | Left link 1 back 2 | 1 | 70.5 × 9 mm | | Left tray 2 floor | 1 | 110 × 140 mm | | Left tray 2 front | 1 | 110 × 32 mm | | Left tray 2 back | 1 | 110 × 32 mm | | Left tray 2 end | 2 | 140 × 32 mm | | Left link 2 front 1 | 1 | 70.5 × 9 mm | | Left link 2 front 2 | 1 | 70.5 × 9 mm | | Left link 2 back 1 | 1 | 70.5 × 9 mm | | Left link 2 back 2 | 1 | 70.5 × 9 mm | | Right tray 1 floor | 1 | 110 × 140 mm | | Right tray 1 front | 1 | 110 × 32 mm | | Right tray 1 back | 1 | 110 × 32 mm | | Right tray 1 end | 2 | 140 × 32 mm | | Right link 1 front 1 | 1 | 70.5 × 9 mm | | Right link 1 front 2 | 1 | 70.5 × 9 mm | | Right link 1 back 1 | 1 | 70.5 × 9 mm | | Right link 1 back 2 | 1 | 70.5 × 9 mm | | Right tray 2 floor | 1 | 110 × 140 mm | | Right tray 2 front | 1 | 110 × 32 mm | | Right tray 2 back | 1 | 110 × 32 mm | | Right tray 2 end | 2 | 140 × 32 mm | | Right link 2 front 1 | 1 | 70.5 × 9 mm | | Right link 2 front 2 | 1 | 70.5 × 9 mm | | Right link 2 back 1 | 1 | 70.5 × 9 mm | | Right link 2 back 2 | 1 | 70.5 × 9 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `tray_width` | `110` | 90 to 170 | | `depth` | `140` | 100 to 220 | | `tray_height` | `32` | 25 to 50 | | `base_height` | `44` | 35 to 75 | | `levels` | `2` | 1 to 3 | | `lids` | `false` | on or off | | `opening` | `0.8` | 0 to 1 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `pin_diameter` | `3` | 3 to 4 | | `pivot_clearance` | `0.3` | 0.2 to 0.6 | | `washer_gap` | `0.5` | 0.3 to 1 | ## Source ```kerf // Cantilever case: two banks of trays rise outward on parallel pivot links. // The pivot offsets and link lengths are computed from the tray dimensions. // Assembly: glue the base and each tray square. Bolt the links outside the // front and back walls, using washers and standoffs to separate link layers. // Link pivots must turn freely. Do not glue the links to the trays. /* [Storage] */ tray_width = 110; // [90:5:170] depth = 140; // [100:5:220] tray_height = 32; // [25:1:50] base_height = 44; // [35:1:75] levels = 2; // [1:1:3] lids = false; /* [Assembled view] */ // Zero closes the trays; one raises them to the designed open position. opening = 0.8; // [0:0.05:1] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] pin_diameter = 3; // [3:0.5:4] pivot_clearance = 0.3; // [0.2:0.05:0.6] washer_gap = 0.5; // [0.3:0.1:1] /* [Hidden] */ $material = "plywood"; t = $thickness; seam = 6; base_w = 2 * tray_width + seam; step = tray_height + 3; offset = tray_width * 0.46; spread = tray_width * 0.24; margin = tray_width * 0.10; link_length = sqrt(offset * offset + step * step); closed_angle = atan2(step, offset); angle = closed_angle + (82 - closed_angle) * opening; dx = offset - link_length * cos(angle); dz = link_length * sin(angle); arm_width = max(9, 2 * t + 3); assert(spread * sin(closed_angle) > arm_width + 0.5, "Increase tray height or width, or use thinner stock, to separate the parallel links"); assert(base_height - tray_height / 2 > t + pin_diameter + 3, "Increase the base height to support its pivot holes"); assert(tray_height / 2 > t + pin_diameter + 2, "Increase tray height for this sheet and pivot size"); assert(margin > pin_diameter / 2 + 3, "Pivot margin is too small"); function anchor(side, k, upper = false) = margin + k * spread + ((side > 0 ? !upper : upper) ? offset : 0); function tray_at(side, j) = [(side < 0 ? 0 : tray_width + seam) + side * j * dx, 0, base_height - tray_height / 2 + j * dz - tray_height / 2]; function pivot(side, j, k, upper = false) = tray_at(side, j) + [anchor(side, k, upper), 0, tray_height / 2]; module pivot_holes(side, base = false, stage = 0) { // Base walls support outgoing links only; top trays receive incoming links. // Intermediate trays connect both stages. for (k = [0:1], upper = base ? [false] : (stage == levels ? [true] : [false, true])) translate([anchor(side, k, upper), base ? base_height - tray_height / 2 : tray_height / 2]) circle(d = pin_diameter + pivot_clearance); } module tray(name, w, h, at, side = 0, stage = 0) { part(str(name, " floor"), at = at) finger_rect([w, depth], "out"); for (back = [false, true]) part(str(name, back ? " back" : " front"), at = at + [0, back ? depth : t, 0], rotate = [90, 0, 0]) difference() { finger_rect([w, h], ["in", "out", "flat", "out"]); if (side == 0) for (s = [-1, 1]) translate([s < 0 ? 0 : tray_width + seam, 0]) pivot_holes(s, true); else pivot_holes(side, stage = stage); } part(str(name, " end"), qty = 2, at = [at, at + [w - t, 0, 0]], rotate = [90, 0, 90]) finger_rect([depth, h], ["in", "in", "flat", "in"]); } module link() difference() { hull() { circle(d = arm_width); translate([link_length, 0]) circle(d = arm_width); } for (x = [0, link_length]) translate([x, 0]) circle(d = pin_diameter + pivot_clearance); } step("Glue up the base well") tray("Base well", base_w, base_height, [0, 0, 0]); for (side = [-1, 1], j = [1:levels]) { tag = str(side < 0 ? "Left" : "Right", " tray ", j); step(str("Glue up the level ", j, " trays and bolt their links on outside the front and back walls")) { tray(tag, tray_width, tray_height, tray_at(side, j), side, j); for (back = [false, true], k = [0:1]) { y = back ? depth + washer_gap + t + (j - 1) * (t + washer_gap) : -washer_gap - (j - 1) * (t + washer_gap); part(str(side < 0 ? "Left" : "Right", " link ", j, back ? " back " : " front ", k + 1), at = pivot(side, j - 1, k) + [0, y, 0], rotate = [90, -(side < 0 ? angle : 180 - angle), 0]) link(); } } if (lids && j == levels) step("Glue a locator under each lid and set the lids on the top trays") { at = tray_at(side, j); part(str(tag, " lid"), at = at + [-2, -2, tray_height]) rect([tray_width + 4, depth + 4], r = 3); part(str(tag, " lid locator"), at = at + [t + 0.7, t + 0.7, tray_height - t]) rect([tray_width - 2 * t - 1.4, depth - 2 * t - 1.4], r = 2); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/beam-balance/ # Beam balance with suspended pans Beam balance: a stable pivoted beam with two level, cord-suspended pans. Glue the fork uprights into the foot and glue a rim onto each pan. Fit the beam on a loose pivot bolt, then tie three equal cords per pan. The tilt control demonstrates motion; it does not calculate equilibrium. This design has 8 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=beam-balance). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/beam-balance/beam-balance.kerf) - [SVG](https://kerfscript.mylasertools.com/models/beam-balance/beam-balance.svg) - [DXF](https://kerfscript.mylasertools.com/models/beam-balance/beam-balance.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 150 × 100 mm | | Fork upright | 2 | 28 × 177 mm | | Balance beam | 1 | 228 × 33 mm | | Left pan floor | 1 | 70 × 70 mm | | Left pan rim | 1 | 70 × 70 mm | | Right pan floor | 1 | 70 × 70 mm | | Right pan rim | 1 | 70 × 70 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `arm` | `105` | 80 to 145 | | `pivot_height` | `165` | 125 to 220 | | `drop` | `95` | 65 to 130 | | `pan_diameter` | `70` | 50 to 90 | | `base_width` | `150` | 120 to 200 | | `base_depth` | `100` | 80 to 140 | | `tilt` | `7` | -12 to 12 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | | `pin_diameter` | `3` | 3 to 5 | | `washer_gap` | `0.5` | 0.3 to 1 | ## Source ```kerf // Beam balance: a stable pivoted beam with two level, cord-suspended pans. // Glue the fork uprights into the foot and glue a rim onto each pan. // Fit the beam on a loose pivot bolt, then tie three equal cords per pan. // The tilt control demonstrates motion; it does not calculate equilibrium. /* [Balance] */ arm = 105; // [80:5:145] pivot_height = 165; // [125:5:220] drop = 95; // [65:5:130] pan_diameter = 70; // [50:5:90] base_width = 150; // [120:5:200] base_depth = 100; // [80:5:140] tilt = 7; // [-12:1:12] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] pin_diameter = 3; // [3:0.5:5] washer_gap = 0.5; // [0.3:0.1:1] /* [Hidden] */ $material = "plywood"; t = $thickness; r = pan_diameter / 2; front = -t / 2 - washer_gap; back = t / 2 + washer_gap + t; assert(abs(tilt) <= 12, "Tilt must stay between -12 and 12 degrees"); assert(pivot_height - arm * sin(12) - drop > 2 * t + 10, "Raise the pivot or shorten the suspension"); assert(arm * cos(12) > r + 22, "Leave room between the pans and the fork"); assert(base_depth > pan_diameter + t + 12, "Widen the foot beneath the pans"); function end(s) = [s * arm * cos(tilt), 0, pivot_height + s * arm * sin(tilt)]; function hanger(s) = end(s) + [0, -t / 2 - 6, 0]; function pan(s) = hanger(s) - [0, 0, drop]; function tie(s, k) = pan(s) + [(r - 6) * cos(120 * k), (r - 6) * sin(120 * k), t]; step("Glue the fork uprights into the foot") { // The foot cuts the slots for the uprights' tabs part("Foot", at = [0, 0, 0], slots = ["Fork upright"], clearance = clearance) rect([base_width, base_depth], center = true, r = 6); part("Fork upright", qty = 2, at = [[0, front, t], [0, back, t]], rotate = [90, 0, 0]) difference() { union() { hull() { translate([-14, 0]) rect([28, 8]); translate([0, pivot_height - t]) circle(r = 12); } translate([-6, -t]) rect([12, t]); } translate([0, pivot_height - t]) circle(d = pin_diameter + 0.3); } } step("Hang the beam on a loose pivot bolt, with a hook at each end") { part("Balance beam", at = [0, t / 2, pivot_height], rotate = [90, -tilt, 0]) difference() { union() { hull() { for (x = [-arm, arm]) translate([x, 0]) circle(r = 9); } hull() { circle(r = 9); translate([0, -14]) circle(r = 10); } } circle(d = pin_diameter + 0.3); for (x = [-arm, arm]) translate([x, 0]) circle(d = 4); } hardware("steel", [0, front - t - 1, pivot_height], [0, back + 1, pivot_height], pin_diameter, label = "pivot bolt"); for (s = [-1, 1]) hardware("steel", hanger(s), end(s) + [0, t / 2 + 1, 0], 3, label = "hook"); } step("Glue a rim on each pan and tie it on with three equal cords") for (s = [-1, 1]) { part(str(s < 0 ? "Left" : "Right", " pan floor"), at = pan(s)) difference() { circle(r = r); for (k = [0:2]) translate([(r - 6) * cos(120 * k), (r - 6) * sin(120 * k)]) circle(d = 2.6); } part(str(s < 0 ? "Left" : "Right", " pan rim"), glue = str(s < 0 ? "Left" : "Right", " pan floor")) difference() { circle(r = r); circle(r = r - 4); } for (k = [0:2]) hardware("cord", hanger(s), tie(s, k), 1.2, sides = 12); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/sector-stand/ # Sector-lock tilting stand Sector stand: a tilting deck on two trunnions, locked through curved slots. Glue the fixed cheeks into the foot, and the moving flanges and front lip into the deck. Bolt each flange to its cheek at the pivot and arc slot. Loosen both arc bolts before adjusting, then tighten them again. This design has 7 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=sector-stand). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/sector-stand/sector-stand.kerf) - [SVG](https://kerfscript.mylasertools.com/models/sector-stand/sector-stand.svg) - [DXF](https://kerfscript.mylasertools.com/models/sector-stand/sector-stand.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 204 × 190 mm | | Left fixed sector | 1 | 190 × 152.9 mm | | Right fixed sector | 1 | 190 × 152.9 mm | | Left moving flange | 1 | 140 × 23 mm | | Right moving flange | 1 | 140 × 23 mm | | Tilting deck | 1 | 180 × 130 mm | | Front lip | 1 | 180 × 18 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `180` | 130 to 260 | | `depth` | `130` | 110 to 190 | | `base_depth` | `190` | 165 to 250 | | `pivot_height` | `65` | 55 to 90 | | `radius` | `78` | 65 to 100 | | `angle` | `35` | 10 to 65 | | `lip_height` | `18` | 14 to 25 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | | `bolt_diameter` | `4` | 3 to 5 | | `washer_gap` | `0.6` | 0.3 to 1 | ## Source ```kerf // Sector stand: a tilting deck on two trunnions, locked through curved slots. // Glue the fixed cheeks into the foot, and the moving flanges and front lip // into the deck. Bolt each flange to its cheek at the pivot and arc slot. // Loosen both arc bolts before adjusting, then tighten them again. use /* [Stand] */ width = 180; // [130:5:260] depth = 130; // [110:5:190] base_depth = 190; // [165:5:250] pivot_height = 65; // [55:5:90] radius = 78; // [65:1:100] angle = 35; // [10:1:65] lip_height = 18; // [14:1:25] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] bolt_diameter = 4; // [3:0.5:5] washer_gap = 0.6; // [0.3:0.1:1] /* [Hidden] */ $material = "plywood"; t = $thickness; py = 25; v = 10; assert(angle >= 10 && angle <= 65, "Keep the deck within the arc-slot limits"); assert(radius < depth - 20, "Keep the clamp pivot inside the flange"); assert(base_depth > py + radius + 18, "Lengthen the foot under the arc cheeks"); assert(pivot_height > 2 * t + 24, "Raise the deck above the foot"); function arc(a, r = radius) = [py + r * cos(a), pivot_height - t + r * sin(a)]; function moving(u, z = 0) = [0, py + u * cos(angle) - z * sin(angle), pivot_height + u * sin(angle) + z * cos(angle)]; function fixed_x(s) = s < 0 ? -width / 2 - washer_gap - t : width / 2 + washer_gap; function flange_x(s) = s < 0 ? -width / 2 + 4 : width / 2 - 4 - t; step("Glue the fixed cheeks into the foot") { // The foot cuts the slots for the cheeks' tabs part("Foot", at = [0, 0, 0], slots = ["Left fixed sector", "Right fixed sector"], clearance = clearance) translate([-(width + 4 * t + 12) / 2, 0]) rect([width + 4 * t + 12, base_depth], r = 5); for (s = [-1, 1]) part(str(s < 0 ? "Left" : "Right", " fixed sector"), at = [fixed_x(s), 0, t], rotate = [90, 0, 90]) difference() { union() { hull() { rect([base_depth, 10]); for (a = [0:5:75]) translate(arc(a, radius + 12)) circle(r = 1); translate([py, pivot_height - t]) circle(r = 12); } for (y = [40, base_depth - 40]) translate([y - 10, -t]) rect([20, t]); } translate([py, pivot_height - t]) circle(d = bolt_diameter + 0.3); union() { polygon(concat([for (a = [10:1:65]) arc(a, radius + (bolt_diameter + 0.5) / 2)], [for (a = [65:-1:10]) arc(a, radius - (bolt_diameter + 0.5) / 2)])); for (a = [10, 65]) translate(arc(a)) circle(d = bolt_diameter + 0.5); } } } step("Glue the moving flanges and the front lip into the deck") { for (s = [-1, 1]) part(str(s < 0 ? "Left" : "Right", " moving flange"), at = [flange_x(s), py, pivot_height], rotate = [90, -angle, 90]) difference() { union() { hull() { circle(r = 10); translate([depth - 5, 0]) rect([10, 20], center = true); } for (u = [depth / 4, 3 * depth / 4]) translate([u - 6, v]) rect([12, t]); } for (u = [0, radius]) translate([u, 0]) circle(d = bolt_diameter + 0.3); } // The deck cuts the slots for the flanges' tabs, at the deck's angle part("Tilting deck", at = moving(0, v) + [-width / 2, 0, 0], rotate = [angle, 0, 0], slots = ["Left moving flange", "Right moving flange"], clearance = clearance) finger_rect([width, depth], ["out", "flat", "flat", "flat"]); part("Front lip", at = moving(t, v) + [-width / 2, 0, 0], rotate = [90 + angle, 0, 0]) finger_rect([width, lip_height], ["in", "flat", "flat", "flat"]); } step("Bolt each flange to its cheek at the pivot and through the arc slot") for (s = [-1, 1], u = [0, radius]) let (p = moving(u), lo = min(fixed_x(s), flange_x(s)) - 1, hi = max(fixed_x(s), flange_x(s)) + t + 1) hardware("steel", [lo, p[1], p[2]], [hi, p[1], p[2]], bolt_diameter, label = u == 0 ? "pivot bolt" : "clamp bolt"); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/screw-press/ # Laminated screw press Screw press: laminated platens and hand knobs with captive hex nuts. Clamp the lower platen onto four threaded rods using washers and nuts. Glue the knob laminations around measured hex nuts without gluing the threads. Put washers beneath the knobs and turn them evenly to press. A small craft press for paper and light glue-ups, not for heavy pressing. This design has 22 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=screw-press). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/screw-press/screw-press.kerf) - [SVG](https://kerfscript.mylasertools.com/models/screw-press/screw-press.svg) - [DXF](https://kerfscript.mylasertools.com/models/screw-press/screw-press.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Lower platen lamination | 3 | 180 × 140 mm | | Upper platen lamination | 3 | 180 × 140 mm | | Knob 1 cover | 2 | 28 × 25.3 mm | | Knob 1 nut pocket | 2 | 28 × 25.3 mm | | Knob 2 cover | 2 | 28 × 25.3 mm | | Knob 2 nut pocket | 2 | 28 × 25.3 mm | | Knob 3 cover | 2 | 28 × 25.3 mm | | Knob 3 nut pocket | 2 | 28 × 25.3 mm | | Knob 4 cover | 2 | 28 × 25.3 mm | | Knob 4 nut pocket | 2 | 28 × 25.3 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `180` | 140 to 260 | | `depth` | `140` | 110 to 200 | | `platen_layers` | `3` | 2 to 5 | | `margin` | `20` | 17 to 28 | | `rod_height` | `120` | 90 to 180 | | `opening` | `50` | 15 to 90 | | `rod_diameter` | `6` | 4 to 8 | | `thread_pitch` | `1` | 0.7 to 1.5 | | `nut_flats` | `10` | 7 to 14 | | `nut_height` | `5` | 3 to 7 | | `nut_clearance` | `0.25` | 0.1 to 0.5 | | `washer_height` | `1` | 0.5 to 2 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Screw press: laminated platens and hand knobs with captive hex nuts. // Clamp the lower platen onto four threaded rods using washers and nuts. // Glue the knob laminations around measured hex nuts without gluing the // threads. Put washers beneath the knobs and turn them evenly to press. // A small craft press for paper and light glue-ups, not for heavy pressing. /* [Platens] */ width = 180; // [140:5:260] depth = 140; // [110:5:200] platen_layers = 3; // [2:1:5] margin = 20; // [17:1:28] rod_height = 120; // [90:5:180] opening = 50; // [15:1:90] /* [Measured hardware] */ rod_diameter = 6; // [4:1:8] thread_pitch = 1; // [0.7:0.05:1.5] nut_flats = 10; // [7:0.5:14] nut_height = 5; // [3:0.5:7] nut_clearance = 0.25; // [0.1:0.05:0.5] washer_height = 1; // [0.5:0.5:2] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; ph = platen_layers * t; middle = ceil(nut_height / t); kh = (middle + 2) * t; top = ph + opening; turn = -360 * opening / thread_pitch; corners = [for (x = [margin, width - margin], y = [margin, depth - margin]) [x, y]]; assert(opening >= 15 && thread_pitch > 0, "Use a positive thread pitch and leave room above the base nuts"); assert(2 * ph + opening + washer_height + kh + 3 < rod_height, "Use longer rods or reduce the opening"); assert(margin > 15, "Keep knobs inside the platen edge"); assert(nut_flats > rod_diameter + 2 && nut_flats < 18, "Measure a suitable hex nut"); assert(width - 2 * margin > 40 && depth - 2 * margin > 35, "Leave a useful pressing area between rods"); module platen() difference() { rect([width, depth], r = 7); for (p = corners) translate(p) circle(d = rod_diameter + 0.6); } module knob() union() { circle(r = 11); for (a = [0:60:300]) translate([10 * cos(a), 10 * sin(a)]) circle(r = 4); } step("Glue up the lower platen and clamp it onto the four rods with nuts") { part("Lower platen lamination", stack = platen_layers, at = [0, 0, 0]) platen(); for (p = corners) { hardware("steel", [p[0], p[1], -nut_height], [p[0], p[1], rod_height], rod_diameter, label = "threaded rod"); for (z = [-nut_height, ph]) hardware("steel", [p[0], p[1], z], [p[0], p[1], z + nut_height], nut_flats / cos(30), sides = 6, label = "hex nut"); } } step("Glue up the upper platen and drop it over the rods") part("Upper platen lamination", stack = platen_layers, at = [0, 0, top]) platen(); step("Glue each knob's layers round a hex nut, without glue on the thread, and screw them down on washers") for (k = [0:3]) { p = corners[k]; z = top + ph + washer_height; part(str("Knob ", k + 1, " cover"), qty = 2, at = [[p[0], p[1], z], [p[0], p[1], z + (middle + 1) * t]], rotate = [0, 0, turn]) difference() { knob(); circle(d = rod_diameter + 0.6); } part(str("Knob ", k + 1, " nut pocket"), stack = middle, at = [p[0], p[1], z + t], rotate = [0, 0, turn]) difference() { knob(); circle(d = (nut_flats + nut_clearance) / cos(30), $fn = 6); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/wheel-calendar/ # Independent wheel calendar Wheel calendar: three independent numeric/date wheels on a tabbed stand. Glue the backboard into the foot, then bolt on the wheels with washers. Read the value beneath each fixed pointer. Set weekday, month and day by hand; there is no automatic month-length or leap-year mechanism. This design has 5 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=wheel-calendar). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/wheel-calendar/wheel-calendar.kerf) - [SVG](https://kerfscript.mylasertools.com/models/wheel-calendar/wheel-calendar.svg) - [DXF](https://kerfscript.mylasertools.com/models/wheel-calendar/wheel-calendar.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Foot | 1 | 410 × 80 mm | | Backboard | 1 | 398 × 185 mm | | DAY wheel | 1 | 150 × 150 mm | | MONTH wheel | 1 | 96 × 96 mm | | WEEK wheel | 1 | 80 × 80 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `day_radius` | `75` | 65 to 95 | | `month_radius` | `48` | 40 to 60 | | `week_radius` | `40` | 35 to 50 | | `day` | `17` | 1 to 31 | | `month` | `11` | 1 to 12 | | `weekday` | `1` | 1 to 7 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | | `clearance` | `0.15` | 0 to 0.4 | | `pin_diameter` | `3` | 3 to 5 | | `washer_gap` | `0.6` | 0.3 to 1 | ## Source ```kerf // Wheel calendar: three independent numeric/date wheels on a tabbed stand. // Glue the backboard into the foot, then bolt on the wheels with washers. // Read the value beneath each fixed pointer. Set weekday, month and day by // hand; there is no automatic month-length or leap-year mechanism. use /* [Wheels] */ day_radius = 75; // [65:5:95] month_radius = 48; // [40:2:60] week_radius = 40; // [35:1:50] day = 17; // [1:1:31] month = 11; // [1:1:12] weekday = 1; // [1:1:7] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] clearance = 0.15; // [0:0.05:0.4] pin_diameter = 3; // [3:0.5:5] washer_gap = 0.6; // [0.3:0.1:1] /* [Hidden] */ $material = "plywood"; t = $thickness; m = 12; radii = [day_radius, month_radius, week_radius]; values = [day, month, weekday]; counts = [31, 12, 7]; titles = ["DAY", "MONTH", "WEEK"]; weeks = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]; centres = [day_radius + m, 2 * day_radius + 3 * m + month_radius, 2 * (day_radius + month_radius) + 5 * m + week_radius]; w = 2 * sum(radii) + 6 * m; h = 2 * max(radii) + 2 * m + 8; assert(day_radius >= 65, "Use a larger day wheel for readable numbers"); step("Glue the backboard into the foot") { // The foot cuts the slots for the backboard's tabs part("Foot", at = [-6, -40, 0], slots = ["Backboard"], clearance = clearance) rect([w + 12, 80], r = 5); part("Backboard", at = [0, t, t], rotate = [90, 0, 0]) { difference() { union() { rect([w, h]); tabs(w, count = 4, width = 20); } for (i = [0:2]) translate([centres[i], radii[i] + m]) circle(d = pin_diameter + 0.3); } score() for (i = [0:2]) { x = centres[i]; z = 2 * radii[i] + m; polyline([[x - 3, z + 9], [x, z + 3], [x + 3, z + 9]]); translate([x, z + 11]) text(titles[i], size = 4, halign = "center"); } } } step("Bolt on the wheels with a washer behind each") for (i = [0:2]) { part(str(titles[i], " wheel"), at = [centres[i], -washer_gap, radii[i] + m + t], rotate = [90, -(values[i] - 1) * 360 / counts[i], 0]) { difference() { circle(r = radii[i]); circle(d = pin_diameter + 0.3); } score() for (j = [0:counts[i] - 1]) rotate(90 - j * 360 / counts[i]) translate([radii[i] - 8, 0]) rotate(-90) text(i == 2 ? weeks[j] : str(j + 1), size = i == 0 ? 4.4 : 4.5, halign = "center", valign = "center"); } hardware("steel", [centres[i], -washer_gap - t - 2, radii[i] + m + t], [centres[i], t + 2, radii[i] + m + t], pin_diameter, label = "bolt"); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/slat-roll/ # Cord-laced roll-up desk mat Slat roll: a flexible desk mat assembled from repeated rigid wooden slats. Align the slats with the specified gap and lace two cords through their paired holes. Leave the spans between slats free to hinge; knot the ends. The curl control shows a bending pose, not an elastic-material simulation. This design has 24 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=slat-roll). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/slat-roll/slat-roll.kerf) - [SVG](https://kerfscript.mylasertools.com/models/slat-roll/slat-roll.svg) - [DXF](https://kerfscript.mylasertools.com/models/slat-roll/slat-roll.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Laced slat | 24 | 230 × 10 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `230` | 150 to 350 | | `slats` | `24` | 12 to 36 | | `slat_width` | `10` | 8 to 16 | | `gap` | `1.5` | 1 to 3 | | `cord_diameter` | `1.5` | 1 to 2 | | `curl` | `0.7` | 0 to 1 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Slat roll: a flexible desk mat assembled from repeated rigid wooden slats. // Align the slats with the specified gap and lace two cords through their // paired holes. Leave the spans between slats free to hinge; knot the ends. // The curl control shows a bending pose, not an elastic-material simulation. /* [Mat] */ width = 230; // [150:5:350] slats = 24; // [12:1:36] slat_width = 10; // [8:1:16] gap = 1.5; // [1:0.1:3] cord_diameter = 1.5; // [1:0.1:2] curl = 0.7; // [0:0.05:1] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; t = $thickness; pitch = slat_width + gap; arc_length = (slats - 1) * pitch; radius = curl == 0 ? 1000000 : arc_length / (curl * 270 * PI / 180); assert(curl >= 0 && curl <= 1, "Curl must stay between the flat and maximum rolled poses"); assert(gap > 2 * t * sin(135 / (slats - 1)) + 0.15, "Increase gaps or slat count for the curled thickness"); assert(slat_width > 2 * cord_diameter + 4, "Widen the slats for the lacing holes"); function a(i) = curl * 270 * i / (slats - 1); function centre(i) = curl == 0 ? [0, i * pitch, 0] : [0, radius * sin(a(i)), radius * (1 - cos(a(i)))]; function at_hole(i, x, v, z) = centre(i) + [x, v * cos(a(i)) - z * sin(a(i)), v * sin(a(i)) + z * cos(a(i))]; part("Laced slat", qty = slats, at = [for (i = [0:slats - 1]) centre(i)], rotate = [for (i = [0:slats - 1]) [a(i), 0, 0]]) difference() { rect([width, slat_width], center = true, r = 2); for (x = [-0.32 * width, 0.32 * width], y = [-slat_width / 4, slat_width / 4]) translate([x, y]) circle(d = cord_diameter + 0.8); } // Each cord runs across the back of a slat, over the gap on the front, // and through both holes for (i = [0:slats - 1], x = [-0.32 * width, 0.32 * width]) { hardware("cord", at_hole(i, x, -slat_width / 4, -0.3), at_hole(i, x, slat_width / 4, -0.3), cord_diameter, sides = 12); if (i < slats - 1) hardware("cord", at_hole(i, x, slat_width / 4, t + 0.3), at_hole(i + 1, x, -slat_width / 4, t + 0.3), cord_diameter, sides = 12); for (v = [-slat_width / 4, slat_width / 4]) hardware("cord", at_hole(i, x, v, -0.3), at_hole(i, x, v, t + 0.3), cord_diameter, sides = 12); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/lock-box/ # Lock box with a key Lock box: a finger-jointed box whose lid locks with a key. The lock is three layers glued on the front: turn the key half a turn and its cam throws a bolt up into the keeper, a slotted plate under the lid's front edge, and the lid cannot lift. The lid's insert keeps it square on the walls. Made with box() from kerf/box and cam_lock() from kerf/lock. This design has 15 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=lock-box). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/lock-box/lock-box.kerf) - [lock-box-sheet-1.svg](https://kerfscript.mylasertools.com/models/lock-box/lock-box-sheet-1.svg) - [lock-box-sheet-1.dxf](https://kerfscript.mylasertools.com/models/lock-box/lock-box-sheet-1.dxf) - [lock-box-sheet-2.svg](https://kerfscript.mylasertools.com/models/lock-box/lock-box-sheet-2.svg) - [lock-box-sheet-2.dxf](https://kerfscript.mylasertools.com/models/lock-box/lock-box-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Box bottom | 1 | 160 × 110 mm | | Box front | 1 | 160 × 80 mm | | Box back | 1 | 160 × 80 mm | | Box left | 1 | 110 × 80 mm | | Box right | 1 | 110 × 80 mm | | Lock base | 1 | 35.8 × 35.3 mm | | Lock frame | 1 | 35.8 × 35.3 mm | | Lock plate | 1 | 35.8 × 35.3 mm | | Lock cam hub | 1 | 11 × 11 mm | | Lock cam | 1 | 17 × 17 mm | | Lock bolt | 1 | 29.8 × 30.3 mm | | Lid | 1 | 160 × 120 mm | | Lid insert | 1 | 153.4 × 103.4 mm | | Keeper | 1 | 35.8 × 13.5 mm | | Key | 1 | 20.4 × 32.2 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `160` | 110 to 300 | | `depth` | `110` | 80 to 220 | | `height` | `80` | 60 to 180 | | `throw` | `7` | 5 to 10 | | `locked` | `true` | on or off | | `$thickness` | `3` | 2.5 to 4 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Lock box: a finger-jointed box whose lid locks with a key. The lock is // three layers glued on the front: turn the key half a turn and its cam // throws a bolt up into the keeper, a slotted plate under the lid's front // edge, and the lid cannot lift. The lid's insert keeps it square on the // walls. Made with box() from kerf/box and cam_lock() from kerf/lock. /* [Box] */ // Outside width width = 160; // [110:5:300] // Outside depth depth = 110; // [80:5:220] // Height of the walls height = 80; // [60:5:180] // How far the bolt moves throw = 7; // [5:0.5:10] // Show the lock locked locked = true; /* [Material] */ $thickness = 3; // [2.5:0.5:4] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ use use $fn = 48; $material = "plywood"; t = $thickness; b = lock_bounds(throw); nose = lock_nose(throw); // The lock's centre on the front, below the lid by room for the keeper: // the bolt's nose and 4 mm of keeper above it zc = height - b[3] - (throw + 4); assert(zc + b[1] > 12, "Make the box taller for the lock"); // The lid overhangs the front by the lock's three layers over = 3 * t + 1; step("Make the box: the bottom, then the walls") box([width, depth, height], name = "Box"); // The lock lies on the front, its bolt moving up step("Glue the lock on the front: the base and the cam hub, the frame, the cam and the bolt, then the plate") place(at = [width / 2, 0, zc], rotate = orient([1, 0, 0], [0, -1, 0])) material(name = "walnut") cam_lock(throw, locked = locked, name = "Lock", key = false); step("Glue the insert and the keeper under the lid") { part("Lid", at = [0, -over, height], slots = ["Keeper"]) rect([width, depth + over], r = 3); part("Lid insert", glue = "Lid", face = "bottom", at = [t + 0.3, over + t + 0.3, 0]) rect([width - 2 * t - 0.6, depth - 2 * t - 0.6], r = 1); // The keeper hangs in the plane of the lock's frame, above it, with a // notch for the bolt's nose and tabs up into the lid kh = height - (zc + b[3] + 0.5); part("Keeper", at = [width / 2 + b[0], -t, zc + b[3] + 0.5], rotate = [90, 0, 0]) difference() { union() { rect([b[2] - b[0], kh]); for (x = [4, b[2] - b[0] - 12]) translate([x, kh - 0.01]) rect([8, t + 0.01]); } translate([(b[2] - b[0] - nose[0]) / 2 - 0.4, -1]) rect([nose[0] + 0.8, nose[1] - b[3] + 1.5]); } } step("Turn the key to lock it") place(at = [width / 2, 0, zc], rotate = orient([1, 0, 0], [0, -1, 0])) material(name = "walnut") part("Key", at = [0, t / 2, 0], rotate = [90, 0, 0]) lock_key(); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/ring-vase/ # Stacked ring vase Ring vase: a vase of stacked rings threaded on two crossed core plates, its outline a smooth curve from the foot to the lip. Rings of the same size are one part, so the cut list stays short. The base cuts its own slots for the core's feet. A glass test tube or a dried posy goes inside. Made with ring_stack() from kerf/rings. This design has 53 parts on 2 sheets. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=ring-vase). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/ring-vase/ring-vase.kerf) - [ring-vase-sheet-1.svg](https://kerfscript.mylasertools.com/models/ring-vase/ring-vase-sheet-1.svg) - [ring-vase-sheet-1.dxf](https://kerfscript.mylasertools.com/models/ring-vase/ring-vase-sheet-1.dxf) - [ring-vase-sheet-2.svg](https://kerfscript.mylasertools.com/models/ring-vase/ring-vase-sheet-2.svg) - [ring-vase-sheet-2.dxf](https://kerfscript.mylasertools.com/models/ring-vase/ring-vase-sheet-2.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Base | 1 | 79 × 79 mm | | Vase 63 | 2 | 63 × 63 mm | | Vase 65 | 2 | 65 × 65 mm | | Vase 67 | 1 | 67 × 67 mm | | Vase 69 | 1 | 69 × 69 mm | | Vase 71 | 1 | 71 × 71 mm | | Vase 73 | 1 | 73 × 73 mm | | Vase 75 | 1 | 75 × 75 mm | | Vase 78 | 2 | 78 × 78 mm | | Vase 80 | 2 | 80 × 80 mm | | Vase 82 | 2 | 82 × 82 mm | | Vase 84 | 2 | 84 × 84 mm | | Vase 85 | 2 | 85 × 85 mm | | Vase 87 | 2 | 87 × 87 mm | | Vase 88 | 2 | 88 × 88 mm | | Vase 89 | 2 | 89 × 89 mm | | Vase 90 | 4 | 90 × 90 mm | | Vase 76 | 1 | 76 × 76 mm | | Vase 74 | 1 | 74 × 74 mm | | Vase 72 | 1 | 72 × 72 mm | | Vase 70 | 1 | 70 × 70 mm | | Vase 68 | 1 | 68 × 68 mm | | Vase 61 | 1 | 61 × 61 mm | | Vase 59 | 1 | 59 × 59 mm | | Vase 57 | 1 | 57 × 57 mm | | Vase 55 | 2 | 55 × 55 mm | | Vase 53 | 2 | 53 × 53 mm | | Vase 51 | 2 | 51 × 51 mm | | Vase 50 | 2 | 50 × 50 mm | | Vase 49 | 3 | 49 × 49 mm | | Vase 48 | 1 | 48 × 48 mm | | Vase 56 | 1 | 56 × 56 mm | | Vase core A | 1 | 33.7 × 153 mm | | Vase core B | 1 | 33.7 × 153 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `height` | `150` | 90 to 260 | | `belly` | `90` | 60 to 160 | | `belly_at` | `0.35` | 0.2 to 0.7 | | `neck` | `50` | 40 to 120 | | `hole` | `34` | 24 to 60 | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Ring vase: a vase of stacked rings threaded on two crossed core plates, // its outline a smooth curve from the foot to the lip. Rings of the same // size are one part, so the cut list stays short. The base cuts its own slots // for the core's feet. A glass test tube or a dried posy goes inside. Made // with ring_stack() from kerf/rings. /* [Vase] */ // Height height = 150; // [90:5:260] // Widest, at the belly belly = 90; // [60:5:160] // Where the belly is, from the foot up (0 to 1) belly_at = 0.35; // [0.2:0.05:0.7] // Neck and lip neck = 50; // [40:5:120] // The hole through every ring, for a glass tube hole = 34; // [24:1:60] /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ use $fn = 72; $material = "plywood"; t = $thickness; n = max(6, round(height / t)); // The outline through the foot, the belly, the neck and the lip profile = spline_points([[0, belly * 0.7], [belly_at * n, belly], [0.8 * n, neck], [n - 1, neck * 1.12]], 8); function width_at(k) = lookup(k, profile); assert(neck > hole + 8, "Make the neck wider or the hole smaller"); step("Slide the two core plates together and stand them in the base") part("Base", at = [0, 0, -t], slots = ["Vase core A", "Vase core B"]) circle(d = belly * 0.7 + 16); // Each ring rounded to the nearest millimetre, so alike rings share a part step("Thread the rings over the core, biggest at the belly, gluing each to the one below") ring_stack([for (k = [0:n - 1]) round(width_at(k))], hole, name = "Vase"); ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/world-map/ # World map sign World map sign: the world's land engraved on a panel, a heart where a place is (home, or where two people met), its name below, and a message on the back. Two keyholes hang it on the wall. Made with world_map() and geo_point() from kerf/geo, a symbol from kerf/symbols and keyholes() from kerf/hanging. This design has 1 part on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=world-map). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/world-map/world-map.kerf) - [SVG](https://kerfscript.mylasertools.com/models/world-map/world-map.svg) - [DXF](https://kerfscript.mylasertools.com/models/world-map/world-map.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Map | 1 | 300 × 172.3 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `width` | `300` | 180 to 600 | | `latitude` | `48.85` | -60 to 80 | | `longitude` | `2.35` | -180 to 180 | | `place` | `"PARIS"` | | | `marker` | `"heart"` | Heart, Star, Home | | `message` | `"WHERE IT ALL BEGAN"` | | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // World map sign: the world's land engraved on a panel, a heart where a // place is (home, or where two people met), its name below, and a message // on the back. Two keyholes hang it on the wall. Made with world_map() and // geo_point() from kerf/geo, a symbol from kerf/symbols and keyholes() from // kerf/hanging. /* [Map] */ // Panel width width = 300; // [180:10:600] // The place to mark, latitude and longitude latitude = 48.85; // [-60:0.01:80] longitude = 2.35; // [-180:0.01:180] // Its name place = "PARIS"; // 20 // The marker marker = "heart"; // [heart:Heart, star:Star, home:Home] // Message engraved on the back message = "WHERE IT ALL BEGAN"; // 40 /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ use use use $fn = 48; $material = "plywood"; w = width; m = w * 0.04; // The map is twice as wide as tall, south of 60 degrees left out; a band // above it for the keyholes, and one below for the name map_w = w - 2 * m; map_h = map_w * 140 / 360; top = 26; h = top + map_h + m + w * 0.09; mid = [w / 2, h - top - map_w * 80 / 360]; here = mid + geo_point(latitude, longitude, map_w); size = w * 0.035; part("Map", at = [0, 0, 0], rotate = [90, 0, 0]) { // The marker cut through, so the wall shows behind it difference() { keyholes(w * 0.7, at = [w / 2, h - 7]) rect([w, h], r = 6); translate(here) symbol(marker, size); } engrave() { translate(mid) intersection() { world_map(map_w); translate([-map_w / 2, -60 * map_w / 360]) rect([map_w, map_h]); } translate([w / 2, m + w * 0.035]) text(place, size = w * 0.05, fit = w * 0.6, halign = "center", valign = "center"); } // A ring scored round the marker score() translate(here) offset(r = 1.5) symbol(marker, size); engrave(side = "back") translate([w / 2, h / 2]) text(message, size = w * 0.035, fit = w * 0.8, halign = "center", valign = "center"); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/pen-tray/ # Bent pen tray Pen tray: a desk tray whose round-bottomed trough is one strip of plywood bent by a living hinge, between two end plates. The strip's tabs go through slots in the ends, which the ends cut for themselves from where the strip passes through them. Made with flex_strip() from kerf/hinge. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=pen-tray). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/pen-tray/pen-tray.kerf) - [SVG](https://kerfscript.mylasertools.com/models/pen-tray/pen-tray.svg) - [DXF](https://kerfscript.mylasertools.com/models/pen-tray/pen-tray.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | End A | 1 | 84 × 84 mm | | Trough | 1 | 154.2 × 206 mm | | End B | 1 | 84 × 84 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `length` | `200` | 120 to 400 | | `width` | `60` | 40 to 120 | | `depth` | `60` | 40 to 150 | | `label` | `"PENS"` | | | `$thickness` | `3` | 2 to 4 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Pen tray: a desk tray whose round-bottomed trough is one strip of // plywood bent by a living hinge, between two end plates. The strip's tabs // go through slots in the ends, which the ends cut for themselves from // where the strip passes through them. Made with flex_strip() from // kerf/hinge. /* [Tray] */ // Length, end to end inside length = 200; // [120:10:400] // Width of the trough inside width = 60; // [40:5:120] // Depth of the trough depth = 60; // [40:5:150] // Text on the ends; leave empty for none label = "PENS"; // 12 /* [Material] */ $thickness = 3; // [2:0.5:4] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ use $fn = 72; $material = "plywood"; t = $thickness; r = width / 2; assert(depth - r >= 22, "Make the trough deeper, for straight sides with tabs"); // The strip's middle line in an end plate: down one side, round the // bottom, up the other path = concat([[0, depth]], [for (a = [180:5:360]) [r, r] + r * [cos(a), sin(a)]], [[width, depth]]); // An end plate: round the trough with a rim, on a flat foot module end_plate() { offset(r = 4) offset(delta = 8) polygon(concat(path, [[width, depth + 0.01], [0, depth + 0.01]])); translate([-12, -12]) rect([width + 24, r + 12]); } module end_label() if (len(label) > 0) engrave() translate([r, -5]) text(label, size = 7, fit = width, halign = "center", valign = "center"); step("Push the strip's tabs into one end, bending it round the bottom") { part("End A", at = [0, 0, 0], rotate = [90, 0, 0], slots = ["Trough"]) { end_plate(); end_label(); } place(rotate = [90, 0, 0]) flex_strip(path, length, name = "Trough"); } // The far end turned round to face out, its label reading from outside step("Press the other end on") part("End B", at = [width, length, 0], rotate = [90, 0, 180], slots = ["Trough"]) { end_plate(); end_label(); } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/models/symbol-tags/ # Symbol name tags Symbol tags: key rings or bag tags, each with a pictogram and a name. The symbols come by name from the 867 in kerf/symbols; list them with symbol_names(), or one category with symbol_names("animals"). The names fit their tag, and the hole has a ring round it for the key ring. This design has 3 parts on 1 sheet. Customize it in the [playground](https://kerfscript.mylasertools.com/playground/?model=symbol-tags). ## Downloads - [KerfScript source](https://kerfscript.mylasertools.com/models/symbol-tags/symbol-tags.kerf) - [SVG](https://kerfscript.mylasertools.com/models/symbol-tags/symbol-tags.svg) - [DXF](https://kerfscript.mylasertools.com/models/symbol-tags/symbol-tags.dxf) ## Parts | Part | Quantity | Size | | --- | ---: | --- | | Tag MILO | 1 | 70.5 × 30 mm | | Tag LUNA | 1 | 70.5 × 30 mm | | Tag OSCAR | 1 | 70.5 × 30 mm | ## Settings | Name | Default | Range or choices | | --- | --- | --- | | `size` | `[70,30]` | 30 to 120 | | `cut_through` | `false` | on or off | | `$thickness` | `3` | 2 to 6 | | `$kerf` | `0.15` | 0 to 0.3 | ## Source ```kerf // Symbol tags: key rings or bag tags, each with a pictogram and a name. // The symbols come by name from the 867 in kerf/symbols; list them with // symbol_names(), or one category with symbol_names("animals"). The // names fit their tag, and the hole has a ring round it for the key ring. /* [Tags] */ // One tag for each name names = ["MILO", "LUNA", "OSCAR"]; // The symbol on each tag, by name symbols = ["paw", "cat", "dog"]; // Tag width and height size = [70, 30]; // [30:5:120] // Cut the symbols through, or engrave them cut_through = false; /* [Material] */ $thickness = 3; // [2:0.5:6] $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ use use $fn = 48; $material = "plywood"; w = size[0]; h = size[1]; s = h * 0.62; assert(len(symbols) >= len(names), "Give a symbol for every name"); for (i = [0:len(names) - 1]) part(str("Tag ", names[i]), at = [0, -i * (h + 10), 0]) { difference() { hanging_loop([5, h / 2], d = 5, wall = 3) rect([w, h], r = h * 0.3); if (cut_through) translate([14 + s / 2, h / 2]) symbol(symbols[i], s); } engrave() { if (!cut_through) translate([14 + s / 2, h / 2]) symbol(symbols[i], s); translate([(w + 14 + s) / 2 + 2, h / 2]) text(names[i], size = h * 0.4, fit = w - s - 22, halign = "center", valign = "center"); } } ``` Set power, speed and cut order in your laser software. See the [laser software guide](https://kerfscript.mylasertools.com/docs/compatibility/index.md). --- > Canonical HTML: https://kerfscript.mylasertools.com/playground/ # KerfScript playground The [interactive playground](https://kerfscript.mylasertools.com/playground/) runs in your browser. Start from a model, write KerfScript or import an SVG drawing, change settings and download .kerf, SVG or DXF files. No installation is needed. - [Choose a model](https://kerfscript.mylasertools.com/models/index.md) - [Getting started](https://kerfscript.mylasertools.com/docs/getting-started/index.md) - [Laser software notes](https://kerfscript.mylasertools.com/docs/compatibility/index.md) --- > Canonical HTML: https://kerfscript.mylasertools.com/licence/ # Licensing Copyright (c) 2026 MyLaserTools. **KerfScript is free to use, including for commercial work.** Write programs, cut and sell what you make, and sell your `.kerf` designs and the files they produce. You don't need to ask us for any of that. KerfScript is dual licensed. You can use it under **either**: 1. **The GNU Affero General Public License, version 3 only** (`AGPL-3.0-only`, full text in [LICENSE](https://github.com/mylasertools/kerfscript/blob/main/LICENSE)), together with the additional permission below, or 2. **A commercial licence** from MyLaserTools, which is **free**. ## Free commercial licence Want to build KerfScript into a closed-source app, plugin, machine, firmware or online service? The commercial licence costs nothing. Just email hi@mylasertools.com with your name or company and what you are building, and we will send it to you. ## Additional permission: your designs are yours As an additional permission under section 7 of the AGPL, MyLaserTools grants that: 1. KerfScript programs you write (`.kerf` files), including programs that `use` or `include` the KerfScript standard library in `lib/`, are not considered a work based on KerfScript, and you may license and sell them on any terms you choose. 2. Files that KerfScript produces from your programs (SVG, DXF, JSON and others) belong to you and are not covered by the AGPL. This permission covers your programs and their output only. It does not cover the KerfScript interpreter, its tools, or modified copies of the standard library, which remain under the AGPL unless you have the commercial licence. ## What the AGPL means for you Under the AGPL you can: - write KerfScript programs and cut, sell or share what you make with them - run the interpreter, including a modified copy, privately or in internal tools without publishing those private changes - use an unmodified copy on a server - distribute software that includes KerfScript, as long as you share its source code under the AGPL If you would rather not share your source code, ask for the free commercial licence. ## Trademarks "KerfScript" and the KerfScript logo are trademarks of MyLaserTools and are not licensed by the AGPL or by the commercial licence unless that licence says so. See [TRADEMARKS.md](https://github.com/mylasertools/kerfscript/blob/main/TRADEMARKS.md). ## Contributions MyLaserTools accepts contributions under the [Contributor License Agreement](https://github.com/mylasertools/kerfscript/blob/main/CLA.md), which lets MyLaserTools keep offering KerfScript under both licences. --- # Trademark policy "KerfScript", the `.kerf` file format name used as a product name, and the KerfScript logo are trademarks of MyLaserTools. The software licences (AGPL-3.0-only and the free commercial licence) cover the code. They do not grant rights to the name or logo. This policy identifies the official KerfScript project while allowing others to describe tools that work with the language. MyLaserTools controls the official language versions and releases. ## You may, without asking - say your design, tutorial, course, video or library is written **in** KerfScript or works **with** KerfScript - describe an independent editor, implementation or service as compatible with a specific KerfScript language version when the claim is accurate, its limitations are clear and you do not imply MyLaserTools endorsement - use KerfScript in a descriptive phrase such as "an editor for KerfScript" alongside your own distinct product name - use the `.kerf` file extension for KerfScript programs - use the name to refer to this project and to unmodified copies of it - publish a fork for development, as long as it is clearly marked as a fork and does not use the name or logo as its own product name ## You need written permission to - name a product, service, company, domain or package "KerfScript" or anything confusingly similar (for example "KerfScript Pro", "kerfscript-cloud") - call your own implementation, editor or service "official" or imply that MyLaserTools endorses it - use the logo, other than to link to this project - distribute a modified interpreter under the KerfScript name ## Language specification The language reference ([LANGUAGE.md](https://kerfscript.mylasertools.com/docs/reference/index.md)) is published under Creative Commons Attribution 4.0 (CC BY 4.0). You may copy, translate and adapt it with attribution and a note describing your changes. Adapted references are not official KerfScript language versions. MyLaserTools publishes the official reference and decides which language changes it adopts. ## Contact hi@mylasertools.com