Fixed door / window preview and made preview of other objects not rotate with wall

This commit is contained in:
Robin Quint
2025-08-04 15:15:00 +02:00
committed by Ryan Schultz
parent 59dd25e1c2
commit 51016de211
+536 -1
View File
@@ -18,15 +18,550 @@
from __future__ import annotations
from math import cos, pi, radians, tan
from typing import Literal, Union
import bmesh
import bpy
import ifcopenshell
import ifcopenshell.util.unit
from mathutils import Vector
from mathutils import Matrix, Quaternion, Vector
import bonsai.tool as tool
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 = Quaternion()
invert_x = False
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.to_quaternion()
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))
invert_x = True
mouse_point.z = snap_obj.matrix_world.translation.z
obj_type = tool.Ifc.get_object(relating_type)
if obj_type.data:
data = ItemDecorator.get_obj_data(obj_type)
data["verts"] = [tuple(obj_type.matrix_world.inverted() @ Vector(v)) for v in data["verts"]]
offset_x = 0
if invert_x:
# correct the x position so that the inverted object occupies the same x extents
min_x = min([p[0] for p in data["verts"]])
max_x = max([p[0] for p in data["verts"]])
offset_x = max_x + min_x
data["verts"] = [tuple(rot_mat @ (Vector((v[0], v[1], (v[2] + rl)))) + mouse_point - Vector((offset_x, 0, 0))) for v in data["verts"]]
return data
class PolylineOperator: