Implement polyline tool for profiles.

This commit is contained in:
Bruno Perdigão
2025-01-08 20:56:21 -03:00
parent 40dcfd450f
commit 8b1d4ca5d4
4 changed files with 229 additions and 77 deletions
@@ -95,6 +95,7 @@ classes = (
profile.ChangeCardinalPoint,
profile.ChangeProfileDepth,
profile.DisableEditingExtrusionAxis,
profile.DrawPolylineProfile,
profile.EditExtrusionAxis,
profile.EnableEditingExtrusionAxis,
profile.ExtendProfile,
+63 -73
View File
@@ -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"])
+161 -4
View File
@@ -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")