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.FlipWall,
wall.MergeWall,
wall.OffsetWalls,
wall.RecalculateWall,
wall.SplitWall,
wall.UnjoinWalls,
@@ -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"]},
}
@@ -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"}
+2 -2
View File
@@ -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(
+1 -2
View File
@@ -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"}
+40 -70
View File
@@ -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()
@@ -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
+17 -7
View File
@@ -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:
+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"]
@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)
+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')"
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"
+41
View File
@@ -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