mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Implement polyline tool for profiles.
This commit is contained in:
@@ -95,6 +95,7 @@ classes = (
|
||||
profile.ChangeCardinalPoint,
|
||||
profile.ChangeProfileDepth,
|
||||
profile.DisableEditingExtrusionAxis,
|
||||
profile.DrawPolylineProfile,
|
||||
profile.EditExtrusionAxis,
|
||||
profile.EnableEditingExtrusionAxis,
|
||||
profile.ExtendProfile,
|
||||
|
||||
@@ -25,7 +25,7 @@ import bmesh
|
||||
import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
from math import sin, cos, tan, radians
|
||||
from math import sin, cos, tan, radians, atan2
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
@@ -981,22 +981,24 @@ class ProductDecorator:
|
||||
def get_profile_preview_data(self, context, relating_type):
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
extrusion_depth = model_props.extrusion_depth
|
||||
cardinal_point = model_props.cardinal_point
|
||||
rot_mat = Quaternion()
|
||||
if relating_type.is_a("IfcBeamType"):
|
||||
y_rot = Quaternion((0.0, 1.0, 0.0), radians(90))
|
||||
z_rot = Quaternion((0.0, 0.0, 1.0), radians(90))
|
||||
rot_mat = y_rot @ z_rot
|
||||
# Get profile data
|
||||
|
||||
polyline_verts = []
|
||||
polyline_data = context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_verts.append(Vector((point.x, point.y, point.z)))
|
||||
polyline_edges = [(i, i+1) for i in range(len(polyline_verts)-1)]
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
@@ -1036,78 +1038,65 @@ class ProductDecorator:
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
|
||||
profile_curve = bpy.data.curves.new("Profile", type='CURVE')
|
||||
profile_curve.dimensions = "2D"
|
||||
profile_curve.splines.new('POLY')
|
||||
profile_curve.splines[0].points.add(len(grouped_verts) - 1)
|
||||
|
||||
for i, point in enumerate(profile_curve.splines[0].points):
|
||||
point.co = Vector((*grouped_verts[i], 0))
|
||||
|
||||
profile_obj = bpy.data.objects.new("Profile", profile_curve)
|
||||
|
||||
preview_curve = bpy.data.curves.new("Polyline", type='CURVE')
|
||||
preview_curve.dimensions = "2D"
|
||||
preview_curve.splines.new('POLY')
|
||||
preview_curve.splines[0].points.add(len(polyline_verts) - 1)
|
||||
for i, point in enumerate(preview_curve.splines[0].points):
|
||||
point.co = Vector((*polyline_verts[i], 0))
|
||||
preview_curve.splines[0].use_smooth = False
|
||||
preview_curve.bevel_mode = "OBJECT"
|
||||
preview_curve.bevel_object = profile_obj
|
||||
|
||||
preview_obj = bpy.data.objects.new("Preview", preview_curve)
|
||||
context.scene.collection.objects.link(preview_obj)
|
||||
bpy.context.view_layer.objects.active = preview_obj
|
||||
selection = preview_obj.select_get()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
preview_obj.select_set(True)
|
||||
bpy.ops.object.convert(target="MESH")
|
||||
preview_obj = bpy.data.objects["Preview"]
|
||||
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in grouped_verts]
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(grouped_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
new_verts = [e for e in new_faces["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
new_faces = bmesh.ops.translate(bm, verts=new_verts, vec=(0.0, 0.0, extrusion_depth))
|
||||
|
||||
new_verts = [bm.verts.new(v.co) for v in preview_obj.data.vertices]
|
||||
index = [[v for v in edge.vertices] for edge in preview_obj.data.edges]
|
||||
new_edges = [bm.edges.new((new_verts[i[0]], new_verts[i[1]])) for i in index]
|
||||
for face in preview_obj.data.polygons:
|
||||
verts = [new_verts[i] for i in face.vertices]
|
||||
bm.faces.new(verts)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Create bounding box
|
||||
verts = bm.verts
|
||||
i = len(verts)
|
||||
|
||||
min_x = min(v.co.x for v in verts)
|
||||
max_x = max(v.co.x for v in verts)
|
||||
min_y = min(v.co.y for v in verts)
|
||||
max_y = max(v.co.y for v in verts)
|
||||
min_z = min(v.co.z for v in verts)
|
||||
max_z = max(v.co.z for v in verts)
|
||||
|
||||
bbox_verts = [
|
||||
(min_x, min_y, min_z),
|
||||
(max_x, min_y, min_z),
|
||||
(max_x, max_y, min_z),
|
||||
(min_x, max_y, min_z),
|
||||
(min_x, min_y, max_z),
|
||||
(max_x, min_y, max_z),
|
||||
(max_x, max_y, max_z),
|
||||
(min_x, max_y, max_z),
|
||||
]
|
||||
|
||||
bbox_edges = [
|
||||
(0 + i, 3 + i),
|
||||
(3 + i, 7 + i),
|
||||
(7 + i, 4 + i),
|
||||
(4 + i, 0 + i),
|
||||
(0 + i, 1 + i),
|
||||
(3 + i, 2 + i),
|
||||
(7 + i, 6 + i),
|
||||
(4 + i, 5 + i),
|
||||
(1 + i, 2 + i),
|
||||
(2 + i, 6 + i),
|
||||
(6 + i, 5 + i),
|
||||
(5 + i, 1 + i),
|
||||
]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
# TODO Angle
|
||||
snap_prop = context.scene.BIMPolylineProperties.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in verts]
|
||||
verts.extend(bbox_verts)
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
data["verts"] = verts
|
||||
data["edges"] = bbox_edges
|
||||
# data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
||||
data["verts"] = [tuple(v.co) for v in bm.verts]
|
||||
data["edges"] = [(edge.verts[0].index, edge.verts[1].index) for edge in bm.edges]
|
||||
data["tris"] = tris
|
||||
|
||||
bpy.data.objects.remove(bpy.data.objects[preview_obj.name], do_unlink=True)
|
||||
bpy.data.objects.remove(bpy.data.objects[profile_obj.name], do_unlink=True)
|
||||
try:
|
||||
bpy.data.curves.remove(profile_obj.data, do_unlink=True)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
bpy.data.curves.remove(preview_obj.data, do_unlink=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
bm.free()
|
||||
|
||||
return data
|
||||
|
||||
def draw_product_preview(self, context):
|
||||
@@ -1152,6 +1141,7 @@ class ProductDecorator:
|
||||
)
|
||||
|
||||
# Profile type products
|
||||
self.line_shader.uniform_float("lineWidth", 0.5)
|
||||
product_preview_data = self.get_profile_preview_data(context, self.relating_type)
|
||||
if product_preview_data:
|
||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||
|
||||
@@ -32,11 +32,13 @@ import bonsai.core.type
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.material
|
||||
import bonsai.core.root
|
||||
from math import pi, degrees, inf
|
||||
from math import pi, degrees, inf, atan2
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bonsai.bim.module.geometry.helper import Helper
|
||||
from bonsai.bim.module.model.wall import DumbWallRecalculator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator, PolylineDecorator, ProductDecorator
|
||||
from bonsai.bim.module.model.polyline import PolylineOperator
|
||||
from typing import Union, Any
|
||||
|
||||
|
||||
class DumbProfileGenerator:
|
||||
@@ -44,7 +46,7 @@ class DumbProfileGenerator:
|
||||
self.relating_type = relating_type
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
|
||||
def generate(self):
|
||||
def generate(self, insertion_type="CURSOR"):
|
||||
self.file = tool.Ifc.get()
|
||||
self.unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
material = ifcopenshell.util.element.get_material(self.relating_type)
|
||||
@@ -67,7 +69,28 @@ class DumbProfileGenerator:
|
||||
self.rotation = 0
|
||||
self.location = Vector((0, 0, 0))
|
||||
self.cardinal_point = int(bpy.context.scene.BIMModelProperties.cardinal_point)
|
||||
return self.derive_from_cursor()
|
||||
if insertion_type == "POLYLINE":
|
||||
return self.derive_from_polyline()
|
||||
elif insertion_type == "CURSOR":
|
||||
return self.derive_from_cursor()
|
||||
|
||||
def derive_from_polyline(self) -> tuple[list[Union[dict[str, Any], None]], bool]:
|
||||
polyline_data = bpy.context.scene.BIMPolylineProperties.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
is_polyline_closed = False
|
||||
if len(polyline_points) > 3:
|
||||
first_vec = Vector((polyline_points[0].x, polyline_points[0].y, polyline_points[0].z))
|
||||
last_vec = Vector((polyline_points[-1].x, polyline_points[-1].y, polyline_points[-1].z))
|
||||
if first_vec == last_vec:
|
||||
is_polyline_closed = True
|
||||
|
||||
profiles = []
|
||||
for i in range(len(polyline_points) - 1):
|
||||
vec1 = Vector((polyline_points[i].x, polyline_points[i].y, polyline_points[i].z))
|
||||
vec2 = Vector((polyline_points[i + 1].x, polyline_points[i + 1].y, polyline_points[i + 1].z))
|
||||
coords = (vec1, vec2)
|
||||
profiles.append(self.create_profile_from_2_points(coords))
|
||||
return profiles, is_polyline_closed
|
||||
|
||||
def derive_from_cursor(self):
|
||||
self.location = bpy.context.scene.cursor.location
|
||||
@@ -86,6 +109,9 @@ class DumbProfileGenerator:
|
||||
"IfcFlowSegmentType"
|
||||
):
|
||||
matrix_world = Matrix.Rotation(pi / 2, 4, "Z") @ Matrix.Rotation(pi / 2, 4, "X") @ matrix_world
|
||||
|
||||
matrix_world = Matrix.Rotation(self.rotation, 4, "Z") @ matrix_world
|
||||
|
||||
matrix_world.translation = self.location
|
||||
if self.container_obj:
|
||||
matrix_world.translation.z = self.container_obj.location.z
|
||||
@@ -146,6 +172,25 @@ class DumbProfileGenerator:
|
||||
|
||||
return obj
|
||||
|
||||
def create_profile_from_2_points(self, coords, should_round=False) -> Union[dict[str, Any], None]:
|
||||
direction = coords[1] - coords[0]
|
||||
length = direction.length
|
||||
if round(length, 4) < 0.1:
|
||||
return
|
||||
data = {"coords": coords}
|
||||
|
||||
self.depth = length
|
||||
self.rotation = atan2(direction[1], direction[0])
|
||||
if should_round:
|
||||
# Round to nearest 50mm (yes, metric for now)
|
||||
self.length = 0.05 * round(length / 0.05)
|
||||
# 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_profile()
|
||||
return data
|
||||
|
||||
|
||||
class DumbProfileRegenerator:
|
||||
def regenerate_from_profile_def(self, profile):
|
||||
@@ -1064,3 +1109,115 @@ class EditExtrusionAxis(bpy.types.Operator, tool.Ifc.Operator):
|
||||
joiner = DumbProfileJoiner()
|
||||
joiner.set_depth(obj, depth)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DrawPolylineProfile(bpy.types.Operator, PolylineOperator):
|
||||
bl_idname = "bim.draw_polyline_profile"
|
||||
bl_label = "Draw Polyline Profile"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data.type == "VIEW_3D"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.relating_type = None
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
relating_type_id = props.relating_type_id
|
||||
if relating_type_id:
|
||||
self.relating_type = tool.Ifc.get().by_id(int(relating_type_id))
|
||||
|
||||
def create_profiles_from_polyline(self, context: bpy.types.Context) -> Union[set[str], None]:
|
||||
if not self.relating_type:
|
||||
return {"FINISHED"}
|
||||
|
||||
model_props = context.scene.BIMModelProperties
|
||||
direction_sense = model_props.direction_sense
|
||||
offset = model_props.offset
|
||||
|
||||
profiles, is_polyline_closed = DumbProfileGenerator(self.relating_type).generate("POLYLINE")
|
||||
if profiles:
|
||||
if is_polyline_closed:
|
||||
for profile1, profile2 in zip(profiles, profiles[1:] + [profiles[0]]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
else:
|
||||
for profile1, profile2 in zip(profiles[:-1], profiles[1:]):
|
||||
DumbProfileJoiner().join_V(profile2["obj"], profile1["obj"])
|
||||
|
||||
|
||||
def modal(self, context, event):
|
||||
if not self.relating_type:
|
||||
self.report({"WARNING"}, "You need to select a profile type.")
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
PolylineDecorator.update(event, self.tool_state, self.input_ui, self.snapping_points[0])
|
||||
tool.Blender.update_viewport()
|
||||
|
||||
self.handle_lock_axis(context, event) # Must come before "PASS_TRHOUGH"
|
||||
|
||||
if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}:
|
||||
self.handle_mouse_move(context, event)
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
# Wall axis settings
|
||||
if event.value == "RELEASE" and event.type == "F":
|
||||
direction_sense = context.scene.BIMModelProperties.direction_sense
|
||||
context.scene.BIMModelProperties.direction_sense = (
|
||||
"NEGATIVE" if direction_sense == "POSITIVE" else "POSITIVE"
|
||||
)
|
||||
|
||||
if event.value == "RELEASE" and event.type == "O":
|
||||
offset_type = context.scene.BIMModelProperties.offset_type
|
||||
items = ["EXTERIOR", "CENTER", "INTERIOR"]
|
||||
index = items.index(offset_type)
|
||||
size = len(items)
|
||||
context.scene.BIMModelProperties.offset_type = items[((index + 1) % size)]
|
||||
|
||||
|
||||
props = bpy.context.scene.BIMModelProperties
|
||||
wall_config = f"""Direction: {props.direction_sense}
|
||||
Offset Type: {props.offset_type}
|
||||
Offset Value: {props.offset}
|
||||
"""
|
||||
self.handle_instructions(context, wall_config)
|
||||
|
||||
self.handle_mouse_move(context, event, should_round=True)
|
||||
|
||||
self.choose_axis(event)
|
||||
|
||||
self.handle_snap_selection(context, event)
|
||||
|
||||
if (
|
||||
not self.tool_state.is_input_on
|
||||
and event.value == "RELEASE"
|
||||
and event.type in {"RET", "NUMPAD_ENTER", "RIGHTMOUSE"}
|
||||
):
|
||||
self.create_profiles_from_polyline(context)
|
||||
context.workspace.status_text_set(text=None)
|
||||
ProductDecorator.uninstall()
|
||||
PolylineDecorator.uninstall()
|
||||
tool.Polyline.clear_polyline()
|
||||
tool.Blender.update_viewport()
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
return cancel
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
super().invoke(context, event)
|
||||
ProductDecorator.install(context)
|
||||
self.tool_state.use_default_container = True
|
||||
self.tool_state.plane_method = "XY"
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
@@ -991,6 +991,10 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
|
||||
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "LAYER3"
|
||||
):
|
||||
bpy.ops.bim.draw_polyline_slab("INVOKE_DEFAULT")
|
||||
elif (
|
||||
relating_type_id and tool.Model.get_usage_type(tool.Ifc.get().by_id(int(relating_type_id))) == "PROFILE"
|
||||
):
|
||||
bpy.ops.bim.draw_polyline_profile("INVOKE_DEFAULT")
|
||||
else:
|
||||
bpy.ops.bim.add_occurrence("INVOKE_DEFAULT")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user