Skip to main content

caddiff

English | 简体中文

git diff for CAD assemblies.

Two STEP files in, one picture of what changed out — plus a change list your CI can gate on.

CI License Python

caddiff highlighting a moved part and a removed feature

Green-ish magenta marks the changed geometry; the red box locates it; the blue arrow shows a 5.00 mm displacement. Both images are generated by caddiff from the synthetic fixture in examples/ — no customer data anywhere in this repo. Browse the full sample report, rendered online, no install needed →


The problem

You put your CAD in git. Now someone asks: "what actually changed between v1.2 and v1.3?"

git diff is useless here. A STEP file is plain text, so git prints four thousand lines of coordinates and tells you nothing. Worse, git will happily merge two STEP files into a file that is syntactically valid and geometrically wrong — and nothing warns you.

Commercial CAD suites sell a fix for this (SOLIDWORKS Compare, TransMagic, 3DViewStation). The open-source side had almost nothing: the few projects in this space render a visual diff and stop there — no BOM alignment, no report, no CI story.

caddiff is the missing piece: a command-line tool that answers what changed with a picture and a machine-readable list.


Quick start

$ docker run --rm -v "$PWD:/data" ghcr.io/angel291592/caddiff:v0.4.0 \
    diff /data/old.stp /data/new.stp -o /data/report

v0.4.0 is the released image. An edge tag also tracks main if you want to live on top of the tree — but for anything you gate a CI run on, pin a released tag (or a digest).

Local install

caddiff itself has zero third-party dependencies — but the geometry and rendering steps run inside FreeCAD, so FREECAD_PYTHON must point at an interpreter that can import FreeCAD:

$ pip install caddiff          # add [pptx] if you want --pptx
$ export FREECAD_PYTHON=/path/to/FreeCAD/bin/python.exe   # Windows: the FreeCAD distribution ships its own python
$ export FREECAD_PYTHON=/usr/bin/python3                  # Linux: the system python3,
$ export PYTHONPATH=/usr/lib/freecad/lib                  #         with FreeCAD's modules on the path
$ caddiff diff old.stp new.stp -o report

On Linux, do not point FREECAD_PYTHON at .../freecad/bin/freecad-python3: that file is the FreeCAD GUI application, so handing it a script opens the GUI and never returns. The system python3 + PYTHONPATH above is what works, and it is how the Docker image is configured (see docs/pipeline.md).

On Ubuntu 24.04 the stock archive has no freecad package, so add the FreeCAD PPA first. Rendering needs a display, and Xvfb provides one on a headless machine:

$ sudo add-apt-repository -y ppa:freecad-maintainers/freecad-stable
$ sudo apt install -y freecad xvfb python3-venv
$ xvfb-run -a caddiff diff old.stp new.stp -o report

If FREECAD_PYTHON is unset, caddiff searches the usual Linux locations and PATH, and fails with an actionable message rather than silently falling back to a Python that cannot import FreeCAD.

With git difftool

So that git diff for CAD assemblies is literally true:

$ git config --global difftool.caddiff.cmd 'caddiff difftool "$LOCAL" "$REMOTE"'
$ git difftool -t caddiff HEAD~1 -- bracket.stp

See docs/git-integration.md for why CAD files need .gitattributes treatment and how to pick revisions.

As a GitHub Action (pull-request gate)

The repo ships a ready-made Action that pulls the same prebuilt image, runs the diff on two files in your repo, and uploads the report directory as a workflow artifact:

- uses: angel291592/caddiff@v0.4.0
  id: caddiff
  with:
    old: models/base.stp
    new: models/pr.stp
    image: ghcr.io/angel291592/caddiff:v0.4.0

- name: React to differences
  if: steps.caddiff.outputs.has-differences == 'true'
  run: echo "::warning::${{ steps.caddiff.outputs.diff-count }} CAD change(s) detected"

Outputs read from diff_manifest.json (has-differences, diff-count, report-dir) — the action fails only on exit code 2 (caddiff could not run); exit code 1 (differences found) is a result, and the report is still uploaded.

image: is spelled out on purpose. @v0.4.0 resolves action.yml from the v0.4.0 tag, and that tag's default is frozen at the previous release — so without this line the action and the geometry engine would run different versions. Same rule as the docker run above: pin the image, not the branch.

Known limits: fork PRs cannot comment on the PR with the default read-only token, and the report is delivered as an artifact rather than a PR comment — see the notes at the top of action.yml.


What it detects

Change type Example How it is reported
Part added / removed a bracket dropped from the assembly BOM-level: listed in summary.bom_added / bom_removed
Instance count changed 2 screws became 4 BOM-level: listed in summary.bom_count_mismatch
moved same part, translated or rotated displacement in mm/°, blue arrow on the image
shape_changed material added or removed boolean symmetric difference, magenta highlight, volume delta

Parts that could not be compared (too many faces, boolean timeout, no solid shape) are never silently dropped — they appear in summary.skipped_parts with a reason. "Not mentioned" must never be readable as "no difference".


What you get

report/
├── diff_manifest.json     machine-readable contract (start here in CI)
├── report.html            human-readable report with the images inline
├── report.md              same content as Markdown (good for PR comments)
├── images/*.png           overview + old/new close-ups per change
├── compare.pptx           only with --pptx
├── bom_diff.json          intermediate
└── geom_diff.json         intermediate

Each change gets an automatically chosen viewpoint: caddiff scans 26 candidate directions (6 faces + 12 edges + 8 corners), scores each on how much of the highlighted geometry is visible and how readable the framing is, and picks the winner. When the change is buried inside the assembly it says so on the image instead of pretending the shot is fine.


Exit codes — use it as a CI gate

Code Meaning
0 no differences
1 differences found
2 the run failed (bad input, timeout, crash)
- name: Check geometry changes
  run: |
    docker run --rm -v "$PWD:/data" ghcr.io/angel291592/caddiff:v0.4.0 \
      diff /data/base.stp /data/pr.stp -o /data/report

The distinction between 1 and 2 matters: a crashed FreeCAD must never look like "nothing changed".


How it compares

There are two different things people mean by "CAD diff", and they are not competitors — they are the two halves of one loop. Here is where caddiff sits.

caddiff text-to-CAD tools
text-to-cad, CADAM
SOLIDWORKS Compare
commercial revision checkers
What it is for verify what changed between two revisions generate / edit a model from a prompt verify revisions
Answers "did this model change, and how?" ✅ deterministic geometry comparison ❌ has no such notion ✅
Works on a file pair you already have ✅ ❌ needs a prompt and a regeneration ✅
Machine-readable change list ✅ diff_manifest.json ❌ ❌
Gates a pull request ✅ exit codes 0/1/2 ❌ ❌
Headless / runs in a container ✅ varies ❌
Needs a CAD licence ❌ ❌ ✅
Open source ✅ Apache-2.0 ✅ (varies) ❌

The complement, not the rival. text-to-CAD tools are extremely good at making changes and have nothing to say about verifying them. That gap widens as more edits are produced by agents instead of hands:

# an agent (or you) just rewrote the bracket
$ caddiff diff old.stp new.stp -o report
$ echo $?
1        # something changed — here is exactly what, and by how much

If you generate models with a text-to-CAD tool, caddiff is what you run after it.

Versus the commercial revision checkers. They solve the same problem well, and they cost a licence per seat, need the CAD application installed, and cannot run in CI. That is the entire reason this project exists — see The problem.

On the open-source side, a handful of projects in this space render a visual diff, and none of them ship BOM-level alignment, a machine-readable manifest, or CI exit codes. That is the gap caddiff fills.


Benchmarks (measured, not estimated)

All numbers below are wall-clock, software rendering, no GPU. Rendering dominates: each change costs one 26-direction scan (≈0.85 s per direction) plus three images.

Input Where it ran Changes found Wall clock
3-part synthetic assembly (examples/) source checkout, Windows + FreeCAD 1.1.3 2 ≈80 s
3-part synthetic assembly, same fixture official image, Linux + FreeCAD 0.21.2 2 ≈35 s
55-part production assembly source checkout, Windows + FreeCAD 1.1.3 3 290–366 s

The image is faster on the small fixture because the container is a clean Linux filesystem — no Windows Defender scanning of the intermediate STEP/BREP files. Do not read that as 0.21 being faster than 1.1.3; the two rows differ in more than one way.

Reproduce the first row yourself:

$ python caddiff/make_moved_fixture.py examples/fixtures
$ caddiff diff examples/fixtures/moved_old.stp examples/fixtures/moved_new.stp -o out

We are not claiming a speed record. If you need sub-second diffs on huge assemblies, this is not the tool for you yet — see the roadmap.


What this does not do

Being explicit about the boundaries, because a tool that over-promises is worse than no tool:

  • No tolerance analysis or GD&T. shape_changed means "the boolean symmetric difference is non-empty", not "this is out of tolerance".
  • No free-form surface deviation maps. We compare solids, not point clouds.
  • No material or density diff. The optional performance summary (--perf) is volume-based: solid volume, a volume-weighted centre, and volume inertia about the assembly centroid. A change of material between revisions is not detectable (STEP files usually carry no material information), and a mass figure is only produced when you pass a density you have confirmed yourself (--density).
  • No huge assemblies. The render step is serial; a 55-part assembly already takes minutes. There is no upper bound enforced, but the experience degrades.
  • Part pairing is heuristic. Same-named parts are matched by bounding-box proximity with a 50 % volume guard. This works well for the common "same assembly, edited in place" case and is known to be weak when parts are renamed wholesale or the whole assembly is moved. Unmatched parts are reported, never guessed. Measured on 24 pairs of real public assemblies: every structural change was caught, but 7 of 15 structurally unchanged pairs still report BOM add/remove pairs purely from renames (human renames, exporter auto-names, even case-only changes) — the name really did change, and geometry cannot pair across names, so read a symmetric added/removed list as "something was renamed". A same-named part that changed beyond the 50 % pairing guard is reported as a whole-part change (degraded, clearly labelled), never dropped.
  • No merge. caddiff tells you what changed. It does not merge two STEP files, and it will not try.
  • No hosted service. Everything runs locally or in your own CI. Nothing is uploaded.

How it works

old.stp ─┐
         ├─► [1] BOM alignment        (pure Python — part names, instance counts)
new.stp ─┘        │
                  ├─► [2] geometric diff     (FreeCAD: classify identical/moved/shape_changed,
                  │                           boolean symmetric difference in a killable subprocess)
                  ├─► [3] render             (FreeCAD GUI + 26-direction viewpoint search → PNG)
                  └─► [4] report             (manifest JSON + HTML + Markdown; PPTX on demand)

Two design rules that are load-bearing:

  • Every expensive geometry operation runs in a subprocess with a hard timeout. Face count does not predict boolean cost — we measured a 912-face part whose single cut ran past 840 s. A face-count gate alone cannot catch that; only a killable subprocess can.
  • FreeCAD is only ever talked to via subprocess + files. No compiled extensions, no dlopen, no shared memory. This keeps the LGPL/GPL boundary clean (see THIRD_PARTY_LICENSES.md) and keeps crashes contained.

Technical deep-dive: docs/pipeline.md.


Roadmap

  • v0.1 – v0.3 (released) — caddiff diff: BOM alignment, geometric diff, rendering, HTML/Markdown/JSON reports, Docker image, GitHub Action, CI exit codes.
  • next — an MCP server so an agent that just edited a model can verify its own work against the previous revision.
  • later — caddiff check (FEA sanity checks) and caddiff build (sandboxed modeling). The namespace is reserved; nothing is promised.

Star History Chart


Contributing

Bug reports, fixture pairs, and pull requests are welcome — see CONTRIBUTING.md. Please never attach a real customer model to an issue: CAD files routinely contain trade secrets, and the derived PNG/JSON/HTML carry the part names too. Use a minimal synthetic sample.

Security reports: SECURITY.md.


License

Repository source code is Apache-2.0 (LICENSE).

The Docker image bundles third-party components under their own licenses — FreeCAD (LGPL-2.1-or-later, and its distribution also contains GPL-licensed files), OpenCASCADE (LGPL-2.1 with the OCCT exception), numpy, Pillow, python-pptx. Those are not covered by the Apache-2.0 license of this repository. Full details, including how to obtain the corresponding source, are in THIRD_PARTY_LICENSES.md.

This project is not affiliated with or endorsed by the FreeCAD project or the FreeCAD Project Association. It uses Open CASCADE Technology.

Metadata

Release files for caddiff 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for caddiff 0.4.1
File Size Uploaded Provenance
caddiff-0.4.1.tar.gz 136.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for caddiff 0.4.1
File Interpreter ABI Platform
caddiff-0.4.1-py3-none-any.whl Python 3 none any Details

Total release size: 277.6 kB

Release files / caddiff-0.4.1.tar.gz

Download URL caddiff-0.4.1.tar.gz
Size 136.8 kB
Tags Source
SHA-256 checksum
How to use checksums
02d454e8665fccb1023c1d7a6264e0fc79e05c2245016656ac120b5ed4ed6557
BLAKE2b-256 checksum
How to use checksums
119c7a29ef86223f7e8816283d52a3eccc74b0ff91e653d625d9812eec132096
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / caddiff-0.4.1-py3-none-any.whl

Download URL caddiff-0.4.1-py3-none-any.whl
Size 140.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
44574625a203690a87e731c7e9e13fd29c5c6b905679c9a87e3635b66d6ea7aa
BLAKE2b-256 checksum
How to use checksums
e371f9e077646de5ffe0c1d6aa2b69e6685ae51f22587c2e41dfe9daf6388112
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for caddiff 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for caddiff-0.4.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page