Query language
One expression, a set of nodes
Every reader turns a file into one tree of nodes. The filter language finds nodes in that tree with a single boolean expression, shaped like a SQL WHERE clause. The answer is always a set of nodes in document order — never property values, never a table — which is what lets the grammar stay one level deep.
class == ON_Brep and within(class == ON_Layer and name == Walls)
It is the same language everywhere it is typed: the box above the tree in the Python viewer, cadaclysm_query in the C ABI, and CadScene.query in the browser build. A filter that will not parse says where, to the byte, rather than matching nothing quietly.
Fields
Nine words name the node
The list is fixed and closed, so a filter means the same thing against every format.
| Field | Type | Meaning |
class | text | What the file called it: ON_Brep, part, IfcWall |
name | text | The node's name |
id | text | The node's identifier — a STEP entity id like #1234 |
visible | bool | Whether the file shows it |
geometry | bool | Whether it carries geometry of its own |
index | int | Its position in document order |
depth | int | How many parents it has; 0 at a root |
children | int | How many direct children |
instanced | bool | Whether it is an instance of another node |
Any other bare word in field position is a property, matched against the node's own properties — Rhino user text, a STEP product's attributes — so the common case stays terse, with no sigil to remember: Material == Steel, PartNumber ~= '^A-'.
A field standing alone is itself an expression. For a property it tests presence: Material matches nodes carrying it, not Material those without. For the three boolean fields it is the value: not visible is the hidden ones. Bare props is every node with at least one property. props. is the one escape hatch for a property that shares a reserved name: props.id reaches a STEP PRODUCT.id that a bare id cannot, because a bare word resolves to a node field first.
Operators
Compare, match, combine
| Operator | Meaning |
== != | Equal, not equal. Text comparison folds case. |
< <= > >= | Numeric only. |
in (a, b, c) | Any of — more readable than a chain of or. |
~= 'pattern' | A regular expression, unanchored, compiled once when the filter is parsed. Case folds too; (?-i) opts back out. |
and or not ( ) | Combine, at precedence not > and > or. |
true false | Whole expressions on their own, so within(true) reads as "has any ancestor at all". |
Text folds case because CAD class and layer names are typed by people against vocabulary they half remember, and a case-exact miss that returns nothing with no error is the failure this language most wants to avoid. A number against a text property parses the text first — Rhino user text is all text, so Weight == 5 would otherwise never match in a .3dm — and a boolean does the same against "true" and "false". A missing property is false in every comparison, so not Material == Steel also finds the nodes with no Material at all.
Quoting
Quotes are lexical, never semantic
Every token may be bare, single-quoted or double-quoted, and the three are the same filter: class == ON_Brep, class == "ON_Brep", class == 'ON_Brep'. Quotes exist so a token can hold what a bare identifier cannot — spaces, dots, a leading digit: name == 'Bolt M8', props."Hello World" == 3, class == '3D_Solid'. Position decides what a token is: left of an operator it is a field, right of it a value.
One exception: keywords are keywords only when bare. and, or, not, in, true, false and props are syntax in bare form and ordinary text in quotes, so a property genuinely named and is reachable as 'and'.
Structure
Six functions look at other nodes
Each takes a full expression and tests it against the node's relatives. They are ordinary terms, so they compose with and, or and not without qualification.
| Function | True when |
within(e) | some ancestor matches e |
child_of(e) | the parent matches e |
has(e) | some descendant matches e |
has_child(e) | some direct child matches e |
instance_of(e) | the node this one is an instance of matches e |
instanced_by(e) | some instance of this node matches e |
instance_of and instanced_by are the only way to follow that cross-link. It is not a parent, so a Rhino block's members are invisible to within and has however the tree is walked.
Worked examples
Against a small Rhino tree
The tree the Rhino reader builds: layers nest, a group sits between a layer and an object, and a block's members hang under a definition that is not a root.
[0] ON_Layer "Structure" root
[1] └─ ON_Layer "Walls" parent 0
[2] ├─ ON_Brep "W-101" parent 1
[3] └─ ON_Group "Ops" parent 1
[4] └─ ON_Extrusion "W-102" parent 3
[5] ON_Layer "Reference" root
[6] └─ ON_InstanceRef "Bolt" parent 5, instance_of → 7
[7] ON_InstanceDefinition "Bolt M8" parent none, not a root
[8] └─ ON_Brep "shank" parent 7
| Filter | Result | Why |
child_of(name == Walls) | 2, 3 | direct children only |
within(name == Walls) | 2, 3, 4 | any depth |
class == ON_Layer and has(class == ON_Extrusion) | 0, 1 | any depth, so both ancestors |
class == ON_Layer and has_child(class == ON_Extrusion) | none | 4's parent is the group |
within(class == ON_InstanceRef) | none | instance_of is not a parent link |
class == ON_Brep and within(name == 'Bolt M8') | 8 | found although 7 is not a root |
children == 0 | 2, 4, 6, 8 | the leaves |
instance_of(has(class == ON_Brep)) | 6 | instances of any block containing a brep |
Across formats
Each reader's own vocabulary
A .3dm has rich classes and Rhino user text for properties; a .stp has three classes, so most filters lead with something else.
# .3dm
class in (ON_Brep, ON_Extrusion, ON_SubD)
class == ON_Brep and within(class == ON_Layer and name == Walls)
PartNumber ~= '^A-\d{4}$'
within(class == ON_Layer and not visible) # hidden only by their layer
class == ON_InstanceRef and instance_of(name == 'Bolt M8')
class == ON_InstanceDefinition and not instanced_by(true) # blocks never placed
class == ON_Layer and children == 0 # empty layers
class == ON_Layer and has(not visible) # layers holding something hidden
# .stp
class == part and not geometry # bodies the mesher could not build
name ~= '^ISO 47\d\d'
props.id == 'PN-4821' # PRODUCT.id, not the entity id
id == '#1234' # the entity id
class == assembly and has(class == part and not geometry)
class == part and not within(class == assembly and name ~= Frame)
The query that decided the shape of the language — everything unfinished, excluding one subtree:
class in (ON_Brep, ON_Extrusion, ON_SubD)
and (not Material
or within(Status == WIP)
or instance_of(class == ON_InstanceDefinition and has(not geometry)))
and not within(class == ON_Layer and name == Reference)
From code
Parse once, run anywhere
In Rust a filter is parsed once (Filter::parse) and runs against any number of documents, so a bad pattern is reported when the filter is written rather than on some later node. The C entry point is the sized-buffer two-call idiom the rest of the ABI uses; a 0 paired with no error is a real empty result, a 0 paired with cadaclysm_last_error is a filter that did not parse.
// C: how many nodes match, then which
uint32_t n = cadaclysm_query(scene, "class == part and not geometry", NULL, 0);
uint32_t *hits = malloc(n * sizeof *hits);
cadaclysm_query(scene, "class == part and not geometry", hits, n);
// browser (wasm): the same filter, the same indices
const hits = scene.query("class == part and not geometry");