Skip to content

Without 'Material.Name' layers merge. - #7700

Merged
theoryshaw merged 1 commit into
v0.8.0from
separate_layers
Mar 21, 2026
Merged

theoryshaw merged 1 commit into
v0.8.0from
separate_layers

Conversation

@theoryshaw

Copy link
Copy Markdown
Member

No description provided.

@theoryshaw

Copy link
Copy Markdown
Member Author

This is through hard won trial and error. Not entirely sure why Material.Name is necessary to not merge the layers--seems counterintuitive for the JoinCriteria parameter.

@theoryshaw
theoryshaw merged commit 547b221 into v0.8.0 Mar 21, 2026
1 of 4 checks passed
@theoryshaw

Copy link
Copy Markdown
Member Author

from AI...

JoinCriteria functions as an equality predicate — two elements merge if and only if they produce identical values for every query in the list. The problem is that this same predicate is evaluated against both the element and its IfcMaterialLayer in a single concatenated key, meaning the list is simultaneously controlling two distinct merging decisions with different semantics and different target entities.

Material.Name works as a layer discriminator not because it expresses that intent, but because getattr(IfcMaterialLayer, "Material") happens to be a valid direct attribute in the schema, returning a distinct IfcMaterial per layer, while the same query returns None on a wall — so it silently has no effect at the element level but a significant effect at the layer level.

The practical problem is that this is a hidden coupling: a developer reading JoinCriteria has no way to know it's also controlling layer behaviour, a developer tuning layer rendering has no way to know they're editing element merge criteria, and the default list as shipped only works correctly because Material.Name and material.Name happen to cancel each other out at the element level while diverging at the layer level — which is a fragile invariant that could easily break if someone reasonably decides to clean up what looks like a duplicate entry.

@theoryshaw

Copy link
Copy Markdown
Member Author

also from AI....

Now we can see exactly what happens. Looking at _get_element_value:

material.Name (lowercase)

python

elif key in ("material", "mat"):
    value = ifcopenshell.util.element.get_material(value, should_skip_usage=True)

It hits this explicit special-case branch, which traverses HasAssociations relationships, handles IfcMaterialLayerSetUsageForLayerSet, inherits from the type element, etc. Then on the next iteration, .Name is resolved as a direct attribute on the returned material entity — with the extra quirk that IfcMaterialLayerSet.Name is silently remapped to LayerSetName:

python

if key == "Name" and value.is_a("IfcMaterialLayerSet"):
    key = "LayerSetName"

Material.Name (uppercase)

It doesn't match any special keyword. It falls through to the isinstance(value, ifcopenshell.entity_instance) branch and does:

python

attribute = getattr(value, key, None)  # key = "Material"

So it's looking for a direct Material attribute on the element itself. A small number of IFC entities actually do have a direct Material attribute — notably IfcMaterialLayer and IfcMaterialProfile. So Material.Name would only return something meaningful if the element being evaluated is one of those entities, not a regular building element like IfcWall.

This confirms the earlier suspicion, but more precisely: Material.Name isn't completely useless — it's just only meaningful in a context where the current value in the traversal chain is already a material layer or profile, not a top-level building element. In the join_criteria context where it's applied directly to an IfcWall or IfcSlab, it would return None for almost all elements.

when a wall, for example, with an IfcMaterialLayerSet, when Material.Name is used, the layer print out separately, but when you don't, the layers merge in the svg. Can you help me understand why?

This is a subtle but elegant interaction between two parts of the code. The key is that join_criteria is evaluated against both the element and the layer:

python

keys = []
for query in join_criteria:
    key = ifcopenshell.util.selector.get_element_value(element, query)
    keys.append(key)

if layer:
    for query in join_criteria:
        key = ifcopenshell.util.selector.get_element_value(layer, query)
        keys.append(key)

hash_keys = hash(tuple(keys))

The layer here is an IfcMaterialLayer entity, retrieved via the layer-id attribute stamped onto the SVG group by generate_material_layers.

Now the difference becomes clear:

  • material.Name on an IfcMaterialLayer — hits the special keyword branch, calls get_material() on the layer, which traverses relationships. IfcMaterialLayer doesn't have material associations in that sense, so this likely returns None for all layers — making them indistinguishable.
  • Material.Name on an IfcMaterialLayer — falls through to getattr(layer, "Material", None), and IfcMaterialLayer does have a direct Material attribute in the IFC schema. So this returns the actual IfcMaterial entity, and then .Name gives the material name — which is different for each layer.

So with Material.Name in the criteria, each layer gets a unique value contributing to its hash key, meaning they never merge. Without it, layers of the same wall class end up with the same hash and get unioned together into a single polygon.

In other words, Material.Name isn't redundant with material.Name at all — they operate on different entities and serve completely different purposes in this context.

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.

1 participant