Compare commits

...

6 Commits

Author SHA1 Message Date
Ryan Schultz c0b7f37fa0 Merge 'select-by-representation-type' (#7916) as ancestor for build (re-apply post-rebase) 2026-07-06 09:49:06 -05:00
Ryan Schultz 4ec042595e Preserve adjusted extrusion openings on duplicate
promote_opening_to_type now preserves an extrusion opening that was manually
adjusted away from the default - detected by comparing its bounding box to a
freshly generated default - not only non-extrusion geometry. The generate-and-
compare check is scoped to the duplicate path via should_preserve_opening.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:22:33 -05:00
Ryan Schultz 48f6e2908b Propagate edited void to all type occurrences
update_type_template_from_opening now re-maps every occurrence's opening onto
the type's Reference template (not only ones already sharing its map) and
reloads the affected host walls, so editing one void updates all instances
even when their openings were independent.

Generated with the assistance of an AI coding tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:22:33 -05:00
Ryan Schultz 1cd7e52c49 Helps with #7853: Select objects by RepresentationType from panel
Clicking the RepresentationType label in the Representations
panel selects all visible objects whose active representation
matches that type. Ctrl+Click broadens the selection to any
object that has the type in any of its representations,
whether currently active or not.

Generated with the assistance of an AI coding tool.
2026-07-05 15:22:33 -05:00
Ryan Schultz 2e6f17ed0f Preserve custom opening geometry via a type-level Reference template
Custom IfcOpeningElement voids (e.g. an IfcPolygonalFaceSet / tessellation)
were lost - reset to a default extrusion - on bim.duplicate_type, project
append, and type switching, because the void lived only on occurrences and
nothing carried it to a new type.

Anchor the shared opening body on the filling type as a 'Reference'
representation map (per IfcShapeRepresentation, 'Reference' is geometry "not
part of the Body representation", used for opening geometries excluded from an
implicit Boolean operation). bim.duplicate_type and append copy a type's
RepresentationMaps, so the template survives; generate_opening_from_filling
consults it before falling back to a generated extrusion.

- map_type_representations: skip 'Reference' maps so occurrences don't receive
  the opening shape as their own Body geometry.
- opening.py: get_/set_type_opening_representation, promote_opening_to_type,
  update_type_template_from_opening; pre/post type.assign_type listeners
  (anchor the old type's void before a switch; regenerate to the assigned
  type's void afterwards, replacing the previous "preserve custom" guard).
- DuplicateType promotes the void before copy; AppendLibraryElement harvests
  the template cross-file from a library instance.
- Write-back on void edit, hooked at both commit paths (UpdateRepresentation
  and OverrideModeSetObject).
- reimport_element_representations renders the requested representation, so
  switching a type to its Reference row shows the void rather than the body.
- Representations panel shows RepresentationIdentifier plus column headers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 15:21:49 -05:00
Ryan Schultz f59942c453 Helps with #7853: Select objects by RepresentationType from panel
Clicking the RepresentationType label in the Representations
panel selects all visible objects whose active representation
matches that type. Ctrl+Click broadens the selection to any
object that has the type in any of its representations,
whether currently active or not.

Generated with the assistance of an AI coding tool.
2026-04-07 10:10:42 -05:00
11 changed files with 448 additions and 2 deletions
@@ -69,6 +69,7 @@ classes = (
operator.RemoveRepresentation,
operator.RemoveRepresentationItem,
operator.RemoveRepresentationItemFromShapeAspect,
operator.SelectByRepresentationType,
operator.SelectConnection,
operator.SelectRepresentationItem,
operator.SwitchRepresentation,
@@ -138,6 +138,11 @@ class RepresentationsData:
"ContextType": representation.ContextOfItems.ContextType or "",
"ContextIdentifier": "",
"TargetView": "",
# The representation's own identifier (e.g. 'Body', 'Reference'), which is
# distinct from the subcontext's ContextIdentifier above. Two reps can share
# one context (e.g. a Body body and a Reference opening template), so showing
# this lets them be told apart in the panel.
"RepresentationIdentifier": representation.RepresentationIdentifier or "",
"RepresentationType": representation_type or "",
"is_active": is_active,
}
@@ -418,6 +418,55 @@ class SelectConnection(bpy.types.Operator, tool.Ifc.Operator):
core.select_connection(tool.Geometry, connection=tool.Ifc.get().by_id(self.connection))
class SelectByRepresentationType(bpy.types.Operator):
bl_idname = "bim.select_by_representation_type"
bl_label = "Select By Representation Type"
bl_description = (
"Select objects whose active representation matches this type. "
"Ctrl+Click to also include objects that have this type in any representation (active or not)"
)
bl_options = {"REGISTER", "UNDO"}
representation_type: bpy.props.StringProperty()
select_inactive: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
def invoke(self, context, event):
self.select_inactive = event.ctrl
return self.execute(context)
def execute(self, context):
ifc = tool.Ifc.get()
if not ifc:
return {"CANCELLED"}
# Strip the "*" suffix used for mapped/resolved representations.
target_type = self.representation_type.rstrip("*")
matched = 0
for obj in context.visible_objects:
element = tool.Ifc.get_entity(obj)
if not element:
obj.select_set(False)
continue
if self.select_inactive:
# Ctrl: match any representation on the element, active or not.
has_type = any(
(ifcopenshell.util.representation.resolve_representation(rep).RepresentationType or "") == target_type
for rep in ifcopenshell.util.representation.get_representations_iter(element)
)
else:
# Default: match only the currently active (displayed) representation.
active_rep = tool.Geometry.get_active_representation(obj)
if active_rep is None:
obj.select_set(False)
continue
resolved = ifcopenshell.util.representation.resolve_representation(active_rep)
has_type = (resolved.RepresentationType or "") == target_type
obj.select_set(has_type)
if has_type:
matched += 1
mode = "any representation" if self.select_inactive else "active representation"
self.report({"INFO"}, f"Selected {matched} object(s) with RepresentationType '{target_type}' ({mode})")
return {"FINISHED"}
class RemoveConnection(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_connection"
bl_label = "Remove Connection"
@@ -710,6 +759,16 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
if mprops.ifc_parameters:
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
# Persist an edited opening void onto its filling type's 'Reference' template so the
# change survives type duplication/append/switching and propagates to siblings. This
# catches the edited_objs commit path; the in-place item edit is caught in
# bim.override_mode_set_object.
edited_element = tool.Ifc.get_entity(obj)
if edited_element and edited_element.is_a("IfcOpeningElement"):
from bonsai.bim.module.model.opening import FilledOpeningGenerator
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
class UpdateParametricRepresentation(bpy.types.Operator):
bl_idname = "bim.update_parametric_representation"
@@ -2489,6 +2548,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
return bpy.ops.bim.edit_boundary_geometry()
elif tool.Geometry.is_representation_item(context.active_object):
self.edit_representation_item(context.active_object)
# If we just edited an opening's void item, persist the new shape onto the
# filling type's 'Reference' template so it survives type duplication/append/
# switching and propagates to siblings.
rep_obj = tool.Geometry.get_geometry_props().representation_obj
edited_element = tool.Ifc.get_entity(rep_obj) if rep_obj else None
if edited_element and edited_element.is_a("IfcOpeningElement"):
from bonsai.bim.module.model.opening import FilledOpeningGenerator
FilledOpeningGenerator().update_type_template_from_opening(edited_element)
tool.Root.reload_item_decorator()
# So you can keep hitting tab to cycle out of edit mode
context.active_object.select_set(False)
+18 -1
View File
@@ -148,12 +148,29 @@ class BIM_PT_representations(Panel):
self.layout.label(text="No Representations Found")
return
header = self.layout.row(align=True)
header.label(text="Context")
header.label(text="Subcontext")
header.label(text="View")
header.label(text="Identifier")
header.label(text="Type")
# Blank icon cells reserve the same width as the switch/remove buttons below so the
# text columns line up with the data rows.
header.label(text="", icon="BLANK1")
header.label(text="", icon="BLANK1")
for representation in RepresentationsData.data["representations"]:
row = self.layout.row(align=True)
row.label(text=representation["ContextType"])
row.label(text=representation["ContextIdentifier"])
row.label(text=representation["TargetView"])
row.label(text=representation["RepresentationType"])
row.label(text=representation["RepresentationIdentifier"])
op = row.operator(
"bim.select_by_representation_type",
text=representation["RepresentationType"],
emboss=False,
)
op.representation_type = representation["RepresentationType"]
op = row.operator(
"bim.switch_representation",
icon="FILE_REFRESH" if representation["is_active"] else "OUTLINER_DATA_MESH",
@@ -41,6 +41,12 @@ def load_post(*args):
profile.DumbProfileRegenerator().regenerate_from_profile,
)
ifcopenshell.api.add_pre_listener(
"type.assign_type",
"Bonsai.Opening.PreserveOnTypeChange",
opening.FilledOpeningGenerator().preserve_opening_on_type_change,
)
ifcopenshell.api.add_post_listener(
"type.assign_type",
"Bonsai.Opening.RegenerateFromType",
@@ -420,6 +420,25 @@ class FilledOpeningGenerator:
tool.Geometry.recut_host(voided_obj, representation)
def preserve_opening_on_type_change(
self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]
) -> None:
"""Pre-listener for type.assign_type: anchor the old type's void before reassigning.
A custom void that lives only on an occurrence (the type has no 'Reference'
template) would be lost when that occurrence is moved to another type - the
post-assign regeneration replaces it. Promoting it onto its current type first
keeps it durable, so switching back later restores it. Idempotent and only acts on
genuinely custom (non-extrusion) voids.
"""
relating_type = settings.get("relating_type")
for related_object in settings.get("related_objects") or []:
if not getattr(related_object, "FillsVoids", None):
continue
old_type = ifcopenshell.util.element.get_type(related_object)
if old_type and old_type != relating_type:
self.promote_opening_to_type(old_type)
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
relating_type = settings["relating_type"]
@@ -437,6 +456,13 @@ class FilledOpeningGenerator:
opening = filling.FillsVoids[0].RelatingOpeningElement
voided_element = opening.VoidsElements[0].RelatingBuildingElement
# Always regenerate the opening to reflect the *assigned* type's void: its
# 'Reference' template if it has one (generate_opening_from_filling consults it),
# else a sibling occurrence's opening, else a generated extrusion. We deliberately
# do NOT preserve the previous type's custom void on a type change - a custom void
# now survives duplicate_type/append by being anchored on the type as a template
# (promote_opening_to_type / harvest), so keeping the old void here would just show
# the wrong type's opening (e.g. switching to a plain type would keep the faceset).
opening_rep = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
ifcopenshell.api.geometry.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
@@ -493,6 +519,14 @@ class FilledOpeningGenerator:
profile = None
filling_type = ifcopenshell.util.element.get_type(filling)
if filling_type:
# A stored opening template (e.g. a custom IfcPolygonalFaceSet carried
# across bim.duplicate_type / append) takes priority over generating a
# default extrusion. Returning the shared template representation lets the
# caller's map_representation reuse its IfcRepresentationMap, so this
# opening stays in sync with the type template and its sibling occurrences.
opening_template = self.get_type_opening_representation(filling_type)
if opening_template is not None:
return opening_template
profile = ifcopenshell.util.representation.get_representation(
filling_type, "Model", "Profile", "ELEVATION_VIEW"
)
@@ -590,6 +624,228 @@ class FilledOpeningGenerator:
return True
return False
def is_opening_representation_custom(self, opening: ifcopenshell.entity_instance) -> bool:
"""Whether the opening's Body has user-authored geometry rather than a generated extrusion.
Openings produced by ``generate_opening_from_filling`` always consist of a
single ``IfcExtrudedAreaSolid``. Anything else (a tessellation such as an
``IfcPolygonalFaceSet``, a brep, a CSG solid, etc.) was authored by the user
and must not be silently replaced with a default extrusion.
"""
representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
if not representation:
return False
representation = ifcopenshell.util.representation.resolve_representation(representation)
return any(not item.is_a("IfcExtrudedAreaSolid") for item in representation.Items)
def should_preserve_opening(self, opening: ifcopenshell.entity_instance) -> bool:
"""Whether an opening's geometry is worth anchoring on the type as a template.
True for user-authored geometry (a tessellation, brep, etc.) or a *manually adjusted*
extrusion - one that no longer matches the default ``generate_opening_from_filling``
would produce for its filling. A plain generated extrusion is regenerable, so it
returns False and is left to regenerate.
"""
if self.is_opening_representation_custom(opening):
return True
return self._is_adjusted_extrusion(opening)
def _is_adjusted_extrusion(self, opening: ifcopenshell.entity_instance) -> bool:
"""Whether the opening's extrusion diverges from the default for its filling.
Generates the default transiently, compares the axis-aligned bounding boxes of the
two bodies (both in the opening's local frame), then removes the temporary default.
A conservative False is returned when the default cannot be computed.
"""
filling = opening.HasFillings[0].RelatedBuildingElement if getattr(opening, "HasFillings", None) else None
filling_obj = tool.Ifc.get_object(filling) if filling else None
current = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
if not filling_obj or current is None:
return False
current = ifcopenshell.util.representation.resolve_representation(current)
default_representation = self.generate_opening_from_filling(filling, filling_obj)
try:
settings = ifcopenshell.geom.settings()
current_bbox = self._representation_bbox(settings, current)
default_bbox = self._representation_bbox(settings, default_representation)
finally:
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=default_representation)
if current_bbox is None or default_bbox is None:
return False
(cur_min, cur_max), (def_min, def_max) = current_bbox, default_bbox
tolerance = 1e-3 # 1 mm; differing extents/position => manually adjusted
return bool(np.any(np.abs(cur_min - def_min) > tolerance) or np.any(np.abs(cur_max - def_max) > tolerance))
@staticmethod
def _representation_bbox(settings: Any, representation: ifcopenshell.entity_instance):
try:
geometry = ifcopenshell.geom.create_shape(settings, representation)
except Exception:
return None
verts = ifcopenshell.util.shape.get_vertices(geometry)
if len(verts) == 0:
return None
return verts.min(axis=0), verts.max(axis=0)
def get_type_opening_representation(
self, filling_type: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
"""Return the type's stored opening template (its 'Reference' representation), if any.
The template is the shared opening body anchored on the type as a
'Reference'-identified representation map (see
:meth:`set_type_opening_representation`). Storing it on the type lets a
custom opening survive ``bim.duplicate_type`` and project append, which copy
the type's ``RepresentationMaps`` but not an opening shared only between
occurrences.
"""
for representation_map in filling_type.RepresentationMaps or []:
representation = representation_map.MappedRepresentation
if representation.RepresentationIdentifier == "Reference":
return representation
def set_type_opening_representation(
self, filling_type: ifcopenshell.entity_instance, representation: ifcopenshell.entity_instance
) -> None:
"""Anchor an opening body representation on the type as its 'Reference' template.
``representation`` is tagged 'Reference' (so it is excluded from the
occurrence body geometry, see
``ifcopenshell.api.type.map_type_representations``) and the
``IfcRepresentationMap`` wrapping it is registered in the type's
``RepresentationMaps``, replacing any previous 'Reference' map. The existing map
is reused when present so that occurrences mapping over it stay in sync with the
type template. Idempotent.
"""
ifc_file = tool.Ifc.get()
representation.RepresentationIdentifier = "Reference"
representation_map = next(
(i for i in ifc_file.get_inverse(representation) if i.is_a("IfcRepresentationMap")), None
)
if representation_map is None:
mapping_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)),
)
representation_map = ifc_file.createIfcRepresentationMap(mapping_origin, representation)
# Keep all non-'Reference' maps (Body, Annotation, ...) plus this one, dropping any
# previous 'Reference' template so the type carries exactly one.
new_maps = [
m
for m in (filling_type.RepresentationMaps or [])
if m == representation_map or m.MappedRepresentation.RepresentationIdentifier != "Reference"
]
if representation_map not in new_maps:
new_maps.append(representation_map)
filling_type.RepresentationMaps = new_maps
def update_type_template_from_opening(self, opening: ifcopenshell.entity_instance) -> None:
"""Write an edited opening's geometry back to its filling type's 'Reference' template.
After a user edits an opening's void shape, anchor the new geometry on the type so
the change is durable (survives duplicate_type/append and switching the type away
and back) and propagates to sibling occurrences. Only acts on custom (non-extrusion)
geometry; a re-generated extrusion needs no template.
"""
if not getattr(opening, "HasFillings", None) or not self.is_opening_representation_custom(opening):
return
ifc_file = tool.Ifc.get()
new_representation = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
if not new_representation:
return
new_representation = ifcopenshell.util.representation.resolve_representation(new_representation)
voided_objs_to_reload: set[bpy.types.Object] = set()
for rel in opening.HasFillings:
filling_type = ifcopenshell.util.element.get_type(rel.RelatedBuildingElement)
if not filling_type:
continue
old_template = self.get_type_opening_representation(filling_type)
if old_template is not None and old_template != new_representation:
# The edit gave this opening its own geometry; re-point the shared template
# map - and therefore every sibling occurrence mapping over it - at the
# edited geometry, then drop the now-orphaned old template.
for inverse in ifc_file.get_inverse(old_template):
if inverse.is_a("IfcRepresentationMap"):
inverse.MappedRepresentation = new_representation
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_template)
self.set_type_opening_representation(filling_type, new_representation)
# Re-map every other occurrence's opening onto the type template so the edit
# propagates even to siblings that have their own independent opening geometry
# (i.e. openings that never shared the template's IfcRepresentationMap).
for occurrence in ifcopenshell.util.element.get_types(filling_type):
sibling_opening = (
occurrence.FillsVoids[0].RelatingOpeningElement
if getattr(occurrence, "FillsVoids", None)
else None
)
if not sibling_opening or sibling_opening == opening:
continue
if not self._remap_opening_to_template(sibling_opening, new_representation):
continue
if sibling_opening.VoidsElements:
voided_element = sibling_opening.VoidsElements[0].RelatingBuildingElement
for part in ifcopenshell.util.element.get_parts(voided_element) or [voided_element]:
if voided_obj := tool.Ifc.get_object(part):
voided_objs_to_reload.add(voided_obj)
# Reload affected host objects so the viewport re-booleans with the propagated void.
for voided_obj in voided_objs_to_reload:
representation = tool.Geometry.get_active_representation(voided_obj)
if representation:
bonsai.core.geometry.switch_representation(
tool.Ifc, tool.Geometry, obj=voided_obj, representation=representation
)
def _remap_opening_to_template(
self, opening: ifcopenshell.entity_instance, template_representation: ifcopenshell.entity_instance
) -> bool:
"""Point an opening's Body at the shared type template, purging its old standalone body.
:return: True if the opening was changed, False if it already maps over the template.
"""
ifc_file = tool.Ifc.get()
old_body = ifcopenshell.util.representation.get_representation(opening, "Model", "Body", "MODEL_VIEW")
if old_body is not None and (
ifcopenshell.util.representation.resolve_representation(old_body) == template_representation
):
return False
mapped_representation = ifcopenshell.api.geometry.map_representation(
ifc_file, representation=template_representation
)
# The mapped wrapper is the opening's own Body (the 'Reference' identifier belongs to
# the type template it maps over, not to the occurrence's representation).
mapped_representation.RepresentationIdentifier = "Body"
if old_body is not None:
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=opening, representation=old_body)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=opening, representation=mapped_representation)
return True
def promote_opening_to_type(self, filling_type: ifcopenshell.entity_instance) -> None:
"""Promote a custom opening from an occurrence to a 'Reference' template on the type.
Called before a type is copied (``bim.duplicate_type``) so that a custom
(non-extrusion) opening, currently shared only between occurrences, is
anchored on the type itself and therefore carried to the copy. No-op if the
type already has a template or has no custom opening to promote.
"""
if self.get_type_opening_representation(filling_type):
return
for occurrence in ifcopenshell.util.element.get_types(filling_type):
if not getattr(occurrence, "FillsVoids", None):
continue
opening = occurrence.FillsVoids[0].RelatingOpeningElement
if not self.should_preserve_opening(opening):
continue
representation = ifcopenshell.util.representation.get_representation(
opening, "Model", "Body", "MODEL_VIEW"
)
representation = ifcopenshell.util.representation.resolve_representation(representation)
self.set_type_opening_representation(filling_type, representation)
return
def get_existing_opening_occurrence_if_any(
self, filling: ifcopenshell.entity_instance
) -> Union[ifcopenshell.entity_instance, None]:
@@ -1000,6 +1256,9 @@ class EditOpenings(Operator, tool.Ifc.Operator):
building_objs.update(similar_openings_building_objs)
if opening_edited:
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
# Persist the edited void onto the filling type's 'Reference' template so
# it survives type duplication/append/switching and propagates to siblings.
self.update_type_template_from_opening(opening_element)
else:
bonsai.core.geometry.edit_object_placement(
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
@@ -633,6 +633,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
if not element:
return {"FINISHED"}
if element.is_a("IfcTypeProduct"):
self.harvest_opening_template(element, library_file)
self.import_type_from_ifc(element, context)
elif element.is_a("IfcProduct"):
# NOTE: Non-types are not exposed in UI directly
@@ -658,6 +659,57 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
bonsai.bim.handler.refresh_ui_data()
return {"FINISHED"}
def harvest_opening_template(
self, type_element: ifcopenshell.entity_instance, library_file: ifcopenshell.file
) -> None:
"""Seed the appended type's 'Reference' opening template from a library instance.
A type carries no opening of its own (openings are occurrence-level via
IfcRelVoidsElement), so a custom opening would otherwise be lost on append and
regenerated as a default extrusion when occurrences are placed. If the library
file has an instance of this type whose opening is custom (non-extrusion), copy
that opening body onto the appended type as its 'Reference' template. No-op when
the type already carries a template (e.g. a Bonsai-authored library) or the
library has no such instance.
"""
from bonsai.bim.module.model.opening import FilledOpeningGenerator
generator = FilledOpeningGenerator()
if generator.get_type_opening_representation(type_element):
return
library_type = library_file.by_id(self.definition)
if not library_type.is_a("IfcTypeProduct"):
return
for occurrence in ifcopenshell.util.element.get_types(library_type):
if not getattr(occurrence, "FillsVoids", None):
continue
opening = occurrence.FillsVoids[0].RelatingOpeningElement
library_representation = ifcopenshell.util.representation.get_representation(
opening, "Model", "Body", "MODEL_VIEW"
)
if not library_representation:
continue
library_representation = ifcopenshell.util.representation.resolve_representation(library_representation)
if all(item.is_a("IfcExtrudedAreaSolid") for item in library_representation.Items):
continue # A generated extrusion - nothing custom worth preserving.
project_file = tool.Ifc.get()
representation = project_file.add(library_representation)
# file.add brings the library's own representation context across; point the
# copy at the project's Body context and drop the now-orphaned duplicate.
body_context = ifcopenshell.util.representation.get_context(
project_file, "Model", "Body", "MODEL_VIEW"
)
if body_context and representation.ContextOfItems != body_context:
orphan_context = representation.ContextOfItems
representation.ContextOfItems = body_context
if not project_file.get_inverse(orphan_context):
project_file.remove(orphan_context)
generator.set_type_opening_representation(type_element, representation)
return
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
self.file = tool.Ifc.get()
logger = logging.getLogger("ImportIFC")
@@ -375,6 +375,14 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
obj = tool.Ifc.get_object(element)
if not obj:
return {"FINISHED"}
# Anchor any custom (non-extrusion) opening on the source type before the
# copy so it is carried to the duplicate as a 'Reference' template, rather
# than regenerated as a default extrusion on the new type's occurrences.
if element.is_a("IfcElementType"):
from bonsai.bim.module.model.opening import FilledOpeningGenerator
FilledOpeningGenerator().promote_opening_to_type(element)
new_obj = obj.copy()
if obj.data:
new_obj.data = obj.data.copy()
+17 -1
View File
@@ -1206,7 +1206,23 @@ class Geometry(bonsai.core.tool.Geometry):
for element in element_types:
if obj := tool.Ifc.get_object(element):
if representation := ifcopenshell.util.representation.get_representation(element, context):
# A type may hold several representations in one context (e.g. a 'Body' body
# plus a 'Reference' opening template), and get_representation() matches only
# by context. When base_representation is one of this type's own
# representations - i.e. we are reimporting it directly, such as switching to
# the Reference rep - render exactly that, otherwise the context lookup could
# return the wrong one. But element_types also contains each occurrence's
# type (see above), for which base_representation is not theirs; fall back to
# the context lookup there (and skip, as before, when it has none).
type_representations = [
ifcopenshell.util.representation.resolve_representation(rm.MappedRepresentation)
for rm in (element.RepresentationMaps or [])
]
if base_representation in type_representations:
representation = base_representation
else:
representation = ifcopenshell.util.representation.get_representation(element, context)
if representation:
geometry = ifcopenshell.geom.create_shape(settings, representation)
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
mesh = meshes.get(mesh_name)
+7
View File
@@ -2124,6 +2124,13 @@ class Model(bonsai.core.tool.Model):
if voided_obj is not None:
voided_objs.add(voided_obj)
# Preserve user-authored opening geometry (e.g. an IfcPolygonalFaceSet
# or other tessellation) instead of replacing it with a default extrusion.
from bonsai.bim.module.model.opening import FilledOpeningGenerator
if FilledOpeningGenerator().is_opening_representation_custom(opening):
continue
body = tool.Geometry.get_body_representation(opening)
if body is None:
continue
@@ -94,6 +94,13 @@ def map_type_representations(
ifcopenshell.api.geometry.remove_representation(file, representation=representation)
for representation_map in relating_type.RepresentationMaps:
representation = representation_map.MappedRepresentation
# 'Reference' representations are, per IfcShapeRepresentation, "not part of
# the Body representation" (used e.g. for opening geometries excluded from an
# implicit Boolean operation). They may be carried on a type purely as a
# template (e.g. a shared opening body) and must not be mapped onto
# occurrences as their own geometry.
if representation.RepresentationIdentifier == "Reference":
continue
mapped_representation = ifcopenshell.api.geometry.map_representation(file, representation=representation)
ifcopenshell.api.geometry.assign_representation(
file,