Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,19 @@ v_*/
#cached python version for pyenv
.python-version

# generated DXF files in examples
examples/*.dxf


# generated DXF files
*.dxf
# but include documentation DXF files
!docs/*.dxf

# generated stl files
*.stl
# but include documentation stl files
!docs/*.stl

# generated step files
*.step
# but include documentation step files
!docs/*.step
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ calling out include:
creating sectional views of assemblies.
- `yapcad.io.step`/`yapcad.io.stl` – faceted exporters suitable for
interchange with FreeCAD, slicers, and other simulation tools.
- `tools/validate_mesh.py` – CLI helper that runs `admesh`, `meshfix`, and an
optional slicer to gauge whether STL output is robust enough for CAM; see
`docs/mesh_validation.md` for usage.

To build the HTML **yapCAD** documentation locally, install the
documentation dependencies and run Sphinx from the project root:
Expand Down
66 changes: 66 additions & 0 deletions docs/mesh_validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Mesh Validation Pipeline

This repository now includes a small command–line helper that mirrors the
checks typically performed downstream of a CAD/boolean workflow. The goal is
to gauge whether the generated STL will survive real fabrication tools even if
the mesh is not mathematically watertight.

## Prerequisites

The script looks for the following utilities on `PATH`:

| Tool | Purpose | macOS install hint |
|----------|--------------------------------------|--------------------|
| `admesh` | STL statistics (holes, volume, etc.) | `brew install admesh` |
| `meshfix`| Mesh repair via CGAL | `brew install meshfix` |

If you prefer to stay in Python space, install the optional
[`pymeshfix`](https://pypi.org/project/PyMeshFix/) and
[`trimesh`](https://pypi.org/project/trimesh/) packages; the validation script
will automatically fall back to those when the stand-alone `meshfix` binary is
missing.

You can pull these extras in with a single command using the optional-deps
group defined in `pyproject.toml`:

```bash
pip install yapCAD[meshcheck]
```
| `prusa-slicer` / `prusa-slicer-console` / `slic3r` | CLI slicer check | download from vendor; add binary to `PATH` |

If a tool is missing the script will skip the corresponding step and include a
note in the report.

## Usage

1. Export an STL using the demo driver (or your own code):

```bash
PYTHONPATH=src python examples/solid_boolean_demo.py \
--mode stl --operation difference --shapes box_hole \
--output box_hole_difference
```

2. Run the validator:

```bash
python tools/validate_mesh.py box_hole_difference.stl \
--workdir build/mesh_checks
```

This prints a summary to stdout and keeps any repaired STL/G-code files
under `build/mesh_checks`.

3. (Optional) Request JSON output for CI:

```bash
python tools/validate_mesh.py box_hole_difference.stl --json
```

The report includes highlights from `admesh`, the return code from `meshfix`
(with output file location), and the status of the slicer run. When the slicer
succeeds you’ll find a G-code file beside the repaired meshes.

This pipeline gives a far more practical read on model quality than the strict
`issolidclosed` check, and mirrors the tools typically invoked before sending a
part to printers or CAM software.
24 changes: 24 additions & 0 deletions docs/solid_boolean_roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Boolean Engine Roadmap

Status snapshot as of this session:

- Native mesh boolean workflow now lives in `yapcad/boolean/native.py`, with `geom3d` re-exporting it for backward compatibility.
- Diagnostics still rely on `tools/validate_mesh.py`; no engine-specific metrics yet.
- External engine integration is underway; a `trimesh` backend is plumbed (requires an installed boolean operator such as Blender/OpenSCAD/Cork).

Near-term tasks:

- [x] **Code extraction** – current boolean implementation now resides in `yapcad/boolean/native.py`, referenced by `geom3d` wrappers.
- [x] **Engine selector UX** – `solid_boolean(..., engine=...)` now routes through the native engine by default, accepts `trimesh` (and optional `trimesh:backend`), and the demo exposes `--engine`; document env flags (`YAPCAD_BOOLEAN_ENGINE`, `YAPCAD_TRIMESH_BACKEND`) for benchmarking.
- [ ] **External prototype** – wrap at least one library boolean (e.g., `trimesh` or PyMeshFix) for STL solids, including geometry conversion helpers.
- [ ] **Benchmark harness** – update the demo CLI and validation scripts to iterate across registered engines, saving STL/lint outputs for comparison.

Open questions:
- Which external kernel offers the best balance of licensing, install footprint, and mesh quality?
- Do we keep the native engine on equal footing (for offline/embedded use), or treat it as a fallback once a third-party backend is available?
- What minimal metrics should every engine report (shell count, single-facet edges, `validate_mesh` scores, render snapshots)?

Once the refactor lands, future work will include:
- Automatic tolerance scaling per engine.
- Capturing validation output in machine-readable form (JSON) to feed regression dashboards.
- Evaluating hybrid workflows (native preprocessing, external boolean, native stitches/cleanup).
103 changes: 103 additions & 0 deletions examples/solid_boolean_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Demonstration of yapCAD solid boolean operations.

This script can visualise the result via OpenGL or export the mesh to STL/STEP.
"""

import argparse
from pathlib import Path

from yapcad.geom import point
from yapcad.geom3d import solid_boolean, translatesolid
from yapcad.geom3d_util import prism, sphere, conic, tube
from yapcad.io.stl import write_stl
from yapcad.io.step import write_step


def _build_solids(kind: str):
if kind == 'cube':
a = prism(2, 2, 2)
b = translatesolid(prism(2, 2, 2), point(0.75, 0.0, 0.0))
elif kind == 'sphere':
a = sphere(2.0)
b = translatesolid(sphere(2.0), point(1.0, 0.0, 0.0))
elif kind == 'box_hole':
a = prism(2, 2, 2)
b = conic(0.6, 0.6, 3.0, center=point(0.0, 0.0, -1.5))
elif kind == 'tube_hole':
a = tube(outer_diameter=3.0, wall_thickness=0.5, length=4.0,
base_point=point(0.0, 0.0, -2.0))
b = conic(0.6, 0.6, 4.2, center=point(1.2, 0.0, -2.1))
else:
raise ValueError(f'unsupported shape kind: {kind!r}')
return a, b


def _render_opengl(solids, result):
from yapcad.pyglet_drawable import pygletDraw

viewer = pygletDraw()
viewer.make_object('obj1', lighting=True, linecolor='white',
material='pearl',
position=point(-2.5, 2.5, 0))

viewer.make_object('obj2', lighting=True, linecolor='white',
material='obsidian',
position=point(2.5, 2.5, 0))

viewer.make_object('rslt', lighting=True, linecolor='white',
material='gold',
position=point(0, 0, 0))

viewer.draw_solid(solids[0], name='obj1')
viewer.draw_solid(solids[1], name='obj2')
viewer.draw_solid(result, name='rslt')
viewer.display()


def _export_stl(result, output):
path = Path(output)
if path.suffix.lower() != '.stl':
path = path.with_suffix('.stl')
write_stl(result, path)
print(f'Wrote STL to {path}')


def _export_step(result, output):
path = Path(output)
if path.suffix.lower() not in {'.step', '.stp'}:
path = path.with_suffix('.step')
write_step(result, path)
print(f'Wrote STEP to {path}')


def main():
parser = argparse.ArgumentParser(description='Solid boolean demonstration.')
parser.add_argument('--mode', choices=['gl', 'stl', 'step'], default='gl',
help='visualise with OpenGL or export as STL/STEP')
parser.add_argument('--operation', choices=['union', 'intersection', 'difference'],
default='union', help='boolean operation to apply')
parser.add_argument('--shapes', choices=['cube', 'sphere', 'box_hole', 'tube_hole'],
default='cube',
help='primitive pair to use')
parser.add_argument('--output', default='boolean_result',
help='output basename for STL/STEP export')
parser.add_argument('--stitch', action='store_true',
help='experimentally stitch open edges in the result')
parser.add_argument('--engine', default=None,
help='boolean engine to use (default: native)')
args = parser.parse_args()

solids = _build_solids(args.shapes)
result = solid_boolean(solids[0], solids[1], args.operation,
stitch=args.stitch, engine=args.engine)

if args.mode == 'gl':
_render_opengl(solids, result)
elif args.mode == 'stl':
_export_stl(result, args.output)
else:
_export_step(result, args.output)


if __name__ == '__main__':
main()
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ tests = [
"pytest",
"pytest-cov"
]
meshcheck = [
"trimesh>=4.0",
"pymeshfix>=0.16",
]
[project.urls]
"Homepage" = "https://github.com/rdevaul/yapCAD/"
"Documentation" = "https://yapcad.readthedocs.io/en/latest/"
Expand Down
21 changes: 21 additions & 0 deletions src/yapcad/boolean/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from . import native as native

__all__ = ['native']

try:
from . import trimesh_engine as trimesh
except Exception: # optional dependency
trimesh = None
else:
__all__.append('trimesh')

ENGINE_REGISTRY = {'native': native}
if trimesh is not None:
ENGINE_REGISTRY['trimesh'] = trimesh


def get_engine(name: str):
return ENGINE_REGISTRY.get(name)


__all__.extend(['ENGINE_REGISTRY', 'get_engine'])
Loading