Skip to content

Bonsai: key cut linework on the material of the cut, not the layer set - #9493

Open
theoryshaw wants to merge 1 commit into
IfcOpenShell:v0.9.0from
theoryshaw:join_cut_material_layers
Open

theoryshaw wants to merge 1 commit into
IfcOpenShell:v0.9.0from
theoryshaw:join_cut_material_layers

Conversation

@theoryshaw

@theoryshaw theoryshaw commented Sep 13, 2026

Copy link
Copy Markdown
Member

Fixes #7830. Does the second half of #7000.

The problem

A material layer's cut linework could never merge with a non-layered element of the same material, whatever JoinCriteria was set to.

The key was built by evaluating every criterion against the element, then — if the cut was a material layer — evaluating every criterion again against the layer and appending those values. A layered cut's key tuple was therefore about twice the length of an unlayered one's, so the two could never hash equal even when every value agreed.

The list was also doing two unrelated jobs at once: deciding which elements merge, and which layers stay apart. It only worked because material.Name and Material.Name happened to cancel at the element level while diverging at the layer level — Material.Name resolves to None on a wall, but to the layer's own IfcMaterial on an IfcMaterialLayer, purely because Material is a real attribute there. That line came from #7700, fixing #7252 and #7369 where every layer hashed alike and collapsed into one polygon.

The change

cut_material — the material of the cut itself: that layer's material for a layered cut, or the element's own material where it has exactly one. The layer pass goes away, so the key is one fixed-length tuple either way and the same material keys identically whether or not it arrived as a layer.

It replaces both material.Name and Material.Name in the defaults. Layers still keep each other apart, so #7252 and #7369 do not regress — each layer keys on its own material, which is exactly what Material.Name was accidentally achieving.

Where nothing identifies a cut's material — no material at all, or several with unlayered geometry — the IFC class is appended instead, so unidentified cuts don't all share one key and merge on contact.

Why not a selector key

cut_material is deliberately join-local vocabulary rather than a new entry in the selector syntax. Which material a cut carries depends on which layer is being drawn — drawing state that no query over the element alone can express; the general syntax can only reach a layer by index (material.item.1.Material.Name), and the index is precisely what the caller knows and the query doesn't. The drawing join code is also the only caller in the tree that ever passed a non-product entity to get_element_value.

Keeping it separate means material.Name still means the layer set name here, exactly as it does in Metadata, IfcCSV and text literals — one query string, one meaning. It also leaves the LayerSetName escape hatch reachable, which hijacking material.* would have closed.

Result

With JoinClasses (#4395) naming the classes and class omitted from JoinCriteria:

JoinClasses  = IfcWall,IfcSlab,IfcFooting
JoinCriteria = cut_material.Name,/Pset_.*Common/.Status,EPset_Status.Status,EPset_Status.UserDefinedStatus

Verified on a real project section. Two footings and the slab's concrete layer merge into a single closed path:

bucket: ['0BvUur15…' (IfcFooting), '2bcEs9SN…' (IfcFooting), '0O$LaON8…' (IfcSlab layer 3405669)]
  3 closed polygons -> 2 unions -> 1 emitted group

while the slab's vapour-barrier layer, each wall's layers, and the unlayered slab outline all stay separate. Confirmed in the output SVG: one <g>, one closed 19-point path, carrying both the IfcFooting and IfcSlab/IfcMaterialLayer classes.

Behaviour change worth flagging

Under the default criteria a layered cut is now keyed on its layer's material alone, where the layer set name previously also participated. Two different layer sets sharing a material will merge where they touch. In the test project that's one case (two wall layer sets both carrying a GENERIC POCHE layer). Arguably the correct drafting convention — same material, no line between them — but it is a change for users who never touch these properties.

Also

JoinCriteria entries are now stripped, as JoinClasses already does. Without it a stray space after a comma stops a query being recognised as a cut_material query and silently disables the layer-aware key.

Related

Documentation is deliberately limited to the EPset_Drawing.JoinCriteria pset description, which is what surfaces in the UI. selector_syntax.rst is not updated, since cut_material is not a selector query and doesn't work in IfcCSV or the other tools that page covers. Nothing in the repo documents EPset_Drawing yet; a reference page for it is worth a follow-up, alongside #9358.

🤖 Generated with Claude Code

Fixes IfcOpenShell#7830. Addresses the second half of IfcOpenShell#7000.

A material layer's cut linework could never merge with a non-layered element of
the same material, no matter what JoinCriteria was set to. The key was built by
evaluating every criterion against the element, then, if the cut was a material
layer, evaluating every criterion again against the layer and appending those
values. A layered cut's key tuple was therefore roughly twice the length of an
unlayered one's, so the two could not hash equal even when every value agreed.

The list was also doing two unrelated jobs at once: controlling which elements
merge, and which layers stay apart. It only worked because "material.Name" and
"Material.Name" happened to cancel at the element level while diverging at the
layer level - "Material.Name" resolves to None on a wall, but to the layer's own
IfcMaterial on an IfcMaterialLayer, purely because "Material" is a real
attribute there. That was added in IfcOpenShell#7700 to fix IfcOpenShell#7252 and IfcOpenShell#7369, where every
layer hashed alike and collapsed into one polygon.

Introduce "cut_material": the material of the cut itself - that layer's material
for a layered cut, or the element's own material where it has exactly one. The
layer pass goes away, so the key is one tuple of fixed length either way, and
the same material keys identically whether or not it arrived as a layer. This
replaces both "material.Name" and "Material.Name" in the default criteria;
layers still keep each other apart, so IfcOpenShell#7252 and IfcOpenShell#7369 do not regress.

"cut_material" is deliberately join-local vocabulary rather than a new selector
key. Which material a cut carries depends on which layer is being drawn, which
is drawing state no query over the element alone can express, and the drawing
join code is the only caller in the tree that ever passed a non-product entity
to get_element_value. Keeping it separate leaves "material.Name" meaning the
layer set name here exactly as it does in Metadata, IfcCSV and text literals.

Where nothing identifies a cut's material - no material at all, or several with
unlayered geometry - the IFC class is appended instead, so unidentified cuts do
not all share one key and merge on contact.

With EPset_Drawing.JoinClasses (IfcOpenShell#4395) naming the classes and "class" omitted
from JoinCriteria, a slab's concrete layer now reads continuous into a concrete
footing. Verified on a real project: the two footings and the slab's concrete
layer merge into a single closed path, while the slab's vapour barrier layer,
the wall layers, and the unlayered slab outline all stay separate.

Note one behaviour change under the default criteria: a layered cut is now keyed
on its layer's material alone, where previously the layer set name also
participated. Two different layer sets sharing a material will merge where they
touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
theoryshaw added a commit to theoryshaw/IfcOpenShell that referenced this pull request Sep 15, 2026
Backport of PR IfcOpenShell#9493 (v0.9.0). Fixes IfcOpenShell#7830. Addresses the second half of IfcOpenShell#7000.

A material layer's cut linework could never merge with a non-layered element of
the same material, no matter what JoinCriteria was set to. The key was built by
evaluating every criterion against the element, then, if the cut was a material
layer, evaluating every criterion again against the layer and appending those
values. A layered cut's key tuple was therefore roughly twice the length of an
unlayered one's, so the two could not hash equal even when every value agreed.

The list was also doing two unrelated jobs at once: controlling which elements
merge, and which layers stay apart. It only worked because "material.Name" and
"Material.Name" happened to cancel at the element level while diverging at the
layer level - "Material.Name" resolves to None on a wall, but to the layer's own
IfcMaterial on an IfcMaterialLayer, purely because "Material" is a real
attribute there. That was added in IfcOpenShell#7700 to fix IfcOpenShell#7252 and IfcOpenShell#7369, where every
layer hashed alike and collapsed into one polygon.

Introduce "cut_material": the material of the cut itself - that layer's material
for a layered cut, or the element's own material where it has exactly one. The
layer pass goes away, so the key is one tuple of fixed length either way, and
the same material keys identically whether or not it arrived as a layer. This
replaces both "material.Name" and "Material.Name" in the default criteria;
layers still keep each other apart, so IfcOpenShell#7252 and IfcOpenShell#7369 do not regress.

"cut_material" is deliberately join-local vocabulary rather than a new selector
key. Which material a cut carries depends on which layer is being drawn, which
is drawing state no query over the element alone can express, and the drawing
join code is the only caller in the tree that ever passed a non-product entity
to get_element_value. Keeping it separate leaves "material.Name" meaning the
layer set name here exactly as it does in Metadata, IfcCSV and text literals.

Where nothing identifies a cut's material - no material at all, or several with
unlayered geometry - the IFC class is appended instead, so unidentified cuts do
not all share one key and merge on contact.

With EPset_Drawing.JoinClasses (IfcOpenShell#4395) naming the classes and "class" omitted
from JoinCriteria, a slab's concrete layer now reads continuous into a concrete
footing. Verified on SECTION - EAST of a real project: the two footings and the
slab's concrete layer merge into a single closed 19 point path, while the slab's
vapour barrier layer, the wall layers, and the unlayered slab outline all stay
separate.

Note one behaviour change under the default criteria: a layered cut is now keyed
on its layer's material alone, where previously the layer set name also
participated. Two different layer sets sharing a material will merge where they
touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

come up with a solution that allows a separate joincriteria for layers

1 participant