README.md
September 7, 2026 · View on GitHub
Geogram × Rosetta
Automatic, multi-language bindings for Bruno Lévy's geometry-processing library — generated, not hand-written
Language bindings for Geogram (a programming library with geometric algorithms) generated by rosetta — Python, Node.js, WebAssembly, TypeScript and Lua from one manifest.json, without touching a line of geogram's source.
(The desktop demo example_python_GUI.py. Thanks to askemwhat for the 3D model)
What is bound
| Family | Functions |
|---|---|
| Surface reconstruction | Co3Ne_smooth, Co3Ne_smooth_and_reconstruct (Co3Ne: smoothing + co-cone triangulation of a point cloud) |
| Remeshing | remesh_smooth (isotropic CVT remeshing, Lloyd + Newton), mesh_repair, fill_holes, tessellate_facets |
| Parameterization & texturing | mesh_make_atlas (charts + LSCM/ABF/spectral/projection + tetris/xatlas packing), mesh_get_charts, and the UVs through mesh.facet_corners.attributes().get_doubles("tex_coord") |
| Intersections & Booleans | mesh_union, mesh_intersection, mesh_difference, mesh_remove_intersections, mesh_facets_have_intersection (exact arithmetic) |
| CSG | CSGCompiler — compile_string, compile_file (OpenSCAD .csg programs: sphere, cube, cylinder, union, difference, intersection, multmatrix, hull, minkowski, linear_extrude, ...); geogram's own class, bound directly |
plus the enums MeshElementsFlags, MeshRepairMode, ChartParameterizer, ChartPacker, GEO::Mesh's own marshalable methods (load, save, copy, clear, show_stats, get_attributes, ...) and its member objects mesh.vertices / mesh.facets / mesh.facet_corners — reference properties over the real stores, through which geometry moves as flat arrays via geogram's own API:
m = geogram.Mesh()
m.facets.assign_triangle_mesh(3, coords, tris, False) # flat [x,y,z,...] + [v0,v1,v2,...]
m.vertices.nb(), m.facets.nb()
coords = m.vertices.point_coordinates() # flat [x,y,z,...] back
(GEO::vector<T> parameters and returns marshal as plain script arrays through rosetta's sequences trait.)
How the src/ folder disappeared
"No facade" removed the wrapper class — scripts hold the real GEO::Mesh
and the algorithms are bound from geogram's own headers — not the last mile
of glue. Two rounds then shrank this folder further. First, binding-friendly
API additions to geogram (bulk sized accessors point_coordinates() /
vertex_indices(), type-erased AttributesManager::get_doubles/set_doubles,
member Mesh::load/save — upstreamed as geogram PR #373). Second, three
rosetta features: member-object properties (mesh.vertices /
mesh.facets bind as references to the real stores; the manifest final
flag suppresses their trampolines so node reaches them too), the
sequences trait (GEO::vector<T> marshals as flat script arrays — and
because the adapters call by name, the first-declared overloads
assign_points / assign_triangle_mesh bind), which together deleted
set_points / set_surface / vertices / nb_vertices / nb_facets from
mesh_ext. A third round then emptied algorithms.{h,cpp} and deleted it:
overload selection by signature (a manifest "signature" picks one member
of an overload set, so GEO::mesh_union & co bind straight from geogram's
header), std::filesystem::path + std::shared_ptr support on every target
(so GEO::CSGCompiler — whose compile_file takes a path and whose
compile_* return shared_ptr<Mesh> — binds as a class), and module_init
(geogram's lifecycle runs when the module loads, so no initialize() wrapper is
needed). A fourth field, generated_headers, replaced the checked-in
geogram/version.h stand-in with geogram's own version.h.in, configured by
rosetta into the generated tree.
A fifth field, out_params, then removed the last two helpers and with them
src/ itself: triangles() is mesh.facets.triangulate() (geogram
fan-triangulates identically) followed by mesh.facet_corners.vertex_indices(),
and tex_coords() is
mesh.facet_corners.attributes().get_doubles("tex_coord"), whose two
written-through references the manifest declares as outputs — so scripts get
(ok, values, dim) back.
There is no src/ any more. The binding is geogram's own API, end to end:
no wrapper class, no glue header, nothing to keep in sync.
Prerequisites
- clang-p2996 (C++26 reflection fork) at
~/devs/c++/clang-p2996/build— used only to build the generator; the bindings themselves compile with stock toolchains. - geogram and rosetta under
extern/— the bootstrap CMakeLists (step 0) fetches both. To use local checkouts instead, put (or symlink) them atextern/geogram/extern/rosettabefore running it: an existing directory is left untouched. geogram needs its submodules (git clone --recurse-submodules --depth 1 https://github.com/BrunoLevy/geogram extern/geogram). - Python 3 +
pybind11(pip install pybind11) for the Python target. - Node.js (
cmake-jsis installed bynpm i) for the Node target. - emsdk (
source ~/emsdk/emsdk_env.sh) for the WASM target. pip install pyvista pyvistaqt PyQt5for the desktop viewer (example_python_GUI.py) only.
Build
One-time bootstrap: fetch rosetta into extern/ and build rosetta_gen:
cmake -B build && cmake --build build
Then, generate the generator + all the bindings and compile all of them:
./extern/rosetta/bin/rosetta_gen --build manifest.json
(Re)-Installing the wheel
pip install --force-reinstall --no-deps --no-cache-dir ../dist/wheels/arch*
All three examples run the same pipeline — CSG (sphere minus cylinder), the three boolean operations on two overlapping spheres, CVT remeshing of the union to 5000 vertices, LSCM/xatlas UV-atlas generation with UV read-back, then Co3Ne reconstruction of the surface from its bare point cloud — and print the same counts:
CSG : sphere minus cylinder -> 470 vertices, 940 facets
booleans : union 1580, intersection 812, difference 1196 facets
remeshing : union remeshed to 5000 vertices, 9996 facets
texturing : 7 charts, 29988 UV corners in [0.000, 1.000]
reconstruct: Co3Ne rebuilt 9996 facets from the point cloud
Desktop demo (example_python_GUI.py)
The screenshot at the top: a pyvista / Qt viewer driving the native Python
binding — same workflow and controls as the browser demo below. Build the
Python target (step 3), pip install pyvista pyvistaqt PyQt5, then:
python3 example_python_GUI.py
- Load reads
data/Intergalactic_Spaceship.obj(27.5k vertices / 55k triangles) natively withmesh.load(), so Open… accepts any format geogram reads (.obj,.off,.ply,.stl,.mesh/.meshb,.geogram) — no OBJ parsing in the host language, unlike the wasm demo. - Repair (
mesh_repair), Fill holes (fill_holes) and Remesh (CVT) (remesh_smooth, with points / Lloyd / Newton controls) run on theGEO::Mesh; Reset restores the pristine model (Mesh.copy). The geometry crosses into pyvista as the same flatvertices.point_coordinates()/triangles()arrays the other viewers use. - Display toggles: plain shaded surface (PBR), wireframe overlay, vertex points, flat shading; the log panel prints per-operation timings. Native and multi-threaded: the default CVT remesh of the spaceship (10k points, 5 Lloyd + 10 Newton) takes 0.4 s against ~20 s in the single-threaded wasm build.
Browser demo (index.html)
The same viewer as a three.js page running the wasm binding in the browser. Build the WASM target (step 5), then serve the project root over HTTP and open the page:
python3 -m http.server # then http://localhost:8000/
- Load fetches
data/Intergalactic_Spaceship.obj; the file picker accepts any local.obj. The OBJ is parsed in JS (fan-triangulated) and pushed into theGEO::Meshthroughmesh.facets().assign_triangle_mesh(...)+mesh_repair(adjacency) — no filesystem in wasm. - Same algorithm buttons and display toggles as the desktop demo; Reset
restores the pristine scan (
Mesh.copy).
Everything runs single-threaded in wasm on the main thread — on the bundled spaceship, repair takes ~0.3 s and the default remesh (10k points, 5 Lloyd + 10 Newton) ~20 s; a 1.1M-triangle scan remeshes in ≈2 min (Newton = 0 is about 3x faster). The log panel prints per-operation timings.
Notes
- CSG dialect: geogram evaluates the compiled OpenSCAD format (
openscad model.scad -o model.csg). High-level transforms are OpenSCAD sugar — writemultmatrix([[1,0,0,tx],[0,1,0,ty],[0,0,1,tz],[0,0,0,1]]) { ... }instead oftranslate([tx,ty,tz]). - There is no
initialize()call any more. geogram's lifecycle —GEO::initialize(which registers the I/O handlersmesh.load()/mesh.save()need), the sevenCmdLine::import_arg_groupcalls the algorithms read their tuning parameters from, and the OpenNL log routing — runs when the module loads, from the manifest'smodule_init. geogram's log starts quiet; to get it back, flip theset_quiet(true)statement inmodule_initand regenerate. It cannot be a runtime switch today:GEO::Loggerderives fromCountedand protects its destructor, which no backend can wrap (rosetta now skips such a class with a note instead of failing the build). - A
CSGCompilerinstance is single-use: geogram keeps parser/builder state, so a secondcompile_string/compile_fileon the same object comes back empty. Construct one per program (they are cheap) — the examples wrap that in a three-linecsg_evalhelper. - Remeshing can leave higher-dimensional points (normals appended): call
mesh.vertices.set_dimension(3)before readingpoint_coordinates()back (the examples do). - On wasm,
mesh.vertices()/mesh.facets()are getter methods returning borrowed handles (embind properties copy) — don't.delete()them, and don't use them after the mesh is gone. embind does not auto-convert JS arrays: useModule.vector_double/Module.vector_unsigned_int(triangle indices areindex_t) and call.delete()on vectors and meshes (seeexample_wasm.js). mesh_union/mesh_intersection/mesh_differencerequire closed surfaces without self-intersections (that is what the CSG primitives produce; usemesh_repair/mesh_remove_intersectionson wild input).facets.assign_triangle_meshcomputes no adjacency — runmesh_repair(M, MESH_REPAIR_DEFAULT, 0.0)after it before the booleans/remeshing.- This project is the test case for the rosetta features it motivated: by-reference unwrapping of bound classes in the node runtime, copyability gates in the emitters (skip what would not compile instead of failing the build), manifest
extensions(free functions as instance methods), member-object reference properties (+ the per-classfinalflag), thesequencestrait (GEO::vector<T>as flat arrays), free-function overload selection by explicitsignature,std::filesystem::path/std::shared_ptrmarshalling on the caster-less backends, themodule_initload-time hook,generated_headers, and manifest-declaredout_params.