mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-12 14:33:28 +00:00
1886 lines
82 KiB
Python
1886 lines
82 KiB
Python
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
|
# Copyright (C) 2020, 2021 Dion Moult <dion@thinkmoult.com>
|
|
#
|
|
# This file is part of BlenderBIM Add-on.
|
|
#
|
|
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# BlenderBIM Add-on is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import bpy
|
|
import copy
|
|
import math
|
|
import bmesh
|
|
import ifcopenshell
|
|
import ifcopenshell.api
|
|
import ifcopenshell.util.unit
|
|
import ifcopenshell.util.element
|
|
import ifcopenshell.util.placement
|
|
import ifcopenshell.util.representation
|
|
import ifcopenshell.util.type
|
|
import mathutils.geometry
|
|
import blenderbim.bim.handler
|
|
import blenderbim.core.type
|
|
import blenderbim.core.root
|
|
import blenderbim.core.geometry
|
|
import blenderbim.core.model as core
|
|
import blenderbim.tool as tool
|
|
from blenderbim.bim.ifc import IfcStore
|
|
from math import pi, sin, cos, degrees
|
|
from time import time
|
|
from mathutils import Vector, Matrix
|
|
from mathutils.bvhtree import BVHTree
|
|
from bpy_extras import view3d_utils
|
|
from blenderbim.bim.module.model.opening import FilledOpeningGenerator
|
|
from blenderbim.bim.module.model.decorator import WallPolylineDecorator
|
|
from typing import Optional
|
|
from lark import Lark, Transformer
|
|
|
|
|
|
class UnjoinWalls(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.unjoin_walls"
|
|
bl_label = "Unjoin Walls"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
core.unjoin_walls(tool.Ifc, tool.Blender, tool.Geometry, DumbWallJoiner(), tool.Model)
|
|
|
|
|
|
class AlignWall(bpy.types.Operator):
|
|
bl_idname = "bim.align_wall"
|
|
bl_label = "Align Wall"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = """ Align the selected walls to the last selected wall:
|
|
'Ext.': align to the EXTERIOR face
|
|
'C/L': align to wall CENTERLINE
|
|
'Int.': align to the INTERIOR face"""
|
|
align_type: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
selected_valid_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
|
return context.active_object and len(selected_valid_objects) > 1
|
|
|
|
def execute(self, context):
|
|
selected_objects = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
|
for obj in selected_objects:
|
|
if obj == context.active_object:
|
|
continue
|
|
aligner = DumbWallAligner(obj, context.active_object)
|
|
if self.align_type == "CENTERLINE":
|
|
aligner.align_centerline()
|
|
elif self.align_type == "EXTERIOR":
|
|
aligner.align_first_layer()
|
|
elif self.align_type == "INTERIOR":
|
|
aligner.align_last_layer()
|
|
tool.Ifc.edit(obj)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class FlipWall(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.flip_wall"
|
|
bl_label = "Flip Wall"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = "Switch the origin from the min XY corner to the max XY corner, and rotates the origin by 180"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
|
joiner = DumbWallJoiner()
|
|
for obj in selected_objs:
|
|
joiner.flip(obj)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class SplitWall(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.split_wall"
|
|
bl_label = "Split Wall"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
bl_description = (
|
|
"Split selected wall into two walls in correspondence of Blender cursor. The cursor must be in the wall volume"
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
|
for obj in selected_objs:
|
|
DumbWallJoiner().split(obj, context.scene.cursor.location)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class MergeWall(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.merge_wall"
|
|
bl_label = "Merge Wall"
|
|
bl_description = "Merge selected walls into one object"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
selected_objs = [o for o in context.selected_objects if o.data and hasattr(o.data, "transform")]
|
|
if len(selected_objs) == 2:
|
|
DumbWallJoiner().merge([o for o in selected_objs if o != context.active_object][0], context.active_object)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class RecalculateWall(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.recalculate_wall"
|
|
bl_label = "Recalculate Wall"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
DumbWallRecalculator().recalculate(context.selected_objects)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ChangeExtrusionDepth(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.change_extrusion_depth"
|
|
bl_label = "Change Extrusion Depth"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
depth: bpy.props.FloatProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
layer2_objs = []
|
|
si_conversion = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
for obj in context.selected_objects:
|
|
element = tool.Ifc.get_entity(obj)
|
|
if not element:
|
|
return
|
|
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if not representation:
|
|
return
|
|
extrusion = tool.Model.get_extrusion(representation)
|
|
if not extrusion:
|
|
return
|
|
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
|
x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
|
extrusion.Depth = self.depth / si_conversion * (1 / cos(x_angle))
|
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
|
for rel in element.ConnectedFrom:
|
|
if rel.is_a() == "IfcRelConnectsElements":
|
|
ifcopenshell.api.run(
|
|
"geometry.disconnect_element",
|
|
tool.Ifc.get(),
|
|
relating_element=rel.RelatingElement,
|
|
related_element=element,
|
|
)
|
|
layer2_objs.append(obj)
|
|
if layer2_objs:
|
|
DumbWallRecalculator().recalculate(layer2_objs)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ChangeExtrusionXAngle(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.change_extrusion_x_angle"
|
|
bl_label = "Change Extrusion X Angle"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
x_angle: bpy.props.FloatProperty(name="X Angle", default=0, subtype="ANGLE")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
layer2_objs = []
|
|
other_objs = []
|
|
x_angle = self.x_angle
|
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
for obj in context.selected_objects:
|
|
element = tool.Ifc.get_entity(obj)
|
|
if not element:
|
|
return
|
|
representation = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if not representation:
|
|
return
|
|
extrusion = tool.Model.get_extrusion(representation)
|
|
if not extrusion:
|
|
return
|
|
x, y, z = extrusion.ExtrudedDirection.DirectionRatios
|
|
existing_x_angle = Vector((0, 1)).angle_signed(Vector((y, z)))
|
|
perpendicular_depth = extrusion.Depth / (1 / cos(existing_x_angle))
|
|
extrusion.Depth = perpendicular_depth * (1 / cos(x_angle))
|
|
extrusion.ExtrudedDirection.DirectionRatios = (0.0, sin(x_angle), cos(x_angle))
|
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
|
layer2_objs.append(obj)
|
|
else:
|
|
blenderbim.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=representation,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
)
|
|
|
|
euler = obj.matrix_world.to_euler()
|
|
euler.x = x_angle
|
|
new_matrix = euler.to_matrix().to_4x4()
|
|
new_matrix.translation = obj.matrix_world.translation
|
|
obj.matrix_world = new_matrix
|
|
if layer2_objs:
|
|
DumbWallRecalculator().recalculate(layer2_objs)
|
|
return {"FINISHED"}
|
|
|
|
|
|
class ChangeLayerLength(bpy.types.Operator, tool.Ifc.Operator):
|
|
bl_idname = "bim.change_layer_length"
|
|
bl_label = "Change Layer Length"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
length: bpy.props.FloatProperty()
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.selected_objects
|
|
|
|
def _execute(self, context):
|
|
joiner = DumbWallJoiner()
|
|
for obj in context.selected_objects:
|
|
joiner.set_length(obj, self.length)
|
|
return {"FINISHED"}
|
|
|
|
|
|
def recalculate_dumb_wall_origin(wall, new_origin=None):
|
|
if new_origin is None:
|
|
new_origin = wall.matrix_world @ Vector(wall.bound_box[0])
|
|
if (wall.matrix_world.translation - new_origin).length < 0.001:
|
|
return
|
|
wall.data.transform(
|
|
Matrix.Translation(
|
|
(wall.matrix_world.inverted().to_quaternion() @ (wall.matrix_world.translation - new_origin))
|
|
)
|
|
)
|
|
wall.matrix_world.translation = new_origin
|
|
for child in wall.children:
|
|
child.matrix_parent_inverse = wall.matrix_world.inverted()
|
|
|
|
|
|
class DrawPolylineWall(bpy.types.Operator):
|
|
bl_idname = "bim.draw_polyline_wall"
|
|
bl_label = "Draw Polyline Wall"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
objs_bvhs = []
|
|
viewport_box = []
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.space_data.type == "VIEW_3D"
|
|
|
|
def __init__(self):
|
|
self.mousemove_count = 0
|
|
self.action_count = 0
|
|
self.visible_objs = []
|
|
self.objs_2d_bbox = []
|
|
self.number_options = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-", "*", "-", "/"}
|
|
self.number_input = []
|
|
self.number_output = ""
|
|
self.number_is_negative = False
|
|
self.is_input_on = False
|
|
self.input_options = ["X", "Y", "D", "A"]
|
|
self.input_type = "OFF"
|
|
self.input_value_xy = [None, None]
|
|
self.input_panel = {"X": "", "Y": "", "D": "", "A": ""}
|
|
self.snap_angle = None
|
|
|
|
def snaping_movement(self, context, event):
|
|
region = context.region
|
|
rv3d = context.region_data
|
|
self.mouse_pos = event.mouse_region_x, event.mouse_region_y
|
|
|
|
offset = 5
|
|
mouse_offset = (
|
|
(-offset, offset),
|
|
(0, offset),
|
|
(offset, offset),
|
|
(-offset, 0),
|
|
(0, 0),
|
|
(offset, 0),
|
|
(-offset, -offset),
|
|
(0, -offset),
|
|
(offset, -offset),
|
|
)
|
|
|
|
# Plane to intersect. Default Container
|
|
default_container_elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z
|
|
plane_origin = Vector((0, 0, default_container_elevation))
|
|
plane_normal = Vector((0, 0, 1))
|
|
|
|
def cast_rays_and_get_best_object():
|
|
best_length_squared = 1.0
|
|
best_obj = None
|
|
best_hit = None
|
|
best_face_index = None
|
|
|
|
objs_to_raycast = []
|
|
|
|
for obj, bbox_2d in self.objs_2d_bbox:
|
|
if obj.type == "MESH" and bbox_2d:
|
|
if tool.Raycast.in_view_2d_bounding_box(self.mouse_pos, bbox_2d):
|
|
objs_to_raycast.append(obj)
|
|
|
|
for obj in objs_to_raycast:
|
|
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
|
|
if hit is None:
|
|
# Tried original mouse position. Now it will try the offsets.
|
|
original_mouse_pos = self.mouse_pos
|
|
for value in mouse_offset:
|
|
self.mouse_pos = tuple(x + y for x, y in zip(original_mouse_pos, value))
|
|
hit, normal, face_index = tool.Raycast.obj_ray_cast(context, event, obj)
|
|
if hit:
|
|
break
|
|
self.mouse_pos = original_mouse_pos
|
|
if hit is not None:
|
|
hit_world = obj.original.matrix_world @ hit
|
|
length_squared = (hit_world - ray_origin).length_squared
|
|
if best_obj is None or length_squared < best_length_squared:
|
|
best_length_squared = length_squared
|
|
best_obj = obj
|
|
best_hit = hit_world
|
|
best_face_index = face_index
|
|
|
|
if best_obj is not None:
|
|
return best_obj, best_hit, best_face_index
|
|
|
|
else:
|
|
return None, None, None
|
|
|
|
snap_threshold = 0.3
|
|
ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event)
|
|
obj, hit, face_index = cast_rays_and_get_best_object()
|
|
intersection = tool.Raycast.ray_cast_to_plane(context, event, plane_origin, plane_normal)
|
|
|
|
# Locks snap into an angle axis
|
|
if event.shift:
|
|
rot_intersection, _, axis_start, axis_end = tool.Snap.snap_on_axis(intersection, self.snap_angle)
|
|
else:
|
|
self.snap_angle = None
|
|
rot_intersection, self.snap_angle, _, _ = tool.Snap.snap_on_axis(intersection)
|
|
|
|
if obj is not None:
|
|
|
|
snap_points = tool.Snap.get_snap_points_on_raycasted_obj(obj, face_index)
|
|
snap_point = tool.Snap.select_snap_point(snap_points, hit, snap_threshold)
|
|
|
|
if snap_point:
|
|
# Creates a mixed snap point between the locked axis and
|
|
# the object snap
|
|
# TODO Use ALT key to give the user the option to choose between the two results.
|
|
# TODO Create decorator for this
|
|
try:
|
|
snap_point_vector = Vector((snap_point[0].x, snap_point[0].y, snap_point[0].z))
|
|
snap_point_axis_1 = (
|
|
Vector((snap_point[0].x + 1000, snap_point[0].y, default_container_elevation)),
|
|
Vector((snap_point[0].x - 1000, snap_point[0].y, default_container_elevation)),
|
|
)
|
|
snap_point_axis_2 = (
|
|
Vector((snap_point[0].x, snap_point[0].y + 1000, default_container_elevation)),
|
|
Vector((snap_point[0].x, snap_point[0].y - 1000, default_container_elevation)),
|
|
)
|
|
snap_angle_axis = (axis_start, axis_end)
|
|
result_1 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_1)
|
|
result_1 = Vector((result_1[0].x, result_1[0].y, default_container_elevation))
|
|
distance_1 = (result_1 - snap_point_vector).length
|
|
|
|
result_2 = tool.Cad.intersect_edges(snap_angle_axis, snap_point_axis_2)
|
|
result_2 = Vector((result_2[0].x, result_2[0].y, default_container_elevation))
|
|
distance_2 = (result_2 - snap_point_vector).length
|
|
|
|
if distance_1 < distance_2:
|
|
best_result = result_1
|
|
else:
|
|
best_result = result_2
|
|
|
|
tool.Snap.update_snaping_point(best_result, "Axis")
|
|
|
|
except Exception as e:
|
|
tool.Snap.update_snaping_point(snap_point[0], snap_point[1])
|
|
|
|
else:
|
|
tool.Snap.update_snaping_point(hit, "Face")
|
|
|
|
else:
|
|
snap_points = tool.Snap.get_snap_points_on_polyline()
|
|
snap_point = tool.Snap.select_snap_point(snap_points, intersection, snap_threshold)
|
|
|
|
if snap_point:
|
|
tool.Snap.update_snaping_point(snap_point[0], snap_point[1])
|
|
elif rot_intersection:
|
|
tool.Snap.update_snaping_point(rot_intersection, "Axis")
|
|
else:
|
|
tool.Snap.update_snaping_point(intersection, "Plane")
|
|
|
|
def validate_input(self, input_number):
|
|
grammar = """
|
|
start: dim expr?
|
|
dim: NUMBER
|
|
expr: (ADD | SUB | MUL | DIV) NUMBER
|
|
|
|
NUMBER: /-?\\d+(?:\\.\\d+)?/
|
|
ADD: "+"
|
|
SUB: "-"
|
|
MUL: "*"
|
|
DIV: "/"
|
|
|
|
%ignore " "
|
|
"""
|
|
|
|
class InputTransform(Transformer):
|
|
def dim(self, args):
|
|
return float(args[0])
|
|
|
|
def expr(self, args):
|
|
op = args[0]
|
|
value = float(args[1])
|
|
if op == "+":
|
|
return lambda x: x + value
|
|
elif op == "-":
|
|
return lambda x: x - value
|
|
elif op == "*":
|
|
return lambda x: x * value
|
|
elif op == "/":
|
|
return lambda x: x / value
|
|
|
|
def start(self, args):
|
|
dimension = args[0]
|
|
if len(args) > 1:
|
|
expression = args[1]
|
|
return expression(dimension)
|
|
else:
|
|
return dimension
|
|
|
|
try:
|
|
parser = Lark(grammar, parser="lalr", transformer=InputTransform())
|
|
result = parser.parse(input_number)
|
|
return True, str(result)
|
|
except:
|
|
self.report({"WARNING"}, "The number typed is not valid.")
|
|
return False, "0"
|
|
|
|
def recalculate_inputs(self, context):
|
|
if self.number_input:
|
|
is_valid, self.number_output = self.validate_input(self.number_output)
|
|
self.input_panel[self.input_type] = self.number_output
|
|
if is_valid:
|
|
if self.input_type in {"X", "Y"}:
|
|
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
|
elif self.input_type in {"D", "A"}:
|
|
self.input_panel = WallPolylineDecorator.calculate_x_and_y(context)
|
|
self.input_panel[self.input_type] = self.number_output
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
# TODO This is creating a hack in generate function from DumbWallGenerator
|
|
# Come up with a better solution
|
|
def create_walls_from_polyline(self, context):
|
|
props = context.scene.BIMModelProperties
|
|
relating_type_id = props.relating_type_id
|
|
|
|
if not relating_type_id:
|
|
return {"FINISHED"}
|
|
|
|
self.container_obj = None
|
|
if container := tool.Root.get_default_container():
|
|
self.container_obj = tool.Ifc.get_object(container)
|
|
|
|
relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
|
|
|
walls, is_polyline_closed = DumbWallGenerator(relating_type).generate(True)
|
|
print(is_polyline_closed)
|
|
if walls:
|
|
if is_polyline_closed:
|
|
for wall1, wall2 in zip(walls, walls[1:] + [walls[0]]):
|
|
print(wall1["obj"], wall2["obj"])
|
|
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
|
|
else:
|
|
for wall1, wall2 in zip(walls[:-1], walls[1:]):
|
|
print(wall1["obj"], wall2["obj"])
|
|
DumbWallJoiner().join_V(wall1["obj"], wall2["obj"])
|
|
|
|
|
|
def modal(self, context, event):
|
|
|
|
if not self.is_input_on:
|
|
if event.type == "MOUSEMOVE":
|
|
self.mousemove_count += 1
|
|
self.is_input_on = False
|
|
self.input_type = "OFF"
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
else:
|
|
self.mousemove_count = 0
|
|
|
|
if self.mousemove_count == 2:
|
|
self.objs_2d_bbox = []
|
|
for obj in self.visible_objs:
|
|
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
|
|
|
|
if self.mousemove_count > 3:
|
|
self.snaping_movement(context, event)
|
|
WallPolylineDecorator.set_mouse_position(event)
|
|
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
|
tool.Blender.update_viewport()
|
|
return {"RUNNING_MODAL"}
|
|
|
|
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
|
tool.Snap.insert_polyline_point()
|
|
|
|
if not self.is_input_on:
|
|
if event.value == "RELEASE" and event.type == "BACK_SPACE":
|
|
tool.Snap.remove_last_polyline_point()
|
|
tool.Blender.update_viewport()
|
|
|
|
if self.is_input_on and event.value == "PRESS" and event.type == "TAB":
|
|
self.recalculate_inputs(context)
|
|
index = self.input_options.index(self.input_type)
|
|
size = len(self.input_options)
|
|
self.input_type = self.input_options[((index + 1) % size)]
|
|
self.number_input = []
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if not self.is_input_on and event.value == "RELEASE" and event.type == "TAB":
|
|
self.recalculate_inputs(context)
|
|
self.is_input_on = True
|
|
self.input_type = "X"
|
|
self.number_input = []
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if not self.is_input_on and event.ascii in self.number_options:
|
|
self.recalculate_inputs(context)
|
|
self.is_input_on = True
|
|
self.input_type = "D"
|
|
self.number_input = []
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if event.value == "RELEASE" and event.type in self.input_options:
|
|
self.recalculate_inputs(context)
|
|
self.is_input_on = True
|
|
self.input_type = event.type
|
|
self.number_input = []
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if self.input_type in self.input_options:
|
|
if (event.ascii in self.number_options) or (
|
|
event.value == "RELEASE" and event.type in {"BACK_SPACE", "MINUS", "NUMPAD_MINUS"}
|
|
):
|
|
if event.type == "BACK_SPACE":
|
|
if len(self.number_input) <= 1:
|
|
self.number_input = []
|
|
else:
|
|
self.number_input = self.number_input[:-1]
|
|
self.number_output = "".join(self.number_input)
|
|
self.input_panel[self.input_type] = self.number_output
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
else:
|
|
self.number_input.append(event.ascii)
|
|
self.number_output = "".join(self.number_input)
|
|
|
|
if self.number_input:
|
|
self.input_panel[self.input_type] = self.number_output
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if not self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER"}:
|
|
self.create_walls_from_polyline(context)
|
|
WallPolylineDecorator.uninstall()
|
|
tool.Snap.clear_polyline()
|
|
tool.Blender.update_viewport()
|
|
return {"FINISHED"}
|
|
|
|
if self.is_input_on and event.value == "RELEASE" and event.type in {"RET", "NUMPAD_ENTER"}:
|
|
if self.input_type in self.input_options:
|
|
self.recalculate_inputs(context)
|
|
self.is_input_on = True
|
|
self.input_type = "OFF"
|
|
self.number_input = []
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
else:
|
|
tool.Snap.insert_polyline_point(float(self.input_panel["X"]), float(self.input_panel["Y"]))
|
|
self.is_input_on = False
|
|
self.input_type = "OFF"
|
|
self.number_input = []
|
|
self.number_output = ""
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
|
|
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
|
return {"PASS_THROUGH"}
|
|
|
|
if self.is_input_on:
|
|
if event.value == "RELEASE" and event.type in {"RIGHTMOUSE", "ESC"}:
|
|
self.is_input_on = False
|
|
self.input_type = "OFF"
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
tool.Blender.update_viewport()
|
|
else:
|
|
if event.value == "RELEASE" and event.type in {"RIGHTMOUSE", "ESC"}:
|
|
WallPolylineDecorator.uninstall()
|
|
tool.Snap.clear_polyline()
|
|
tool.Blender.update_viewport()
|
|
return {"CANCELLED"}
|
|
|
|
return {"RUNNING_MODAL"}
|
|
|
|
def invoke(self, context, event):
|
|
if context.space_data.type == "VIEW_3D":
|
|
WallPolylineDecorator.install(context)
|
|
WallPolylineDecorator.set_input_panel(self.input_panel, self.input_type)
|
|
self.visible_objs = tool.Raycast.get_visible_objects(context)
|
|
for obj in self.visible_objs:
|
|
self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj))
|
|
self.snaping_movement(context, event)
|
|
WallPolylineDecorator.set_mouse_position(event)
|
|
self.input_panel = WallPolylineDecorator.calculate_distance_and_angle(context, self.is_input_on)
|
|
tool.Blender.update_viewport()
|
|
context.window_manager.modal_handler_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
else:
|
|
self.report({"WARNING"}, "Active space must be a View3d")
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DumbWallAligner:
|
|
# An alignment shifts the origin of all walls to the closest point on the
|
|
# local X axis of the reference wall. In addition, the Z rotation is copied.
|
|
# Z translations are ignored for alignment.
|
|
def __init__(self, wall, reference_wall):
|
|
self.wall = wall
|
|
self.reference_wall = reference_wall
|
|
|
|
def align_centerline(self):
|
|
self.align_rotation()
|
|
|
|
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
|
|
l_end = Vector(self.reference_wall.bound_box[4]).lerp(Vector(self.reference_wall.bound_box[7]), 0.5)
|
|
|
|
start = self.reference_wall.matrix_world @ l_start
|
|
end = self.reference_wall.matrix_world @ l_end
|
|
|
|
l_snap_point = Vector(self.wall.bound_box[0]).lerp(Vector(self.wall.bound_box[3]), 0.5)
|
|
snap_point = self.wall.matrix_world @ l_snap_point
|
|
offset = snap_point - self.wall.matrix_world.translation
|
|
|
|
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
|
|
|
|
new_origin = point - offset
|
|
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
|
|
|
def align_last_layer(self):
|
|
self.align_rotation()
|
|
|
|
if self.is_rotation_flipped():
|
|
element = tool.Ifc.get_entity(self.wall)
|
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
|
DumbWallJoiner().flip(self.wall)
|
|
bpy.context.view_layer.update()
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
|
|
else:
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
|
|
else:
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
|
|
|
|
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[3])
|
|
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[7])
|
|
|
|
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
|
|
|
|
offset = snap_point - self.wall.matrix_world.translation
|
|
new_origin = point - offset
|
|
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
|
|
|
def align_first_layer(self):
|
|
self.align_rotation()
|
|
|
|
if self.is_rotation_flipped():
|
|
element = tool.Ifc.get_entity(self.wall)
|
|
if tool.Model.get_usage_type(element) == "LAYER2":
|
|
DumbWallJoiner().flip(self.wall)
|
|
bpy.context.view_layer.update()
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
|
|
else:
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[3])
|
|
else:
|
|
snap_point = self.wall.matrix_world @ Vector(self.wall.bound_box[0])
|
|
|
|
start = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[0])
|
|
end = self.reference_wall.matrix_world @ Vector(self.reference_wall.bound_box[4])
|
|
|
|
point, _ = mathutils.geometry.intersect_point_line(snap_point, start, end)
|
|
|
|
offset = snap_point - self.wall.matrix_world.translation
|
|
new_origin = point - offset
|
|
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
|
|
|
|
def align_rotation(self):
|
|
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
|
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
|
angle = reference.angle_signed(wall)
|
|
if round(degrees(angle) % 360) in (0, 180):
|
|
return
|
|
elif angle > (pi / 2):
|
|
self.wall.rotation_euler[2] -= pi - angle
|
|
else:
|
|
self.wall.rotation_euler[2] += angle
|
|
bpy.context.view_layer.update()
|
|
|
|
def is_rotation_flipped(self):
|
|
reference = (self.reference_wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
|
wall = (self.wall.matrix_world.to_quaternion() @ Vector((1, 0, 0))).to_2d()
|
|
angle = reference.angle_signed(wall)
|
|
return round(degrees(angle) % 360) == 180
|
|
|
|
|
|
class DumbWallRecalculator:
|
|
def recalculate(self, walls):
|
|
queue = set()
|
|
for wall in walls:
|
|
element = tool.Ifc.get_entity(wall)
|
|
queue.add((element, wall))
|
|
for rel in getattr(element, "ConnectedTo", []):
|
|
queue.add((rel.RelatedElement, tool.Ifc.get_object(rel.RelatedElement)))
|
|
for rel in getattr(element, "ConnectedFrom", []):
|
|
queue.add((rel.RelatingElement, tool.Ifc.get_object(rel.RelatingElement)))
|
|
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
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
|
|
def generate(self, draw_from_polyline=False):
|
|
self.file = IfcStore.get_file()
|
|
self.layers = tool.Model.get_material_layer_parameters(self.relating_type)
|
|
if not self.layers["thickness"]:
|
|
return
|
|
|
|
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
|
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
|
|
|
props = bpy.context.scene.BIMModelProperties
|
|
self.container = None
|
|
if container := tool.Root.get_default_container():
|
|
self.container = container
|
|
self.container_obj = tool.Ifc.get_object(container)
|
|
self.width = self.layers["thickness"]
|
|
self.height = props.extrusion_depth
|
|
self.length = props.length
|
|
self.rotation = 0.0
|
|
self.location = Vector((0, 0, 0))
|
|
self.x_angle = 0 if tool.Cad.is_x(props.x_angle, 0, tolerance=0.001) else props.x_angle
|
|
|
|
if draw_from_polyline:
|
|
return self.derive_from_polyline()
|
|
else:
|
|
return self.derive_from_cursor()
|
|
|
|
def has_sketch(self):
|
|
return (
|
|
bpy.context.scene.grease_pencil
|
|
and len(bpy.context.scene.grease_pencil.layers) == 1
|
|
and bpy.context.scene.grease_pencil.layers[0].info == "Note"
|
|
and bpy.context.scene.grease_pencil.layers[0].active_frame.strokes
|
|
)
|
|
|
|
def derive_from_polyline(self):
|
|
polyline_data = bpy.context.scene.BIMModelProperties.polyline_point
|
|
is_polyline_closed = False
|
|
if len(polyline_data) > 3:
|
|
first_vec = Vector((polyline_data[0].x, polyline_data[0].y, polyline_data[0].z))
|
|
last_vec = Vector((polyline_data[-1].x, polyline_data[-1].y, polyline_data[-1].z))
|
|
if first_vec == last_vec:
|
|
is_polyline_closed = True
|
|
|
|
walls = []
|
|
for i in range(len(polyline_data) - 1):
|
|
vec1 = Vector((polyline_data[i].x, polyline_data[i].y, polyline_data[i].z))
|
|
vec2 = Vector((polyline_data[i + 1].x, polyline_data[i + 1].y, polyline_data[i + 1].z))
|
|
coords = (vec1, vec2)
|
|
walls.append(self.create_wall_from_2_points(coords))
|
|
return walls, is_polyline_closed
|
|
|
|
def derive_from_sketch(self):
|
|
objs = []
|
|
strokes = []
|
|
layer = bpy.context.scene.grease_pencil.layers[0]
|
|
|
|
for stroke in layer.active_frame.strokes:
|
|
if len(stroke.points) == 1:
|
|
continue
|
|
data = self.create_wall_from_2_points((stroke.points[0].co, stroke.points[-1].co))
|
|
if data:
|
|
strokes.append(data)
|
|
objs.append(data["obj"])
|
|
|
|
if len(objs) < 2:
|
|
return objs
|
|
|
|
l_joins = set()
|
|
for stroke in strokes:
|
|
if not stroke["obj"]:
|
|
continue
|
|
for stroke2 in strokes:
|
|
if stroke2 == stroke or not stroke2["obj"]:
|
|
continue
|
|
if self.has_nearby_ends(stroke, stroke2):
|
|
wall_join = "-JOIN-".join(sorted([stroke["obj"].name, stroke2["obj"].name]))
|
|
if wall_join not in l_joins:
|
|
l_joins.add(wall_join)
|
|
DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_L()
|
|
elif self.has_end_near_stroke(stroke, stroke2):
|
|
DumbWallJoiner(stroke["obj"], stroke2["obj"]).join_T()
|
|
bpy.context.scene.grease_pencil.layers.remove(layer)
|
|
return objs
|
|
|
|
def create_wall_from_2_points(self, coords):
|
|
direction = coords[1] - coords[0]
|
|
length = direction.length
|
|
if length < 0.1:
|
|
return
|
|
data = {"coords": coords}
|
|
|
|
# Round to nearest 50mm (yes, metric for now)
|
|
self.length = 0.05 * round(length / 0.05)
|
|
self.rotation = math.atan2(direction[1], direction[0])
|
|
# Round to nearest 5 degrees
|
|
nearest_degree = (math.pi / 180) * 5
|
|
self.rotation = nearest_degree * round(self.rotation / nearest_degree)
|
|
self.location = coords[0]
|
|
data["obj"] = self.create_wall()
|
|
return data
|
|
|
|
def has_end_near_stroke(self, stroke, stroke2):
|
|
point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][0], *stroke2["coords"])
|
|
if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][0]):
|
|
return True
|
|
point, distance = mathutils.geometry.intersect_point_line(stroke["coords"][1], *stroke2["coords"])
|
|
if distance > 0 and distance < 1 and self.is_near(point, stroke["coords"][1]):
|
|
return True
|
|
|
|
def has_nearby_ends(self, stroke, stroke2):
|
|
return (
|
|
self.is_near(stroke["coords"][0], stroke2["coords"][0])
|
|
or self.is_near(stroke["coords"][0], stroke2["coords"][1])
|
|
or self.is_near(stroke["coords"][1], stroke2["coords"][0])
|
|
or self.is_near(stroke["coords"][1], stroke2["coords"][1])
|
|
)
|
|
|
|
def is_near(self, point1, point2):
|
|
return (point1 - point2).length < 0.1
|
|
|
|
def derive_from_cursor(self):
|
|
RAYCAST_PRECISION = 0.01
|
|
self.location = bpy.context.scene.cursor.location
|
|
if self.container:
|
|
for subelement in ifcopenshell.util.element.get_decomposition(self.container):
|
|
if not subelement.is_a("IfcWall"):
|
|
continue
|
|
sibling_obj = tool.Ifc.get_object(subelement)
|
|
if not sibling_obj or not isinstance(sibling_obj.data, bpy.types.Mesh):
|
|
continue
|
|
inv_obj_matrix = sibling_obj.matrix_world.inverted()
|
|
local_location = inv_obj_matrix @ self.location
|
|
try:
|
|
raycast = sibling_obj.closest_point_on_mesh(local_location, distance=RAYCAST_PRECISION)
|
|
except:
|
|
# If the mesh has no faces
|
|
raycast = [None]
|
|
if not raycast[0]:
|
|
continue
|
|
for face in sibling_obj.data.polygons:
|
|
normal = (sibling_obj.matrix_world.to_quaternion() @ face.normal).normalized()
|
|
face_center = sibling_obj.matrix_world @ face.center
|
|
if (
|
|
normal.z != 0
|
|
or abs(mathutils.geometry.distance_point_to_plane(self.location, face_center, normal)) > 0.01
|
|
):
|
|
continue
|
|
|
|
rotation = math.atan2(normal[1], normal[0])
|
|
rotated_y_axis = Matrix.Rotation(-rotation, 4, "Z")[1].xyz
|
|
|
|
# since wall thickness goes by local Y+ axis
|
|
# we find best position for the next wall
|
|
# by finding the face of another wall that will be very close to the some test point.
|
|
# test point is calculated by applying to cursor position some little offset along the face
|
|
#
|
|
# a bit different offset to be safe on raycast
|
|
test_pos = self.location + rotated_y_axis * RAYCAST_PRECISION * 1.1
|
|
test_pos_local = inv_obj_matrix @ test_pos
|
|
raycast = sibling_obj.closest_point_on_mesh(test_pos_local, distance=RAYCAST_PRECISION)
|
|
|
|
if not raycast[0]:
|
|
continue
|
|
self.rotation = rotation
|
|
break
|
|
|
|
if self.rotation != 0:
|
|
break
|
|
return self.create_wall()
|
|
|
|
def create_wall(self):
|
|
props = bpy.context.scene.BIMModelProperties
|
|
ifc_class = self.get_relating_type_class(self.relating_type)
|
|
mesh = bpy.data.meshes.new("Dummy")
|
|
obj = bpy.data.objects.new(tool.Model.generate_occurrence_name(self.relating_type, ifc_class), mesh)
|
|
|
|
matrix_world = Matrix.Rotation(self.rotation, 4, "Z")
|
|
matrix_world.translation = self.location
|
|
if self.container_obj:
|
|
matrix_world.translation.z = self.container_obj.location.z + props.rl1
|
|
obj.matrix_world = matrix_world
|
|
bpy.context.view_layer.update()
|
|
|
|
element = blenderbim.core.root.assign_class(
|
|
tool.Ifc,
|
|
tool.Collector,
|
|
tool.Root,
|
|
obj=obj,
|
|
ifc_class=ifc_class,
|
|
should_add_representation=False,
|
|
)
|
|
ifcopenshell.api.run("type.assign_type", self.file, related_objects=[element], relating_type=self.relating_type)
|
|
if self.axis_context:
|
|
representation = ifcopenshell.api.run(
|
|
"geometry.add_axis_representation",
|
|
tool.Ifc.get(),
|
|
context=self.axis_context,
|
|
axis=[(0.0, 0.0), (self.length, 0.0)],
|
|
)
|
|
ifcopenshell.api.run(
|
|
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
|
|
)
|
|
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
|
representation = ifcopenshell.api.run(
|
|
"geometry.add_wall_representation",
|
|
tool.Ifc.get(),
|
|
context=self.body_context,
|
|
thickness=self.layers["thickness"],
|
|
offset=self.layers["offset"],
|
|
length=self.length,
|
|
height=self.height,
|
|
x_angle=self.x_angle,
|
|
)
|
|
ifcopenshell.api.run(
|
|
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=representation
|
|
)
|
|
blenderbim.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=representation,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
)
|
|
tool.Blender.remove_data_block(mesh)
|
|
pset = ifcopenshell.api.run("pset.add_pset", self.file, product=element, name="EPset_Parametric")
|
|
ifcopenshell.api.run("pset.edit_pset", self.file, pset=pset, properties={"Engine": "BlenderBIM.DumbLayer2"})
|
|
obj.select_set(True)
|
|
return obj
|
|
|
|
def get_relating_type_class(self, relating_type):
|
|
classes = ifcopenshell.util.type.get_applicable_entities(relating_type.is_a(), tool.Ifc.get().schema)
|
|
return [c for c in classes if "StandardCase" not in c][0]
|
|
|
|
|
|
def calculate_quantities(usecase_path, ifc_file, settings):
|
|
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
|
obj = settings["blender_object"]
|
|
product = ifc_file.by_id(obj.BIMObjectProperties.ifc_definition_id)
|
|
parametric = ifcopenshell.util.element.get_psets(product).get("EPset_Parametric")
|
|
if not parametric or "Engine" not in parametric or parametric["Engine"] != "BlenderBIM.DumbLayer2":
|
|
return
|
|
qto = ifcopenshell.api.run(
|
|
"pset.add_qto", ifc_file, should_run_listeners=False, product=product, name="Qto_WallBaseQuantities"
|
|
)
|
|
length = obj.dimensions[0] / unit_scale
|
|
width = obj.dimensions[1] / unit_scale
|
|
height = obj.dimensions[2] / unit_scale
|
|
|
|
bm_gross = bmesh.new()
|
|
bm_gross.from_mesh(obj.data)
|
|
bm_gross.faces.ensure_lookup_table()
|
|
|
|
bm_net = bmesh.new()
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
evaluated_mesh = obj.evaluated_get(depsgraph).data
|
|
bm_net.from_mesh(evaluated_mesh)
|
|
bm_net.faces.ensure_lookup_table()
|
|
|
|
gross_footprint_area = sum([f.calc_area() for f in bm_gross.faces if f.normal.z < -0.9])
|
|
net_footprint_area = sum([f.calc_area() for f in bm_net.faces if f.normal.z < -0.9])
|
|
gross_side_area = sum([f.calc_area() for f in bm_gross.faces if f.normal.y > 0.9])
|
|
net_side_area = sum([f.calc_area() for f in bm_net.faces if f.normal.y > 0.9])
|
|
gross_volume = bm_gross.calc_volume()
|
|
net_volume = bm_net.calc_volume()
|
|
bm_gross.free()
|
|
bm_net.free()
|
|
|
|
ifcopenshell.api.run(
|
|
"pset.edit_qto",
|
|
ifc_file,
|
|
should_run_listeners=False,
|
|
qto=qto,
|
|
properties={
|
|
"Length": round(length, 2),
|
|
"Width": round(width, 2),
|
|
"Height": round(height, 2),
|
|
"GrossFootprintArea": round(gross_footprint_area, 2),
|
|
"NetFootprintArea": round(net_footprint_area, 2),
|
|
"GrossSideArea": round(gross_side_area, 2),
|
|
"NetSideArea": round(net_side_area, 2),
|
|
"GrossVolume": round(gross_volume, 2),
|
|
"NetVolume": round(net_volume, 2),
|
|
},
|
|
)
|
|
|
|
|
|
class DumbWallPlaner:
|
|
def regenerate_from_layer(self, usecase_path, ifc_file, settings):
|
|
if settings["attributes"].get("LayerThickness") is None:
|
|
return
|
|
walls = []
|
|
layer = settings["layer"]
|
|
for layer_set in layer.ToMaterialLayerSet:
|
|
total_thickness = sum([l.LayerThickness for l in layer_set.MaterialLayers])
|
|
if not total_thickness:
|
|
continue
|
|
for inverse in ifc_file.get_inverse(layer_set):
|
|
if not inverse.is_a("IfcMaterialLayerSetUsage") or inverse.LayerSetDirection != "AXIS2":
|
|
continue
|
|
if ifc_file.schema == "IFC2X3":
|
|
for rel in ifc_file.get_inverse(inverse):
|
|
if not rel.is_a("IfcRelAssociatesMaterial"):
|
|
continue
|
|
walls.extend([tool.Ifc.get_object(e) for e in rel.RelatedObjects])
|
|
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])
|
|
|
|
def regenerate_from_type(self, usecase_path, ifc_file, settings):
|
|
relating_type = settings["relating_type"]
|
|
|
|
new_material = ifcopenshell.util.element.get_material(relating_type)
|
|
if not new_material or not new_material.is_a("IfcMaterialLayerSet"):
|
|
return
|
|
|
|
parametric = ifcopenshell.util.element.get_psets(relating_type).get("EPset_Parametric")
|
|
layer_set_direction = None
|
|
if parametric:
|
|
layer_set_direction = parametric.get("LayerSetDirection", layer_set_direction)
|
|
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc_file)
|
|
for related_object in settings["related_objects"]:
|
|
self._regenerate_from_type(related_object, layer_set_direction)
|
|
|
|
def _regenerate_from_type(
|
|
self, related_object: ifcopenshell.entity_instance, layer_set_direction: Optional[str]
|
|
) -> None:
|
|
obj = tool.Ifc.get_object(related_object)
|
|
if not obj or not obj.data or not obj.data.BIMMeshProperties.ifc_definition_id:
|
|
return
|
|
|
|
material = ifcopenshell.util.element.get_material(related_object)
|
|
if not material or not material.is_a("IfcMaterialLayerSetUsage"):
|
|
return
|
|
if layer_set_direction:
|
|
material.LayerSetDirection = layer_set_direction
|
|
if material.LayerSetDirection == "AXIS2":
|
|
DumbWallRecalculator().recalculate([obj])
|
|
|
|
|
|
class DumbWallJoiner:
|
|
def __init__(self):
|
|
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
|
self.axis_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Plan", "Axis", "GRAPH_VIEW")
|
|
self.body_context = ifcopenshell.util.representation.get_context(tool.Ifc.get(), "Model", "Body", "MODEL_VIEW")
|
|
|
|
def unjoin(self, wall1):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
if not element1:
|
|
return
|
|
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATSTART")
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
|
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis = copy.deepcopy(axis1["reference"])
|
|
body = copy.deepcopy(axis1["reference"])
|
|
self.recreate_wall(element1, wall1, axis, body)
|
|
|
|
def split(self, wall1, target):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
if not element1:
|
|
return
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis2 = copy.deepcopy(axis1)
|
|
intersect, cut_percentage = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
|
|
if cut_percentage < 0 or cut_percentage > 1 or tool.Cad.is_x(cut_percentage, (0, 1)):
|
|
return
|
|
connection = "ATEND" if cut_percentage > 0.5 else "ATSTART"
|
|
|
|
wall2 = self.duplicate_wall(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element2, connection_type="ATSTART")
|
|
|
|
# During the duplication process, unfilled voids are copied, so we need
|
|
# to check openings on both element1 and element2. Let's check element1
|
|
# first.
|
|
for opening in [
|
|
r.RelatedOpeningElement for r in element1.HasOpenings if not r.RelatedOpeningElement.HasFillings
|
|
]:
|
|
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
|
|
opening_location = opening_matrix.translation
|
|
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
|
|
if opening_position > cut_percentage:
|
|
# The opening should be removed from element1.
|
|
ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
|
|
|
|
# Now let's check element2.
|
|
for opening in [
|
|
r.RelatedOpeningElement for r in element2.HasOpenings if not r.RelatedOpeningElement.HasFillings
|
|
]:
|
|
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
|
|
opening_location = opening_matrix.translation
|
|
_, opening_position = mathutils.geometry.intersect_point_line(opening_location.to_2d(), *axis1["reference"])
|
|
if opening_position < cut_percentage:
|
|
# The opening should be removed from element2.
|
|
ifcopenshell.api.run("void.remove_opening", tool.Ifc.get(), opening=opening)
|
|
|
|
# During the duplication process, filled voids are not copied. So we
|
|
# only need to check fillings on the original element1.
|
|
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings if r.RelatedOpeningElement.HasFillings]:
|
|
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
|
|
filling_location = filling_obj.matrix_world.translation
|
|
_, filling_position = mathutils.geometry.intersect_point_line(filling_location.to_2d(), *axis1["reference"])
|
|
if filling_position > cut_percentage:
|
|
# The filling should be moved from element1 to element2.
|
|
FilledOpeningGenerator().generate(filling_obj, wall2, target=filling_obj.matrix_world.translation)
|
|
|
|
axis1["reference"][1] = intersect
|
|
axis2["reference"][0] = intersect
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"])
|
|
|
|
def flip(self, wall1):
|
|
if tool.Ifc.is_moved(wall1):
|
|
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=wall1)
|
|
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
if not element1 or tool.Model.get_usage_type(element1) != "LAYER2":
|
|
return
|
|
|
|
for rel in element1.ConnectedTo:
|
|
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatingConnectionType in ["ATSTART", "ATEND"]:
|
|
rel.RelatingConnectionType = "ATSTART" if rel.RelatingConnectionType == "ATEND" else "ATEND"
|
|
for rel in element1.ConnectedFrom:
|
|
if rel.is_a("IfcRelConnectsPathElements") and rel.RelatedConnectionType in ["ATSTART", "ATEND"]:
|
|
rel.RelatedConnectionType = "ATSTART" if rel.RelatedConnectionType == "ATEND" else "ATEND"
|
|
|
|
layers1 = tool.Model.get_material_layer_parameters(element1)
|
|
axis1 = tool.Model.get_wall_axis(wall1, layers1)
|
|
axis1["reference"][0], axis1["reference"][1] = axis1["reference"][1], axis1["reference"][0]
|
|
|
|
flip_matrix = Matrix.Rotation(pi, 4, "Z")
|
|
wall1.matrix_world = wall1.matrix_world @ flip_matrix
|
|
wall1.matrix_world[0][3], wall1.matrix_world[1][3] = axis1["reference"][0]
|
|
bpy.context.view_layer.update()
|
|
|
|
# The wall should flip, but all openings and fills should stay and shift to the opposite axis
|
|
opening_matrixes = {}
|
|
filling_matrixes = {}
|
|
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings]:
|
|
opening_matrix = Matrix(ifcopenshell.util.placement.get_local_placement(opening.ObjectPlacement).tolist())
|
|
location = opening_matrix.translation
|
|
location_on_base = tool.Cad.point_on_edge(location, axis1["base"])
|
|
location_on_side = tool.Cad.point_on_edge(location, axis1["side"])
|
|
if (location_on_base - location).length < (location_on_side - location).length:
|
|
axis_offset = location_on_side - location_on_base
|
|
offset_from_axis = location_on_base - location
|
|
opening_matrix.translation = location_on_base - axis_offset - offset_from_axis
|
|
else:
|
|
axis_offset = location_on_side - location_on_base
|
|
offset_from_axis = location_on_side - location
|
|
opening_matrix.translation = location_on_side - axis_offset - offset_from_axis
|
|
opening_matrixes[opening] = opening_matrix
|
|
|
|
for filling in [r.RelatedBuildingElement for r in opening.HasFillings]:
|
|
filling_obj = tool.Ifc.get_object(filling)
|
|
filling_matrix = filling_obj.matrix_world.copy()
|
|
|
|
location = filling_matrix.translation
|
|
location_on_base = tool.Cad.point_on_edge(location, axis1["base"])
|
|
location_on_side = tool.Cad.point_on_edge(location, axis1["side"])
|
|
if (location_on_base - location).length < (location_on_side - location).length:
|
|
axis_offset = location_on_side - location_on_base
|
|
offset_from_axis = location_on_base - location
|
|
filling_matrix.translation = location_on_base - axis_offset - offset_from_axis
|
|
else:
|
|
axis_offset = location_on_side - location_on_base
|
|
offset_from_axis = location_on_side - location
|
|
filling_matrix.translation = location_on_side - axis_offset - offset_from_axis
|
|
filling_matrixes[filling] = filling_matrix
|
|
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
DumbWallRecalculator().recalculate([wall1])
|
|
|
|
for opening in [r.RelatedOpeningElement for r in element1.HasOpenings]:
|
|
opening_matrix = opening_matrixes[opening]
|
|
ifcopenshell.api.run(
|
|
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=opening_matrix
|
|
)
|
|
for filling in [r.RelatedBuildingElement for r in opening.HasFillings]:
|
|
filling_matrix = filling_matrixes[filling]
|
|
filling_obj = tool.Ifc.get_object(filling)
|
|
filling_obj.matrix_world = filling_matrix
|
|
|
|
if filling_matrixes:
|
|
bpy.context.view_layer.update()
|
|
|
|
body = ifcopenshell.util.representation.get_representation(element1, "Model", "Body", "MODEL_VIEW")
|
|
blenderbim.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=wall1,
|
|
representation=body,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
)
|
|
|
|
def merge(self, wall1, wall2):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis2 = tool.Model.get_wall_axis(wall2)
|
|
|
|
angle = tool.Cad.angle_edges(axis1["reference"], axis2["reference"], signed=False, degrees=True)
|
|
if not tool.Cad.is_x(angle, 0, tolerance=0.001):
|
|
return
|
|
|
|
intersect1, connection1 = mathutils.geometry.intersect_point_line(axis2["reference"][0], *axis1["reference"])
|
|
if not tool.Cad.is_x((intersect1 - axis2["reference"][0]).length, 0):
|
|
return
|
|
|
|
intersect2, connection2 = mathutils.geometry.intersect_point_line(axis2["reference"][1], *axis1["reference"])
|
|
if not tool.Cad.is_x((intersect2 - axis2["reference"][1]).length, 0):
|
|
return
|
|
|
|
changed_connections = set()
|
|
|
|
if connection1 < 0:
|
|
changed_connections.add("ATSTART")
|
|
axis1["reference"][0] = intersect2 if connection2 < connection1 else intersect1
|
|
elif connection1 > 1:
|
|
changed_connections.add("ATEND")
|
|
axis1["reference"][1] = intersect2 if connection2 > connection1 else intersect1
|
|
|
|
for connection in changed_connections:
|
|
ifcopenshell.api.run(
|
|
"geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection
|
|
)
|
|
|
|
for rel in element2.ConnectedTo:
|
|
if rel.RelatingConnectionType in changed_connections:
|
|
other = tool.Ifc.get_object(rel.RelatedElement)
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_path",
|
|
tool.Ifc.get(),
|
|
relating_element=element1,
|
|
related_element=rel.RelatedElement,
|
|
relating_connection=rel.RelatingConnectionType,
|
|
related_connection=rel.RelatedConnectionType,
|
|
)
|
|
|
|
for rel in element2.ConnectedFrom:
|
|
if rel.RelatedConnectionType in changed_connections:
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_path",
|
|
tool.Ifc.get(),
|
|
relating_element=rel.RelatingElement,
|
|
related_element=element1,
|
|
relating_connection=rel.RelatingConnectionType,
|
|
related_connection=rel.RelatedConnectionType,
|
|
)
|
|
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
bpy.data.objects.remove(wall2)
|
|
|
|
def duplicate_wall(self, wall1):
|
|
wall2 = wall1.copy()
|
|
wall2.data = wall2.data.copy()
|
|
for collection in wall1.users_collection:
|
|
collection.objects.link(wall2)
|
|
blenderbim.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=wall2)
|
|
return wall2
|
|
|
|
def join_Z(self, wall1, slab2):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(slab2)
|
|
|
|
for rel in element1.ConnectedFrom:
|
|
if rel.is_a() == "IfcRelConnectsElements" and rel.Description == "TOP":
|
|
ifcopenshell.api.run(
|
|
"geometry.disconnect_element",
|
|
tool.Ifc.get(),
|
|
relating_element=rel.RelatingElement,
|
|
related_element=element1,
|
|
)
|
|
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_element",
|
|
tool.Ifc.get(),
|
|
relating_element=element2,
|
|
related_element=element1,
|
|
description="TOP",
|
|
)
|
|
|
|
self.recreate_wall(element1, wall1)
|
|
|
|
def join_L(self, wall1, wall2):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis2 = tool.Model.get_wall_axis(wall2)
|
|
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
|
if intersect:
|
|
intersect, _ = intersect
|
|
else:
|
|
return
|
|
wall1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
|
wall2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2["reference"]) > 0.5 else "ATSTART"
|
|
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_path",
|
|
tool.Ifc.get(),
|
|
relating_element=element1,
|
|
related_element=element2,
|
|
relating_connection=wall1_end,
|
|
related_connection=wall2_end,
|
|
description="BUTT",
|
|
)
|
|
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"])
|
|
|
|
def join_E(self, wall1, target):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
intersect, connection = mathutils.geometry.intersect_point_line(target.to_2d(), *axis1["reference"])
|
|
connection = "ATEND" if connection > 0.5 else "ATSTART"
|
|
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type=connection)
|
|
|
|
axis = copy.deepcopy(axis1["reference"])
|
|
body = copy.deepcopy(axis1["reference"])
|
|
axis[1 if connection == "ATEND" else 0] = intersect
|
|
body[1 if connection == "ATEND" else 0] = intersect
|
|
|
|
self.recreate_wall(element1, wall1, axis, body)
|
|
|
|
def set_length(self, wall1, si_length):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
if not element1:
|
|
return
|
|
|
|
ifcopenshell.api.run("geometry.disconnect_path", tool.Ifc.get(), element=element1, connection_type="ATEND")
|
|
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis = copy.deepcopy(axis1["reference"])
|
|
body = copy.deepcopy(axis1["reference"])
|
|
end = (wall1.matrix_world @ Vector((si_length, 0, 0))).to_2d()
|
|
axis[1] = end
|
|
body[1] = end
|
|
self.recreate_wall(element1, wall1, axis, body)
|
|
|
|
def join_T(self, wall1, wall2):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis2 = tool.Model.get_wall_axis(wall2)
|
|
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
|
if intersect:
|
|
intersect, _ = intersect
|
|
else:
|
|
return
|
|
connection = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
|
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_path",
|
|
tool.Ifc.get(),
|
|
related_element=element1,
|
|
relating_element=element2,
|
|
relating_connection="ATPATH",
|
|
related_connection=connection,
|
|
description="BUTT",
|
|
)
|
|
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
|
|
def join_V(self, wall1, wall2):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
axis1 = tool.Model.get_wall_axis(wall1)
|
|
axis2 = tool.Model.get_wall_axis(wall2)
|
|
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
|
if intersect:
|
|
intersect, _ = intersect
|
|
else:
|
|
return
|
|
wall1_end = "ATEND" if tool.Cad.edge_percent(intersect, axis1["reference"]) > 0.5 else "ATSTART"
|
|
wall2_end = "ATEND" if tool.Cad.edge_percent(intersect, axis2["reference"]) > 0.5 else "ATSTART"
|
|
|
|
ifcopenshell.api.run(
|
|
"geometry.connect_path",
|
|
tool.Ifc.get(),
|
|
relating_element=element1,
|
|
related_element=element2,
|
|
relating_connection=wall1_end,
|
|
related_connection=wall2_end,
|
|
description="MITRE",
|
|
)
|
|
|
|
self.recreate_wall(element1, wall1, axis1["reference"], axis1["reference"])
|
|
self.recreate_wall(element2, wall2, axis2["reference"], axis2["reference"])
|
|
|
|
def recreate_wall(self, element, obj, axis=None, body=None):
|
|
if axis is None or body is None:
|
|
axis = body = tool.Model.get_wall_axis(obj)["reference"]
|
|
self.axis = copy.deepcopy(axis)
|
|
self.body = copy.deepcopy(body)
|
|
extrusion_data = self.get_extrusion_data(tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id))
|
|
height = extrusion_data["height"]
|
|
x_angle = extrusion_data["x_angle"]
|
|
self.clippings = []
|
|
layers = tool.Model.get_material_layer_parameters(element)
|
|
|
|
for rel in element.ConnectedTo:
|
|
if rel.is_a("IfcRelConnectsPathElements"):
|
|
connection = rel.RelatingConnectionType
|
|
other = tool.Ifc.get_object(rel.RelatedElement)
|
|
if connection not in ["ATPATH", "NOTDEFINED"]:
|
|
self.join(
|
|
obj, other, connection, rel.RelatedConnectionType, is_relating=True, description=rel.Description
|
|
)
|
|
for rel in element.ConnectedFrom:
|
|
if rel.is_a("IfcRelConnectsPathElements"):
|
|
connection = rel.RelatedConnectionType
|
|
other = tool.Ifc.get_object(rel.RelatingElement)
|
|
if connection not in ["ATPATH", "NOTDEFINED"]:
|
|
self.join(
|
|
obj,
|
|
other,
|
|
connection,
|
|
rel.RelatingConnectionType,
|
|
is_relating=False,
|
|
description=rel.Description,
|
|
)
|
|
|
|
previous_matrix = obj.matrix_world.copy()
|
|
previous_origin = previous_matrix.translation.xy
|
|
obj.matrix_world.translation.xy = self.body[0]
|
|
bpy.context.view_layer.update()
|
|
|
|
for rel in element.ConnectedFrom:
|
|
if rel.is_a() == "IfcRelConnectsElements":
|
|
height = self.clip(obj, tool.Ifc.get_object(rel.RelatingElement))
|
|
|
|
new_matrix = copy.deepcopy(obj.matrix_world)
|
|
new_matrix.invert()
|
|
|
|
for clipping in self.clippings:
|
|
if clipping["operand_type"] == "IfcHalfSpaceSolid":
|
|
clipping["matrix"] = new_matrix @ clipping["matrix"]
|
|
|
|
length = (self.body[1] - self.body[0]).length
|
|
|
|
if self.axis_context:
|
|
axis = [(new_matrix @ a.to_3d()).to_2d() for a in self.axis]
|
|
new_axis = ifcopenshell.api.run(
|
|
"geometry.add_axis_representation", tool.Ifc.get(), context=self.axis_context, axis=axis
|
|
)
|
|
old_axis = ifcopenshell.util.representation.get_representation(element, "Plan", "Axis", "GRAPH_VIEW")
|
|
if old_axis:
|
|
for inverse in tool.Ifc.get().get_inverse(old_axis):
|
|
ifcopenshell.util.element.replace_attribute(inverse, old_axis, new_axis)
|
|
blenderbim.core.geometry.remove_representation(
|
|
tool.Ifc, tool.Geometry, obj=obj, representation=old_axis
|
|
)
|
|
else:
|
|
ifcopenshell.api.run(
|
|
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_axis
|
|
)
|
|
|
|
new_body = ifcopenshell.api.run(
|
|
"geometry.add_wall_representation",
|
|
tool.Ifc.get(),
|
|
context=self.body_context,
|
|
length=length,
|
|
height=height,
|
|
x_angle=x_angle,
|
|
offset=layers["offset"],
|
|
thickness=layers["thickness"],
|
|
clippings=self.clippings,
|
|
booleans=tool.Model.get_manual_booleans(element),
|
|
)
|
|
|
|
old_body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
|
if old_body:
|
|
for inverse in tool.Ifc.get().get_inverse(old_body):
|
|
ifcopenshell.util.element.replace_attribute(inverse, old_body, new_body)
|
|
obj.data.BIMMeshProperties.ifc_definition_id = int(new_body.id())
|
|
obj.data.name = f"{self.body_context.id()}/{new_body.id()}"
|
|
blenderbim.core.geometry.remove_representation(tool.Ifc, tool.Geometry, obj=obj, representation=old_body)
|
|
else:
|
|
ifcopenshell.api.run(
|
|
"geometry.assign_representation", tool.Ifc.get(), product=element, representation=new_body
|
|
)
|
|
|
|
wall_moved = tool.Ifc.is_moved(obj)
|
|
if wall_moved:
|
|
# Openings should move with the host overall ...
|
|
# ... except their position should stay the same along the local X axis of the wall
|
|
for opening in [
|
|
r.RelatedOpeningElement for r in element.HasOpenings if not r.RelatedOpeningElement.HasFillings
|
|
]:
|
|
percent = tool.Cad.edge_percent(
|
|
self.body[0], (previous_origin, (previous_matrix @ Vector((1, 0, 0))).to_2d())
|
|
)
|
|
is_x_offset_increased = True if percent < 0 else False
|
|
|
|
change_in_x = (self.body[0] - previous_origin).length / self.unit_scale
|
|
coordinates = list(opening.ObjectPlacement.RelativePlacement.Location.Coordinates)
|
|
if is_x_offset_increased:
|
|
coordinates[0] += change_in_x
|
|
else:
|
|
coordinates[0] -= change_in_x
|
|
opening.ObjectPlacement.RelativePlacement.Location.Coordinates = coordinates
|
|
|
|
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
|
|
|
# If opening has filling then stick to the filling's position
|
|
# We're applying new openings position only after wall position is applied
|
|
for opening in [r.RelatedOpeningElement for r in element.HasOpenings if r.RelatedOpeningElement.HasFillings]:
|
|
similar_openings = blenderbim.core.geometry.get_similar_openings(tool.Ifc, opening)
|
|
filling_obj = tool.Ifc.get_object(opening.HasFillings[0].RelatedBuildingElement)
|
|
filling_moved = tool.Ifc.is_moved(filling_obj)
|
|
if filling_moved:
|
|
blenderbim.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=filling_obj)
|
|
if filling_moved or wall_moved:
|
|
ifcopenshell.api.run(
|
|
"geometry.edit_object_placement", tool.Ifc.get(), product=opening, matrix=filling_obj.matrix_world
|
|
)
|
|
blenderbim.core.geometry.edit_similar_opening_placement(tool.Geometry, opening, similar_openings)
|
|
|
|
blenderbim.core.geometry.switch_representation(
|
|
tool.Ifc,
|
|
tool.Geometry,
|
|
obj=obj,
|
|
representation=new_body,
|
|
should_reload=True,
|
|
is_global=True,
|
|
should_sync_changes_first=False,
|
|
)
|
|
tool.Geometry.record_object_materials(obj)
|
|
|
|
def create_matrix(self, p, x, y, z):
|
|
return Matrix([x, y, z, p]).to_4x4().transposed()
|
|
|
|
def get_extrusion_data(self, representation):
|
|
results = {"item": None, "height": 3.0, "x_angle": 0, "is_sloped": False, "direction": Vector((0, 0, 1))}
|
|
item = representation.Items[0]
|
|
while True:
|
|
if item.is_a("IfcExtrudedAreaSolid"):
|
|
results["item"] = item
|
|
x, y, z = item.ExtrudedDirection.DirectionRatios
|
|
if not tool.Cad.is_x(x, 0) or not tool.Cad.is_x(y, 0) or not tool.Cad.is_x(z, 1):
|
|
results["direction"] = Vector(item.ExtrudedDirection.DirectionRatios)
|
|
results["x_angle"] = Vector((0, 1)).angle_signed(Vector((y, z)))
|
|
results["is_sloped"] = True
|
|
results["height"] = (item.Depth * self.unit_scale) / (1 / cos(results["x_angle"]))
|
|
break
|
|
elif item.is_a("IfcBooleanClippingResult"): # should be before IfcBooleanResult check
|
|
item = item.FirstOperand
|
|
elif item.is_a("IfcBooleanResult"):
|
|
if item.FirstOperand.is_a("IfcExtrudedAreaSolid") or item.FirstOperand.is_a("IfcBooleanResult"):
|
|
item = item.FirstOperand
|
|
else:
|
|
item = item.SecondOperand
|
|
else:
|
|
break
|
|
return results
|
|
|
|
def join(self, wall1, wall2, connection1, connection2, is_relating=True, description="BUTT"):
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(wall2)
|
|
layers1 = tool.Model.get_material_layer_parameters(element1)
|
|
layers2 = tool.Model.get_material_layer_parameters(element2)
|
|
axis1 = tool.Model.get_wall_axis(wall1, layers1)
|
|
axis2 = tool.Model.get_wall_axis(wall2, layers2)
|
|
body1 = ifcopenshell.util.representation.get_representation(element1, "Model", "Body", "MODEL_VIEW")
|
|
body2 = ifcopenshell.util.representation.get_representation(element2, "Model", "Body", "MODEL_VIEW")
|
|
extrusion1 = self.get_extrusion_data(body1)
|
|
extrusion2 = self.get_extrusion_data(body2)
|
|
direction1 = (wall1.matrix_world.to_quaternion() @ extrusion1["direction"]).normalized()
|
|
direction2 = (wall2.matrix_world.to_quaternion() @ extrusion2["direction"]).normalized()
|
|
height1 = extrusion1["height"] * self.unit_scale
|
|
height2 = extrusion2["height"] * self.unit_scale
|
|
depth1 = direction1 * height1
|
|
depth2 = direction2 * height2
|
|
normal1 = (axis1["base"][1] - axis1["base"][0]).to_3d().normalized().cross(direction1)
|
|
normal2 = (axis2["base"][1] - axis2["base"][0]).to_3d().normalized().cross(direction2)
|
|
|
|
angle = tool.Cad.angle_edges(axis1["reference"], axis2["reference"], signed=True, degrees=True)
|
|
if tool.Cad.is_x(abs(angle), (0, 180), tolerance=0.001):
|
|
return False
|
|
|
|
# Work out axis line
|
|
intersect = tool.Cad.intersect_edges(axis1["reference"], axis2["reference"])
|
|
if intersect:
|
|
intersect, _ = intersect
|
|
else:
|
|
return False
|
|
|
|
proposed_axis = [self.axis[0], intersect] if connection1 == "ATEND" else [intersect, self.axis[1]]
|
|
|
|
if tool.Cad.is_x(tool.Cad.angle_edges(self.axis, proposed_axis, degrees=True), 180, tolerance=0.001):
|
|
# The user has moved the wall into an invalid position that cannot connect at the desired end
|
|
return False
|
|
|
|
self.axis = proposed_axis
|
|
|
|
# Work out body
|
|
|
|
# Bottom and top plane point
|
|
bp1 = wall1.matrix_world @ Vector(wall1.bound_box[0])
|
|
bp2 = wall2.matrix_world @ Vector(wall2.bound_box[0])
|
|
tp1 = wall1.matrix_world @ Vector(wall1.bound_box[1])
|
|
|
|
# Axis lines on bottom, for reference, base, and side axes
|
|
def to_3d_axis(axis, z):
|
|
return (Vector((*axis[0], z)), Vector((*axis[1], z)))
|
|
|
|
bra1 = to_3d_axis(axis1["reference"], bp1.z)
|
|
bba1 = to_3d_axis(axis1["base"], bp1.z)
|
|
tba1 = to_3d_axis(axis1["base"], tp1.z)
|
|
bsa1 = to_3d_axis(axis1["side"], bp1.z)
|
|
bba2 = to_3d_axis(axis2["base"], bp2.z)
|
|
bsa2 = to_3d_axis(axis2["side"], bp2.z)
|
|
|
|
# Intersecting the walls sides defined by planes gives 4 lines of intersection
|
|
# Line point, and line direction
|
|
lpb1, ldb1 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bba2[0], normal2)
|
|
lpb2, ldb2 = mathutils.geometry.intersect_plane_plane(bba1[0], normal1, bsa2[0], normal2)
|
|
lps1, lds1 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bba2[0], normal2)
|
|
lps2, lds2 = mathutils.geometry.intersect_plane_plane(bsa1[0], normal1, bsa2[0], normal2)
|
|
|
|
# Intersecting the 4 lines gives the 8 possible verts of intersection
|
|
# 4 on bottom, and 4 on top. 4 on our base line, 4 on our side line.
|
|
# Diagram: https://i.imgur.com/jwWx2Ox.png
|
|
# NOTE: bb/bs always equal lpb/lps?
|
|
bb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, bp1, Vector((0, 0, 1)))
|
|
bb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, bp1, Vector((0, 0, 1)))
|
|
bs1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, bp1, Vector((0, 0, 1)))
|
|
bs2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, bp1, Vector((0, 0, 1)))
|
|
|
|
# similar to bb/bs but also have local z offset
|
|
tb1 = mathutils.geometry.intersect_line_plane(lpb1, lpb1 + ldb1, tp1, Vector((0, 0, 1)))
|
|
tb2 = mathutils.geometry.intersect_line_plane(lpb2, lpb2 + ldb2, tp1, Vector((0, 0, 1)))
|
|
ts1 = mathutils.geometry.intersect_line_plane(lps1, lps1 + lds1, tp1, Vector((0, 0, 1)))
|
|
ts2 = mathutils.geometry.intersect_line_plane(lps2, lps2 + lds2, tp1, Vector((0, 0, 1)))
|
|
|
|
# Let's distinguish the 8 points by whether they are nearer or further away from the other end
|
|
# These 8 points will be used to find the final body position and clippings.
|
|
connected_at_end = connection1 == "ATEND"
|
|
i = 0 if connected_at_end else 1
|
|
|
|
def get_closest_and_furthest_vectors(ref_point_2d, vectors, clamp_axis=None):
|
|
def clamp_point_by_direction(point, edge):
|
|
percent = tool.Cad.edge_percent(point, edge)
|
|
if percent < 0:
|
|
return edge[0]
|
|
return point
|
|
|
|
# When there is a small angle between walls, intersection points can occur outside the wall's axis.
|
|
# Which can lead to inaccuracies - therefore we bottom clamp them to stay within the axis
|
|
if clamp_axis:
|
|
# if wall connected at the start then reference point will be at the end
|
|
# therefore we reverse the axis
|
|
if not connected_at_end:
|
|
clamp_axis = clamp_axis[::-1]
|
|
vectors = tuple([clamp_point_by_direction(v, clamp_axis) for v in vectors])
|
|
|
|
return tool.Cad.closest_and_furthest_vectors(ref_point_2d.to_3d(), vectors)
|
|
|
|
bbn, bbf = get_closest_and_furthest_vectors(axis1["base"][i], (bb1, bb2), bba1)
|
|
bsn, bsf = get_closest_and_furthest_vectors(axis1["side"][i], (bs1, bs2))
|
|
tbn, tbf = get_closest_and_furthest_vectors(axis1["base"][i], (tb1, tb2), tba1)
|
|
tsn, tsf = get_closest_and_furthest_vectors(axis1["side"][i], (ts1, ts2))
|
|
|
|
j = 1 if connected_at_end else 0
|
|
if description == "MITRE":
|
|
# Mitre joints are an unofficial convention
|
|
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
|
|
tbf_ = tool.Cad.point_on_edge(tbf, bba1)
|
|
tsf_ = tool.Cad.point_on_edge(tsf, bba1)
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_))
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_))
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
|
|
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
|
|
|
|
if connection1 == connection2:
|
|
if (connected_at_end and angle > 0) or (not connected_at_end and angle < 0):
|
|
pt = bbf.to_2d().to_3d()
|
|
x_axis = bsn - bbf
|
|
y_axis = tbf - bbf
|
|
else:
|
|
pt = bbn.to_2d().to_3d()
|
|
x_axis = bsf - bbn
|
|
y_axis = tbn - bbn
|
|
else:
|
|
if (connected_at_end and angle < 0) or (not connected_at_end and angle > 0):
|
|
pt = bbf.to_2d().to_3d()
|
|
x_axis = bsn - bbf
|
|
y_axis = tbf - bbf
|
|
else:
|
|
pt = bbn.to_2d().to_3d()
|
|
x_axis = bsf - bbn
|
|
y_axis = tbn - bbn
|
|
|
|
if connection1 != "ATEND":
|
|
y_axis *= -1
|
|
|
|
x_axis.normalize()
|
|
y_axis.normalize()
|
|
z_axis = x_axis.cross(y_axis)
|
|
y_axis = z_axis.cross(x_axis)
|
|
|
|
self.clippings.append(
|
|
{
|
|
"type": "IfcBooleanClippingResult",
|
|
"operand_type": "IfcHalfSpaceSolid",
|
|
"matrix": self.create_matrix(pt, x_axis, y_axis, z_axis),
|
|
}
|
|
)
|
|
else:
|
|
# This is the standard L and T joints described by IFC
|
|
if (
|
|
tool.Cad.is_x(abs(angle), (90, 270), tolerance=0.001)
|
|
and not extrusion1["is_sloped"]
|
|
and not extrusion2["is_sloped"]
|
|
):
|
|
if is_relating:
|
|
self.body[j] = tool.Cad.point_on_edge(bbf, bra1).to_2d()
|
|
else:
|
|
self.body[j] = tool.Cad.point_on_edge(bbn, bra1).to_2d()
|
|
return True
|
|
|
|
bsf_ = tool.Cad.point_on_edge(bsf, bba1)
|
|
tbf_ = tool.Cad.point_on_edge(tbf, bba1)
|
|
tsf_ = tool.Cad.point_on_edge(tsf, bba1)
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (bbf, bsf_)).copy()
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tbf_)).copy()
|
|
new_body = tool.Cad.furthest_vector(bba1[i], (new_body, tsf_)).copy()
|
|
self.body[j] = tool.Cad.point_on_edge(new_body, bra1).to_2d()
|
|
|
|
if is_relating:
|
|
pt = bbf.to_2d().to_3d()
|
|
x_axis = bsf - bbf
|
|
y_axis = tbf - bbf
|
|
else:
|
|
pt = bbn.to_2d().to_3d()
|
|
x_axis = bsn - bbn
|
|
y_axis = tbn - bbn
|
|
if connection1 != "ATEND":
|
|
y_axis *= -1
|
|
z_axis = x_axis.cross(y_axis)
|
|
y_axis = z_axis.cross(x_axis)
|
|
|
|
self.clippings.append(
|
|
{
|
|
"type": "IfcBooleanClippingResult",
|
|
"operand_type": "IfcHalfSpaceSolid",
|
|
"matrix": self.create_matrix(pt, x_axis, y_axis, z_axis),
|
|
}
|
|
)
|
|
|
|
return True
|
|
|
|
def clip(self, wall1, slab2):
|
|
"""returns height of the clipped wall, adds clipping plane to `clippings`"""
|
|
element1 = tool.Ifc.get_entity(wall1)
|
|
element2 = tool.Ifc.get_entity(slab2)
|
|
|
|
layers1 = tool.Model.get_material_layer_parameters(element1)
|
|
axis1 = tool.Model.get_wall_axis(wall1, layers1)
|
|
|
|
bases = [axis1["base"][0].to_3d(), axis1["base"][1].to_3d(), axis1["side"][0].to_3d(), axis1["side"][1].to_3d()]
|
|
|
|
extrusion = self.get_extrusion_data(tool.Ifc.get().by_id(wall1.data.BIMMeshProperties.ifc_definition_id))
|
|
wall_dir = wall1.matrix_world.to_quaternion() @ extrusion["direction"]
|
|
|
|
slab_pt = slab2.matrix_world @ Vector((0, 0, 0))
|
|
slab_dir = slab2.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
|
|
|
tops = [mathutils.geometry.intersect_line_plane(b, b + wall_dir, slab_pt, slab_dir) for b in bases]
|
|
top_index = max(range(4), key=lambda i: tops[i].z)
|
|
i_top = tops[top_index]
|
|
i_bottom = bases[top_index]
|
|
|
|
quaternion = slab2.matrix_world.to_quaternion()
|
|
x_axis = quaternion @ Vector((1, 0, 0))
|
|
y_axis = quaternion @ Vector((0, 1, 0))
|
|
z_axis = quaternion @ Vector((0, 0, 1))
|
|
self.clippings.append(
|
|
{
|
|
"type": "IfcBooleanClippingResult",
|
|
"operand_type": "IfcHalfSpaceSolid",
|
|
"matrix": self.create_matrix(i_top, x_axis, y_axis, z_axis),
|
|
}
|
|
)
|
|
|
|
return (i_top - i_bottom).length
|