Fix space regen not saving geometry to IFC (#7055)

Space regeneration was only updating the Blender mesh and marking the
object as edited, but the IFC representation was never synced on save.
Replace the bmesh-based approach with ShapeBuilder to write geometry
directly to IFC as an IfcExtrudedAreaSolid, then reload via
switch_representation. This applies to both new space creation and
existing space regeneration.

Also changes assign_ifcspace_class_to_obj to call
bonsai.core.root.assign_class directly with
should_add_representation=False instead of bpy.ops.bim.assign_class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-03-22 22:13:17 +11:00
parent a3f2e061fb
commit d8de623086
3 changed files with 86 additions and 44 deletions
+8 -12
View File
@@ -219,13 +219,8 @@ def generate_space(
else:
assert space_polygon
bm = spatial.get_bmesh_from_polygon(space_polygon, h=h, polygon_is_si=True)
mesh = spatial.get_named_mesh_from_bmesh(name="Space", bmesh=bm)
if element and element.is_a("IfcSpace"):
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
spatial.edit_active_space_obj_from_mesh(mesh)
spatial.set_space_representation_from_polygon(active_obj, element, space_polygon, h, polygon_is_si=True)
spatial.translate_obj_to_z_location(active_obj, z)
else:
if relating_type:
@@ -233,12 +228,13 @@ def generate_space(
else:
name = "Space"
obj = spatial.get_named_obj_from_mesh(name, mesh)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, space_polygon, h, polygon_is_si=True)
if relating_type:
spatial.assign_relating_type_to_element(ifc, type, element, relating_type)
@@ -257,16 +253,16 @@ def generate_spaces_from_walls(
for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h, polygon_is_si=False)
name = "Space" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm)
spatial.set_obj_origin_to_bboxcenter_and_zero_elevation(obj)
obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.translate_obj_to_z_location(obj, z)
spatial.assign_ifcspace_class_to_obj(obj)
element = ifc.get_entity(obj)
spatial.set_space_representation_from_polygon(obj, element, poly, h, polygon_is_si=False)
def toggle_space_visibility(ifc: type[tool.Ifc], spatial: type[tool.Spatial]) -> None:
model = ifc.get()
+3 -2
View File
@@ -1004,9 +1004,10 @@ class Spatial:
def get_named_obj_from_mesh(cls, name, mesh): pass
def get_named_mesh_from_bmesh(cls, name, bmesh): pass
def get_transformed_mesh_from_local_to_global(cls, mesh): pass
def edit_active_space_obj_from_mesh(cls, mesh): pass
def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass
def create_object(cls, name): pass
def set_obj_origin_to_polygon_center(cls, obj, poly, polygon_is_si=True): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj): pass
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj): pass
def get_selected_objects(cls): pass
def get_active_obj(cls): pass
+75 -30
View File
@@ -28,6 +28,7 @@ import bmesh
import bpy
import ifcopenshell
import ifcopenshell.api.attribute
import ifcopenshell.api.geometry
import ifcopenshell.api.type
import ifcopenshell.geom
import ifcopenshell.util.classification
@@ -1033,6 +1034,23 @@ class Spatial(bonsai.core.tool.Spatial):
obj = cls.get_named_obj_from_mesh(name, mesh)
return obj
@classmethod
def create_object(cls, name: str) -> bpy.types.Object:
mesh = bpy.data.meshes.new(name=name)
obj = bpy.data.objects.new(name, mesh)
return obj
@classmethod
def set_obj_origin_to_polygon_center(
cls, obj: bpy.types.Object, poly: Polygon, polygon_is_si: bool = True
) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
centroid = poly.centroid
if polygon_is_si:
obj.location = Vector((centroid.x, centroid.y, 0))
else:
obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0))
@classmethod
def get_named_obj_from_mesh(cls, name: str, mesh: bpy.types.Mesh) -> bpy.types.Object:
obj = bpy.data.objects.new(name, mesh)
@@ -1054,17 +1072,55 @@ class Spatial(bonsai.core.tool.Spatial):
return mesh
@classmethod
def edit_active_space_obj_from_mesh(cls, mesh: bpy.types.Mesh) -> None:
active_obj = bpy.context.active_object
old_mesh = active_obj.data
old_mesh_name = old_mesh.name
assert active_obj and isinstance(old_mesh, bpy.types.Mesh)
tool.Geometry.get_mesh_props(mesh).ifc_definition_id = tool.Geometry.get_mesh_props(old_mesh).ifc_definition_id
tool.Geometry.change_object_data(active_obj, mesh, is_global=True)
tool.Ifc.edit(active_obj)
tool.Blender.remove_data_block(old_mesh)
# Rename after old mesh is removed to avoid .001 suffix.
mesh.name = old_mesh_name
def set_space_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
h: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation of a space from a polygon.
:param obj: The Blender object for the space.
:param element: The IfcSpace entity.
:param poly: The space polygon in world space.
:param h: The height in SI (meters).
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
"""
ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
bpy.context.view_layer.update()
mat_inv = obj.matrix_world.inverted()
coords_2d = []
for v in shapely.get_exterior_ring(poly).coords[:-1]:
world_si = Vector((v[0], v[1], 0))
if not polygon_is_si:
world_si = world_si * unit_scale
local_si = mat_inv @ world_si
coords_2d.append(Vector((local_si.x, local_si.y)) / unit_scale)
curve = builder.polyline(coords_2d, closed=True)
item = builder.extrude(curve, magnitude=h / unit_scale)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body:
context = old_body.ContextOfItems
ifcopenshell.api.geometry.unassign_representation(ifc_file, product=element, representation=old_body)
ifcopenshell.api.geometry.remove_representation(ifc_file, representation=old_body)
else:
context = ifcopenshell.util.representation.get_context(ifc_file, "Model", "Body", "MODEL_VIEW")
new_body = builder.get_representation(context, item)
ifcopenshell.api.geometry.assign_representation(ifc_file, product=element, representation=new_body)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=new_body,
)
@classmethod
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None:
@@ -1082,24 +1138,6 @@ class Spatial(bonsai.core.tool.Spatial):
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_bboxcenter_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
inverted = mat.inverted()
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector())
global_bbox_center = mat @ local_bbox_center
global_obj_origin = global_bbox_center
global_obj_origin.z = 0
oldLoc = obj.location
newLoc = global_obj_origin
diff = newLoc - oldLoc
for vert in obj.data.vertices:
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
mat = obj.matrix_world
@@ -1188,7 +1226,14 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
bpy.ops.bim.assign_class(obj=obj.name, ifc_class="IfcSpace")
bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class="IfcSpace",
should_add_representation=False,
)
@classmethod
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None: