mesh2cad 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ name: ci
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ - uses: actions/setup-python@v5
9
+ with:
10
+ python-version: "3.12"
11
+ - run: pip install -e .[dev]
12
+ - run: pytest -q
@@ -0,0 +1,24 @@
1
+ name: publish
2
+ # Manual only: publishes to PyPI via Trusted Publishing (no token stored).
3
+ on:
4
+ workflow_dispatch:
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-python@v5
11
+ with: {python-version: "3.12"}
12
+ - run: pip install build twine && python -m build && twine check dist/*
13
+ - uses: actions/upload-artifact@v4
14
+ with: {name: dist, path: dist}
15
+ pypi:
16
+ needs: build
17
+ runs-on: ubuntu-latest
18
+ environment: pypi
19
+ permissions:
20
+ id-token: write
21
+ steps:
22
+ - uses: actions/download-artifact@v4
23
+ with: {name: dist, path: dist}
24
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ *.egg-info/
2
+ *.py[cod]
3
+ *.step
4
+ .pytest_cache/
5
+ .venv/
6
+ __pycache__/
7
+ build/
8
+ dist/
9
+ out/
10
+ docs/__pycache__/
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+ - Benchmark (`benchmarks/run.py`) now includes the cone parts (countersunk plate, tapered boss, chamfered boss) and a cone face column; README table refreshed.
5
+ - Docs: Limits and `server.json` description now list cones; Glama score badge and MCP directory links in the README.
6
+
7
+ ## 0.3.0 — 2026-10-15
8
+ - Cone primitive: `Cone`, `fit_cone` (constant normal-to-axis angle + tangent planes through the apex), `refine_cone`.
9
+ - Segmentation fits cones for chamfers, countersinks and tapered bosses; a coaxial cylinder + cone component is split by normal-to-axis angle before fitting. Cones win over sphere/cylinder fits only when clearly better.
10
+ - Exact B-rep conical frustum faces between two planes perpendicular to the axis, with plane ∩ cone circles on the planar faces. Hybrid fallback snaps vertices onto the cone.
11
+ - Fidelity report counts `cone` faces. CLI `--no-cones` and MCP `cones` flag (default on).
12
+ - Synthetic roundtrip: countersunk plate, 10° tapered boss, chamfered boss.
13
+
14
+ ## 0.2.0 — 2026-10-14
15
+ - Sphere primitive: `Sphere`, `fit_sphere` (normal rays + algebraic fit), `refine_sphere`.
16
+ - Segmentation emits `Region(surface=Sphere)` for leftover caps and for clusters of small patches that tile a sphere (noisy or faceted ball ends).
17
+ - Exact B-rep spherical faces when the region is bounded by plane ∩ sphere circles (cap or zone). Hybrid fallback snaps vertices onto the sphere.
18
+ - Fidelity report counts `sphere` faces. CLI `--no-spheres` and MCP `spheres` flag (default on).
19
+ - Synthetic roundtrip: hemispherical dome, spherical pocket, and a sphere clipped to a zone between two planes.
20
+
21
+ ## 0.1.0 — 2026-10-14
22
+ First release.
23
+ - Mesh loading/cleaning for STL, OBJ, PLY (trimesh).
24
+ - Segmentation into planes and cylinders (region growing, strip-chain cylinder detection, robust cylinder refinement, region merge/absorb).
25
+ - Exact B-rep STEP export via OpenCascade for planar parts with through-holes and bosses; hybrid/faceted fallbacks.
26
+ - Fidelity report (Hausdorff, RMS, volume error, face types, validity) as JSON/Markdown.
27
+ - CLI: `mesh2cad convert`, `mesh2cad report`, `mesh2cad mcp`.
28
+ - MCP server with `convert`, `analyze`, `report` tools.
29
+ - Synthetic self-test benchmark (6 parts × 3 noise levels × 3 seeds, all exact) in CI.
@@ -0,0 +1,10 @@
1
+ # Stdio MCP server for Glama / local Docker use.
2
+ FROM python:3.12-slim
3
+
4
+ WORKDIR /app
5
+ COPY pyproject.toml README.md LICENSE ./
6
+ COPY src ./src
7
+
8
+ RUN pip install --no-cache-dir ".[mcp]"
9
+
10
+ ENTRYPOINT ["mesh2cad", "mcp"]
mesh2cad-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emir Mrkaljevic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,117 @@
1
+ Metadata-Version: 2.5
2
+ Name: mesh2cad
3
+ Version: 0.3.0
4
+ Summary: Convert STL/OBJ/PLY meshes into B-rep CAD (STEP) with primitive fitting
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: cadquery-ocp
9
+ Requires-Dist: networkx
10
+ Requires-Dist: numpy
11
+ Requires-Dist: rtree
12
+ Requires-Dist: scipy
13
+ Requires-Dist: trimesh
14
+ Provides-Extra: dev
15
+ Requires-Dist: cadquery; extra == 'dev'
16
+ Requires-Dist: mcp; extra == 'dev'
17
+ Requires-Dist: pytest; extra == 'dev'
18
+ Provides-Extra: mcp
19
+ Requires-Dist: mcp; extra == 'mcp'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # mesh2cad
23
+ [![ci](https://github.com/emirmrkaljevic/mesh2cad/actions/workflows/ci.yml/badge.svg)](https://github.com/emirmrkaljevic/mesh2cad/actions/workflows/ci.yml)
24
+ [![emirmrkaljevic/mesh2cad MCP server](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad/badges/score.svg)](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad)
25
+
26
+ Turn 3D meshes (STL/OBJ/PLY) into **real, editable CAD (STEP)** — fitted planes, cylinders, spheres, and cones instead of triangle soup. CLI + MCP server for AI agents. MIT, pure Python on top of OpenCascade.
27
+
28
+ ![demo](https://raw.githubusercontent.com/emirmrkaljevic/mesh2cad/main/docs/demo.gif)
29
+
30
+ ![before/after](https://raw.githubusercontent.com/emirmrkaljevic/mesh2cad/main/docs/before_after.png)
31
+
32
+ *Left: 21k-triangle mesh with 0.1 mm noise. Right: the STEP mesh2cad writes — 10 planes + 2 cylinders, max deviation 0.05 mm from the true part.*
33
+
34
+ ## Install
35
+ ```
36
+ pip install "mesh2cad[mcp] @ git+https://github.com/emirmrkaljevic/mesh2cad"
37
+ ```
38
+ (PyPI release coming.)
39
+
40
+ ## Use
41
+ ```
42
+ mesh2cad convert part.stl -o part.step --tol 0.1 --report report.md
43
+ mesh2cad report part.stl --json
44
+ ```
45
+ `--tol` ≈ the noise / accuracy of your mesh in model units (default 0.1 % of the bounding-box diagonal).
46
+
47
+ Output: STEP + fidelity report — method (`exact` / `faceted-hybrid` / `faceted`), face types, solid validity, two-sided Hausdorff / RMS deviation, volume error, % area per surface type.
48
+
49
+ ### MCP server (Claude Desktop, Cursor, …)
50
+ ```json
51
+ { "mcpServers": { "mesh2cad": { "command": "mesh2cad", "args": ["mcp"] } } }
52
+ ```
53
+ Tools: `convert(input_path, output_path?, tol?, cylinders?, spheres?, cones?)`, `analyze(input_path, tol?, cylinders?, spheres?, cones?)`, `report(input_path, tol?, cylinders?, spheres?, cones?)`. `spheres` and `cones` default on.
54
+
55
+ Listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad) and the [TensorBlock MCP catalog](https://tensorblock.co/mcp/servers/github-emirmrkaljevic-mesh2cad-0f1c0e66).
56
+
57
+ ## How it works
58
+ 1. Load + clean mesh (trimesh); Taubin-smoothed copy for normals.
59
+ 2. Region growing into planes; strip chains and leftovers are fitted as cylinders, spheres, or cones (normal-ray / algebraic / constant-normal-angle fits, robust nonlinear refinement). A coaxial cylinder + cone component (countersink, chamfered pin) is split by normal-to-axis angle first. Small noisy patches that tile a sphere are merged into one spherical region. Split/redundant regions are merged and absorbed.
60
+ 3. Exact B-rep (OCP / OpenCascade): plane polygons from triple-plane intersections, circles from plane ∩ cylinder and plane ∩ sphere, plane ∩ cone, cylindrical faces, spherical caps and zones, conical frustums, sewn into a solid and validated.
61
+ 4. Fallbacks: hybrid faceted solid (vertices snapped to fitted surfaces, coplanar triangles merged), then raw faceted solid.
62
+ 5. Fidelity report against the input mesh.
63
+
64
+ ## Self-test benchmark
65
+ Ground-truth cadquery parts → tessellated + subdivided (≤ 2 mm edges) → Gaussian vertex noise → mesh2cad → compared with the truth. 3 seeds, worst case shown (`python benchmarks/run.py`); the same cases run in CI.
66
+
67
+ | part | noise σ (mm) | exact | faces (plane/cyl[/sph[/cone]]) | Hausdorff (mm) | RMS (mm) | vol err % | time s |
68
+ |---|---|---|---|---|---|---|---|
69
+ | cube | 0.0 | 3/3 | 6/0 | 0.000 | 0.000 | 0.00 | 0.7 |
70
+ | cube | 0.05 | 3/3 | 6/0 | 0.019 | 0.006 | 0.07 | 0.6 |
71
+ | cube | 0.2 | 3/3 | 6/0 | 0.075 | 0.026 | 0.34 | 0.6 |
72
+ | l_bracket | 0.0 | 3/3 | 8/0 | 0.000 | 0.000 | 0.00 | 0.9 |
73
+ | l_bracket | 0.05 | 3/3 | 8/0 | 0.040 | 0.006 | 0.03 | 1.0 |
74
+ | l_bracket | 0.2 | 3/3 | 8/0 | 0.216 | 0.026 | 0.15 | 1.0 |
75
+ | stepped_block | 0.0 | 3/3 | 11/0 | 0.000 | 0.000 | 0.00 | 2.1 |
76
+ | stepped_block | 0.05 | 3/3 | 11/0 | 0.032 | 0.006 | 0.02 | 2.3 |
77
+ | stepped_block | 0.2 | 3/3 | 11/0 | 0.136 | 0.022 | 0.07 | 2.1 |
78
+ | plate_with_hole | 0.0 | 3/3 | 6/1 | 0.015 | 0.002 | 0.00 | 1.9 |
79
+ | plate_with_hole | 0.05 | 3/3 | 6/1 | 0.028 | 0.006 | 0.05 | 1.9 |
80
+ | plate_with_hole | 0.2 | 3/3 | 6/1 | 0.078 | 0.023 | 0.18 | 1.9 |
81
+ | boss | 0.0 | 3/3 | 7/1 | 0.027 | 0.006 | 0.01 | 1.9 |
82
+ | boss | 0.05 | 3/3 | 7/1 | 0.031 | 0.007 | 0.04 | 2.5 |
83
+ | boss | 0.2 | 3/3 | 7/1 | 0.090 | 0.016 | 0.07 | 3.5 |
84
+ | ribbed_plate | 0.0 | 3/3 | 10/2 | 0.013 | 0.002 | 0.00 | 2.3 |
85
+ | ribbed_plate | 0.05 | 3/3 | 10/2 | 0.028 | 0.005 | 0.03 | 2.4 |
86
+ | ribbed_plate | 0.2 | 3/3 | 10/2 | 0.108 | 0.020 | 0.12 | 2.3 |
87
+ | spherical_dome | 0.0 | 3/3 | 6/0/1 | 0.065 | 0.007 | 0.04 | 1.9 |
88
+ | spherical_dome | 0.05 | 3/3 | 6/0/1 | 0.067 | 0.008 | 0.06 | 2.0 |
89
+ | spherical_dome | 0.2 | 3/3 | 6/0/1 | 0.241 | 0.028 | 0.25 | 1.9 |
90
+ | spherical_pocket | 0.0 | 3/3 | 6/0/1 | 0.046 | 0.004 | 0.02 | 2.2 |
91
+ | spherical_pocket | 0.05 | 3/3 | 6/0/1 | 0.057 | 0.005 | 0.03 | 2.1 |
92
+ | spherical_pocket | 0.2 | 3/3 | 6/0/1 | 0.075 | 0.015 | 0.13 | 2.1 |
93
+ | countersunk_plate | 0.0 | 3/3 | 6/1/0/1 | 0.039 | 0.003 | 0.01 | 2.2 |
94
+ | countersunk_plate | 0.05 | 3/3 | 6/1/0/1 | 0.055 | 0.009 | 0.07 | 2.1 |
95
+ | countersunk_plate | 0.2 | 0/3 | 16322/0, 16324/0 | 0.745 | 0.130 | 0.17 | 41.6 |
96
+ | tapered_boss | 0.0 | 3/3 | 7/0/0/1 | 0.086 | 0.011 | 0.05 | 1.8 |
97
+ | tapered_boss | 0.05 | 3/3 | 7/0/0/1 | 0.095 | 0.012 | 0.05 | 1.7 |
98
+ | tapered_boss | 0.2 | 3/3 | 7/0/0/1 | 0.123 | 0.025 | 0.22 | 1.7 |
99
+ | chamfered_boss | 0.0 | 3/3 | 7/1/0/1 | 0.053 | 0.005 | 0.02 | 2.9 |
100
+ | chamfered_boss | 0.05 | 1/3 | 16461/0, 16473/0, 7/1/0/1 | 0.160 | 0.035 | 0.08 | 31.6 |
101
+ | chamfered_boss | 0.2 | 0/3 | 16565/0, 16569/0, 16570/0 | 0.703 | 0.133 | 0.26 | 34.8 |
102
+
103
+ "exact" = valid B-rep with the true face count. All rows are the worst of 3 seeds from `python benchmarks/run.py` (v0.3.0). Rows with fewer than 3/3 exact fell back to a faceted solid (face count = triangles): noisy narrow countersink/chamfer bands don't constrain a cone yet, so CI checks those parts only where they are exact.
104
+
105
+ ## Limits (v0.3)
106
+ Planes, cylinders, spheres, and cones. Cylinders must end on planes perpendicular to their axis (through-holes, bosses, pins). Spheres are exact when each boundary loop is a single plane ∩ sphere circle (domes, spherical bosses, spherical pockets, and zones between two parallel planes). A sphere that runs into an edge or a cylinder falls back to the hybrid solid — vertices are still snapped onto the fitted sphere, but the face stays faceted. Cones are exact when they run between two planes perpendicular to their axis: countersinks, chamfered holes and bosses, drafted/tapered bosses. Cones may also meet a coaxial cylinder (countersunk hole, chamfered pin). A cone that ends in its apex falls back to the hybrid solid, and countersinks are reliable up to ~0.05 mm noise, narrow chamfers on clean meshes only for now (wide drafted walls up to 0.2 mm). No fillets or free-form yet.
107
+
108
+ ## Development
109
+ ```
110
+ pip install -e .[dev]
111
+ pytest
112
+ python docs/make_demo.py # regenerate images (needs matplotlib, pillow)
113
+ ```
114
+
115
+ License: MIT
116
+
117
+ <!-- mcp-name: io.github.emirmrkaljevic/mesh2cad -->
@@ -0,0 +1,96 @@
1
+ # mesh2cad
2
+ [![ci](https://github.com/emirmrkaljevic/mesh2cad/actions/workflows/ci.yml/badge.svg)](https://github.com/emirmrkaljevic/mesh2cad/actions/workflows/ci.yml)
3
+ [![emirmrkaljevic/mesh2cad MCP server](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad/badges/score.svg)](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad)
4
+
5
+ Turn 3D meshes (STL/OBJ/PLY) into **real, editable CAD (STEP)** — fitted planes, cylinders, spheres, and cones instead of triangle soup. CLI + MCP server for AI agents. MIT, pure Python on top of OpenCascade.
6
+
7
+ ![demo](https://raw.githubusercontent.com/emirmrkaljevic/mesh2cad/main/docs/demo.gif)
8
+
9
+ ![before/after](https://raw.githubusercontent.com/emirmrkaljevic/mesh2cad/main/docs/before_after.png)
10
+
11
+ *Left: 21k-triangle mesh with 0.1 mm noise. Right: the STEP mesh2cad writes — 10 planes + 2 cylinders, max deviation 0.05 mm from the true part.*
12
+
13
+ ## Install
14
+ ```
15
+ pip install "mesh2cad[mcp] @ git+https://github.com/emirmrkaljevic/mesh2cad"
16
+ ```
17
+ (PyPI release coming.)
18
+
19
+ ## Use
20
+ ```
21
+ mesh2cad convert part.stl -o part.step --tol 0.1 --report report.md
22
+ mesh2cad report part.stl --json
23
+ ```
24
+ `--tol` ≈ the noise / accuracy of your mesh in model units (default 0.1 % of the bounding-box diagonal).
25
+
26
+ Output: STEP + fidelity report — method (`exact` / `faceted-hybrid` / `faceted`), face types, solid validity, two-sided Hausdorff / RMS deviation, volume error, % area per surface type.
27
+
28
+ ### MCP server (Claude Desktop, Cursor, …)
29
+ ```json
30
+ { "mcpServers": { "mesh2cad": { "command": "mesh2cad", "args": ["mcp"] } } }
31
+ ```
32
+ Tools: `convert(input_path, output_path?, tol?, cylinders?, spheres?, cones?)`, `analyze(input_path, tol?, cylinders?, spheres?, cones?)`, `report(input_path, tol?, cylinders?, spheres?, cones?)`. `spheres` and `cones` default on.
33
+
34
+ Listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/mesh2cad) and the [TensorBlock MCP catalog](https://tensorblock.co/mcp/servers/github-emirmrkaljevic-mesh2cad-0f1c0e66).
35
+
36
+ ## How it works
37
+ 1. Load + clean mesh (trimesh); Taubin-smoothed copy for normals.
38
+ 2. Region growing into planes; strip chains and leftovers are fitted as cylinders, spheres, or cones (normal-ray / algebraic / constant-normal-angle fits, robust nonlinear refinement). A coaxial cylinder + cone component (countersink, chamfered pin) is split by normal-to-axis angle first. Small noisy patches that tile a sphere are merged into one spherical region. Split/redundant regions are merged and absorbed.
39
+ 3. Exact B-rep (OCP / OpenCascade): plane polygons from triple-plane intersections, circles from plane ∩ cylinder and plane ∩ sphere, plane ∩ cone, cylindrical faces, spherical caps and zones, conical frustums, sewn into a solid and validated.
40
+ 4. Fallbacks: hybrid faceted solid (vertices snapped to fitted surfaces, coplanar triangles merged), then raw faceted solid.
41
+ 5. Fidelity report against the input mesh.
42
+
43
+ ## Self-test benchmark
44
+ Ground-truth cadquery parts → tessellated + subdivided (≤ 2 mm edges) → Gaussian vertex noise → mesh2cad → compared with the truth. 3 seeds, worst case shown (`python benchmarks/run.py`); the same cases run in CI.
45
+
46
+ | part | noise σ (mm) | exact | faces (plane/cyl[/sph[/cone]]) | Hausdorff (mm) | RMS (mm) | vol err % | time s |
47
+ |---|---|---|---|---|---|---|---|
48
+ | cube | 0.0 | 3/3 | 6/0 | 0.000 | 0.000 | 0.00 | 0.7 |
49
+ | cube | 0.05 | 3/3 | 6/0 | 0.019 | 0.006 | 0.07 | 0.6 |
50
+ | cube | 0.2 | 3/3 | 6/0 | 0.075 | 0.026 | 0.34 | 0.6 |
51
+ | l_bracket | 0.0 | 3/3 | 8/0 | 0.000 | 0.000 | 0.00 | 0.9 |
52
+ | l_bracket | 0.05 | 3/3 | 8/0 | 0.040 | 0.006 | 0.03 | 1.0 |
53
+ | l_bracket | 0.2 | 3/3 | 8/0 | 0.216 | 0.026 | 0.15 | 1.0 |
54
+ | stepped_block | 0.0 | 3/3 | 11/0 | 0.000 | 0.000 | 0.00 | 2.1 |
55
+ | stepped_block | 0.05 | 3/3 | 11/0 | 0.032 | 0.006 | 0.02 | 2.3 |
56
+ | stepped_block | 0.2 | 3/3 | 11/0 | 0.136 | 0.022 | 0.07 | 2.1 |
57
+ | plate_with_hole | 0.0 | 3/3 | 6/1 | 0.015 | 0.002 | 0.00 | 1.9 |
58
+ | plate_with_hole | 0.05 | 3/3 | 6/1 | 0.028 | 0.006 | 0.05 | 1.9 |
59
+ | plate_with_hole | 0.2 | 3/3 | 6/1 | 0.078 | 0.023 | 0.18 | 1.9 |
60
+ | boss | 0.0 | 3/3 | 7/1 | 0.027 | 0.006 | 0.01 | 1.9 |
61
+ | boss | 0.05 | 3/3 | 7/1 | 0.031 | 0.007 | 0.04 | 2.5 |
62
+ | boss | 0.2 | 3/3 | 7/1 | 0.090 | 0.016 | 0.07 | 3.5 |
63
+ | ribbed_plate | 0.0 | 3/3 | 10/2 | 0.013 | 0.002 | 0.00 | 2.3 |
64
+ | ribbed_plate | 0.05 | 3/3 | 10/2 | 0.028 | 0.005 | 0.03 | 2.4 |
65
+ | ribbed_plate | 0.2 | 3/3 | 10/2 | 0.108 | 0.020 | 0.12 | 2.3 |
66
+ | spherical_dome | 0.0 | 3/3 | 6/0/1 | 0.065 | 0.007 | 0.04 | 1.9 |
67
+ | spherical_dome | 0.05 | 3/3 | 6/0/1 | 0.067 | 0.008 | 0.06 | 2.0 |
68
+ | spherical_dome | 0.2 | 3/3 | 6/0/1 | 0.241 | 0.028 | 0.25 | 1.9 |
69
+ | spherical_pocket | 0.0 | 3/3 | 6/0/1 | 0.046 | 0.004 | 0.02 | 2.2 |
70
+ | spherical_pocket | 0.05 | 3/3 | 6/0/1 | 0.057 | 0.005 | 0.03 | 2.1 |
71
+ | spherical_pocket | 0.2 | 3/3 | 6/0/1 | 0.075 | 0.015 | 0.13 | 2.1 |
72
+ | countersunk_plate | 0.0 | 3/3 | 6/1/0/1 | 0.039 | 0.003 | 0.01 | 2.2 |
73
+ | countersunk_plate | 0.05 | 3/3 | 6/1/0/1 | 0.055 | 0.009 | 0.07 | 2.1 |
74
+ | countersunk_plate | 0.2 | 0/3 | 16322/0, 16324/0 | 0.745 | 0.130 | 0.17 | 41.6 |
75
+ | tapered_boss | 0.0 | 3/3 | 7/0/0/1 | 0.086 | 0.011 | 0.05 | 1.8 |
76
+ | tapered_boss | 0.05 | 3/3 | 7/0/0/1 | 0.095 | 0.012 | 0.05 | 1.7 |
77
+ | tapered_boss | 0.2 | 3/3 | 7/0/0/1 | 0.123 | 0.025 | 0.22 | 1.7 |
78
+ | chamfered_boss | 0.0 | 3/3 | 7/1/0/1 | 0.053 | 0.005 | 0.02 | 2.9 |
79
+ | chamfered_boss | 0.05 | 1/3 | 16461/0, 16473/0, 7/1/0/1 | 0.160 | 0.035 | 0.08 | 31.6 |
80
+ | chamfered_boss | 0.2 | 0/3 | 16565/0, 16569/0, 16570/0 | 0.703 | 0.133 | 0.26 | 34.8 |
81
+
82
+ "exact" = valid B-rep with the true face count. All rows are the worst of 3 seeds from `python benchmarks/run.py` (v0.3.0). Rows with fewer than 3/3 exact fell back to a faceted solid (face count = triangles): noisy narrow countersink/chamfer bands don't constrain a cone yet, so CI checks those parts only where they are exact.
83
+
84
+ ## Limits (v0.3)
85
+ Planes, cylinders, spheres, and cones. Cylinders must end on planes perpendicular to their axis (through-holes, bosses, pins). Spheres are exact when each boundary loop is a single plane ∩ sphere circle (domes, spherical bosses, spherical pockets, and zones between two parallel planes). A sphere that runs into an edge or a cylinder falls back to the hybrid solid — vertices are still snapped onto the fitted sphere, but the face stays faceted. Cones are exact when they run between two planes perpendicular to their axis: countersinks, chamfered holes and bosses, drafted/tapered bosses. Cones may also meet a coaxial cylinder (countersunk hole, chamfered pin). A cone that ends in its apex falls back to the hybrid solid, and countersinks are reliable up to ~0.05 mm noise, narrow chamfers on clean meshes only for now (wide drafted walls up to 0.2 mm). No fillets or free-form yet.
86
+
87
+ ## Development
88
+ ```
89
+ pip install -e .[dev]
90
+ pytest
91
+ python docs/make_demo.py # regenerate images (needs matplotlib, pillow)
92
+ ```
93
+
94
+ License: MIT
95
+
96
+ <!-- mcp-name: io.github.emirmrkaljevic/mesh2cad -->
@@ -0,0 +1,44 @@
1
+ """Synthetic benchmark: cadquery parts -> mesh -> noise -> mesh2cad -> compare to truth.
2
+
3
+ Usage: python benchmarks/run.py [--seeds 3]
4
+ """
5
+ import argparse
6
+ import pathlib
7
+ import sys
8
+ import time
9
+
10
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "tests"))
11
+ from parts import CONES, CURVED, PLANAR, SPHERES, to_mesh # noqa: E402
12
+
13
+ from mesh2cad.convert import convert_mesh # noqa: E402
14
+ from mesh2cad.io import add_noise # noqa: E402
15
+
16
+ ap = argparse.ArgumentParser()
17
+ ap.add_argument("--seeds", type=int, default=3)
18
+ a = ap.parse_args()
19
+ print("| part | noise σ (mm) | exact | faces (plane/cyl[/sph[/cone]]) | Hausdorff (mm) | RMS (mm) | vol err % | time s |")
20
+ print("|---|---|---|---|---|---|---|---|")
21
+
22
+
23
+ def _faces(f):
24
+ parts = [str(f.get("plane", 0)), str(f.get("cylinder", 0))]
25
+ if f.get("sphere") or f.get("cone") or f.get("other"):
26
+ parts.append(str(f.get("sphere", 0)))
27
+ if f.get("cone"):
28
+ parts.append(str(f.get("cone", 0)))
29
+ s = "/".join(parts)
30
+ return s + ("+" if f.get("other") else "")
31
+
32
+
33
+ for name, fn in {**PLANAR, **CURVED, **SPHERES, **CONES}.items():
34
+ ref = to_mesh(fn())
35
+ for sigma in (0.0, 0.05, 0.2):
36
+ ok, H, R, V, T, F = 0, [], [], [], [], set()
37
+ for seed in range(a.seeds):
38
+ t = time.time()
39
+ r = convert_mesh(add_noise(ref, sigma, seed=seed), tol=max(sigma, 0.01), ref=ref).report
40
+ T.append(time.time() - t)
41
+ ok += r["method"] == "exact" and r["output"]["valid_solid"]
42
+ F.add(_faces(r["output"]["faces"]))
43
+ H.append(r["deviation"]["hausdorff"]); R.append(r["deviation"]["rms"]); V.append(r["volume_error_pct"])
44
+ print(f"| {name} | {sigma} | {ok}/{a.seeds} | {', '.join(sorted(F))} | {max(H):.3f} | {max(R):.3f} | {max(V):.2f} | {max(T):.1f} |")
Binary file
Binary file
@@ -0,0 +1,84 @@
1
+ """Render README before/after images and demo GIF (needs matplotlib, pillow)."""
2
+ import pathlib
3
+ import sys
4
+
5
+ import matplotlib
6
+ matplotlib.use("Agg")
7
+ import matplotlib.pyplot as plt # noqa: E402
8
+ import numpy as np # noqa: E402
9
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection # noqa: E402
10
+ from PIL import Image # noqa: E402
11
+
12
+ ROOT = pathlib.Path(__file__).resolve().parents[1]
13
+ sys.path.insert(0, str(ROOT / "tests"))
14
+ import cadquery as cq # noqa: E402
15
+ from parts import ribbed_plate as demo_part, to_mesh # noqa: E402
16
+
17
+ from mesh2cad.convert import convert_mesh # noqa: E402
18
+ from mesh2cad.io import add_noise # noqa: E402
19
+ from mesh2cad.report import shape_to_mesh # noqa: E402
20
+ from OCP.TopAbs import TopAbs_FACE # noqa: E402
21
+ from OCP.TopExp import TopExp_Explorer # noqa: E402
22
+
23
+ OUT = ROOT / "docs"
24
+
25
+
26
+ def faces_of(shape):
27
+ exp, out = TopExp_Explorer(shape, TopAbs_FACE), []
28
+ while exp.More():
29
+ out.append(shape_to_mesh(exp.Current(), 0.05))
30
+ exp.Next()
31
+ return out
32
+
33
+
34
+ def draw(ax, polys, colors, edges, title, azim):
35
+ ax.clear()
36
+ pc = Poly3DCollection(polys, facecolors=colors, edgecolors=colors if edges == "same" else edges,
37
+ linewidths=0.15 if edges != "same" else 0.4)
38
+ ax.add_collection3d(pc)
39
+ ax.set_xlim(-20, 20); ax.set_ylim(-20, 20); ax.set_zlim(-10, 14)
40
+ ax.set_box_aspect((1, 1, 0.65))
41
+ ax.view_init(28, azim)
42
+ ax.set_axis_off()
43
+ ax.set_title(title, fontsize=11)
44
+
45
+
46
+ def shade(normals, base):
47
+ light = np.array([0.4, -0.5, 0.75]); light /= np.linalg.norm(light)
48
+ k = 0.45 + 0.55 * np.clip(normals @ light, 0, 1)
49
+ return np.clip(np.asarray(base)[None, :3] * k[:, None], 0, 1)
50
+
51
+
52
+ def main():
53
+ ref = to_mesh(demo_part())
54
+ noisy = add_noise(ref, 0.1, seed=0)
55
+ res = convert_mesh(noisy, tol=0.1, ref=ref)
56
+ r = res.report
57
+ print(r["method"], r["output"]["faces"], r["deviation"])
58
+ before = (noisy.triangles, shade(noisy.face_normals, (0.75, 0.75, 0.8)), (0.2, 0.2, 0.25, 0.6))
59
+ cmap = plt.get_cmap("tab20")
60
+ P, C = [], []
61
+ for i, fm in enumerate(faces_of(res.shape)):
62
+ P += list(fm.triangles)
63
+ C += list(shade(fm.face_normals, cmap(i % 20)))
64
+ after = (np.array(P), np.array(C), "same")
65
+ f = r["output"]["faces"]
66
+ t_before = f"input: {len(noisy.faces)} noisy triangles"
67
+ t_after = f"mesh2cad STEP: {f.get('plane', 0)} planes + {f.get('cylinder', 0)} cylinders"
68
+ fig = plt.figure(figsize=(10, 4.4), dpi=90)
69
+ a1, a2 = fig.add_subplot(121, projection="3d"), fig.add_subplot(122, projection="3d")
70
+ draw(a1, *before, t_before, -60); draw(a2, *after, t_after, -60)
71
+ fig.tight_layout(); fig.savefig(OUT / "before_after.png"); plt.close(fig)
72
+ frames = []
73
+ for az in range(-60, 300, 15):
74
+ fig = plt.figure(figsize=(8, 3.6), dpi=70)
75
+ a1, a2 = fig.add_subplot(121, projection="3d"), fig.add_subplot(122, projection="3d")
76
+ draw(a1, *before, t_before, az); draw(a2, *after, t_after, az)
77
+ fig.tight_layout(); fig.canvas.draw()
78
+ frames.append(Image.fromarray(np.asarray(fig.canvas.buffer_rgba())[..., :3]).convert("P", palette=Image.ADAPTIVE))
79
+ plt.close(fig)
80
+ frames[0].save(OUT / "demo.gif", save_all=True, append_images=frames[1:], duration=120, loop=0, optimize=True)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ main()
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": ["emirmrkaljevic"]
4
+ }
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mesh2cad"
7
+ version = "0.3.0"
8
+ description = "Convert STL/OBJ/PLY meshes into B-rep CAD (STEP) with primitive fitting"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = ["trimesh", "rtree", "networkx", "numpy", "scipy", "cadquery-ocp"]
13
+
14
+ [project.optional-dependencies]
15
+ mcp = ["mcp"]
16
+ dev = ["pytest", "cadquery", "mcp"]
17
+
18
+ [project.scripts]
19
+ mesh2cad = "mesh2cad.cli:main"
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
+ "name": "io.github.emirmrkaljevic/mesh2cad",
4
+ "description": "Mesh (STL/OBJ/PLY) to editable STEP CAD: fitted planes, cylinders, spheres, cones + fidelity report",
5
+ "repository": {"url": "https://github.com/emirmrkaljevic/mesh2cad", "source": "github"},
6
+ "version": "0.3.0",
7
+ "packages": [
8
+ {
9
+ "registryType": "pypi",
10
+ "identifier": "mesh2cad",
11
+ "version": "0.3.0",
12
+ "transport": {"type": "stdio"},
13
+ "packageArguments": [{"type": "positional", "value": "mcp"}]
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"