feat: add freecad plugin for agent-scripted CAD with a live GUI loop#159
Merged
Conversation
Adds a plugin for designing in FreeCAD the way an agent and a human can actually share: the agent authors the model as a Python script, FreeCAD renders it, and the human pans, rotates, isolates parts and drags things around. The script is the source of truth; the .FCStd is a disposable build artefact. It covers woodworking and 3D printing in one tool, which SketchUp (surface modeller, no real solids) and Blender (mesh modeller, no constraints) cannot. The loop runs both ways. fcsnap reports the user's actual camera, their selection, and how each part's placement now differs from where the script put it — so a drag becomes a proposal to fold back into the source rather than something the next rebuild silently destroys. - scripts: fcrun (headless build + validation + exports), fclive (live session, rebuilds on save, preserves the camera), fcquit (clean shutdown), fcsnap (read the session back), fcrender (PNGs, so the agent can check its own geometry before spending the user's attention) - lib/wwkit: parametric modelling, real dimensional stock (a 2x4 is 1.5x3.5", "3/4" ply is 23/32"), joinery (trench/dado/rabbet/mortise/ tenon/hole/fillet/chamfer), clash + printability checks, cut list - lib/wwcut: cut *plan* — packs parts into stock boards and sheets with kerf, honours a transport limit, and reports what to buy - skills: freecad-modeling (model-only guide), freecad-live (/freecad:live) - examples: mixed wood-and-printed bracket box; shelf unit in real 1x10 with housed shelves, a rabbeted back, and a mortise-and-tenon stretcher Edges are selected by geometric predicate rather than by index, which makes these models structurally immune to FreeCAD's topological naming problem. Deliberately does not depend on dprojects/Woodworking: every one of its tools is bound to FreeCADGui.Selection or runs Qt dialog code at import time, so none of it is callable headless. We do stamp the Woodworking_Width/Height/Length properties it looks for, so its report tool works on our documents if a user has it installed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new FreeCAD plugin that enables an “agent-authored Python script ↔ human-steered GUI” modeling loop, including headless builds/exports, live-reload sessions with camera preservation, session snapshots (camera/selection/drags), and geometry/render helpers for woodworking + 3D printing workflows.
Changes:
- Introduces FreeCAD plugin packaging for both Claude and Copilot marketplaces (metadata + hooks registration).
- Adds core command scripts (
fcrun,fclive,fcquit,fcsnap,fcrender) plus a live-reload watcher and renderer. - Adds modeling libraries (
wwkit,wwcut) and examples/skills documentation for the new workflow.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/sync.sh | Adds freecad to the vendored-utils sync manifest (approve/hook-compat). |
| plugins-copilot/freecad/hooks/hooks.json | Registers the Copilot preToolUse hook to run approve-own-scripts.sh. |
| plugins-copilot/freecad/.claude-plugin/plugin.json | Copilot plugin metadata for freecad. |
| plugins-claude/freecad/skills/modeling/SKILL.md | “Modeling” skill guidance and loop conventions (script as source of truth, etc.). |
| plugins-claude/freecad/skills/live/SKILL.md | “Live session” skill guidance for starting/stopping and iterating. |
| plugins-claude/freecad/scripts/hook-compat.sh | Normalizes hook payloads across Claude Code and Copilot CLI. |
| plugins-claude/freecad/scripts/fcsnap | CLI entrypoint to request a snapshot from the live session and emit JSON. |
| plugins-claude/freecad/scripts/fcsession | Shared helpers for per-model control file paths and process detection. |
| plugins-claude/freecad/scripts/fcrun | Headless/GUI FreeCAD runner with output filtering and error detection. |
| plugins-claude/freecad/scripts/fcrender | Uses GUI FreeCAD to render .FCStd to PNGs via render.py. |
| plugins-claude/freecad/scripts/fcquit | Clean shutdown via watcher stop-file (avoids hard-kill corruption dialogs). |
| plugins-claude/freecad/scripts/fclive | Launches the GUI live-reload watcher (foreground or background). |
| plugins-claude/freecad/scripts/approve-own-scripts.sh | Hook script that auto-approves plugin-local scripts and guards PR publish/merge. |
| plugins-claude/freecad/README.md | End-user documentation for requirements, commands, and workflow rationale. |
| plugins-claude/freecad/lib/wwkit.py | FreeCAD modeling toolkit (parts, joinery, checks, reports, export, intent stamping). |
| plugins-claude/freecad/lib/wwcut.py | Cut-plan packing logic for boards and sheet goods (transport constraints, kerf, grain rules). |
| plugins-claude/freecad/lib/render.py | GUI-driven renderer (views, settle timing, watchdog, logging). |
| plugins-claude/freecad/lib/live-reload.py | GUI watcher that rebuilds on save, preserves camera, supports “snap” payloads. |
| plugins-claude/freecad/hooks/hooks.json | Registers Claude Code hook to auto-approve own scripts for Bash tool use. |
| plugins-claude/freecad/examples/joinery.py | Example model demonstrating real lumber sizes + joinery + cut planning. |
| plugins-claude/freecad/examples/bracket-box.py | Example mixed wood/printed model demonstrating printable parts + intent. |
| plugins-claude/freecad/.claude-plugin/plugin.json | Claude plugin metadata for freecad. |
| .github/plugin/marketplace.json | Adds freecad to the GitHub marketplace manifest (Copilot source path). |
| .claude-plugin/marketplace.json | Adds freecad to the Claude marketplace manifest (Claude source path). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+141
to
+153
| filter() { | ||
| # --line-buffered for the same reason as PYTHONUNBUFFERED above: grep would | ||
| # otherwise hold the watcher's output hostage in a 4K block. | ||
| grep --line-buffered -avE \ | ||
| -e '^[A-Za-z_][A-Za-z0-9_]*=' \ | ||
| -e '3Dconnexion|Navlib' \ | ||
| -e '^Running: ' \ | ||
| -e '\([0-9]+ %\)' \ | ||
| -e '^(Recompute|Checking topology|Checking for self-intersections|saving)\.{3,}' \ | ||
| -e '^\[FreeCAD Console mode' \ | ||
| -e '^>>> *$' \ | ||
| -e '^[[:space:]]*$' | ||
| } |
Comment on lines
+32
to
+45
| # The GUI swallows stdout into its Report View, so the log file is how we find | ||
| # out what happened. Errors there are otherwise completely invisible. | ||
| FCRENDER_DOC="$(cd "$(dirname "$DOC")" && pwd)/$(basename "$DOC")" \ | ||
| FCRENDER_OUT="$(cd "$OUT" && pwd)" \ | ||
| FCRENDER_LOG="$LOG" \ | ||
| FCRENDER_VIEWS="$VIEWS" \ | ||
| FCRENDER_W="${FCRENDER_W:-1200}" \ | ||
| FCRENDER_H="${FCRENDER_H:-900}" \ | ||
| "$HERE/fcrun" --gui "$LIB_DIR/render.py" >/dev/null 2>&1 || true | ||
|
|
||
| cat "$LOG" | ||
| if grep -qa "RENDER FAILED" "$LOG"; then | ||
| exit 1 | ||
| fi |
Comment on lines
+30
to
+33
| if ! _fc_running; then | ||
| echo "fcsnap: no live session — start one with: fclive -b $(basename "$SCRIPT")" >&2 | ||
| exit 3 | ||
| fi |
Comment on lines
+468
to
+474
| def gap(self, a, b): | ||
| """Shortest distance between two parts. Negative means they overlap.""" | ||
| d = a.shape.distToShape(b.shape)[0] | ||
| if a.shape.common(b.shape).Volume > 1e-6: | ||
| d = -d | ||
| say("GAP %s <-> %s %s mm" % (a.name, b.name, fmt(d))) | ||
| return d |
Comment on lines
+598
to
+606
| def filament(self, density=PLA_DENSITY): | ||
| printed = self.of_kind("printed") | ||
| if not printed: | ||
| return 0.0 | ||
| grams = sum(p.shape.Volume / 1000.0 * density for p in printed) | ||
| each = printed[0].shape.Volume / 1000.0 * density | ||
| say("FILAMENT %d part(s), %.1f g each, %.0f g total (%.0f%% of a 1kg spool)" | ||
| % (len(printed), each, grams, grams / 10.0)) | ||
| return grams |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A plugin for designing in FreeCAD the way an agent and a human can actually share the work: the agent authors the model as a Python script, FreeCAD renders it, and the human pans, rotates, isolates parts, and drags things around. The script is the source of truth; the
.FCStdis a disposable build artefact.One tool covering both woodworking and 3D printing — which is why it isn't SketchUp (surface modeller, no true solids, notorious for non-manifold STLs) or Blender (mesh modeller, no constraints, nothing enforcing that a printed bracket fits a wooden slot).
The loop runs both ways
The interesting part is
fcsnap. Without it the agent only pushes geometry, and the human's manipulation is invisible to it and destroyed by the next rebuild.fcsnapreturns the user's actual camera (not a canned view), what they have selected (so "this one" resolves to a name), andmoved— how each part's placement now differs from where the script put it.That turns a drag into a proposal the agent folds back into the source, instead of something the next rebuild silently eats.
Contents
fcrunfclivefcquitfcsnapfcrenderlib/wwkit— parametric modelling; real dimensional stock (a 2x4 is 1.5x3.5", "3/4" ply is 23/32"); joinery (trench/dado/rabbet/mortise/tenon/hole/fillet/chamfer); clash + printability checks; cut list. Units aremm/in/both— metric shop, imperial lumberyard.lib/wwcut— a cut plan, not just a cut list: packs parts into stock boards and sheets with kerf, honours a transport limit (a 12ft board is a useless plan if it won't go in the truck), and says what to buy. Grain is respected: rotation is off by default, because a plan that saves half a sheet by cross-graining your cabinet sides is not a saving.freecad-modeling(model-only guide),freecad-live(/freecad:live)Two design decisions worth flagging
Edges are selected by geometric predicate, never by index.
Edge7gets renumbered by a recompute — that is FreeCAD's topological naming problem, and it's what breaks mouse-built models. A predicate can't be renumbered, so these models are structurally immune to it.Deliberately does not depend on
dprojects/Woodworking. Every one of its tools is bound toFreeCADGui.Selectionor executes Qt dialog code at import time, so none of it is callable headless — it would break the entire loop. Its joinery is also thinner than it looks (no dado, rabbet, dovetail, or finger-joint generator anywhere in it). We do stamp theWoodworking_Width/Height/Lengthproperties it looks for, so its cut-list report works on our documents if a user has it installed — without the stamp it silently drops every part, since it ignores boolean results.FreeCAD behaviours absorbed (each cost a real debugging session)
newDocument("bracket-box")yieldsbracket_box. Look it up by the original name and you miss, so a live-reload loop stacksbracket_box,bracket_box1, ... forever instead of replacing.print()never reaches stdout — FreeCAD rebinds it to the Report View, so anything watching the process pipe sees silence forever. Hence the log files.-cdoes not exit — it drops into an interactive console awaiting EOF, leaking a FreeCAD process indefinitely.Testing
Developed and verified end-to-end on macOS (Apple Silicon, FreeCAD 1.1.1): headless build, live reload with camera preservation, snapshot/drag-diff, clean shutdown, offscreen render.
A read-only Linux portability audit was run and its findings applied —
mktemp -t(fails on GNU), script-path absolutisation (Flatpak doesn't carry the caller's cwd into the sandbox), broadened process detection (name varies across distros/AppImage/Flatpak), anxvfb-runpath for headless boxes,PySide2/PySide6fallback imports, and a Flatpak--filesystem=hostpreflight warning.Not yet exercised on real Linux hardware — that's the known gap.
🤖 Generated with Claude Code