mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-20 15:08:51 +00:00
Add bisect_and_cap helper to tool.Geometry
Bisects a BMesh against a set of planes (clear_outer per plane), then fills the cut edges as cap faces tagged via a BMesh int layer so the tag survives subsequent bisects. Cut edges are grouped into connected components before filling so a hollow profile's outer + inner loops produce two separate cap faces instead of a single welded outer face that hides the hole. Pre-welds T-junctions introduced by IFC Boolean meshes so the cut closes into a fillable loop. Callers are responsible for input mesh quality. Non-watertight inputs (terrain, single-shell surfaces) may produce degenerate cap faces — that's an accepted user-supplied data limitation which can be revisited if real-world feedback shows it matters. Used by the clip-box feature to compute cross-section caps per IFC product mesh. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -157,6 +157,112 @@ class Geometry(bonsai.core.tool.Geometry):
|
|||||||
for modifier in obj.modifiers:
|
for modifier in obj.modifiers:
|
||||||
obj.modifiers.remove(modifier)
|
obj.modifiers.remove(modifier)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _group_edges_into_loops(cls, edges) -> list[list]:
|
||||||
|
"""Group an edge set into connected components by shared vertices.
|
||||||
|
|
||||||
|
Each returned group is a list of edges that share at least one
|
||||||
|
vertex chain. A hollow profile's bisect produces two disjoint
|
||||||
|
loops (outer ring + inner ring) — grouping splits them so each
|
||||||
|
can be filled independently as a separate cap face, rather than
|
||||||
|
``contextual_create`` welding them into one solid outer face
|
||||||
|
with the inner loop demoted to interior decoration.
|
||||||
|
"""
|
||||||
|
edge_set = set(edges)
|
||||||
|
visited: set = set()
|
||||||
|
groups: list[list] = []
|
||||||
|
for start in edges:
|
||||||
|
if start in visited:
|
||||||
|
continue
|
||||||
|
group: list = []
|
||||||
|
stack: list = [start]
|
||||||
|
while stack:
|
||||||
|
e = stack.pop()
|
||||||
|
if e in visited:
|
||||||
|
continue
|
||||||
|
visited.add(e)
|
||||||
|
group.append(e)
|
||||||
|
for v in e.verts:
|
||||||
|
for adj in v.link_edges:
|
||||||
|
if adj in edge_set and adj not in visited:
|
||||||
|
stack.append(adj)
|
||||||
|
groups.append(group)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bisect_and_cap(
|
||||||
|
cls,
|
||||||
|
bm,
|
||||||
|
planes_local,
|
||||||
|
*,
|
||||||
|
tag_layer_name: str = "bbim_cap",
|
||||||
|
dist: float = 1e-4,
|
||||||
|
weld_dist: float = 1e-5,
|
||||||
|
):
|
||||||
|
"""Clip ``bm`` against each ``(plane_co, plane_no)`` and fill the cuts.
|
||||||
|
|
||||||
|
Per plane, ``bmesh.ops.bisect_plane(clear_outer=True)`` discards
|
||||||
|
the outside half-space and ``bmesh.ops.contextual_create`` fills
|
||||||
|
the resulting cut edges with cap faces tagged via a BMesh int
|
||||||
|
layer so the tag propagates to any split-children from subsequent
|
||||||
|
planes. After all planes, near-coincident vertices are welded
|
||||||
|
(``weld_dist``) so adjacent caps from the same cross-section
|
||||||
|
merge cleanly.
|
||||||
|
|
||||||
|
Callers are responsible for input mesh quality. Non-watertight
|
||||||
|
inputs (terrain, single-shell surfaces) may produce degenerate
|
||||||
|
cap faces; that's an accepted user-supplied data limitation.
|
||||||
|
|
||||||
|
Returns the cap-tag BMLayerItem, or ``None`` if ``bm`` is empty.
|
||||||
|
"""
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
if not bm.faces:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Pre-weld nearby verts: T-junctions in messy IFC meshes (a third
|
||||||
|
# vertex sitting in the middle of an edge from a Boolean
|
||||||
|
# operation) make the bisect cut terminate early, leaving open
|
||||||
|
# loops that no fill op can close. Welding the T-junction's
|
||||||
|
# near-coincident vertex into the host edge before bisecting
|
||||||
|
# turns the cut into a closed loop.
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=max(weld_dist, 1e-4))
|
||||||
|
|
||||||
|
cap_layer = bm.faces.layers.int.new(tag_layer_name)
|
||||||
|
for plane_co, plane_no in planes_local:
|
||||||
|
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||||
|
if not geom:
|
||||||
|
break
|
||||||
|
results = bmesh.ops.bisect_plane(
|
||||||
|
bm,
|
||||||
|
geom=geom,
|
||||||
|
dist=dist,
|
||||||
|
plane_co=plane_co,
|
||||||
|
plane_no=plane_no,
|
||||||
|
clear_outer=True,
|
||||||
|
)
|
||||||
|
cut_edges = [e for e in results["geom_cut"] if isinstance(e, bmesh.types.BMEdge)]
|
||||||
|
if not cut_edges:
|
||||||
|
continue
|
||||||
|
# Group cut edges into connected components BEFORE filling.
|
||||||
|
# Feeding ``contextual_create`` all edges at once (outer +
|
||||||
|
# inner of a hollow profile) makes it create a SINGLE outer
|
||||||
|
# face and treat inner edges as decoration — collapsing the
|
||||||
|
# hole. Filling each connected loop separately produces one
|
||||||
|
# cap face per ring.
|
||||||
|
for loop_edges in cls._group_edges_into_loops(cut_edges):
|
||||||
|
try:
|
||||||
|
fill = bmesh.ops.contextual_create(bm, geom=loop_edges)
|
||||||
|
except (RuntimeError, TypeError):
|
||||||
|
continue
|
||||||
|
for f in fill.get("faces", []):
|
||||||
|
if isinstance(f, bmesh.types.BMFace) and f.is_valid:
|
||||||
|
f[cap_layer] = 1
|
||||||
|
|
||||||
|
if weld_dist > 0.0:
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=weld_dist)
|
||||||
|
return cap_layer
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_scale(cls, obj: bpy.types.Object) -> None:
|
def clear_scale(cls, obj: bpy.types.Object) -> None:
|
||||||
"""Apply and clear object scale.
|
"""Apply and clear object scale.
|
||||||
|
|||||||
Reference in New Issue
Block a user