mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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"}
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user