mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
Fix extend_walls_to_underside ridge artifact
When the operator was called twice on the same wall for a ridge roof, the two IfcPolygonalFaceSet clip solids shared an exact ridge edge (kissing-solid). OCCT produced spurious extra vertices at the coincident boundary. Fix by building the clip solid from a rectangle on the slope plane that extends slightly past the face edge (1 project unit margin) rather than the exact face footprint. Adjacent slope solids now volumetrically overlap at the ridge instead of sharing a boundary face, which OCCT handles correctly. Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
a002e1e56d
commit
97cd08ee92
@@ -306,7 +306,9 @@ class ExtendWallsToUnderside(bpy.types.Operator, tool.Ifc.Operator):
|
||||
if (obj := tool.Blender.get_active_object(is_selected=True)) and (element := tool.Ifc.get_entity(obj)):
|
||||
slab = obj
|
||||
for obj in tool.Blender.get_selected_objects(include_active=False):
|
||||
if (element := tool.Ifc.get_entity(obj)) and tool.Model.get_usage_type(element) == "LAYER2":
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
usage = tool.Model.get_usage_type(element) if element else None
|
||||
if element and usage == "LAYER2":
|
||||
walls.append(obj)
|
||||
if slab and walls:
|
||||
core.extend_wall_to_slab(tool.Ifc, tool.Geometry, tool.Model, slab, walls)
|
||||
|
||||
@@ -168,8 +168,9 @@ def extend_wall_to_slab(
|
||||
slab_obj: bpy.types.Object,
|
||||
wall_objs: list[bpy.types.Object],
|
||||
) -> None:
|
||||
if not (clip := model.get_slab_clipping_bmesh(slab_obj)):
|
||||
return # Nothing to clip?
|
||||
clip = model.get_slab_clipping_bmesh(slab_obj)
|
||||
if not clip:
|
||||
return
|
||||
slab = ifc.get_entity(slab_obj)
|
||||
for obj in wall_objs:
|
||||
if ifc.is_moved(obj):
|
||||
|
||||
@@ -2561,7 +2561,8 @@ class Model(bonsai.core.tool.Model):
|
||||
face.normal_update()
|
||||
normal = face.normal.to_4d()
|
||||
normal.w = 0
|
||||
if (obj.matrix_world @ normal).z >= -0.5:
|
||||
world_normal_z = (obj.matrix_world @ normal).z
|
||||
if world_normal_z >= -0.5:
|
||||
continue
|
||||
new_verts = []
|
||||
for vert in face.verts:
|
||||
@@ -2575,6 +2576,7 @@ class Model(bonsai.core.tool.Model):
|
||||
return
|
||||
|
||||
bmesh.ops.recalc_face_normals(clipping_bm, faces=clipping_bm.faces)
|
||||
clipping_bm.faces.ensure_lookup_table()
|
||||
return clipping_bm # clipping_bm is in project units
|
||||
|
||||
@classmethod
|
||||
@@ -2588,17 +2590,53 @@ class Model(bonsai.core.tool.Model):
|
||||
min_z = min(zs)
|
||||
max_z = max(zs)
|
||||
|
||||
operand = None
|
||||
if (z := max_z - min_z) and not np.isclose(z, 0.0):
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
|
||||
ifc_file = tool.Ifc.get()
|
||||
builder = ifcopenshell.util.shape_builder.ShapeBuilder(ifc_file)
|
||||
|
||||
result = bmesh.ops.extrude_face_region(bm, geom=bm.faces)
|
||||
extruded_verts = [elem for elem in result["geom"] if isinstance(elem, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(bm, verts=extruded_verts, vec=(0, 0, z))
|
||||
# Build one IfcPolygonalFaceSet clip solid per clipping face.
|
||||
# Each solid uses a rectangle on the slope plane rather than the exact face
|
||||
# footprint. The original approach (exact footprint) caused a kissing-solid /
|
||||
# boundary-coincidence bug when the operator is called twice for a ridge roof: the
|
||||
# two slope solids share an exact ridge edge, and OCCT produces spurious extra
|
||||
# vertices. Extending each solid slightly past the ridge (by margin) creates a
|
||||
# volumetric overlap instead of a kissing boundary — OCCT handles overlapping
|
||||
# DIFFERENCE operands correctly.
|
||||
margin = 1.0 # project units past the face edge — enough to ensure overlap at ridge
|
||||
operands = []
|
||||
for face in bm.faces:
|
||||
face.normal_update()
|
||||
normal = Vector(face.normal).normalized()
|
||||
|
||||
verts = [v.co for v in bm.verts]
|
||||
faces = [[v.index for v in p.verts] for p in bm.faces]
|
||||
operand = builder.mesh(verts, faces)
|
||||
# Orthonormal basis spanning the slope plane.
|
||||
ref = Vector((0, 0, 1)) if abs(normal.z) < 0.9 else Vector((1, 0, 0))
|
||||
tangent1 = normal.cross(ref).normalized()
|
||||
tangent2 = normal.cross(tangent1).normalized()
|
||||
|
||||
centroid = sum((v.co for v in face.verts), Vector()) / len(face.verts)
|
||||
|
||||
# Tight bounding rectangle in slope-plane coords, plus a small margin.
|
||||
t1_coords = [(v.co - centroid).dot(tangent1) for v in face.verts]
|
||||
t2_coords = [(v.co - centroid).dot(tangent2) for v in face.verts]
|
||||
half1 = max(abs(c) for c in t1_coords) + margin
|
||||
half2 = max(abs(c) for c in t2_coords) + margin
|
||||
|
||||
# Rectangle on the slope plane, extruded upward in wall-local Z.
|
||||
clip_bm = bmesh.new()
|
||||
v0 = clip_bm.verts.new(centroid + half1 * tangent1 + half2 * tangent2)
|
||||
v1 = clip_bm.verts.new(centroid - half1 * tangent1 + half2 * tangent2)
|
||||
v2 = clip_bm.verts.new(centroid - half1 * tangent1 - half2 * tangent2)
|
||||
v3 = clip_bm.verts.new(centroid + half1 * tangent1 - half2 * tangent2)
|
||||
bottom_face = clip_bm.faces.new([v0, v1, v2, v3])
|
||||
result = bmesh.ops.extrude_face_region(clip_bm, geom=[bottom_face])
|
||||
top_verts = [e for e in result["geom"] if isinstance(e, bmesh.types.BMVert)]
|
||||
bmesh.ops.translate(clip_bm, verts=top_verts, vec=Vector((0, 0, max_z - min_z)))
|
||||
clip_bm.verts.ensure_lookup_table()
|
||||
|
||||
clip_verts = [v.co for v in clip_bm.verts]
|
||||
clip_faces = [[v.index for v in f.verts] for f in clip_bm.faces]
|
||||
operand = builder.mesh(clip_verts, clip_faces)
|
||||
clip_bm.free()
|
||||
operands.append(operand)
|
||||
|
||||
for extrusion in ifcopenshell.util.shape.get_base_extrusions(wall) or []:
|
||||
if extrusion.Position:
|
||||
@@ -2615,9 +2653,9 @@ class Model(bonsai.core.tool.Model):
|
||||
|
||||
extrusion.Depth = max_z / direction[2]
|
||||
|
||||
if operand:
|
||||
if operands:
|
||||
booleans = ifcopenshell.api.geometry.add_boolean(
|
||||
tool.Ifc.get(), first_item=extrusion, second_items=[operand]
|
||||
ifc_file, first_item=extrusion, second_items=operands
|
||||
)
|
||||
tool.Model.mark_manual_booleans(wall, booleans)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user