The geometry-introspection function family is a BCad-specific extension to the
OpenSCAD language. Combined with the object-expression model (a call in
expression position yields a node value — see docs/unified_callable.md),
these functions operate directly on geometry values: you can "store" the
result of a build into a variable and then inspect its geometric properties
(face count, volume, bounding box, BRep validity). They are the standard tool
for scripted tests (assert() + approximate comparison) and, with the new
syntax, also handy for practical layout work — see the recipes below.
Storing geometry in a variable
Every geometry call works in expression position — no capture() needed:
c = cube([1, 2, 3]);
echo(str("Volume = ", volume(c)));
The variable holds the geometry node (an SCLContext); all diagnostic
functions accept a node, a (possibly nested) list of nodes, or a raw
SCLShape.
- A single call / braceless chain → the node of the whole tree:
r = rotate(45) translate([10, 0, 0]) cube(5); - A block of several children → a list of the objects it produced, with no
grouping node: the compound appears only when the value is emitted into the
scene. Diagnostics flatten lists, so
volume(...)works directly:comp = { cube(5); translate([10, 0, 0]) cube(5); };(group() { ... }is still the way to get an explicit named node with fields, e.g.c = group() { size: 5; cube(size); }; c.size.) - A loop in expression position → a list with one element per iteration, each
element being that iteration's value:
parts = for (i = [1, 2]) { cube(i); } - One object → the object itself, two or more → a list. A one-element list is never produced by a block or a loop body.
- If the child has no geometry → the node's shape is empty (diagnostics return
undef).
Example:
t360 = linear_extrude(height=2, twist=360)
translate([20, 0, 0])
rotate([90, -90, 0])
square(2);
Diagnostic functions
All functions take a geometry argument (a node or a shape) and return a number or a vector:
| Function | Returns | Notes |
|---|---|---|
num_faces(geom) |
number | number of faces |
num_edges(geom) |
number | number of edges |
num_vertices(geom) |
number | number of vertices |
volume(geom) |
number | volume (VolumeProperties().Mass()) |
surface_area(geom) |
number | surface area |
centroid(geom) |
[x, y, z] |
center of mass |
bbox_min(geom) |
[x, y, z] |
bounding box min corner |
bbox_max(geom) |
[x, y, z] |
bounding box max corner |
is_valid(geom) |
true/false |
BRep validity (BRepCheck_Analyzer.IsValid()) |
Note on
is_valid:BRepCheck_Analyzervalidates topology strictly. Degenerate geometry (for example, extruding a profile that was rotated out of the XY plane — a "sheet" with zero thickness) is reported invalid, even though geometrically it is a legitimate 2D surface. This is expected behavior, not a bug.Note on
bbox_min/bbox_max: the box comes from OCCT'sBnd_Box, which deliberately grows every side byPrecision::Confusion()(1e-7) so the box is a guaranteed container — a point may lie a hair outside its ideal boundary due to numeric rounding, and the box must still contain it. Consequentlybbox_min(cube(1))returns[-1e-07, -1e-07, -1e-07]andbbox_maxreturns[1.0000001, 1.0000001, 1.0000001]— expected, not a bug. When comparing bbox values use a tolerance above 1e-7 (thecheck_approxdefaulteps=0.01is fine). The gap is symmetric: the center(min+max)/2is exact, while the sizemax-minis inflated by 2e-7 (negligible).Empty geometry: when the node has no shape —
x = group() { };, a failed or empty boolean operation — every function in the table above returnsundef(not an error).
Example:
cyl = cylinder(h=5, r=3, $fn=12);
echo(str("faces = ", num_faces(cyl))); // 14 (dodecagonal prism)
echo(str("valid = ", is_valid(cyl))); // true
echo(str("bbox = ", bbox_min(cyl), " .. ", bbox_max(cyl)));
Practical recipes
With geometry stored as a value, the diagnostic functions double as layout tools:
m = cube([10, 20, 30], center=true);
// размер объекта (завышен на 2e-7 — допуск бокса, см. примечание выше)
sz = bbox_max(m) - bbox_min(m); // ~[10, 20, 30]
// центр по bbox — точный (допуск симметричен и сокращается)
center = (bbox_max(m) + bbox_min(m)) / 2; // ~[0, 0, 0]
// пристыковать второй объект вплотную справа
a = cube([10, 10, 10]);
b = translate(bbox_max(a) + [0, 2, 0]) cube([10, 10, 10]);
// равномерно вписать объект в целевой размер по одному измерению
k = 20 / (bbox_max(a)[0] - bbox_min(a)[0]); // 2 — куб 10 -> 20
s = scale(k) a;
// перенести центр объекта в заданную точку
p = [0, 0, 0];
t = translate(p - (bbox_max(a) + bbox_min(a)) / 2) a;
assert(condition, message)
Available both as a module and as a function. When the condition is false, it aborts the script with the given message (visible as a Traceback in test mode).
sq = square([2, 2]);
assert(num_faces(sq) == 1, "a square must have 1 face");
Together with the diagnostic functions it forms the standard test template
(see the existing tests in tests/).
Library tests/lib/testing.scad
Included via use <lib/testing.scad>; provides an approximate-comparison helper:
function check_approx(a, b, eps=0.01) = ...
- Compares numbers and vectors with tolerance
eps(default0.01). check_approx(a, b)→truewhen|a - b| <= eps(vectors vianorm()).
Typical test:
```scl
use
m1 = cube([1, 1, 1]); assert(check_approx(bbox_min(m1), [0, 0, 0]), "bbox_min"); assert(check_approx(volume(m1), 1), "volume"); assert(is_valid(m1), "valid");
echo("--- OK ---"); ```
Running a test:
bcad-launcher --test tests/test_xxx.scad
The test is considered failed if the output contains a Traceback (from assert()).