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.

FieldTypeMeaning
classtextWhat the file called it: ON_Brep, part, IfcWall
nametextThe node's name
idtextThe node's identifier — a STEP entity id like #1234
visibleboolWhether the file shows it
geometryboolWhether it carries geometry of its own
indexintIts position in document order
depthintHow many parents it has; 0 at a root
childrenintHow many direct children
instancedboolWhether 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

OperatorMeaning
== !=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 falseWhole 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.

FunctionTrue 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
FilterResultWhy
child_of(name == Walls)2, 3direct children only
within(name == Walls)2, 3, 4any depth
class == ON_Layer and has(class == ON_Extrusion)0, 1any depth, so both ancestors
class == ON_Layer and has_child(class == ON_Extrusion)none4's parent is the group
within(class == ON_InstanceRef)noneinstance_of is not a parent link
class == ON_Brep and within(name == 'Bolt M8')8found although 7 is not a root
children == 02, 4, 6, 8the leaves
instance_of(has(class == ON_Brep))6instances 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");

Status

The two libraries ship: the SDK at github.com/rdeioris/cadaclysm-sdk fetches them for your machine, licensed per seat or per server. The Unreal plugin and the Unity package are not on their marketplaces yet; the crates are proprietary and not on crates.io.

What you can do today: open a file in your browser (the forge runs from the repository), and write to us at info@blitter.studio about the plugins or a kernel licence.