[Bug]: freecad - part boolean produces no geometry, and export emits intermediate shapes instead of the final result

Author: tcgztopCreated Sep 13, 2026Updated Sep 13, 2026

Affected Software/Harness

freecad

Version / Commit

cli-anything-freecad 1.0.0 (@ repo commit 810c18b0d1ab9b234bc996c9fd999318523a3ef0, main 2026-08-21); FreeCAD 1.1.3 (Revision 20260725)

Operating System

Windows

Python Version

3.12.10

Steps to Reproduce

Setup (on this machine FreeCAD is NOT on PATH and NOT under C:\Program Files,

so FREECAD_PATH is required; PYTHONUTF8 is also needed - see Additional Context)

$env:FREECAD_PATH = "D:\Program Files\FreeCAD 1.1\bin\freecadcmd.exe" $env:PYTHONUTF8 = "1"

---------- Defect A: part boolean cut produces no geometry ----------

cli-anything-freecad document new -o demo.FCStd cli-anything-freecad -p demo.FCStd part add box -n Base -P length=40 -P width=30 -P height=20 cli-anything-freecad -p demo.FCStd part add cylinder -n Hole -P radius=6 -P height=30 cli-anything-freecad -p demo.FCStd part boolean cut 0 1 -n Bracket cli-anything-freecad -p demo.FCStd export render bracket.stl -p stl --overwrite

Control experiment: identical solids, but NO boolean at all

cli-anything-freecad document new -o control.FCStd cli-anything-freecad -p control.FCStd part add box -n Base -P length=40 -P width=30 -P height=20 cli-anything-freecad -p control.FCStd part add cylinder -n Hole -P radius=6 -P height=30 cli-anything-freecad -p control.FCStd export render control.stl -p stl --overwrite

The two STL files turn out byte-for-byte identical:

sha256sum bracket.stl control.stl

---------- Defect A, engine-level confirmation ----------

-o writes the harness's JSON session, not a real FCStd, so export a real one:

cli-anything-freecad -p demo.FCStd export render real_demo.FCStd -p fcstd --overwrite

Then open it with the engine itself:

freecadcmd inspect_fc.py

-> Base Part::Box 24000.0 / Hole Part::Cylinder 3392.92

-> NO 'Bracket' object exists in the document at all

---------- Defect B: export emits intermediate shapes (PartDesign path) ----------

cli-anything-freecad document new -o pd.FCStd cli-anything-freecad -p pd.FCStd body new -n Body cli-anything-freecad -p pd.FCStd body additive-box 0 -l 40 -w 30 -h 20 cli-anything-freecad -p pd.FCStd body subtractive-cylinder 0 -r 6 -h 30 --position 20,15,-5 cli-anything-freecad -p pd.FCStd export render pd.stl -p stl --overwrite

The engine says the Body is correct: 21738.05 mm^3 (= 24000 - pi6^220)

cli-anything-freecad -p pd.FCStd export render real_pd.FCStd -p fcstd --overwrite

Expected Behavior

Defect A: part boolean cut 0 1 should create a cut solid, and exporting should produce a mesh of that cut solid (box minus cylinder). For a 40x30x20 box with an r=6 cylinder through it, the expected volume is 24000 - pi6^220 = 21738.05 mm^3.

Defect B: the exported STL/STEP should contain the project's final result only, not the intermediate PartDesign feature solids as well.

Actual Behavior

Defect A - the boolean result is never created.

Every step reports success (Boolean cut: Bracket; part list shows a part with type "cut"; export returns method: freecad-headless and a plausible file size), but the exported STL is byte-for-byte identical to a model where no boolean was performed at all. Opening the document with FreeCAD itself shows only two objects (Base, Hole) - there is no Bracket object, so the boolean never produced geometry. The exported "cut" body also has a bounding box of 46x36x30, which is larger than its own source box (40x30x20) - geometrically impossible for a subtraction.

Defect B - even when the geometry is right, the export is polluted.

On the PartDesign path the geometry IS computed correctly (the engine reports Body.Shape.Volume = 21738.05 mm^3, exactly the expected 24000 - pi6^220). But the exported STL has 1052 triangles / ~67478 mm^3 of mesh volume, versus 520 triangles / 21738.99 mm^3 when only the Body is exported. The extra geometry is the intermediate feature solids, exported as additional overlapping shells.

Neither defect is visible from the exit code, the file size, or the container metadata - only measuring the geometry reveals them.

Relevant Logs / Tracebacks

# --- Defect A: the harness output looks completely healthy ---
$ cli-anything-freecad -p demo.FCStd part boolean cut 0 1 -n Bracket
Boolean cut: Bracket
  id: 3   name: Bracket   type: cut   params: {"base_id": 1, "tool_id": 2}

$ cli-anything-freecad -p demo.FCStd export render bracket.stl -p stl --overwrite
Exported: ...\bracket.stl
  format: stl   file_size: 25684   method: freecad-headless

# --- Defect A: control experiment - identical solids, NO boolean performed ---
bracket.stl   sha256 f9c80c5e84fec914...   512 triangles   25684 bytes   bbox 46x36x30
control.stl   sha256 f9c80c5e84fec914...   512 triangles   25684 bytes   bbox 46x36x30
-> byte-for-byte identical

# --- Defect A: what the engine actually sees inside the document ---
$ freecadcmd inspect_fc.py
real_demo.FCStd
  Base   Part::Box        V = 24000.00 mm3   A = 5200.00 mm2   bbox=40.0x30.0x20.0
  Hole   Part::Cylinder   V =  3392.92 mm3   A = 1357.17 mm2   bbox=12.0x12.0x30.0
  (there is no Bracket object)

# --- Defect B: the engine says the PartDesign body is correct ... ---
real_pd.FCStd
  Body                          PartDesign::Body                 V = 21738.05 mm3   bbox=40.0x30.0x20.0
  Feature_additive_box          PartDesign::AdditiveBox          V = 24000.00 mm3   bbox=40.0x30.0x20.0
  Feature_subtractive_cylinder  PartDesign::SubtractiveCylinder  V = 21738.05 mm3   bbox=40.0x30.0x20.0

# --- Defect B: ... but the harness's exported mesh disagrees ---
file                          triangles     bytes     mesh volume (mm3)
harness pd.stl                     1052     52684        67477.98    <- polluted
body-only export (reference)        520     26084        21738.99    <- matches engine
control pdctl.stl                    24      1284        48000.00    <- exactly 2x the 24000 box

Additional Context

Root cause (reading utils/freecad_macro_gen.py at commit 810c18b):

  1. _gen_parts() only emits primitives. A part whose type is "cut" (which is what part boolean stores) falls through to:

    lines.append(f"# WARNING: Unknown part type '{part_type}' for '{name}'")
    

    so no boolean object is ever added to the FreeCAD document.

  2. _gen_boolean_ops() iterates project.get("boolean_ops", []), but part boolean records its result in parts (with type "cut"). The two never meet.

  3. The exporter - used by both the part and body paths - collects every valid shape:

    for obj in doc.Objects:
        if hasattr(obj, 'Shape') and obj.Shape.isValid():
            export_objects.append(obj)
    

    so intermediate feature solids are exported alongside the final result.

Three secondary observations from the same session (minor, but they cost time):

  1. setup.py calls open("cli_anything/freecad/README.md") without encoding=. On a Chinese-locale Windows (default GBK) this raises UnicodeDecodeError, so the package cannot be installed with the documented pip install -e . Workaround: set PYTHONUTF8=1.

  2. document new -o demo.FCStd writes the harness's JSON session to a file named .FCStd (it starts with '{', not 'PK'). FreeCAD cannot open it; the engine-level checks above required exporting a real document via -p fcstd.

  3. find_freecad() searches PATH and C:\Program Files\FreeCAD*, but not the registry and not other drives. Here FreeCAD lives at D:\Program Files\FreeCAD 1.1, so FREECAD_PATH was required. Probing the Windows uninstall registry keys would find it automatically.

Reproduction scripts used for the evidence above (read-only; none modify the repo): verify_stl.py - independent binary-STL parser (triangle count, volume, bbox) inspect_fc.py - opens a real FCStd with freecadcmd and prints each Shape's Volume / Area / BoundingBox (engine ground truth) export_body_only.py - exports only the PartDesign Body, as a clean reference

Scope note: only part add + part boolean, and body additive-box + body subtractive-cylinder were tested. sketch / assembly / fem / cam / mesh / motion / techdraw and the remaining PartDesign features were NOT exercised, so this should not be read as a general statement about those paths.

Happy to split this into two issues if that helps triage - Defect A and Defect B have different triggers and different fixes.