mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-06 07:51:47 +00:00
Bonsai: normalize occurrence-local representations onto the type
Adds tooling to surface and normalize representations that live on an occurrence but not its type, aligning with the convention that typed occurrences share the type's representations (per maintainer feedback on #8788). - Promote to Type (bim.promote_representation_to_type): slot-based, "type wins". Copies the promoted rep onto the type as a RepresentationMap, then for every occurrence of the type removes ANY existing rep in the same slot -- context (context/subcontext/target view) + RepresentationIdentifier + resolved RepresentationType -- and assigns the type's mapped rep in its place; occurrences with none inherit it. Covers both local reps and reps inherited from a floating IfcRepresentationMap not anchored to the type (Revit exports), so no duplicate is left; removes the type's existing slot map first so promoting is idempotent. Geometry is not compared, so independently-meshed / mirrored / rotated instances are consolidated too. Adds tool.Geometry copy_representation_deep and add_type_representation_map. - Representations panel: group rows under Type (mapped/inherited) vs Occurrence (local) headers so occurrence-local reps are surfaced. Adds is_mapped / element_is_type / element_has_type to RepresentationsData; drops the old "*" suffix. Copy and add-representation behaviour is unchanged from base. Design note at docs/dev-notes/occurrence-representations.md. Issue: #8788 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,7 @@ classes = (
|
||||
operator.OverrideOriginSet,
|
||||
operator.OverrideOutlinerDelete,
|
||||
operator.OverridePasteBuffer,
|
||||
operator.PromoteRepresentationToType,
|
||||
operator.PurgeUnusedRepresentations,
|
||||
operator.RefreshLinkedAggregate,
|
||||
operator.RemoveConnection,
|
||||
|
||||
@@ -102,12 +102,32 @@ class RepresentationsData:
|
||||
def load(cls):
|
||||
cls.data = {"representations": cls.representations()}
|
||||
cls.data["contexts"] = cls.contexts()
|
||||
cls.data["element_is_type"] = cls.element_is_type()
|
||||
cls.data["element_has_type"] = cls.element_has_type()
|
||||
|
||||
# Only after cls.representations().
|
||||
cls.data["shape_aspects"] = cls.shape_aspects()
|
||||
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def element_is_type(cls) -> bool:
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
return bool(element and element.is_a("IfcTypeProduct"))
|
||||
|
||||
@classmethod
|
||||
def element_has_type(cls) -> bool:
|
||||
"""True when the active element is an occurrence with a type — i.e. a
|
||||
local representation could be promoted onto that type."""
|
||||
obj = tool.Geometry.get_active_or_representation_obj()
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or element.is_a("IfcTypeProduct"):
|
||||
return False
|
||||
return bool(ifcopenshell.util.element.get_type(element))
|
||||
|
||||
@classmethod
|
||||
def representations(cls) -> list[dict[str, Any]]:
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -122,11 +142,8 @@ class RepresentationsData:
|
||||
active_representation_id = active_representation.id()
|
||||
|
||||
for representation in tool.Geometry.get_representations_iter(element):
|
||||
representation_type = representation.RepresentationType
|
||||
resolved_representation = ifcopenshell.util.representation.resolve_representation(representation)
|
||||
|
||||
if resolved_representation != representation:
|
||||
representation_type = resolved_representation.RepresentationType + "*"
|
||||
representation_type = resolved_representation.RepresentationType
|
||||
|
||||
is_active = (
|
||||
representation.id() == active_representation_id
|
||||
@@ -145,6 +162,10 @@ class RepresentationsData:
|
||||
"RepresentationIdentifier": representation.RepresentationIdentifier or "",
|
||||
"RepresentationType": representation_type or "",
|
||||
"is_active": is_active,
|
||||
# True when this representation is an IfcMappedItem resolving to
|
||||
# the type's MappedRepresentation, i.e. inherited from the type
|
||||
# rather than local to the occurrence.
|
||||
"is_mapped": resolved_representation != representation,
|
||||
}
|
||||
if representation.ContextOfItems.is_a("IfcGeometricRepresentationSubContext"):
|
||||
data["ContextIdentifier"] = representation.ContextOfItems.ContextIdentifier or ""
|
||||
|
||||
@@ -545,6 +545,36 @@ class RemoveRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report({"INFO"}, f"{self.bl_label} was finished in {operator_time:.2f} seconds.")
|
||||
|
||||
|
||||
class PromoteRepresentationToType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.promote_representation_to_type"
|
||||
bl_label = "Promote Representation to Type"
|
||||
bl_description = (
|
||||
"Move this occurrence-local representation onto its type so occurrences can inherit it.\n"
|
||||
"Occurrences with an identical local representation inherit from the type;\n"
|
||||
"occurrences with a divergent local representation keep their own override"
|
||||
)
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
representation_id: bpy.props.IntProperty()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
representation_id: int
|
||||
|
||||
def _execute(self, context):
|
||||
assert context.active_object
|
||||
counts = core.promote_representation_to_type(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=context.active_object,
|
||||
representation=tool.Ifc.get().by_id(self.representation_id),
|
||||
)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
"Representation promoted to type. "
|
||||
f"{counts['occurrences']} occurrence(s) now inherit it "
|
||||
f"({counts['replaced']} local representation(s) replaced).",
|
||||
)
|
||||
|
||||
|
||||
class PurgeUnusedRepresentations(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.purge_unused_representations"
|
||||
bl_label = "Purge Unused Representations"
|
||||
|
||||
@@ -159,7 +159,7 @@ class BIM_PT_representations(Panel):
|
||||
header.label(text="", icon="BLANK1")
|
||||
header.label(text="", icon="BLANK1")
|
||||
|
||||
for representation in RepresentationsData.data["representations"]:
|
||||
def draw_representation_row(representation, allow_promote=False):
|
||||
row = self.layout.row(align=True)
|
||||
row.label(text=representation["ContextType"])
|
||||
row.label(text=representation["ContextIdentifier"])
|
||||
@@ -171,6 +171,10 @@ class BIM_PT_representations(Panel):
|
||||
emboss=False,
|
||||
)
|
||||
op.representation_type = representation["RepresentationType"]
|
||||
if allow_promote:
|
||||
row.operator(
|
||||
"bim.promote_representation_to_type", icon="EXPORT", text=""
|
||||
).representation_id = representation["id"]
|
||||
op = row.operator(
|
||||
"bim.switch_representation",
|
||||
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
|
||||
@@ -180,6 +184,28 @@ class BIM_PT_representations(Panel):
|
||||
op.disable_opening_subtractions = False
|
||||
row.operator("bim.remove_representation", icon="X", text="").representation_id = representation["id"]
|
||||
|
||||
representations = RepresentationsData.data["representations"]
|
||||
# For a type element every representation is its own; for an occurrence
|
||||
# split them into those inherited from the type (mapped) vs those local
|
||||
# to the occurrence, so it's clear which are driven by the type.
|
||||
if RepresentationsData.data["element_is_type"]:
|
||||
for representation in representations:
|
||||
draw_representation_row(representation)
|
||||
else:
|
||||
type_representations = [r for r in representations if r["is_mapped"]]
|
||||
occurrence_representations = [r for r in representations if not r["is_mapped"]]
|
||||
# A local representation can be promoted onto the type only when the
|
||||
# occurrence actually has a type to promote it onto.
|
||||
allow_promote = RepresentationsData.data["element_has_type"]
|
||||
if type_representations:
|
||||
self.layout.label(text="Type", icon="LINKED")
|
||||
for representation in type_representations:
|
||||
draw_representation_row(representation)
|
||||
if occurrence_representations:
|
||||
self.layout.label(text="Occurrence", icon="OBJECT_DATA")
|
||||
for representation in occurrence_representations:
|
||||
draw_representation_row(representation, allow_promote=allow_promote)
|
||||
|
||||
# Presentation layers.
|
||||
self.layout.separator()
|
||||
if not LayersData.is_loaded:
|
||||
|
||||
@@ -176,6 +176,96 @@ def remove_representation(
|
||||
geometry.delete_data(data)
|
||||
|
||||
|
||||
def promote_representation_to_type(
|
||||
ifc: type[tool.Ifc],
|
||||
geometry: type[tool.Geometry],
|
||||
obj: bpy.types.Object,
|
||||
representation: ifcopenshell.entity_instance,
|
||||
) -> dict[str, int]:
|
||||
"""Move an occurrence-local representation onto its type (slot-based, type wins).
|
||||
|
||||
The representation is copied onto the type as a mapped representation. Then,
|
||||
for every occurrence of the type, **any** existing representation in the same
|
||||
*slot* — same context (context/subcontext/target view), same
|
||||
``RepresentationIdentifier`` and same ``RepresentationType`` — is removed and
|
||||
replaced by the type's mapped representation. This covers both a local
|
||||
(non-mapped) rep and an already-mapped rep the occurrence inherited from
|
||||
another map (e.g. a floating ``IfcRepresentationMap`` not anchored to the
|
||||
type, as Revit exports), so no duplicate is left. If the type already holds a
|
||||
rep in the slot it is removed too, so promoting is idempotent / replaces
|
||||
rather than accumulating maps.
|
||||
|
||||
Geometry is NOT compared: the type's representation replaces the
|
||||
occurrence's for that slot, even when the occurrence's geometry differs
|
||||
(e.g. an independently meshed / mirrored / rotated instance) — such
|
||||
occurrences visibly adopt the type's geometry.
|
||||
|
||||
:return: counts dict with ``replaced`` (occurrence reps removed) and
|
||||
``occurrences`` (occurrences that now reference the type's mapped rep).
|
||||
"""
|
||||
element = ifc.get_entity(obj)
|
||||
assert element
|
||||
element_type = geometry.get_element_type(element)
|
||||
assert element_type, "Cannot promote a representation without a type."
|
||||
context = representation.ContextOfItems
|
||||
identifier = representation.RepresentationIdentifier
|
||||
# Compare the *resolved* type: an inherited rep is a "MappedRepresentation"
|
||||
# whose real type lives on the map's target, so matching the raw
|
||||
# RepresentationType would miss it and leave a duplicate.
|
||||
rep_type = geometry.resolve_mapped_representation(representation).RepresentationType
|
||||
|
||||
def _in_slot(r: ifcopenshell.entity_instance) -> bool:
|
||||
return (
|
||||
r.ContextOfItems == context
|
||||
and r.RepresentationIdentifier == identifier
|
||||
and geometry.resolve_mapped_representation(r).RepresentationType == rep_type
|
||||
)
|
||||
|
||||
# Copy the promoted geometry for the type up front, before any removal below
|
||||
# can touch the source occurrence's representation.
|
||||
type_representation = geometry.copy_representation_deep(representation)
|
||||
|
||||
# Snapshot every occurrence's reps in the slot (local AND mapped) so we can
|
||||
# replace them -- an occurrence may already inherit a mapped rep in the slot,
|
||||
# which would otherwise be left behind as a duplicate.
|
||||
occurrence_reps: dict[ifcopenshell.entity_instance, list[ifcopenshell.entity_instance]] = {}
|
||||
for occurrence in geometry.get_elements_of_type(element_type):
|
||||
occurrence_reps[occurrence] = [r for r in geometry.get_representations_iter(occurrence) if _in_slot(r)]
|
||||
|
||||
counts = {"replaced": 0, "occurrences": 0}
|
||||
# 1. Drop every existing rep in the slot from each occurrence.
|
||||
for occurrence, reps_in_slot in occurrence_reps.items():
|
||||
occurrence_obj = ifc.get_object(occurrence)
|
||||
for rep in reps_in_slot:
|
||||
if occurrence_obj is not None and not geometry.is_mapped_representation(rep):
|
||||
# Blender-aware removal for a local mesh the object may display.
|
||||
remove_representation(ifc, geometry, obj=occurrence_obj, representation=rep)
|
||||
else:
|
||||
# Mapped wrapper (or object not loaded): remove from this
|
||||
# occurrence only. remove_representation's remove_deep2 keeps a
|
||||
# shared map alive until its last user is gone.
|
||||
ifc.run("geometry.unassign_representation", product=occurrence, representation=rep)
|
||||
ifc.run("geometry.remove_representation", representation=rep)
|
||||
counts["replaced"] += 1
|
||||
|
||||
# 2. Drop any rep the type already holds in this slot, so we replace rather
|
||||
# than accumulate maps. (Must run before adding the new map below.)
|
||||
for rm in list(element_type.RepresentationMaps or []):
|
||||
mapped = rm.MappedRepresentation
|
||||
if mapped and _in_slot(mapped):
|
||||
ifc.run("geometry.unassign_representation", product=element_type, representation=mapped)
|
||||
ifc.run("geometry.remove_representation", representation=mapped)
|
||||
|
||||
# 3. Add the promoted geometry to the type and map it onto every occurrence.
|
||||
geometry.add_type_representation_map(element_type, type_representation)
|
||||
for occurrence in occurrence_reps:
|
||||
mapped_representation = ifc.run("geometry.map_representation", representation=type_representation)
|
||||
ifc.run("geometry.assign_representation", product=occurrence, representation=mapped_representation)
|
||||
counts["occurrences"] += 1
|
||||
|
||||
return counts
|
||||
|
||||
|
||||
def purge_unused_representations(ifc: type[tool.Ifc], geometry: type[tool.Geometry]) -> int:
|
||||
"""Purge representations without inverses.
|
||||
|
||||
|
||||
@@ -445,6 +445,8 @@ class Geometry:
|
||||
def clear_modifiers(cls, obj): pass
|
||||
def clear_scale(cls, obj): pass
|
||||
def copy_data_links(cls, data, copied_entities) -> None: pass
|
||||
def copy_representation_deep(cls, representation): pass
|
||||
def add_type_representation_map(cls, element_type, representation): pass
|
||||
def delete_data(cls, data): pass
|
||||
def delete_ifc_object(cls, obj): pass
|
||||
def delete_opening_object_placement(cls, opening): pass
|
||||
|
||||
@@ -888,6 +888,38 @@ class Geometry(bonsai.core.tool.Geometry):
|
||||
) -> Union[ifcopenshell.entity_instance, None]:
|
||||
return ifcopenshell.util.representation.get_representation(element, context)
|
||||
|
||||
@classmethod
|
||||
def copy_representation_deep(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||
"""Deep copy a representation, sharing geometric contexts and preserving
|
||||
named profiles (mirrors ``tool.Root.copy_representation``'s exclusions)."""
|
||||
|
||||
def exclude_callback(attribute: ifcopenshell.entity_instance) -> bool:
|
||||
return attribute.is_a("IfcProfileDef") and attribute.ProfileName
|
||||
|
||||
return ifcopenshell.util.element.copy_deep(
|
||||
tool.Ifc.get(),
|
||||
representation,
|
||||
exclude=["IfcGeometricRepresentationContext"],
|
||||
exclude_callback=exclude_callback,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def add_type_representation_map(
|
||||
cls, element_type: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
|
||||
) -> ifcopenshell.entity_instance:
|
||||
"""Register ``representation`` on ``element_type`` as a new
|
||||
``IfcRepresentationMap`` (mapping origin at the type's local origin).
|
||||
Subsequent ``geometry.map_representation`` calls reuse this map."""
|
||||
ifc_file = tool.Ifc.get()
|
||||
origin = ifc_file.createIfcAxis2Placement3D(
|
||||
ifc_file.createIfcCartesianPoint((0.0, 0.0, 0.0)),
|
||||
ifc_file.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
ifc_file.createIfcDirection((1.0, 0.0, 0.0)),
|
||||
)
|
||||
rep_map = ifc_file.createIfcRepresentationMap(origin, representation)
|
||||
element_type.RepresentationMaps = list(element_type.RepresentationMaps or []) + [rep_map]
|
||||
return rep_map
|
||||
|
||||
@classmethod
|
||||
def get_cartesian_point_offset(cls, obj: bpy.types.Object) -> npt.NDArray[np.float64] | None:
|
||||
props = tool.Blender.get_object_bim_props(obj)
|
||||
|
||||
Reference in New Issue
Block a user