mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Fix #5968. Fix #4040. Rewrote roof generation using per face extrusions and cutting planes instead of edge sliding.
WARNING! The roof generation is now quite significantly different. This WILL change your geometry when you refresh roofs from existing models. - The roof profile now always represents the top of eave. This better represents how things are built rather than bottom of eave which was previous. - The roof therefore always grows down from the profile. The rafter edge angle can only be acute. This means that the roof will never grow larger than the footprint profile that you've drawn. (before, the footprint was not guaranteed to match). - The roof thickness is now the actual thickness of the roof, not the "vertical dimension" of the roof. This means that the roof thickness can now match intended layer thicknesses instead of you needing to do math to work it out. I've rewritten the internals of how roofs were generated to be hopefully a lot simpler but as a tradeoff it's more restrictive. After the skeleton is generated, non-uniform angles would be handled through vertex splitting and moving. This works in simple scenarios but fails in more complex ones. The new approach only handles non-uniform angles on triangular faces. These faces are really easy to handle compared to ngons, but are also more robust. The rafter edge angle is also now handled using a clipping plane, which is a lot, lot simpler than vertex sliding math. Each face is now processed separately and then merged at the end. This means that if a face has a different angle, it will now correctly represent the different thickness at that portion of the roof. In the process consistent roof thickness / rafter angle bugs were fixed.
This commit is contained in:
@@ -208,11 +208,11 @@ class ProfileDecorator:
|
||||
if draw_faces:
|
||||
self.draw_faces(bm, all_vertices)
|
||||
|
||||
self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
self.draw_batch("LINES", all_vertices, background_elements_color, arc_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
|
||||
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
|
||||
self.draw_batch("LINES", all_vertices, unselected_elements_color, unselected_edges)
|
||||
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
|
||||
|
||||
self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
|
||||
self.draw_batch("POINTS", error_vertices, error_elements_color)
|
||||
|
||||
@@ -829,7 +829,7 @@ class BIMRoofProperties(PropertyGroup):
|
||||
soft_max=to_percentage(radians(60.0)),
|
||||
)
|
||||
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
|
||||
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi, default=pi / 2, subtype="ANGLE")
|
||||
rafter_edge_angle: bpy.props.FloatProperty(name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE")
|
||||
|
||||
def get_general_kwargs(self, generation_method=None, convert_to_project_units=False):
|
||||
if generation_method is None:
|
||||
|
||||
@@ -25,17 +25,15 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.helper import convert_property_group_from_si
|
||||
from bonsai.bim.module.model.data import RoofData, refresh
|
||||
from bonsai.bim.module.model.decorator import ProfileDecorator
|
||||
|
||||
import json
|
||||
from math import tan, pi, radians
|
||||
from mathutils import Vector, Matrix
|
||||
from math import cos, tan, pi, radians
|
||||
from mathutils import Vector, Matrix, Quaternion
|
||||
import mathutils.geometry
|
||||
from bpypolyskel import bpypolyskel
|
||||
import shapely
|
||||
from pprint import pprint
|
||||
from typing import Literal, Union, Any
|
||||
|
||||
# reference:
|
||||
@@ -89,7 +87,7 @@ class GenerateHippedRoof(bpy.types.Operator, tool.Ifc.Operator):
|
||||
self.report(op_status, error_message)
|
||||
return {"CANCELLED"}
|
||||
|
||||
generate_hiped_roof_bmesh(bm, self.mode, self.height, self.angle)
|
||||
generate_hipped_roof_bmesh(bm, self.mode, self.height, self.angle)
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -107,7 +105,7 @@ def is_valid_roof_footprint(bm: bmesh.types.BMesh) -> tuple[set[str], str]:
|
||||
return ({"FINISHED"}, "")
|
||||
|
||||
|
||||
def generate_hiped_roof_bmesh(
|
||||
def generate_hipped_roof_bmesh(
|
||||
bm: bmesh.types.BMesh,
|
||||
mode: Literal["ANGLE", "HEIGHT"] = "ANGLE",
|
||||
height: float = 1.0,
|
||||
@@ -125,29 +123,23 @@ def generate_hiped_roof_bmesh(
|
||||
|
||||
If roof bmesh needed only to supply into decorator then there is no reason to mutate it.
|
||||
"""
|
||||
|
||||
if not mutate_current_bmesh:
|
||||
bm = bm.copy()
|
||||
|
||||
# CLEAN UP
|
||||
bm_mesh_clean_up(bm)
|
||||
|
||||
boundary_lines = []
|
||||
|
||||
original_geometry_data = dict()
|
||||
rafter_edge_angle = pi / 2 - rafter_edge_angle # It's easier to work with this angle.
|
||||
angled_edges = []
|
||||
angle_layer = bm.edges.layers.float.get("BBIM_gable_roof_angles")
|
||||
separate_verts_layer = bm.edges.layers.int.get("BBIM_gable_roof_separate_verts")
|
||||
if angle_layer:
|
||||
original_geometry_data["edges"] = [
|
||||
(set(bm_get_indices(e.verts)), e[angle_layer], e[separate_verts_layer]) for e in bm.edges
|
||||
angled_edges = [
|
||||
(set([v.co.copy().freeze() for v in e.verts]), e[angle_layer]) for e in bm.edges if e[angle_layer]
|
||||
]
|
||||
else:
|
||||
original_geometry_data["edges"] = [(set(bm_get_indices(e.verts)), None, None) for e in bm.edges]
|
||||
|
||||
original_geometry_data["verts"] = {v.index: v.co.copy() for v in bm.verts}
|
||||
footprint_z = bm.verts[:][0].co.z
|
||||
|
||||
def calculate_hiped_roof():
|
||||
def calculate_hipped_roof():
|
||||
boundary_lines = []
|
||||
for edge in bm.edges:
|
||||
boundary_lines.append(shapely.LineString([v.co for v in edge.verts]))
|
||||
|
||||
@@ -187,202 +179,210 @@ def generate_hiped_roof_bmesh(
|
||||
nonlocal height, angle
|
||||
if mode == "HEIGHT":
|
||||
height = height
|
||||
angle = 0.0
|
||||
tan_angle = 0.0
|
||||
else:
|
||||
angle = tan(angle)
|
||||
tan_angle = tan(angle)
|
||||
height = 0.0
|
||||
|
||||
faces = bpypolyskel.polygonize(
|
||||
verts, start_exterior_index, total_exterior_verts, inner_loops, height, angle, faces, unit_vectors
|
||||
verts, start_exterior_index, total_exterior_verts, inner_loops, height, tan_angle, faces, unit_vectors
|
||||
)
|
||||
|
||||
edges = []
|
||||
return verts, edges, faces
|
||||
|
||||
verts, edges, faces = calculate_hiped_roof()
|
||||
verts, edges, faces = calculate_hipped_roof()
|
||||
bm.clear()
|
||||
|
||||
new_verts = [bm.verts.new(v) for v in verts]
|
||||
new_edges = [bm.edges.new([new_verts[vi] for vi in edge]) for edge in edges]
|
||||
new_faces = [bm.faces.new([new_verts[vi] for vi in face]) for face in faces]
|
||||
|
||||
def find_identical_new_vert(co: Vector) -> Union[bmesh.types.BMVert, None]:
|
||||
for v in bm.verts:
|
||||
if tool.Cad.is_x((co - v.co).length, 0):
|
||||
return v
|
||||
if mode == "HEIGHT": # Calculate the angle we ended up with.
|
||||
new_faces[0].normal_update()
|
||||
angle = new_faces[0].normal.angle(Vector((0, 0, 1)))
|
||||
|
||||
def find_other_polygon_verts(edge: bmesh.types.BMEdge) -> list[bmesh.types.BMVert]:
|
||||
polygon = edge.link_faces[0]
|
||||
return [v for v in polygon.verts if v not in edge.verts]
|
||||
|
||||
def change_angle(projected_vert_co: Vector, edge_verts: list[bmesh.types.BMVert], new_angle: float) -> Vector:
|
||||
A, B = [v.co for v in edge_verts]
|
||||
P = projected_vert_co
|
||||
# bpypolyskel performs a straight skeleton with equal weights. Ideally, we
|
||||
# need support for weighted straight skeletons (CGAL has this function) to
|
||||
# calculate skeletons of varying angles. Doing anything else, such as
|
||||
# creating new vertices, splitting edges, or sliding vertices is extremely
|
||||
# error prone and only works in simple scenarios.
|
||||
|
||||
AP = P - A
|
||||
AB = B - A
|
||||
AB_dir = AB.normalized()
|
||||
proj_length = AP.dot(AB_dir)
|
||||
C = A + AB_dir * proj_length
|
||||
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
|
||||
|
||||
Pp = P * Vector([1, 1, 0]) + Vector([0, 0, C.z])
|
||||
angle_tan = tan(new_angle)
|
||||
dist = (P.z - Pp.z) / angle_tan
|
||||
PPnew = C + (Pp - C).normalized() * dist
|
||||
Pnew = PPnew * Vector([1, 1, 0]) + Vector([0, 0, P.z])
|
||||
return Pnew
|
||||
|
||||
footprint_edges = []
|
||||
footprint_verts = set()
|
||||
verts_to_change = {}
|
||||
verts_to_rip = []
|
||||
bottom_chords_to_remove = []
|
||||
|
||||
def is_footprint_vert(v: bmesh.types.BMVert) -> bool:
|
||||
return tool.Cad.is_x(v.co.z - footprint_z, 0)
|
||||
# In the meantime, we can support the simplest, but luckily also most
|
||||
# common scenario of a gable roof, or hipped roof where the varied angle
|
||||
# only occurs on a triangle face.
|
||||
|
||||
def is_footprint_edge(edge: bmesh.types.BMEdge) -> bool:
|
||||
return all(is_footprint_vert(v) for v in edge.verts)
|
||||
return all(tool.Cad.is_x(v.co.z - footprint_z, 0) for v in edge.verts)
|
||||
|
||||
# find footprint edges
|
||||
footprint_edges = set()
|
||||
gable_edges = set()
|
||||
edges_to_delete = set()
|
||||
face_angles = {}
|
||||
|
||||
# Post process skeleton to handle gables and custom angles.
|
||||
for edge in bm.edges:
|
||||
if is_footprint_edge(edge):
|
||||
footprint_edges.append(edge)
|
||||
footprint_verts.update(edge.verts)
|
||||
|
||||
old_verts_remap = {}
|
||||
for old_vert in original_geometry_data["verts"]:
|
||||
old_vert_co = original_geometry_data["verts"][old_vert]
|
||||
old_verts_remap[old_vert] = find_identical_new_vert(old_vert_co)
|
||||
|
||||
# iterate over edges from original geometry
|
||||
# if their angle was redefined by user - apply the changes to the related vertices
|
||||
# to match the requested angle
|
||||
|
||||
process_later = []
|
||||
for old_edge_verts, defined_angle, separate_verts in original_geometry_data["edges"]:
|
||||
if not defined_angle:
|
||||
if not is_footprint_edge(edge):
|
||||
continue
|
||||
|
||||
edge_verts_remaped = set(old_verts_remap[old_vert] for old_vert in old_edge_verts)
|
||||
|
||||
identical_edge = None
|
||||
for edge in footprint_edges:
|
||||
if set(edge.verts) == edge_verts_remaped:
|
||||
identical_edge = edge
|
||||
break
|
||||
assert identical_edge
|
||||
|
||||
if separate_verts:
|
||||
verts_to_move = find_other_polygon_verts(identical_edge)
|
||||
for v in verts_to_move:
|
||||
vert_co = v.co
|
||||
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
|
||||
verts_to_rip.append([v, new_vert_co, identical_edge])
|
||||
else:
|
||||
process_later.append([identical_edge, edge_verts_remaped, defined_angle])
|
||||
|
||||
if defined_angle >= pi / 2:
|
||||
bottom_chords_to_remove.append(identical_edge)
|
||||
|
||||
def separate_vert(
|
||||
bm: bmesh.types.BMesh, vert: bmesh.types.BMVert, edge: bmesh.types.BMEdge, new_co: Vector
|
||||
) -> bmesh.types.BMVert:
|
||||
face = next(f for f in vert.link_faces if edge in f.edges)
|
||||
new_v = bmesh.utils.face_vert_separate(face, vert)
|
||||
new_v.co = new_co
|
||||
new_edge = bm.edges.new((vert, new_v))
|
||||
|
||||
for cur_edge in face.edges:
|
||||
if cur_edge == edge:
|
||||
continue
|
||||
bmesh.ops.contextual_create(bm, geom=[new_edge, cur_edge])
|
||||
return new_v
|
||||
|
||||
# correct angle for verts that require verts separation
|
||||
# store separated verts for angle correction later
|
||||
related_verts = {}
|
||||
for v, new_co, edge in verts_to_rip:
|
||||
new_v = separate_vert(bm, v, edge, new_co)
|
||||
related_verts.setdefault(v, []).append(new_v)
|
||||
|
||||
# verts angle correction.
|
||||
# we're taking into account new verts created after verts separation
|
||||
# required for asymmetrical gable roof
|
||||
for identical_edge, edge_verts_remaped, defined_angle in process_later:
|
||||
verts_to_move = find_other_polygon_verts(identical_edge)
|
||||
for v in verts_to_move[:]:
|
||||
verts_to_move.extend(related_verts.get(v, []))
|
||||
|
||||
for v in verts_to_move:
|
||||
vert_co = verts_to_change.get(v, v.co)
|
||||
new_vert_co = change_angle(vert_co, edge_verts_remaped, defined_angle)
|
||||
verts_to_change[v] = new_vert_co
|
||||
|
||||
# apply all changes once at the end
|
||||
for v in verts_to_change:
|
||||
v.co = verts_to_change[v]
|
||||
|
||||
bmesh.ops.delete(bm, geom=bottom_chords_to_remove, context="EDGES")
|
||||
|
||||
# add roof thickness
|
||||
extrusion_geom = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=bm.faces)["geom"])
|
||||
extruded_edges = extrusion_geom["edges"]
|
||||
extruded_verts = extrusion_geom["verts"]
|
||||
rafter_edge_angle = pi / 2 - rafter_edge_angle
|
||||
default_offset_dir = Vector([0, 0, 1]) * roof_thickness
|
||||
footprint_verts = set()
|
||||
|
||||
if not tool.Cad.is_x(rafter_edge_angle, 0):
|
||||
footprint_edges = []
|
||||
for edge in extruded_edges:
|
||||
if is_footprint_edge(edge):
|
||||
footprint_edges.append(edge)
|
||||
footprint_verts.update(edge.verts)
|
||||
|
||||
def get_top_vert(v):
|
||||
non_footprint_verts = []
|
||||
for edge in v.link_edges:
|
||||
v1 = edge.other_vert(v)
|
||||
if not is_footprint_vert(v1):
|
||||
non_footprint_verts.append(v1)
|
||||
|
||||
if len(non_footprint_verts) == 1:
|
||||
return non_footprint_verts[0]
|
||||
|
||||
return min(non_footprint_verts, key=lambda v1: (v1.co - v.co).length)
|
||||
|
||||
top_verts = dict()
|
||||
for v in footprint_verts:
|
||||
top_verts[v] = get_top_vert(v)
|
||||
|
||||
# TODO: rafter_edge_angle might differ
|
||||
# if footprint edges are not connected by 90 degrees
|
||||
verts_offsets = dict()
|
||||
for edge in footprint_edges:
|
||||
v0, v1 = edge.verts
|
||||
edge_dir = (v0.co - v1.co).normalized()
|
||||
offset_dir = Matrix.Rotation(rafter_edge_angle, 4, edge_dir) @ default_offset_dir
|
||||
for v in edge.verts:
|
||||
previous_offset = verts_offsets.get(v, None)
|
||||
if previous_offset:
|
||||
previous_offset.xy += offset_dir.xy
|
||||
footprint_edges.add(edge)
|
||||
if len(edge.link_faces) != 1 or len(edge.link_faces[0].verts) != 3:
|
||||
continue # Too complicated for us to figure out without a weighted skeleton
|
||||
face_verts = edge.link_faces[0].verts
|
||||
verts = set([v.co.copy().freeze() for v in edge.verts])
|
||||
for angled_edge, edge_angle in angled_edges:
|
||||
if angled_edge == verts:
|
||||
face_angles[edge.link_faces[0]] = edge_angle
|
||||
ridge_vert = [v for v in face_verts if v.co.copy().freeze() not in verts][0]
|
||||
if len(ridge_vert.link_edges) == 3:
|
||||
ridge_edge = [e for e in ridge_vert.link_edges if e not in edge.link_faces[0].edges][0]
|
||||
other_ridge_vert = ridge_edge.other_vert(ridge_vert)
|
||||
else:
|
||||
verts_offsets[v] = offset_dir
|
||||
# We cannot actually get this correct without a weighted
|
||||
# skeleton, but for now let's assume we duplicate the ridge
|
||||
# vert and slide the vertex towards the midpoint of the
|
||||
# footprint edge.
|
||||
edge_midpoint = edge.verts[0].co.lerp(edge.verts[1].co, 0.5)
|
||||
edge_midpoint.z = ridge_vert.co.z
|
||||
poke = bmesh.ops.poke(bm, faces=[edge.link_faces[0]])
|
||||
other_ridge_vert = poke["verts"][0]
|
||||
other_ridge_vert.co = ridge_vert.co + (edge_midpoint - ridge_vert.co).normalized() * 0.01
|
||||
ridge_vert, other_ridge_vert = other_ridge_vert, ridge_vert
|
||||
|
||||
for v in verts_offsets:
|
||||
vert_offset = verts_offsets[v]
|
||||
top = top_verts[v].co
|
||||
bot = v.co + default_offset_dir
|
||||
base = v.co
|
||||
offsetted = v.co + vert_offset
|
||||
# all this math is needed to maintain the same roof thickness
|
||||
# for different rafter edge angles
|
||||
v.co = mathutils.geometry.intersect_line_line(bot, top, base, offsetted)[0]
|
||||
face_center = (face_verts[0].co + face_verts[1].co + face_verts[2].co) / 3
|
||||
x_axis = (edge.verts[1].co - edge.verts[0].co).normalized()
|
||||
z_axis = Vector((0, 0, -1))
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
positive = edge.verts[0].co + (y_axis * 0.01)
|
||||
negative = edge.verts[0].co - (y_axis * 0.01)
|
||||
if (face_center - positive).length < (face_center - negative).length:
|
||||
y_axis = -y_axis
|
||||
x_axis = y_axis.cross(z_axis) # Recalculate X axis to make rotation sign consistent
|
||||
rotation_quaternion = Quaternion(x_axis, edge_angle)
|
||||
plane_no = rotation_quaternion @ z_axis
|
||||
|
||||
if intersect := tool.Cad.intersect_edge_plane(
|
||||
other_ridge_vert.co, ridge_vert.co, edge.verts[0].co, plane_no
|
||||
):
|
||||
edge_percent = tool.Cad.edge_percent(intersect, (other_ridge_vert.co, ridge_vert.co))
|
||||
if edge_percent <= 0:
|
||||
ridge_vert.co = other_ridge_vert.co
|
||||
else:
|
||||
ridge_vert.co = intersect
|
||||
|
||||
if tool.Cad.is_x(edge_angle, pi / 2, tolerance=radians(1)):
|
||||
footprint_edges.remove(edge)
|
||||
edges_to_delete.add(edge)
|
||||
gable_edges.update(
|
||||
[e for e in ridge_vert.link_edges if e.other_vert(ridge_vert) != other_ridge_vert]
|
||||
)
|
||||
break
|
||||
|
||||
bmesh.ops.delete(bm, geom=list(edges_to_delete), context="EDGES")
|
||||
|
||||
# Determine cutting planes for rafter_edge_angle
|
||||
# The skeleton is always extruded in the -Z. A cutting plane is applied at
|
||||
# the footprint or gable edge if a rafter_edge_angle is specified.
|
||||
cutting_planes = {}
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
if not tool.Cad.is_x(rafter_edge_angle, 0, tolerance=radians(1)):
|
||||
for e in footprint_edges:
|
||||
face = e.link_faces[0]
|
||||
planes = []
|
||||
verts = [v.co for v in face.verts]
|
||||
face_center = sum(verts[1:], start=verts[0].copy()) / len(verts)
|
||||
v1, v2 = [v.co.copy() for v in e.verts]
|
||||
if not tool.Cad.is_x(v1.z, footprint_z):
|
||||
v1, v2 = v2, v1
|
||||
x_axis = (v2 - v1).normalized()
|
||||
z_axis = Vector((0, 0, -1))
|
||||
y_axis = z_axis.cross(x_axis)
|
||||
positive = v1 + (y_axis * 0.01)
|
||||
negative = v1 - (y_axis * 0.01)
|
||||
plane_no = y_axis
|
||||
if (face_center - positive).length < (face_center - negative).length:
|
||||
plane_no = -y_axis
|
||||
x_axis = plane_no.cross(z_axis) # Recalculate X axis to make rotation sign consistent
|
||||
rotation_quaternion = Quaternion(x_axis, rafter_edge_angle)
|
||||
plane_no = rotation_quaternion @ plane_no
|
||||
planes.append((v1, plane_no))
|
||||
cutting_planes[face] = planes
|
||||
|
||||
# Gable edges are special - they are not cut at the rafter_edge_angle.
|
||||
# We copy the neighbouring cutting planes to make sure they mitre
|
||||
# correctly.
|
||||
for e in gable_edges:
|
||||
face = e.link_faces[0]
|
||||
planes = []
|
||||
neighbouring_faces = set()
|
||||
neighbouring_faces.update(e.verts[0].link_faces)
|
||||
neighbouring_faces.update(e.verts[1].link_faces)
|
||||
for f in neighbouring_faces:
|
||||
planes.extend(cutting_planes.get(f, []))
|
||||
cutting_planes[face] = planes
|
||||
|
||||
# Extrude skeleton using roof thickness
|
||||
for top_face in [f for f in bm.faces]:
|
||||
face = top_face.copy()
|
||||
face.tag = True # Tags keep track of which geometry is part of our current skeleton fragment
|
||||
|
||||
if face.normal.z > 0:
|
||||
face.normal_flip()
|
||||
|
||||
# This means that different fragments of the roof may not exactly join together.
|
||||
# It's technically correct, but may look weird if the architect doesn't know what they're doing.
|
||||
face_angle = face_angles.get(top_face, angle)
|
||||
# Alternatively maybe we can offer a "distort" mode which doesn't preserve thickness at custom angles:
|
||||
# face_angle = angle
|
||||
extrusion_height = roof_thickness / cos(face_angle)
|
||||
extrusion_vector = Vector((0, 0, -1)) * extrusion_height
|
||||
extrusion = tool.Model.bm_sort_out_geom(bmesh.ops.extrude_face_region(bm, geom=[face])["geom"])
|
||||
bmesh.ops.translate(bm, vec=extrusion_vector, verts=extrusion["verts"])
|
||||
|
||||
for plane_co, plane_no in cutting_planes.get(top_face, []):
|
||||
geom = set()
|
||||
for f in bm.faces:
|
||||
if f.tag:
|
||||
geom.add(f)
|
||||
for e in f.edges:
|
||||
geom.add(e)
|
||||
for v in f.verts:
|
||||
geom.add(v)
|
||||
|
||||
result = bmesh.ops.bisect_plane(
|
||||
bm, geom=list(geom), plane_co=plane_co, plane_no=plane_no, clear_outer=True, clear_inner=False
|
||||
)
|
||||
for g in result["geom"]:
|
||||
if isinstance(g, bmesh.types.BMFace):
|
||||
g.tag = True
|
||||
bm.faces.ensure_lookup_table()
|
||||
result = bmesh.ops.triangle_fill(
|
||||
bm, use_dissolve=True, edges=[g for g in result["geom_cut"] if isinstance(g, bmesh.types.BMEdge)]
|
||||
)
|
||||
for g in result["geom"]:
|
||||
if isinstance(g, bmesh.types.BMFace):
|
||||
g.tag = True
|
||||
verts = set()
|
||||
[verts.update(f.verts) for f in bm.faces if f.tag]
|
||||
bmesh.ops.remove_doubles(bm, verts=list(verts), dist=1e-4)
|
||||
|
||||
for f in bm.faces:
|
||||
f.tag = False
|
||||
|
||||
verts = [v for v in extruded_verts if v not in footprint_verts]
|
||||
bmesh.ops.translate(bm, vec=default_offset_dir, verts=verts)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces[:])
|
||||
|
||||
# Merge fragments and remove internal faces.
|
||||
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
|
||||
faces_to_delete = set()
|
||||
for face in [f for f in bm.faces]:
|
||||
is_internal = True
|
||||
for e in face.edges:
|
||||
if len(e.link_faces) < 3:
|
||||
is_internal = False
|
||||
if is_internal:
|
||||
faces_to_delete.add(face)
|
||||
bmesh.ops.delete(bm, geom=list(faces_to_delete), context="FACES")
|
||||
return bm
|
||||
|
||||
|
||||
@@ -413,9 +413,6 @@ def update_roof_modifier_ifc_data(context: bpy.types.Context) -> None:
|
||||
if props.roof_type == "HIP/GABLE ROOF":
|
||||
element.PredefinedType = "GABLE_ROOF" if roof_is_gabled() else "HIP_ROOF"
|
||||
|
||||
# occurrences attributes
|
||||
# occurrences = tool.Ifc.get_all_element_occurrences(element)
|
||||
|
||||
tool.Model.add_body_representation(obj)
|
||||
|
||||
|
||||
@@ -450,19 +447,17 @@ def update_roof_modifier_bmesh(obj: bpy.types.Object) -> None:
|
||||
|
||||
# generating roof path
|
||||
new_verts = [bm.verts.new(Vector(v) * si_conversion) for v in path_data["verts"]]
|
||||
new_edges = []
|
||||
for i in range(len(path_data["edges"])):
|
||||
e = path_data["edges"][i]
|
||||
edge = bm.edges.new((new_verts[e[0]], new_verts[e[1]]))
|
||||
edge[angle_layer] = angle_layer_data[i] if angle_layer_data else 0
|
||||
edge[separate_verts_layer] = separate_verts_data[i] if separate_verts_data else 0
|
||||
new_edges.append(edge)
|
||||
|
||||
if props.is_editing_path:
|
||||
tool.Blender.apply_bmesh(obj.data, bm)
|
||||
return
|
||||
|
||||
generate_hiped_roof_bmesh(
|
||||
generate_hipped_roof_bmesh(
|
||||
bm,
|
||||
props.generation_method,
|
||||
props.height,
|
||||
@@ -692,7 +687,7 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
main_bm.edges.layers.int.new("BBIM_preview")
|
||||
|
||||
second_bm = generate_hiped_roof_bmesh(
|
||||
second_bm = generate_hipped_roof_bmesh(
|
||||
bm,
|
||||
props.generation_method,
|
||||
props.height,
|
||||
@@ -701,7 +696,6 @@ class EnableEditingRoofPath(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.rafter_edge_angle,
|
||||
mutate_current_bmesh=False,
|
||||
)
|
||||
bmesh.ops.translate(second_bm, verts=second_bm.verts, vec=Vector((0, 0, 1)))
|
||||
|
||||
tool.Blender.bmesh_join(main_bm, second_bm, callback=mark_preview_edges)
|
||||
return main_bm
|
||||
|
||||
Reference in New Issue
Block a user