- Python 100%
Force an even finger count (n = 2 * round(h / 2 t_side), min 2) so the front/back slot pattern is the sides' pattern mirrored about mid-height. One finger Job per (height, t_side, tolerance) group then cuts all four walls of every drawer in one pack: sides with the grooved edge at X = 0, fronts and backs rotated half a turn about the stack normal (grooved edge at X = h). Run once per end by tipping the pack over about the panel-height axis. - finger_layout mirrors FreeCAD's half-away-from-zero round() - FingerJobResult loses kind; LumberjackFingers is a bool; G-code <Doc>_<group>_fingers_<h>mm.nc; op names Fingers_<h>mm_Slot<i>_<k> - summary spells out the pack orientation - README: "Milling the finger joints: the flipped double pack" procedure - tests: mirror property for several heights, eight-clone pack, flipped fronts/backs verified by probes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|---|---|---|
| templates | ||
| .gitignore | ||
| cam.py | ||
| CLAUDE.md | ||
| drawers.md | ||
| drawers.py | ||
| Init.py | ||
| InitGui.py | ||
| naming.py | ||
| nesting.py | ||
| panels.py | ||
| project.py | ||
| README.md | ||
| reload.py | ||
| sheetdraw.py | ||
| test_cam.py | ||
| test_cam_gui.py | ||
| test_rect.py | ||
Lumberjack Workbench
A FreeCAD workbench for woodworking and furniture design.
Overview
Lumberjack provides tools to streamline furniture design workflows in FreeCAD 1.1+, with a focus on parametric design and efficient project setup.
Features
Automatic Alias Synchronization
The workbench installs a document observer that watches the parameter spreadsheet (p) and automatically:
- Sets aliases on value cells when you type a parameter name
- Updates aliases when you rename parameters
- Clears aliases when you delete parameter names
- Validates alias names and warns about invalid characters
This means you never need to manually set aliases - just type the parameter name in column A and it becomes available as p.parameter_name in expressions.
New Project Command
Creates a new FreeCAD document with a pre-configured parameter spreadsheet:
| Column | Purpose |
|---|---|
| param (A) | Parameter name - automatically becomes the alias |
| value (B) | Parameter value - reference via p.<param_name> |
| description (C) | Human-readable description |
Create Panel Command
Creates furniture panels as PartDesign bodies with parametric dimensions:
- Name: Label for the body
- Thickness: Expression for pad length (default:
p.thickness) - Width: Expression for sketch width (default:
p.width) - Height: Expression for sketch height (default:
p.height)
The dialog uses FreeCAD's native expression input (Gui::QuantitySpinBox) with full auto-completion support. All expressions remain editable on the created objects.
Create Drawer Command
Creates a complete parametric drawer as a Std_Part (App::Part) containing PartDesign
bodies — two sides, a front, a back, a bottom, and an optional dedicated drawer front.
The drawer is placed inside the currently active container (if any).
Box parameters (always present):
- Name: base name for the drawer Part and its bodies.
- Width / Height / Depth: outer box dimensions (mm).
- Side thickness (
t_side): thickness of the side/front/back panels. - Bottom thickness (
t_bottom): thickness of the bottom panel. - Bottom offset (
bottom_v_offset): raises the bottom panel above the flush position.
Options:
- Corner joinery (
corner_joint): Tongue and dado (recessed) (default), Half-lap, Mitered, Tongue and dado (flush) or Finger joint — see Joinery below. The choice stays live and editable on the created drawer; switching it re-dimensions the panels and re-shapes or suppresses the joinery pockets. (Drawers created before this option carry anoverlap_boxboolean instead; they keep working and are migrated when recreated. Mitered was called Overlap before; saved drawers keep working.) - Finger joint tolerance (
finger_tolerance, default 0.05 mm, enabled for the finger joint only): every finger slot is cut this much wider on each flank (teeth thinner by the same amount), i.e.4 xthe value of clearance per finger. - Add a dedicated drawer front (
has_front): when checked, an extra front panel is added with its own parameters:- Front width / Front height (
width_front,height_front): outer size of the front. - Front thickness (
t_front). - Front offset (
front_v_offset): positive values move the front down; the front's lower edge sits atz = -front_v_offset.
- Front width / Front height (
- Handle slots in the sides (
handle_slot, live): a through slot in each side for carrying the box — a stadium Slot width (handle_width, 100 mm) wide and Slot diameter (handle_diameter, 32 mm) high, centred in the depth, its top edge Slot offset from top (handle_v_offset, 20 mm) below the side's top edge. No roundovers are modelled; apply them off the CNC. The slot must stay above the bottom groove.
Joinery: The bottom is captured in a groove (width 0.5 * t_bottom) cut into the inner
faces of the side, front, and back panels ("captured-bottom drawer"), and the bottom panel
carries a matching perimeter rabbet so its upper-half tongue seats into the grooves. The
back's groove is open towards its lower edge, the front's and the sides' are closed: glue
up the sides and the front, slide the bottom in from the back, then drop the back in.
(Front and Back are therefore not identical parts.) With tongue-and-dado corners the sides'
groove is a stopped dado, 0.5 * t_side short of each end, so it does not show on the
sides' end grain; it ends inside the corner dados, so the bit's round end never leaves the
dado void and the bottom's square corners still seat. With finger joints every wall's groove
is stopped 0.5 * t_side short (inside the fingers) and the back's groove is closed too: the
box is glued up in one go with the bottom captured.
The corners depend on corner_joint. In every variant the sides run the full depth and
carry the corner pocket on their inner face at each end, and the front and back tuck into
them:
- Tongue and dado (recessed): a dado at each side end —
0.5 * t_sidewide,0.5 * t_sidedeep, offset0.5 * t_sidefrom the end edge, running the full panel height. The front and back aret_sideshorter than thewidthand get a matching half-lap (0.5 * t_sidelong and deep) on their inner face at each end, so the outer half of their thickness forms the tongue that slides into the side dados. Self-locating during glue-up and fully machinable face-up. - Tongue and dado (flush): the same side dado, but the front and back sit flush with
the side ends; their tongue is the inner half and the lap is on the outer face. A lock
joint needs the lap and the bottom groove on opposite faces, so that lap cannot be
reached with the panel lying inner-face-up: it is modelled, not machined, and the CAM
run reports it as a manual cut (four straight
0.5 * t_side × 0.5 * t_siderabbets per drawer on the table saw or router table). - Half-lap: the side pocket widens to
t_sideand runs out to the end edge (a rabbet). The front and back (stillt_sideshorter) sit in it flush with the side ends and need no cut of their own. One cut per corner; the bit only has to be<= t_side. - Mitered (formerly Overlap): no corner pocket; the front and back run the full
widthso all four walls overlap — stock for corners cut by hand. - Finger joint: a box joint with square fingers, modelled in the bodies. All four walls
run full size. An even number
n = 2 * max(1, round(height / (2 * t_side)))of fingers of pitchheight / n(exactlyt_sidewhen the height is an even multiple of it; 10.5 mm for 84 mm / 12 mm) at each corner,t_sidedeep. The sides carry the teeth at the even positions from the bottom edge (a tooth at the bottom edge), the front and back at the odd ones; with an even count the two patterns are each other's upside-down image. The slots are one pocket per wall repeated by aLinearPatternwith an expression-driven count, so height changes stay live. The fingers are not cut on the sheet; the CAM run adds a separate finger Job (below).
Coordinate system: The bottom panel is centered on the Part origin in X (width) and Y
(depth); its bottom face is at z = 0 (at the default bottom_v_offset of 0). The whole
drawer Part is rotated 180° about Z so its front faces the FreeCAD "front" (−Y) view.
Recessed vs. flush: The machined joinery sits on the panels' inner faces, so every
panel is cut in a single setup. With Tongue and dado (recessed) the price is that the
front and back sit 0.5 * t_side behind the ends of the sides — the sides form a small lip
at the front and the back (hidden behind a dedicated drawer front, which still lands on the
side ends with a 0.5 * t_side gap to the box front; visible without one). The interior
depth is then depth - 3 * t_side and the bottom panel depth - 2 * t_side long. With the
other three variants the front and back are flush, the interior depth is
depth - 2 * t_side and the bottom depth - t_side long. The outer width and depth
are unaffected either way, and the joinery does not depend on whether a drawer front is
requested.
Parameters / data model: Every parameter is stored as an editable, expression-capable
property. Because FreeCAD raises a cyclic-reference error when a child body references its
parent App::Part's properties, the parameters live on a lightweight sibling holder object
(labelled "… Parameters") inside the drawer Part. Edit values there (or bind them to a
spreadsheet cell, e.g. p.drawer_width) and recompute — the panels update accordingly.
The holder has no shape, so it is ignored by the cutlist; each panel body is named after
the drawer (e.g. Drawer_SideL, Drawer_Bottom) and is picked up by the cutlist generator.
Notes:
- All dialog fields (including the checkboxes) remember their last value/expression between invocations, to streamline creating several similar drawers in a row.
has_frontis applied at creation time: toggling it later on the holder will not add or remove the drawer-front body.
Recreate: with exactly one existing drawer selected (the Part or anything inside
it), Create Drawer opens pre-filled with that drawer's expressions and rebuilds it with
the current code under the same name, container and placement. Use this after updates to
drawers.py. Objects referencing the old bodies must be regenerated (a CAM Job: run
Drawer CAM Job again). Recreating does not change the remembered "last used" values.
Bottom joint (live, decided by t_bottom < t_side): a bottom thinner than the sides is
inserted at full thickness into a groove t_bottom wide that starts t_bottom above the
box bottom (no rabbet on the bottom). A bottom at least as thick as the sides is captured:
groove t_bottom / 2 wide starting t_bottom / 2 up, bottom rabbeted to a t_bottom / 2
tongue. bottom_v_offset raises the groove in both cases.
Drawer CAM Job Command
Generates FreeCAD CAM Jobs with operations, hold-down tabs and G-code for drawers made
with Create Drawer. Select one or more drawers (a drawer Part, anything inside it, or a
previously generated Job) and run Drawer CAM Job (toolbar, pie menu key G).
The dialog asks for (all values are remembered):
- Tool bit from the CAM toolbit library (
~/.local/share/FreeCAD/v1-1/CamAssets). A single bit is used for everything; it must not be wider than the bottom groove (t_bottomfor an inserted bottom,t_bottom / 2for a captured one) or the rabbets, otherwise the command refuses with an explanation. - Spindle speed, XY feed, plunge feed, step down.
- Sheet width / height: the machine work area, default 630 x 1080 mm. Every panel must fit in some orientation (with tool clearance at the right/bottom edges).
- Clamp height (default 20 mm): rapids travel 2 mm above it.
- Origin corner: top-left (zero at the far-left corner, Y negative towards you) or bottom-left (zero at the near-left corner, Y positive). Panels hug the two sheet edges at that corner, which are the clamping edges.
- Skip drawer fronts (default on): fronts are plain rectangles, usually of another material, and are left out of the Jobs.
- Post processor (default
uccnc) and whether to write the G-code now.
Nesting (nesting.py): the panels of all selected drawers are grouped by thickness and
packed in columns from the sheet's top-left corner, long side vertical whenever it fits,
rotated otherwise. Neighbouring panels are exactly one tool diameter apart so one cut
separates both and its tabs hold both. Edges flush with the sheet's top and left edges are
not cut at all, so those edges are where the sheet gets clamped; the summary lists the
positions where cuts do reach those edges. Panels that do not fit go onto another sheet.
All Jobs of one run are collected in an App::Part container labelled CAM <name>,
where <name> describes the selected drawers compactly (naming.py): shared name parts
are factored out and numbered series are collapsed, e.g. Kitchen_Left_Top,
Kitchen_Left_Bottom, Kitchen_Right_Top become CAM Kitchen Left Bottom/Top, Right Top,
and Drawer001 .. Drawer003 become CAM Drawer 001-003. Selecting the container selects
all its drawers for the command. Per sheet one Job labelled Job <t>mm sheet <n> (plus
the drawer names when the sheet holds only some of the container's drawers), each inside
its own sheet frame (App::Part labelled <t>mm sheet <n>). The frames are displayed
side by side along X, one sheet width plus a 10 % gap apart, so the sheets do not overlap
in the 3D view; the first sheet sits at the origin. Only the display is shifted: every
Job, its stock and operations keep machine coordinates and the G-code is unaffected.
Each sheet frame also gets a TechDraw page (sheetdraw.py) titled with the name of
the sheet's G-code file, on a blank A4 portrait page (templates/A4_Portrait_Blank.svg,
no frame or title block): the title on top, then the sheet with its panels, cut lines,
tabs and the panel label inside every panel, and underneath a legend with one strip per
panel (label and length x width x thickness, dashed cut guides between strips) to cut
off and tape onto the physical parts. Double click the page in the tree to open it;
File > Export or the TechDraw toolbar print it.
- Coordinates: zero is the chosen sheet corner, X to the right, Z = 0 on the sheet surface; Y is negative (top-left origin) or positive (bottom-left origin). Cut a blank at least as large as the reported minimum, square at that corner, and zero the machine there.
- The models are the panel bodies laid flat, pocketed face up, at their nested places. The stock is the whole sheet.
- Slot passes for the bottom groove of the walls, the corner joinery of the walls (the end dados or rabbets of the sides and, for the recessed tongue and dado, the end laps of the front/back; none with Mitered or Finger joint) and, for a captured bottom, its four rabbet strips. Passes overlap by 50 % of the tool diameter and overshoot open ends; the stopped side grooves of the tongue-and-dado variants end with the tool tangent to the stop, the stopped grooves of finger-jointed walls with the tool centre on the stop (the bottom reaches the stop; the round end runs on inside the finger). The outer-face laps of the flush tongue and dado are not machined; the run summary lists them as a manual cut.
- Finger Job (finger-jointed drawers only): per group of drawers with the same height,
t_sideand tolerance, one extra JobJob Fingers <h>mmin its own frame holding all four walls of every drawer of the group in one pack. The walls stand on end, stacked face to face along +Y from Y = 0 (any order), the end face to be cut at Z = 0 — lower-left origin, no orientation options. Sides with the grooved edge at X = 0, fronts and backs upside down (grooved edge at X =h): the even finger count makes a flipped front show the sides' slot pattern. Every finger slot is a set of Slot passes across the whole pack,t_sidedeep, running 5 mm plus a tool radius past both outer faces into sacrificial boards clamped there (flush with the end faces) against tear-out. Run the finger Job once per end, turning the pack end over end. The bit must be thinner thant_side. G-code:<Document>_<container name>_fingers_<h>mm.nc. No TechDraw page yet. Selecting several finger-jointed drawers of different heights gives one Job per height (reported in the summary). See Milling the finger joints below for the workshop procedure. - Handle slots are cut as a through Profile of the slot's outline (the four top edges of the model clone, inside, tool-compensated) with a Tags dress-up: one tab in the middle of each straight segment holds the waste piece; knock it out and clean the tabs by hand. The bit must be narrower than the slot diameter.
- One Slot per merged cut line (through cut, 0.2 mm into the spoilboard) with a Tags dress-up: tabs at 1/3 and 2/3 of every panel edge on that line, 10 mm wide, 3 mm high (at most half the thickness).
- G-code at
<document folder>/<Document>_<container name>_<t>mm_<n>.nc(also set as the Job output).
Milling the finger joints: the flipped double pack
The fingers are cut in a separate setup after the sheet Jobs have produced the walls. The
finger Job is one G-code file per height, and it is run twice on the same pack — once
for each end of the panels. The pack holds all walls of the group: for two drawers of the
same height that is eight panels (four sides, two fronts, two backs), so the passes run
through a 8 x t_side stack in one go.
How it works: every wall gets the same number of finger slots, but the sides have them at the odd positions from the bottom edge (a tooth sits at the bottom edge) and the fronts and backs at the even ones. Because the finger count is always even, the front/back pattern is exactly the sides' pattern turned upside down — so one slot pattern cuts both kinds as long as the fronts and backs stand in the pack head over heels. No separate "male" and "female" Jobs, and no re-clamping between the two kinds.
Building the pack (job coordinates: X along the panel height, Y through the pack, Z up, zero at the lower-left corner of the pack's top end face):
- Stand every wall on end so the end face to be cut is on top and flush across the pack (Z = 0). The bottom ends may hang at different heights when depths and widths differ.
- Sides: bottom (grooved) edge towards X = 0, i.e. to the left.
Fronts and backs: upside down — their grooved edge points to X =
height, to the right. Quick check before clamping: looking at the pack from the side, the sides' grooves are all on the left, the fronts' and backs' grooves all on the right. - Stack the panels face to face along +Y starting at Y = 0; the order within the pack and which face points which way do not matter (the passes go through the whole pack).
- Clamp a sacrificial board flat against each outer face of the pack, flush with the top end faces (not proud — the passes start at Z = 0 and would otherwise plunge into it). The passes overshoot both faces by 5 mm plus a tool radius, so the tear-out happens there.
- Zero X and Y on the lower-left corner of the pack's top end face (the sides' bottom edge, first panel face) and Z on that face. Run the Job.
- Turn the whole pack end over end so the other end faces up — tip it over sideways, about the axis that runs along the panel heights (X). Every panel keeps its edges where they were: the sides' grooves stay left, the fronts' and backs' grooves stay right (only the face order in the stack reverses, which does not matter). Turning the pack about either of the other two axes (through the stack, or vertical) would swap left and right and mirror the pattern — if the grooves changed sides, turn it back. Re-zero Z on the new top face (X/Y are unchanged if the pack sits in the same jig) and run the same Job again.
Both ends of every wall now carry the pattern, and a side mates with a front or back at any
corner. The pitch is height / n with n = 2 * round(height / (2 * t_side)), so the fingers
are only exactly t_side wide when the height is an even multiple of t_side; the slots are
finger_tolerance wider on each flank than the pitch (0.05 mm by default, 0.2 mm total
clearance per finger). The bit must be thinner than t_side (it also has to fit the
t_bottom groove for the sheet Jobs anyway).
Running the command again re-nests and replaces the Jobs of the selected drawers (and Jobs they shared with other drawers, so a container is always regenerated as a whole). Manual changes to those Jobs are lost. The container is kept when the set of drawers is unchanged, so you may rename it; when the set changes a fresh container with a generated name replaces the old ones.
The corner joinery is modelled in the drawer bodies and mirrored here, so the pockets on
the sheet match the solids. Note that the tongue-and-dado dados and laps are only
t_side / 2 wide, so that variant needs a bit of at most t_side / 2 (6 mm for 12 mm
stock); the half-lap rabbet accepts up to t_side. A bit that is too wide is reported as a
Corner/Lap problem before the run starts.
Sync Aliases Command
Manually sync all aliases in the parameter spreadsheet. Useful if:
- You opened an existing project without the workbench active
- Something went wrong with automatic sync
- You want to batch-update aliases
Installation
Option 1: Symlink (Recommended for Development)
# macOS
ln -s /path/to/FreeCAD/Mod/Lumberjack ~/Library/Application\ Support/FreeCAD/Mod/Lumberjack
# Linux
ln -s /path/to/FreeCAD/Mod/Lumberjack ~/.local/share/FreeCAD/Mod/Lumberjack
# Windows (run as admin)
mklink /D "%APPDATA%\FreeCAD\Mod\Lumberjack" "C:\path\to\FreeCAD\Mod\Lumberjack"
Option 2: Copy
Copy the entire Lumberjack folder to your FreeCAD Mod directory:
- macOS:
~/Library/Application Support/FreeCAD/Mod/ - Linux:
~/.local/share/FreeCAD/Mod/ - Windows:
%APPDATA%\FreeCAD\Mod\
Usage
Creating a New Project
- Switch to the Lumberjack workbench
- Click "New Project" in the toolbar or use Lumberjack → New Project
- A new document is created with a parameter spreadsheet
Adding Parameters
- Open the parameter spreadsheet
p(double-click "Parameters" in the model tree) - In column A, type a parameter name (e.g.,
shelf_spacing) - The alias is automatically set on the value cell in column B
- Enter your value in column B
- Optionally add a description in column C
- Use the parameter in expressions:
p.shelf_spacing
Creating Panels
- Ensure you have an active document (create a new project first)
- Click "Create Panel" in the toolbar or use Lumberjack → Create Panel
- Enter a name for the panel
- Adjust thickness, width, and height expressions as needed
- Click "Create"
The panel is created as a PartDesign Body with a sketch and pad. All dimensions are expression-driven.
Creating Drawers
- Ensure you have an active document and (optionally) activate the container the drawer should be placed in.
- Click "Create Drawer" in the toolbar, use Lumberjack → Create Drawer, or press D in the Lumberjack quick menu.
- Enter a name and set the box dimensions (width, height, depth, side/bottom thickness).
- Optionally pick the corner joinery (e.g. Finger joint), and Add a dedicated drawer front to enable the front-panel fields.
- Click "Create".
The drawer is created as a Std_Part containing the panel bodies. All dimensions stay
editable on the parameter holder inside the Part (and can reference spreadsheet cells), and
every panel body is exposed to the cutlist generator.
Generating drawer G-code
- Select the drawers in the tree
- Run Drawer CAM Job (
q,qthenG, or the toolbar) - Pick the tool bit and check sheet size, clamp height, feeds and post processor
- Read the summary: minimum blank per sheet and where cuts reach the clamp edges
- Inspect the Jobs in the CAM workbench (simulator works per operation)
- The
.ncfiles sit next to the document, one per sheet
Headless tests: test_cam.py (console) and test_cam_gui.py (offscreen GUI), see the
docstrings for the command lines.
Parameter Naming Rules
Parameter names must be valid Python identifiers:
- ✅ Start with a letter or underscore
- ✅ Contain only letters, numbers, and underscores
- ❌ No spaces or special characters
- ❌ Cannot be Python keywords (
if,for,class, etc.)
Valid examples: thickness, shelf_1, total_width, _private
Invalid examples: 1st_shelf, total-width, my param, class
Architecture
Lumberjack/
├── Init.py # Runs at FreeCAD start, installs alias sync observer
├── InitGui.py # Runs at GUI start, registers workbench and commands
├── project.py # Project setup and spreadsheet creation
├── panels.py # Panel creation dialog and logic
├── drawers.py # Parametric drawer (Std_Part with panel bodies) and dialog
├── cam.py # Drawer CAM Job generation (operations, tabs, G-code)
├── nesting.py # Sheet nesting and cut-line planning (pure Python)
├── naming.py # Compact names for CAM containers (pure Python)
├── sheetdraw.py # SVG for the TechDraw sheet overview pages (pure Python)
├── templates/ # Blank A4 portrait TechDraw template for those pages
├── reload.py # Development helpers: hot-reload modules, smoke tests
├── test_cam.py # Headless end-to-end test for cam.py
├── test_cam_gui.py # Offscreen GUI smoke test for cam.py
└── README.md # This file
Troubleshooting
Workbench Doesn't Appear
- Check that the
Lumberjackfolder is in the correct Mod directory - Restart FreeCAD
- Check the Python console for error messages
Aliases Not Syncing
- Ensure the spreadsheet is named
p(internal name, not label) - Check the console for "Lumberjack: Alias sync observer installed" message
- Try the manual "Sync Parameter Aliases" command
- Verify parameter names are valid (no spaces, starts with letter)
Panel Creation Fails
- Ensure you have an active document
- Check that the parameter spreadsheet
pexists with the referenced parameters - Look at the console for detailed error messages
Configuration
Edit project.py to customize default parameters:
# Number of parameter rows to create
NUM_ROWS = 10
# Spreadsheet internal name (for expressions)
SPREADSHEET_NAME = "p"
# Spreadsheet display label (shown in tree)
SPREADSHEET_LABEL = "Parameters"
# Default parameters to pre-fill
DEFAULT_PARAMETERS = [
("thickness", "18", "Material thickness (mm)"),
("width", "600", "Overall width (mm)"),
("height", "800", "Overall height (mm)"),
("depth", "400", "Overall depth (mm)"),
]
Version History
v2.0.0
- Complete rewrite for FreeCAD 1.1+
- Automatic alias synchronization via document observer
- Create Panel command with expression-driven dimensions
- Uses native
Gui::QuantitySpinBoxwith expression binding
v1.x (Legacy)
- Original FreeCAD-lumberjack for LinkStage3/Assembly3 fork
- Not compatible with FreeCAD 1.1
Future Development
Planned features:
- Panel edge banding options
- Grain direction indicators
- Cutlist generation integration
- Drawer CAM: second bit for the through cut, configurable tabs, nesting of plain panels
- Material database
- Hardware library (hinges, slides, etc.)
- Assembly helpers
- Project templates
License
MIT
Contributing
Contributions are welcome! Please:
- Follow the existing code style
- Add tests for new functionality
- Update documentation
- Test with FreeCAD 1.1+