mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
See #7566. Fix unusable lag due to dense meshes with product preview
Previously, when the add occurrence modal operator was executed, on every modal loop (i.e. every mouse movement) it would fetch the mesh geometry to be previewed, store the verts / edges / faces in mesh collections, then the decorator would fetch that geometry, the clear the collections, in a loop. I've removed the Blender collections. Instead the same strategy is used as in ItemDecorator i.e. the mesh and verts are fetched once during decorator installation, then on each draw call only a single vertex loop to multiply by the transformation matrix for snapping and mouse position. You can test with the LOD400 model in #7566. On my machine it would cause lag on anything with >500 faces. Now it seems to work without lag on a 26k polygon mesh.
This commit is contained in:
@@ -75,27 +75,8 @@ class ItemDecorator:
|
||||
special_edges = []
|
||||
|
||||
if (total_triangles := len(obj.data.loop_triangles)) > 0:
|
||||
# TODO: this is a far too small threshold.
|
||||
# This is just a stopgap optimisation until other slowdowns are solved.
|
||||
if total_triangles > 500:
|
||||
vert_map = {}
|
||||
i = 0
|
||||
verts = []
|
||||
tris = []
|
||||
for tri in obj.data.loop_triangles[:500]:
|
||||
new_tri = []
|
||||
for vert in tri.vertices:
|
||||
if vert in vert_map:
|
||||
new_tri.append(vert_map[vert])
|
||||
else:
|
||||
vert_map[vert] = i
|
||||
new_tri.append(i)
|
||||
i += 1
|
||||
verts.append(tuple(obj.matrix_world @ obj.data.vertices[vert].co))
|
||||
tris.append(new_tri)
|
||||
else:
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
verts = [tuple(obj.matrix_world @ v.co) for v in obj.data.vertices]
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
|
||||
i = len(verts)
|
||||
matrix_world = obj.matrix_world
|
||||
|
||||
@@ -140,7 +140,6 @@ classes = (
|
||||
prop.BIMRailingProperties,
|
||||
prop.BIMRoofProperties,
|
||||
prop.BIMPolylineProperties,
|
||||
prop.BIMProductPreviewProperties,
|
||||
prop.BIMExternalParametricGeometryProperties,
|
||||
ui.BIM_PT_array,
|
||||
ui.BIM_PT_stair,
|
||||
@@ -263,7 +262,6 @@ def register():
|
||||
|
||||
bpy.types.Scene.BIMModelProperties = bpy.props.PointerProperty(type=prop.BIMModelProperties)
|
||||
bpy.types.Scene.BIMPolylineProperties = bpy.props.PointerProperty(type=prop.BIMPolylineProperties)
|
||||
bpy.types.Scene.BIMProductPreviewProperties = bpy.props.PointerProperty(type=prop.BIMProductPreviewProperties)
|
||||
bpy.types.Object.BIMArrayProperties = bpy.props.PointerProperty(type=prop.BIMArrayProperties)
|
||||
bpy.types.Object.BIMStairProperties = bpy.props.PointerProperty(type=prop.BIMStairProperties)
|
||||
bpy.types.Object.BIMSverchokProperties = bpy.props.PointerProperty(type=prop.BIMSverchokProperties)
|
||||
@@ -288,7 +286,6 @@ def unregister():
|
||||
|
||||
del bpy.types.Scene.BIMModelProperties
|
||||
del bpy.types.Scene.BIMPolylineProperties
|
||||
del bpy.types.Scene.BIMProductPreviewProperties
|
||||
del bpy.types.Object.BIMArrayProperties
|
||||
del bpy.types.Object.BIMStairProperties
|
||||
del bpy.types.Object.BIMSverchokProperties
|
||||
|
||||
@@ -26,10 +26,10 @@ import ifcopenshell
|
||||
import bonsai.tool as tool
|
||||
import math
|
||||
import mathutils
|
||||
from math import sin, cos, radians
|
||||
from math import sin, cos, tan, radians
|
||||
from bpy.types import SpaceView3D
|
||||
from bpy_extras import view3d_utils
|
||||
from mathutils import Vector, Matrix
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from gpu_extras.presets import draw_circle_2d
|
||||
from typing import Union
|
||||
@@ -879,13 +879,43 @@ class PolylineDecorator:
|
||||
class ProductDecorator:
|
||||
is_installed = False
|
||||
handlers = []
|
||||
preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"]
|
||||
relating_type = None
|
||||
obj_data: dict[str, list] = {}
|
||||
obj_matrix_i = None
|
||||
|
||||
@classmethod
|
||||
def install(cls, context):
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
|
||||
if cls.is_installed:
|
||||
cls.uninstall()
|
||||
|
||||
props = tool.Model.get_model_props()
|
||||
|
||||
handler = cls()
|
||||
if (
|
||||
(props.relating_type_id)
|
||||
and (relating_type := tool.Ifc.get().by_id(int(props.relating_type_id)))
|
||||
and (relating_type_obj := tool.Ifc.get_object(relating_type))
|
||||
):
|
||||
handler.relating_type = relating_type
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE":
|
||||
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
|
||||
handler.preview_mode = "PROFILE_VERTICAL"
|
||||
else:
|
||||
handler.preview_mode = "PROFILE_HORIZONTAL"
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
handler.preview_mode = "LAYER2"
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
|
||||
handler.preview_mode = "LAYER3"
|
||||
else:
|
||||
handler.preview_mode = "GENERIC"
|
||||
if relating_type_obj.data:
|
||||
handler.obj_data = ItemDecorator.get_obj_data(relating_type_obj)
|
||||
handler.obj_data["raw_verts"] = [Vector(v) for v in handler.obj_data["verts"]]
|
||||
handler.obj_matrix_i = relating_type_obj.matrix_world.inverted()
|
||||
|
||||
cls.handlers.append(
|
||||
SpaceView3D.draw_handler_add(handler.draw_product_preview, (context,), "WINDOW", "POST_VIEW")
|
||||
)
|
||||
@@ -893,10 +923,6 @@ class ProductDecorator:
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls):
|
||||
props = tool.Model.get_product_preview_props() # updated by model/polyline.py
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
for handler in cls.handlers:
|
||||
try:
|
||||
SpaceView3D.draw_handler_remove(handler, "WINDOW")
|
||||
@@ -912,14 +938,6 @@ class ProductDecorator:
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
def get_product_preview_data(self, context) -> dict[str, Any]:
|
||||
props = tool.Model.get_product_preview_props()
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = [(*v.value_3d,) for v in props.verts]
|
||||
data["edges"] = [(int(e.value_2d[0]), int(e.value_2d[1])) for e in props.edges]
|
||||
data["tris"] = [(int(t.value_3d[0]), int(t.value_3d[1]), int(t.value_3d[2])) for t in props.tris]
|
||||
return data
|
||||
|
||||
def draw_product_preview(self, context):
|
||||
def transparent_color(color, alpha=0.1):
|
||||
color = [i for i in color]
|
||||
@@ -945,12 +963,549 @@ class ProductDecorator:
|
||||
else:
|
||||
return
|
||||
|
||||
product_preview_data = self.get_product_preview_data(context)
|
||||
if product_preview_data:
|
||||
self.draw_batch("LINES", product_preview_data["verts"], decorator_color, product_preview_data["edges"])
|
||||
self.draw_batch(
|
||||
"TRIS", product_preview_data["verts"], transparent_color(decorator_color), product_preview_data["tris"]
|
||||
if self.preview_mode == "LAYER2":
|
||||
data = self.get_wall_preview_data()
|
||||
elif self.preview_mode == "LAYER3":
|
||||
data = self.get_slab_preview_data()
|
||||
elif self.preview_mode == "PROFILE_VERTICAL":
|
||||
data = self.get_vertical_profile_preview_data()
|
||||
elif self.preview_mode == "PROFILE_HORIZONTAL":
|
||||
data = self.get_horizontal_profile_preview_data()
|
||||
elif self.preview_mode == "GENERIC":
|
||||
data = self.get_generic_preview_data()
|
||||
if data:
|
||||
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
|
||||
self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"])
|
||||
|
||||
def get_wall_preview_data(self):
|
||||
relating_type = self.relating_type
|
||||
# Get properties from object type
|
||||
model_props = tool.Model.get_model_props()
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_vertical
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * unit_scale
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
if x_angle > radians(90) or x_angle < radians(-90):
|
||||
height *= -1
|
||||
angle_distance = height * tan(x_angle)
|
||||
thickness *= 1 / cos(x_angle)
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(data["verts"])):
|
||||
points.append(Vector(data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
data["edges"].extend(edges)
|
||||
data["tris"].extend(tris)
|
||||
|
||||
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||
return data
|
||||
|
||||
def get_slab_preview_data(self):
|
||||
relating_type = self.relating_type
|
||||
model_props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"] * abs(1 / cos(x_angle))
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_horizontal
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 3:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
if x_angle:
|
||||
# Get vertices relative to the first polyline point as origin
|
||||
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
|
||||
# Make the transformation relative to the x_angle
|
||||
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
|
||||
# Convert back to world origin
|
||||
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
|
||||
if offset != 0:
|
||||
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
|
||||
is_closed = True
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
bm = tool.Model.create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
bm.verts.ensure_lookup_table()
|
||||
if x_angle:
|
||||
rot_mat = Matrix.Rotation(x_angle, 3, "X")
|
||||
if abs(x_angle) > (pi / 2):
|
||||
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
|
||||
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + 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, thickness))
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
return data
|
||||
|
||||
def get_vertical_profile_preview_data(self) -> dict[str, Any]:
|
||||
relating_type = self.relating_type
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
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
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
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()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||
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))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
min_z = min(v.co.z for v in bm.verts)
|
||||
max_z = max(v.co.z for v in bm.verts)
|
||||
# Add axis verts
|
||||
verts.append(tuple(mouse_point))
|
||||
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
def get_horizontal_profile_preview_data(self) -> dict[str, Any]:
|
||||
relating_type = self.relating_type
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
cardinal_point = model_props.cardinal_point
|
||||
|
||||
polyline_verts = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.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)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = []
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
|
||||
grouped_verts = [(v) for v in grouped_verts]
|
||||
|
||||
all_bm = bmesh.new()
|
||||
for i in range(len(polyline_verts) - 1):
|
||||
mesh = bpy.data.meshes.new("TempMesh")
|
||||
# Create the initial mesh from the profile verts
|
||||
bm = tool.Model.create_bmesh_from_vertices(grouped_verts, is_closed=True)
|
||||
bm.verts.ensure_lookup_table()
|
||||
# Creates the clipping plane formed by two segments.
|
||||
# The first one is for the profile start, based on the current and previous segment of the polyline.
|
||||
# The second is for the profile end, based on the current and the next segment.
|
||||
if i == 0:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_start = d
|
||||
else:
|
||||
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
|
||||
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
|
||||
clip_start = (d1 - d2).normalized()
|
||||
|
||||
if i == len(polyline_verts) - 2:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_end = d
|
||||
else:
|
||||
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
|
||||
clip_end = (d1 - d2).normalized()
|
||||
|
||||
# Rotates the profile face to the right direction
|
||||
direction = polyline_verts[i + 1] - polyline_verts[i]
|
||||
position = polyline_verts[i]
|
||||
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
|
||||
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
|
||||
|
||||
# Extrude and move the new face
|
||||
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
|
||||
# Apply the cutting planes
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i],
|
||||
plane_no=clip_start,
|
||||
clear_inner=True,
|
||||
)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i + 1],
|
||||
plane_no=clip_end,
|
||||
clear_outer=True,
|
||||
)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
all_bm.from_mesh(mesh)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
|
||||
|
||||
# It's necessary to add the mesh to an object to get the expected result.
|
||||
mesh = bpy.data.meshes.new("TempMesh2")
|
||||
all_bm.to_mesh(mesh)
|
||||
all_bm.free()
|
||||
obj = bpy.data.objects.new("TempObj", mesh)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
def get_generic_preview_data(self):
|
||||
if not (data := self.obj_data):
|
||||
return
|
||||
relating_type = self.relating_type
|
||||
model_props = tool.Model.get_model_props()
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
default_container_elevation = tool.Root.get_default_container_elevation()
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Matrix()
|
||||
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
|
||||
layers = tool.Model.get_material_layer_parameters(snap_element)
|
||||
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
|
||||
axis_base = axes["base"]
|
||||
axis_side = axes["side"]
|
||||
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
|
||||
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
|
||||
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
|
||||
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
|
||||
rot_mat = snap_obj.matrix_world
|
||||
else:
|
||||
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
|
||||
rot_mat = (
|
||||
(snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180)))
|
||||
.to_matrix()
|
||||
.to_4x4()
|
||||
)
|
||||
|
||||
mouse_point.z = snap_obj.matrix_world.translation.z
|
||||
|
||||
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
mouse_point.z = container_obj.location.z
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
|
||||
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
|
||||
if not subcontexts:
|
||||
subcontexts = [("Model", "Body", "MODEL_VIEW")]
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj_type)
|
||||
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
|
||||
for subcontext in subcontexts:
|
||||
if subcontext == active_context_params:
|
||||
break
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj_type,
|
||||
representation,
|
||||
)
|
||||
context.view_layer.update()
|
||||
break
|
||||
|
||||
translate_mouse = Matrix.Translation(mouse_point)
|
||||
translate_rl = Matrix.Translation((0.0, 0.0, rl))
|
||||
combined_m = translate_mouse @ rot_mat @ translate_rl @ self.obj_matrix_i
|
||||
data["verts"] = [tuple(combined_m @ v) for v in data["raw_verts"]]
|
||||
return data
|
||||
|
||||
|
||||
class WallAxisDecorator:
|
||||
|
||||
@@ -35,565 +35,12 @@ import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
import bonsai.core.model as core
|
||||
import bonsai.tool as tool
|
||||
from math import pi, sin, cos, degrees, tan, radians
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
from bonsai.bim.module.model.opening import FilledOpeningGenerator
|
||||
from mathutils import Vector
|
||||
from bonsai.bim.module.model.decorator import PolylineDecorator
|
||||
from bonsai.bim.module.geometry.decorator import ItemDecorator
|
||||
from typing import Optional, Union, Literal, Any
|
||||
from lark import Lark, Transformer
|
||||
|
||||
|
||||
def create_bmesh_from_vertices(vertices, is_closed=False):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
|
||||
def get_wall_preview_data(context, relating_type):
|
||||
# Get properties from object type
|
||||
model_props = tool.Model.get_model_props()
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"]
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_vertical
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * unit_scale
|
||||
|
||||
height = float(model_props.extrusion_depth)
|
||||
rl = float(model_props.rl1)
|
||||
x_angle = float(model_props.x_angle)
|
||||
if x_angle > radians(90) or x_angle < radians(-90):
|
||||
height *= -1
|
||||
angle_distance = height * tan(x_angle)
|
||||
thickness *= 1 / cos(x_angle)
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 2:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
|
||||
is_closed = False
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
is_closed = True
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
|
||||
bm_base = create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
base_vertices = tool.Cad.offset_edges(bm_base, offset)
|
||||
offset_base_verts = tool.Cad.offset_edges(bm_base, thickness + offset)
|
||||
top_vertices = tool.Cad.offset_edges(bm_base, angle_distance + offset)
|
||||
offset_top_verts = tool.Cad.offset_edges(bm_base, angle_distance + thickness + offset)
|
||||
if is_closed:
|
||||
base_vertices.append(base_vertices[0])
|
||||
offset_base_verts.append(offset_base_verts[0])
|
||||
top_vertices.append(top_vertices[0])
|
||||
offset_top_verts.append(offset_top_verts[0])
|
||||
|
||||
if offset_base_verts is not None:
|
||||
for v in base_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in offset_base_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl))
|
||||
|
||||
for v in top_vertices:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
for v in offset_top_verts[::-1]:
|
||||
data["verts"].append((v.co.x, v.co.y, v.co.z + rl + height))
|
||||
|
||||
bm_base.free()
|
||||
|
||||
# Edges and Tris
|
||||
points = []
|
||||
side_edges_1 = []
|
||||
side_edges_2 = []
|
||||
base_edges = []
|
||||
|
||||
for i in range(len(data["verts"])):
|
||||
points.append(Vector(data["verts"][i]))
|
||||
|
||||
n = len(points) // 2
|
||||
bottom_side_1 = [[i, (i + 1) % (n)] for i in range((n - 1) // 2)]
|
||||
bottom_side_2 = [[i, (i + 1) % (n)] for i in range(n // 2, n - 1)]
|
||||
bottom_connections = [[i, n - i - 1] for i in range(n // 2)]
|
||||
bottom_loop = bottom_connections + bottom_side_1 + bottom_side_2
|
||||
side_edges_1.extend(bottom_side_1)
|
||||
side_edges_2.extend(bottom_side_2)
|
||||
base_edges.extend(bottom_loop)
|
||||
|
||||
upper_side_1 = [[i + n for i in edges] for edges in bottom_side_1]
|
||||
upper_side_2 = [[i + n for i in edges] for edges in bottom_side_2]
|
||||
upper_loop = [[i + n for i in edges] for edges in bottom_loop]
|
||||
side_edges_1.extend(upper_side_1)
|
||||
side_edges_2.extend(upper_side_2)
|
||||
base_edges.extend(upper_loop)
|
||||
|
||||
loops = [side_edges_1, side_edges_2, base_edges]
|
||||
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
for i, group in enumerate(loops):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in points]
|
||||
new_edges = [bm.edges.new((new_verts[e[0]], new_verts[e[1]])) for e in group]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
|
||||
if i == 2:
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.bridge_loops(bm, edges=bm.edges, use_pairs=True, use_cyclic=True)
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[l.vert.index for l in loop] for loop in bm.calc_loop_triangles()]
|
||||
data["edges"].extend(edges)
|
||||
data["tris"].extend(tris)
|
||||
|
||||
data["edges"] = list(set(tuple(e) for e in data["edges"]))
|
||||
data["tris"] = list(set(tuple(t) for t in data["tris"]))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_slab_preview_data(context, relating_type):
|
||||
model_props = tool.Model.get_model_props()
|
||||
x_angle = 0 if tool.Cad.is_x(model_props.x_angle, 0, tolerance=0.001) else model_props.x_angle
|
||||
direction_sense = model_props.direction_sense
|
||||
direction = 1
|
||||
if direction_sense == "NEGATIVE":
|
||||
direction = -1
|
||||
|
||||
layers = tool.Model.get_material_layer_parameters(relating_type)
|
||||
if not layers["thickness"]:
|
||||
return
|
||||
thickness = layers["thickness"] * abs(1 / cos(x_angle))
|
||||
thickness *= direction
|
||||
|
||||
offset_type = model_props.offset_type_horizontal
|
||||
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
|
||||
offset = model_props.offset * abs(1 / cos(x_angle)) * unit_scale
|
||||
|
||||
data = {}
|
||||
data["verts"] = []
|
||||
# Verts
|
||||
polyline_vertices = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.insertion_polyline
|
||||
polyline_points = polyline_data[0].polyline_points if polyline_data else []
|
||||
if len(polyline_points) < 3:
|
||||
data = []
|
||||
return
|
||||
for point in polyline_points:
|
||||
polyline_vertices.append(Vector((point.x, point.y, point.z)))
|
||||
if x_angle:
|
||||
# Get vertices relative to the first polyline point as origin
|
||||
local_vertices = [v - Vector(polyline_vertices[0]) for v in polyline_vertices]
|
||||
# Make the transformation relative to the x_angle
|
||||
transformed_vertices = [Vector((v.x, v.y * (1 / cos(x_angle)), v.z)) for v in local_vertices]
|
||||
# Convert back to world origin
|
||||
polyline_vertices = [v + Vector(polyline_vertices[0]) for v in transformed_vertices]
|
||||
if offset != 0:
|
||||
polyline_vertices = [v + Vector((0, 0, offset)) for v in polyline_vertices]
|
||||
is_closed = True
|
||||
if (
|
||||
polyline_vertices[0].x == polyline_vertices[-1].x
|
||||
and polyline_vertices[0].y == polyline_vertices[-1].y
|
||||
and polyline_vertices[0].z == polyline_vertices[-1].z
|
||||
):
|
||||
polyline_vertices.pop(-1) # Remove the last point. The edges are going to inform that the shape is closed.
|
||||
bm = create_bmesh_from_vertices(polyline_vertices, is_closed)
|
||||
bm.verts.ensure_lookup_table()
|
||||
if x_angle:
|
||||
rot_mat = Matrix.Rotation(x_angle, 3, "X")
|
||||
if abs(x_angle) > (pi / 2):
|
||||
rot_mat = rot_mat @ Matrix.Scale(-1, 3, (0, 1, 0))
|
||||
bmesh.ops.rotate(bm, cent=Vector(bm.verts[0].co), verts=bm.verts, matrix=rot_mat)
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + 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, thickness))
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
return data
|
||||
|
||||
|
||||
def get_vertical_profile_preview_data(
|
||||
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
|
||||
) -> dict[str, Any]:
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
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
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
# Create extrusion bmesh
|
||||
bm = bmesh.new()
|
||||
|
||||
grouped_verts.append(grouped_verts[0]) # Close profile
|
||||
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()
|
||||
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.001)
|
||||
|
||||
new_faces = bmesh.ops.contextual_create(bm, geom=bm.edges)
|
||||
|
||||
new_faces = bmesh.ops.extrude_face_region(bm, geom=bm.faces, use_dissolve_ortho_edges=True)
|
||||
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))
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
|
||||
# Calculate rotation, mouse position, angle and cardinal point
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, snap_prop.z))
|
||||
data = {}
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
verts = [tuple(rot_mat @ Vector(v)) for v in verts]
|
||||
verts = [tuple(Vector(v) + mouse_point) for v in verts]
|
||||
min_z = min(v.co.z for v in bm.verts)
|
||||
max_z = max(v.co.z for v in bm.verts)
|
||||
# Add axis verts
|
||||
verts.append(tuple(mouse_point))
|
||||
verts.append(tuple(mouse_point + Vector((0, 0, max_z))))
|
||||
# Add only profile edges
|
||||
edges = []
|
||||
for edge in bm.edges:
|
||||
if (edge.verts[0].co.z == min_z and edge.verts[1].co.z == min_z) or (
|
||||
edge.verts[0].co.z == max_z and edge.verts[1].co.z == max_z
|
||||
):
|
||||
edges.append(edge)
|
||||
# Add axis edge
|
||||
edges = [(edge.verts[0].index, edge.verts[1].index) for edge in edges]
|
||||
edges.append((len(verts) - 1, len(verts) - 2))
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
|
||||
bm.free()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_horizontal_profile_preview_data(
|
||||
context: bpy.types.Context, relating_type: ifcopenshell.entity_instance
|
||||
) -> dict[str, Any]:
|
||||
material = ifcopenshell.util.element.get_material(relating_type)
|
||||
try:
|
||||
profile_curve = material.MaterialProfiles[0].Profile
|
||||
except:
|
||||
return {}
|
||||
|
||||
model_props = tool.Model.get_model_props()
|
||||
cardinal_point = model_props.cardinal_point
|
||||
|
||||
polyline_verts = []
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
polyline_data = polyline_props.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)]
|
||||
|
||||
# Get profile shape
|
||||
settings = ifcopenshell.geom.settings()
|
||||
settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS)
|
||||
shape = ifcopenshell.geom.create_shape(settings, profile_curve)
|
||||
|
||||
verts = shape.verts
|
||||
if not verts:
|
||||
raise RuntimeError(f"Profile shape has no vertices, it probably is invalid: '{profile_curve}'.")
|
||||
|
||||
edges = shape.edges
|
||||
|
||||
grouped_verts = [[verts[i], verts[i + 1], 0] for i in range(0, len(verts), 3)]
|
||||
grouped_edges = [[edges[i], edges[i + 1]] for i in range(0, len(edges), 2)]
|
||||
|
||||
# Create offsets based on cardinal point
|
||||
min_x = min(v[0] for v in grouped_verts)
|
||||
max_x = max(v[0] for v in grouped_verts)
|
||||
min_y = min(v[1] for v in grouped_verts)
|
||||
max_y = max(v[1] for v in grouped_verts)
|
||||
|
||||
x_offset = (max_x - min_x) / 2
|
||||
y_offset = (max_y - min_y) / 2
|
||||
|
||||
match cardinal_point:
|
||||
case "1":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "2":
|
||||
grouped_verts = [(v[0], v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "3":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] + y_offset, v[2]) for v in grouped_verts]
|
||||
case "4":
|
||||
grouped_verts = [(v[0] - x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "5":
|
||||
grouped_verts = [(v[0], v[1], v[2]) for v in grouped_verts]
|
||||
case "6":
|
||||
grouped_verts = [(v[0] + x_offset, v[1], v[2]) for v in grouped_verts]
|
||||
case "7":
|
||||
grouped_verts = [(v[0] - x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "8":
|
||||
grouped_verts = [(v[0], v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
case "9":
|
||||
grouped_verts = [(v[0] + x_offset, v[1] - y_offset, v[2]) for v in grouped_verts]
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
data["verts"] = []
|
||||
data["edges"] = []
|
||||
data["tris"] = []
|
||||
|
||||
grouped_verts = [(v) for v in grouped_verts]
|
||||
|
||||
all_bm = bmesh.new()
|
||||
for i in range(len(polyline_verts) - 1):
|
||||
mesh = bpy.data.meshes.new("TempMesh")
|
||||
# Create the initial mesh from the profile verts
|
||||
bm = create_bmesh_from_vertices(grouped_verts, is_closed=True)
|
||||
bm.verts.ensure_lookup_table()
|
||||
# Creates the clipping plane formed by two segments.
|
||||
# The first one is for the profile start, based on the current and previous segment of the polyline.
|
||||
# The second is for the profile end, based on the current and the next segment.
|
||||
if i == 0:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_start = d
|
||||
else:
|
||||
d1 = (polyline_verts[i] - polyline_verts[i - 1]).normalized()
|
||||
d2 = (polyline_verts[i] - polyline_verts[i + 1]).normalized()
|
||||
clip_start = (d1 - d2).normalized()
|
||||
|
||||
if i == len(polyline_verts) - 2:
|
||||
d = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
clip_end = d
|
||||
else:
|
||||
d1 = (polyline_verts[i + 1] - polyline_verts[i]).normalized()
|
||||
d2 = (polyline_verts[i + 1] - polyline_verts[i + 2]).normalized()
|
||||
clip_end = (d1 - d2).normalized()
|
||||
|
||||
# Rotates the profile face to the right direction
|
||||
direction = polyline_verts[i + 1] - polyline_verts[i]
|
||||
position = polyline_verts[i]
|
||||
rotation_matrix = direction.to_track_quat("Z", "Y").to_matrix().to_4x4()
|
||||
bmesh.ops.transform(bm, verts=bm.verts, matrix=rotation_matrix)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=position)
|
||||
bmesh.ops.translate(bm, verts=bm.verts, vec=-direction)
|
||||
|
||||
# Extrude and move the new face
|
||||
last_face = bmesh.ops.extrude_face_region(bm, geom=bm.edges[:] + bm.faces[:])
|
||||
new_verts = [e for e in last_face["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=new_verts, vec=direction * 3)
|
||||
# Apply the cutting planes
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i],
|
||||
plane_no=clip_start,
|
||||
clear_inner=True,
|
||||
)
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
cut = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
|
||||
plane_co=polyline_verts[i + 1],
|
||||
plane_no=clip_end,
|
||||
clear_outer=True,
|
||||
)
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
mesh.update()
|
||||
all_bm.from_mesh(mesh)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh"])
|
||||
|
||||
# It's necessary to add the mesh to an object to get the expected result.
|
||||
mesh = bpy.data.meshes.new("TempMesh2")
|
||||
all_bm.to_mesh(mesh)
|
||||
all_bm.free()
|
||||
obj = bpy.data.objects.new("TempObj", mesh)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bpy.data.meshes.remove(bpy.data.meshes["TempMesh2"])
|
||||
|
||||
verts = [tuple(v.co) for v in bm.verts]
|
||||
edges = [[v.index for v in e.verts] for e in bm.edges]
|
||||
tris = [[loop.vert.index for loop in triangles] for triangles in bm.calc_loop_triangles()]
|
||||
data["verts"] = verts
|
||||
data["edges"] = edges
|
||||
data["tris"] = tris
|
||||
bm.free()
|
||||
return data
|
||||
|
||||
|
||||
def get_generic_product_preview_data(context, relating_type):
|
||||
model_props = tool.Model.get_model_props()
|
||||
if relating_type.is_a("IfcDoorType"):
|
||||
rl = float(model_props.rl1)
|
||||
elif relating_type.is_a("IfcWindowType"):
|
||||
rl = float(model_props.rl2)
|
||||
else:
|
||||
rl = 0
|
||||
polyline_props = tool.Model.get_polyline_props()
|
||||
snap_prop = polyline_props.snap_mouse_point[0]
|
||||
default_container_elevation = tool.Root.get_default_container_elevation()
|
||||
mouse_point = Vector((snap_prop.x, snap_prop.y, default_container_elevation))
|
||||
snap_obj = bpy.data.objects.get(snap_prop.snap_object)
|
||||
snap_element = tool.Ifc.get_entity(snap_obj)
|
||||
rot_mat = Matrix()
|
||||
if relating_type.is_a() in ["IfcDoorType", "IfcWindowType"] and snap_element and snap_element.is_a("IfcWall"):
|
||||
layers = tool.Model.get_material_layer_parameters(snap_element)
|
||||
axes = tool.Model.get_wall_axis(snap_obj, layers=layers)
|
||||
axis_base = axes["base"]
|
||||
axis_side = axes["side"]
|
||||
point_on_base_axis = tool.Cad.point_on_edge(mouse_point, axis_base)
|
||||
point_on_side_axis = tool.Cad.point_on_edge(mouse_point, axis_side)
|
||||
if (point_on_base_axis - mouse_point).length_squared <= (point_on_side_axis - mouse_point).length_squared:
|
||||
# mouse is snapped to the base axis, the preview looks exactly like the placed door / window
|
||||
rot_mat = snap_obj.matrix_world
|
||||
else:
|
||||
# mouse is snapped to the side axis, the preview is inverted, rotate it now and correct x position later
|
||||
rot_mat = (snap_obj.matrix_world.to_quaternion() @ Quaternion(Vector((0, 0, 1)), radians(180))).to_matrix().to_4x4()
|
||||
|
||||
mouse_point.z = snap_obj.matrix_world.translation.z
|
||||
|
||||
if snap_element and (container := ifcopenshell.util.element.get_container(snap_element)):
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
mouse_point.z = container_obj.location.z
|
||||
|
||||
obj_type = tool.Ifc.get_object(relating_type)
|
||||
|
||||
subcontexts = tool.Drawing.get_active_drawing_subcontexts()
|
||||
if not subcontexts:
|
||||
subcontexts = [("Model", "Body", "MODEL_VIEW")]
|
||||
|
||||
active_context = tool.Geometry.get_active_representation_context(obj_type)
|
||||
active_context_params = tool.Geometry.get_subcontext_parameters(active_context)
|
||||
for subcontext in subcontexts:
|
||||
if subcontext == active_context_params:
|
||||
break
|
||||
|
||||
representation = ifcopenshell.util.representation.get_representation(relating_type, *subcontext)
|
||||
if representation:
|
||||
bonsai.core.geometry.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj_type,
|
||||
representation,
|
||||
)
|
||||
context.view_layer.update()
|
||||
break
|
||||
|
||||
if obj_type.data:
|
||||
data = ItemDecorator.get_obj_data(obj_type)
|
||||
obj_type_matrix_i = obj_type.matrix_world.inverted()
|
||||
translate_mouse = Matrix.Translation(mouse_point)
|
||||
translate_rl = Matrix.Translation((0.0, 0.0, rl))
|
||||
combined_m = translate_mouse @ rot_mat @ translate_rl @ obj_type_matrix_i
|
||||
data["verts"] = [tuple(combined_m @ Vector(v)) for v in data["verts"]]
|
||||
return data
|
||||
|
||||
|
||||
class PolylineOperator:
|
||||
# TODO Fill doc strings
|
||||
""" """
|
||||
@@ -989,37 +436,6 @@ class PolylineOperator:
|
||||
tool.Blender.update_viewport()
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def get_product_preview_data(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
if tool.Model.get_usage_type(relating_type) == "PROFILE":
|
||||
if relating_type.is_a() in {"IfcColumnType", "IfcPileType"}:
|
||||
data = get_vertical_profile_preview_data(context, relating_type)
|
||||
else:
|
||||
data = get_horizontal_profile_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER2":
|
||||
data = get_wall_preview_data(context, relating_type)
|
||||
elif tool.Model.get_usage_type(relating_type) == "LAYER3":
|
||||
data = get_slab_preview_data(context, relating_type)
|
||||
else:
|
||||
data = get_generic_product_preview_data(context, relating_type)
|
||||
|
||||
# Update properties so it can be used by the decorator
|
||||
props = tool.Model.get_product_preview_props()
|
||||
props.verts.clear()
|
||||
props.edges.clear()
|
||||
props.tris.clear()
|
||||
if not data:
|
||||
return
|
||||
|
||||
for vert in data["verts"]:
|
||||
v = props.verts.add()
|
||||
v.value_3d = vert
|
||||
for edge in data["edges"]:
|
||||
e = props.edges.add()
|
||||
e.value_2d = edge
|
||||
for tri in data["tris"]:
|
||||
t = props.tris.add()
|
||||
t.value_3d = tri
|
||||
|
||||
def set_offset(self, context: bpy.types.Context, relating_type: ifcopenshell.entity_instance) -> None:
|
||||
props = tool.Model.get_model_props()
|
||||
direction_sense = props.direction_sense
|
||||
|
||||
@@ -249,8 +249,6 @@ class DrawOccurrence(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
if event.value == "RELEASE" and event.type == "LEFTMOUSE":
|
||||
self.create_occurrence(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -1189,11 +1189,8 @@ class DrawPolylineProfile(bpy.types.Operator, PolylineOperator, tool.Ifc.Operato
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -1697,17 +1697,6 @@ class ProductPreviewItem(PropertyGroup):
|
||||
value_2d: tuple[float, float]
|
||||
|
||||
|
||||
class BIMProductPreviewProperties(PropertyGroup):
|
||||
verts: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
edges: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
tris: bpy.props.CollectionProperty(type=ProductPreviewItem)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
verts: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
edges: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
tris: bpy.types.bpy_prop_collection_idprop[ProductPreviewItem]
|
||||
|
||||
|
||||
def update_is_editing(self: "BIMExternalParametricGeometryProperties", context: bpy.types.Context) -> None:
|
||||
if self.is_editing:
|
||||
return
|
||||
|
||||
@@ -1135,11 +1135,8 @@ class DrawPolylineSlab(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -787,11 +787,8 @@ class DrawPolylineWall(bpy.types.Operator, PolylineOperator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
self.handle_keyboard_input(context, event)
|
||||
|
||||
self.handle_inserting_polyline(context, event)
|
||||
|
||||
self.get_product_preview_data(context, self.relating_type)
|
||||
|
||||
cancel = self.handle_cancelation(context, event)
|
||||
if cancel is not None:
|
||||
ProductDecorator.uninstall()
|
||||
|
||||
@@ -66,7 +66,6 @@ if TYPE_CHECKING:
|
||||
BIMRailingProperties,
|
||||
BIMExternalParametricGeometryProperties,
|
||||
BIMPolylineProperties,
|
||||
BIMProductPreviewProperties,
|
||||
)
|
||||
|
||||
|
||||
@@ -108,11 +107,6 @@ class Model(bonsai.core.tool.Model):
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMPolylineProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def get_product_preview_props(cls) -> BIMProductPreviewProperties:
|
||||
assert (scene := bpy.context.scene)
|
||||
return scene.BIMProductPreviewProperties # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
@classmethod
|
||||
def convert_si_to_unit(cls, value: T) -> T:
|
||||
if isinstance(value, (tuple, list)):
|
||||
@@ -2750,3 +2744,20 @@ class Model(bonsai.core.tool.Model):
|
||||
list(nodes_to_update)
|
||||
finally:
|
||||
SvIfcStore.use_bonsai_file = False
|
||||
|
||||
@classmethod
|
||||
def create_bmesh_from_vertices(cls, vertices, is_closed=False):
|
||||
bm = bmesh.new()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in vertices]
|
||||
if is_closed:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
new_edges.append(
|
||||
bm.edges.new((new_verts[-1], new_verts[0]))
|
||||
) # Add an edge between the last an first point to make it closed.
|
||||
else:
|
||||
new_edges = [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)]
|
||||
|
||||
bm.verts.index_update()
|
||||
bm.edges.index_update()
|
||||
return bm
|
||||
|
||||
Reference in New Issue
Block a user