mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
New feature to extend columns, beams, and members to a target
This commit is contained in:
@@ -12,6 +12,7 @@ classes = (
|
||||
wall.SplitWall,
|
||||
wall.AddWallOpening,
|
||||
slab.AddSlabOpening,
|
||||
profile.ExtendProfile,
|
||||
prop.BIMModelProperties,
|
||||
ui.BIM_PT_authoring,
|
||||
ui.BIM_PT_authoring_architectural,
|
||||
|
||||
@@ -8,7 +8,7 @@ import ifcopenshell.util.element
|
||||
import mathutils.geometry
|
||||
import blenderbim.bim.handler
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from math import pi, degrees
|
||||
from math import pi, degrees, inf
|
||||
from mathutils import Vector, Matrix
|
||||
from ifcopenshell.api.pset.data import Data as PsetData
|
||||
from ifcopenshell.api.material.data import Data as MaterialData
|
||||
@@ -232,3 +232,136 @@ class DumbProfileRegenerator:
|
||||
if not obj or obj not in IfcStore.edited_objs:
|
||||
return
|
||||
bpy.ops.bim.update_representation(obj=obj.name)
|
||||
|
||||
|
||||
class ExtendProfile(bpy.types.Operator):
|
||||
bl_idname = "bim.extend_profile"
|
||||
bl_label = "Extend Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
selected_objs = context.selected_objects
|
||||
for obj in selected_objs:
|
||||
bpy.ops.bim.dynamically_void_product(obj=obj.name)
|
||||
if len(selected_objs) == 0:
|
||||
return {"FINISHED"}
|
||||
if not context.active_object:
|
||||
return {"FINISHED"}
|
||||
if len(selected_objs) == 1:
|
||||
DumbProfileExtender(context.active_object, target_coordinate=context.scene.cursor.location).extend()
|
||||
IfcStore.edited_objs.add(context.active_object)
|
||||
return {"FINISHED"}
|
||||
if len(selected_objs) < 2:
|
||||
return {"FINISHED"}
|
||||
for obj in selected_objs:
|
||||
if obj == context.active_object:
|
||||
continue
|
||||
DumbProfileExtender(obj, context.active_object).extend()
|
||||
IfcStore.edited_objs.add(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DumbProfileExtender:
|
||||
# A profile is a prismatic extrusion along its local Z axis.
|
||||
def __init__(self, profile, target=None, target_coordinate=None):
|
||||
self.profile = profile
|
||||
self.target = target
|
||||
self.target_coordinate = target_coordinate
|
||||
|
||||
# An extension of a profile is determined by casting a ray from the cardinal
|
||||
# point of either extreme along the profiles local Z axis to a target
|
||||
# object. The nearest result from this raycast will be the target extension
|
||||
# point.
|
||||
def extend(self):
|
||||
extension_data = self.get_closest_extension_point()
|
||||
if not extension_data:
|
||||
return
|
||||
if extension_data["end"] == "bottom":
|
||||
self.profile.matrix_world.translation = extension_data["contact"]
|
||||
if extension_data["direction"] == "up":
|
||||
self.shift_top_faces(-extension_data["distance"])
|
||||
elif extension_data["direction"] == "down":
|
||||
self.shift_top_faces(extension_data["distance"])
|
||||
if extension_data["end"] == "top":
|
||||
if extension_data["direction"] == "up":
|
||||
self.shift_top_faces(extension_data["distance"])
|
||||
elif extension_data["direction"] == "down":
|
||||
self.shift_top_faces(-extension_data["distance"])
|
||||
|
||||
def shift_top_faces(self, z):
|
||||
vertices = []
|
||||
for f in self.get_profile_top_faces():
|
||||
vertices.extend(f.vertices)
|
||||
for v in set(vertices):
|
||||
self.profile.data.vertices[v].co.z += z
|
||||
|
||||
# An top face is defined as having at least one vertex on the max
|
||||
# Z-axis, and a non-insignificant Z component of its face normal
|
||||
def get_profile_top_faces(self):
|
||||
faces = []
|
||||
max_z = max([v[2] for v in self.profile.bound_box])
|
||||
for f in self.profile.data.polygons:
|
||||
if abs(f.normal.z) < 0.1:
|
||||
continue
|
||||
for v in f.vertices:
|
||||
if self.profile.data.vertices[v].co.z == max_z:
|
||||
faces.append(f)
|
||||
break
|
||||
return faces
|
||||
|
||||
def get_closest_extension_point(self):
|
||||
top = self.profile.matrix_world @ Vector((0, 0, self.profile.dimensions[2]))
|
||||
bottom = self.profile.matrix_world.translation
|
||||
up = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
down = self.profile.matrix_world.to_quaternion() @ Vector((0, 0, -1))
|
||||
|
||||
if self.target:
|
||||
return self.get_closest_extension_point_from_target_obj(top, bottom, up, down)
|
||||
elif self.target_coordinate:
|
||||
return self.get_closest_extension_point_from_target_coordinate(top, bottom, up, down)
|
||||
|
||||
def get_closest_extension_point_from_target_coordinate(self, top, bottom, up, down):
|
||||
point = mathutils.geometry.intersect_line_plane(
|
||||
bottom, top, self.target_coordinate, self.profile.matrix_world.to_quaternion() @ Vector((0, 0, 1))
|
||||
)
|
||||
if not point:
|
||||
return
|
||||
top_distance = (point - top).length
|
||||
bottom_distance = (point - bottom).length
|
||||
if top_distance < bottom_distance:
|
||||
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, top, bottom)
|
||||
if signed_distance > 0:
|
||||
return {"contact": point, "distance": top_distance, "end": "top", "direction": "down"}
|
||||
else:
|
||||
return {"contact": point, "distance": top_distance, "end": "top", "direction": "up"}
|
||||
else:
|
||||
intersection_point, signed_distance = mathutils.geometry.intersect_point_line(point, bottom, top)
|
||||
if signed_distance > 0:
|
||||
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "up"}
|
||||
else:
|
||||
return {"contact": point, "distance": bottom_distance, "end": "bottom", "direction": "down"}
|
||||
|
||||
def get_closest_extension_point_from_target_obj(self, top, bottom, up, down):
|
||||
t_top = self.target.matrix_world.inverted() @ top
|
||||
t_bottom = self.target.matrix_world.inverted() @ bottom
|
||||
t_up = self.target.matrix_world.inverted().to_quaternion() @ up
|
||||
t_down = self.target.matrix_world.inverted().to_quaternion() @ down
|
||||
|
||||
results = []
|
||||
results.append((self.target.ray_cast(t_top, t_up, distance=50), top, "top", "up"))
|
||||
results.append((self.target.ray_cast(t_top, t_down, distance=50), top, "top", "down"))
|
||||
results.append((self.target.ray_cast(t_bottom, t_up, distance=50), bottom, "bottom", "up"))
|
||||
results.append((self.target.ray_cast(t_bottom, t_down, distance=50), bottom, "bottom", "down"))
|
||||
|
||||
min_distance = float(inf)
|
||||
final = None
|
||||
|
||||
for result in results:
|
||||
if result[0][0]:
|
||||
contact = self.target.matrix_world @ result[0][1]
|
||||
distance = (contact - result[1]).length
|
||||
if distance < min_distance:
|
||||
min_distance = distance
|
||||
final = {"contact": contact, "distance": distance, "end": result[2], "direction": result[3]}
|
||||
|
||||
return final
|
||||
|
||||
@@ -609,6 +609,8 @@ class DumbWallJoiner:
|
||||
min_x = min([v[0] for v in wall.bound_box])
|
||||
max_x = max([v[0] for v in wall.bound_box])
|
||||
for f in wall.data.polygons:
|
||||
if abs(f.normal.x) < 0.1:
|
||||
continue
|
||||
end_face_index = self.get_wall_face_end(wall, f, min_x, max_x)
|
||||
if end_face_index == 1:
|
||||
min_faces.append(f)
|
||||
@@ -619,9 +621,9 @@ class DumbWallJoiner:
|
||||
# 1 is the leftmost (minimum local X axis) end, and 2 is the rightmost end
|
||||
def get_wall_face_end(self, wall, face, min_x, max_x):
|
||||
for v in face.vertices:
|
||||
if wall.data.vertices[v].co.x == min_x and abs(face.normal.x) > 0.1:
|
||||
if wall.data.vertices[v].co.x == min_x:
|
||||
return 1
|
||||
if wall.data.vertices[v].co.x == max_x and abs(face.normal.x) > 0.1:
|
||||
if wall.data.vertices[v].co.x == max_x:
|
||||
return 2
|
||||
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class BimTool(WorkSpaceTool):
|
||||
row.label(text="Add Type Instance", icon="EVENT_A")
|
||||
|
||||
if props.ifc_class == "IfcWallType":
|
||||
row = layout.row(align=True)
|
||||
row = layout.row()
|
||||
row.label(text="Join")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -56,7 +56,7 @@ class BimTool(WorkSpaceTool):
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Mitre", icon="EVENT_Y")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row = layout.row()
|
||||
row.label(text="Wall Tools")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
@@ -73,6 +73,13 @@ class BimTool(WorkSpaceTool):
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Opening", icon="EVENT_O")
|
||||
|
||||
if props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
|
||||
row = layout.row()
|
||||
row.label(text="Join")
|
||||
row = layout.row(align=True)
|
||||
row.label(text="", icon="EVENT_SHIFT")
|
||||
row.label(text="Extend", icon="EVENT_E")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Align")
|
||||
row = layout.row(align=True)
|
||||
@@ -119,6 +126,8 @@ class Hotkey(bpy.types.Operator):
|
||||
def hotkey_S_E(self):
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
bpy.ops.bim.join_wall(join_type="T")
|
||||
elif self.props.ifc_class in ["IfcColumnType", "IfcBeamType", "IfcMemberType"]:
|
||||
bpy.ops.bim.extend_profile()
|
||||
|
||||
def hotkey_S_V(self):
|
||||
if self.props.ifc_class == "IfcWallType":
|
||||
|
||||
Reference in New Issue
Block a user