Bonsai: key cut linework on the material of the cut, not the layer set - #9493
Open
theoryshaw wants to merge 1 commit into
Open
theoryshaw wants to merge 1 commit into
theoryshaw wants to merge 1 commit into
Conversation
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>
This was referenced Sep 13, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
JoinCriteriawas 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.NameandMaterial.Namehappened to cancel at the element level while diverging at the layer level —Material.Nameresolves toNoneon a wall, but to the layer's ownIfcMaterialon anIfcMaterialLayer, purely becauseMaterialis 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.NameandMaterial.Namein 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 whatMaterial.Namewas 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_materialis 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 toget_element_value.Keeping it separate means
material.Namestill means the layer set name here, exactly as it does inMetadata, IfcCSV and text literals — one query string, one meaning. It also leaves theLayerSetNameescape hatch reachable, which hijackingmaterial.*would have closed.Result
With
JoinClasses(#4395) naming the classes andclassomitted fromJoinCriteria:Verified on a real project section. Two footings and the slab's concrete layer merge into a single closed path:
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 theIfcFootingandIfcSlab/IfcMaterialLayerclasses.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 POCHElayer). Arguably the correct drafting convention — same material, no line between them — but it is a change for users who never touch these properties.Also
JoinCriteriaentries are now stripped, asJoinClassesalready does. Without it a stray space after a comma stops a query being recognised as acut_materialquery and silently disables the layer-aware key.Related
mats.Categoryvia the material set item, for Be able to use 'JoinCriteria' with something likemats.Category. #7000. Worth a look alongside this:cut_material.Categoryreads the material's own attribute.merge_duplicate_edgesimmediately aftermerge_linework_and_add_metadata. Verified itsoperator.pyhunks still apply cleanly on top of this branch, so the two are independent.Documentation is deliberately limited to the
EPset_Drawing.JoinCriteriapset description, which is what surfaces in the UI.selector_syntax.rstis not updated, sincecut_materialis not a selector query and doesn't work in IfcCSV or the other tools that page covers. Nothing in the repo documentsEPset_Drawingyet; a reference page for it is worth a follow-up, alongside #9358.🤖 Generated with Claude Code