Introspection & measurement
These tools read the current scene without modifying it: validating geometry, querying topology, computing physical properties, measuring distances and surface deviations, and inspecting assembly hierarchies. Reach for them after execute_script builds a body, or any time you need ground-truth numbers before making a design decision.
Tools
validate_geometry · compute_metrics · query_topology · measure_distance · measure_deviation · measure_vertex_fit · deviation_histogram · cross_section_compare · symmetric_difference_volume · recognize_features · inspect_assembly
Signed / spatially-resolved surface comparison — the certify-a-reconstruction toolset (#61–#63, #66, #70) — also renders to PNG via signed_deviation_heatmap and overlay_render.
validate_geometry
Per-body topology validation against OCCTSwift’s TopologyGraph.validate().
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
bodyId | string | no | Specific body to validate. If omitted, validates every BREP body in the scene. |
Returns — JSON object with a per-body validity report: valid boolean and a list of any violations found (open shells, bad orientation, degenerate edges, etc.). Returns an error string if the body is not found.
Example
// tool call arguments
{ "bodyId": "housing" }
// example result
{ "bodyId": "housing", "valid": true, "issues": [] }
Drives — GraphIO + TopologyGraph.validate() in-process (no subprocess).
compute_metrics
Compute volume, surface area, center of mass, bounding box, and/or principal axes for a scene body.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
bodyId | string | yes | Body to measure. |
metrics | string[] | no | Subset to compute. Default: all except boundingBoxOptimal. Items: volume, surfaceArea, centerOfMass, boundingBox, boundingBoxOptimal, principalAxes. boundingBoxOptimal (tight AddOptimal extent) is opt-in — list it explicitly. |
Returns — JSON object keyed by requested metric name. boundingBox and boundingBoxOptimal each return { min: [x,y,z], max: [x,y,z] }. centerOfMass returns [x,y,z]. principalAxes returns three orthogonal unit vectors. Returns an error string if the body is not found.
Example
// tool call arguments
{ "bodyId": "part", "metrics": ["volume", "surfaceArea", "boundingBoxOptimal"] }
// example result
{
"volume": 5890.3,
"surfaceArea": 2104.7,
"boundingBoxOptimal": { "min": [0.0, 0.0, 0.0], "max": [25.0, 20.0, 15.0] }
}
Notes — boundingBox uses Bnd_Box and over-reports extents for curved B-spline faces (it encloses the control-point hull, not the actual surface). Use boundingBoxOptimal (BRepBndLib::AddOptimal) when you need the tight envelope, at a small extra compute cost. boundingBoxOptimal is intentionally excluded from the default-all set.
Drives — direct OCCTSwift property calls (no occtkit subprocess).
query_topology
Find faces, edges, or vertices on a body matching optional criteria. Returns stable index-based IDs (face[N], edge[N], vertex[N]) that can be passed to selection tools.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
bodyId | string | yes | Body to query. |
entity | string (enum) | yes | Entity kind: face, edge, or vertex. |
filter | object | no | Optional filter: surfaceType, curveType, minArea, maxArea. |
limit | integer (≥1) | no | Maximum number of results to return. |
Returns — Array of matching topology entries, each with its stable ID, geometric properties (type, area/length as applicable), and centroid. Edge entries (#119) also carry endpoints ([start, end], every edge kind) plus a unit direction for LINE edges, and circleCenter/radius/axis/startAngle/endAngle (radians, measured from the circle’s own xAxis) for CIRCULAR edges. Returns an empty array when no entities match the filter.
Example
// tool call arguments
{ "bodyId": "bracket", "entity": "face", "filter": { "surfaceType": "plane" }, "limit": 10 }
// example result
[
{ "id": "face[0]", "surfaceType": "plane", "area": 400.0, "centroid": [0.0, 0.0, 10.0] },
{ "id": "face[2]", "surfaceType": "plane", "area": 400.0, "centroid": [0.0, 0.0, -10.0] }
]
Notes — The returned IDs can be passed directly to select_topology to mint a selectionId for use in remap and annotation workflows.
measure_distance
Minimum distance between two scene bodies. Returns ≈0 if the bodies overlap or touch.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | First body. |
toBodyId | string | yes | Second body. |
computeContacts | boolean | no | Also return up to 32 contact pairs (closest point pairs). |
Returns — JSON object with distance (minimum gap in model units). If computeContacts is true, also includes contacts: array of up to 32 pairs, each with pointOnFrom and pointOnTo coordinates.
Example
// tool call arguments
{ "fromBodyId": "shaft", "toBodyId": "bearing", "computeContacts": true }
// example result
{
"distance": 0.05,
"contacts": [
{ "pointOnFrom": [12.5, 0.0, 30.0], "pointOnTo": [12.55, 0.0, 30.0] }
]
}
Notes — This is the minimum gap metric — not surface deviation. For comparing a reconstruction against a reference mesh use measure_deviation instead. A result of ≈0 means bodies are touching or penetrating; it does not indicate the amount of overlap.
measure_deviation
Signed, spatially-resolved surface deviation between two scene bodies — the primary metric for certifying a reconstruction against its source mesh. As of #62 the report is a full vector (not just a min gap), with an optional per-station sweep along an axis.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | Source body (e.g. the reconstruction). |
toBodyId | string | yes | Reference body (e.g. the input mesh). |
deflection | number | no | Mesh linear deflection in model units. Smaller = finer tessellation = tighter bound. Default: 0.5% of the from-body bbox diagonal. |
maxSamples | integer | no | Max source surface samples per direction (stride-subsampled). Default 20000. |
sectionAxis | number[3] | no | [x,y,z] axis to bin the forward samples along. With sections, adds a per-station signedMean sweep. |
sections | integer | no | Number of along-axis bins for the per-section sweep (≥2). Requires sectionAxis. |
signMode | string | no | robust (default) or nearest — see Which way is out?. |
Returns — JSON object:
fromToTo/toToFrom— directed deviation each way:{ max, rms, mean, p95, signedMean, signedMin, signedMax, worstPoint, samples, signedSamples, ambiguousSamples, ambiguousFraction }.signedMean ≠ 0reveals a systematic proud(+) / shy(−) bias a Hausdorff hides;fromToTocatches over-extension,toToFromunder-coverage. The signed trio isnullwhen no sample had a trustworthy sign (signedSamples: 0) — read that as unavailable, never as no bias.symmetricHausdorff— the single worst-case nearest-surface distance in either direction. Unaffected bysignMode.signMode— which correspondence rule ran.sections(optional) — whensectionAxis+sectionsgiven, an array of{ offset, signedMean, rms, samples }per station; a near-constant non-zerosignedMeanacross stations is the systematic section-error fingerprint. Sign-ambiguous samples are excluded from a station’s figures but still define the axis span, sooffsetalways measures from the body’s minimum projection.
All distances are in model units. Sign convention: + proud (from outside the reference), − shy.
Example
// tool call arguments
{ "fromBodyId": "recon", "toBodyId": "source_mesh", "deflection": 0.1, "sectionAxis": [0,0,1], "sections": 6 }
// example result (abridged)
{
"fromToTo": { "max": 0.18, "rms": 0.06, "mean": 0.04, "p95": 0.15, "signedMean": -0.03, "signedMin": -0.18, "signedMax": 0.05, "worstPoint": [42.1, 7.3, 0.0], "samples": 17230, "signedSamples": 17230, "ambiguousSamples": 0, "ambiguousFraction": 0.0 },
"toToFrom": { "max": 0.22, "rms": 0.08, "mean": 0.05, "p95": 0.19, "signedMean": 0.02, "signedMin": -0.06, "signedMax": 0.22, "worstPoint": [41.9, 7.1, 0.0], "samples": 17230, "signedSamples": 17230, "ambiguousSamples": 0, "ambiguousFraction": 0.0 },
"symmetricHausdorff": 0.22,
"signMode": "robust",
"sections": [ { "offset": 5.0, "signedMean": -0.03, "rms": 0.05, "samples": 2900 } ]
}
Notes — Unlike measure_distance (minimum gap, ≈0 for overlapping bodies), this samples each body’s tessellated surface. Fidelity scales with deflection. Import the reference mesh with import_file(format: "stl") or load an invalid in-progress reconstruction with read_brep(allowInvalid: true) before calling.
measure_vertex_fit
Exact per-vertex distance table from fromBodyId’s own vertices to toBodyId’s real BRep geometry (#118). Every scene body is stored as BRep (an STL import is a facet shell, one planar face per triangle), so fromBodyId’s own vertices ARE its mesh corner points; each is measured via exact BRepExtrema (no re-tessellation), with the nearest entity kind on toBodyId classified per vertex. Swift-only.
Server: Swift
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | Body whose own vertices are measured (typically a mesh/STL import). Must differ from toBodyId. |
toBodyId | string | yes | Body each vertex is measured against (typically a BRep solid, e.g. a reconstruction). |
maxVertices | integer (≥1) | no | Cap on vertices measured (stride-subsampled if fromBodyId has more). Default 2000. |
worstN | integer (≥0) | no | Worst-N vertices (by distance, largest-first) included in the response. Default 20. |
includeAllVertices | boolean | no | Return every sampled vertex’s entry, not just the worst-N. Default false. |
Returns: JSON object: vertexCount (total vertices on fromBodyId), sampledCount, stride, mean/rms/max/p95 (all sampled distances), worst (array of { index, point, distance, nearestKind }, largest-first), vertices (same shape, every sampled entry, only when includeAllVertices: true), and warnings.
Example
// tool call arguments
{ "fromBodyId": "scan_mesh", "toBodyId": "recon", "worstN": 5 }
// example result (abridged)
{
"vertexCount": 812, "sampledCount": 812, "stride": 1,
"mean": 0.021, "rms": 0.034, "max": 0.211, "p95": 0.098,
"worst": [
{ "index": 403, "point": [12.4, -3.1, 8.0], "distance": 0.211, "nearestKind": "edge" }
],
"warnings": []
}
Notes: the instrument neither measure_distance (body-to-body, capped at 32 pairs), measure_deviation (mesh-to-mesh, approximate re-tessellation, no raw per-vertex table), nor find_correspondences (only matches topological vertices, a handful on a typical solid) provides. Entity INDEX (which specific face/edge) isn’t resolved, only the kind (vertex/edge/face): resolving the index would multiply the per-vertex BRepExtrema cost by the target’s face/edge count for a “nice to have.” The tool spends its second (expensive) BRepExtrema call only on entries that reach the response, not every sampled vertex.
deviation_histogram
Signed point-to-surface deviation distribution between two bodies — the statistical companion to measure_deviation, with an optional histogram PNG (#62). Swift-only.
Server: Swift
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | Candidate body. |
referenceBodyId | string | yes | Reference body. |
deflection | number | no | Mesh linear deflection. Default 0.5% of the from-body bbox diagonal. |
tolerance | number | no | If set, the report includes withinTolerance (fraction of samples within tolerance of the nearest reference surface). |
signMode | string | no | robust (default) or nearest — see Which way is out?. |
outputPath | string | no | Write a histogram PNG here. Omit for numbers only. |
Returns — { mean, std, median, p95, signedMin, signedMax, maxAbs, withinTolerance?, buckets: [{ lo, hi, count }], samples, signedSamples, ambiguousSamples, ambiguousFraction, signMode }. Sign convention: + proud, − shy.
The distribution — mean / std / median / signedMin / signedMax / buckets — is built from sign-reliable samples only, because one flipped sign plants a mirror hump at −d and reads as exactly the bimodal systematic error this tool exists to spot. They come back null (and buckets empty, and no PNG) when nothing had a trustworthy sign. p95 / maxAbs / withinTolerance are magnitudes to the nearest surface and keep every sample, so maxAbs can be smaller than |signedMin| against an open thin-walled reference — see below.
Which way is out? (signMode)
measure_deviation, deviation_histogram and signed_deviation_heatmap share one signed-distance engine, so they share a signMode knob.
A deviation’s sign depends on which reference triangle a sample is judged against, and against an open, thin-walled reference (a raw scan / STL skin) the nearest one is often the wrong one. A candidate flank sitting 4.5 mm inside a 2 mm wall is only 2.5 mm from that wall’s inner surface, so that surface wins on proximity and — its outward normal facing the cavity — reports +2.5 proud for a part that is 4.5 shy. Wrong side, wrong magnitude, and nothing ties, so no coin-flip check catches it (#72).
| mode | behaviour |
|---|---|
robust (default, v1.17.0+) | Rejects reference triangles whose outward normal opposes the sample’s own before the nearest survivor wins, recovering both the side and the magnitude. Samples with no compatible surface in reach are reported ambiguous and withheld from the signed statistics rather than guessed. |
nearest | The nearest triangle wins, whatever it is — the pre-v1.17 rule. Correct against a watertight / single-surface reference. |
Two families of number, and signMode moves only one:
| family | measures to | moved by signMode? |
|---|---|---|
max / rms / mean / p95 / worstPoint / symmetricHausdorff / maxAbs / withinTolerance | the nearest reference surface | no — same meaning as pre-v1.17 |
signedMean / signedMin / signedMax / sections / histogram buckets / heatmap colours | the surface the sample corresponds to | yes |
Against a watertight reference these are the same surface and the two families agree. Against an open thin-walled one they diverge on purpose: max: 2.5 next to signedMin: -4.5 says the nearest reference geometry is an inner wall 2.5 away while the skin that flank belongs to is 4.5 above it. Both true. A gap between them is itself the tell that the reference is thin-walled.
An ambiguousFraction near 1.0 means the reference’s winding is likely inverted relative to the sampled body; the signed figures then come back null rather than a zero that would read as perfectly centred.
cross_section_compare
Slice both bodies at N stations across their shared axis-extent overlap and compare the 2D profiles — the highest-leverage detector of a reconstruction whose cross-section is the wrong shape everywhere yet whose 3D mean looks fine (#61, #66, #70). Swift-only.
Server: Swift
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | Candidate body (e.g. the reconstruction). |
referenceBodyId | string | yes | Reference body (e.g. the source mesh). |
axis | number[3] | yes | [x,y,z] section sweep axis (e.g. the carbody longitudinal axis). |
stations | integer | no | Number of evenly-spaced cut planes across the shared overlap. Default 12. |
through | number[3] | no | A point the axis passes through. Default: from-body bbox centre. |
deflection | number | no | Mesh linear deflection. Default 0.5% of the from-body bbox diagonal. |
outerEnvelope | boolean | no | Compare against the reference’s outer boundary per angular direction (default true) so inner window-return / frame paths of a thin-wall or scanned part don’t pollute the metric. false = raw point-to-main-loop. |
outputDir | string | no | Directory for per-station overlay PNGs. Omit for numbers only. |
imagePrefix | string | no | Filename prefix for station PNGs. Default "section". |
Returns — a report with overlap ([lo,hi] shared axis extent), referenceMode ("envelope" | "profile"), meanSignedAcrossSections, maxAbsSignedSection, worstStation / worstAxisCoord, warnings[], and a sections[] array. Each section carries station, axisCoord (world position along the axis), offset (overlap-relative), signedMean / rms / maxAbs, centroidOffset, areaRatio, shapeL2 (pose-invariant shape scalar — defined for open profiles too), fromContours / referenceContours / fromOpenPaths / referenceOpenPaths, openProfile, registrationSmell, and imagePath. |
Notes — Handles open-shell references (raw scan / STL skin) whose sections are open arcs. registrationSmell flags a station that sliced only one body (mis-registration / differing extents). Pair with import_file(format: "stl") to get the reference mesh into the scene.
symmetric_difference_volume
The two ONE-SIDED volumes between a candidate and a reference body, and their sum: the direct geometric fidelity figure a mean/RMS surface deviation can hide via cancellation (a part undersized in one place and oversized in another reads better on a mean). boolean_op cannot produce a true symmetric-difference solid against a mesh-only, possibly non-watertight reference (empirically: subtract fails both directions on a real STL reference); this instead classifies sample points against both bodies via OCCTSwiftMesh.Mesh.windingNumber(at:), the generalized winding number, which stays a well-defined real number on open/self-intersecting meshes rather than the undefined result a parity/ray test gives (#122). Swift-only.
Server: Swift
Parameters
| name | type | required | description |
|---|---|---|---|
fromBodyId | string | yes | Candidate body (e.g. the reconstruction). |
referenceBodyId | string | yes | Reference body (e.g. the source mesh); may be a non-watertight mesh. |
deflection | number | no | Mesh linear deflection. Default 0.5% of the from-body bbox diagonal. |
maxSamples | integer | no | Monte Carlo sample points (deterministic Halton low-discrepancy sequence) tested against both meshes. Default 300. |
Returns: { samples, ambiguousSamples, ambiguousFraction, boundingBoxVolumeMm3, fromVolumeMm3, referenceVolumeMm3, intersectionVolumeMm3, unionVolumeMm3, fromOnlyVolumeMm3, referenceOnlyVolumeMm3, symmetricDifferenceVolumeMm3, symmetricDifferenceFraction, estimatedStdErrMm3, fromExactVolumeMm3, referenceExactVolumeMm3, fromWatertight, referenceWatertight, reliable, warnings }.
fromOnlyVolumeMm3: excess / over-build material (inside the candidate, not the reference).referenceOnlyVolumeMm3: missing / under-build material (inside the reference, not the candidate).symmetricDifferenceVolumeMm3: the sum of the two;symmetricDifferenceFractionis that divided by the union volume (1 minus IoU: 0 = identical, 1 = disjoint),nullwhen the union itself sampled to ~0.estimatedStdErrMm3: Monte Carlo standard error onsymmetricDifferenceVolumeMm3. A measured value under ~2x this is noise-dominated at the currentmaxSamples, not necessarily a well-registered pair; raisemaxSamplesto resolve it.fromExactVolumeMm3/referenceExactVolumeMm3: exact BREP mass-properties volume where computable, reported as a cross-check only (silently unreliable for a non-closed shape, which is exactly the reference-body case this tool exists to handle).reliable:falsewhenambiguousFractionexceeds 15% (the winding-number classification itself was too uncertain at this sample count), or when a body’s sampled volume disagrees with its own exact BREP volume by more than sampling noise can explain.
Example
// tool call arguments
{ "fromBodyId": "recon", "referenceBodyId": "source_mesh", "maxSamples": 2000 }
// example result (abridged)
{
"fromVolumeMm3": 58900.3, "referenceVolumeMm3": 58750.1,
"fromOnlyVolumeMm3": 210.4, "referenceOnlyVolumeMm3": 60.2,
"symmetricDifferenceVolumeMm3": 270.6, "symmetricDifferenceFraction": 0.0046,
"estimatedStdErrMm3": 38.1, "reliable": true, "warnings": []
}
Notes: Cost is O(fromTriangles + referenceTriangles) per sample point, so total cost scales with maxSamples × (fromTriangles + referenceTriangles): windingNumber has no spatial acceleration upstream (fine at the diagnostic sample counts this tool targets, not fine at the 5-digit maxSamples values measure_deviation defaults to). Keep maxSamples modest for scan-scale meshes; raise it only when estimatedStdErrMm3 says the current sample count can’t resolve the difference you’re looking for. Classification is orientation-agnostic (a point classifies as inside when the winding number is close to any nonzero integer, not specifically +1), so an inverted-winding reference still reports correctly without a separate orientation pre-pass. fromVolumeMm3/referenceVolumeMm3 are each estimated from their OWN body’s confidently-classified samples only, independent of the other body’s mesh quality; the joint figures (intersectionVolumeMm3, fromOnlyVolumeMm3, referenceOnlyVolumeMm3, symmetricDifferenceVolumeMm3) need both bodies confident at the same sample point and extrapolate from that jointly-confident subset when some samples are ambiguous.
recognize_features
Detect pockets and holes via OCCTSwift’s Attributed Adjacency Graph (AAG) heuristics.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
bodyId | string | yes | Body to analyse. |
kinds | string[] (enum) | no | Feature kinds to detect: pocket, hole. Default: both. |
Returns — JSON object with a features array. Each entry includes the feature kind, the face indices that make up the feature, and geometric properties (e.g. diameter for holes, depth for pockets).
Example
// tool call arguments
{ "bodyId": "flange", "kinds": ["hole"] }
// example result
{
"features": [
{ "kind": "hole", "faces": ["face[4]", "face[5]"], "diameter": 6.0, "depth": 12.0 },
{ "kind": "hole", "faces": ["face[8]", "face[9]"], "diameter": 6.0, "depth": 12.0 }
]
}
Notes — For the full graph-level feature recognition pipeline (B-Rep graph output, featureNodeIds) see feature_recognize in the Topology graph family. This tool returns a lightweight per-body feature list; feature_recognize writes a labelled TopologyGraph for downstream reconstruction use.
inspect_assembly
Walk an XCAF assembly hierarchy and return the component tree with transforms.
Server: Swift + Node
Parameters
| name | type | required | description |
|---|---|---|---|
bodyId | string | no | Scene body (BREP — returns a degenerate single-node response). |
inputPath | string | no | Absolute path to a STEP, IGES, or XBF file for the full tree. |
depth | integer (≥0) | no | Maximum tree depth to traverse. Default: unlimited. |
Returns — JSON assembly tree: each node has name, shape (if a leaf), transform (4×4 matrix), and a children array. A BREP bodyId returns a single-node tree.
Example
// tool call arguments
{ "inputPath": "/Users/me/Downloads/assembly.step", "depth": 3 }
// example result
{
"name": "Assembly",
"transform": [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]],
"children": [
{ "name": "BaseFrame", "shape": "BaseFrame", "transform": [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]], "children": [] },
{ "name": "Lid", "shape": "Lid", "transform": [[1,0,0,50],[0,1,0,0],[0,0,1,0],[0,0,0,1]], "children": [] }
]
}
Notes — Pass inputPath (not bodyId) to get the full multi-level component tree from a STEP/IGES/XBF file. Use import_file first if you want the assembly bodies added to the scene.