diff --git a/src/bonsai/bonsai/core/covering.py b/src/bonsai/bonsai/core/covering.py index a8c7a73f25..2ab0d39cc4 100644 --- a/src/bonsai/bonsai/core/covering.py +++ b/src/bonsai/bonsai/core/covering.py @@ -51,21 +51,11 @@ def add_instance_flooring_covering_from_cursor( if isinstance(space_polygon, str): return - bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True) - name = "Covering" - mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm) - - obj = spatial.get_named_obj_from_mesh(name, mesh) - + obj = spatial.create_object("Covering") spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj) 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_swept_area_outer_curve_from_2d_vertices(obj, vertices=points) - body = spatial.get_body_representation(obj) - spatial.regen_obj_representation(obj, body) + spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True) def add_instance_ceiling_covering_from_cursor( @@ -95,21 +85,11 @@ def add_instance_ceiling_covering_from_cursor( if isinstance(space_polygon, str): return - bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, polygon_is_si=True) - name = "Covering" - mesh = spatial.get_named_mesh_from_bmesh(name=name, bmesh=bm) - - obj = spatial.get_named_obj_from_mesh(name, mesh) - + obj = spatial.create_object("Covering") spatial.set_obj_origin_to_cursor_position_and_zero_elevation(obj) 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_swept_area_outer_curve_from_2d_vertices(obj, vertices=points) - body = spatial.get_body_representation(obj) - spatial.regen_obj_representation(obj, body) + spatial.set_covering_representation_from_polygon(obj, space_polygon, polygon_is_si=True) 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): return - bm = spatial.get_bmesh_from_polygon(space_polygon, h=0, 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) + spatial.set_covering_representation_from_polygon(active_obj, space_polygon, polygon_is_si=True) # 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() 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=0, polygon_is_si=False) name = "Covering" + str(i) - obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) - - spatial.set_obj_origin_to_bboxcenter(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) - - points = spatial.get_2d_vertices_from_obj(obj) - points = spatial.get_scaled_2d_vertices(points) - spatial.assign_type_to_obj(obj) - - spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points) - body = spatial.get_body_representation(obj) - spatial.regen_obj_representation(obj, body) + spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False) 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() 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=0, polygon_is_si=False) name = "Covering" + str(i) - obj = spatial.get_named_obj_from_bmesh(name, bmesh=bm) - - spatial.set_obj_origin_to_bboxcenter(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) - - points = spatial.get_2d_vertices_from_obj(obj) - points = spatial.get_scaled_2d_vertices(points) - spatial.assign_type_to_obj(obj) - - spatial.assign_swept_area_outer_curve_from_2d_vertices(obj, vertices=points) - body = spatial.get_body_representation(obj) - spatial.regen_obj_representation(obj, body) + spatial.set_covering_representation_from_polygon(obj, poly, polygon_is_si=False) class NoDefaultContainer(Exception): diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index fa3ba8a860..f815bf0963 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -999,15 +999,12 @@ class Spatial: 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_buffered_poly_from_linear_ring(cls, linear_ring): pass - def get_bmesh_from_polygon(cls, poly, h, polygon_is_si=False): pass - def get_named_obj_from_bmesh(cls, name, bmesh): 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 get_2d_vertices_from_polygon(cls, poly, obj, polygon_is_si=True): pass + def set_extrusion_representation_from_polygon(cls, obj, element, poly, depth_ifc, 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 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 get_selected_objects(cls): pass def get_active_obj(cls): pass @@ -1015,14 +1012,9 @@ class Spatial: def get_active_obj_height(cls): pass def get_relating_type_id(cls): 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_type_to_obj(cls, obj): 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_hide_spaces(cls, spaces): pass def set_default_container(cls, container): pass diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index e598f3fb9d..7bbdb28904 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -21,7 +21,6 @@ from __future__ import annotations import json from collections import defaultdict from collections.abc import Generator, Iterable -from math import pi from typing import TYPE_CHECKING, Any, Literal, Optional, Union import bmesh @@ -991,49 +990,6 @@ class Spatial(bonsai.core.tool.Spatial): ) 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 def create_object(cls, name: str) -> bpy.types.Object: mesh = bpy.data.meshes.new(name=name) @@ -1051,47 +1007,23 @@ class Spatial(bonsai.core.tool.Spatial): 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) - return obj @classmethod - def get_named_mesh_from_bmesh(cls, name: str, bmesh: bmesh.types.BMesh) -> bpy.types.Mesh: - 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( + def get_2d_vertices_from_polygon( cls, - obj: bpy.types.Object, - element: ifcopenshell.entity_instance, poly: Polygon, - h: float, + obj: bpy.types.Object, polygon_is_si: bool = True, - ) -> None: - """Create or replace the IFC body representation of a space from a polygon. + ) -> list[list[float]]: + """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 element: The IfcSpace entity. - :param poly: The space polygon in world space. - :param h: The height in SI (meters). + :param poly: The polygon in world space. + :param obj: The Blender object whose local space is used. :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() 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 = [] @@ -1100,10 +1032,33 @@ class Spatial(bonsai.core.tool.Spatial): 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) + 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) - 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") if old_body: @@ -1123,20 +1078,20 @@ class Spatial(bonsai.core.tool.Spatial): ) @classmethod - def set_obj_origin_to_bboxcenter(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 + 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. - oldLoc = obj.location - newLoc = global_bbox_center - 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 + :param h: The height in SI (meters). + """ + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + cls.set_extrusion_representation_from_polygon(obj, element, poly, h / unit_scale, polygon_is_si) @classmethod 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: 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 def assign_ifcspace_class_to_obj(cls, obj: bpy.types.Object) -> None: bonsai.core.root.assign_class( @@ -1237,18 +1151,40 @@ class Spatial(bonsai.core.tool.Spatial): @classmethod 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() ifc_file = tool.Ifc.get() 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() 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) assert element 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 def assign_relating_type_to_element( cls, @@ -1259,14 +1195,6 @@ class Spatial(bonsai.core.tool.Spatial): ) -> None: 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 def set_space_visibility(cls, is_visible: bool) -> None: