Docs · C++

C++ API

The C++ wrappers over the two libraries: cadaclysm_capi reads, meshes and writes; cadaclysm_blacksmith builds exact solids. Every type and call, with the signature as the wrapper declares it — read from its source when this page is built.

Install

Install and load

Three headers under cpp/include/cadaclysm/ in the SDK (include/cadaclysm/ in a release archive): cadaclysm.hpp (the reader, namespace cadaclysm), cadaclysm_blacksmith.hpp (the kernel, cadaclysm::blacksmith) and result.hpp. Header-only: nothing to compile, but you link the libraries. C++17: MSVC 2019+, GCC 9+, Clang 9+.

Get the libraries with the SDK's fetch.py (or unpack a release archive's lib/ and include/); keep the wrapper files and the libraries from the same release. Details on the start page.

# CMake, against an unpacked release archive:
find_package(cadaclysm CONFIG REQUIRED)   # -DCMAKE_PREFIX_PATH=<archive>
target_link_libraries(app PRIVATE cadaclysm::blacksmith)

Where the library is found

Linked at build time: the operating system loads cadaclysm_capi and cadaclysm_blacksmith as it does any shared library — beside the executable on Windows, through the rpath or LD_LIBRARY_PATH on Linux, @rpath on macOS. CADACLYSM_LIBRARY plays no part.

The licence

Unlicensed, everything works and a notice is printed on every open and export. A licence file removes it: set CADACLYSM_LICENSE, or put cadaclysm.lic beside the executable or in the working directory, or load it from code — once per library:

cadaclysm::license("cadaclysm.lic");
cadaclysm::blacksmith::license("cadaclysm.lic");
Quick start

Build a part, then read it back

The kernel builds a plate with a boss, bores it and rounds its corners, then writes STEP — the Examples page's first part:

#include <cadaclysm/cadaclysm_blacksmith.hpp>

#include <cmath>
#include <cstdio>

using namespace cadaclysm::blacksmith;

static cadaclysm::Result<void> build() {
    CADACLYSM_TRY(outline, Profile::rect(120, 80));
    CADACLYSM_TRY(plate, Workplane::xy().extrude(outline, 14).solid());
    CADACLYSM_TRY(circle, Profile::circle(22));
    CADACLYSM_TRY(boss, Workplane::from_solid(plate).faces((Selector::max)(Axis::z)).workplane().extrude(circle, 26).solid());
    CADACLYSM_TRY(joined, plate.join(boss));

    CADACLYSM_TRY(hole, Profile::circle(11));
    CADACLYSM_TRY(rod, Workplane::xy().extrude(hole, 60).solid());
    CADACLYSM_TRY(bore, rod.translate(0, 0, -10));
    CADACLYSM_TRY(bored, joined.cut(bore));

    // The plate's four vertical corners: the lines along Z between two planes
    // (the boss has vertical seams too, but those lie on its cylinder).
    std::vector<std::uint32_t> corners;
    CADACLYSM_TRY(edges, bored.edges());
    for (const Edge& edge : edges) {
        std::optional<Vec3> d = edge.direction();
        bool planes = true;
        for (std::uint32_t f : edge.faces) {
            CADACLYSM_TRY(kind, bored.face_kind(f));
            planes = planes && kind == "plane";
        }
        if (d && std::fabs((*d)[2]) > 0.99 && planes) corners.push_back(edge.index);
    }
    CADACLYSM_TRY(part, bored.fillet(corners, 12));
    return part.step("plate.stp");
}

int main() {
    cadaclysm::Result<void> built = build();
    if (!built) {
        std::fprintf(stderr, "%s\n", built.error().message.c_str());
        return 1;
    }
    return 0;
}

The reader opens that file, walks its tree, meshes what it draws and writes glTF:

#include <cadaclysm/cadaclysm.hpp>

#include <cstdio>
#include <string>

int main() {
    cadaclysm::Result<cadaclysm::Scene> opened = cadaclysm::open("plate.stp");
    if (!opened) {
        std::fprintf(stderr, "%s\n", opened.error().message.c_str());
        return 1;
    }
    const cadaclysm::Scene& scene = *opened;
    std::printf("%s %g m per unit\n", scene.schema().c_str(), scene.metres_per_unit());

    // The tree: assemblies, parts and bodies, parents before children.
    for (const cadaclysm::Node& node : scene.walk()) {
        std::printf("%s%s [%s]\n", std::string(2 * node.depth(), ' ').c_str(), node.label().c_str(), node.kind().c_str());
    }

    // What to draw: every placement of every shape, meshed on first ask.
    for (const cadaclysm::Placement& placement : scene.placements()) {
        cadaclysm::Node geometry = placement.geometry();
        std::printf("%s %u triangles\n", geometry.label().c_str(), geometry.mesh().triangle_count());
    }

    return scene.save("plate.glb", "glb").ok() ? 0 : 1;
}
Reader · cadaclysm_capi

Reading, meshing, writing

Module functions

Loading the library, the licence, and opening a file — from disk or from bytes already in memory. Every other object on this page comes out of cadaclysm::open() or cadaclysm::open_memory().

cadaclysm::open()

Result<Scene> open(const std::string& path, const OpenOptions& options = OpenOptions())

Open a CAD file and read its tree. The format comes from the extension (a .zip or .stpZ opens its first readable member — Scene::source_name() says which — and a .gz is read as the format named before it, part.step.gz as .step). The tree is read now and the geometry is built lazily, node by node, when it is first asked for.

convention is the space to read into — a Convention, optionally with the file-units and world-UV flags — and the library converts everything it hands back into it. schema names an extra EXPRESS schema (.exp, or a directory of them); every schema the SDK ships is already built in, so it is needed only for one the library does not carry. colours asks for per-vertex colours on bodies the file painted in more than one colour.

Never returns an empty handle: on failure it returns an Error in its Result carrying the library's reason.

cadaclysm::open_memory()

Result<Scene> open_memory(const void* data, std::size_t size, const std::string& format, const OpenOptions& options = OpenOptions())

Open a file already in bytes — a download, a database blob, an archive member. With no file name to take the format from, it is named as an extension would name it: step, ifc, igs, 3dm, brep, scad (a leading dot is fine). The bytes are copied; the buffer can be reused as soon as this returns. Otherwise as cadaclysm::open().

cadaclysm::version()

std::string version()

The version of the library actually loaded — the one worth reporting in a bug.

cadaclysm::build_date()

std::string build_date()

When the loaded library was built, YYYY-MM-DD. A licence covers every build dated on or before its expiry.

cadaclysm::license()

Result<void> license(const std::string& text_or_path)

Load a licence: the certificate text, or the path of a file holding it. Without this call the library looks in the CADACLYSM_LICENSE environment variable, then for cadaclysm.lic beside the executable and in the working directory. On a licence that does not verify it returns an Error in its Result with the reason, and the previous licence (if any) stays in use.

cadaclysm::license_info()

std::string license_info()

One line about the licence in use — customer=… expiry=… entitlements=… — or unlicensed (unlicensed -- <reason> when a licence was found but did not verify). Never std::nullopt.

cadaclysm::license_notice_count()

std::uint64_t license_notice_count()

How many unlicensed notices the library has printed to stderr in this process. An application with no console to watch (a GUI, a game) can poll this and show its own banner.

cadaclysm::mesh_formats()

std::vector<MeshFormat> mesh_formats()

Every mesh format Node::save_mesh() writes, with its file extension and a label for a save menu: stl (STL (binary)), stl-ascii (writes .stl too), msh (Gmsh), glb, gltf and obj in this release. Build the menu from this list rather than hard-coding it, and a format added to the library appears without a code change.

cadaclysm::lod_levels()

std::uint32_t lod_levels()

How many coarser levels Node::mesh_lod() offers above the mesh itself (level 0): 3 in this release.

cadaclysm::formats()

std::vector<Format> formats()

Every format this build reads, each with the file extensions it takes — what an open dialog's filter is built from, the way cadaclysm::mesh_formats() feeds a save menu.

datetime_of

A schedule date (seconds since 1970-01-01T00:00, as WorkSchedule::at() and TaskTime give them) as a naive datetime.

Seconds since 1970-01-01T00:00, no zone: std::chrono::sys_seconds{std::chrono::seconds{static_cast<long long>(seconds)}}.

seconds_of

A naive datetime as a schedule date: seconds since 1970-01-01T00:00, the inverse of datetime_of.

Seconds since 1970-01-01T00:00, no zone: std::chrono::sys_seconds{std::chrono::seconds{static_cast<long long>(seconds)}}.

cadaclysm::pick_save()

std::optional<std::string> pick_save(std::optional<std::string_view> suggested_name = std::nullopt)

Ask the user where to save, through the platform's own dialog, with suggested_name prefilled. std::nullopt when they cancel or no dialog is available. Blocks until the user acts; on macOS call it from the main thread.

cadaclysm::pick_file()

std::optional<std::string> pick_file()

Ask the user for a file through the platform's own open dialog, filtered to what this build can read. std::nullopt when they cancel or no dialog is available (on Linux, neither an XDG portal nor zenity). Blocks until the user acts; on macOS call it from the main thread.

cadaclysm::declared_schema()

Result<std::string> declared_schema(const std::string& model)

The schema a STEP or IFC file says it speaks (its FILE_SCHEMA line), read from the first few kilobytes — cheap even on a very large file. Empty when it names none.

cadaclysm::resolve_schema()

Result<SchemaChoice> resolve_schema(const std::string& model, const std::optional<std::string>& schema)

Which .exp of a schema directory matches a model: the chosen file, or — when the file's declared name resembles none of them — the whole list as fallbacks to try in turn. cadaclysm::open() does this itself when given a directory; this is for a caller that wants to report the choice.

library_path

Where the shared library was found: CADACLYSM_LIBRARY (a file or a directory) first, then beside the wrapper, then a lib/ directory in any parent (the SDK's layout).

Linked at build time: the OS loader finds the libraries; see Install.

cadaclysm::NONE

inline constexpr std::uint32_t NONE = CADACLYSM_NONE

The node index the C API uses for "no such node" (CADACLYSM_NONE, 0xFFFFFFFF). The wrappers turn it into std::nullopt where a node may be missing (Node::parent(), Node::instance_of()), so it matters only when reading raw indices.

OpenOptions C++ only

struct OpenOptions
std::optional<std::string> schema
std::uint32_t convention
bool colors
double source_metres_per_unit
std::string name

cadaclysm::open() and cadaclysm::open_memory() take Python's keyword arguments as one struct, its fields set by name. convention is a number, so a Convention is OR'd with cadaclysm::FILE_UNITS and cadaclysm::UV_WORLD into it — options.convention = cadaclysm::Convention::unreal | cadaclysm::FILE_UNITS; — and a bare preset is static_cast<std::uint32_t>(cadaclysm::Convention::y_up). name is what an in-memory scene reports as its Scene::path().

packed C++ only

Result<std::uint32_t> packed(std::string_view colour)

cadaclysm::packed(std::string_view): a colour packed 0xRRGGBB from "#rgb", "#rrggbb" or a CSS colour name, read by the library's own parser — for a host that wants the number itself.

SvgColour C++ only

class SvgColour
bool is_text() const
const std::string& text() const
std::int64_t number() const

What Scene::svg()'s stroke and background hold: colour text ("#rgb", "#rrggbb" or a CSS colour name) or a packed 0xRRGGBB, each converting implicitly — options.stroke = "navy";, options.background = 0x000080;. The text is read when the drawing is made, by the reader's parser for a scene and the kernel's for a solid; text it refuses is an Error naming the field.

Scene

class Scene

An open document. Close it when done — Scene::close(), or let the language's scope do it (its destructor, at the end of the scope). Everything it hands back borrows from it: node handles, meshes, polylines. See Lifetimes for what survives a close.

Scene::close()

void close() noexcept

Give the document back. Idempotent. Every mesh and polyline view still held reads freed memory afterwards (the wrappers that can tell refuse to read them). Node and Godot hand back copies, so nothing of theirs goes stale here.

Scene::closed()

bool closed() const noexcept

Whether Scene::close() has run.

Scene::path()

const std::string& path() const

The file it was read from, or the name given to cadaclysm::open_memory().

Scene::schema_path()

const std::optional<std::string>& schema_path() const

The .exp actually used, or std::nullopt — worth reporting when a directory was passed.

Scene::convention()

std::uint32_t convention() const

The convention it was opened with. Nothing the library hands back says what space it is in, and every array out of this scene is in this one.

Scene::version()

std::string version() const

The version of the library that read it.

Scene::schema()

std::string schema() const

The schema the file named, or empty for a format that names none.

Scene::schema_read()

std::string schema_read() const

The schema that actually read it. A file declaring a release candidate reads under the finished schema of the same version where that is what is built in; a file whose schema is unknown reads under the one that defines its entity types.

Scene::substituted()

bool substituted() const

Whether something other than the file's own schema read it — Scene::schema() and Scene::schema_read() differ.

Scene::metres_per_unit()

double metres_per_unit() const

What one length unit in the file is worth in metres; 1 where the file did not say.

Scene::bounds()

Bounds bounds() const

Everything the model covers, in world coordinates — the one figure not in a node's own frame. This meshes the whole model, being the only way to know how far it reaches; to frame a view quickly, use the bounds of the nodes already built.

Scene::bounds64()

Bounds64 bounds64() const

Scene::bounds() in double precision, in world coordinates: the model's extent unnarrowed, exact far from the origin. This meshes the whole model, as Scene::bounds() does.

Scene::diagnostics()

std::vector<std::string> diagnostics() const

What the file held that the reader could not build, one line each.

Scene::joints()

std::vector<Joint> joints() const

The connections between those links, in the file's order; each names, through Joint::pair(), how it moves.

Scene::states()

std::vector<State> states() const

The file's named mechanism poses, in file order, each a State. Empty for a file that records none.

Scene::sequences()

std::vector<Sequence> sequences() const

The file's motion sequences — INTERPOLATED_CONFIGURATION_SEQUENCE, a mechanism moving through its states — in file order, each a Sequence. Empty for a file that records none.

Scene::schedules()

std::vector<WorkSchedule> schedules() const

The file's work schedules (IFC 4D: IfcWorkSchedule and the tasks it controls), in file order, each a WorkSchedule. Empty for a file that records none.

Scene::base()

std::optional<Link> base() const

The link the file declares as the mechanism's base, or std::nullopt when it declares none.

Scene::pose()

Result<Pose> pose(const std::vector<StateValue>& values = {}, std::optional<Link> ground = std::nullopt) const

The mechanism posed by values — rows of a joint and one number per freedom of its pair, in the pair's freedom order, exactly State::values's row type — with ground (a Link, or std::nullopt for the default ground: the declared Scene::base(), else the link on the most joints) held fixed. Values outside a freedom's range are used as given and reported in the answer's Pose::out_of_range. returns an Error in its Result on a bad row or a file with no mechanism, carrying the library's own message.

Scene::pose_state()

Result<Pose> pose_state(const State& state, std::optional<Link> ground = std::nullopt) const

The mechanism posed by a named State's values, ground held fixed as Scene::pose() does. returns an Error in its Result as Scene::pose() does.

Scene::geometry_diagnostics()

std::vector<std::string> geometry_diagnostics() const

What the reader built but the geometry stage could not finish — a face that would not trim, a surface that would not mesh — one line each. Scene::diagnostics() is what the *file* held that could not be read; this is what the geometry did.

Scene::source_name()

std::optional<std::string> source_name() const

The archive member this was read from, or std::nullopt for a plain file.

Scene::nodes()

std::vector<Node> nodes() const

Every node, in index order: assemblies, shapes, layers, storeys — structure as well as geometry. To draw, iterate Scene::placements() instead.

Scene::roots()

std::vector<Node> roots() const

The nodes nothing else contains: where a tree view starts.

Scene::walk()

std::vector<Node> walk() const

Every node reachable from the roots, parents before children.

Scene::query()

Result<std::vector<std::uint32_t>> query(const std::string& filter) const

The indices of the nodes a filter matches, in document order. The filter is one boolean expression in the query language — class == ON_Brep and within(name == Walls). A filter that does not parse returns an Error in its Result with the parser's message and position; one that matches nothing is an empty result, not an error.

Scene::placements()

std::vector<Placement> placements() const

What the document draws, and where. Not the nodes: a block or an instanced part is one node of geometry drawn at several places, and a node walk draws it once at its definition's frame. Iterate this to draw, and the nodes to build a tree. See Placement.

Scene::realize_all()

std::uint32_t realize_all() const

Build every mesh now, across all cores, and return how many were built. Reading is lazy so a tree can be on screen while the shapes are still coming; asking node by node meshes on one core, this uses them all. Watch it from another thread with Scene::realized() and Scene::realize_total(); stop it with Scene::cancel().

Scene::realize_meshes()

std::uint32_t realize_meshes(bool skip_surfaced = true) const

Scene::realize_all(), leaving alone every node that carries surfaces when skip_surfaced is true (the default): a renderer drawing those parts from their surfaces never pays for their triangles, and takes their bounds from Node::bounds(), which falls back to the surfaces.

Scene::realized()

std::uint32_t realized() const

How many nodes Scene::realize_all() has finished. Safe to read from another thread.

Scene::realize_total()

std::uint32_t realize_total() const

How many it will build in all; zero until it starts.

Scene::cancel()

void cancel() const

Ask a running Scene::realize_all() to stop. One-way for the life of the scene: later calls return at once, and meshes are still built one node at a time on request.

Scene::forget_meshes()

void forget_meshes() const

Drop every mesh the scene has built and give the memory back; the next ask rebuilds. For a viewer that has uploaded what it needs. What it frees is the document's own mesh, which Node::mesh64() lends: a view from before it points at freed memory afterwards, so the scene counts its forgets and a Mesh64 read after one is refused as a stale view rather than read. Mesh, Polylines and the Béziers are the library's own caches, which a forget does not touch and which stay good until Scene::close(); Node and Godot hand back copies, unaffected by either.

Scene::save()

Result<void> save(const std::string& path, const std::string& format = "glb") const

Write the whole scene: glb (binary glTF), gltf (text glTF, one file) or obj (every placement baked to its own named object, with a .mtl beside it when anything has a colour). Every placement of every shape, named and placed as the tree is, one material per colour; in the scene's convention (use Y-up metres for the space glTF specifies). A format outside these three, or a failed write, returns an Error in its Result.

Scene::surface_matrix()

Matrix4 surface_matrix() const

The 4×4 that puts Node::surfaces() into the space everything else is already in. Meshes and polylines arrive in the scene's convention; surfaces arrive in the file's own frame, because converting a surface means converting its parameter space too. Identity for a document opened in its native convention and units.

Scene::show()

Result<void> show(const O& options = O{}) const

Draw every visible placement — each block instance where the file puts it — with the viewer in use. Keywords: view= (front, back, left, right, top, bottom, iso), az=, el=, zoom=, up= (default from the convention the scene was opened with), edges= (the B-rep edges over the shapes; on the terminal library, edges=False also hides free curves — the two share one layer there), width=, height=, hint=. No tolerance=: a document is drawn at the tolerance it was read with. With the GPU library (import cadaclysm.view.wgpu) the drawing replaces what one shared window shows and stays live — at the REPL, in IPython and in Jupyter by themselves, in a script until it exits; cadaclysm.view.View draws several items or several windows.

Takes a cadaclysm::ShowOptions (edges true by default, azimuth/elevation/up std::optional). A tolerance is refused: a document is drawn at the tolerance it was read with. Blocks until the window is closed on the wgpu library (cadaclysm::view::use_wgpu()); the terminal prints a still. Several items or a window that stays: cadaclysm::View. A member template: #include <cadaclysm/view.hpp> to call it, and link cadaclysm::view_loader (cadaclysm::view is the linked C library, for hosts that call cadaclysm_view.h directly).

Scene::view()

auto view(const O& options = O{}) const

Orbit the model with the viewer in use; returns the camera where it was left — a dict of eye, target, up, orthographic, azimuth and elevation — and pick, what was last clicked (item, node, face) or None. A fresh window, run until closed, with the GPU library.

Takes the options show does; returns a Result<Camera>.

Scene::svg()

Result<void> svg(const std::string& path, const SvgOptions& options = SvgOptions()) const

Every visible placement's wireframe as SVG, from a camera the keywords describe rather than a viewer window: view= (front, back, left, right, top, bottom, iso — Scene::show()'s own table), az=/el= over it, up= (default from the convention this scene was opened with), fov= (0, the default, is orthographic), size= (width, height), margin= (fraction of the content's extent left each side), tolerance= (how far a written curve may stray, in page units), stroke= and background= ("#rgb", "#rrggbb" or a CSS colour name in every language, beside that language's own numeric form; background= std::nullopt for transparent), width= (the stroke's, in page units), edges=, curves=, isocurves=, polylines= (which line sets are drawn; edges by default), and silhouettes= (also draw where each solid's surfaces turn away from the eye — the outline a sphere has no edge for, on by default: a sphere's and a torus's only curves are the seams where their one face meets itself, which are not edges of the body and are never drawn, so without it those two draw a blank page. A line set in its own right, so silhouettes= with edges= off draws the outline alone).

The SVG *is* the drawing, not a picture of one: with path, this writes it to the file and returns std::nullopt; without, it returns the SVG text instead of writing anything. returns an Error in its Result on a refused option (naming the field) or a failed write.

Scene::node C++ only

std::uint32_t size() const
std::optional<Node> node(std::uint32_t index) const

The node count, and one node by index (std::nullopt past the end), without building a list.

Node

class Node

One node of the document — an assembly, a part, a body, a layer, a placement. A handle, not a snapshot: each property asks the scene when read, so nothing goes stale and nothing is built that is never looked at. Names and attributes are cheap; Node::bounds() and Node::mesh() build the geometry.

Node.scene

The scene it belongs to.

Not exposed: keep the Scene you opened.

Node::index()

std::uint32_t index() const noexcept

Its index in the scene, stable while the scene is open: a key for a map of what has been uploaded.

Node::name()

std::string name() const

The name the file gave it, or empty.

Node::id()

std::string id() const

What the file calls it: a STEP #N, an IFC GlobalId, a Rhino object id.

Node::kind()

std::string kind() const

Its type in the file: an IFC class, an openNURBS class, a STEP shape kind.

Node::label()

std::string label() const

Something to put in a tree row: the name, else the kind, else #index.

Node::depth()

std::uint32_t depth() const

How far down the tree it sits; a root is zero.

Node::generator()

std::string generator() const

What its geometry was before it was triangles — brep, mesh, csg — or empty for a node that draws nothing.

Node::visible()

bool visible() const

Whether the file says to show it when opened. The node's own switch, not inherited; true where the format has no such switch.

Node::visible_now()

bool visible_now() const

Node::visible() with every ancestor consulted: a layer switched off hides what hangs under it.

Node::locked()

bool locked() const

Whether the file says it cannot be selected or edited (Rhino's lock, own or by layer). A locked node is still drawn.

Node::parent()

std::optional<Node> parent() const

The node containing this one, or std::nullopt for a root.

Node::children()

std::vector<Node> children() const

The nodes directly under this one.

Node::instance_of()

std::optional<Node> instance_of() const

The node whose geometry this one places, or std::nullopt. A part placed seventy times is one mesh and seventy transforms; this is how a caller knows to upload it once.

Node::select_as()

Node select_as() const

What a click on this node's geometry should select — usually itself. Formats that hang geometry under the object it belongs to (an IFC representation under its product) point back at the object.

Node::attributes()

std::vector<Attribute> attributes() const

Everything the file said about the node, as Attribute values.

Node::can_mesh()

bool can_mesh() const

Whether the node has geometry of its own to draw. Builds nothing; most nodes are structure and answer false.

Node::colour()

std::optional<std::array<float, 4>> colour() const

The colour the file gave it as RGBA in 0–1, or std::nullopt — most STEP files carry none, and the caller's default is the right answer.

Node::transform()

Matrix4 transform() const

Where the node's geometry sits: a 4×4 in double precision, composed through every frame above it. Meshes stay single precision in their own frame under a double transform, so a model at survey coordinates keeps its millimetres.

Node::raw_transform()

std::array<double, 16> raw_transform() const

The same matrix as 16 numbers in the C API's column-major order, ready for a GPU uniform.

Node::bounds()

Bounds bounds() const

The extent of the node's geometry in that geometry's own frame. Builds the geometry if needed; carry it through Node::transform() for world coordinates.

Node::bounds64()

Bounds64 bounds64() const

Node::bounds() in double precision: the node's extent in that geometry's own frame, unnarrowed — exact far from the origin, where Node::bounds()'s float32 corners are not. Builds the geometry if needed.

Node::mesh()

Mesh mesh() const

Its triangles in their own frame, built now if they have not been. A node that instances another hands back the instanced node's arrays — the same memory for every placement. The arrays are views into the scene — copies in Node and Godot; see Lifetimes.

Node::mesh64()

Mesh64 mesh64() const

Node::mesh() in double precision: the document's own mesh, lent rather than narrowed — the same indices, Node::mesh()'s positions being exactly these rounded. For a mesh used as geometry (an exporter, a measurement, a solver) far from the origin, where float32 cannot hold the file's coordinates. Where the wrapper hands out views over the scene's memory, forgetting the scene's meshes invalidates them — these lend the document's own mesh, where the float mesh is a copy the library keeps and a forget leaves alone; asked again it is built again. Each one is stamped with the scene's forget count, so reading its arrays after a forget raises a stale view rather than reading freed memory — an array already taken out of it before the forget is not caught.

A checked build counts the scene's forgets and reports a stale view rather than reading freed memory; an unchecked one reads it, as the C ABI says.

Node::fem_mesh()

Result<FemMesh> fem_mesh(double tolerance = 0.01, double max_size = 0.0, const std::optional<std::array<double, 16>>& placement = std::nullopt) const

Its body meshed for a solver, as a FemMesh: nodes welded by bits — two mesh points are one node only where their coordinates are the same doubles, so no tolerance ever merges two distinct points and a crack stays a crack — triangles wound outward, and every node tagged with the lowest-dimension B-rep entity it lies on. Meshed in the part's own frame, following the hop from an instance to the shape it draws that Node::mesh() follows, so a node instanced six times meshes once.

tolerance is the chordal tolerance in model units, and it alone governs how closely the mesh follows the geometry. max_size is a size ceiling, 0 for none (curvature alone): it splits boundary segments to at most that length and lays interior stations max_size / √2 apart as a target, and it adds nodes without refining the boundary geometry — a caller that wants the rim nearer its curve lowers the tolerance, where max_size only makes the elements smaller. FemMesh::longest_edge() is what the mesh actually came to, and the figure to check.

placement is sixteen numbers, in Placement::raw_transform()'s own column-major order — which Node::bounds_placed() takes too, in the wrappers that have it — applied in double precision throughout. The kernel's Solid::fem_mesh() takes twelve instead — an origin and three axes — so a caller moving between the two reformats the placement.

The space is the body's own for a B-rep and the scene's for a mesh, which FemMesh::from_mesh() is the flag for: under a convention other than the native one those are two different spaces, and mixing these nodes with Node::transform() without reading it gives a rotated part.

A cracked body is not a failure: it comes back with FemMesh::watertight() false and its cracks in FemMesh::open_edges() and FemMesh::folded_edges(), and nothing is welded shut to make it look sound. It returns an Error in its Result for a tolerance or size the mesher refuses, a placement that is not sixteen finite and invertible numbers, a node with neither a B-rep nor a mesh (an assembly, a storey, a layer, a curve), and a body that meshes to no triangles at all.

The unlicensed notice is printed here, once, and on neither of the two .msh calls — where the kernel library is the other way round. Read FemMesh::msh_text() before porting either convention onto the other side.

Node::mesh_lod()

Mesh mesh_lod(std::uint32_t level) const

Its triangles at a coarser level of detail: 0 is Node::mesh() itself, 1 up to cadaclysm::lod_levels() each about a quarter of the triangles of the one before, and past that empty. Every level shares the level-0 vertices — the same positions, only the indices differ — so upload the vertices once and switch level by drawing a different index range. Simplifying costs about what meshing did, once per node.

Node::lod_error()

float lod_error(std::uint32_t level) const

How far Node::mesh_lod() at this level moved the surface, in the scene's units — what to pick a level by against the pixel size on screen. Zero at level 0.

Node::surfaces()

Surfaces surfaces() const

Its faces as exact surfaces plus the trim loops that cut them, each in the surface's own (u, v). Nothing is meshed to produce it, and reading it costs Node::mesh() nothing. Empty where the reader has no parametric description (a tessellated body, a mesh format). In the file's frame — see Scene::surface_matrix().

Node::edges()

Polylines edges() const

Its feature edges as polylines, for an outline overlay. Builds the geometry if needed.

Node::brep()

std::optional<Brep> brep() const

Its exact B-rep, as a Brep, for the kernel's Solid::from_node() to operate on — or std::nullopt where it has none (a mesh, a curve, a CSG body, a JT or OpenSCAD part). Shared with the scene, not copied.

Node::curves()

Polylines curves() const

Its free curves as polylines; a 2D drawing is all of these.

Node::isocurves()

Polylines isocurves() const

Lines ruled across its surfaces, so a curved face reads as curved in a wireframe. A flat face yields its outline, so these can overlap Node::edges().

Node::edge_beziers()

Beziers edge_beziers() const

Its feature edges as cubic Bézier curves, a Beziers — exact where the file's curves were, where Node::edges() are their chords. Builds the geometry if needed.

Node::edge_beziers64()

Beziers64 edge_beziers64() const

Node::edge_beziers() in double precision, kept beside the float Béziers: a forget leaves it valid, unlike Node::mesh64() — in a wrapper that lends views. Node and Godot copy both, so a forget leaves both valid.

Node::curve_beziers()

Beziers curve_beziers() const

Its free curves as cubic Béziers; see Node::edge_beziers().

Node::curve_beziers64()

Beziers64 curve_beziers64() const

Node::curve_beziers() in double precision; see Node::edge_beziers64().

Node::isocurve_beziers()

Beziers isocurve_beziers() const

Its isocurves as cubic Béziers; see Node::edge_beziers().

Node::isocurve_beziers64()

Beziers64 isocurve_beziers64() const

Node::isocurve_beziers() in double precision; see Node::edge_beziers64().

Node::collision()

std::optional<Collision> collision(std::uint32_t hull_budget = 0) const

The collision body for what this node draws, as a Collision: a box, sphere, capsule or cylinder where one fits, else a convex hull. hull_budget is the most triangles a hull may have; 0 (the default) asks for the Unity limit of 255 and is not clamped to it. std::nullopt for a node that draws nothing. Builds the mesh if it is not built; cached per node and budget.

Node::collision_hull()

CollisionHull collision_hull(std::uint32_t hull_budget = 0) const

The convex hull Node::collision() counted, as triangles — a CollisionHull, a view into the scene (a copy in Node and Godot). Empty for a node that draws nothing. Asking the same node for a different hull_budget refits it and frees the hull the earlier call returned: hold one budget.

The surface path

Node::is_meshed()

bool is_meshed() const

Whether its mesh has been built and is held — by Scene::realize_all(), by an ask for it, or by anything else that needed it. A renderer drawing the part from its surfaces checks it never paid for the triangles.

Node::triangle_estimate()

std::int64_t triangle_estimate() const

About how many triangles Node::mesh() would give, without building it — for sizing a budget before meshing. Exact for a stored mesh, within a few tens of percent for a B-rep; -1 where the reader cannot say without doing the work, and for a node that draws nothing. Treat -1 as unknown, never as zero.

Node::bounds_placed()

Bounds bounds_placed(const std::array<double, 16>& placement) const
Bounds bounds_placed() const

The box of what this node draws under a placement (16 numbers, column-major, as Placement::raw_transform(); std::nullopt for the identity), for a part drawn from its surfaces: every sample is carried through the convention and the placement before it is boxed, so it is tighter than placing the corners of Node::bounds(). All zeros for a part with no surfaces.

Node::bounds_placed64()

Bounds64 bounds_placed64(const std::array<double, 16>& placement) const
Bounds64 bounds_placed64() const

Node::bounds_placed() in double precision, from the unnarrowed surface samples; see Node::bounds64().

Node::surface_edges()

Polylines surface_edges() const

Its face boundaries taken from its trimmed surfaces — the outline that costs no tessellation, where Node::edges() meshes the part. In the surfaces' own frame (Scene::surface_matrix()); empty without surfaces. A shared edge appears once from each face.

Node::surface_edge_beziers()

Beziers surface_edge_beziers() const

Its edges as the exact curves, a Beziers, where the part has them without meshing — a Rhino extrusion's rims are its profile — and empty everywhere else. A caller drawing from surfaces tries it before Node::surface_edges(), whose trims are thinned to the mesh tolerance and so cut chords across a curve the surface rounds. The same segments as Node::edge_beziers(), in the scene's space: not the surfaces' frame, so no Scene::surface_matrix().

Node::edge_colours()

std::vector<std::optional<std::array<float, 4>>> edge_colours() const

One RGBA per polyline of Node::edges(), and none for an edge the file does not style; empty when nothing is styled. STEP files carry them as CURVE_STYLE styling.

Node::surface_edge_colours()

std::vector<std::optional<std::array<float, 4>>> surface_edge_colours() const

Node::edge_colours() for Node::surface_edges().

Node::surface_isocurves()

Polylines surface_isocurves() const

Its isocurves taken from its trimmed surfaces and clipped to the trims, without meshing: lines at a surface's bend lines and an even spread where it has none; a flat face gets none. In the surfaces' frame; empty without surfaces.

Node::surface_pick()

std::optional<Vec3> surface_pick(const Vec3& from, const Vec3& to) const

Where a segment first meets this part's surfaces, or std::nullopt. Exact — it answers from the surface and tests the trims at the hit's own (u, v) — and in the surfaces' own frame: carry a ray from the scene's space through the inverse of Scene::surface_matrix() first.

Node::surface_proxy_mesh()

Mesh surface_proxy_mesh(std::uint32_t cells) const

A coarse mesh over its surfaces for the things that need triangles and not a picture — ray tracing, distance fields: each face gridded cells by cells over its trim window, two triangles a cell whose centre lies inside the trims, never welded. Built once per part at the first cells asked for. In the scene's space, like Node::mesh(). Empty without surfaces or for zero cells.

Node::save_mesh()

Result<void> save_mesh(const std::string& path, const std::string& format = "stl") const

Write this node's own mesh — where it is defined, without its placement — in one of cadaclysm::mesh_formats(). A node that draws nothing, or an unknown format, returns an Error in its Result; ask Node::can_mesh() first to grey out a menu entry. To write the whole model, see Scene::save().

Node::walk()

std::vector<Node> walk() const

This node and every node under it, parents before children.

Node::show()

Result<void> show(const O& options = O{}) const

Draw what this node and everything under it places with the viewer in use — shown by handle on its scene, hiding the siblings along the path from the scene's root to this node (each ancestor's own geometry stays visible) and framing this node. Keywords as Scene::show(). With the GPU library (import cadaclysm.view.wgpu) the drawing replaces what one shared window shows and stays live — at the REPL, in IPython and in Jupyter by themselves, in a script until it exits; cadaclysm.view.View draws several items or several windows.

Takes a cadaclysm::ShowOptions (edges true by default, azimuth/elevation/up std::optional). A tolerance is refused: a document is drawn at the tolerance it was read with. Blocks until the window is closed on the wgpu library (cadaclysm::view::use_wgpu()); the terminal prints a still. Several items or a window that stays: cadaclysm::View. A member template: #include <cadaclysm/view.hpp> to call it, and link cadaclysm::view_loader (cadaclysm::view is the linked C library, for hosts that call cadaclysm_view.h directly). The shown Node borrows from its Scene: keep the Scene open.

Node::view()

auto view(const O& options = O{}) const

Orbit this node and everything under it; returns the camera where it was left — a dict of eye, target, up, orthographic, azimuth and elevation — and pick, what was last clicked (item, node, face) or None. A fresh window, run until closed, with the GPU library.

Takes the options show does; returns a Result<Camera>.

Node::svg()

Result<void> svg(const std::string& path, const SvgOptions& options = SvgOptions()) const

This node's own wireframe as SVG, in its own frame — Scene::svg()'s keywords, read from just this node rather than every placement. With path, writes the file and returns std::nullopt; without, returns the SVG text. returns an Error in its Result on a refused option (naming the field) or a failed write.

Brep

class Brep

A body's exact B-rep — the trimmed surfaces its mesh is cut from — shared with the scene rather than copied, and held by this object until it is released. It is what Node::brep() hands the kernel's Solid::from_node(), which operates on it without a copy, and it can say whether it is a Manifold. It outlives the scene it came from for as long as anything holds it. In the node's own frame and the file's own units and axes; the kernel library must come from the same release as the reader.

Brep::pointer()

const CadaclysmBrep* pointer() const noexcept

The brep's C pointer, which the kernel's wrapper hands across. returns an Error in its Result once released.

Brep::layout_id()

static std::string layout_id()

How this library lays a brep out in memory: its compiler, target and source. The kernel shares a brep only with a reader whose id equals its own.

Brep::manifold()

Result<Manifold> manifold() const

Whether its faces make a manifold — every edge bordered by one face or two, the faces round every vertex one fan — and whether it is closed, as a Manifold. Read off the topology the file wrote, not a mesh. returns an Error in its Result once released.

Brep::sewed()

Result<std::pair<Brep, SewReport>> sewed(std::optional<double> tolerance = std::nullopt, std::optional<double> vertex = std::nullopt) const

This brep with its free edges sewn, and what was sewn, as a new Brep and a SewReport; this one is left as it was. Two free edges become one shared edge where both their ends coincide within vertex — by default the rounding Brep::manifold() merges vertices at — and their curves lie within tolerance of each other, by default vertex too, so only edges that are one curve written twice are joined. A larger tolerance also joins two curves between the same two vertices that part by up to it; vertex is what decides which edges are candidates at all, and raising it is what could close a slot. Nothing moves. returns an Error in its Result once released.

Brep::release()

void release() noexcept

Give the reference back now. Leaving a with block, or the garbage collector, does it otherwise.

Placement

class Placement

One drawing of one node's geometry at one place: what Scene::placements() lists. Two drawings of the same shape name the same geometry node, and so the same arrays — upload once, draw twice.

Placement.scene

The scene it belongs to.

Not exposed: keep the Scene you opened.

Placement::index()

std::uint32_t index() const noexcept

Its index in Scene::placements().

Placement::geometry()

Node geometry() const

The node whose mesh, edges and curves this draws.

Placement::select()

Node select() const

What a click on this drawing selects: the placement's own node rather than the shared shape, which would light up every copy.

Placement::transform()

Matrix4 transform() const

Where to draw it: a 4×4, already composed through every frame from the root.

Placement::raw_transform()

std::array<double, 16> raw_transform() const

The same matrix as 16 numbers, column-major.

Joint

class Joint

A connection between two links. Its two ends keep the file's order, not a parent and a child, since a mechanism may be a network with loops.

Joint.scene

The scene this joint belongs to.

Not exposed: keep the Scene you opened.

Joint::index()

std::uint32_t index() const noexcept

Its position in the scene's joints.

Joint::name()

std::string name() const

Its name, as the file gives it.

Joint::start()

Link start() const

The link it starts at.

Joint::end()

Link end() const

The link it ends at.

Joint::pair()

std::optional<Pair> pair() const

How this joint moves: the kinematic pair naming it, a Pair, or std::nullopt when the file gives it none.

Pair

class Pair

A kinematic pair: the kind of relative motion a joint allows between its two links — what Joint::pair() returns.

Pair::kind()

PairKind kind() const noexcept

One of thirteen kinds: revolute, prismatic, cylindrical, spherical, spherical_with_pin, planar, universal, homokinetic, screw, fully_constrained, unconstrained, low_order, other.

Pair::type_name()

std::optional<std::string> type_name() const

For other: the pair's entity type name (GEAR_PAIR). std::nullopt otherwise.

Pair::pitch()

std::optional<double> pitch() const

For screw: its pitch, a length per revolution in the scene's units. std::nullopt otherwise.

Pair::frame_start()

Matrix4 frame_start() const

The pair's contact frame fixed in its joint's start link, in world coordinates at the file's rest pose: a 4x4, already composed like Node::transform().

Pair::frame_end()

Matrix4 frame_end() const

The pair's contact frame fixed in its joint's end link.

Pair::raw_frame_start()

std::array<double, 16> raw_frame_start() const

frame_start as 16 numbers, column-major — like Node::raw_transform().

Pair::raw_frame_end()

std::array<double, 16> raw_frame_end() const

frame_end as 16 numbers, column-major.

Pair::freedoms()

std::vector<Freedom> freedoms() const

Its degrees of freedom, in the reader's per-kind order, each a Freedom. A State's values compose in this order.

Freedom

struct Freedom

One degree of freedom of a Pair: which motion, about or along which contact-frame axis, and its optional range. From Pair::freedoms().

Freedom::motion

Motion motion

rotation or translation.

Freedom::axis

FreedomAxis axis

x, y or z: the contact frames' axis the motion is about or along.

Freedom::lower

std::optional<double> lower

The lower limit — radians for a rotation, a length for a translation — or std::nullopt for unbounded.

Freedom::upper

std::optional<double> upper

The upper limit, or std::nullopt for unbounded.

State

struct State

A named pose of the file's mechanism: a MECHANISM_STATE_REPRESENTATION. From Scene::states().

State::index

std::uint32_t index

Its position in the scene's states.

State::name

std::string name

Its name, as the file gives it.

State::values

std::vector<StateValue> values

One row per joint this state sets, in file order: the joint, then one value per freedom of its pair, in the pair's freedom order.

Sequence

struct Sequence

A motion the file records: an INTERPOLATED_CONFIGURATION_SEQUENCE, its states at increasing parameter values. From Scene::sequences().

Sequence::index

std::uint32_t index

Its position in the scene's sequences.

Sequence::name

std::string name

Its name: its representation's, name #2 and on for further sequences in one representation.

Sequence::closed

bool closed

Whether the motion returns to its first segment (its last segment is not discontinuous).

Sequence::seconds_per_unit

std::optional<double> seconds_per_unit

Seconds per unit of the parameter where the file states a time unit, or std::nullopt where it states none (the parameter is then read as seconds).

The seconds rule, to play a sequence on a clock: with a duration, stretch start to end (plus the closing span of a closed sequence, (end - start) divided by one less than the number of segments) over it; with none, seconds are (t - start) times Sequence::seconds_per_unit, or times 1 where it is std::nullopt.

Sequence::segments

std::vector<Segment> segments

The segments, in file order (at least two), each a Segment.

Sequence::start()

double start() const

The first segment's parameter.

Sequence::end()

double end() const

The last segment's parameter (the closing span of a closed sequence not included).

Sequence::pose()

Result<Pose> pose(double t, std::optional<Link> ground = std::nullopt) const

The mechanism posed by this sequence at parameter t, in the file's own parameter: a segment is a keyframe of its state, a discontinuous segment holds until the next and every other kind moves linearly, an open sequence holds its ends and a closed one wraps (its closing span, last back to first, is the mean of its spans, and each joint loops on its own keys, so one that only some segments name still comes back round smoothly). ground is held fixed as Scene::pose() does. returns an Error in its Result for a t that is not finite.

Segment

struct Segment

One step of a Sequence: the state it reaches, its parameter, and how the motion leaves it for the next. From Sequence::segments.

Segment::state

State state

The State this segment reaches.

Segment::t

double t

Its parameter.

Segment::interpolation

std::string interpolation

How the motion leaves it: "undefined", "discontinuous", "synchronous" or "linear".

WorkSchedule

struct WorkSchedule

A building's programme: an IfcWorkSchedule and the tasks it controls. From Scene::schedules(). Dates are seconds since 1970-01-01T00:00 (no time zone); durations and lags are seconds.

WorkSchedule::index

std::uint32_t index

Its position in the scene's schedules.

WorkSchedule::name

std::string name

Its name, else its identification, else schedule #k (k from 1, in file order).

WorkSchedule::identification

std::string identification

Its identification, empty where the file gives none.

WorkSchedule::kind

std::string kind

"planned", "actual", "baseline", "notdefined" or "other".

WorkSchedule::kind_name

std::string kind_name

The text of a user-defined kind (kind is "other"); empty otherwise.

WorkSchedule::start

std::optional<double> start

The schedule's start date, or std::nullopt where the file gives none.

WorkSchedule::finish

std::optional<double> finish

The schedule's finish date, or std::nullopt where the file gives none.

WorkSchedule::tasks()

std::vector<Task> tasks() const

Its tasks, in file order, each a Task.

WorkSchedule::at()

Result<std::vector<ElementState>> at(double date, std::string_view dates = "scheduled") const

What each element this schedule names is doing on date, as ElementState rows in node order; an element no task names is not listed. dates says which of a task's dates to play: "scheduled" (the default) or "actual", where each actual date overrides its scheduled one. A task builds its nodes over its window, or removes them (demolition, dismantling, removal, disposal); summary tasks span their children. returns an Error in its Result for a date that is not finite, and a dates other than those two words is refused.

Task

struct Task

One task of a WorkSchedule. From WorkSchedule::tasks().

Task.schedule

The WorkSchedule it belongs to.

Not exposed: keep the WorkSchedule you read it from.

Task::index

std::uint32_t index

Its position in its schedule's tasks.

Task::name

std::string name

Its name.

Task::identification

std::string identification

Its identification, empty where the file gives none.

Task::kind

std::string kind

"attendance", "construction", "demolition", "dismantle", "disposal", "installation", "logistic", "maintenance", "move", "operation", "removal", "renovation", "notdefined" or "other".

Task::kind_name

std::string kind_name

The text of a user-defined kind (kind is "other"); empty otherwise.

Task::status

std::string status

Its status as the file writes it, empty where none.

Task::milestone

bool milestone

Whether it is a milestone: an instant, not a span.

Task::parent()

std::optional<Task> parent() const

The summary task nesting it, or std::nullopt for a top-level task.

Task::time

TaskTime time

Its dates, a TaskTime.

Task::nodes()

std::vector<Node> nodes() const

The nodes it builds or removes, each a Node.

TaskTime

struct TaskTime

Every date pair a task gives, in seconds since 1970-01-01T00:00; each is std::nullopt where the file gives none. From Task::time.

TaskTime::schedule_start

std::optional<double> schedule_start

The scheduled start.

TaskTime::schedule_finish

std::optional<double> schedule_finish

The scheduled finish.

TaskTime::early_start

std::optional<double> early_start

The early start.

TaskTime::early_finish

std::optional<double> early_finish

The early finish.

TaskTime::late_start

std::optional<double> late_start

The late start.

TaskTime::late_finish

std::optional<double> late_finish

The late finish.

TaskTime::actual_start

std::optional<double> actual_start

The actual start.

TaskTime::actual_finish

std::optional<double> actual_finish

The actual finish.

TaskTime::duration

std::optional<double> duration

The scheduled duration, in seconds.

ElementState

struct ElementState

What one element is doing on a date: WorkSchedule::at()'s rows.

ElementState::node

Node node

The Node.

ElementState::phase

std::string phase

"unbuilt", "building", "built", "demolishing" or "demolished".

ElementState::progress

double progress

How far through the window that governs the phase, 0 to 1: 0 unbuilt, 1 built or demolished.

ElementState::variance

std::optional<double> variance

Seconds late (positive) or early (negative) against the schedule, or std::nullopt where there is none to give.

Pose

struct Pose

A posed mechanism: Scene::pose(), Scene::pose_state() or Sequence::pose()'s answer. The document itself is unchanged. Copied eagerly out of the C handle when it is made, so it borrows nothing and outlives a Scene::close().

Pose::ground

Link ground

The link held fixed.

Pose::unsolved

std::vector<Joint> unsolved

Joints that close a loop and were not honoured, in file order, each a Joint.

Pose::out_of_range

std::vector<std::tuple<Joint, std::uint32_t, double>> out_of_range

One entry per given value that fell outside its freedom's range: the Joint, the freedom's index in its pair, and the value used.

Pose::transform()

Matrix4 transform(const Node& node) const

A node's posed transform, in the same form Node::transform() returns.

Pose::raw_transform()

std::array<double, 16> raw_transform(const Node& node) const

The same matrix in the ABI's own column-major order, as 16 numbers.

Pose::show()

Result<void> show(const O& options = O{}) const

Draw this pose's scene with the viewer in use, as Scene::show() does, but with each placement moved by its select node's pose — each node a joint turned draws at its posed transform rather than its rest one. Same keywords as Scene::show().

Takes a cadaclysm::ShowOptions (edges true by default, azimuth/elevation/up std::optional). A tolerance is refused: a document is drawn at the tolerance it was read with. Blocks until the window is closed on the wgpu library (cadaclysm::view::use_wgpu()); the terminal prints a still. Several items or a window that stays: cadaclysm::View. A member template: #include <cadaclysm/view.hpp> to call it, and link cadaclysm::view_loader (cadaclysm::view is the linked C library, for hosts that call cadaclysm_view.h directly). The shown Pose borrows from its Scene: keep the Scene open.

Pose::view()

auto view(const O& options = O{}) const

Pose::show()'s orbiting twin; returns what Scene::view() returns — the camera where it was left, and the last pick.

Takes the options show does; returns a Result<Camera>.

Mesh

class Mesh

A node's triangles, in the node's own frame: what Node::mesh() returns. In every wrapper that can lend a view the arrays are read-only views into the scene's memory, not copies — a large assembly is tens of millions of triangles, and most of them go straight to a GPU — and Mesh::copy() makes arrays of your own. Node and Godot copy instead, having no borrowed-view machinery at all: their arrays are theirs already, outlive Scene::close(), and need no copy. The same split as a FemMesh's arrays, and for the same reason.

Mesh::positions()

Span<const float> positions() const

Three floats a vertex.

Mesh::normals()

Span<const float> normals() const

Three floats a vertex, or std::nullopt for a mesh that carries none.

Mesh::uvs()

Span<const float> uvs() const

Two floats a vertex, or std::nullopt: only readers asked for world-scale UVs fill them. One unit of u or v is one world unit, so faces overlap in UV space — a tiling material, not a lightmap.

Mesh::colors()

Span<const float> colors() const

Four floats (RGBA) a vertex, or std::nullopt — the common case. Only a body painted in several colours, opened with colours on, carries them.

Mesh::indices()

Span<const std::uint32_t> indices() const

Three vertex indices a triangle, unsigned 32-bit.

Mesh::vertex_count()

std::uint32_t vertex_count() const

How many vertices.

Mesh::index_count()

std::uint32_t index_count() const

How many indices: three a triangle.

Mesh::triangle_count()

std::uint32_t triangle_count() const

How many triangles.

Mesh::copy()

MeshData copy() const

The same arrays in memory of your own, safe to keep after Scene::close(). Deliberately visible: on a large model this is where the gigabytes go.

Span C++ only

using Span = std::span<T>
class Span

What every array view is: std::span<const T> under C++20, else the same read-only subset of it — data(), size(), empty(), begin()/end(), operator[] and subspan() — so a range-for or an upload call reads it the same way either way.

Polylines

class Polylines

Edges or curves already flattened to points, in the node's own frame: what Node::edges(), Node::curves() and Node::isocurves() return. Views into the scene, like Mesh — copies in Node and Godot, as there.

Polylines::positions()

Span<const float> positions() const

Three floats a point, the runs end to end.

Polylines::counts()

Span<const std::uint32_t> counts() const

How many points each run has, in order.

Polylines::polyline_count()

std::uint32_t polyline_count() const

How many runs.

Polylines::vertex_count()

std::uint32_t vertex_count() const

How many points in all.

Polylines::segment_indices()

std::vector<std::uint32_t> segment_indices() const

Index pairs into the positions, two per line segment — what GL_LINES and every pair-taking API want. Indices rather than points, so a caller can transform the points once and expand afterwards.

Polylines::segments()

std::vector<float> segments() const

The segment endpoints themselves, two points per segment.

Beziers

class Beziers

Edges, curves or isocurves as cubic Bézier curves, exact where the file's curves were: what Node::edge_beziers(), Node::curve_beziers() and Node::isocurve_beziers() return. Views into the scene, like Polylines — copies in Node and Godot, as there.

Beziers::points()

Span<const float> points() const

Four control points a curve, three floats each.

Beziers::weights()

Span<const float> weights() const

A weight per control point: all ones for a polynomial curve, and the weights that make a circular arc exact for a rational one.

Beziers::count()

std::uint32_t count() const

How many curves.

Beziers::copy()

BeziersData copy() const

The same arrays in memory of your own, safe to keep after Scene::close().

Mesh64

class Mesh64

Mesh in double precision, unnarrowed: what Node::mesh64() returns. Positions, normals and uvs keep the file's own precision; colours stay float32, as Mesh::colors() do. Views into the scene's memory, like Mesh — copies in Node and Godot, as there.

Mesh64::positions()

Span<const double> positions() const

Three doubles a vertex.

Mesh64::normals()

Span<const double> normals() const

Three doubles a vertex, or std::nullopt for a mesh that carries none.

Mesh64::uvs()

Span<const double> uvs() const

Two doubles a vertex, or std::nullopt; see Mesh::uvs().

Mesh64::colors()

Span<const float> colors() const

Four floats (RGBA) a vertex, or std::nullopt — always float32, as Mesh::colors() are.

Mesh64::indices()

Span<const std::uint32_t> indices() const

Three vertex indices a triangle, unsigned 32-bit — the same triangles as Mesh::indices().

Mesh64::vertex_count()

std::uint32_t vertex_count() const

How many vertices.

Mesh64::index_count()

std::uint32_t index_count() const

How many indices: three a triangle.

Mesh64::triangle_count()

std::uint32_t triangle_count() const

How many triangles.

Mesh64::copy()

MeshData64 copy() const

The same arrays in memory of your own, safe to keep after a forget or Scene::close().

Beziers64

class Beziers64

Beziers in double precision, unnarrowed: what Node::edge_beziers64(), Node::curve_beziers64() and Node::isocurve_beziers64() return. Views into the scene, like Beziers — copies in Node and Godot, as there.

Beziers64::points()

Span<const double> points() const

Four control points a curve, three doubles each.

Beziers64::weights()

Span<const double> weights() const

A weight per control point, as doubles.

Beziers64::count()

std::uint32_t count() const

How many curves.

Beziers64::copy()

BeziersData64 copy() const

The same arrays in memory of your own, safe to keep after Scene::close().

Bounds64

struct Bounds64

Bounds in double precision, unnarrowed: what Node::bounds64(), Node::bounds_placed64() and Scene::bounds64() return.

Bounds64::min

std::array<double, 3> min

The low corner.

Bounds64::max

std::array<double, 3> max

The high corner.

Bounds64::is_empty()

bool is_empty() const noexcept

Whether this is the all-zero box that stands for nothing.

Bounds64::size()

std::array<double, 3> size() const noexcept

The extent along each axis.

Bounds64::centre()

std::array<double, 3> centre() const noexcept

The midpoint.

Collision and CollisionHull

struct Collision

What Node::collision() found: shape (0 none, 1 box, 2 sphere, 3 capsule, 4 cylinder, 5 hull, named by shape_name), confidence, the axis a capsule or cylinder runs along, frame (16 numbers, column-major) and half_extent — always the true oriented box — radius, height, the fit's error, and the hull's vertex and index counts. Plain data, copied out of the scene. Node::collision_hull() is the hull itself: positions (three floats a vertex) and indices (three a triangle), views into the scene — copies in Node and Godot.

Collision::shape_name()

std::string_view shape_name() const noexcept

none, box, sphere, capsule, cylinder or hull.

Collision::error

double error

How far the fitted shape strays from the mesh, in the scene's units; zero for an exact fit.

Collision::frame

std::array<double, 16> frame

The oriented box's frame, 16 numbers column-major.

Collision::half_extent

std::array<double, 3> half_extent

Half the oriented box's size along its own three axes.

Collision

struct Collision
std::uint32_t shape
std::uint32_t confidence
std::uint32_t axis
double radius
double height
std::uint32_t hull_vertex_count
std::uint32_t hull_index_count

shape, confidence, axis, radius, height, hull_vertex_count and hull_index_count, alongside frame, half_extent, error and shape_name.

CollisionHull

class CollisionHull
Span<const float> positions() const
Span<const std::uint32_t> indices() const
std::uint32_t vertex_count() const
std::uint32_t index_count() const

The hull's positions, indices, vertex_count and index_count.

Meshlets

class Meshlets

A mesh split into meshlets, optionally with coarser levels above them, for a mesh-shader or meshlet-based renderer. Built from any mesh — a Node::mesh() or arrays of your own — and owned by you: free it, or let the language's scope do it (see Lifetimes).

Meshlets::build()

static Result<Meshlets> build(Span<const float> positions, Span<const float> normals, Span<const std::uint32_t> indices, std::uint32_t max_triangles, std::uint32_t max_vertices, std::int32_t levels = 0)

Split positions (three floats a vertex), normals (the same, or none) and indices (three a triangle) into meshlets of at most max_triangles and max_vertices each. The limits are the consumer's own, with no default: Nanite takes 128 and 256, a mesh-shader pipeline 124 and 64, and the vertex cap is the one that bites. levels above 0 groups and simplifies each level into the next until one meshlet is left; Meshlets::level() and a meshlet's children say which is which. A budget of zero, or arrays whose lengths do not agree, returns an Error in its Result before the library is called.

Meshlets::count()

std::uint32_t count() const

How many meshlets, every level counted.

Meshlets::triangle_count()

std::uint32_t triangle_count(std::uint32_t i) const

How many triangles meshlet i has.

Meshlets::vertex_count()

std::uint32_t vertex_count(std::uint32_t i) const

How many vertices meshlet i has.

Meshlets::level()

std::uint32_t level(std::uint32_t i) const

0 for a leaf over the mesh itself, higher for a simplified level above it.

Meshlets::group()

std::uint32_t group(std::uint32_t i) const

Which group of a levelled build meshlet i belongs to.

Meshlets::error()

float error(std::uint32_t i) const

How far meshlet i's level moved the surface; zero at level 0.

Meshlets::child_count()

std::uint32_t child_count(std::uint32_t i) const

How many finer meshlets sit below meshlet i.

Meshlet

struct Meshlet
std::uint32_t index
std::uint32_t level
std::uint32_t group
float error
std::uint32_t vertex_count() const noexcept
std::uint32_t triangle_count() const noexcept
std::vector<float> positions
std::vector<float> normals
std::vector<std::uint32_t> indices
std::vector<std::uint32_t> children
Meshlet meshlet(std::uint32_t i) const

One meshlet's arrays and numbers, copied out: index, level, group, error, vertex_count, triangle_count, positions, normals (zeros where the mesh had none), indices (into the meshlet's own vertices) and children. The arrays are yours.

Meshlets::free()

void free() noexcept

Give the meshlets back. Idempotent; the collector or scope does it otherwise.

Meshlets::freed()

bool freed() const noexcept

Whether Meshlets::free() has run.

FemMesh

class FemMesh

One body meshed for a solver: nodes welded by bits, triangles wound outward, every node tagged with the lowest-dimension B-rep entity it lies on, and every crack reported rather than closed. What Node::fem_mesh() returns, and owned by you: free it, or let the language's scope do it (see Lifetimes).

A handle rather than a snapshot. In every wrapper that can lend a view, the big arrays are read-only views into the library's own memory, as Node::mesh()'s are and for the same reason — a solver mesh is megabytes, and copying it to hand it over would cost that twice. Node and Godot copy instead, having no borrowed-view machinery at all, so for those two the array is theirs and everything below about a stale view falls away. Either way the owner of the memory is this object rather than the scene: Scene::close() neither frees a FEM mesh nor stales one, and meshing the body again does not either. Only FemMesh::free() does, or the language's own scope or collector reclaiming the handle.

So keep the FEM mesh itself reachable for as long as you read a view of it, and copy anything that must outlive it. What refuses a stale read is a different mechanism in each language, so all ten are named here rather than a rule with an "otherwise" in it. Swift's view type carries its owner and asks it before every element, so the read itself is refused — it traps inside the subscript getter, measured in a release build. Rust settles it before the program runs: its free takes the mesh by value, so a slice can neither be asked for after it nor held across it. Godot and Node hand back copies, so there is nothing of the library's left to go stale. Python, C#, Java and C++ check the handle in the accessor, so a view asked for after the free is refused and one already in hand is a bare array, span or buffer that reads freed memory — measured in C#, where a span taken before the free read the body's own coordinates out of the freed block. Python's numpy array does hold the mesh, which keeps it reachable (below), but nothing asks the mesh when the array is read, so one kept past an explicit free() dangles as C#'s span does. Go's and LuaJIT's arrays are fields filled when the mesh was built rather than accessors, so neither moment is refused there; their methods are, and for those two this is documented rather than enforced, as their own module headers already do for the scene.

Staying reachable is a second question, and a narrower one. In C#, Java, Go and LuaJIT the view is the platform's own array type with nowhere to hold a reference back to the handle, so a program that keeps a view and lets go of the mesh can have the collector free the handle under it with nothing ever calling FemMesh::free() — measured in C#, Java and Go. C++ has no collector but the same shape at scope end: the mesh's destructor frees the handle when it goes out of scope, and a Span kept past that reads freed memory. A view type that carries its owner is safe from that (Python's, Swift's), and so is a language that rules the case out — Rust borrows the mesh for the view's lifetime, Godot and Node copy. The same rule as Solid::mesh()'s own views.

FemMesh::nodes()

Span<const double> nodes() const

Every node's position, three doubles each, placed by Node::fem_mesh()'s placement and in the space FemMesh::from_mesh() names. Every node is used by at least one triangle.

The five arrays are Spans into the handle's own memory, and every accessor here checks the handle in both modes — not only under CADACLYSM_CHECKED, as the scene's and the solid's views do, because what it guards is a pointer handed to C, the way Meshlets' own check does, rather than a borrowed view's owner. So a call on a freed mesh — an array, a count, or either .msh call — goes to CADACLYSM_BAD_ACCESS rather than returning an Error, even in a release build. A Span already in hand is a pointer and a length from then on, and reading one after free() — or after the FemMesh itself went out of scope — reads freed memory in either mode: measured on a body away from the origin, it still gave that body's own coordinates until the block was reused. So hold the FemMesh while you read a span, and copy (std::vector<double>(mesh.nodes().begin(), mesh.nodes().end())) anything that must outlive it. FemEdge's nodes and runs borrow the same way.

FemMesh::triangles()

Span<const std::uint32_t> triangles() const

Three node indices a triangle, wound outward — a mirroring placement is wound back.

FemMesh::triangle_face()

Span<const std::uint32_t> triangle_face() const

Which B-rep face each triangle lies on, one per triangle, into the body's FemMesh::face_count() faces.

FemMesh::node_kind()

Span<const std::uint32_t> node_kind() const

What each node lies on — 0 a B-rep vertex, 1 an edge, 2 a face — one per node: the lowest-dimension entity it lies on, which is the .msh format's own classification rule. FemMesh::node_entity() says which entity of that kind.

FemMesh::node_entity()

Span<const std::uint32_t> node_entity() const

Which vertex, edge or face each node lies on, read by the matching FemMesh::node_kind(): an index into FemMesh::vertices(), into FemMesh::edges(), or into the body's faces.

FemMesh::face_count()

std::uint32_t face_count() const

The body's faces; FemMesh::triangle_face() and a FemMesh::node_kind() of 2 index them.

FemMesh::edges()

Result<std::vector<FemEdge>> edges() const

One FemEdge per B-rep edge, in the order a FemMesh::node_kind() of 1 indexes them. Empty for a FemMesh::from_mesh() body, which has no B-rep edges at all.

FemEdge

struct FemEdge
std::uint32_t id
Span<const std::uint32_t> nodes
Span<const std::uint32_t> runs
std::pair<std::uint32_t, std::uint32_t> faces
std::pair<std::uint32_t, std::uint32_t> ends
bool closed
bool seam
std::vector<Span<const std::uint32_t>> chains() const

One B-rep edge's chain of nodes, and where that chain breaks. nodes are this mesh's node indices in order along the edge, its end vertices included. runs says where the chain breaks: each entry is a start offset into nodes, and the nodes from one offset up to the next — the last offset running to the end — are a polyline of their own, with nothing joining across a boundary. The two ends either side of a boundary are two points of the edge with no mesh edge between them, a crack along the edge or a stretch of it sampled on one face only. (0,) is the ordinary answer, and a caller reading nodes as one polyline without looking here jumps the gap silently.

faces is (face_a, face_b) and ends is (end_a, end_b), the second of each a sentinel where there is none — an open body's rim, or both ends at one vertex. 0 is a real face and a real vertex, not a sentinel. Which end comes first is the first trim's direction and means nothing else: the pair bounds the edge, it does not orient it. closed where the nodes make one loop, never with more than one run; seam where one face bounds the edge twice, and both faces are then that same face.

id is the body's own edge id, not this mesh's edge index: FemMesh::edges() is a densely renumbered subset of the body's edges, ascending by id, with every edge collapsed to a point left out, so edge 0 of a STEP body's mesh routinely has an id in the hundreds. Everything else that names an edge means the index — a FemMesh::node_kind() of 1 read through FemMesh::node_entity(), the third number of a FemMesh::open_edges() or FemMesh::folded_edges() row, and the edge_<i> physical group of FemMesh::msh_text() — and this is the one way back from any of them to the topology the file wrote.

FemMesh::vertices()

Result<std::vector<FemVertex>> vertices() const

One FemVertex per B-rep vertex, in the order a FemMesh::node_kind() of 0 indexes them. Empty for a FemMesh::from_mesh() body.

FemVertex

struct FemVertex
std::uint32_t node
Vec3 point
bool has_position

One B-rep vertex: node is the mesh node there, a sentinel where the mesh has none, which is ordinary rather than a fault — the analysis rebuilds a vertex wherever two trims meet, so a sphere has 48 of them where the mesh has two points, and a caller walking these skips the sentinel instead of reading it as a gap.

point is where the topology says the vertex is, in the same space and under the same placement as FemMesh::nodes() — the file's own vertex rather than a mesh node, so the two can differ by the reader's rounding. Meaningless unless has_position: it is all zeros then, a point no geometry has and one a solver would read as a node at the origin.

FemMesh::open_edges()

Result<std::vector<std::array<std::uint32_t, 3>>> open_edges() const

Every crack, as (a, b, brep_edge): a directed mesh edge (a, b) with no (b, a), and the B-rep edge both nodes lie on where they share one.

Empty unless the body's topology is closed — for a B-rep body, whose mesh is otherwise not asked about at all. The census asks whether a body that ought to enclose a solid does, and an open one — a sheet, a bag of surfaces, a shell the file itself wrote open — makes no such claim to check: its rim is not a crack. So such a body reports FemMesh::watertight() false with this and FemMesh::folded_edges() both empty, and that trio of answers together is what says "not asked", not "nothing found".

A FemMesh::from_mesh() body is the exception, and the opposite case. A bare mesh carries no topology to say whether it ought to close, so its census always runs over the welded triangles: an open one lists its cracks here with FemMesh::watertight() false, a closed one reports FemMesh::watertight() true with no topology consulted at all, and an empty census there really does mean "nothing found".

FemMesh::folded_edges()

Result<std::vector<std::array<std::uint32_t, 3>>> folded_edges() const

Every fold, as FemMesh::open_edges() reports a crack: a directed mesh edge used by more than one triangle, once.

A body can be folded without being open — a solid no thicker than a line leaves no hole for an open edge to find — and the closure census's own known-bad bodies are folds rather than open cracks. A caller that checks FemMesh::open_edges() alone calls such a body sound. Empty under the same rule as FemMesh::open_edges().

FemMesh::crossings()

Result<std::vector<std::array<std::uint32_t, 2>>> crossings() const

Every pair of triangles that pass through each other, as (a, b) with a < b, sorted: an edge of one through the other's interior. Their B-rep faces are FemMesh::triangle_face() at a and b.

Computed for every body, open or closed. FemMesh::watertight(), FemMesh::open_edges() and FemMesh::folded_edges() are topological and cannot see a surface passing through itself; a mesh is ready for a volume mesher when it is FemMesh::watertight() and this is empty — Gmsh refuses one that crosses. A copy.

FemMesh::watertight()

bool watertight() const

The welded mesh closes — every directed mesh edge paired with its reverse and none used twice — and, for a B-rep body, so does the topology behind it. false for every B-rep body whose topology is not closed, whose mesh is then not asked about; read FemMesh::open_edges() for what an empty census beside a false here does and does not mean.

A FemMesh::from_mesh() body has no topology to ask of, so this says only that its triangles close: a closed render mesh reports true with nothing exact behind it at all.

Ready for a volume mesher means this and an empty FemMesh::crossings(): this census is topological and cannot see a surface passing through itself.

FemMesh::from_mesh()

bool from_mesh() const

This came from the scene's own mesh rather than from a B-rep: one face, every node on face 0, no edges and no vertices.

It is also which space the mesh is in. A B-rep body's FEM mesh is in the file's own units and axes, whatever Convention the scene was opened with, because it is taken off the B-rep — and a B-rep is in the file's own space for the reason Brep gives. A node with no B-rep falls back to the scene's mesh, which is converted, so that one comes back in the scene's convention, wound counter-clockwise about the outward normal even where the convention winds the other way.

And it is which contract the census is reporting under: read FemMesh::open_edges().

FemMesh::min_angle()

double min_angle() const

The smallest interior angle of any triangle, in degrees. There is always one: a body that meshed to no triangles is a refusal, not a mesh.

FemMesh::worst_triangle()

std::uint32_t worst_triangle() const

The triangle with that angle, as an index into FemMesh::triangles().

FemMesh::longest_edge()

double longest_edge() const

The longest triangle edge, placed. The figure to check against Node::fem_mesh()'s max_size, and the only one that says what the mesh actually is. max_size bounds the boundary segments and merely targets the interior: measured at 1.03 x max_size on a face whose parameters run unevenly, where a full-size boundary piece met a much shorter one left by halving. One small enough beside the body to reach the mesher's own piece and station ceilings is not honoured at all.

FemMesh::msh_text()

Result<std::string> msh_text() const

The mesh as Gmsh 4.1 ASCII .msh text: an entity per B-rep vertex, edge and face, a volume where the body closes, and a physical group naming each.

Gmsh reads the file as written — measured with Gmsh 4.15: node and triangle counts equal to this mesh's, and a physical group per entity. To fill a closed body with tetrahedra, give Gmsh a volume of its own over the file's surfaces. In its Python API: gmsh.open(path), then faces = [t for _, t in gmsh.model.getBoundary([(3, 1)], oriented=False)], gmsh.model.geo.addVolume([gmsh.model.geo.addSurfaceLoop(faces)]), gmsh.model.geo.synchronize() and gmsh.model.mesh.generate(3). A plain generate(3) leaves a volume read from any .msh file empty — Gmsh's own saved meshes too — and gmsh.model.mesh.createGeometry() re-meshes the surfaces rather than keeping these triangles.

Prefer Gmsh's HXT algorithm: gmsh.option.setNumber("Mesh.Algorithm3D", 10) before generate(3). On these surfaces the default left tetrahedra of exactly zero volume (four in a 100 mm bar), which no solver can use, and HXT's worst element came out far better on curved bodies (a minimum SICN of 0.1 to 0.3 where the default's was 0.001). Then name the tetrahedra before saving: HXT fills the new volume rather than the file's own, and gmsh.write keeps only elements in a physical group, so without this step every tetrahedron is dropped from the file without a word — filled = [t for _, t in gmsh.model.getEntities(3) if len(gmsh.model.mesh.getElementsByType(4, t)[0])], gmsh.model.removePhysicalGroups(gmsh.model.getPhysicalGroups(3)) and gmsh.model.addPhysicalGroup(3, filled, name="body"). The saved file also lists a few interior nodes no element uses — points Gmsh's refinement tried and gave up on — so a solver reading it should keep only the tetrahedra's own nodes: each unused one is an unconstrained unknown.

On this side of the ABI the library's text is borrowed — a slot on this handle, replaced by the next call on it and gone when the mesh is freed. The wrapper copies it out on the way, so what a caller holds is a string of its own. The kernel library's own msh_text is the other way round: an owned string, released by the wrapper (cadaclysm_blacksmith_string_free at the ABI), two asks giving two independent texts. A reader porting one side's reasoning onto the other leaks or double-frees.

No unlicensed notice is printed here. Node::fem_mesh() gave it once when the mesh was built, and this ABI deliberately does not repeat it on either .msh call — where the kernel library notices on both of its writers and not on its builder. Each matches its own siblings, so moving the call to look like the other side breaks a convention.

It returns an Error in its Result for a mesh the writer refuses, naming the field it cannot honour, and for a freed handle.

A freed handle traps here rather than returning an Error. This call, save_msh and every array accessor check the handle first, and on a freed one that check reaches CADACLYSM_BAD_ACCESS (FemMesh::msh_text: the FEM mesh is freed) and does not return — in both modes, not only under CADACLYSM_CHECKED, because what it guards is a pointer about to be handed to C rather than a borrowed view's owner. Measured. So the Result above carries the writer's refusal and nothing else; a freed handle is a bug in the caller, as it is for Meshlets.

FemMesh::save_msh()

Result<void> save_msh(const std::string& path) const

FemMesh::msh_text() written to a path by the library itself: the same bytes from the same writer, straight to the file rather than through the borrowed slot, so a text asked of this handle on another thread cannot be freed under the write. It returns an Error in its Result for a mesh the writer refuses or a file it cannot write, naming the path. No notice here either; see FemMesh::msh_text().

FemMesh::free()

void free() noexcept

Give the mesh back, and with it every view taken from it. Idempotent; the collector or the scope does it otherwise.

FemMesh::freed()

bool freed() const noexcept

Whether FemMesh::free() has run.

Surfaces and Face

class Surfaces

A node's faces as exact surfaces and trims: what Node::surfaces() returns. Iterate it for Face values. In the file's own frame; Scene::surface_matrix() brings it into the scene's.

Surfaces::faces()

const std::vector<Face>& faces() const

The faces, one per trimmed face of the body.

Face

struct Face
std::uint32_t kind
std::array<float, 3> origin
std::array<float, 4> domain
std::array<float, 4> scalars
std::vector<Span<const float>> loops
Span<const float> nurbs

One trimmed face. kind is the surface: 0 plane, 1 cylinder, 2 cone, 3 sphere, 4 torus, 5 revolution, 6 extrusion, 7 NURBS, 8 sum. origin, ax, ay, az are its frame, scalars its kind-dependent sizes (radius, angle…) and domain its (u min, v min, u max, v max). loops holds the trim loops as (u, v) points, each closing implicitly; profile, profile2 and nurbs carry what a swept or NURBS surface needs. reversed flips the normal; transposed swaps u and v. The C header's CadaclysmFace is the full description.

Bounds

struct Bounds

An axis-aligned box: what Node::bounds() and Scene::bounds() return. All zeros means "nothing here".

Bounds::min

std::array<float, 3> min

The low corner.

Bounds::max

std::array<float, 3> max

The high corner.

Bounds::is_empty()

bool is_empty() const noexcept

Whether this is the all-zero box that stands for nothing.

Bounds::size()

std::array<float, 3> size() const noexcept

The extent along each axis.

Bounds::centre()

std::array<float, 3> centre() const noexcept

The midpoint.

Attribute

struct Attribute

One thing the file said about a node: what Node::attributes() lists.

Attribute::name

std::string name

What the file called it.

Attribute::kind

ValueKind kind

Which kind of value it holds — a ValueKind. Lets a caller tell a reference from prose, or total the numbers.

Attribute::value

std::variant<std::monostate, std::string, std::int64_t, double, bool> value

The value, in the language's own type where it has one for the kind.

A std::variant: std::monostate for none, std::string for text (lists and references too), std::int64_t, double or bool; read it with std::get_if.

Attribute::text()

std::string text() const

The value rendered for display, identically in every wrapper: true/false, reals in their shortest exact form, lists as [a, b, c].

Convention

enum class Convention : std::uint32_t

The coordinate space to open a file into. The library converts on the way out, so a caller names the space it draws in and reads geometry already in it — nothing to rotate or scale afterwards.

Convention

enum class Convention : std::uint32_t
native
unreal
unity
y_up
blender

The presets: NATIVE keeps the file's own axes and units; UNREAL is Z up, left-handed, centimetres; UNITY Y up, left-handed, metres; Y_UP Y up, right-handed, metres (glTF, three.js, most real-time engines); BLENDER Z up, right-handed, metres.

cadaclysm::FILE_UNITS

inline constexpr std::uint32_t FILE_UNITS = 0x100

Combine with a preset to keep its axes but the file's own units.

cadaclysm::UV_WORLD

inline constexpr std::uint32_t UV_WORLD = 0x200

Combine with a preset to ask for world-scale texture coordinates in Mesh::uvs(). Off by default: eight bytes a vertex nobody asked for.

cadaclysm::parse_convention()

Result<std::uint32_t> parse_convention(std::string_view text)

A convention from a name a user typed: unreal, or unreal+file-units. An unknown name returns an Error in its Result listing the accepted ones, rather than silently reading as native.

ValueKind

enum class ValueKind : std::uint32_t

Which kind of value an Attribute holds.

ValueKind

enum class ValueKind : std::uint32_t
none
text
integer
real
boolean
list
reference

TEXT, INTEGER, REAL, BOOLEAN; LIST (the elements rendered as [a, b, c]); REFERENCE (another entity, by the id the file gave it, so it can be followed rather than shown as prose); NONE for an attribute that had no value.

Manifold

struct Manifold

Whether a body's faces make a manifold — every edge bordered by one face or two, the faces round every vertex one fan — told from its topology rather than a mesh: what a brep's manifold and the kernel's Solid::manifold() return. Orientation is not asked. The topology is the file's: faces that name no shared edge (IGES, each surface its own sheet; an IFC face written as one polygon) read as open however well they meet in space.

Manifold::faces

std::uint32_t faces

How many faces.

Manifold::edges

std::uint32_t edges

How many distinct edges: one shared by two faces counts once.

Manifold::vertices

std::uint32_t vertices

How many distinct vertices.

Manifold::boundary_edges

std::uint32_t boundary_edges

Edges only one face borders: a sheet's rim, a hole in a shell.

Manifold::non_manifold_edges

std::uint32_t non_manifold_edges

Edges three or more faces border: a fin, or two solids meeting along a line.

Manifold::non_manifold_vertices

std::uint32_t non_manifold_vertices

Vertices whose faces make more than one fan: two solids touching at a corner.

Manifold::is_manifold

bool is_manifold

No non-manifold edge or vertex: a manifold, possibly with a boundary.

Manifold::is_closed

bool is_closed

A manifold with no boundary edge either: it encloses a solid.

SewReport

struct SewReport

What Brep::sewed() did, as plain data: the edges it joined, the free edges before and after, how many groups of free edges it left free, the widest gap it sewed, and the two tolerances it used.

SewReport::pairs

std::uint32_t pairs

How many pairs of free edges it joined into one shared edge.

SewReport::free_before

std::uint32_t free_before

Free edges (those only one face borders) before sewing.

SewReport::free_after

std::uint32_t free_after

Free edges after sewing.

SewReport::refused

std::uint32_t refused

How many groups of free edges between the same two vertices it left free: too far apart for the tolerance, or more than two.

SewReport::worst_gap

double worst_gap

The widest gap between two curves it sewed.

SewReport::tolerance

double tolerance

How close two curves had to lie, as used: by default vertex.

SewReport::vertex

double vertex

How close two edges' ends had to coincide, as used: by default the rounding Brep::manifold() merges vertices at.

Errors

struct Error

A call into the library failed; the message is the library's own reason. One type for every reader failure.

Result and Error C++ only

class [[nodiscard]] Result
bool ok() const noexcept
const Error& error() const
T& value() &
const T& value() const&
T value_or(U&& fallback) const&
Result<std::invoke_result_t<F, const T&>> map(F&& f) const&
std::invoke_result_t<F, const T&> and_then(F&& f) const&
struct Error
std::string message
Origin origin
enum class Origin

Every call that can fail returns a Result<T> (result.hpp): a value or an Error, the library's own message and which library said it. Nothing throws, and the headers build with exceptions off. Test it (if (!r)), then *r or r->; CADACLYSM_TRY(var, expr) declares var or returns the error from the enclosing function, and CADACLYSM_TRY_VOID(expr) does the same for a Result<void>. map and and_then chain without the macro. Error.origin says which library answered: Origin::reader, Origin::kernel, or Origin::view for every call of cadaclysm/view.hpp — the view library's own message, or the wrapper's refusal.

A misuse — value() on an error, a read through a stale view, a call on a closed or moved-from owner — calls CADACLYSM_BAD_ACCESS(message), which prints and aborts unless it is defined before the first include; it must not return, and a longjmp out of it is only safe from the plain view, node and placement accessors. The stale-view checks are checked mode's: on unless NDEBUG, or set CADACLYSM_CHECKED to 0 or 1. Every translation unit must agree on both, and on C++17 or C++20 (a Span or a std::span).

Kernel · cadaclysm_blacksmith

Building solids

Frames and axes

A frame is twelve numbers: an origin, then the x, y and z axes, each three numbers (0,0,0, 1,0,0, 0,1,0, 0,0,1 is the world). A profile is drawn in its frame's x/y and extruded along its z. An axis is six numbers: a point and a direction, which need not be unit. Workplane::xy() and friends start on the three world planes; Solid::face_frame() gives the frame on a face. A Frame builds one for you — Frame::xy() at any origin, Frame::at() from a point and a normal — and checks that its axes are square and right-handed, which a bare twelve numbers are not.

Frames are const Frame&, always checked: Frame::xy, Frame::at, Frame::make and Frame::of build them. An axis is an AxisLine, {{px, py, pz}, {dx, dy, dz}}.

Module functions

The kernel's own library, licence, STEP and .brep writers. It is a separate shared library (cadaclysm_blacksmith) from the reader, with its own licence call; one licence file serves both.

cadaclysm::blacksmith::write_step()

Result<void> write_step(const std::string& path, const Solids& solids, const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre)

Write several solids as one STEP file, each its own body. unit is mm, m or in. schema is left out for AP203 (built in — no file needed; AP242 when any solid, face or edge is coloured, since AP203 has no colour entities), the name of another built-in schema such as AP242's AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF (AP214's AUTOMOTIVE_DESIGN cannot carry the writer's mechanical_context), or a custom EXPRESS schema: a path to its .exp or its text. Colours from coloured and edges_coloured are written as STEP styling wherever the schema has it, a solid's read back as the node's colour; a named schema without it writes the solids bare.

cadaclysm::blacksmith::write_step_text()

Result<std::string> write_step_text(const Solids& solids, const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre)

The same STEP file as text, for a caller that stores or sends it rather than writing a file.

write_step_assembly

Write an assembly as one STEP file: parts names each solid, in its own coordinates, and each is written once as its own product; placements lists (name, frame) pairs, each an occurrence of that part at that frame (right-handed and orthonormal), all under one root product. A reader tessellates a part once however many times it is placed, and shows each placement under its part's name. schema and unit as cadaclysm::blacksmith::write_step().

Not in this wrapper: build an Assembly and write it with step or step_text.

write_step_assembly_text

The same assembly as STEP text.

Not in this wrapper: build an Assembly and write it with step or step_text.

cadaclysm::blacksmith::write_brep()

Result<void> write_brep(const std::string& path, const Solids& solids)

Write several solids as one OCCT .brep file, each its own solid under one compound (a single solid is the file's root): the exact surfaces and curves, with a curve in each face's own parameters for every edge, so Open CASCADE's BRepTools::Read gives a shape its BRepCheck_Analyzer finds valid. No unit is declared — a .brep carries none — so the numbers written are the numbers held. The library writes the file itself.

cadaclysm::blacksmith::write_brep_text()

Result<std::string> write_brep_text(const Solids& solids)

The same .brep file as text.

cadaclysm::blacksmith::write_sat()

Result<void> write_sat(const std::string& path, const Solids& solids, Unit unit)

Write several solids as one ACIS SAT file, each its own body: planes, cylinders, cones, spheres and tori as their own records, splines and swept surfaces as exact NURBS, in the layout Rhino's own exporter writes. unit is mm, m or in and goes into the header as millimetres per unit. The library writes the file itself, so a refusal names it.

cadaclysm::blacksmith::write_sat_text()

Result<std::string> write_sat_text(const Solids& solids, Unit unit)

The same SAT file as text.

cadaclysm::blacksmith::write_svg()

Result<void> write_svg(const std::string& path, const Solids& solids, const SvgOptions& options)
Result<void> write_svg(const std::string& path, const Solids& solids, const Profiles& profiles, const SvgOptions& options)

Several solids and profiles' wireframe as one SVG, any mix and any order: a <g id="solid-<i>"> per solid then a <g id="profile-<i>"> per profile, each stroked in its own colour where it carries one — a solid's edge colour first, then its body colour (a face colour says nothing about a wireframe) — and the keywords' stroke= otherwise. The keywords are Scene::svg()'s own: view= (front, back, left, right, top, bottom, iso), az=/el= over it, up= (default z: neither a solid nor a profile carries a convention of its own to default it from), fov= (0, the default, is orthographic), size=, margin=, tolerance=, stroke= and background= ("#rgb", "#rrggbb" or a CSS colour name in every language, beside that language's own numeric form; background= std::nullopt for transparent), width= (the stroke's, in page units), edges=, curves=, isocurves=, polylines= (which line sets are drawn; edges by default), and silhouettes= (also draw where each solid's surfaces turn away from the eye — the outline a sphere has no edge for, on by default: a sphere's and a torus's only curves are the seams where their one face meets itself, which are not edges of the body and are never drawn, so without it those two draw a blank page. A line set in its own right, so silhouettes= with edges= off draws the outline alone).

A list of solids alone draws exactly as it always did. The SVG is the drawing itself: with path, writes it to the file and returns std::nullopt; without, returns the SVG text. Both lists empty returns an Error in its Result; so does anything in them that is neither a solid nor a profile, a refused option, or a failed write.

cadaclysm::blacksmith::default_schema()

Result<std::string> default_schema()

Where an ap203.exp file is found (CADACLYSM_SCHEMAS, else schemas/ in a parent). No longer needed to write STEP: the kernel's AP203 is built in.

cadaclysm::blacksmith::version()

std::string version()

The version of the kernel library actually loaded.

cadaclysm::blacksmith::build_date()

std::string build_date()

When the loaded kernel was built, YYYY-MM-DD.

cadaclysm::blacksmith::pair_freedom_count()

std::uint32_t pair_freedom_count(std::string_view kind)

How many freedoms a pair kind has, so a caller can size Assembly::pair()'s ranges. 0 for an unknown name, and for fully_constrained, which has none.

cadaclysm::blacksmith::license()

Result<void> license(const std::string& text_or_path)

Load a licence into the kernel — the text, or a file's path. The reader has its own call; the same file works for both.

cadaclysm::blacksmith::license_info()

std::string license_info()

One line about the kernel's licence, or unlicensed. Never std::nullopt.

cadaclysm::blacksmith::license_notice_count()

std::uint64_t license_notice_count()

How many unlicensed notices the kernel has printed to stderr in this process.

cadaclysm::blacksmith::set_option()

Result<void> set_option(const std::string& name, double value)

Set a kernel option, by its name, for the whole process from the next call on — every thread, every solid. The options are numbers under stable names, one call for all of them, so an option added later needs no new function:

boolean.mesh_budget (default 500000): the most triangles a boolean (join, cut, common, split) builds its trees from, its two solids' meshes together. Past it the boolean refuses with a tolerance error rather than running for minutes or hours. A whole number, at least 1, or infinity for no limit.

boolean.finest_tolerance (default 1e-8): the finest tolerance a boolean takes, as a share of how far its two solids reach; under it the boolean refuses before meshing anything. From 0 (no floor) to below 1.

profile_boolean.point_budget (default 8000000): the most points a profile boolean follows its two profiles' arcs and splines with. Past it the boolean refuses with a tolerance error; 8 million is about 128 MB. A whole number, at least 1, or infinity for no limit.

measure.default_accuracy (default 1e-7): the relative accuracy Solid::mass() refines to when it is asked for 0, the default. Above 0 and below 1.

A name that is not an option, or a value the option does not take, returns an Error in its Result and changes nothing, saying which and what it takes.

cadaclysm::blacksmith::option()

Result<double> option(const std::string& name)

A kernel option's value, by its name: what cadaclysm::blacksmith::set_option() last set, else its default (see cadaclysm::blacksmith::set_option() for the names). A name that is not an option returns an Error in its Result, listing the ones there are.

cadaclysm::blacksmith::brep_layout_id()

std::string brep_layout_id()

How the loaded kernel lays a brep out in memory: its compiler, target and source. Solid::from_node() works only where this equals the reader's Brep::layout_id() — the two libraries from the same release.

library_path

Where the kernel library was found: CADACLYSM_BLACKSMITH_LIBRARY first, then as the reader's.

Linked at build time: the OS loader finds the libraries; see Install.

cadaclysm::blacksmith::boxes_apart()

Result<bool> boxes_apart(const Box& a, const Frame& frame_a, const Box& b, const Frame& frame_b)

True only if box a at frame_a and box b at frame_b cannot touch — a separating axis among the boxes' face normals and edge-direction crosses; touching is not apart. Sharper than moving each box (Box::moved()) and asking Box::overlaps(), which boxes the *result* on the world axes again and so can call two oriented boxes overlapping when they are not (two unit boxes, the second turned 45 degrees about z and moved 2.3 along x: their world boxes overlap, but the boxes themselves do not touch). frame_a or frame_b not twelve finite numbers or not rigid returns an Error in its Result.

Tolerances C++ only

inline constexpr double DEFAULT_TOLERANCE = 0.05
inline constexpr double FILLET_TOLERANCE = 1e-6

The defaults behind every tolerance argument: DEFAULT_TOLERANCE (0.05) for booleans, meshes and bounds, FILLET_TOLERANCE (1e-6) for fillet, chamfer, refillet and shell — to pass one explicitly before a later argument.

Unit C++ only

enum class Unit : std::uint32_t
metre
millimetre
inch

The unit a STEP file is written in; millimetres by default.

rgb C++ only

Result<Vec3> rgb(std::string_view colour)

{r, g, b} in 0..1 from "#rgb", "#rrggbb" or a CSS colour name.

packed C++ only

Result<std::uint32_t> packed(std::string_view colour)

blacksmith::packed(std::string_view): a colour packed 0xRRGGBB from "#rgb", "#rrggbb" or a CSS colour name — for a host that wants the number itself; cadaclysm::blacksmith::write_svg()'s stroke/background take the text directly, as a cadaclysm::SvgColour.

Profile

class Profile

A closed outline with holes, in its own x/y — what gets extruded, revolved, lofted or swept. Immutable: every method returns a new one.

Its loops must be simple: an outline that crosses or touches itself (a figure-eight, a vertex landing on another side), a hole that runs into the boundary, or two holes that overlap are refused by every call that builds a face or a closed solid, naming the loops — extrude: hole 0 crosses the boundary. The open calls (extrude_open, revolve_open, sweep_open, loft_open) build sheets, and take such a profile as it is.

Profile::rect()

static Result<Profile> rect(double w, double h)

A w × h rectangle centred on the origin.

Profile::circle()

static Result<Profile> circle(double r)

A circle of radius r about the origin.

Profile::slot()

static Result<Profile> slot(const Vec2& centre, double length, double r)

A slot (stadium) length long overall, with end radius r, centred on centre and running along x. length must exceed 2 * r.

Profile::polygon()

static Result<Profile> polygon(const std::vector<Vec2>& points)

A closed polygon through the points, in order, its side back to the first point a segment of its own. At least three points.

Profile::regular_polygon()

static Result<Profile> regular_polygon(const Vec2& centre, double radius, std::uint32_t sides, double angle = 0.0)

A regular polygon of sides sides (at least 3) on the circle of radius about centre, its first corner at angle radians from the sketch's x axis (0 by default), the rest counter-clockwise.

Profile::star()

static Result<Profile> star(const Vec2& centre, double outer, double inner, std::uint32_t points, double angle = 0.0)

A star of points tips (at least 3) on the circle of outer about centre, its inner corners on the circle of inner (positive, under outer), alternating: the first tip at angle radians from the sketch's x axis (0 by default), the rest counter-clockwise, each inner corner half a step on from the tip before it. 2 * points straight sides; an inner not under outer returns an Error in its Result.

Profile::text()

static Result<std::vector<Profile>> text(const std::string& text, double size = 10.0, const std::string& font = "", const std::string& halign = "left", const std::string& valign = "baseline", double spacing = 1.0, const std::string& direction = "ltr", const std::optional<std::vector<std::uint8_t>>& font_bytes = std::nullopt)

text set in a font, one profile per closed shape — a letter with its counters as holes (o one, 8 two; i is two profiles) — on the sketch plane, the baseline along x from the origin, each outline counter-clockwise and its holes clockwise, a curved side the font's own cubic Bezier kept exactly: an extruded O has curved walls, and writes to STEP as splines. size (10 by default) is roughly the height of a capital. font is a family, optionally with a style ("Liberation Sans:style=Bold"), a font file's path, or empty for the bundled Liberation Sans Regular — which also serves when the family is not found, so text never comes back empty; font_bytes a font file's bytes, used instead of font when given (a page or a phone fetches its own font). halign is left (default), center or right; valign baseline (default), bottom, center or top; spacing (1 by default) multiplies the gap between glyphs; direction ltr (default) or rtl. Glyphs are laid out one after another by their advance widths — no shaping, so Latin sets as expected and scripts that need ligatures or reordering do not. Empty text is an empty list. A size or spacing not positive and finite, an alignment or direction not one of those words, or font bytes that are not a font returns an Error in its Result.

Profile::spline()

static Result<Profile> spline(const std::vector<Vec2>& points, std::uint32_t degree = 3, const std::optional<std::vector<double>>& weights = std::nullopt, bool closed = false)

A spline of degree (3 by default) through the control polygon points, weights one per point or std::nullopt. Open, it starts on the first point and ends on the last: an open chain, for Solid::extrude_open() or Profile::chain(). Closed, it is periodic — smooth through its own start, no corner there — and a closed profile. The degree is lowered to fit the points; a degree of zero, too few points (two open, three closed) or a weight not positive returns an Error in its Result.

Profile::path()

static Path path(const Vec2& start)

Start drawing an outline segment by segment at start; see Path.

Profile::parabola()

static Path parabola(const Vec2& vertex, const Vec2& axis, double focal, double from, double to)

Start drawing on the arc of the parabola with vertex, axis direction axis and focal length focal, over the across-axis coordinates from..to: the path begins at the arc's first point and holds the arc as one conic segment — a reflector from rim to rim; parabola((0, 0), (0, 1), 20, -50, 50) is a dish 100 wide opening up. A zero axis, a focal length not positive and finite, or from not under to returns an Error in its Result.

Profile::chain()

static Result<Profile> chain(const Profiles& pieces, double tolerance = 1e-6)

Open profiles — paths ended open — joined end to end into one: the forge's merge. They may come in any order and either way round: each next piece is the first of the rest with an end within tolerance (1e-6 by default) of either end of the chain so far, reversed where that makes it meet. Every segment is kept exactly — a line a line, an arc an arc, a spline the same spline. Closed where the chain's two ends meet, otherwise an open chain. A piece that is empty, has holes, is closed on its own or meets none of the others returns an Error in its Result naming it by its index.

Profile::from_loops()

static Result<Profile> from_loops(const Profiles& loops)

Closed loops, in any order, as one profile: the loop enclosing the most area is the boundary and every other a hole in it, in the order given — a sketch's rectangle and the circles drawn inside it. Each loop is a closed profile with no holes of its own, wound either way; one that closes within rounding is closed exactly. A loop that is open, empty or encloses no area, loops that cross or touch, a hole outside the boundary, or one inside another hole (an island) returns an Error in its Result, naming the loops by their index.

Profile::close_loop()

Result<Profile> close_loop() const

This profile closed — the forge's sketch "close": where its last segment stops short of its start (a path ended open), a straight segment back to it; where it already comes back within 1e-9 of its extent, its last segment made to land on the start exactly. A closed profile comes back as it is, and holes are closed the same way.

Profile::pieces()

Result<std::vector<Profile>> pieces(const Profiles& cutters, double tolerance = 1e-6) const

This curve cut where the cutters cross, touch or run along it — the sketch trim's pieces: in order along the curve from its start, each an open profile of portions of this one's own segments (a line's stretch a line, an arc's an arc, a spline's the same spline over part of its domain, nothing refitted). One piece, this curve, where nothing cuts it; a closed curve's piece round its start is one piece. Cuts closer than tolerance to each other fold onto one. A curve with no segments returns an Error in its Result.

Profile::trim()

Result<std::vector<Profile>> trim(const Profiles& cutters, std::uint32_t piece, double tolerance = 1e-6) const

This curve with piece piece of Profile::pieces() taken away — the sketch trim, the forge's: what is left, as open profiles. One for a closed curve (its other pieces run together from where the removed one ended), the stretches before and after for an open one, none where the piece was the whole curve. A piece the curve does not have returns an Error in its Result.

Profile::with_hole()

Result<Profile> with_hole(const Profile& hole) const

This outline with hole cut out of it.

Profile::hits()

Result<std::vector<Hit>> hits(const Profile& other, double tolerance = HIT_TOLERANCE) const

Where this outline's curves cross, touch or run along other's, both read in one plane, as Hit values ordered along this outline. Points closer than tolerance merge; two curves within tolerance of each other for longer than it are one run when they part only where one of them ends or the stretch is flat — one curve following the other, offset within tolerance or tilted by under about half of it, even where it leaves mid-both; a tangency or a shallow crossing is one point. A loop that stops short of its start is an open chain, its closing side not tested.

Profile::common()

Result<std::vector<Profile>> common(const Profile& other, double tolerance = HIT_TOLERANCE) const

The region this outline and other share, both read in one plane, as a list of zero or more profiles — each boundary counter-clockwise, each hole clockwise, arcs and splines kept exact. An arc kept from an input can still come out split at that input's own seam point (two circles' lens is four arcs, one pair per circle) — exact, not an approximation. Two loops of a result may touch at a point (two holes whose corners meet, one from each input): a right point set that the verbs needing simple loops — Solid::extrude(), a boolean taking it as an input — refuse. Both must be closed and simple; no shared area is an empty list. A tolerance not positive and finite, an outline open or crossing itself, a tolerance too fine for these outlines (following their arcs and splines to a tenth of it would take more than 8 million points, about 128 MB), and, as a defect rather than an outcome, a result that fails to close, returns an Error in its Result.

Profile::translate()

Result<Profile> translate(double dx, double dy) const

This outline moved by (dx, dy).

Profile::coloured()

Result<Profile> coloured(const Vec3& rgb_value) const
Result<Profile> coloured(std::string_view colour) const

A new outline coloured (r, g, b), each 0..1, "#rgb", "#rrggbb" or a CSS colour name ("tomato", "RebeccaPurple"): how it is drawn. The verbs that make a profile from one carry it (translate, round, with_hole keeps the outer's, chain and from_loops the first's); a solid made from it takes nothing — colour a solid with Solid::coloured().

Takes a Vec3, or, as a std::string_view overload, colour text through blacksmith::rgb().

Profile::colour()

Result<std::optional<Vec3>> colour() const

The outline's colour as (r, g, b), or none.

Profile::round()

Result<Profile> round(double radius, const std::optional<std::vector<std::uint32_t>>& corners = std::nullopt, bool as_open = false) const

This outline with its corners rounded by radius: where two straight segments meet, both are cut back and an exact arc tangent to both goes between them; a corner next to an arc or a spline is left as it is. With no corners every such corner is rounded, the holes' too; a list picks corners of the outline — corner k is where segment k ends. open reads the profile as an open chain whose two ends stay square. A radius that does not fit returns an Error in its Result naming the corner.

Profile::to_arcs()

Result<Profile> to_arcs(double tolerance = 0.01) const

This profile with every free-form side (a Bezier, a conic, a NURBS) replaced by lines and arcs within tolerance of it — what a G-code writer can say. Lines and arcs pass through as they are, every loop closes on itself exactly, holes are converted the same way. tolerance (0.01 by default) not positive and finite, or a spline that does not evaluate, returns an Error in its Result.

Profile::offset()

Result<std::vector<Profile>> offset(double distance, double tolerance = 0.01) const

This profile grown by distance (shrunk where it is negative), as zero or more profiles, all lines and arcs: outward corners rounded by |distance|, holes moving against the boundary (a hole shrinks as the profile grows). Free-form sides are fitted to tolerance first (Profile::to_arcs()). A shrink that eats the whole profile is an empty list. A distance not finite, a tolerance (0.01 by default) not positive and finite, a loop with no sides, or loops that cross or touch, returns an Error in its Result.

Profile::polylines()

Result<ProfilePolylines> polylines(double tolerance = DEFAULT_TOLERANCE) const

The outline, then each hole, as polylines at z = 0 within tolerance of its arcs and splines — what a viewer draws it with. A closed loop repeats its first point at the end; an open chain (a profile ended open) stays open. Views, like Solid::mesh() — copies in Node, Godot and Python.

Returns a view into the profile's cache: asking again at another tolerance makes it stale (a bad access in checked mode).

Profile.show

Draw the outline and holes with the viewer in use, from the top by default. Keywords as Solid::show(); edges= is ignored, the lines being the whole picture. With the GPU library (import cadaclysm.view.wgpu) the drawing replaces what one shared window shows and stays live — at the REPL, in IPython and in Jupyter by themselves, in a script until it exits; cadaclysm.view.View draws several items or several windows.

Comes with the viewers follow-up.

Profile.view

Orbit the outline with the viewer in use; returns the camera where it was left — a dict of eye, target, up, orthographic, azimuth and elevation — and pick, what was last clicked (item, node, face) or None. A fresh window, run until closed, with the GPU library.

Comes with the viewers follow-up.

Profile::svg()

Result<void> svg(const std::string& path, const SvgOptions& options = SvgOptions{SvgView::top}) const

This profile's own loops as SVG: a line a line, an arc an arc, a spline the NURBS' own Béziers — exact curves, nothing flattened to the polyline Profile::polylines() samples. The keywords are cadaclysm::blacksmith::write_svg()'s own, except view= defaults to top here rather than iso: a profile lies in z = 0, so its own plane is already the page and it reads flat and true. In the languages whose options are a struct (view already iso, not nullable), that default holds only when no options value is passed at all — pass one for anything else and its own view applies. With path, writes the file and returns std::nullopt; without, returns the SVG text. returns an Error in its Result on a refused option or a failed write.

Path

class Path

An outline drawn a segment at a time — lines, arcs, Béziers, NURBS — then closed into a Profile. Ending it consumes the builder.

Path::line_to()

Path& line_to(double x, double y) &

A straight segment to (x, y).

Path::arc_to()

Path& arc_to(double x, double y, const Vec2& centre, bool ccw = true) &

A circular arc to (x, y) about centre, counter-clockwise unless ccw is false.

Path::bezier_to()

Path& bezier_to(const Vec2& c1, const Vec2& c2, const Vec2& to) &

A cubic Bézier through control points c1, c2 to to.

Path::conic_to()

Path& conic_to(const Vec2& to, const Vec2& control, double weight) &

A conic arc to (x, y) through the control point control with middle weight: under 1 an elliptical arc, 1 a parabola, over 1 a hyperbola — the rational quadratic Bézier, kept exact. A weight not positive and finite, an end on the current point, or a control point on the chord returns an Error in its Result.

Path::parabola_to()

Path& parabola_to(const Vec2& to, const Vec2& control) &

A parabolic arc to (x, y) whose end tangents meet at control: Path::conic_to() with weight 1.

Path::hyperbola_to()

Path& hyperbola_to(const Vec2& to, const Vec2& control, double weight) &

A hyperbolic arc to (x, y) through control with middle weight over 1; a weight of 1 or under returns an Error in its Result.

Path::parabola_by_vertex()

Path& parabola_by_vertex(const Vec2& to, const Vec2& vertex) &

The parabolic arc to (x, y) whose vertex is vertex: the axis and focal length are solved from the two ends. A vertex on the chord, or one no parabola through both ends has (the vertex must be the arc's extreme point), returns an Error in its Result.

Path::parabola_by_focus()

Path& parabola_by_focus(const Vec2& to, const Vec2& focus) &

The parabolic arc to (x, y) whose focus is focus: of the two parabolas through the ends with that focus, the one whose vertex lies between the ends' projections, then the one whose arc cups the focus (the focus between the arc and its chord), then the more symmetric; with the focus beyond the chord that is the arch over the ends, not the shallow dish — draw that one with Profile::parabola(). A focus on the chord returns an Error in its Result.

Path::nurbs_to()

Path& nurbs_to(const std::vector<Vec2>& control, const std::vector<double>& knots, std::uint32_t degree, const std::optional<std::vector<double>>& weights = std::nullopt) &

A NURBS segment: control is every control point after the current one, the endpoint last; knots the full knot vector; weights one per control point including the current one, or std::nullopt for a non-rational curve.

Path::end()

Result<Profile> end()

Close the outline back to its start and return the Profile.

Path::end_open()

Result<Profile> end_open()

The path as it stands, not closed: an open chain for Solid::extrude_open(), Solid::sweep_open() or Solid::loft_open().

err C++ only

const Error* err() const noexcept
const Error* err() const noexcept
const Error* err() const noexcept

The builders latch: a step that fails keeps its error and every later step is skipped, so a chain reads without a check per step — Profile::path({0, 0}).line_to(10, 0).line_to(10, 5).end(). end(), end_open() or solid() return the error; err() shows it early, a null pointer while all is well. Path(start) is the same as Profile::path().

SweepPath

class SweepPath

The 3D path a profile is carried along by Solid::sweep(): lines and circular arcs. Sweeping only borrows it, so one path can be swept many times; close it when done.

SweepPath::at()

static SweepPath at(const Vec3& point)

Start a path at a 3D point.

SweepPath::along()

static SweepPath along(const Profile& curve, const Frame& frame, double tolerance = DEFAULT_TOLERANCE, bool leave_open = true)

The path a 2D chain (usually from Path::end_open()) draws on frame: a line a straight piece, an arc a circular one, a Bézier or spline fitted with biarcs — arcs tangent to each other and to the curve — within tolerance, so the path is tangent throughout and the sweep exact along it. open false closes the path back to its start.

SweepPath::line_to()

SweepPath& line_to(const Vec3& point) &

A straight piece to a 3D point.

SweepPath::arc()

SweepPath& arc(const Vec3& centre, const Vec3& axis, double angle) &

Turn angle radians (in (0, 2π]) about the axis through centre along axis.

SweepPath::close()

void close() noexcept

Free the path.

Slant

struct Slant

A plane a Solid::extrude_between() starts or ends on, read as a height over the sketch plane at each point: at + grad · (x, y). Flat for an ordinary cap; sloped for a mitre.

Slant::at

double at

The height at the sketch origin.

Slant::grad

Vec2 grad

The slope in x and y.

Slant::flat()

static Slant flat(double height)

A flat plane at height at.

Slant::of_plane()

static Result<Slant> of_plane(const Frame& frame, const Vec3& point, const Vec3& normal)

The plane through point square to normal, as heights over frame. A plane that contains the extrusion direction has no height and returns an Error in its Result.

Frame

class Frame

A frame built for you instead of twelve numbers typed out: an origin and three unit axes, square to each other and right-handed (z = x × y). It goes wherever a frame does. Immutable. The constructor takes the origin and the three axes, normalises them, and returns an Error in its Result when they are not square or not right-handed. Every way to a Frame also refuses an origin that is not three finite numbers, returning an Error in its Result.

Frame::xy()

static Result<Frame> xy(const Vec3& o = {0, 0, 0})

The world XY plane through origin: z up, as Workplane::xy().

Frame::xz()

static Result<Frame> xz(const Vec3& o = {0, 0, 0})

The world XZ plane through origin: x along X, y along Z, so z is -Y, as Workplane::xz().

Frame::yz()

static Result<Frame> yz(const Vec3& o = {0, 0, 0})

The world YZ plane through origin: x along Y, y along Z, so z is +X, as Workplane::yz().

Frame::at()

static Result<Frame> at(const Vec3& o, const Vec3& normal, const std::optional<Vec3>& x_hint = std::nullopt)

The plane through origin square to normal, which becomes the frame's z (it need not be unit). Its x axis is x laid onto that plane; with none, world X laid onto it, or world Y when the normal is within about 25° of X — the axes Solid::face_frame() gives a face facing normal. So a normal along +Z, -Y or +X gives exactly Frame::xy(), Frame::xz() or Frame::yz(). A zero normal, or an x along the normal, returns an Error in its Result.

Frame::of()

static Result<Frame> of(const std::array<double, 12>& raw)

Twelve numbers — what Solid::face_frame() and Workplane::frame() hand back — as a checked frame, to read its axes or move it.

Frame::midplane()

static Result<Frame> midplane(const Frame& a, const Frame& b)

The plane midway between the planes of frames a and b: for parallel planes the one halfway between, on a's axes; for planes that meet, the plane bisecting them through the line they meet on, its x along that line.

Frame::through()

static Result<Frame> through(const Vec3& p, const Vec3& q, const Vec3& r)

The plane through the points p, q and r: its origin p, its x towards q, its z the normal the three turn about counter-clockwise. Three points on one line returns an Error in its Result.

Frame::origin()

Vec3 origin() const noexcept
Vec3 x() const noexcept
Vec3 y() const noexcept
Vec3 z() const noexcept

The origin and the three axes, each three numbers.

Frame::translate()

Result<Frame> translate(double dx, double dy, double dz) const

This frame moved by (dx, dy, dz) in world coordinates.

Frame::offset()

Result<Frame> offset(double distance) const

This frame moved distance along its own z: Frame::xy() and offset() return Result<Frame>, so Frame::xy().and_then([](Frame f) { return f.offset(5); }) is the XY plane at z = 5.

Frame::make C++ only

static Result<Frame> make(const Vec3& o, const Vec3& xa, const Vec3& ya, const Vec3& za)
const std::array<double, 12>& raw() const noexcept

Python's Frame(origin, x, y, z), checked, and the twelve numbers a frame stands for.

Workplane

class Workplane

The fluent chain: a frame, the solid built so far, and the face last picked. A build step replaces the solid rather than adding to it — combine solids explicitly with Solid::join(). Every step returns the Workplane and keeps the first refused step's error instead of failing at once: the steps after it are skipped, and solid() returns that error.

Workplane::xy()

static Workplane xy()

Start on the XY plane at the origin (Z up). Workplane::xz() and Workplane::yz() start on the other two.

Workplane::xz()

static Workplane xz()

Start on the XZ plane.

Workplane::yz()

static Workplane yz()

Start on the YZ plane.

Workplane::on()

static Workplane on(const Frame& f)

Start on any frame (see Frames).

Workplane::from_solid()

static Workplane from_solid(const Solid& source)

Start from an existing solid, on the XY plane — the usual way to pick one of its faces and build on it.

Workplane::frame()

Frame frame() const

The current frame, 12 numbers.

Workplane::cuboid()

Workplane& cuboid(double x, double y, double z) &

A box x × y × z centred on the current frame's origin and aligned to its axes — on a picked face, half of it below the face; replaces the solid.

Workplane::cylinder()

Workplane& cylinder(double r, double height) &

A cylinder of radius r and height h standing on the current frame; replaces the solid.

Workplane::face()

Workplane& face(const Profile& profile) &

The planar sheet the profile bounds on this frame — see Solid::face(); replaces the solid.

Workplane::extrude()

Workplane& extrude(const Profile& profile, double height) &

The profile extruded height along the frame's z; replaces the solid.

Workplane::revolve()

Workplane& revolve(const Profile& profile, double angle) &

The profile revolved angle radians about the frame's y axis; replaces the solid.

Workplane::translate()

Workplane& translate(double dx, double dy, double dz) &

Slide the current solid. Keeps the face selection — a rigid move keeps every face's index.

Workplane::faces()

Workplane& faces(const Selector& selector) &

Pick a face of the current solid with a Selector.

Workplane::workplane()

Workplane& workplane() &

Move the frame onto the face last picked (outward normal as z), so the next step builds on it.

Workplane::solid()

Result<Solid> solid()

The solid built so far. On an empty chain it returns an Error in its Result.

Selector and Axis

class Selector

Which face to pick: the one furthest along an axis, furthest against it, the one facing a direction, or by index. Used by Workplane::faces() and Solid::select_face().

Selector::max()

static Selector (max)(Axis axis)

The face furthest along axis.

Selector::min()

static Selector (min)(Axis axis)

The face furthest against axis.

Selector::normal()

static Selector normal(const Vec3& direction)

The face whose outward normal is nearest direction (need not be unit).

Selector::index()

static Selector index(std::uint32_t i)

The face with this index.

Axis

enum class Axis : std::uint32_t
x
y
z

X, Y, Z: the axes Selector::max() and Selector::min() take.

Solid

class Solid

An exact B-rep solid (or an open sheet): planes, cylinders, cones, spheres, tori and NURBS, trimmed and joined, never approximated by triangles. Immutable — every operation returns a new one. Close it when done, or let the language's scope do it; see Lifetimes.

Primitives

Solid::cuboid()

static Result<Solid> cuboid(double x, double y, double z)

A box x × y × z, centred on the origin.

Solid::cylinder()

static Result<Solid> cylinder(double r, double h)

A cylinder of radius r, from z = 0 to h.

Solid::cone()

static Result<Solid> cone(double r, double h)

A cone of base radius r and height h, apex up.

Solid::sphere()

static Result<Solid> sphere(double r)

A sphere of radius r about the origin.

Solid::torus()

static Result<Solid> torus(double major, double minor)

A torus about the z axis: major to the tube's centre, minor the tube's radius. A tube wider than its ring (minor > major, a spindle) builds the solid it sweeps, the outer sheet with a pole at each end on the axis; minor == major is refused.

Solid::wedge()

static Result<Solid> wedge(double x, double y, double z, double top_x)

A box whose top face is top_x long instead of x: a ramp.

Solid::pyramid()

static Result<Solid> pyramid(std::uint32_t sides, double r, double h)

A pyramid of sides sides (at least 3): its base the regular polygon of radius r on z = 0 (r to a corner, the first on +x), its tip at height h.

From a profile

Solid::extrude()

static Result<Solid> extrude(const Profile& profile, const Frame& frame, double height)

The profile on frame, extruded height along the frame's z.

Solid::extrude_open()

static Result<Solid> extrude_open(const Profile& profile, const Frame& frame, double height)

The walls only, no caps: an open sheet. Takes an open Path::end_open() chain as well as a closed profile.

Solid::extrude_tapered()

static Result<Solid> extrude_tapered(const Profile& profile, const Frame& frame, double height, double taper)

Extrude with a draft: the walls lean out by taper radians as they rise (in, when negative). Every wall stays exact — a plane off a line, a cone off an arc.

Solid::extrude_open_tapered()

static Result<Solid> extrude_open_tapered(const Profile& profile, const Frame& frame, double height, double taper)

The tapered walls without caps.

Solid::extrude_between()

static Result<Solid> extrude_between(const Profile& profile, const Frame& frame, const Slant& bottom, const Slant& top)

Extrude between two planes rather than two heights: bottom and top are each a Slant (a bare number is a flat one). With both flat this is Solid::extrude(); with a slope it is the mitred end of a frame member. A top that comes down to or through the bottom returns an Error in its Result.

Solid::extrude_open_between()

static Result<Solid> extrude_open_between(const Profile& profile, const Frame& frame, const Slant& bottom, const Slant& top)

Solid::extrude_between() without the caps.

Solid::revolve()

static Result<Solid> revolve(const Profile& profile, const AxisLine& axis, double angle)

The profile swung angle radians about axis (a point and a direction — see Frames). The profile's x is read as the radius and its y as the height along the axis, so it must lie to one side of it.

Solid::revolve_open()

static Result<Solid> revolve_open(const Profile& profile, const AxisLine& axis, double angle)

The revolved surface of an open profile: a sheet.

Solid::revolve_in_plane()

static Result<Solid> revolve_in_plane(const Profile& profile, const Frame& frame, const Vec2& a, const Vec2& b, double angle)

The profile on frame swung angle radians about the axis through the sketch points a and b (each (x, y) on the frame) — the profile and its axis drawn together, as a sketch draws them, where Solid::revolve() reads the profile as (radius, height). The profile may lie on either side of the axis and touch it (a half-disc with its diameter on the axis turns into a ball), but not cross it. The sweep starts where the profile is drawn and turns right-handed about b - a, so a partial turn leaves one end of the solid over the profile itself.

Solid::revolve_open_in_plane()

static Result<Solid> revolve_open_in_plane(const Profile& profile, const Frame& frame, const Vec2& a, const Vec2& b, double angle)

Solid::revolve_in_plane() for a curve: its segments swung into a sheet, no caps.

Solid::coil()

static Result<Solid> coil(const Profile& profile, const AxisLine& axis, double pitch, double turns)

The profile coiled about axis (a point and a direction): read as Solid::revolve() reads it — x the distance from the axis, y along it — and turned turns times while climbing pitch along the axis each turn: a spring, a thread. The walls follow the helix to a few millionths of the radius (a helix is not a NURBS curve, so they are a close fit, exact at both ends); the two ends are the profile itself, flat. A profile reaching the axis, one with holes, or — from a full turn up — a pitch no taller than the profile returns an Error in its Result.

Solid::loft()

static Result<Solid> loft(const Profile& a, const Frame& frame_a, const Profile& b, const Frame& frame_b)

The solid between profile a on one frame and b on another: ruled walls between matching sides (both profiles need the same number of sides, and no holes), capped by the two.

Solid::loft_open()

static Result<Solid> loft_open(const Profile& a, const Frame& frame_a, const Profile& b, const Frame& frame_b)

The ruled walls without the caps.

Solid::loft_through()

static Result<Solid> loft_through(const Sections& sections)

The solid smooth through every section — a profile on its frame, in order: each wall interpolates its side across all the profiles (cubic through four or more, quadratic through three, Solid::loft() through two), capped by the first and the last. Every section of the result is its profile exactly, arcs and all. The profiles must have the same number of sides and no holes; otherwise returns an Error in its Result.

Sections is a std::vector of Sections, each a {profile, frame} pair, both borrowed for the call.

Solid::loft_through_open()

static Result<Solid> loft_through_open(const Sections& sections)

The walls through the curves without the caps: an open sheet.

Solid::sweep()

static Result<Solid> sweep(const Profile& profile, const Frame& frame, const SweepPath& path)

The profile on frame, carried along a SweepPath. A straight piece is an extrusion and an arc a revolution about the arc's axis, so nothing is approximated — a circle along an arc is an exact torus wall.

Solid::sweep_open()

static Result<Solid> sweep_open(const Profile& profile, const Frame& frame, const SweepPath& path)

The swept walls without caps: an open sheet.

Solid::pipe()

static Result<Solid> pipe(const SweepPath& path, double radius, double thickness = 0.0)

A circle of radius carried along a SweepPath, square to where it starts: a solid rod, or with a positive thickness a tube whose walls are that thick. The path is only borrowed, as by Solid::sweep(), and refused the same way.

Solid::extrude_faces()

Result<Solid> extrude_faces(double height) const

Every face of a sheet pushed height along its own normal, walled and closed: the sheet as a solid of that thickness.

Solid::face()

static Result<Solid> face(const Profile& profile, const Frame& frame)

The flat sheet a profile bounds on frame: one planar face, each hole a hole through it, its normal the frame's z however the profile winds, every edge the exact line, arc or spline its segment is. An open sheet — raise it with Solid::extrude_faces(), cut it with Solid::trim().

Placing

Solid::place()

Result<Solid> place(const Frame& frame) const

A solid built about the origin moved onto frame: its origin to the frame's origin, its axes to the frame's (see Frames).

Solid::translate()

Result<Solid> translate(double dx, double dy, double dz) const

Moved by (dx, dy, dz).

Solid::scaled()

Result<Solid> scaled(double factor) const

Scaled by factor about the origin: every length times factor, exactly. factor must be positive and finite.

Solid::rotate()

Result<Solid> rotate(const AxisLine& axis, double radians) const

Turned radians about axis (a point and a direction).

Solid::mirror()

Result<Solid> mirror(const Frame& plane) const

Reflected across plane: a frame whose z is the mirror plane's normal.

Booleans

Solid::join()

Result<Solid> join(const Solid& other, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}, bool merge = false) const

The union with other, as an exact B-rep. merge (off by default, so face and edge numbers stay as they were) merges the flush faces the join leaves, as Solid::merge_flush() does — Go takes it as a trailing true, Java as an overload; Solid::cut() and Solid::common() take it too. tolerance (0.05 by default) is the mesh tolerance the boolean decides at: both solids are meshed at it, so a tighter one is as correct and slower. It is also the resolution: two faces on one surface to within it — a peg the size of its hole, or a hair thinner — are one surface, and the join is a fit; a gap wider than it is a gap. progress, where the wrapper takes one, is called with a phase name and a done/total count.

merge and progress are the last two arguments; progress is a std::function<void(std::string_view, std::size_t, std::size_t)>.

Solid::cut()

Result<Solid> cut(const Solid& other, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}, bool merge = false) const

This solid with other removed.

Solid::common()

Result<Solid> common(const Solid& other, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}, bool merge = false) const

What this solid and other share.

Solid::intersect()

Result<Intersection> intersect(const Solid& other, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

Where this solid's faces cross or coincide with other's, at tolerance, as an Intersection: every crossing as a Chain, every coincident face pair that shares a region as an Overlap. Not a boolean: neither solid changes, nothing is built. tolerance (0.05 by default) is read as Solid::join()'s — both solids' mesh resolution and how close a point may sit to both faces' exact surfaces to be reported. No crossing at all is an empty Intersection, never an error. progress, where the wrapper takes one, is called with a phase and a done/total count: meet on the exact route; mesh, cull, cross, snap, curve where it falls back to the mesh route.

There is one chain per face pair per branch: a chain does not cross a face boundary or a closed curve's own seam, so a crossing that runs past either comes back as more than one chain, joined by matching ends. Two faces on one surface sharing no region — an edge or a rim resting against another face, two flush faces meeting along a line — are silent: neither a chain nor an overlap. Known limit: a crossing narrower than tolerance — two surfaces passing within it of each other without their meshes actually crossing — can be missed; where this bites the surfaces are near-tangent, and the chain that is found (if any) carries tangent.

Solid::section()

Result<std::vector<Profile>> section(double z, double tolerance = 0.05) const

This solid cut by the plane Z = z: its closed outlines there, in XY, nested into profiles with holes (boundaries counter-clockwise, holes clockwise), a line or a circle crossing kept exact and any other curve as lines within tolerance. A plane that misses the solid is an empty list. A z not finite, a tolerance (0.05 by default) not positive and finite, a face lying in the plane (section a little above or below it), or a crossing that does not close, returns an Error in its Result.

Solid::hits()

Result<SolidHits> hits(const Profile& profile, const Frame& frame, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

Where profile, placed on frame, pierces this solid's faces, and the pieces its loops cut into, as a SolidHits. Neither is changed. A point hit lies within tolerance of the segment's exact curve and of the face's exact surface, inside the face's trim; its profile spot (Hit::a_start: loop, segment, t) and face spot (Hit::b_start: face, u, v) evaluate to the point within tolerance; touch where the curve's tangent lies within 1e-3 (sine) of the surface's tangent plane there (a graze), false at a crossing. A run is a stretch of one segment lying within tolerance of one face and inside it, longer than tolerance. Hits within tolerance of each other merge (a hit at a segment join reported once, as (k, t = 1); a closed loop's closing join reads (0, 0)). Every point is in world space (the frame applied).

Pieces (Piece) only for a closed body — an open body has none — in loop order, covering every loop exactly; a piece's spots read a segment join as the next segment's start (k + 1, 0), and an open chain runs from (0, 0) to (n - 1, 1); a loop no hit cuts is one closed piece. inside by the piece middle's winding number over the body's mesh; a piece lying on the surface is inside. Known limit: a segment passing within tolerance of a face without its polyline crossing the mesh can be missed (near-tangent grazes).

progress, where the wrapper takes one, is called with phases mesh, cull, hits and pieces and a done/total count. A tolerance not positive and finite, a solid with no faces or that meshes to nothing, a profile with no segments, or a free-form segment that is not an evaluable NURBS curve returns an Error in its Result.

Solid::split_sheet()

Result<Solid> split_sheet(const Solid& tool, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

This solid or sheet cut along tool's boundary with nothing removed: each face comes back as its pieces outside tool and then its pieces inside, in the original face order — the start of a surface trim. tool must be a closed solid. Keep the pieces you want with Solid::drop_faces(), or split and drop in one call with Solid::trim().

Faces and sheets

Solid::face_sheet()

Result<Solid> face_sheet(std::uint32_t f) const

One face alone, as an open sheet: its surface, its loops and the exact curves on its edges, the rest of the solid left behind — raised by Solid::extrude_faces() it is the prism over that face. Keeps the face's colour.

Solid::drop_faces()

Result<Solid> drop_faces(const std::vector<std::uint32_t>& which) const

This solid without the faces listed: the rest keep their surfaces, curves and colours in their order, so an index into the result is this one's with the dropped ones closed up. Dropping every face returns an Error in its Result.

Solid::trim()

Result<Solid> trim(const Solid& tool, Keep keep = Keep::outside, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

This sheet (or solid) cut along the closed tool's boundary and the pieces on one side thrown away: keep Keep::outside (the default) keeps what lies outside the tool — a hole punched through — and Keep::inside what lies within it. Nothing on the kept side returns an Error in its Result. tolerance and progress as for Solid::join().

keep is Keep::outside or Keep::inside.

Solid::push_pull()

Result<Solid> push_pull(std::uint32_t f, double distance, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const
Result<Solid> push_pull(const std::vector<std::uint32_t>& which, double distance, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const
Result<Solid> push_pull(std::initializer_list<std::uint32_t> which, double distance, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

Face face pushed out by distance along its outward normal — pulled in, negative — as a face extrude does it: the prism over it joined on (cut out) at tolerance, and the flush faces merged, so a box's top raised is one taller box of six faces rather than a box and a prism with every side wall split at the seam. A face on a cylinder, a cone, a sphere or a torus moves out along its normal instead, as a press-pull does: the surface a step out — a boss fatter, a bore or a countersink narrower, a dome fuller — with the flat faces beside it carried along in their own planes. Any other curved face is refused, as is a curved face with anything but a plane it can follow beside it, reaching a cone's apex, pushed to its axis or centre, off a plane beside it or run into another edge. A flat face keeps its own colour where it now lies.

Several faces push together, as a press-pull on a selection: each by its own rule, one after another in the order given, each found again after the pushes before it renumbered the faces — a box's top and a side pushed 5 is the box 5 taller and 5 wider, a boss's top and wall the boss taller and fatter. A face on the same curved surface as one before it, and joined to it, moved with that one and is not pushed twice. No faces, or a face an earlier push took away, returns an Error in its Result.

Overloaded: a face index, or a std::vector<std::uint32_t> of them.

Solid::refillet()

Result<Solid> refillet(std::uint32_t f, double radius, double tolerance = FILLET_TOLERANCE) const

The round face belongs to — a fillet's bands, balls and rim bands joined to that face — made again at radius, as a press-pull on a fillet face: taken back to the sharp edges it replaced, and those rounded again, so the round is the one Solid::fillet() makes at that radius. Rounds of straight edges between planes (their ends square corners, mitres, balls, or a cylinder, cone or sphere the edge runs into — a D-cut shaft's top edge, a rib's into a boss) and of circular rims between a plane and a cylinder or cone (a boss's foot, a bore's mouth, a counterbore's step); a face that is not one, or a radius that does not fit, returns an Error in its Result.

Solid::unfillet()

Result<Solid> unfillet(std::uint32_t f) const

The round face belongs to taken off, the faces beside it made sharp again, meeting on the edges the round replaced — the delete of a fillet face. The same rounds as Solid::refillet().

Solid::rechamfer()

Result<Solid> rechamfer(std::uint32_t f, double distance, double tolerance = FILLET_TOLERANCE) const

The chamfer face belongs to — its bevels (flat between two planes, cones round rims) and the corner triangles joined to that face — cut again at distance, as a press-pull on a chamfer face: taken back to the sharp edges it cut, and those bevelled again, so the chamfer is the one Solid::chamfer() cuts at that distance. A flat bevel's ends may run into a cylinder, cone or sphere, as a round's may. A face that is not a chamfer's bevel, or a distance that does not fit, returns an Error in its Result.

Solid::unchamfer()

Result<Solid> unchamfer(std::uint32_t f) const

The chamfer face belongs to taken off, the faces beside it made sharp again — the delete of a chamfer face. The same chamfers as Solid::rechamfer().

Solid::merge_flush()

Result<Solid> merge_flush() const

This solid with its flush faces merged: flat faces on one plane, facing one way and meeting along their edges — the seams Solid::join() leaves where two parts are flush — made one face, and the vertices left mid-way along a straight edge taken out.

Solid::split()

Result<std::vector<Solid>> split(const Solid& tool, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

This solid split by tool into bodies — returned as a list: a closed tool gives the parts outside it, then the parts inside; a flat sheet (a Solid::face()) splits by the whole plane it lies on. Each connected part is a body of its own, so a U cut across both arms is three. The new faces are pieces of the tool's, the colours carried over. A tool that does not cross the solid, or a curved sheet, returns an Error in its Result. tolerance and progress as for Solid::join().

Solid::split_by_plane()

Result<std::vector<Solid>> split_by_plane(const Frame& plane, double tolerance = DEFAULT_TOLERANCE, const Progress& progress = {}) const

This solid split by the plane through plane's origin, square to its z (a frame): the bodies in front of it first, then those behind.

Solid::lumps()

Result<std::vector<Solid>> lumps() const

This solid's connected bodies, each a solid of its own — faces sharing an edge are one body — in the order of their first faces. One body comes back as itself; a boolean that leaves two parts gives two.

Finishing

Solid::edges()

Result<std::vector<Edge>> edges() const

The solid's edges as Edge values — what Solid::fillet() and Solid::chamfer() take. Copied; safe to keep.

Solid::fillet()

Result<Solid> fillet(const std::vector<std::uint32_t>& edge_indices, double radius, double tolerance = FILLET_TOLERANCE, const Progress& progress = {}) const

Round the given edges (Edge values or their indices) with radius. Exact: the blend faces are cylinders, tori and NURBS, and the neighbours are trimmed back onto them.

Solid::chamfer()

Result<Solid> chamfer(const std::vector<std::uint32_t>& edge_indices, double distance, double tolerance = FILLET_TOLERANCE) const

A flat bevel instead of a round: each edge cut back distance along both its faces.

Solid::shell()

Result<Solid> shell(double thickness, const std::vector<std::uint32_t>& open_faces = {}, double tolerance = FILLET_TOLERANCE, const Progress& progress = {}) const

Hollow the solid to walls thickness thick — inward for a positive thickness, outward (the solid becoming the cavity) for a negative one. The faces listed in open are removed so the hollow is reachable.

Solid::thicken()

Result<Solid> thicken(double thickness, double tolerance = FILLET_TOLERANCE, const Progress& progress = {}) const

A sheet made a solid thickness thick: its faces, their twins moved thickness along the faces' normals (against them for a negative thickness), and a wall round every open edge. Two faces of a folded sheet meet on their offsets' mitre; a closed sheet thickens to a hollow. Free-form (NURBS) faces offset by a fit held to tolerance. A thickness a face cannot take — a radius used up, a free-form offset folding over — returns an Error in its Result.

Asking

Solid::faces()

std::uint32_t faces() const

How many faces.

Solid::face_kind()

Result<std::string> face_kind(std::uint32_t f) const

A face's surface: plane, cylinder, cone, sphere, torus, nurbs, revolution, extrusion or other.

Solid::select_face()

Result<std::uint32_t> select_face(const Selector& selector) const

The index of the face a Selector picks.

Solid::face_frame()

Result<Frame> face_frame(std::uint32_t f) const

The frame on a face: origin at its centre, z its outward normal, x world X laid onto the face (world Y on a face facing close to X) — Frame::at()'s rule, so the top of a box gets the XY plane's axes. What Workplane::workplane() moves onto.

Solid::face_ref()

Result<std::array<double, 8>> face_ref(std::uint32_t f) const

A face by what it is, eight numbers: the surface's kind (plane 0, cylinder 1, cone 2, sphere 3, torus 4, NURBS 5, revolution 6, extrusion 7, sum 8), a point on the surface at the face's middle (x, y, z), the outward normal there (x, y, z), and the face's extent. What a feature made on a face keeps — a sketch on it, a shell's opening — so the face is found again with Solid::find_face() when the solid is rebuilt with its faces moved, split or renumbered. Take it before any move you apply to the solid, and look it up on the unmoved one.

Solid::find_face()

Result<std::optional<std::uint32_t>> find_face(const std::array<double, 8>& ref, std::optional<std::uint32_t> hint = std::nullopt, double tolerance = 1e-3) const

The face a reference from Solid::face_ref() refers to: among the faces of that kind whose surface passes through the point, facing the same way, the one the point lies in — or, where it lies in none (the face shrank away, a hole opened under it), the one whose boundary comes nearest. hint is the index the face had, preferred among faces that fit equally well; tolerance how far the point may sit off a surface to still be on it. None where the face is gone.

Solid::bounds()

Result<std::pair<Vec3, Vec3>> bounds() const

The axis-aligned box, at the same cost whatever the solid's size: its mesh at a thousandth of its own extent, in double, so the box is inside the exact one and short of it by at most that. Worked out once per solid, apart from the tessellation Solid::mesh() caches. Solid::bounds_at() is the box of the mesh at a tolerance you name.

Solid::bounds_at()

Result<std::pair<Vec3, Vec3>> bounds_at(double tolerance) const

The bounds over the tessellation at tolerance — the same cache Solid::mesh() fills, so asking both costs one mesh.

Solid::bounds64()

Result<std::pair<Vec3, Vec3>> bounds64(double tolerance = DEFAULT_TOLERANCE) const

Solid::bounds() in double precision: the axis-aligned bounds over the mesh's unnarrowed positions at tolerance (0.05 where the language defaults it) — exact far from the origin, where Solid::bounds()'s float32 mesh is not.

Solid::leaked_edges()

Result<std::uint32_t> leaked_edges(double tolerance = DEFAULT_TOLERANCE) const

How many mesh edges at tolerance are bound by anything other than two triangles: zero for a closed solid. A seam two solids share along a line does not count; a hole or a fold does.

Solid::unpaired_edges()

Result<std::uint32_t> unpaired_edges(double tolerance = DEFAULT_TOLERANCE) const

How many mesh edges have triangle uses that do not cancel out: zero for a closed, consistently oriented solid. Unlike Solid::leaked_edges() this catches a fold — two triangles running the same way.

Solid::is_watertight()

Result<bool> is_watertight(double tolerance = DEFAULT_TOLERANCE) const

Whether Solid::leaked_edges() is zero.

Solid::manifold()

Result<Manifold> manifold() const

Whether the faces make a manifold — every edge bordered by one face or two, the faces round every vertex one fan — and whether it is closed, as a Manifold. Read off the solid's topology, not a mesh, so it takes no tolerance; whether the faces all face out is Solid::unpaired_edges()'s question.

Solid::mass()

Result<Mass> mass(double accuracy = 0.0) const

What this solid measures, per unit density, as a Mass: area, volume, centroid, the inertia tensor about the centroid and its principal moments and axes — each figure beside a bound on its own error. Measured on the exact surfaces, refined until every error is under accuracy (a share of the figure) or the refinement gives up, which Mass::accuracy_met reports; the values are honest either way, they are simply less certain, and the errors say by how much. Volume, centroid and inertia mean something only where the body is closed — Mass::closed — and a body whose faces all point inward is measured as if they did not, which Mass::inverted says. An accuracy not positive and finite returns an Error in its Result.

Solid::bounds_box()

Result<Box> bounds_box(double tolerance = DEFAULT_TOLERANCE) const

This solid's Box at tolerance (0.05 by default): its boolean mesh, padded by twice that — the broad phase's own bound, cheaper to move and to test against another than the solid itself. Collider builds one of these per part at construction. A tolerance not positive and finite, or a solid that meshes to nothing, returns an Error in its Result.

Naming

Solid::named()

Result<Solid> named(std::string_view name) const

This solid, named name. The name rides through an operation with exactly one source solid (place, translate, rotate, mirror, scaled, coloured, edges_coloured, fillet, chamfer, shell, thicken, face_sheet, drop_faces, lump, trim, split_by_plane, push_pull, and so on) and is dropped by one with two or more sources (join, cut, common, split_sheet, split) and by a fresh primitive or sweep. It is what Assembly::place_solid() defaults a placement's own name to, and the product name a lone named solid gets written into STEP (Solid::step()/Solid::step_text() — SAT and OCCT .brep have no product name to set). An empty name returns an Error in its Result.

Solid::name()

std::optional<std::string> name() const

This solid's name, or none, as Solid::named() set it, kept or dropped by whatever built this solid.

Colour

Solid::coloured()

Result<Solid> coloured(const Vec3& rgb_value, std::optional<std::uint32_t> f = std::nullopt) const
Result<Solid> coloured(std::string_view colour, std::optional<std::uint32_t> f = std::nullopt) const

A new solid coloured (r, g, b), each 0..1, "#rgb", "#rrggbb" or a CSS colour name — or, given a face (Go: ColouredFace), just that face, whose colour then wins over the solid's. What is made from a coloured solid inherits: a move keeps every colour; a boolean, fillet, chamfer or shell gives each face the colour of the input face it lies on (a cut's bore takes the tool's), and a new face — a round, a shell's inner wall — the solid's. Solid::step() and cadaclysm::blacksmith::write_step() write the colours as STEP styling (AP242; a named schema that lacks styling writes the solids bare), and a solid's colour reads back as Node::colour().

Takes a Vec3 or, as a std::string_view overload, colour text, and an optional face.

Solid::colour()

Result<std::optional<Vec3>> colour() const

The solid's own colour as (r, g, b), or none.

Solid::face_colour()

Result<std::optional<Vec3>> face_colour(std::uint32_t f) const

A face's colour as drawn: its own, else the solid's, else none.

Solid::edges_coloured()

Result<Solid> edges_coloured(const Vec3& rgb_value, std::optional<std::vector<std::uint32_t>> edge_indices = std::nullopt) const
Result<Solid> edges_coloured(std::string_view colour, std::optional<std::vector<std::uint32_t>> edge_indices = std::nullopt) const

A new solid with its edges coloured (r, g, b), "#rgb", "#rrggbb" or a CSS colour name: every edge, or, given edges (as Solid::fillet() takes them: Edge records or indices), just those, whose colour then wins over the all-edges one — an empty list colours none. Inherited as face colours are: a move keeps every edge colour; a boolean, fillet, chamfer or shell gives each edge the colour of the input edge it lies on, and a new edge (a cut's rim, a round's edges) the all-edges colour. Solid::step() and cadaclysm::blacksmith::write_step() write them as STEP CURVE_STYLE styling, which the reader reads back as Node::edge_colours().

edges_coloured(Vec3, ...) takes numbers; a std::string_view overload takes colour text.

Solid::edge_colour()

Result<std::optional<Vec3>> edge_colour(std::uint32_t e) const

An edge's colour as drawn: its own, else the solid's edge colour, else none.

Solid::brep()

Result<void> brep(const std::string& path) const

Write this solid as an OCCT .brep file; see cadaclysm::blacksmith::write_brep().

Solid::brep_text()

Result<std::string> brep_text() const

The same .brep file as text.

From files

Solid::open()

static Result<Solid> open(const std::string& path, std::optional<std::size_t> body = std::nullopt)

The body a CAD file holds, as a solid: STEP (AP203/214/242), ACIS .sat, Rhino .3dm, OCCT .brep, IGES or IFC, read where it draws, in the file's own units and axes. A file drawing several bodies needs body (0-based, in drawing order) or Solid::open_all(). What such a solid can do is what its geometry allows: fillet and chamfer want line and circle edges; booleans take any surface, but new edges traced on a free-form face are not always writable back to STEP; and every verb meshes its operands first, so its cost grows with the body's face count. Reads through the reader library, which must be from the same release.

Solid::open_all()

static Result<std::vector<Solid>> open_all(const std::string& path)

Every body a CAD file draws, as solids placed where it draws them: one per placement, so a part placed twice is two solids.

Solid::from_node()

static Result<Solid> from_node(const cadaclysm::Node& node, bool placed = true)

The body a reader Node draws, as a solid — sharing the reader's brep (Node::brep()), not copying it; the scene can be closed first. placed (the default) puts it where the node's transform does, where its mesh draws; otherwise it keeps the node's own frame. The two libraries' layouts (cadaclysm::blacksmith::brep_layout_id()) must agree, or it returns an Error in its Result.

Output

Solid::mesh()

Result<Mesh> mesh(double tolerance = DEFAULT_TOLERANCE) const

Triangles at tolerance: positions, normals (three floats a vertex) and indices. In every wrapper that lends a view, views owned by a held tessellation — valid for as long as you keep the view, whatever happens to the solid: meshed again at any tolerance, closed. Rust borrows the solid for the view's lifetime instead; Node and Godot copy. A raw span, slice or buffer taken out of a view does not keep the view itself reachable in a collected language: hold the view while you read one, and copy what must outlive it. The same rule as a FEM mesh's views, for the same reason. A view pins its whole tessellation — positions, normals, indices, edges, face counts, in both precisions — until the view itself is freed or collected, whichever the language uses; for data that must outlive the view, copy() it and drop the view.

Returns a view that owns a tessellation of its own: it outlives the solid being meshed again at another tolerance and the solid's own close().

Solid::mesh64()

Result<Mesh64> mesh64(double tolerance = DEFAULT_TOLERANCE) const

Solid::mesh() in double precision: the same tessellation at tolerance, unnarrowed — positions and normals exact far from the origin, where Solid::mesh()'s float32 positions are not. A view owned by a held tessellation, like Solid::mesh(): valid whatever happens to the solid, in every wrapper that lends a view — Node and Godot copying instead.

Returns a view that owns a tessellation of its own, like mesh: it outlives the solid being meshed again at another tolerance and the solid's own close().

Solid::fem_mesh()

Result<FemMesh> fem_mesh(double tolerance = 0.01, double max_size = 0.0, const std::optional<Frame>& placement = std::nullopt, const Progress& progress = {}) const

This solid meshed for a solver, as a FemMesh: nodes welded by bits — two mesh points are one node only where their coordinates are the same doubles, so no tolerance ever merges two distinct points and a crack stays a crack — triangles wound outward, and every node tagged with the lowest-dimension B-rep entity it lies on.

tolerance is the chordal tolerance in model units, and it alone governs how closely the mesh follows the geometry. max_size is a size ceiling, 0 for none (curvature alone): it splits boundary segments to at most that length and lays interior stations max_size / √2 apart as a target, and it adds nodes without refining the boundary geometry — a caller that wants the rim nearer its curve lowers the tolerance, where max_size only makes the elements smaller. FemMesh::longest_edge() is what the mesh actually came to, and the figure to check.

placement is a Frame or twelve numbers — origin, x, y, z — as every frame here, and left out for the identity, a solid meshed in its own coordinates being the common case. The reader's Node::fem_mesh() takes sixteen, column-major, so a caller moving between the two reformats the placement.

A cracked solid is not a failure: it comes back with FemMesh::watertight() false and its cracks in FemMesh::open_edges() and FemMesh::folded_edges(), and nothing is welded shut to make it look sound. It returns an Error in its Result for a tolerance or size the mesher refuses, a placement that is not twelve finite and invertible numbers, a closed solid this library cannot mesh, and a solid that meshes to no triangles at all.

No unlicensed notice here: FemMesh::msh_text() and FemMesh::save_msh() print it, this library noticing on its writers rather than on its builders — where the reader library notices in its own constructor and on neither .msh call.

Solid::face_triangles()

Result<FaceTriangles> face_triangles(double tolerance = DEFAULT_TOLERANCE) const

How many triangles each face meshed to at tolerance, one count per face in face order: the triangles of Solid::mesh() at the same tolerance run face by face, so face f's are the counts[f] after the first counts[:f].sum(), and the counts sum to the mesh's triangle count. What a viewer colours a face by. A view, like Solid::mesh() — a copy in Node and Godot.

Returns a view that owns a tessellation of its own, like mesh; copy() for counts that must outlive it.

Solid::edge_polylines()

Result<EdgePolylines> edge_polylines(double tolerance = DEFAULT_TOLERANCE) const

The feature edges as polylines at tolerance, one run of points per edge. Views, like Solid::mesh() — copies in Node and Godot.

Solid::edge_polyline_colours()

Result<std::vector<std::optional<Vec3>>> edge_polyline_colours(double tolerance = DEFAULT_TOLERANCE) const

A colour per polyline of Solid::edge_polylines() at the same tolerance, as drawn: (r, g, b), or none for a polyline on no coloured edge; an empty list where the solid has no edge paint at all — what a viewer colours the edge lines by. Copied out, unlike the polylines.

Solid::show()

Result<void> show(const O& options = O{}) const

Draw the solid with the viewer in use — in a terminal, the picture is left in the scrollback. Each face keeps its own colour (Solid::face_colour(): a colour of its own, else the solid's). Keywords: view= (front, back, left, right, top, bottom, iso), az=, el=, zoom=, up=, edges=, width=, height=, hint=, tolerance=. With the GPU library (import cadaclysm.view.wgpu) the drawing replaces what one shared window shows and stays live — at the REPL, in IPython and in Jupyter by themselves, in a script until it exits; cadaclysm.view.View draws several items or several windows.

Takes a cadaclysm::ShowOptions (edges true by default, azimuth/elevation/up std::optional; tolerance is a std::optional<double>). Blocks until the window is closed on the wgpu library (cadaclysm::view::use_wgpu()); the terminal prints a still. Several items or a window that stays: cadaclysm::View. A member template: #include <cadaclysm/view.hpp> to call it, and link cadaclysm::view_loader (cadaclysm::view is the linked C library, for hosts that call cadaclysm_view.h directly).

Solid::view()

auto view(const O& options = O{}) const

Orbit the solid with the viewer in use until it is closed; returns the camera where it was left — a dict of eye, target, up, orthographic, azimuth and elevation — and pick, what was last clicked (item, node, face) or None. Keywords as Solid::show(). A fresh window, run until closed, with the GPU library.

Takes the options show does; returns a Result<Camera>.

Solid::step()

Result<void> step(const std::string& path, const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre) const

Write this solid as a STEP file (AP203, or AP242 when coloured); see cadaclysm::blacksmith::write_step() for schema and unit.

Solid::step_text()

Result<std::string> step_text(const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre) const

The same STEP file as text.

Solid::sat()

Result<void> sat(const std::string& path, Unit unit = Unit::millimetre) const

Write this solid as an ACIS SAT file; see cadaclysm::blacksmith::write_sat() for unit.

Solid::sat_text()

Result<std::string> sat_text(Unit unit = Unit::millimetre) const

The same SAT file as text.

Solid::svg()

Result<void> svg(const std::string& path, const SvgOptions& options = SvgOptions()) const

This solid's wireframe as SVG, from a camera the keywords describe — cadaclysm::blacksmith::write_svg()'s own words, read by the library itself rather than a viewer. With path, writes the file and returns std::nullopt; without, returns the SVG text. returns an Error in its Result on a refused option or a failed write.

Solid.silhouette_beziers

This solid's silhouette from a camera described by options, owned and freed with a separate call. Exact Béziers where the surface has a closed silhouette form — sphere, cylinder, cone, extrusion — and for every other surface, a torus and a NURBS patch included, a straight-chord polyline carried as degree-1 Béziers: there is no spline fitter here, so a sphere's rim arrives as arcs and a torus's as chords. A solid with no silhouette (curved faces that no eye turns away from, like a sphere seen from neither outside nor in) gives std::nullopt and count 0, which is not an error and never touches last_error.

Not yet in this wrapper; use the C entry point.

Solid.silhouette_beziers_free

Release silhouette Bézier data: the data from a silhouette_beziers call, passed with its matching count. std::nullopt is a no-op. The count must be the value that same call wrote through its own out-parameter — the buffer carries no length of its own.

A managed wrapper never exposes a manual free — it releases the buffer itself. This documents the C function.

Solid::to_scene()

Result<cadaclysm::Scene> to_scene(const std::optional<std::string>& schema = std::nullopt) const

This solid as a reader Scene, through STEP in memory: the door from the kernel to everything the reader does — its tree, meshes, glTF/OBJ/STL export. Needs the reader library as well.

Solid::close()

void close() noexcept

Free the solid now. The garbage collector, or the language's scope, does it otherwise.

AxisLine and Keep C++ only

using AxisLine = std::array<Vec3, 2>
enum class Keep
outside
inside

An axis is {{px, py, pz}, {dx, dy, dz}}: a point and a direction. Solid::trim() keeps the side Keep names.

Progress C++ only

using Progress = std::function<void(std::string_view phase, std::size_t done, std::size_t total)>

What the long operations — the booleans, Solid::fillet(), Solid::shell(), Solid::split() — call with a phase name and a done/total count, when one is passed.

Assembly

class Assembly

A mutable tree of placements: a name, and zero or more solids or other assemblies placed in it at a frame. Unlike Solid, placing shares rather than copies — placing one assembly under another does not snapshot it, so a later placement on the shared one shows up wherever it already sits. Close it when done, or let the language's scope do it; see Lifetimes — closing an assembly does not free what was placed in it if that is still reachable from somewhere else.

Assembly::name()

std::string name() const

This assembly's own name, given when it was made.

Assembly::place_solid()

Result<std::string> place_solid(const Solid& s, const Frame& frame, std::optional<std::string_view> name = std::nullopt) const
Result<std::string> place_assembly(const Assembly& placed, const Frame& frame, std::optional<std::string_view> name = std::nullopt) const

Place thing (a Solid or another Assembly) at frame in this assembly, called name — or, with name left out, thing's own name (Solid::name() for a solid, "part" for an unnamed one, or the placed assembly's own name), numbered past any already taken here ("bolt", "bolt 2", ...). frame must be right-handed and orthonormal, or the call returns an Error in its Result. An explicit name already taken here returns an Error in its Result. Placing an assembly that is this one, or anywhere above this one in the tree already, returns an Error in its Result naming the cycle, since writing that out would never terminate. Returns the placement's name.

Assembly::joint()

Result<void> joint(std::string_view name, std::string_view start, std::string_view end) const

Connect this assembly's links start and end, kept in that order, as a joint called name. An empty name, a name taken by another joint here, a link this assembly does not have, a link joined to itself or a (start, end) pair already joined returns an Error in its Result; the reverse pair is a different joint.

Assembly::pair()

Result<void> pair(std::string_view joint_name, std::string_view kind, const Frame& frame, const PairOptions& options = {}) const

Give joint joint its pair: kind (one of revolute, prismatic, cylindrical, screw, spherical, spherical_with_pin, planar, universal, homokinetic, fully_constrained or unconstrained), the contact frame at rest, an optional end_frame (the frame the pair reaches at its rest values; frame itself, i.e. all-zero rest values, when left out), ranges (one entry per freedom of kind, each unbounded or a lower/upper pair; left out entirely for a kind that allows none), and pitch (required and non-zero for screw, refused for any other kind). An unknown or already-paired joint, an unknown kind, a bad pitch, ranges given where none are allowed or of the wrong length, a non-finite or inverted range, or an end_frame this kind's motion cannot reach (or reaches only outside a given range) returns an Error in its Result, and the assembly is left as it was.

Assembly::state()

Result<void> state(std::string_view name, const std::vector<StateRow>& rows) const

Add a named state: the rows (Python: a mapping, or an iterable of (joint, values) pairs) set each named joint to its values, in that joint's pair's freedom order. An empty or duplicate name, no rows, an unknown joint, a joint set twice, a joint with no pair or none of its pair's freedoms, a row of the wrong length, or a non-finite or out-of-range value returns an Error in its Result, and the assembly is left as it was.

Assembly::sequence()

Result<void> sequence(std::string_view name, const std::vector<SequenceSegment>& segments, bool seconds = false) const

Add a named motion sequence over this assembly's states: segments are (state name, parameter, interpolation) rows, the interpolation one of "undefined", "discontinuous", "synchronous" or "linear", and seconds writes the parameter's unit as the second. Closed or open is the last segment's interpolation: closed unless it is "discontinuous". An empty or duplicate name, fewer than two segments, an unknown state, a non-finite or decreasing parameter, or an unknown interpolation returns an Error in its Result, and the assembly is left as it was.

Assembly::base()

Result<void> base(std::string_view link) const

Declare this assembly's base link. link naming no link of this assembly, or a base already declared, returns an Error in its Result naming the link already there.

Assembly.posed

This assembly with its mechanism posed, as a new assembly; this one is left as it was. Each joint is named by its path — the placement names down the tree, then the joint's name, joined by / ("drive FL/spin"), a / or \ inside a name escaped with \ — or by its bare name, which means a joint of this assembly by that name, else the only joint of that name anywhere under it; each copy of a sub-assembly placed more than once poses on its own. Values are absolute, one per freedom of the joint's pair, radians for rotations and the assembly's units for translations. A state's path is applied first and overridden joint by joint; ground names one of this assembly's links to hold fixed (default: its base, else its busiest link). A sub-assembly no key reaches is shared with this one. An unknown or ambiguous key, a joint given twice, one without a pair, the wrong number of values, a value that is not finite or is outside its range, an unknown state or link, or a loop the pose leaves open returns an Error in its Result.

Not yet in this wrapper; use Python or C for now.

Assembly::step_text()

Result<std::string> step_text(const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre) const

This assembly, and everything placed under it, as one STEP file: this assembly the root product, each sub-assembly and each distinct part (the same solid with the same paint and name) written once, each placement an occurrence named as it was placed. schema and unit as cadaclysm::blacksmith::write_step(). An assembly reachable from this one, this one included, that places nothing returns an Error in its Result — a reader would never show it.

Assembly::step()

Result<void> step(const std::string& path, const std::optional<std::string>& schema = std::nullopt, Unit unit = Unit::millimetre) const

Assembly::step_text() written to a file.

Assembly::to_scene()

Result<cadaclysm::Scene> to_scene(const std::optional<std::string>& schema = std::nullopt) const

This assembly as a reader Scene, through STEP in memory; see Solid::to_scene().

Assembly::close()

void close() noexcept

Free this handle now. The garbage collector, or the language's scope, does it otherwise.

Assembly::create C++ only

static Result<Assembly> create(std::string_view name)

Python's Assembly(name): a new, empty assembly called name, or a Result holding the refusal for an empty one.

Edge

struct Edge

One edge of a solid as plain data, copied out of it: what Solid::edges() lists and Solid::fillet() takes.

A method that takes edges — Solid::fillet(), Solid::chamfer(), Solid::edges_coloured() — takes a list of them, Edge values or their indices, and refuses a single one: one edge is a list of one. So do the face and corner lists of Solid::drop_faces(), Solid::shell() and Profile::round(); Solid::push_pull() takes one face on its own or a list.

Edge::index

std::uint32_t index

Its index — what Solid::fillet() and Solid::chamfer() take.

Edge::kind

std::string kind

The curve: line, circle, ellipse, parabola, hyperbola, nurbs or other.

Edge::faces

std::vector<std::uint32_t> faces

The faces meeting on it, as face indices.

Edge::segments

std::vector<std::array<Vec3, 2>> segments

The two ends of each piece of the edge.

Edge::curve

std::optional<Curve> curve

The edge's exact curve, as a Curve — std::nullopt for an edge with no exact curve (kind other). Filled when the edges are listed, so the edge stays plain data.

Edge::is_line()

bool is_line() const

Whether the edge is straight.

Edge::direction()

std::optional<Vec3> direction() const

The unit direction of a straight edge, or std::nullopt for a curved one. Picking the vertical edges of a plate is a filter on this.

Curve

struct Curve

One edge's, or one intersection chain's, exact curve as plain data, copied out: what Edge::curve or Chain::curve holds. kind is line, circle, ellipse, parabola, hyperbola or nurbs.

t0..t1 is the edge's (or the chain's) parameter range on its own curve: a line's fraction (0..1 over origin -> origin + x, where x is the full to - from, NOT unit — so point(t) = origin + x*t); a circle's or ellipse's angle in radians about origin in the x, y plane (point(t) = origin + x*radius*cos(t) + y*radius2*sin(t), radius2 = radius for a circle); a parabola's own parameter with radius its focal length F (point(t) = origin + x*F*t*t + y*2*F*t, radius2 0); a hyperbola's with radius, radius2 its semi-axes a, b (point(t) = origin + x*a*cosh(t) + y*b*sinh(t)); a NURBS's knot parameter (knots[degree] <= t0 < t1 <= knots[n]). Frame vectors x, y, z are unit for conics; for a line x is the direction with length = the line's length and y, z are zero. Always t0 < t1: an edge whose segments run against its curve's own parameter reports the same range — read the direction from Edge::segments, not from the range. A chain's Chain::points always run WITH the curve's own parameter instead: point(t0) is the chain's first point and point(t1) its last for an open chain (a closed chain simply goes once round, t0..t1 its whole domain).

For a NURBS the frame is zero and so are the radii; for a conic or a line degree is 0 and knots, poles are empty. Evaluating point(t) is left to the caller for now.

Curve::kind

std::string kind

The curve: line, circle, ellipse, parabola, hyperbola or nurbs. Never other: a curve of that kind is std::nullopt instead.

Curve::origin

Vec3 origin

The frame's origin, three numbers: a circle's or ellipse's centre, a parabola's vertex, a hyperbola's centre, a line's start. Zero for a NURBS.

Curve::x

Vec3 x

The frame's first axis: a conic's unit direction at angle 0 (a parabola's or hyperbola's axis); for a line the whole to - from, so origin + x is its far end. Zero for a NURBS.

Curve::y

Vec3 y

The frame's second axis: a conic's unit direction at a quarter turn. Zero for a line or a NURBS.

Curve::z

Vec3 z

The frame's normal, x cross y, unit for a conic. Zero for a line or a NURBS.

Curve::radius

double radius

A circle's radius, an ellipse's along x, a parabola's focal length, a hyperbola's semi-axis along x; 0 for a line or a NURBS.

Curve::radius2

double radius2

An ellipse's radius along y, a hyperbola's semi-axis along y; equal to radius for a circle; 0 for a parabola, a line or a NURBS.

Curve::t0

double t0

Where the edge starts on the curve, in the range convention above.

Curve::t1

double t1

Where it ends: greater than t0. A whole circle is one edge, t1 - t0 a full turn. On a conic t1 may exceed 2pi when the arc crosses angle 0 (say t0 5.6, t1 7.0): keep the angles as reported and never normalise them into 0..2pi, or the arc is drawn the long way round.

Curve::degree

std::uint32_t degree

A NURBS's degree; 0 for a conic or a line.

Curve::knots

std::vector<double> knots

A NURBS's knot vector, poles + degree + 1 long; empty for a conic or a line.

Curve::poles

std::vector<Vec3> poles

A NURBS's control points, three numbers each; empty for a conic or a line.

Curve::weights

std::optional<std::vector<double>> weights

One weight per pole for a rational NURBS; std::nullopt for a plain (non-rational) B-spline, a conic or a line.

Intersection

struct Intersection

What Solid::intersect() found, copied out: chains, one Chain per face pair per branch of the crossing, and overlaps, one Overlap per coincident face pair sharing a region. Both empty where the solids do not meet.

Intersection::chains

std::vector<Chain> chains

Every crossing found, as Chain values.

Intersection::overlaps

std::vector<Overlap> overlaps

Every coincident face pair sharing a region, as Overlap values.

Chain

struct Chain

One branch of one face pair's crossing, as plain data: one of Intersection::chains. Every point is within tolerance of both faces' exact surfaces. There is one chain per face pair per branch — a chain does not cross a face boundary or a closed curve's own seam, so a crossing that runs past either comes back as more than one chain, and a caller joins them by matching ends. closed when the chain's own ends meet.

Chain::points

std::vector<Vec3> points

The points along the crossing, in walk order; a closed chain does not repeat its first point.

Chain::closed

bool closed

Whether the chain's own ends meet.

Chain::face_a

std::uint32_t face_a
std::uint32_t face_b

The two faces that cross: the index in the first solid Solid::intersect() was called on, then the index in the second.

Chain::tangent

bool tangent

Whether the two surfaces are near-tangent along the chain, or the snap did not settle within tolerance — the points are then the best estimate found, not exact to the tolerance everywhere. A closed chain that does not go once round its own curve (a sliver of mesh noise where two surfaces barely cross) is reported with no curve at all, tangent still true.

Chain::curve

std::optional<Curve> curve

The crossing's exact curve, as a Curve over the chain's own t0..t1 — std::nullopt where no curve fits every point of the chain within tolerance (tangent noise, or a crossing with no closed form).

Overlap

struct Overlap

A face of the first solid and a face of the second that coincide — lie on one surface and share a region — as plain data: one of Intersection::overlaps. A face pair sharing only an edge or a rim (no shared region: two pipes end to end, two boxes flush on a side) is silent — neither an overlap nor a chain. loops may come back empty for a partial overlap whose own outline crosses the other face's: that is a valid, meaningful answer, not an error.

Overlap::face_a

std::uint32_t face_a
std::uint32_t face_b

The two coincident faces: the index in the first solid Solid::intersect() was called on, then the index in the second.

Overlap::loops

std::vector<std::vector<Vec3>> loops

The shared region's boundary: closed rings of points on the surface, outer loop before holes, each ring's first point not repeated.

Mass

struct Mass

What a body measures, per unit density: what Solid::mass() returns. Every figure carries a bound on its own error, because a measurement without one cannot be compared with anybody else's — including another kernel's. Measured on the exact surfaces where a face has one and on a mesh where it has not, which Mass::from_mesh reports.

Mass::closed

bool closed

The body is topologically closed. Only then do the volume, the centroid and the inertia mean anything; they are not numbers at all otherwise.

Mass::inverted

bool inverted

Every face pointed inward. The figures are given as if they had not.

Mass::accuracy_met

bool accuracy_met

Every error came under the accuracy asked for. false where the refinement gave up first — the values and their errors are still honest, simply less certain.

Mass::from_mesh

bool from_mesh

Measured on a mesh rather than on exact surfaces: exact for that mesh, and its errors are then the rounding floor rather than a statement about the surface.

Mass.of

Measure solid at accuracy, which is the same call as Solid::mass() written the other way round: Mass.of(solid) where the body reads better as the object of the sentence.

Not in C++: solid.mass() is the only spelling, and a static taking a Solid would have to be declared after it.

Mass::weight()

double weight(double density) const

density * volume, in whatever units the model and the density agree on. Per unit density is what everything here is, because an assembly of two materials would have to undo a density applied on its behalf — so Mass.of(part).mass(7850) is a steel part in kilograms where the model is in metres, and the inertia scales the same way.

Spelled weight here: a member named mass on a type named Mass reads as a repetition, and this is the one figure that is not per unit density.

Mass::area

double area

The surface area.

Mass::area_error

double area_error

A bound on area's own error.

Mass::volume

double volume

The volume enclosed, where Mass::closed.

Mass::volume_error

double volume_error

A bound on volume's own error.

Mass::centroid

Vec3 centroid

The volume's centroid, where Mass::closed.

Mass::centroid_error

double centroid_error

A bound on the distance from centroid to the true centroid.

Mass::inertia

std::array<Vec3, 3> inertia

The inertia tensor about the centroid, row-major and symmetric, products of inertia negated (inertia[0][1] is minus the integral of (x - gx)(y - gy)).

Mass::inertia_error

double inertia_error

A bound on every entry of inertia.

Mass::principal_moments

Vec3 principal_moments

The tensor's eigenvalues, ascending.

Mass::principal_axes

std::array<Vec3, 3> principal_axes

Their axes, one unit vector each, in the same order.

Box

struct Box

An axis-aligned box: lo and hi, three numbers each. The broad phase's own shape (Solid::bounds_box(), Collider's own boxes): cheap to move and to test against another before anything is meshed.

Box::lo

Vec3 lo

The box's low corner.

Box::hi

Vec3 hi

The box's high corner.

Box::moved()

Result<Box> moved(const Frame& frame) const

This box moved by frame (rigid), boxed again on the world axes — not this box's corners individually moved, which would not in general be a box at all: the *bounding* box of the moved corners. A frame not twelve finite numbers or not rigid returns an Error in its Result.

Box::overlaps()

bool overlaps(const Box& other) const

Whether this box and other share a point (touching counts).

ColliderStats

struct ColliderStats

Counts of one Collider::check() call, as Collider::stats() returns them (all zero before the first call): the candidate pairs considered, how many of them the world boxes and the oriented boxes cleared, how many the share left to another call, how the rest were answered (cached, closed_form, meshed), and how many were refused. confirmed is the subset of meshed that were thin mesh-path overlaps sent to Solid::common() itself for a final answer.

ColliderStats::considered

std::uint32_t considered

The candidate pairs looked at: every pair of parts in different groups, less any in allowed or skipped_share.

ColliderStats::world_boxes

std::uint32_t world_boxes

Of those, how many two world boxes (Box::overlaps()) cleared at once — decided apart with no oriented test.

ColliderStats::oriented_boxes

std::uint32_t oriented_boxes

Of the rest, how many cadaclysm::blacksmith::boxes_apart() cleared — decided apart on the parts' own oriented boxes.

ColliderStats::skipped_share

std::uint32_t skipped_share

Pairs this call's share left to another call, not decided here at all.

ColliderStats::cached

std::uint32_t cached

Pairs answered from an earlier pose's cache rather than worked afresh.

ColliderStats::closed_form

std::uint32_t closed_form

Pairs decided by an exact formula (both parts a shape the kernel has one for), no mesh built.

ColliderStats::meshed

std::uint32_t meshed

Pairs that needed a mesh-against-mesh test to decide.

ColliderStats::refused

std::uint32_t refused

Pairs that could not be decided at all — each a CollisionHit with kind "refused".

ColliderStats::confirmed

std::uint32_t confirmed

Of meshed, how many thin mesh-path overlaps were sent on to Solid::common() itself for a final answer.

CollisionHit

class CollisionHit

One pair Collider::check() found overlapping (kind "overlap"), or could not decide (kind "refused", text says why), as plain data. positions/normals are world xyz triples, three per triangle (unwelded); indices indexes them — all empty for a refused pair. depth is the thinnest side of the overlap's world box, 0 for a refused pair.

CollisionHit::solid() rebuilds the exact overlap now, from the frames Collider::check() found it at; this keeps the check call's own answer alive behind every hit of it until the last one is let go, freeing it only then.

CollisionHit::a

std::uint32_t a

The first part's index, as given to Collider.

CollisionHit::b

std::uint32_t b

The second part's index.

CollisionHit::kind

std::string kind

"overlap" or "refused".

CollisionHit::overlap

bool overlap

The same answer as kind, as a plain bool.

CollisionHit::depth

double depth

The thinnest side of the overlap's world box; 0 for a refused pair.

CollisionHit::text

std::string text

Why a refused pair could not be decided; empty for an overlap.

CollisionHit::positions

std::vector<Vec3> positions

The overlap solid's mesh positions, world xyz triples, three per triangle (unwelded); empty for a refused pair.

CollisionHit::normals

std::vector<Vec3> normals

The overlap solid's mesh normals, matching positions; empty for a refused pair.

CollisionHit::indices

std::vector<std::uint32_t> indices

Indices into positions/normals; empty for a refused pair.

CollisionHit::solid()

Result<Solid> solid() const

The exact overlap of this pair at the frames Collider::check() found it at, as a new Solid. returns an Error in its Result for a refused pair (kind "refused") or a check answer already closed some other way.

Collider

class Collider

Parts meshed once, asked pose after pose which of them interfere: solids at rest, meshed at tolerance (0.05 by default); groups one group index per solid (std::nullopt: each solid its own group — Collider::check() then takes one frame per solid, and refuses a groups of any other length); allowed pairs of part indices never tested (designed contacts, by part index, flat: (a0, b0, a1, b1, ...)). Close it when done, or let the language's scope do it; see Lifetimes.

Collider::group_count()

std::uint32_t group_count() const

How many groups this collider has — the highest group index past the constructor, plus one.

Collider::stats()

Result<ColliderStats> stats() const

Counts of the last Collider::check() call, as a ColliderStats (all zero before the first one).

Collider::check()

Result<std::vector<CollisionHit>> check(const std::vector<std::reference_wrapper<const Frame>>& frames, const std::optional<std::pair<std::uint32_t, std::uint32_t>>& share = std::nullopt) const

The pairs that interfere, or could not be decided, with each group placed at its matching frame in frames (one Frame, or twelve numbers, per group, in group order), as CollisionHit values. share: std::nullopt, every pair past the box filters is decided now; (k, n), only those whose index is k modulo n — the shares of one pose are disjoint and cover it, so a caller splits one pose's work across n workers and puts the answers back together. frames not the collider's own Collider::group_count(), a frame that is not twelve finite numbers or not rigid, or a share with k >= n, returns an Error in its Result.

Collider::close()

void close() noexcept

Free this handle now. The garbage collector, or the language's scope, does it otherwise.

Hit

struct Hit

One place two curves meet, as plain data, copied out: what Profile::hits() lists. A point has run false and start equal to end; a run has the two curves coinciding from start to end. A point at the join of two segments is reported once, on either: as segment k at t 1 or as segment k + 1 at t 0.

Hit::run

bool run

Whether the curves coincide along a stretch rather than meeting at a point.

Hit::touch

bool touch

For a point: the curves are tangent there rather than crossing. Where a side ends there, true if the two continue each other smoothly; an end resting on the other at an angle, or a corner, is not a touch.

Hit::start

Vec3 start

Where it starts, three numbers (z is 0 for two profiles).

Hit::end

Vec3 end

Where it ends: start again for a point.

Hit::a_start

Spot a_start

Where it starts on the first curve, as a Spot.

Hit::a_end

Spot a_end

Where it ends on the first curve.

Hit::b_start

Spot b_start

Where it starts on the second curve.

Hit::b_end

Spot b_end

Where it ends on the second curve.

Spot

struct Spot

Where a Hit lands on one side: a profile's loop, segment and position along it, or (from later calls) a solid's face and its surface parameters.

Spot::loop_index

std::uint32_t loop_index

The loop: 0 the boundary or the open chain, then the holes in the order they were added. NONE on a face.

Spot::segment

std::uint32_t segment

The segment of that loop, in drawing order. NONE on a face.

Spot::t

double t

How far along the segment, 0 at its start to 1 at its end: by length on a line, by angle on an arc, by parameter on a spline.

Spot::face

std::uint32_t face

The face, on a solid; NONE on a profile.

Spot::u

double u

The face's first surface parameter; 0 on a profile.

Spot::v

double v

The face's second surface parameter; 0 on a profile.

SolidHits

struct SolidHits

What Solid::hits() found, copied out: hits, every place profile pierces or grazes this solid's faces, as Hit values (a the profile, b the face: face, u, v), ordered along the profile; pieces, the profile's loops cut at the hits, as Piece values — empty unless the solid is closed.

SolidHits::hits

std::vector<Hit> hits

Every place profile meets a face, as Hit values.

SolidHits::pieces

std::vector<Piece> pieces

The profile's loops cut at the hits, as Piece values; empty for an open solid.

Piece

struct Piece

One stretch of Solid::hits()'s profile between two cuts, as plain data: one of SolidHits::pieces. A piece's spots follow the cut, not the hit that made it: a segment join reads as the next segment's start (k + 1, 0), not the hit's (k, 1), and a closed loop's closing join stays (0, 0); an open chain's end reads (n - 1, 1). A loop no hit cuts is one piece, geometrically closed, start and end both (0, 0).

Piece::inside

bool inside

Whether the piece is inside the solid, by its middle's winding number over the solid's mesh — a piece lying on the surface (a run, or a midpoint within tolerance of a face and inside it) counts as inside too, with no third state.

Piece::start

Spot start

Where the piece starts, as a Spot.

Piece::end

Spot end

Where the piece ends, as a Spot.

Piece::profile

Profile profile

The piece on its own, as an open Profile — unless the piece is a whole uncut loop, which comes back closed.

Manifold

using Manifold = cadaclysm::Manifold

What Solid::manifold() returns: whether the solid's faces make a manifold, and whether it is closed, told from its topology rather than a mesh.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.faces

How many faces.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.edges

How many distinct edges: one shared by two faces counts once.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.vertices

How many distinct vertices.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.boundary_edges

Edges only one face borders: a sheet's rim, a hole in a shell.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.non_manifold_edges

Edges three or more faces border: a fin, or two solids meeting along a line.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.non_manifold_vertices

Vertices whose faces make more than one fan: two solids touching at a corner.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.is_manifold

No non-manifold edge or vertex: a manifold, possibly with a boundary.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

Manifold.is_closed

A manifold with no boundary edge either: it encloses a solid.

blacksmith::Manifold is cadaclysm::Manifold: Solid::manifold returns the reader's struct.

FemMesh

class FemMesh

One solid meshed for a solver: nodes welded by bits, triangles wound outward, every node tagged with the lowest-dimension B-rep entity it lies on, and every crack reported rather than closed. What Solid::fem_mesh() returns, and owned by you: free it, or let the language's scope do it (see Lifetimes).

A handle rather than a snapshot, as a Solid is. In every wrapper that can lend a view, the big arrays are read-only views into the library's own memory, as Solid::mesh()'s are — a solver mesh is megabytes. Node and Godot copy instead, having no borrowed-view machinery at all, so for those two the array is theirs and everything below about a stale view falls away. Either way the owner of the memory is this object rather than the solid: Solid::close() does not free a FEM mesh, and meshing the solid again at another tolerance does not touch it either — a FEM mesh and a held tessellation (Solid::mesh()'s own) are each their own handle, neither one the solid's cache that a re-mesh replaces. Only FemMesh::free() does, or the language's own scope or collector reclaiming the handle.

So keep the FEM mesh itself reachable for as long as you read a view of it, and copy anything that must outlive it. What refuses a stale read is a different mechanism in each language, so all ten are named here rather than a rule with an "otherwise" in it. Swift's view type carries its owner and asks it before every element, so the read itself is refused — it traps inside the subscript getter, measured in a release build. Rust settles it before the program runs: its free takes the mesh by value, so a slice can neither be asked for after it nor held across it. Godot and Node hand back copies, so there is nothing of the library's left to go stale. Python, C#, Java and C++ check the handle in the accessor, so a view asked for after the free is refused and one already in hand is a bare array, span or buffer that reads freed memory — measured in C#, where a span taken before the free read the body's own coordinates out of the freed block. Python's numpy array does hold the mesh, which keeps it reachable (below), but nothing asks the mesh when the array is read, so one kept past an explicit free() dangles as C#'s span does. Go's and LuaJIT's arrays are fields filled when the mesh was built rather than accessors, so neither moment is refused there; their methods are, and for those two this is documented rather than enforced, as their own module headers already do for the scene.

Staying reachable is a second question, and a narrower one. In C#, Java, Go and LuaJIT the view is the platform's own array type with nowhere to hold a reference back to the handle, so a program that keeps a view and lets go of the mesh can have the collector free the handle under it with nothing ever calling FemMesh::free() — measured in C#, Java and Go. C++ has no collector but the same shape at scope end: the mesh's destructor frees the handle when it goes out of scope, and a Span kept past that reads freed memory. A view type that carries its owner is safe from that (Python's, Swift's), and so is a language that rules the case out — Rust borrows the mesh for the view's lifetime, Godot and Node copy. The same rule as Solid::mesh()'s own views.

It is freed where a Solid is closed: the same word the reader library's FEM mesh uses, so one FEM mesh is released the same way on both sides of the ABI.

FemMesh::nodes()

Span<const double> nodes() const

Every node's position, three doubles each: placed by Solid::fem_mesh()'s placement, in the solid's own coordinates otherwise.

The five arrays are Spans into the handle's own memory, and every accessor here checks the handle in both modes — not only under CADACLYSM_CHECKED, as the scene's and the solid's views do, because what it guards is a pointer handed to C, the way Meshlets' own check does, rather than a borrowed view's owner. So a call on a freed mesh — an array, a count, or either .msh call — goes to CADACLYSM_BAD_ACCESS rather than returning an Error, even in a release build. A Span already in hand is a pointer and a length from then on, and reading one after free() — or after the FemMesh itself went out of scope — reads freed memory in either mode: measured on a body away from the origin, it still gave that body's own coordinates until the block was reused. So hold the FemMesh while you read a span, and copy (std::vector<double>(mesh.nodes().begin(), mesh.nodes().end())) anything that must outlive it. FemEdge's nodes and runs borrow the same way.

FemMesh::triangles()

Span<const std::uint32_t> triangles() const

Three node indices a triangle, wound outward — a mirroring placement is wound back.

FemMesh::triangle_face()

Span<const std::uint32_t> triangle_face() const

Which face each triangle lies on, one per triangle: the same faces Solid::face_kind() names.

FemMesh::node_kind()

Span<const std::uint32_t> node_kind() const

What each node lies on — 0 a B-rep vertex, 1 an edge, 2 a face — one per node: the lowest-dimension entity it lies on, which is the .msh format's own classification rule. FemMesh::node_entity() says which entity of that kind.

FemMesh::node_entity()

Span<const std::uint32_t> node_entity() const

Which vertex, edge or face each node lies on, read by the matching FemMesh::node_kind(): an index into FemMesh::vertices(), into FemMesh::edges(), or into the solid's faces.

FemMesh::face_count()

std::uint32_t face_count() const

The solid's faces — the same faces Solid::faces() counts.

FemMesh::edges()

Result<std::vector<FemEdge>> edges() const

One FemEdge per B-rep edge, in the order a FemMesh::node_kind() of 1 indexes them. Not Solid::edges()' numbering: these are the manifold analysis's, ascending by edge id.

FemEdge

struct FemEdge
std::uint32_t id
Span<const std::uint32_t> nodes
Span<const std::uint32_t> runs
std::pair<std::uint32_t, std::uint32_t> faces
std::pair<std::uint32_t, std::uint32_t> ends
bool closed
bool seam
std::vector<Span<const std::uint32_t>> chains() const

One B-rep edge's chain of nodes, and where that chain breaks. nodes are this mesh's node indices in order along the edge, its end vertices included. runs says where the chain breaks: each entry is a start offset into nodes, and the nodes from one offset up to the next — the last offset running to the end — are a polyline of their own, with nothing joining across a boundary. The two ends either side of a boundary are two points of the edge with no mesh edge between them. (0,) is the ordinary answer, and a caller reading nodes as one polyline without looking here jumps the gap silently.

faces is (face_a, face_b) and ends is (end_a, end_b), the second of each a sentinel where there is none — an open sheet's rim, or both ends at one vertex. 0 is a real face and a real vertex, not a sentinel, and faces numbers the solid's faces as Solid::face_kind() does. closed where the nodes make one loop, never with more than one run; seam where one face bounds the edge twice, and both faces are then that same face.

id is the solid's own edge id, not this mesh's edge index: FemMesh::edges() is a densely renumbered subset of the solid's edges, with every edge collapsed to a point left out, so a sphere — whose two pole runs collapse — reports its seam as edge 0 with an id of 1. Everything else that names an edge means the index: a FemMesh::node_kind() of 1 read through FemMesh::node_entity(), the third number of a FemMesh::open_edges() or FemMesh::folded_edges() row, and the edge_<i> physical group of FemMesh::msh_text(). It is not a row of Solid::edges() either, that table being the solid's edges grouped by geometry; the id names the topological edge, which is what the .msh entities and the censuses speak in.

FemMesh::vertices()

Result<std::vector<FemVertex>> vertices() const

One FemVertex per B-rep vertex, in the order a FemMesh::node_kind() of 0 indexes them.

FemVertex

struct FemVertex
std::uint32_t node
Vec3 point
bool has_position

One B-rep vertex: node is the mesh node there, a sentinel where the mesh has none, which is ordinary rather than a fault — the analysis rebuilds a vertex wherever two trims meet, so a sphere has 48 of them where the mesh has two points, and a caller walking these skips the sentinel instead of reading it as a gap.

point is where the topology says the vertex is, in the same space and under the same placement as FemMesh::nodes(). Meaningless unless has_position: it is all zeros then, a point no geometry has and one a solver would read as a node at the origin.

FemMesh::open_edges()

Result<std::vector<std::array<std::uint32_t, 3>>> open_edges() const

Every crack, as (a, b, brep_edge): a directed mesh edge (a, b) with no (b, a), and the B-rep edge both nodes lie on where they share one.

Empty unless the solid's topology is closed, whose mesh is otherwise not asked about at all: an open sheet from Solid::face(), Solid::face_sheet(), Solid::drop_faces() or Solid::extrude_open() makes no claim to enclose anything, so its rim is not a crack. Such a solid reports FemMesh::watertight() false with this and FemMesh::folded_edges() both empty, and that trio of answers together is what says "not asked", not "nothing found".

That rule holds here without exception, where the reader library has one: every solid has a B-rep behind it, so this library has no mesh-only body whose census speaks from the triangles alone. See FemMesh::from_mesh().

FemMesh::folded_edges()

Result<std::vector<std::array<std::uint32_t, 3>>> folded_edges() const

Every fold, as FemMesh::open_edges() reports a crack: a directed mesh edge used by more than one triangle, once.

A solid can be folded without being open — one no thicker than a line leaves no hole for an open edge to find — and the closure census's own known-bad bodies are folds rather than open cracks. A caller that checks FemMesh::open_edges() alone calls such a solid sound. Empty under the same rule as FemMesh::open_edges().

FemMesh::crossings()

Result<std::vector<std::array<std::uint32_t, 2>>> crossings() const

Every pair of triangles that pass through each other, as (a, b) with a < b, sorted: an edge of one through the other's interior. Their B-rep faces are FemMesh::triangle_face() at a and b.

Computed for every solid, open or closed. FemMesh::watertight(), FemMesh::open_edges() and FemMesh::folded_edges() are topological and cannot see a surface passing through itself; a mesh is ready for a volume mesher when it is FemMesh::watertight() and this is empty — Gmsh refuses one that crosses. A copy.

FemMesh::watertight()

bool watertight() const

The topology is closed and the welded mesh is too — every directed mesh edge paired with its reverse and none used twice. false for every solid whose topology is not closed; read FemMesh::open_edges() for what an empty census beside a false here does and does not mean.

Ready for a volume mesher means this and an empty FemMesh::crossings(): this census is topological and cannot see a surface passing through itself.

FemMesh::from_mesh()

bool from_mesh() const

Always false here, and kept so the two ABIs hand back one struct: a Solid always has a B-rep, so this library has no mesh-only body to report.

The reader's Node::fem_mesh() sets it for a node with no B-rep, where it also says which space the mesh is in — here there is only one space, the solid's own under the placement — and where a true means the crack census spoke from the triangles alone rather than from a topology. That second difference cannot arise on this side.

FemMesh::min_angle()

double min_angle() const

The smallest interior angle of any triangle, in degrees. There is always one: a solid that meshed to no triangles is a refusal, not a mesh.

FemMesh::worst_triangle()

std::uint32_t worst_triangle() const

The triangle with that angle, as an index into FemMesh::triangles().

FemMesh::longest_edge()

double longest_edge() const

The longest triangle edge, placed. The figure to check against Solid::fem_mesh()'s max_size, and the only one that says what the mesh actually is. max_size bounds the boundary segments and merely targets the interior: measured at 1.03 x max_size on a face whose parameters run unevenly, where a full-size boundary piece met a much shorter one left by halving. One small enough beside the solid to reach the mesher's own piece and station ceilings is not honoured at all.

FemMesh::msh_text()

Result<std::string> msh_text() const

The mesh as Gmsh 4.1 ASCII .msh text: an entity per B-rep vertex, edge and face, a volume where the solid closes, and a physical group naming each.

Gmsh reads the file as written — measured with Gmsh 4.15: node and triangle counts equal to this mesh's, and a physical group per entity. To fill a closed solid with tetrahedra, give Gmsh a volume of its own over the file's surfaces. In its Python API: gmsh.open(path), then faces = [t for _, t in gmsh.model.getBoundary([(3, 1)], oriented=False)], gmsh.model.geo.addVolume([gmsh.model.geo.addSurfaceLoop(faces)]), gmsh.model.geo.synchronize() and gmsh.model.mesh.generate(3). A plain generate(3) leaves a volume read from any .msh file empty — Gmsh's own saved meshes too — and gmsh.model.mesh.createGeometry() re-meshes the surfaces rather than keeping these triangles.

Prefer Gmsh's HXT algorithm: gmsh.option.setNumber("Mesh.Algorithm3D", 10) before generate(3). On these surfaces the default left tetrahedra of exactly zero volume (four in a 100 mm bar), which no solver can use, and HXT's worst element came out far better on curved bodies (a minimum SICN of 0.1 to 0.3 where the default's was 0.001). Then name the tetrahedra before saving: HXT fills the new volume rather than the file's own, and gmsh.write keeps only elements in a physical group, so without this step every tetrahedron is dropped from the file without a word — filled = [t for _, t in gmsh.model.getEntities(3) if len(gmsh.model.mesh.getElementsByType(4, t)[0])], gmsh.model.removePhysicalGroups(gmsh.model.getPhysicalGroups(3)) and gmsh.model.addPhysicalGroup(3, filled, name="body"). The saved file also lists a few interior nodes no element uses — points Gmsh's refinement tried and gave up on — so a solver reading it should keep only the tetrahedra's own nodes: each unused one is an unconstrained unknown.

On this side of the ABI the library's text is owned and handed over, released by the wrapper (cadaclysm_blacksmith_string_free at the ABI) as every other text this library writes — Solid::step_text(), Solid::sat_text(), Solid::brep_text(). Two asks give two independent texts, and neither dies with the handle. The reader library's own msh_text is the other way round: it borrows a slot on its handle, replaced by the next call and gone with the mesh. A reader porting one side's reasoning onto the other leaks or double-frees.

The unlicensed notice is printed here, on this writer and on FemMesh::save_msh(), and not by Solid::fem_mesh(): meshing is not a licensed output and the .msh file is, which is where Solid::sat_text() and Solid::brep_text() put theirs too. The reader library notices in its constructor instead and on neither .msh call; each matches its own siblings, so moving the call to look like the other side breaks a convention.

It returns an Error in its Result for a mesh the writer refuses, naming the field it cannot honour, and for a freed handle.

A freed handle traps here rather than returning an Error. This call, save_msh and every array accessor check the handle first, and on a freed one that check reaches CADACLYSM_BAD_ACCESS (blacksmith::FemMesh::msh_text: the FEM mesh is freed) and does not return — in both modes, not only under CADACLYSM_CHECKED, because what it guards is a pointer about to be handed to C rather than a borrowed view's owner. Measured. So the Result above carries the writer's refusal and nothing else; a freed handle is a bug in the caller, as it is for Meshlets.

FemMesh::save_msh()

Result<void> save_msh(const std::string& path) const

FemMesh::msh_text() written to a path, replacing any file there. It returns an Error in its Result for a mesh the writer refuses or a file it cannot write, naming the path. Prints the unlicensed notice; see FemMesh::msh_text().

FemMesh::free()

void free() noexcept

Give the mesh back, and with it every view taken from it. Idempotent; the collector or the scope does it otherwise.

FemMesh::freed()

bool freed() const noexcept

Whether the mesh has been freed.

Tool

class Tool

A cutter: Grbl tool number (1 to 255, the T word), diameter and flute_length in mm, feed and plunge in mm/min, rpm its spindle speed, ramping into its cuts at ramp_angle degrees. Immutable; a job keeps its own copy.

Tool::flat()

static Result<Tool> flat(std::uint32_t number, double diameter, double flute_length, double feed, double plunge, double rpm, double ramp_angle = 3.0)

A flat end mill. A number outside 1..255, a size, feed or speed not positive and finite, or a ramp_angle outside (0, 90], returns an Error in its Result.

Tool::ball()

static Result<Tool> ball(std::uint32_t number, double diameter, double flute_length, double feed, double plunge, double rpm, double ramp_angle = 3.0)

A ball end mill, as flat but its tip a half sphere of the diameter; in 2.5D it cuts where a flat does, bottom being the tip.

Tool::drill()

static Result<Tool> drill(std::uint32_t number, double diameter, double flute_length, double feed, double rpm)

A drill, plunging at feed. A job takes it for Job::drill() only.

Tool::with_ramp_angle C++ only

Result<Tool> with_ramp_angle(double degrees) const

A new tool: this one, ramping into its cuts at degrees from the horizontal instead. Python sets the ramp when the tool is made (Tool::flat()'s and Tool::ball()'s ramp_angle), and those two call this themselves for an angle other than the library's own 3.0; it is public so a tool already in hand can be re-ramped without repeating its other arguments. Fails for an angle outside (0, 90].

Stock

class Stock

The block of material a job cuts, an axis-aligned box. Immutable; a job keeps its own copy.

Stock::box()

static Result<Stock> box(const Vec3& min, const Vec3& max)

The box from min (x, y, z) to max. Unless every min is below its max and all six are finite, returns an Error in its Result.

Stock::around()

static Result<Stock> around(const Solid& solid, double margin = 0.0)

solid's bounds grown by margin in -X, +X, -Y, +Y and +Z; the bottom stays at the solid's lowest Z. A margin negative or not finite returns an Error in its Result.

Program

struct Program

One file a job wrote: a Grbl program (tool its T number, name <job name>-T<n>, or <job name>-<k>-T<n> for a repeated tool) or, from Job::write_camotics(), a simulation copy or the CAMotics project (tool 0). Read-only.

Program::tool

std::uint32_t tool

The program's Grbl T number, or 0 for a CAMotics project file.

Program::name

std::string name

The program's name without extension (<name>-T<n>, or <name>-<k>-T<n> for a repeated tool) from Job::gcode(); the file name from Job::write_camotics().

Program::text

std::string text

The program's text.

Job

class Job

A machining job on a Stock: operations kept in the order they are added, written as one Grbl 1.1 program per run of operations sharing a tool. Mutable — each operation is added in place, and a refused one leaves the job as it was. safe_z is the absolute Z of rapids between operations and at a program's start and end, above the stock's top; an operation enters and leaves its passes clearance above its top; tolerance fits free-form sides and offsets; name names the programs (<name>-T<n>); every program waits spindle_dwell seconds after starting the spindle; resolution is the CAMotics project's voxel size in mm. A safe_z not above the stock, or a setting the job refuses, returns an Error in its Result.

Job::face()

Result<void> face(const Tool& tool, double z, double stepover, double stepdown)

Face the stock's top down to z, in zigzag rows along X stepover apart and passes at most stepdown deep. A drill, a z above the stock's top, a stepover past the tool's diameter, or what the job otherwise refuses, returns an Error in its Result.

Job::contour()

Result<void> contour(const Tool& tool, const Profile& profile, double top, double bottom, const std::string& side = "outside", double stepdown = 1.0, bool climb = true, bool open = false)

Cut along profile from top to bottom in passes at most stepdown deep: side "outside" the profile, "inside" it, or "on" its line; climb for climb milling, else conventional; open to cut the outline as an open chain (only "on" its line, and with no holes). Any other side raises ValueError; what the job otherwise refuses, returns an Error in its Result.

Job::pocket()

Result<void> pocket(const Tool& tool, const Profile& profile, double top, double bottom, double stepover, double stepdown)

Clear profile — its holes left standing as islands — from top to bottom, in rings stepover apart (at most the tool's radius) and passes at most stepdown deep. What the job refuses returns an Error in its Result; a pocket too small for the tool is refused by Job::gcode(), naming the operation.

Job::drill()

Result<void> drill(const Tool& tool, const std::vector<Vec2>& points, double top, double bottom, std::optional<double> peck = std::nullopt)

Drill a hole at each (x, y) of points from top to bottom, pecking peck deep at a time (retracting between pecks), or straight to depth for none. A point that is not two numbers raises ValueError; a tool that is not a drill, no points, or what the job otherwise refuses, returns an Error in its Result.

Job::gcode()

Result<std::vector<Program>> gcode() const

The job as Grbl 1.1 programs, one per run of consecutive operations sharing a tool (T1, T2, T1 gives three), each named <name>-T<n> (or <name>-<k>-T<n> for run k of a tool that runs more than once); an empty list for a job with no operations. An operation whose geometry the tool cannot cut (named by its index and kind) returns an Error in its Result.

Job::write_camotics()

Result<std::vector<std::string>> write_camotics(const std::string& path) const

A CAMotics project at path and its files beside it, named after the project's file stem (out/part.camotics writes out/part-T1.nc), not the job's name: each program, a simulation copy of it (.sim.nc, which the project lists), and the project last. Creates the directory. Returns the paths written, the project last. A path with no file stem, what Job::gcode() refuses, or a file that cannot be written, returns an Error in its Result.

Job::machine()

Result<Report> machine(const Solid& part, const Tool& end_mill, const std::vector<const Tool*>& drills = {}, std::optional<double> stepover = std::nullopt, std::optional<double> stepdown = std::nullopt, double skin = 0.3, std::optional<double> peck = std::nullopt)

Recognise part — a solid inside the job's stock — by Z-level sectioning, and append the operations that make it: one face (if the stock top is above the part's top), the pockets level by level from the top down, then one drill per drill tool and bottom. end_mill must be a flat or ball tool, not a drill; drills are tried against each hole found, the closest match within 0.05 mm winning. stepover defaults to 0.4 x diameter, stepdown to min(diameter / 2, flute_length) (std::nullopt asks for the kernel's own default for either); skin (at least 0, default 0.3) is how far above the part's lowest point the outline stops, so the part stays attached to the stock — 0 cuts through; peck is passed to every drill operation (std::nullopt for none). Returns a Report of what could not be cut — nothing that would gouge is ever emitted; a feature the tools can't make is machined as far as possible and reported instead. The job changes only on success: what the kernel otherwise refuses (a drill as end_mill, a drills entry that isn't a drill, a tool number clashing with one already in the job, part outside the stock, a skin negative or not less than the part's height, no level found, or a stepover or stepdown out of range) returns an Error in its Result and leaves the job exactly as it was.

Job::operations()

Result<std::vector<Operation>> operations() const

Every operation added to this job so far, in order — whether by Job::face(), Job::contour(), Job::pocket(), Job::drill() or Job::machine() — as a list of Operation.

Report

struct Report

What Job::machine() could not make: a read-only sequence of ReportItem, in the order found, read once when machine returns. len(report) counts the items, it iterates and indexes them, and str(report) is the kernel's own text — one line per item, or "nothing left uncut"; bool(report) is true when it has items.

Report item

struct ReportItem

One thing Job::machine() left uncut, one of Report's items. Read-only.

ReportItem::kind

std::string kind

"corner" (material the end mill doesn't reach), "hole" (a round hole no drill matches or a matched drill can't make), "slope" (a face that is neither horizontal nor vertical, cut as steps), "undercut" (material under an overhang) or "too_deep" (a level below the end mill's flute length, cut only that far).

ReportItem::at

Vec3 at

The item's XYZ location — a centroid for an area, the axis point at the top for a hole.

ReportItem::size

double size

An area in mm^2 for corner/undercut, a diameter in mm for hole, and a height in mm (its Z range) for slope and too_deep.

ReportItem::reason

std::string reason

One sentence naming the cause and the tool involved.

ReportItem::region

std::vector<Profile> region

The XY area the item covers, as a list of Profile: a corner's or undercut's piece, a hole's disc, the area a slope's face covers, or for too_deep the reachable area cut short (the whole stock when it is the part's top).

Operation

struct Operation

One operation of a Job, read back by Job::operations(). Read-only.

Operation::kind

std::string kind

"face", "contour", "pocket" or "drill".

Operation::tool

std::uint32_t tool

Its Grbl T number.

Operation::params

std::map<std::string, std::optional<double>> params

A dict of the operation's own arguments, in the kind-specific order it stores them — z, top, bottom, stepover, stepdown, side, climb, open, peck and the point count, with a profile summarised by its bounds. side reads as "outside"/"inside"/"on"; climb and open as a bool; points as an int; a NaN peck as std::nullopt; every other value stays a float.

A std::map<std::string, std::optional<double>> of the library's own numbers, sorted by name: side is 0 outside, 1 inside or 2 on its line; climb and open are 1 or 0; points is a count; a peck of none is std::nullopt.

Errors

using BuildError = Error

What the kernel refused, in its own words: a profile that crosses itself, a fillet too large for its faces, a boolean with nothing left. The call that failed returns it, as the Error in its Result.

Lifetimes

What borrows, what to close

Scene, Solid, Profile, Path and SweepPath are move-only and free themselves when destroyed; close() does it early. Every call that can fail returns a Result<T> — never an exception — and CADACLYSM_TRY(var, expr) carries the error up.

Node::mesh() hands back Spans over the scene's memory, valid until Scene::close(); copy() gives vectors of your own. Solid::mesh() instead returns a view that owns a tessellation of its own, built with the view and never replaced under it: nothing about it goes stale, whether the solid is meshed again at another tolerance or closed, so it carries no generation check either. A Span already in hand is a pointer and a length from then on, so hold the view while you read it, and copy() what must outlive the view itself. FemMesh is the other shape: a handle of your own, which Scene::close() neither frees nor stales, and whose accessors trap on a freed handle in both modes — that check guards a pointer handed to C, as Meshlets' does, not a view's owner.

Strings are always copied on the way out. A Scene may be read from several threads, and Scene::cancel() called from one while another runs Scene::realize_all(). A Solid caches its tessellation: do not share one between threads.