mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-05 23:41:44 +00:00
IfcGeom: make permissive-shape-reuse folding style and instance aware
The permissive-shape-reuse task fold collapsed tasks onto the innermost single-child taxonomy node. Three defects followed from that. 1. When the shared representation had a single item, the fold descended past the representation collection down to the raw geometry item, and Converter::create_brep_for_representation_and_product later failed with 'RepresentationIdentifier not found' on every folded product, aborting the conversion. The public AC20-FZK-Haus model fails to convert entirely with these settings. 2. Styles attached to traversed wrapper collections, such as an IfcStyledItem on an IfcMappedItem occurrence, were dropped, so folded occurrences lost their per-occurrence colours. 3. Folding was applied with use-world-coords enabled even though product placements are then baked into the shared geometry, which collapses all folded products onto the first product's placement. The regular grouping in mapping::reuse_ok_() already refuses reuse in that case. The fold key is now the pair of the shared node and the effective hoisted surface style, so occurrences with the same style share one processed representation while differently styled occurrences keep their own, which also covers styled IfcMappedItem occurrences from issue #143 that the regular grouping must skip. Folded groups are rewrapped in a collection carrying the hoisted style and the first folded task's representation instance, and folding is skipped under use-world-coords. Verified against IfcConvert glTF output on synthetic mapped-item models (styled occurrences, non-identity mapping targets) and AC20-FZK-Haus: world-space geometry and materials match the unfolded output exactly, and default-settings output over the IFC4 test corpus is byte-identical. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -54,13 +54,23 @@ bool IfcGeom::Iterator::initialize() {
|
||||
tasks_.push_back(res);
|
||||
}
|
||||
|
||||
if (settings_.get<ifcopenshell::geometry::settings::NoParallelMapping>().get() && settings_.get<ifcopenshell::geometry::settings::PermissiveShapeReuse>().get()) {
|
||||
std::unordered_map<
|
||||
ifcopenshell::geometry::taxonomy::item::ptr,
|
||||
std::vector<std::pair<IfcUtil::IfcBaseEntity*, ifcopenshell::geometry::taxonomy::matrix4::ptr>>> folded;
|
||||
if (settings_.get<ifcopenshell::geometry::settings::NoParallelMapping>().get() && settings_.get<ifcopenshell::geometry::settings::PermissiveShapeReuse>().get() &&
|
||||
// With world coords enabled, product placements are baked into the BRep, so
|
||||
// geometries cannot be shared between products, same as in mapping::reuse_ok_().
|
||||
!settings_.get<ifcopenshell::geometry::settings::UseWorldCoords>().get()) {
|
||||
// Tasks are folded based on the innermost shared taxonomy node combined with the
|
||||
// effective surface style hoisted from the traversed wrappers, so that occurrences
|
||||
// of the same mapped representation with different styles keep their own style.
|
||||
typedef std::pair<ifcopenshell::geometry::taxonomy::item::ptr, ifcopenshell::geometry::taxonomy::style::ptr> fold_key_t;
|
||||
struct fold_group_t {
|
||||
ifcopenshell::geometry::taxonomy::item::ptr original;
|
||||
std::vector<std::pair<IfcUtil::IfcBaseEntity*, ifcopenshell::geometry::taxonomy::matrix4::ptr>> products;
|
||||
};
|
||||
std::map<fold_key_t, fold_group_t> folded;
|
||||
|
||||
for (auto& r : tasks_) {
|
||||
auto i = r.item;
|
||||
ifcopenshell::geometry::taxonomy::style::ptr style;
|
||||
|
||||
Eigen::Matrix4d m4 = Eigen::Matrix4d::Identity();
|
||||
|
||||
@@ -69,16 +79,24 @@ bool IfcGeom::Iterator::initialize() {
|
||||
if (col->matrix) {
|
||||
m4 *= col->matrix->ccomponents();
|
||||
}
|
||||
if (col->surface_style) {
|
||||
style = col->surface_style;
|
||||
}
|
||||
i = col->children[0];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto& group = folded[{ i, style }];
|
||||
if (!group.original) {
|
||||
group.original = r.item;
|
||||
}
|
||||
|
||||
for (auto& p : r.products) {
|
||||
auto pl = ifcopenshell::geometry::taxonomy::matrix4::ptr(p.second->clone_());
|
||||
pl->components() *= m4;
|
||||
folded[i].push_back(
|
||||
group.products.push_back(
|
||||
{ p.first, pl }
|
||||
);
|
||||
}
|
||||
@@ -89,10 +107,20 @@ bool IfcGeom::Iterator::initialize() {
|
||||
tasks_.clear();
|
||||
size_t i = 0;
|
||||
for (auto& p : folded) {
|
||||
auto item = p.first.first;
|
||||
if (item != p.second.original) {
|
||||
// Reattach the hoisted style and a representation instance for downstream use.
|
||||
auto wrap = ifcopenshell::geometry::taxonomy::make<ifcopenshell::geometry::taxonomy::collection>();
|
||||
wrap->children.push_back(std::dynamic_pointer_cast<ifcopenshell::geometry::taxonomy::geom_item>(item));
|
||||
wrap->matrix = ifcopenshell::geometry::taxonomy::make<ifcopenshell::geometry::taxonomy::matrix4>();
|
||||
wrap->surface_style = p.first.second;
|
||||
wrap->instance = p.second.original->instance;
|
||||
item = wrap;
|
||||
}
|
||||
tasks_.emplace_back();
|
||||
tasks_.back().index = i++;
|
||||
tasks_.back().item = p.first;
|
||||
tasks_.back().products = p.second;
|
||||
tasks_.back().item = item;
|
||||
tasks_.back().products = std::move(p.second.products);
|
||||
}
|
||||
logger_.Notice("SYS", 26, "Merged " + std::to_string(old_size) + " tasks into " + std::to_string(tasks_.size()) + " tasks due to permissive shape reuse");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 IfcOpenShell contributors
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcOpenShell is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Tests for permissive-shape-reuse task folding in the iterator (issue #143).
|
||||
|
||||
Occurrences of one IfcRepresentationMap whose IfcMappedItems carry their own
|
||||
IfcStyledItem cannot be grouped by the regular reuse logic. With
|
||||
no-parallel-mapping and permissive-shape-reuse the iterator folds such tasks
|
||||
onto the shared taxonomy node, which must keep per-occurrence styles and
|
||||
placements intact.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.geom
|
||||
import ifcopenshell.guid
|
||||
import ifcopenshell.util.shape
|
||||
|
||||
RED = (1.0, 0.0, 0.0)
|
||||
BLUE = (0.0, 0.0, 1.0)
|
||||
GRAY = (0.7, 0.7, 0.7) # default material
|
||||
|
||||
|
||||
def make_model(occurrence_styles=(RED, RED, BLUE, None), target_offset=None):
|
||||
"""One mapped box representation, one occurrence per entry in occurrence_styles.
|
||||
|
||||
Styles are attached with an IfcStyledItem on each occurrence's IfcMappedItem,
|
||||
which defeats the regular mapped item grouping (StyledByItem count != 0).
|
||||
"""
|
||||
f = ifcopenshell.file(schema="IFC4")
|
||||
origin = f.createIfcCartesianPoint((0.0, 0.0, 0.0))
|
||||
axis_placement = f.createIfcAxis2Placement3D(origin, None, None)
|
||||
context = f.createIfcGeometricRepresentationContext(None, "Model", 3, 1e-5, axis_placement, None)
|
||||
units = f.createIfcUnitAssignment(
|
||||
[
|
||||
f.createIfcSIUnit(None, "LENGTHUNIT", None, "METRE"),
|
||||
f.createIfcSIUnit(None, "PLANEANGLEUNIT", None, "RADIAN"),
|
||||
]
|
||||
)
|
||||
f.createIfcProject(ifcopenshell.guid.new(), None, "Proj", None, None, None, None, [context], units)
|
||||
|
||||
profile = f.createIfcRectangleProfileDef(
|
||||
"AREA", None, f.createIfcAxis2Placement2D(f.createIfcCartesianPoint((0.0, 0.0)), None), 1.0, 1.0
|
||||
)
|
||||
solid = f.createIfcExtrudedAreaSolid(profile, axis_placement, f.createIfcDirection((0.0, 0.0, 1.0)), 1.0)
|
||||
mapped_rep = f.createIfcShapeRepresentation(context, "Body", "SweptSolid", [solid])
|
||||
rep_map = f.createIfcRepresentationMap(axis_placement, mapped_rep)
|
||||
|
||||
surface_styles = {}
|
||||
for colour in {s for s in occurrence_styles if s is not None}:
|
||||
rgb = f.createIfcColourRgb(None, *colour)
|
||||
shading = f.createIfcSurfaceStyleShading(rgb, 0.0)
|
||||
surface_styles[colour] = f.createIfcSurfaceStyle(str(colour), "POSITIVE", [shading])
|
||||
|
||||
if target_offset is not None:
|
||||
target_origin = f.createIfcCartesianPoint(tuple(map(float, target_offset)))
|
||||
else:
|
||||
target_origin = origin
|
||||
|
||||
products = []
|
||||
for i, colour in enumerate(occurrence_styles):
|
||||
target = f.createIfcCartesianTransformationOperator3D(None, None, target_origin, 1.0, None)
|
||||
mapped_item = f.createIfcMappedItem(rep_map, target)
|
||||
if colour is not None:
|
||||
f.createIfcStyledItem(mapped_item, [surface_styles[colour]], None)
|
||||
shape_rep = f.createIfcShapeRepresentation(context, "Body", "MappedRepresentation", [mapped_item])
|
||||
definition = f.createIfcProductDefinitionShape(None, None, [shape_rep])
|
||||
placement = f.createIfcLocalPlacement(
|
||||
None, f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((i * 2.0, 0.0, 0.0)), None, None)
|
||||
)
|
||||
products.append(
|
||||
f.createIfcBuildingElementProxy(
|
||||
ifcopenshell.guid.new(), None, f"Elem{i}", None, None, placement, definition, None, None
|
||||
)
|
||||
)
|
||||
return f, products
|
||||
|
||||
|
||||
def iterate(f, permissive_shape_reuse):
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("apply-default-materials", True)
|
||||
if permissive_shape_reuse:
|
||||
settings.set("no-parallel-mapping", True)
|
||||
settings.set("permissive-shape-reuse", True)
|
||||
elements = {}
|
||||
iterator = ifcopenshell.geom.iterator(settings, f, 1)
|
||||
assert iterator.initialize()
|
||||
while True:
|
||||
element = iterator.get()
|
||||
elements[element.name] = element
|
||||
if not iterator.next():
|
||||
break
|
||||
return elements
|
||||
|
||||
|
||||
def element_colours(element):
|
||||
return {
|
||||
tuple(round(c, 3) for c in rgba[:3]) for rgba in ifcopenshell.util.shape.get_material_colors(element.geometry)
|
||||
}
|
||||
|
||||
|
||||
def world_vertices(element):
|
||||
matrix = ifcopenshell.util.shape.get_shape_matrix(element)
|
||||
verts = np.hstack(
|
||||
[ifcopenshell.util.shape.get_vertices(element.geometry), np.ones((len(element.geometry.verts) // 3, 1))]
|
||||
)
|
||||
return {tuple(np.round(row, 5)) for row in (verts @ matrix.T)[:, :3]}
|
||||
|
||||
|
||||
def test_folding_keeps_styles_and_placements():
|
||||
f, _ = make_model()
|
||||
elements = iterate(f, permissive_shape_reuse=True)
|
||||
assert len(elements) == 4
|
||||
|
||||
assert element_colours(elements["Elem0"]) == {RED}
|
||||
assert element_colours(elements["Elem1"]) == {RED}
|
||||
assert element_colours(elements["Elem2"]) == {BLUE}
|
||||
assert element_colours(elements["Elem3"]) == {GRAY}
|
||||
|
||||
for i in range(4):
|
||||
matrix = ifcopenshell.util.shape.get_shape_matrix(elements[f"Elem{i}"])
|
||||
assert np.allclose(matrix[:3, 3], (i * 2.0, 0.0, 0.0))
|
||||
|
||||
# occurrences with an identical style share one processed representation
|
||||
assert elements["Elem0"].geometry.id == elements["Elem1"].geometry.id
|
||||
assert elements["Elem0"].geometry.id != elements["Elem2"].geometry.id
|
||||
|
||||
|
||||
def test_folding_matches_unfolded_output():
|
||||
f, _ = make_model()
|
||||
folded = iterate(f, permissive_shape_reuse=True)
|
||||
unfolded = iterate(f, permissive_shape_reuse=False)
|
||||
assert folded.keys() == unfolded.keys()
|
||||
for name in folded:
|
||||
assert world_vertices(folded[name]) == world_vertices(unfolded[name])
|
||||
assert element_colours(folded[name]) == element_colours(unfolded[name])
|
||||
|
||||
|
||||
def test_folding_hoists_non_identity_mapping_target():
|
||||
f, _ = make_model(target_offset=(0.0, 0.0, 5.0))
|
||||
folded = iterate(f, permissive_shape_reuse=True)
|
||||
unfolded = iterate(f, permissive_shape_reuse=False)
|
||||
assert len(folded) == 4
|
||||
assert folded["Elem0"].geometry.id == folded["Elem1"].geometry.id
|
||||
for name in folded:
|
||||
assert world_vertices(folded[name]) == world_vertices(unfolded[name])
|
||||
assert element_colours(folded[name]) == element_colours(unfolded[name])
|
||||
# the mapping target translation must survive the fold
|
||||
assert min(v[2] for v in world_vertices(folded["Elem0"])) == 5.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
|
||||
pytest.main(["-vvsx", __file__])
|
||||
Reference in New Issue
Block a user