diff --git a/src/bonsai/bonsai/bim/module/model/__init__.py b/src/bonsai/bonsai/bim/module/model/__init__.py index 9dbb766399..a8be9df249 100644 --- a/src/bonsai/bonsai/bim/module/model/__init__.py +++ b/src/bonsai/bonsai/bim/module/model/__init__.py @@ -77,6 +77,7 @@ classes = ( wall.ExtendWallsToWall, wall.FlipWall, wall.MergeWall, + wall.OffsetWalls, wall.RecalculateWall, wall.SplitWall, wall.UnjoinWalls, diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 5789f6cb71..505f6d7664 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -599,6 +599,7 @@ class PolylineOperator: "Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]}, "Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]}, "Close Polyline": {"icons": True, "keys": ["EVENT_C"]}, + "Offset": {"icons": True, "keys": ["EVENT_O"]}, "Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]}, } diff --git a/src/bonsai/bonsai/bim/module/model/profile.py b/src/bonsai/bonsai/bim/module/model/profile.py index ea3f16ed20..c0fa809515 100644 --- a/src/bonsai/bonsai/bim/module/model/profile.py +++ b/src/bonsai/bonsai/bim/module/model/profile.py @@ -36,7 +36,6 @@ import bonsai.core.root from bonsai.bim.ifc import IfcStore from math import pi, degrees, atan2 from mathutils import Vector, Matrix -from bonsai.bim.module.model.wall import DumbWallRecalculator from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.polyline import PolylineOperator from typing import Union, Any, Optional @@ -969,7 +968,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator): obj.matrix_world @= rotate_matrix bpy.context.view_layer.update() DumbProfileRecalculator().recalculate(profile_objs) - DumbWallRecalculator().recalculate(layer2_objs) + tool.Model.recalculate_walls(layer2_objs) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 66c962b2ae..04f39dd33a 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup): items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")], name="Vertical Layer Offset Type", default="EXTERIOR", - description="It's a convention that affects the offset to reference line", + description="Offset convention to reference line", ) offset_type_horizontal: bpy.props.EnumProperty( items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")], name="Horizontal Layer Offset Type", default="TOP", - description="It's a convention that affects the offset to reference line", + description="Offset convention to reference line", ) offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line") show_wall_axis: bpy.props.BoolProperty( diff --git a/src/bonsai/bonsai/bim/module/model/slab.py b/src/bonsai/bonsai/bim/module/model/slab.py index ab1e8f517e..15a742aa5b 100644 --- a/src/bonsai/bonsai/bim/module/model/slab.py +++ b/src/bonsai/bonsai/bim/module/model/slab.py @@ -36,7 +36,6 @@ from math import cos, pi from mathutils import Vector, Matrix from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.polyline import PolylineOperator -from bonsai.bim.module.model.wall import DumbWallRecalculator from typing import Optional @@ -1035,5 +1034,5 @@ class RecalculateSlab(bpy.types.Operator, tool.Ifc.Operator): if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"): walls.append(tool.Ifc.get_object(rel.RelatedElement)) - DumbWallRecalculator().recalculate(walls) + tool.Model.recalculate_walls(walls) return {"FINISHED"} diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index e7b422c29d..7131307b82 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -117,8 +117,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator): ifcopenshell.api.geometry.connect_wall( tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True ) - joiner.recreate_wall(element, obj) - joiner.recreate_wall(target_element, target_obj) + tool.Model.recreate_wall(element, obj) + tool.Model.recreate_wall(target_element, target_obj) else: self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element") @@ -129,10 +129,10 @@ class AlignWall(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} bl_description = """ Align the selected walls to the active wall: 'Ext.': align to the EXTERIOR face - 'C/L': align to wall CENTERLINE + 'C/L': align to wall CENTER 'Int.': align to the INTERIOR face""" - AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"] + AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"] align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration] items=((i, i, "") for i in get_args(AlignType)) ) @@ -233,7 +233,7 @@ class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator): def _execute(self, context): objects = tool.Model.get_selected_mesh_ifc_objects() - DumbWallRecalculator().recalculate(objects) + tool.Model.recalculate_walls(objects) return {"FINISHED"} @@ -280,7 +280,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator): layer2_objs.append(obj) if layer2_objs: - DumbWallRecalculator().recalculate(layer2_objs) + tool.Model.recalculate_walls(layer2_objs) return {"FINISHED"} @@ -393,7 +393,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator): obj.rotation_euler.z = current_z_rot if layer2_objs: - DumbWallRecalculator().recalculate(layer2_objs) + tool.Model.recalculate_walls(layer2_objs) return {"FINISHED"} @@ -416,7 +416,24 @@ class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator): selected_objs = tool.Model.get_selected_mesh_ifc_objects() for obj in selected_objs: joiner.set_length(obj, self.length) - return {"FINISHED"} + + +class OffsetWalls(bpy.types.Operator, tool.Ifc.Operator): + bl_idname = "bim.offset_walls" + bl_label = "Offset Walls" + bl_description = "Offset selected objects from their reference line." + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + if not tool.Model.has_selected_mesh_ifc_objects(): + cls.poll_message_set("No mesh IFC objects selected.") + return False + return True + + def _execute(self, context): + props = tool.Model.get_model_props() + core.offset_walls(tool.Ifc, tool.Blender, tool.Model, props.offset_type_vertical) class AddWallsFromSlab(bpy.types.Operator, tool.Ifc.Operator): @@ -487,12 +504,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator): material_set_usage = model.by_id(material.id()) # if material.is_a("IfcMaterialLayerSetUsage"): attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense} - ifcopenshell.api.run( - "material.edit_layer_usage", - model, - **{"usage": material_set_usage, "attributes": attributes}, - ) - DumbWallRecalculator().recalculate([wall["obj"]]) + ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes) + tool.Model.recalculate_walls([wall["obj"]]) if walls: if is_polyline_closed: @@ -682,30 +695,6 @@ class DumbWallAligner: return round(degrees(angle) % 360) == 180 -class DumbWallRecalculator: - def recalculate(self, walls: list[bpy.types.Object]) -> None: - queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set() - for wall in walls: - element = tool.Ifc.get_entity(wall) - if tool.Ifc.is_moved(wall): - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall) - queue.add((element, wall)) - for rel in getattr(element, "ConnectedTo", []): - obj = tool.Ifc.get_object(rel.RelatedElement) - if tool.Ifc.is_moved(obj): - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - queue.add((rel.RelatedElement, obj)) - for rel in getattr(element, "ConnectedFrom", []): - obj = tool.Ifc.get_object(rel.RelatingElement) - if tool.Ifc.is_moved(obj): - bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) - queue.add((rel.RelatingElement, obj)) - joiner = DumbWallJoiner() - for element, wall in queue: - if tool.Model.get_usage_type(element) == "LAYER2" and wall: - joiner.recreate_wall(element, wall) - - class DumbWallGenerator: def __init__(self, relating_type): self.relating_type = relating_type @@ -937,7 +926,7 @@ class DumbWallPlaner: else: for rel in inverse.AssociatedTo: walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects]) - DumbWallRecalculator().recalculate([w for w in set(walls) if w]) + tool.Model.recalculate_walls([w for w in set(walls) if w]) def regenerate_from_type(self, usecase_path, ifc_file, settings): relating_type = settings["relating_type"] @@ -968,7 +957,7 @@ class DumbWallPlaner: if layer_set_direction: material.LayerSetDirection = layer_set_direction if material.LayerSetDirection == "AXIS2": - DumbWallRecalculator().recalculate([obj]) + tool.Model.recalculate_walls([obj]) class DumbWallJoiner: @@ -988,7 +977,7 @@ class DumbWallJoiner: axis1 = tool.Model.get_wall_axis(wall1) axis = copy.deepcopy(axis1["reference"]) body = copy.deepcopy(axis1["reference"]) - self.recreate_wall(element1, wall1, axis, body) + tool.Model.recreate_wall(element1, wall1) def split(self, wall1: bpy.types.Object, target: Vector) -> None: unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) @@ -1089,8 +1078,8 @@ class DumbWallJoiner: self.set_axis(element1, p1, p3) self.set_axis(element2, p3, p2) - self.recreate_wall(element1, wall1) - self.recreate_wall(element2, wall2) + tool.Model.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element2, wall2) def flip(self, wall1: bpy.types.Object) -> None: if tool.Ifc.is_moved(wall1): @@ -1117,7 +1106,7 @@ class DumbWallJoiner: ifcopenshell.api.geometry.edit_object_placement( tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False ) - self.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element1, wall1) def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None: if tool.Ifc.is_moved(wall1): @@ -1170,7 +1159,7 @@ class DumbWallJoiner: related_connection=rel.RelatedConnectionType, ) - self.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element1, wall1) tool.Geometry.delete_ifc_object(wall2) @@ -1203,7 +1192,7 @@ class DumbWallJoiner: description="TOP", ) - self.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element1, wall1) def set_axis(self, wall, p1, p2): axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW") @@ -1232,7 +1221,7 @@ class DumbWallJoiner: self.set_axis(element1, p1, intersect) else: self.set_axis(element1, intersect, p2) - self.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element1, wall1) def set_length(self, wall1: bpy.types.Object, si_length: float) -> None: element1 = tool.Ifc.get_entity(wall1) @@ -1246,7 +1235,7 @@ class DumbWallJoiner: p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) p2[0] = p1[0] + si_length / unit_scale self.set_axis(element1, p1, p2) - self.recreate_wall(element1, wall1) + tool.Model.recreate_wall(element1, wall1) def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None: element1 = tool.Ifc.get_entity(wall1) @@ -1269,7 +1258,7 @@ class DumbWallJoiner: description="BUTT", ) - self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) + tool.Model.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"]) def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None: wall1 = tool.Ifc.get_entity(obj1) @@ -1279,27 +1268,8 @@ class DumbWallJoiner: if tool.Ifc.is_moved(obj2): bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj2) ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2) - self.recreate_wall(wall1, obj1) - self.recreate_wall(wall2, obj2) - - def recreate_wall(self, element: ifcopenshell.entity_instance, obj: bpy.types.Object, axis=None, body=None) -> None: - rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element) - bonsai.core.geometry.switch_representation( - tool.Ifc, - tool.Geometry, - obj=obj, - representation=rep, - should_reload=True, - is_global=True, - should_sync_changes_first=False, - ) - tool.Geometry.record_object_materials(obj) - - unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) - matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) - matrix[:, 3] *= unit_scale - obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) - tool.Geometry.record_object_position(obj) + tool.Model.recreate_wall(wall1, obj1) + tool.Model.recreate_wall(wall2, obj2) def create_matrix(self, p, x, y, z): return Matrix([x, y, z, p]).to_4x4().transposed() diff --git a/src/bonsai/bonsai/bim/module/model/workspace.py b/src/bonsai/bonsai/bim/module/model/workspace.py index 6d65646599..90a9849e30 100644 --- a/src/bonsai/bonsai/bim/module/model/workspace.py +++ b/src/bonsai/bonsai/bim/module/model/workspace.py @@ -773,6 +773,12 @@ class EditObjectUI: op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="") op.x_angle = cls.props.x_angle + row = cls.layout.row(align=True) if ui_context != "TOOL_HEADER" else row + row.prop( + data=cls.props, property="offset_type_vertical", text="Offset" if ui_context != "TOOL_HEADER" else "" + ) + row.operator("bim.offset_walls", icon="FILE_REFRESH", text="") + elif AuthoringData.data["active_material_usage"] == "LAYER3": row.prop(data=cls.props, property="x_angle", text="Angle" if ui_context != "TOOL_HEADER" else "A") op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="") @@ -1182,12 +1188,12 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): return if self.active_material_usage == "LAYER2": try: - core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "CENTERLINE") + core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "CENTER") except core.RequireAtLeastTwoLayeredElements as e: self.report({"ERROR"}, str(e)) else: try: - core.align_objects(tool.Blender, tool.Model, "CENTERLINE") + core.align_objects(tool.Blender, tool.Model, "CENTER") except core.RequireAtLeastTwoElements as e: self.report({"ERROR"}, str(e)) @@ -1357,7 +1363,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator): except core.RequireAtLeastTwoElements as e: self.report({"ERROR"}, str(e)) - def hotkey_S_Y(self): if not bpy.context.selected_objects: return diff --git a/src/bonsai/bonsai/core/model.py b/src/bonsai/bonsai/core/model.py index 7e7eac6045..5c4cade5c5 100644 --- a/src/bonsai/bonsai/core/model.py +++ b/src/bonsai/bonsai/core/model.py @@ -27,6 +27,9 @@ if TYPE_CHECKING: from mathutils import Vector from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner + AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"] + OffsetType = Literal["CENTER", "EXTERIOR", "INTERIOR"] + def unjoin_walls( ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model @@ -82,12 +85,19 @@ def join_walls_LV( joiner.connect(another_selected_object, active_obj) +def offset_walls(ifc: tool.Ifc, blender: tool.Blender, model: tool.Model, offset_type: OffsetType): + objs = [ + obj + for obj in blender.get_selected_objects() + if (element := ifc.get_entity(obj)) and model.get_usage_type(element) == "LAYER2" + ] + for obj in objs: + model.offset_wall(obj, offset_type) + model.recalculate_walls(objs) + + def align_walls( - ifc: tool.Ifc, - blender: tool.Blender, - model: tool.Model, - aligner: DumbWallAligner, - align_type: Literal["CENTERLINE", "EXTERIOR", "INTERIOR"], + ifc: tool.Ifc, blender: tool.Blender, model: tool.Model, aligner: DumbWallAligner, align_type: AlignType ): reference_obj = blender.get_active_object(is_selected=True) if not (e := ifc.get_entity(reference_obj) or not model.get_usage_type(e) == "LAYER2"): @@ -103,7 +113,7 @@ def align_walls( ) aligner.set_reference_wall(reference_obj) for obj in objs: - if align_type == "CENTERLINE": + if align_type == "CENTER": aligner.align_centerline(obj) elif align_type == "EXTERIOR": aligner.align_first_layer(obj) @@ -111,7 +121,7 @@ def align_walls( aligner.align_last_layer(obj) -def align_objects(blender: tool.Blender, model: tool.Model, align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]): +def align_objects(blender: tool.Blender, model: tool.Model, align_type: Literal["CENTER", "POSITIVE", "NEGATIVE"]): reference_obj = blender.get_active_object(is_selected=True) objs = [o for o in blender.get_selected_objects() if o != reference_obj] if not reference_obj or not objs: diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 9dd2222959..e179de44e7 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -2328,11 +2328,14 @@ class Model(bonsai.core.tool.Model): return [s for s in group_node.inputs if s.type != "GEOMETRY"] @classmethod - def align_objects(cls, reference_obj: bpy.types.Object, objs: Iterable[bpy.types.Object], align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]): - if align_type == "CENTERLINE": - point = reference_obj.matrix_world @ ( - Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2) - ) + def align_objects( + cls, + reference_obj: bpy.types.Object, + objs: Iterable[bpy.types.Object], + align_type: Literal["CENTER", "POSITIVE", "NEGATIVE"], + ): + if align_type == "CENTER": + point = reference_obj.matrix_world @ (Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2)) elif align_type == "POSITIVE": point = reference_obj.matrix_world @ Vector(reference_obj.bound_box[6]) elif align_type == "NEGATIVE": @@ -2351,10 +2354,16 @@ class Model(bonsai.core.tool.Model): obj.matrix_world = Matrix.Translation(reference_y_axis * -y_distances[i]) @ obj.matrix_world @classmethod - def get_axis_distances(cls, point: Vector, axis: Vector, objs: Iterable[bpy.types.Object], align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]) -> list[float]: + def get_axis_distances( + cls, + point: Vector, + axis: Vector, + objs: Iterable[bpy.types.Object], + align_type: Literal["CENTER", "POSITIVE", "NEGATIVE"], + ) -> list[float]: results = [] for obj in objs: - if align_type == "CENTERLINE": + if align_type == "CENTER": obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2)) elif align_type == "POSITIVE": obj_point = obj.matrix_world @ Vector(obj.bound_box[6]) @@ -2364,7 +2373,66 @@ class Model(bonsai.core.tool.Model): return results @classmethod - def offset_wall(cls, wall: bpy.types.Object, baseline: Literal["EXTERIOR", "INTERIOR", "CENTERLINE"]) -> None: + def offset_wall(cls, wall: bpy.types.Object, baseline: Literal["EXTERIOR", "INTERIOR", "CENTER"]) -> None: element = tool.Ifc.get_entity(wall) - if baseline == "CENTERLINE": - pass + usage = ifcopenshell.util.element.get_material(element) + if not usage.is_a("IfcMaterialLayerSetUsage"): + return + layer_set = usage.ForLayerSet + if baseline == "CENTER": + if usage.DirectionSense == "POSITIVE": + usage.OffsetFromReferenceLine = -layer_set.TotalThickness / 2 + else: + usage.OffsetFromReferenceLine = layer_set.TotalThickness / 2 + elif baseline == "INTERIOR": + if usage.DirectionSense == "POSITIVE": + usage.OffsetFromReferenceLine = -layer_set.TotalThickness + else: + usage.OffsetFromReferenceLine = 0.0 + elif baseline == "EXTERIOR": + if usage.DirectionSense == "POSITIVE": + usage.OffsetFromReferenceLine = 0.0 + else: + usage.OffsetFromReferenceLine = layer_set.TotalThickness + + @classmethod + def recreate_wall(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None: + rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element) + bonsai.core.geometry.switch_representation( + tool.Ifc, + tool.Geometry, + obj=obj, + representation=rep, + should_reload=True, + is_global=True, + should_sync_changes_first=False, + ) + tool.Geometry.record_object_materials(obj) + + unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) + matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement) + matrix[:, 3] *= unit_scale + obj.matrix_world = tool.Loader.apply_blender_offset_to_matrix_world(obj, matrix) + tool.Geometry.record_object_position(obj) + + @classmethod + def recalculate_walls(cls, walls: list[bpy.types.Object]) -> None: + queue: set[tuple[ifcopenshell.entity_instance, bpy.types.Object]] = set() + for wall in walls: + element = tool.Ifc.get_entity(wall) + if tool.Ifc.is_moved(wall): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall) + queue.add((element, wall)) + for rel in getattr(element, "ConnectedTo", []): + obj = tool.Ifc.get_object(rel.RelatedElement) + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + queue.add((rel.RelatedElement, obj)) + for rel in getattr(element, "ConnectedFrom", []): + obj = tool.Ifc.get_object(rel.RelatingElement) + if tool.Ifc.is_moved(obj): + bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj) + queue.add((rel.RelatingElement, obj)) + for element, wall in queue: + if tool.Model.get_usage_type(element) == "LAYER2" and wall: + cls.recreate_wall(element, wall) diff --git a/src/bonsai/test/bim/feature/model.feature b/src/bonsai/test/bim/feature/model.feature index cf072bf5a4..bcf57acf73 100644 --- a/src/bonsai/test/bim/feature/model.feature +++ b/src/bonsai/test/bim/feature/model.feature @@ -286,7 +286,28 @@ Scenario: Split a wall which has a flipped door And I press "bim.hotkey(hotkey='S_K')" Then the object "IfcDoor/Door" is at "8.01,0.1,0" -Scenario: Align two walls - centerline +Scenario: Offset walls + Given an empty IFC project + And I load the demo construction library + And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" + And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" + And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" + And I press "bim.add_occurrence" + When the object "IfcWall/Wall" is selected + And I set "scene.BIMModelProperties.offset_type_vertical" to "EXTERIOR" + And I press "bim.offset_walls" + Then the object "IfcWall/Wall" bottom left corner is at "0,0,0" + And the object "IfcWall/Wall" top right corner is at "1,0.1,3" + When I set "scene.BIMModelProperties.offset_type_vertical" to "INTERIOR" + And I press "bim.offset_walls" + Then the object "IfcWall/Wall" bottom left corner is at "0,-0.1,0" + And the object "IfcWall/Wall" top right corner is at "1,0,3" + When I set "scene.BIMModelProperties.offset_type_vertical" to "CENTER" + And I press "bim.offset_walls" + Then the object "IfcWall/Wall" bottom left corner is at "0,-0.05,0" + And the object "IfcWall/Wall" top right corner is at "1,0.05,3" + +Scenario: Align walls Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" @@ -304,46 +325,18 @@ Scenario: Align two walls - centerline And the object "IfcWall/Wall" top right corner is at "1,0.1,3" And the object "IfcWall/Wall.001" bottom left corner is at "10,-0.1,0" And the object "IfcWall/Wall.001" top right corner is at "11,0.2,3" - -Scenario: Align two walls - interior - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.add_occurrence" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL300'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And the cursor is at "10,5,0" - And I press "bim.add_occurrence" - When the object "IfcWall/Wall.001" is selected - And additionally the object "IfcWall/Wall" is selected - And I press "bim.hotkey(hotkey='S_V')" + When I press "bim.hotkey(hotkey='S_V')" Then the object "IfcWall/Wall" bottom left corner is at "0,0,0" And the object "IfcWall/Wall" top right corner is at "1,0.1,3" And the object "IfcWall/Wall.001" bottom left corner is at "10,-0.2,0" And the object "IfcWall/Wall.001" top right corner is at "11,0.1,3" - -Scenario: Align two walls - exterior - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.add_occurrence" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL300'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And the cursor is at "10,5,0" - And I press "bim.add_occurrence" - When the object "IfcWall/Wall.001" is selected - And additionally the object "IfcWall/Wall" is selected - And I press "bim.hotkey(hotkey='S_X')" + When I press "bim.hotkey(hotkey='S_X')" Then the object "IfcWall/Wall" bottom left corner is at "0,0,0" And the object "IfcWall/Wall" top right corner is at "1,0.1,3" And the object "IfcWall/Wall.001" bottom left corner is at "10,0,0" And the object "IfcWall/Wall.001" top right corner is at "11,0.3,3" -Scenario: Align two walls - centerline fail due to selection criteria +Scenario: Align walls - centerline fail due to selection criteria Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" @@ -353,7 +346,7 @@ Scenario: Align two walls - centerline fail due to selection criteria When the object "IfcWall/Wall" is selected Then I press "bim.hotkey(hotkey='S_C')" and expect error "Error: At least two vertically layered elements must be selected to match alignments." -Scenario: Align two elements - centerline +Scenario: Align elements Given an empty IFC project And I load the demo construction library And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" @@ -370,38 +363,12 @@ Scenario: Align two elements - centerline And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145" And the object "IfcDoor/Door.001" bottom left corner is at "10,-0.455,0" And the object "IfcDoor/Door.001" top right corner is at "9.9,0.555,2.145" - -Scenario: Align two elements - positive - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.add_occurrence" - And the cursor is at "10,5,0" - And I press "bim.add_occurrence" - And the object "IfcDoor/Door.001" is rotated by "0,0,90" deg - When the object "IfcDoor/Door.001" is selected - And additionally the object "IfcDoor/Door" is selected - And I press "bim.hotkey(hotkey='S_V')" + When I press "bim.hotkey(hotkey='S_V')" Then the object "IfcDoor/Door" bottom left corner is at "0,0,0" And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145" And the object "IfcDoor/Door.001" bottom left corner is at "10,-0.910,0" And the object "IfcDoor/Door.001" top right corner is at "9.9,0.1,2.145" - -Scenario: Align two elements - negative - Given an empty IFC project - And I load the demo construction library - And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" - And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()" - And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}" - And I press "bim.add_occurrence" - And the cursor is at "10,5,0" - And I press "bim.add_occurrence" - And the object "IfcDoor/Door.001" is rotated by "0,0,90" deg - When the object "IfcDoor/Door.001" is selected - And additionally the object "IfcDoor/Door" is selected - And I press "bim.hotkey(hotkey='S_X')" + When I press "bim.hotkey(hotkey='S_X')" Then the object "IfcDoor/Door" bottom left corner is at "0,0,0" And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145" And the object "IfcDoor/Door.001" bottom left corner is at "10,0,0" diff --git a/src/bonsai/test/tool/test_model.py b/src/bonsai/test/tool/test_model.py index f41a4d5988..b0b7411856 100644 --- a/src/bonsai/test/tool/test_model.py +++ b/src/bonsai/test/tool/test_model.py @@ -678,3 +678,44 @@ class TestApplyIfcMaterialChanges(NewFile): ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) tool.Material.ensure_material_unassigned([element]) assert self.get_mesh(obj).materials[:] == [bpy.data.materials["Red"]] + + +class TestOffsetWall(NewFile): + def test_run(self): + ifc = ifcopenshell.file() + tool.Ifc.set(ifc) + + wall_type = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWallType", name="WAL01") + material_set = ifcopenshell.api.material.add_material_set(ifc, set_type="IfcMaterialLayerSet") + material = ifcopenshell.api.material.add_material(ifc, name="PB01", category="gypsum") + layer = ifcopenshell.api.material.add_layer(ifc, layer_set=material_set, material=material) + ifcopenshell.api.material.edit_layer(ifc, layer=layer, attributes={"LayerThickness": 100}) + ifcopenshell.api.material.assign_material(ifc, products=[wall_type], material=material_set) + + wall = ifcopenshell.api.root.create_entity(ifc, ifc_class="IfcWall") + ifcopenshell.api.type.assign_type(ifc, related_objects=[wall], relating_type=wall_type) + rel = ifcopenshell.api.material.assign_material(ifc, products=[wall], type="IfcMaterialLayerSetUsage") + usage = rel.RelatingMaterial + obj = bpy.data.objects.new("Wall", None) + tool.Ifc.link(wall, obj) + + usage.DirectionSense = "POSITIVE" + subject.offset_wall(obj, "CENTER") + assert usage.OffsetFromReferenceLine == -50 + usage.DirectionSense = "NEGATIVE" + subject.offset_wall(obj, "CENTER") + assert usage.OffsetFromReferenceLine == 50 + + usage.DirectionSense = "POSITIVE" + subject.offset_wall(obj, "INTERIOR") + assert usage.OffsetFromReferenceLine == -100 + usage.DirectionSense = "NEGATIVE" + subject.offset_wall(obj, "INTERIOR") + assert usage.OffsetFromReferenceLine == 0 + + usage.DirectionSense = "POSITIVE" + subject.offset_wall(obj, "EXTERIOR") + assert usage.OffsetFromReferenceLine == 0 + usage.DirectionSense = "NEGATIVE" + subject.offset_wall(obj, "EXTERIOR") + assert usage.OffsetFromReferenceLine == 100