cybercadkernel is a desktop (macOS arm64) Python package that drives the
kernel through its stable plain-C ABI (cc_*, declared in
include/cybercadkernel/cc_kernel.h). It is a pure consumer of the ABI — it
never changes it — and it loads a shared library built against Homebrew
OpenCASCADE, so Python exercises the real B-rep engine
(cc_brep_available() == 1), not the CPU-only host stub.
It is a development / tooling artifact: for driving, testing, and visualizing the kernel on the desktop. It is not shipped to iOS.
- Low-level 1:1
ctypesbinding (cybercadkernel/_cffi.py) — all 111cc_*functions and every POD struct (CCMesh,CCMassProps,CCProfileSeg,CCEdgePolyline,CCFaceMesh,CCShapeId, plus the full-coverage additionsCCProjection,CCInterference,CCValidityReport,CCDisplayMesh,CCTetMesh,CCVolumeMeshOptions,CCQualityReport,CCPmiSummary,CCDrawing/CCDrawingSegment/CCHlrOptions,CCSection/CCSectionLoop) withargtypes/restypeset, by-value struct returns, andT**out-parameters. An ABI-layout guard (test_ffi.py) asserts each struct'sctypes.sizeofagainst a Csizeofof the same header, andtest_all_abi_symbols_are_boundpins the bound count at 111 / 111 — the whole header, no gaps. (_ffi.pyis a parallel table kept in lockstep.) - Pythonic object model (
cybercadkernel/api.py):Kernel— facade over engine state (brep_available,require_brep,last_error, the additiveset_parallel/set_gpu_tessellationtoggles) and the shape factories (extrude,revolve,box, lofts/sweeps/threads, STEP/IGES import, reference-geometry helpers).Shape— a body wrapping aCCShapeIdwith a context-managed, GC-safe lifetime (autocc_shape_releaseon__exit__,__del__backstop, idempotent double-release, use-after-release guard). Booleans (fuse/cut/common), features (fillet_edges/chamfer_edges/shell), transforms (translate/scale), queries (mass properties / bbox / moments / subshape ids), tessellation, and STEP/IGES export live here. Every op returns a newShapeand leaves the operands valid (functional ABI).Mesh— a tessellation result as owned NumPy arrays: vertices(N, 3)float64, triangles(M, 3)int32. C-owned buffers are copied into Python-owned arrays before the C buffer is freed.MassProps,BoundingBox,ReferencePlane,ReferenceAxis,FaceMesh,EdgePolyline— query results as dataclasses.CyberCadError(aliasKernelError) — raised fromcc_last_error()whenever a call would return0/ invalid, andKernelLibraryNotFoundwhen the dylib cannot be located.
- Visualization helpers (
cybercadkernel/viz.py) — see below.
The binding needs the macOS OCCT dylib. Build it with Homebrew OCCT and the CMake-driven build script:
brew install opencascade # OCCT 7.9.x
scripts/build-macos-dylib.sh # → build-mac/libcybercadkernel.dylib
# CLEAN=1 scripts/build-macos-dylib.sh # wipe build-mac/ and rebuildscripts/build-macos-dylib.sh configures CMake with -DCYBERCAD_MACOS_OCCT=ON
and builds the cybercadkernel shared target: every src/**/*.cpp except
the Metal TUs (src/compute/metal/*), compiled -std=c++20
-DCYBERCAD_HAS_OCCT -arch arm64 against the Homebrew OCCT headers, then
linked into a dylib against the TK* libraries with an rpath so OCCT resolves
at load time. Metal is excluded on macOS (no -DCYBERCAD_HAS_METAL);
cc_set_gpu_tessellation is a safe no-op there and cc_gpu_tessellation_enabled()
returns 0.
A standalone
python/build_dylib.shcodifies the same recipe with a manualclang++loop (no CMake) and is kept as a fallback / reference.
Install the package (editable) and its test/viz extras:
pip install -e "python/[test]" # numpy, pytest, trimesh, matplotlibThe library is discovered at import time via $CYBERCADKERNEL_DYLIB, then
build-mac/ relative to the repo root, then the bare library name.
from cybercadkernel import Kernel
k = Kernel() # raises unless a B-rep engine is linked
assert k.brep_available # property, not a call
def box(dx, dy, dz): # extrude takes (x, y) profile points
return k.extrude([(0, 0), (dx, 0), (dx, dy), (0, dy)], dz)
# 10×10×10 box, cut by a 5×5×10 corner tool → 875 mm³.
with box(10, 10, 10) as big, box(5, 5, 5) as small:
with big.cut(small) as part:
mp = part.mass_properties()
print(mp.volume) # 875.0
print(mp.centroid) # exact B-rep centroid
part.step_export("part.step") # STEP round-trips (preserves volume)
mesh = part.tessellate(0.1) # deflection in mm
print(mesh.vertices.shape) # (N, 3) float64
print(mesh.triangles.shape) # (M, 3) int32
from cybercadkernel import viz
viz.export_stl(mesh, "part.stl") # binary STL (also .ply/.glb/.obj)
viz.render_png(mesh, "part.png") # offscreen PNG (headless-safe)
# Reference geometry: a datum plane through 3 points → origin + unit normal.
plane = k.ref_plane_from_points((0, 0, 0), (1, 0, 0), (0, 1, 0))
assert plane.normal == (0.0, 0.0, 1.0)Failures never cross the boundary as C error codes: a failed cc_* call
(returning 0 / valid == 0) raises CyberCadError carrying the
cc_last_error() message, and a stub build raises BRepUnavailableError.
trimesh and matplotlib are optional — importing viz pulls in neither;
each entry point raises a clear error only if the backend it needs is missing.
to_trimesh(mesh)— wrap aMesh(or a raw(vertices, triangles)pair) as atrimesh.Trimesh(process=False, so geometry is preserved verbatim).export_mesh(mesh, path)— export by extension; plus explicitexport_stl/export_ply/export_glb.render_png(mesh, path, resolution=(512, 512), prefer="auto")— render an offscreen PNG.prefer="auto"tries trimesh's GL renderer and, if no GL context is available (the common headless case), degrades gracefully to a pure-matplotlibPoly3DCollectionraster on theAggbackend.prefer="matplotlib"forces the always-headless-safe fallback;prefer="gl"requires a GL context and raises otherwise.
CYBERCADKERNEL_DYLIB="$PWD/build-mac/libcybercadkernel.dylib" \
python -m pytest python/tests -qconftest.py builds/loads the dylib and skips with an actionable message if
it cannot (no error). Every geometry test then gates on the real engine
(brep_available) and asserts exact numbers — a stub build skips loudly
rather than passing a trivially-true check.
Verified suite (macOS arm64, pytest): 75 passed, 1 skipped (up from 35 — the
new test_full_api.py and test_engine_and_declines.py cover the full-coverage
additions). The single skip is test_viz.py::test_render_png_gl_offscreen_or_skip
— the offscreen GL render path (no GL context / pyglet available); the
matplotlib PNG fallback and the STL round-trip are asserted unconditionally (both
headless-safe). Geometry asserted through Python (exact, abs=1e-6 unless
noted):
| Assertion | Value |
|---|---|
| box 10×10×10 volume / area / centroid | 1000.0 / 600.0 / (5, 5, 5) |
| boolean CUT (a − b, 5×5×10 overlap) | 875.0 |
| boolean FUSE (two overlapping boxes) | 1875.0 |
| boolean COMMON | 125.0 |
| operands survive a boolean | a = 1000, b = 125 |
| 3×4×5 box: volume / area / centroid | 60.0 / 94.0 / (1.5, 2, 2.5) |
| 3×4×5 box bbox | min (0,0,0) max (3,4,5) |
| cube principal moments | i1 == i2 == i3 > 0 |
| revolved cylinder volume | matches πr²h (rel 1e-3) |
| tessellation | watertight, float64 (N,3) verts, int32 (M,3) tris; 6 box face-meshes |
| STEP / IGES round-trip | preserves volume |
| fillet / translate / scale | volume down / bbox moves / volume cubes |
| loft circles (equal r) / (taper) | cylinder πr²h / frustum volume |
| loft of 3 square rings | prism 2000.0 |
| variable sweep circle→circle | cone / frustum (rel 2e-2) |
| draft / asym chamfer | strictly reduce volume |
| sheet base flange 50×30×2 | 3000.0 |
| edge flange → unfold | flat-pattern area > base area |
| n-gon fill (10/10 triangle) | patch mesh area 50.0 |
| section of box at z=5 | 1 loop, area 100.0, perimeter 40.0 |
| HLR of box (front view) | 4 visible + 4 hidden segments |
| point projection onto z=10 | distance 40.0, foot (5,5,10) |
check_solid of a box |
valid, decided, first_failure == 0 |
| interference clash / clear | overlap 125.0 / min distance 5.0 |
solid_count of a two-lump fuse |
2 (each lump 1000) |
glTF .glb / .gltf / USDZ export |
valid magic-byte files |
| display mesh | unit per-vertex normals + UVs |
| tet-mesh quality (regular tet) | min dihedral 70.53, scaled J 1.0 |
The measurement / curvature and tet-meshing wrappers are asserted to honestly
decline (raise CyberCadError) on this MIT desktop build, since it links
neither CYBERCAD_HAS_NUMSCI nor CYBERCAD_HAS_TETGEN.
Live smoke through Python: brep_available = True, box volume
999.9999999999998.
python -m build produces a self-contained wheel with the native dylib
inside, so an install needs no build-mac/ checkout or env var at runtime:
python/build_dylib.sh # → build-mac/libcybercadkernel.dylib
cd python && python -m build --wheel # → dist/cybercadkernel-*.whl (dylib bundled)
pip install dist/cybercadkernel-*.whl # imports + runs real geometry as-issetup.py's build_py hook copies the dylib (from $CYBERCADKERNEL_DYLIB, else
build-mac/) into cybercadkernel/lib/ as package data; discovery probes that
bundled path first. A clean-venv install of the wheel imports and runs real
geometry (box volume 1000, brep_available = True) with no env var set.
- pybind11 variant — deferred by design. The
ctypesbinding is complete (all 111cc_*) and passing on real geometry; a pybind11 layer would only be a parallel reimplementation of the same plain-C ABI for marginal per-call speed and compiled type stubs, at the cost of a C++ build-toolchain dependency in the wheel. No functional capability is gated on it. Revisit only if ctypes dispatch is shown to be a real hot-loop bottleneck. - Interactive
pyvistarender — partial. An offscreen PNG helper (viz.render_png, trimesh GL + headless matplotlib fallback) is provided and covers the examples-gallery / CI need; a live interactivepyvistaviewer stays optional.
Building a solid under the native engine (kernel.engine = "native") and then
consuming it under the OCCT engine — e.g. kernel.engine = "occt" followed by
step_export / mass_properties on that native body — segfaults. This
reproduces on the raw C ABI with pure ctypes (no binding involvement): the OCCT
path is handed a native body it cannot read, contradicting the header's guarantee
that the engine never hands a native body to OCCT. It is a kernel/engine defect,
outside the (correct) binding. Keep engine usage consistent — build and consume
a native body under the native engine, then restore OCCT — as the header
prescribes; the test suite does exactly this and never trips it.