Bonsai: emit one SVG class per metadata value, and per layer where cut - #9494
Open
theoryshaw wants to merge 3 commits into
Open
theoryshaw wants to merge 3 commits into
theoryshaw wants to merge 3 commits 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>
A drawing's EPset_Drawing.Metadata queries become CSS classes on each cut group.
A query may resolve to several values - "mats.Category" on a layered wall, say -
and the whole list was being stringified into one class, so a three layer wall
carrying a single Category came out as:
matsCategory-NoneNoneBrick
That matches no meaningful selector, and it is unstable: adding a layer changes
the class. A list holding only None, such as "mats.Category" on a beam whose
material has no Category, produced "matsCategory-None", because a list of one
None is still truthy.
Emit one class per value instead, skipping None entries and any value with
nothing left after canonicalisation, and de-duplicating. The wall above now
gets matsCategory-Brick, and the beam gets no matsCategory class at all.
Material queries are also resolved against the layer's own material where the
cut is a single material layer. Otherwise the concrete layer of a slab is tagged
with the gravel layer's classes as well - true of the element, but not of that
cut - and now that those classes match CSS selectors, whichever rule appears
later in the stylesheet paints the layer wrongly. This was found live: a footing
merged with a slab's concrete layer rendered with the gravel pattern, because
the merged group inherited matsCategory-Gravel from the slab element and the
gravel rule sits after the concrete rule in the stylesheet.
The whole element cut group of a multi material element still carries every
value, which is correct - that cut really does span them all.
Note this makes previously dead classes live. Any element with several materials
was only ever emitting the concatenated form, which matched nothing; such
elements will now match single value selectors that were already in a
stylesheet. That is the point of the change, but it can alter existing drawings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follows the precedence util.selector uses for "mats.Category" since IfcOpenShell#9044: Category is defined on both the material set item and the IfcMaterial it references, and the set item wins where it is set. Without this a layer group read Category from the material only, so a layer that carries its own Category emitted no class while the element level query reported one - the same query giving two answers depending on whether the cut happened to be a layer, which is what the per layer resolution was meant to stop. Found on a wall whose GENERIC POCHE layer is categorised 'Materials' while the material itself has no Category. Name is unaffected and still comes from the material, which is where it lives. 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.
The problem
A drawing's
EPset_Drawing.Metadataqueries become CSS classes on each cut group. A query can resolve to several values —mats.Categoryon a layered wall — and the whole list was stringified into one class. A three-layer wall carrying a single Category came out as:That matches no meaningful selector, and it's unstable: add a layer and the class changes. Separately, a list holding only
None—mats.Categoryon a beam whose material has no Category — producedmatsCategory-None, because a one-element list ofNoneis still truthy.The change
One class per value, skipping
Noneentries and anything left empty by canonicalisation, de-duplicated:Single-material elements are unchanged, so the common case sees no churn. Beams with an uncategorised material now emit no
matsCategoryclass instead ofmatsCategory-None.Material queries resolve per layer where the cut is one material layer. Otherwise a slab's concrete layer is tagged with the gravel layer's classes too — true of the element, false of that cut — and once those classes match selectors, whichever rule sits later in the stylesheet paints it wrongly.
This was found live rather than by inspection. A footing merged with a slab's concrete layer rendered with the gravel pattern:
because the merged group inherited
matsCategory-Gravelfrom the slab element. After the change each layer group describes itself:The whole-element cut group of a multi-material element still carries every value, which is right: that cut really does span them all.
Why per-layer here, when #9493 deliberately did not redirect
material.*Worth stating, since the two decisions look contradictory. In
JoinCriteriathe query runs against an element, and which layer is in play is invisible in the query text — so redirecting the word would hide something the author cannot see, which is why that PR introduced an explicitcut_materialinstead. Here the<g>is the layer: every other class on it (IfcMaterialLayer,layer-material-*) already describes the layer, so a class claiming otherwise is simply wrong about the element it is attached to.If reviewers prefer symmetry, the alternative is to teach
Metadatathecut_material.head too and leavemats.*alone — but then existingMetadatasettings keep producing the mis-tagging until each user edits them.Behaviour change worth flagging
This makes previously dead classes live. Any element with several materials only ever emitted the concatenated form, which matched nothing; those elements will now match single-value selectors already present in stylesheets. That is the point of the change, but it can alter existing drawings — in the test project, 43 elements begin matching a
.matsCategory-*fill rule they never matched before.Related
material.Categorydoesn't seem to work in EPset_Drawing'sMetadataparameter. #3844 — Dion noted the same wart from the other end: "If you usemats.Nameit will turn up as things likematsName-Testy1MaterialTesty2Material."util.selector: resolve mats.Category via the material set item #9044 — resolves
mats.Categorythrough the material set item, now with a fallback to the material's ownCategory(25b2b4aa, added after I reported a model where that data would otherwise be lost). The second commit here mirrors that precedence for layer groups, so the same query gives the same answer whether or not the cut happens to be a layer. Verified together against that model:Bonsai: per element drawing appearance via assignable CSS classes #8800 — also extends
get_svg_classes; no overlap with these lines.🤖 Generated with Claude Code