Fix #6501. Fix #6104. Fix #6189. You can now change wall offset convention after drawing walls.

People kept on getting confused with the align tool thinking it changed
the baseline to reference line offset whereas it actually just aligned
the object bodies. This tool now does exactly that.

Also start refactoring the "DumbWall" classes into the tools so we can
test them properly.
This commit is contained in:
Dion Moult
2025-04-21 17:17:09 +10:00
parent 2db6b860b2
commit 56bfe80b3b
11 changed files with 218 additions and 157 deletions
@@ -77,6 +77,7 @@ classes = (
wall.ExtendWallsToWall, wall.ExtendWallsToWall,
wall.FlipWall, wall.FlipWall,
wall.MergeWall, wall.MergeWall,
wall.OffsetWalls,
wall.RecalculateWall, wall.RecalculateWall,
wall.SplitWall, wall.SplitWall,
wall.UnjoinWalls, wall.UnjoinWalls,
@@ -599,6 +599,7 @@ class PolylineOperator:
"Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]}, "Increment Angle": {"icons": True, "keys": ["EVENT_SHIFT", "MOUSE_MMB_SCROLL"]},
"Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]}, "Modify Snap Point": {"icons": True, "keys": ["EVENT_M"]},
"Close Polyline": {"icons": True, "keys": ["EVENT_C"]}, "Close Polyline": {"icons": True, "keys": ["EVENT_C"]},
"Offset": {"icons": True, "keys": ["EVENT_O"]},
"Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]}, "Remove Point": {"icons": True, "keys": ["EVENT_BACKSPACE"]},
} }
@@ -36,7 +36,6 @@ import bonsai.core.root
from bonsai.bim.ifc import IfcStore from bonsai.bim.ifc import IfcStore
from math import pi, degrees, atan2 from math import pi, degrees, atan2
from mathutils import Vector, Matrix 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.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.polyline import PolylineOperator
from typing import Union, Any, Optional from typing import Union, Any, Optional
@@ -969,7 +968,7 @@ class Rotate90(bpy.types.Operator, tool.Ifc.Operator):
obj.matrix_world @= rotate_matrix obj.matrix_world @= rotate_matrix
bpy.context.view_layer.update() bpy.context.view_layer.update()
DumbProfileRecalculator().recalculate(profile_objs) DumbProfileRecalculator().recalculate(profile_objs)
DumbWallRecalculator().recalculate(layer2_objs) tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"} return {"FINISHED"}
+2 -2
View File
@@ -221,13 +221,13 @@ class BIMModelProperties(PropertyGroup):
items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")], items=[("EXTERIOR", "Exterior", ""), ("CENTER", "Center", ""), ("INTERIOR", "Interior", "")],
name="Vertical Layer Offset Type", name="Vertical Layer Offset Type",
default="EXTERIOR", 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( offset_type_horizontal: bpy.props.EnumProperty(
items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")], items=[("TOP", "Top", ""), ("CENTER", "Center", ""), ("BOTTOM", "Bottom", "")],
name="Horizontal Layer Offset Type", name="Horizontal Layer Offset Type",
default="TOP", 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") offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line")
show_wall_axis: bpy.props.BoolProperty( show_wall_axis: bpy.props.BoolProperty(
+1 -2
View File
@@ -36,7 +36,6 @@ from math import cos, pi
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator from bonsai.bim.module.model.polyline import PolylineOperator
from bonsai.bim.module.model.wall import DumbWallRecalculator
from typing import Optional 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"): if rel.is_a() == "IfcRelConnectsElements" and rel.RelatedElement.is_a("IfcWall"):
walls.append(tool.Ifc.get_object(rel.RelatedElement)) walls.append(tool.Ifc.get_object(rel.RelatedElement))
DumbWallRecalculator().recalculate(walls) tool.Model.recalculate_walls(walls)
return {"FINISHED"} return {"FINISHED"}
+40 -70
View File
@@ -117,8 +117,8 @@ class ExtendWallsToWall(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.geometry.connect_wall( ifcopenshell.api.geometry.connect_wall(
tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True tool.Ifc.get(), wall1=element, wall2=target_element, is_atpath=True
) )
joiner.recreate_wall(element, obj) tool.Model.recreate_wall(element, obj)
joiner.recreate_wall(target_element, target_obj) tool.Model.recreate_wall(target_element, target_obj)
else: else:
self.report({"ERROR"}, "Please select at least one LAYER2 element and one active LAYER2 element") 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_options = {"REGISTER", "UNDO"}
bl_description = """ Align the selected walls to the active wall: bl_description = """ Align the selected walls to the active wall:
'Ext.': align to the EXTERIOR face 'Ext.': align to the EXTERIOR face
'C/L': align to wall CENTERLINE 'C/L': align to wall CENTER
'Int.': align to the INTERIOR face""" 'Int.': align to the INTERIOR face"""
AlignType = Literal["CENTERLINE", "EXTERIOR", "INTERIOR"] AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration] align_type: bpy.props.EnumProperty( # type: ignore [reportRedeclaration]
items=((i, i, "") for i in get_args(AlignType)) 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): def _execute(self, context):
objects = tool.Model.get_selected_mesh_ifc_objects() objects = tool.Model.get_selected_mesh_ifc_objects()
DumbWallRecalculator().recalculate(objects) tool.Model.recalculate_walls(objects)
return {"FINISHED"} return {"FINISHED"}
@@ -280,7 +280,7 @@ class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
layer2_objs.append(obj) layer2_objs.append(obj)
if layer2_objs: if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs) tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"} return {"FINISHED"}
@@ -393,7 +393,7 @@ class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
obj.rotation_euler.z = current_z_rot obj.rotation_euler.z = current_z_rot
if layer2_objs: if layer2_objs:
DumbWallRecalculator().recalculate(layer2_objs) tool.Model.recalculate_walls(layer2_objs)
return {"FINISHED"} return {"FINISHED"}
@@ -416,7 +416,24 @@ class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
selected_objs = tool.Model.get_selected_mesh_ifc_objects() selected_objs = tool.Model.get_selected_mesh_ifc_objects()
for obj in selected_objs: for obj in selected_objs:
joiner.set_length(obj, self.length) 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): 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()) material_set_usage = model.by_id(material.id())
# if material.is_a("IfcMaterialLayerSetUsage"): # if material.is_a("IfcMaterialLayerSetUsage"):
attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense} attributes = {"OffsetFromReferenceLine": offset, "DirectionSense": direction_sense}
ifcopenshell.api.run( ifcopenshell.api.run("material.edit_layer_usage", model, usage=material_set_usage, attributes=attributes)
"material.edit_layer_usage", tool.Model.recalculate_walls([wall["obj"]])
model,
**{"usage": material_set_usage, "attributes": attributes},
)
DumbWallRecalculator().recalculate([wall["obj"]])
if walls: if walls:
if is_polyline_closed: if is_polyline_closed:
@@ -682,30 +695,6 @@ class DumbWallAligner:
return round(degrees(angle) % 360) == 180 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: class DumbWallGenerator:
def __init__(self, relating_type): def __init__(self, relating_type):
self.relating_type = relating_type self.relating_type = relating_type
@@ -937,7 +926,7 @@ class DumbWallPlaner:
else: else:
for rel in inverse.AssociatedTo: for rel in inverse.AssociatedTo:
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects]) 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): def regenerate_from_type(self, usecase_path, ifc_file, settings):
relating_type = settings["relating_type"] relating_type = settings["relating_type"]
@@ -968,7 +957,7 @@ class DumbWallPlaner:
if layer_set_direction: if layer_set_direction:
material.LayerSetDirection = layer_set_direction material.LayerSetDirection = layer_set_direction
if material.LayerSetDirection == "AXIS2": if material.LayerSetDirection == "AXIS2":
DumbWallRecalculator().recalculate([obj]) tool.Model.recalculate_walls([obj])
class DumbWallJoiner: class DumbWallJoiner:
@@ -988,7 +977,7 @@ class DumbWallJoiner:
axis1 = tool.Model.get_wall_axis(wall1) axis1 = tool.Model.get_wall_axis(wall1)
axis = copy.deepcopy(axis1["reference"]) axis = copy.deepcopy(axis1["reference"])
body = 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: def split(self, wall1: bpy.types.Object, target: Vector) -> None:
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get()) 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(element1, p1, p3)
self.set_axis(element2, p3, p2) self.set_axis(element2, p3, p2)
self.recreate_wall(element1, wall1) tool.Model.recreate_wall(element1, wall1)
self.recreate_wall(element2, wall2) tool.Model.recreate_wall(element2, wall2)
def flip(self, wall1: bpy.types.Object) -> None: def flip(self, wall1: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1): if tool.Ifc.is_moved(wall1):
@@ -1117,7 +1106,7 @@ class DumbWallJoiner:
ifcopenshell.api.geometry.edit_object_placement( ifcopenshell.api.geometry.edit_object_placement(
tool.Ifc.get(), product=element1, matrix=matrix, is_si=False, should_transform_children=False 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: def merge(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
if tool.Ifc.is_moved(wall1): if tool.Ifc.is_moved(wall1):
@@ -1170,7 +1159,7 @@ class DumbWallJoiner:
related_connection=rel.RelatedConnectionType, related_connection=rel.RelatedConnectionType,
) )
self.recreate_wall(element1, wall1) tool.Model.recreate_wall(element1, wall1)
tool.Geometry.delete_ifc_object(wall2) tool.Geometry.delete_ifc_object(wall2)
@@ -1203,7 +1192,7 @@ class DumbWallJoiner:
description="TOP", description="TOP",
) )
self.recreate_wall(element1, wall1) tool.Model.recreate_wall(element1, wall1)
def set_axis(self, wall, p1, p2): def set_axis(self, wall, p1, p2):
axis = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW") 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) self.set_axis(element1, p1, intersect)
else: else:
self.set_axis(element1, intersect, p2) 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: def set_length(self, wall1: bpy.types.Object, si_length: float) -> None:
element1 = tool.Ifc.get_entity(wall1) element1 = tool.Ifc.get_entity(wall1)
@@ -1246,7 +1235,7 @@ class DumbWallJoiner:
p1, p2 = ifcopenshell.util.representation.get_reference_line(element1) p1, p2 = ifcopenshell.util.representation.get_reference_line(element1)
p2[0] = p1[0] + si_length / unit_scale p2[0] = p1[0] + si_length / unit_scale
self.set_axis(element1, p1, p2) 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: def join_T(self, wall1: bpy.types.Object, wall2: bpy.types.Object) -> None:
element1 = tool.Ifc.get_entity(wall1) element1 = tool.Ifc.get_entity(wall1)
@@ -1269,7 +1258,7 @@ class DumbWallJoiner:
description="BUTT", 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: def connect(self, obj1: bpy.types.Object, obj2: bpy.types.Object) -> None:
wall1 = tool.Ifc.get_entity(obj1) wall1 = tool.Ifc.get_entity(obj1)
@@ -1279,27 +1268,8 @@ class DumbWallJoiner:
if tool.Ifc.is_moved(obj2): if tool.Ifc.is_moved(obj2):
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=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) ifcopenshell.api.geometry.connect_wall(tool.Ifc.get(), wall1=wall1, wall2=wall2)
self.recreate_wall(wall1, obj1) tool.Model.recreate_wall(wall1, obj1)
self.recreate_wall(wall2, obj2) tool.Model.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)
def create_matrix(self, p, x, y, z): def create_matrix(self, p, x, y, z):
return Matrix([x, y, z, p]).to_4x4().transposed() return Matrix([x, y, z, p]).to_4x4().transposed()
@@ -773,6 +773,12 @@ class EditObjectUI:
op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="") op = row.operator("bim.change_extrusion_x_angle", icon="FILE_REFRESH", text="")
op.x_angle = cls.props.x_angle 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": elif AuthoringData.data["active_material_usage"] == "LAYER3":
row.prop(data=cls.props, property="x_angle", text="Angle" if ui_context != "TOOL_HEADER" else "A") 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="") 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 return
if self.active_material_usage == "LAYER2": if self.active_material_usage == "LAYER2":
try: 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: except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e)) self.report({"ERROR"}, str(e))
else: else:
try: try:
core.align_objects(tool.Blender, tool.Model, "CENTERLINE") core.align_objects(tool.Blender, tool.Model, "CENTER")
except core.RequireAtLeastTwoElements as e: except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e)) self.report({"ERROR"}, str(e))
@@ -1357,7 +1363,6 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
except core.RequireAtLeastTwoElements as e: except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e)) self.report({"ERROR"}, str(e))
def hotkey_S_Y(self): def hotkey_S_Y(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
return return
+17 -7
View File
@@ -27,6 +27,9 @@ if TYPE_CHECKING:
from mathutils import Vector from mathutils import Vector
from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner
AlignType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
OffsetType = Literal["CENTER", "EXTERIOR", "INTERIOR"]
def unjoin_walls( def unjoin_walls(
ifc: tool.Ifc, blender: tool.Blender, geometry: tool.Geometry, joiner: DumbWallJoiner, model: tool.Model 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) 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( def align_walls(
ifc: tool.Ifc, ifc: tool.Ifc, blender: tool.Blender, model: tool.Model, aligner: DumbWallAligner, align_type: AlignType
blender: tool.Blender,
model: tool.Model,
aligner: DumbWallAligner,
align_type: Literal["CENTERLINE", "EXTERIOR", "INTERIOR"],
): ):
reference_obj = blender.get_active_object(is_selected=True) 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"): 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) aligner.set_reference_wall(reference_obj)
for obj in objs: for obj in objs:
if align_type == "CENTERLINE": if align_type == "CENTER":
aligner.align_centerline(obj) aligner.align_centerline(obj)
elif align_type == "EXTERIOR": elif align_type == "EXTERIOR":
aligner.align_first_layer(obj) aligner.align_first_layer(obj)
@@ -111,7 +121,7 @@ def align_walls(
aligner.align_last_layer(obj) 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) reference_obj = blender.get_active_object(is_selected=True)
objs = [o for o in blender.get_selected_objects() if o != reference_obj] objs = [o for o in blender.get_selected_objects() if o != reference_obj]
if not reference_obj or not objs: if not reference_obj or not objs:
+78 -10
View File
@@ -2328,11 +2328,14 @@ class Model(bonsai.core.tool.Model):
return [s for s in group_node.inputs if s.type != "GEOMETRY"] return [s for s in group_node.inputs if s.type != "GEOMETRY"]
@classmethod @classmethod
def align_objects(cls, reference_obj: bpy.types.Object, objs: Iterable[bpy.types.Object], align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]): def align_objects(
if align_type == "CENTERLINE": cls,
point = reference_obj.matrix_world @ ( reference_obj: bpy.types.Object,
Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2) 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": elif align_type == "POSITIVE":
point = reference_obj.matrix_world @ Vector(reference_obj.bound_box[6]) point = reference_obj.matrix_world @ Vector(reference_obj.bound_box[6])
elif align_type == "NEGATIVE": 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 obj.matrix_world = Matrix.Translation(reference_y_axis * -y_distances[i]) @ obj.matrix_world
@classmethod @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 = [] results = []
for obj in objs: 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)) obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif align_type == "POSITIVE": elif align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6]) obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
@@ -2364,7 +2373,66 @@ class Model(bonsai.core.tool.Model):
return results return results
@classmethod @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) element = tool.Ifc.get_entity(wall)
if baseline == "CENTERLINE": usage = ifcopenshell.util.element.get_material(element)
pass 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)
+28 -61
View File
@@ -286,7 +286,28 @@ Scenario: Split a wall which has a flipped door
And I press "bim.hotkey(hotkey='S_K')" And I press "bim.hotkey(hotkey='S_K')"
Then the object "IfcDoor/Door" is at "8.01,0.1,0" 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 Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" 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" 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" bottom left corner is at "10,-0.1,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.2,3" And the object "IfcWall/Wall.001" top right corner is at "11,0.2,3"
When I press "bim.hotkey(hotkey='S_V')"
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')"
Then the object "IfcWall/Wall" bottom left corner is at "0,0,0" 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" 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" bottom left corner is at "10,-0.2,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.1,3" And the object "IfcWall/Wall.001" top right corner is at "11,0.1,3"
When I press "bim.hotkey(hotkey='S_X')"
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')"
Then the object "IfcWall/Wall" bottom left corner is at "0,0,0" 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" 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" bottom left corner is at "10,0,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.3,3" 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 Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType" 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 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." 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 Given an empty IFC project
And I load the demo construction library And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType" 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" 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" 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" And the object "IfcDoor/Door.001" top right corner is at "9.9,0.555,2.145"
When I press "bim.hotkey(hotkey='S_V')"
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')"
Then the object "IfcDoor/Door" bottom left corner is at "0,0,0" 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" 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" 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" And the object "IfcDoor/Door.001" top right corner is at "9.9,0.1,2.145"
When I press "bim.hotkey(hotkey='S_X')"
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')"
Then the object "IfcDoor/Door" bottom left corner is at "0,0,0" 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" 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" And the object "IfcDoor/Door.001" bottom left corner is at "10,0,0"
+41
View File
@@ -678,3 +678,44 @@ class TestApplyIfcMaterialChanges(NewFile):
ifcopenshell.api.material.unassign_material(ifc_file, products=[element]) ifcopenshell.api.material.unassign_material(ifc_file, products=[element])
tool.Material.ensure_material_unassigned([element]) tool.Material.ensure_material_unassigned([element])
assert self.get_mesh(obj).materials[:] == [bpy.data.materials["Red"]] 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