Metadata-Version: 2.4
Name: caddiff
Version: 0.3.6
Summary: git diff for CAD assemblies — compare two STEP/STP assemblies and show what changed
Author: The caddiff Authors
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/angel291592/caddiff
Project-URL: Documentation, https://github.com/angel291592/caddiff/tree/main/docs
Project-URL: Demo, https://angel291592.github.io/caddiff/report.html
Project-URL: Changelog, https://github.com/angel291592/caddiff/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/angel291592/caddiff/issues
Keywords: cad,step,stp,diff,freecad,bom,assembly,git,cli
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Manufacturing
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Provides-Extra: pptx
Requires-Dist: python-pptx>=0.6.23; extra == "pptx"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Dynamic: license-file

<div align="center">

# caddiff

English | [简体中文](https://github.com/angel291592/caddiff/blob/main/README.zh-CN.md)

**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](https://github.com/angel291592/caddiff/actions/workflows/ci.yml/badge.svg)](https://github.com/angel291592/caddiff/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/angel291592/caddiff/blob/main/LICENSE)
[![Python](https://img.shields.io/badge/python-3.9%2B-blue.svg)](pyproject.toml)

</div>

![caddiff highlighting a moved part and a removed feature](https://raw.githubusercontent.com/angel291592/caddiff/main/docs/images/hero.png)

> 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/`](https://github.com/angel291592/caddiff/tree/main/examples) — no customer data anywhere in this repo.
> **[Browse the full sample report, rendered online, no install needed →](https://angel291592.github.io/caddiff/report.html)**

---

## 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 (recommended — nothing to install)

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

`v0.3.5` 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`:

```console
$ 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`](https://github.com/angel291592/caddiff/blob/main/docs/pipeline.md)).

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:

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

See [`docs/git-integration.md`](https://github.com/angel291592/caddiff/blob/main/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:

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

- 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.3.5` resolves `action.yml` from the `v0.3.5`
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`](https://github.com/angel291592/caddiff/blob/main/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) |

```yaml
- name: Check geometry changes
  run: |
    docker run --rm -v "$PWD:/data" ghcr.io/angel291592/caddiff:v0.3.5 \
      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<br><sub>[text-to-cad](https://github.com/earthtojake/text-to-cad), [CADAM](https://github.com/Adam-CAD/CADAM)</sub> | SOLIDWORKS Compare<br><sub>commercial revision checkers</sub> |
|---|---|---|---|
| **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:

```console
# 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](#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/`](https://github.com/angel291592/caddiff/tree/main/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:

```console
$ 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 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`](https://github.com/angel291592/caddiff/blob/main/THIRD_PARTY_LICENSES.md)) and keeps crashes contained.

Technical deep-dive: [`docs/pipeline.md`](https://github.com/angel291592/caddiff/blob/main/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](https://api.star-history.com/image?repos=angel291592/caddiff&type=Date)](https://star-history.com/#angel291592/caddiff&Date)

---

## Contributing

Bug reports, fixture pairs, and pull requests are welcome — see
[`CONTRIBUTING.md`](https://github.com/angel291592/caddiff/blob/main/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`](https://github.com/angel291592/caddiff/blob/main/SECURITY.md).

---

## License

Repository source code is **Apache-2.0** ([`LICENSE`](https://github.com/angel291592/caddiff/blob/main/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`](https://github.com/angel291592/caddiff/blob/main/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.
