mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 14:41:25 +00:00
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>
This commit is contained in:
@@ -138,6 +138,11 @@ class RepresentationsData:
|
|||||||
"ContextType": representation.ContextOfItems.ContextType or "",
|
"ContextType": representation.ContextOfItems.ContextType or "",
|
||||||
"ContextIdentifier": "",
|
"ContextIdentifier": "",
|
||||||
"TargetView": "",
|
"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 "",
|
"RepresentationType": representation_type or "",
|
||||||
"is_active": is_active,
|
"is_active": is_active,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -710,6 +710,16 @@ class UpdateRepresentation(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if mprops.ifc_parameters:
|
if mprops.ifc_parameters:
|
||||||
core.get_representation_ifc_parameters(tool.Geometry, obj=obj)
|
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):
|
class UpdateParametricRepresentation(bpy.types.Operator):
|
||||||
bl_idname = "bim.update_parametric_representation"
|
bl_idname = "bim.update_parametric_representation"
|
||||||
@@ -2489,6 +2499,15 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
return bpy.ops.bim.edit_boundary_geometry()
|
return bpy.ops.bim.edit_boundary_geometry()
|
||||||
elif tool.Geometry.is_representation_item(context.active_object):
|
elif tool.Geometry.is_representation_item(context.active_object):
|
||||||
self.edit_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()
|
tool.Root.reload_item_decorator()
|
||||||
# So you can keep hitting tab to cycle out of edit mode
|
# So you can keep hitting tab to cycle out of edit mode
|
||||||
context.active_object.select_set(False)
|
context.active_object.select_set(False)
|
||||||
|
|||||||
@@ -148,11 +148,23 @@ class BIM_PT_representations(Panel):
|
|||||||
self.layout.label(text="No Representations Found")
|
self.layout.label(text="No Representations Found")
|
||||||
return
|
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"]:
|
for representation in RepresentationsData.data["representations"]:
|
||||||
row = self.layout.row(align=True)
|
row = self.layout.row(align=True)
|
||||||
row.label(text=representation["ContextType"])
|
row.label(text=representation["ContextType"])
|
||||||
row.label(text=representation["ContextIdentifier"])
|
row.label(text=representation["ContextIdentifier"])
|
||||||
row.label(text=representation["TargetView"])
|
row.label(text=representation["TargetView"])
|
||||||
|
row.label(text=representation["RepresentationIdentifier"])
|
||||||
row.label(text=representation["RepresentationType"])
|
row.label(text=representation["RepresentationType"])
|
||||||
op = row.operator(
|
op = row.operator(
|
||||||
"bim.switch_representation",
|
"bim.switch_representation",
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ def load_post(*args):
|
|||||||
profile.DumbProfileRegenerator().regenerate_from_profile,
|
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(
|
ifcopenshell.api.add_post_listener(
|
||||||
"type.assign_type",
|
"type.assign_type",
|
||||||
"Bonsai.Opening.RegenerateFromType",
|
"Bonsai.Opening.RegenerateFromType",
|
||||||
|
|||||||
@@ -420,6 +420,25 @@ class FilledOpeningGenerator:
|
|||||||
|
|
||||||
tool.Geometry.recut_host(voided_obj, representation)
|
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:
|
def regenerate_from_type(self, usecase_path: str, ifc_file: ifcopenshell.file, settings: dict[str, Any]) -> None:
|
||||||
relating_type = settings["relating_type"]
|
relating_type = settings["relating_type"]
|
||||||
|
|
||||||
@@ -437,6 +456,13 @@ class FilledOpeningGenerator:
|
|||||||
opening = filling.FillsVoids[0].RelatingOpeningElement
|
opening = filling.FillsVoids[0].RelatingOpeningElement
|
||||||
voided_element = opening.VoidsElements[0].RelatingBuildingElement
|
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")
|
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.unassign_representation(tool.Ifc.get(), product=opening, representation=opening_rep)
|
||||||
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
ifcopenshell.api.geometry.remove_representation(tool.Ifc.get(), representation=opening_rep)
|
||||||
@@ -493,6 +519,14 @@ class FilledOpeningGenerator:
|
|||||||
profile = None
|
profile = None
|
||||||
filling_type = ifcopenshell.util.element.get_type(filling)
|
filling_type = ifcopenshell.util.element.get_type(filling)
|
||||||
if filling_type:
|
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(
|
profile = ifcopenshell.util.representation.get_representation(
|
||||||
filling_type, "Model", "Profile", "ELEVATION_VIEW"
|
filling_type, "Model", "Profile", "ELEVATION_VIEW"
|
||||||
)
|
)
|
||||||
@@ -590,6 +624,126 @@ class FilledOpeningGenerator:
|
|||||||
return True
|
return True
|
||||||
return False
|
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 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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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.is_opening_representation_custom(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(
|
def get_existing_opening_occurrence_if_any(
|
||||||
self, filling: ifcopenshell.entity_instance
|
self, filling: ifcopenshell.entity_instance
|
||||||
) -> Union[ifcopenshell.entity_instance, None]:
|
) -> Union[ifcopenshell.entity_instance, None]:
|
||||||
@@ -1000,6 +1154,9 @@ class EditOpenings(Operator, tool.Ifc.Operator):
|
|||||||
building_objs.update(similar_openings_building_objs)
|
building_objs.update(similar_openings_building_objs)
|
||||||
if opening_edited:
|
if opening_edited:
|
||||||
tool.Geometry.run_geometry_update_representation(obj=opening_obj)
|
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:
|
else:
|
||||||
bonsai.core.geometry.edit_object_placement(
|
bonsai.core.geometry.edit_object_placement(
|
||||||
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
tool.Ifc, tool.Geometry, tool.Surveyor, obj=opening_obj
|
||||||
|
|||||||
@@ -633,6 +633,7 @@ class AppendLibraryElement(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not element:
|
if not element:
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
if element.is_a("IfcTypeProduct"):
|
if element.is_a("IfcTypeProduct"):
|
||||||
|
self.harvest_opening_template(element, library_file)
|
||||||
self.import_type_from_ifc(element, context)
|
self.import_type_from_ifc(element, context)
|
||||||
elif element.is_a("IfcProduct"):
|
elif element.is_a("IfcProduct"):
|
||||||
# NOTE: Non-types are not exposed in UI directly
|
# 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()
|
bonsai.bim.handler.refresh_ui_data()
|
||||||
return {"FINISHED"}
|
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:
|
def import_material_from_ifc(self, element: ifcopenshell.entity_instance, context: bpy.types.Context) -> None:
|
||||||
self.file = tool.Ifc.get()
|
self.file = tool.Ifc.get()
|
||||||
logger = logging.getLogger("ImportIFC")
|
logger = logging.getLogger("ImportIFC")
|
||||||
|
|||||||
@@ -375,6 +375,14 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
obj = tool.Ifc.get_object(element)
|
obj = tool.Ifc.get_object(element)
|
||||||
if not obj:
|
if not obj:
|
||||||
return {"FINISHED"}
|
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()
|
new_obj = obj.copy()
|
||||||
if obj.data:
|
if obj.data:
|
||||||
new_obj.data = obj.data.copy()
|
new_obj.data = obj.data.copy()
|
||||||
|
|||||||
@@ -1206,7 +1206,23 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
|
|
||||||
for element in element_types:
|
for element in element_types:
|
||||||
if obj := tool.Ifc.get_object(element):
|
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)
|
geometry = ifcopenshell.geom.create_shape(settings, representation)
|
||||||
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
|
mesh_name = tool.Loader.get_mesh_name_from_shape(geometry)
|
||||||
mesh = meshes.get(mesh_name)
|
mesh = meshes.get(mesh_name)
|
||||||
|
|||||||
@@ -2124,6 +2124,13 @@ class Model(bonsai.core.tool.Model):
|
|||||||
if voided_obj is not None:
|
if voided_obj is not None:
|
||||||
voided_objs.add(voided_obj)
|
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)
|
body = tool.Geometry.get_body_representation(opening)
|
||||||
if body is None:
|
if body is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ def map_type_representations(
|
|||||||
ifcopenshell.api.geometry.remove_representation(file, representation=representation)
|
ifcopenshell.api.geometry.remove_representation(file, representation=representation)
|
||||||
for representation_map in relating_type.RepresentationMaps:
|
for representation_map in relating_type.RepresentationMaps:
|
||||||
representation = representation_map.MappedRepresentation
|
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)
|
mapped_representation = ifcopenshell.api.geometry.map_representation(file, representation=representation)
|
||||||
ifcopenshell.api.geometry.assign_representation(
|
ifcopenshell.api.geometry.assign_representation(
|
||||||
file,
|
file,
|
||||||
|
|||||||
Reference in New Issue
Block a user