mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-08 00:41:36 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0b7f37fa0 | |||
| 4ec042595e | |||
| 48f6e2908b | |||
| 1cd7e52c49 | |||
| 2e6f17ed0f | |||
| f59942c453 |
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user