Fix covering geometry not persisting to IFC, same root cause as #7055

The covering tool used bmesh as an intermediate and relied on
type.assign_type post-listeners (removed in 44a52863a) to generate
the IfcExtrudedAreaSolid body. With those listeners gone, coverings
had no body representation and assign_swept_area_outer_curve crashed.

Build covering representations from scratch using ShapeBuilder, reading
the extrusion depth from the type's IfcMaterialLayerSet. Also replace
bpy.ops.bim.assign_class with bonsai.core.root.assign_class using
should_add_representation=False, consistent with the space fix.

Refactored shared coordinate-conversion and extrusion-building logic
into get_2d_vertices_from_polygon and set_extrusion_representation_from_polygon,
used by both space and covering code paths. Removed all bmesh-dependent
dead code from the spatial tool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-03-22 23:18:59 +11:00
parent ab96add772
commit 60ebb99fda
3 changed files with 84 additions and 214 deletions
+11 -61
View File
@@ -51,21 +51,11 @@ def add_instance_flooring_covering_from_cursor(
if isinstance(space_polygon, str): if isinstance(space_polygon, str):
return return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True) obj = spatial.create_object("Covering")
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj) spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z) spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj) spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_covering_from_cursor( def add_instance_ceiling_covering_from_cursor(
@@ -95,21 +85,11 @@ def add_instance_ceiling_covering_from_cursor(
if isinstance(space_polygon, str): if isinstance(space_polygon, str):
return return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True) obj = spatial.create_object("Covering")
name = "Covering"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
obj = spatial.get_named_obj_from_mesh(name, mesh)
spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj) spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj)
spatial.translate_obj_to_z_location(obj, z + ceiling_height) spatial.translate_obj_to_z_location(obj, z + ceiling_height)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj) spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None: def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spatial]) -> None:
@@ -127,19 +107,7 @@ def regen_selected_covering_object(root: type[tool.Root], spatial: type[tool.Spa
if isinstance(space_polygon, str): if isinstance(space_polygon, str):
return return
bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True) spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True)
name = "Aux"
mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm)
mesh = spatial.get_transformed_mesh_from_local_to_global(mesh)
obj = spatial.get_named_obj_from_mesh(name, mesh)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_swept_area_outer_curve_from_2d_vertices(active_obj, vertices=points)
body = spatial.get_body_representation(active_obj)
spatial.regen_obj_representation(active_obj, body)
# TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS # TODO CHECK IF IT IS POSSIBLE TO CREATE ONLY ONE CORE FUNCTION FOR _FROM_WALLS
@@ -151,22 +119,13 @@ def add_instance_flooring_coverings_from_walls(root: type[tool.Root], spatial: t
union = spatial.get_union_shape_from_selected_objects() union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors): for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i) name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.translate_obj_to_z_location(obj, z) spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj) spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
def add_instance_ceiling_coverings_from_walls( def add_instance_ceiling_coverings_from_walls(
@@ -179,22 +138,13 @@ def add_instance_ceiling_coverings_from_walls(
union = spatial.get_union_shape_from_selected_objects() union = spatial.get_union_shape_from_selected_objects()
for i, linear_ring in enumerate(union.interiors): for i, linear_ring in enumerate(union.interiors):
poly = spatial.get_buffered_poly_from_linear_ring(linear_ring) poly = spatial.get_buffered_poly_from_linear_ring(linear_ring)
bm = spatial.get_bmesh_from_polygon(poly, h=0, polygon_is_si=False)
name = "Covering" + str(i) name = "Covering" + str(i)
obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) obj = spatial.create_object(name)
spatial.set_obj_origin_to_polygon_center(obj, poly, polygon_is_si=False)
spatial.set_obj_origin_to_bboxcenter(obj)
spatial.translate_obj_to_z_location(obj, z) spatial.translate_obj_to_z_location(obj, z)
points = spatial.get_2d_vertices_from_obj(obj)
points = spatial.get_scaled_2d_vertices(points)
spatial.assign_type_to_obj(obj) spatial.assign_type_to_obj(obj)
spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False)
spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points)
body = spatial.get_body_representation(obj)
spatial.regen_obj_representation(obj, body)
class NoDefaultContainer(Exception): class NoDefaultContainer(Exception):
+3 -11
View File
@@ -999,15 +999,12 @@ class Spatial:
def get_purged_inner_holes_poly(cls, union_geom, min_area): pass def get_purged_inner_holes_poly(cls, union_geom, min_area): pass
def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass def get_poly_valid_interior_list(cls, poly, min_area, interiors_list): pass
def get_buffered_poly_from_linear_ring(cls, linear_ring): pass def get_buffered_poly_from_linear_ring(cls, linear_ring): pass
def get_bmesh_from_polygon(cls, poly, h, polygon_is_si=False): pass def get_2d_vertices_from_polygon(cls, poly, obj, polygon_is_si=True): pass
def get_named_obj_from_bmesh(cls, name, bmesh): pass def set_extrusion_representation_from_polygon(cls, obj, element, poly, depth_ifc, polygon_is_si=True): pass
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 set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass def set_space_representation_from_polygon(cls, obj, element, poly, h, polygon_is_si=True): pass
def set_covering_representation_from_polygon(cls, obj, poly, polygon_is_si=True): pass
def create_object(cls, name): 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_polygon_center(cls, obj, poly, polygon_is_si=True): pass
def set_obj_origin_to_bboxcenter(cls, obj): pass
def set_obj_origin_to_cursor_position_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_selected_objects(cls): pass
def get_active_obj(cls): pass def get_active_obj(cls): pass
@@ -1015,14 +1012,9 @@ class Spatial:
def get_active_obj_height(cls): pass def get_active_obj_height(cls): pass
def get_relating_type_id(cls): pass def get_relating_type_id(cls): pass
def translate_obj_to_z_location(cls, obj, z): pass def translate_obj_to_z_location(cls, obj, z): pass
def get_2d_vertices_from_obj(cls, obj): pass
def get_scaled_2d_vertices(cls, points): pass
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj, vertices): pass
def get_body_representation(cls, obj): pass
def assign_ifcspace_class_to_obj(cls, obj): pass def assign_ifcspace_class_to_obj(cls, obj): pass
def assign_type_to_obj(cls, obj): pass def assign_type_to_obj(cls, obj): pass
def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass def assign_relating_type_to_element(cls, ifc, type, element, relating_type): pass
def regen_obj_representation(cls, obj, body): pass
def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass def toggle_spaces_visibility_wired_and_textured(cls, spaces): pass
def toggle_hide_spaces(cls, spaces): pass def toggle_hide_spaces(cls, spaces): pass
def set_default_container(cls, container): pass def set_default_container(cls, container): pass
+70 -142
View File
@@ -21,7 +21,6 @@ from __future__ import annotations
import json import json
from collections import defaultdict from collections import defaultdict
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
from math import pi
from typing import TYPE_CHECKING, Any, Literal, Optional, Union from typing import TYPE_CHECKING, Any, Literal, Optional, Union
import bmesh import bmesh
@@ -991,49 +990,6 @@ class Spatial(bonsai.core.tool.Spatial):
) )
return poly return poly
@classmethod
def get_bmesh_from_polygon(cls, poly: Polygon, h: float, polygon_is_si: bool = False) -> bmesh.types.BMesh:
"""
:param h: Height, in meters.
:param polygon_is_si: Should be True if `poly` is defined in meters.
"""
mat = Matrix()
bm = bmesh.new()
bm.verts.index_update()
bm.edges.index_update()
mat_invert = mat.inverted()
si_conversion = 1.0 if polygon_is_si else ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
new_verts = [
# bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion)) for v in poly.exterior.coords[0:-1]
bm.verts.new(mat_invert @ (Vector([v[0], v[1], 0]) * si_conversion))
for v in shapely.get_exterior_ring(poly).coords[0:-1]
]
[bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0]))
bm.verts.index_update()
bm.edges.index_update()
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-5)
bmesh.ops.triangle_fill(bm, edges=bm.edges)
bmesh.ops.dissolve_limit(bm, angle_limit=pi / 180 * 5, verts=bm.verts, edges=bm.edges)
if h != 0:
extrusion = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
extruded_verts = [g for g in extrusion["geom"] if isinstance(g, bmesh.types.BMVert)]
bmesh.ops.translate(bm, vec=[0.0, 0.0, h], verts=extruded_verts)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
return bm
@classmethod
def get_named_obj_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Object:
mesh = cls.get_named_mesh_from_bmesh(name, bmesh)
obj = cls.get_named_obj_from_mesh(name, mesh)
return obj
@classmethod @classmethod
def create_object(cls, name: str) -> bpy.types.Object: def create_object(cls, name: str) -> bpy.types.Object:
mesh = bpy.data.meshes.new(name=name) mesh = bpy.data.meshes.new(name=name)
@@ -1051,47 +1007,23 @@ class Spatial(bonsai.core.tool.Spatial):
else: else:
obj.location = Vector((centroid.x * unit_scale, centroid.y * unit_scale, 0)) 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)
return obj
@classmethod @classmethod
def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh: def get_2d_vertices_from_polygon(
mesh = bpy.data.meshes.new(name=name)
bmesh.to_mesh(mesh)
bmesh.free()
return mesh
@classmethod
def get_transformed_mesh_from_local_to_global(cls, mesh: bpy.types.Mesh) -> bpy.types.Mesh:
active_obj = cls.get_active_obj()
mat = active_obj.matrix_world
mesh.transform(mat.inverted())
mesh.update()
return mesh
@classmethod
def set_space_representation_from_polygon(
cls, cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon, poly: Polygon,
h: float, obj: bpy.types.Object,
polygon_is_si: bool = True, polygon_is_si: bool = True,
) -> None: ) -> list[list[float]]:
"""Create or replace the IFC body representation of a space from a polygon. """Convert a world-space shapely polygon to 2D vertices in obj's local space, in IFC file units.
:param obj: The Blender object for the space. :param poly: The polygon in world space.
:param element: The IfcSpace entity. :param obj: The Blender object whose local space is used.
: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. :param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
:return: List of [x, y] coordinates (not closed).
""" """
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file) unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
bpy.context.view_layer.update() bpy.context.view_layer.update()
mat_inv = obj.matrix_world.inverted() mat_inv = obj.matrix_world.inverted()
coords_2d = [] coords_2d = []
@@ -1100,10 +1032,33 @@ class Spatial(bonsai.core.tool.Spatial):
if not polygon_is_si: if not polygon_is_si:
world_si = world_si * unit_scale world_si = world_si * unit_scale
local_si = mat_inv @ world_si local_si = mat_inv @ world_si
coords_2d.append(Vector((local_si.x, local_si.y)) / unit_scale) coords_2d.append([local_si.x / unit_scale, local_si.y / unit_scale])
return coords_2d
@classmethod
def set_extrusion_representation_from_polygon(
cls,
obj: bpy.types.Object,
element: ifcopenshell.entity_instance,
poly: Polygon,
depth_ifc: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation from a polygon extrusion.
:param obj: The Blender object.
:param element: The IFC product entity.
:param poly: The polygon in world space.
:param depth_ifc: The extrusion depth in IFC file units.
:param polygon_is_si: True if polygon coords are in SI, False if in IFC file units.
"""
ifc_file = tool.Ifc.get()
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
coords_2d = cls.get_2d_vertices_from_polygon(poly, obj, polygon_is_si)
curve = builder.polyline(coords_2d, closed=True) curve = builder.polyline(coords_2d, closed=True)
item = builder.extrude(curve, magnitude=h / unit_scale) item = builder.extrude(curve, magnitude=depth_ifc)
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW") old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
if old_body: if old_body:
@@ -1123,20 +1078,20 @@ class Spatial(bonsai.core.tool.Spatial):
) )
@classmethod @classmethod
def set_obj_origin_to_bboxcenter(cls, obj: bpy.types.Object) -> None: def set_space_representation_from_polygon(
mat = obj.matrix_world cls,
inverted = mat.inverted() obj: bpy.types.Object,
local_bbox_center = 0.125 * sum((Vector(b) for b in obj.bound_box), Vector()) element: ifcopenshell.entity_instance,
global_bbox_center = mat @ local_bbox_center poly: Polygon,
h: float,
polygon_is_si: bool = True,
) -> None:
"""Create or replace the IFC body representation of a space from a polygon.
oldLoc = obj.location :param h: The height in SI (meters).
newLoc = global_bbox_center """
diff = newLoc - oldLoc unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
for vert in obj.data.vertices: cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si)
aux_vector = mat @ vert.co
aux_vector = aux_vector - diff
vert.co = inverted @ aux_vector
obj.location = newLoc
@classmethod @classmethod
def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None: def set_obj_origin_to_cursor_position_and_zero_elevation(cls, obj: bpy.types.Object) -> None:
@@ -1183,47 +1138,6 @@ class Spatial(bonsai.core.tool.Spatial):
if z != 0: if z != 0:
obj.location = obj.location + Vector((0, 0, z)) obj.location = obj.location + Vector((0, 0, z))
@classmethod
def get_2d_vertices_from_obj(cls, obj: bpy.types.Object) -> list[tuple]:
points = []
vectors = [v.co for v in obj.data.vertices.values()]
for vector in vectors:
points.append(vector.xy)
points.append(vectors[0].xy)
return points
@classmethod
def get_scaled_2d_vertices(cls, points: list[Vector]) -> list[tuple[float, float]]:
model = tool.Ifc.get()
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(model)
_points = []
for p in points:
_p = list(p)
_p[0] /= unit_scale
_p[1] /= unit_scale
_points.append(_p)
return _points
@classmethod
def assign_swept_area_outer_curve_from_2d_vertices(cls, obj: bpy.types.Object, vertices: list[Vector]) -> None:
body = cls.get_body_representation(obj)
model = tool.Ifc.get()
extrusion = tool.Model.get_extrusion(body)
area = extrusion.SweptArea
old_area = area.OuterCurve
builder = ifcopenshell.util.shape_builder.ShapeBuilder(model)
outer_curve = builder.polyline(vertices, closed=True)
area.OuterCurve = outer_curve
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), old_area)
@classmethod
def get_body_representation(cls, obj: bpy.types.Object) -> Union[ifcopenshell.entity_instance, None]:
element = tool.Ifc.get_entity(obj)
return ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
@classmethod @classmethod
def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None: def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None:
bonsai.core.root.assign_class( bonsai.core.root.assign_class(
@@ -1237,18 +1151,40 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod @classmethod
def assign_type_to_obj(cls, obj: bpy.types.Object) -> None: def assign_type_to_obj(cls, obj: bpy.types.Object) -> None:
# TODO this code looks in the wrong spot and suspicious
props = tool.Model.get_model_props() props = tool.Model.get_model_props()
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
relating_type_id = props.relating_type_id relating_type_id = props.relating_type_id
relating_type = tool.Ifc.get().by_id(int(relating_type_id)) relating_type = ifc_file.by_id(int(relating_type_id))
ifc_class = relating_type.is_a() ifc_class = relating_type.is_a()
instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, ifc_file.schema)[0] instance_class = ifcopenshell.util.type.get_applicable_entities(ifc_class, ifc_file.schema)[0]
bpy.ops.bim.assign_class(obj=obj.name, ifc_class=instance_class) bonsai.core.root.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=instance_class,
should_add_representation=False,
)
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
assert element assert element
ifcopenshell.api.type.assign_type(ifc_file, related_objects=[element], relating_type=relating_type) ifcopenshell.api.type.assign_type(ifc_file, related_objects=[element], relating_type=relating_type)
@classmethod
def set_covering_representation_from_polygon(
cls,
obj: bpy.types.Object,
poly: Polygon,
polygon_is_si: bool = True,
) -> None:
"""Create the covering body representation from a polygon, extruded by the type's material layer thickness."""
element = tool.Ifc.get_entity(obj)
relating_type = ifcopenshell.util.element.get_type(element)
material = ifcopenshell.util.element.get_material(relating_type, should_skip_usage=True)
depth = 0.0
if material and material.is_a("IfcMaterialLayerSet"):
depth = sum(layer.LayerThickness for layer in material.MaterialLayers)
cls.set_extrusion_representation_from_polygon(obj, element, poly, depth, polygon_is_si)
@classmethod @classmethod
def assign_relating_type_to_element( def assign_relating_type_to_element(
cls, cls,
@@ -1259,14 +1195,6 @@ class Spatial(bonsai.core.tool.Spatial):
) -> None: ) -> None:
bonsai.core.type.assign_type(ifc, tool.Model, type, element=element, type=relating_type) bonsai.core.type.assign_type(ifc, tool.Model, type, element=element, type=relating_type)
@classmethod
def regen_obj_representation(cls, obj: bpy.types.Object, body: ifcopenshell.entity_instance) -> None:
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=body,
)
@classmethod @classmethod
def set_space_visibility(cls, is_visible: bool) -> None: def set_space_visibility(cls, is_visible: bool) -> None: