#Extract Mesh Faces By ClosedPolyline
import Rhino
import rhinoscriptsyntax as rs
import scriptcontext as sc
import math
from collections import deque
# =========================
# SETTINGS
# =========================
SAMPLES_PER_SEGMENT = 12
EDGE_TOL_FACTOR = 2.0
INCLUDE_BOUNDARY = True # True = output inside + boundary, False = inside only
# =========================
# GEOMETRY HELPERS
# =========================
def point_segment_distance(pt, a, b):
ax, ay, az = a.X, a.Y, a.Z
bx, by, bz = b.X, b.Y, b.Z
px, py, pz = pt.X, pt.Y, pt.Z
abx = bx - ax
aby = by - ay
abz = bz - az
apx = px - ax
apy = py - ay
apz = pz - az
ab_len_sq = abx * abx + aby * aby + abz * abz
if ab_len_sq <= 1e-20:
dx = px - ax
dy = py - ay
dz = pz - az
return math.sqrt(dx * dx + dy * dy + dz * dz)
t = (apx * abx + apy * aby + apz * abz) / ab_len_sq
if t < 0.0:
t = 0.0
elif t > 1.0:
t = 1.0
cx = ax + t * abx
cy = ay + t * aby
cz = az + t * abz
dx = px - cx
dy = py - cy
dz = pz - cz
return math.sqrt(dx * dx + dy * dy + dz * dz)
def face_center(mesh, fi):
f = mesh.Faces[fi]
if f.IsTriangle:
ids = [f.A, f.B, f.C]
else:
ids = [f.A, f.B, f.C, f.D]
sx = sy = sz = 0.0
for vid in ids:
v = mesh.Vertices[vid]
sx += v.X
sy += v.Y
sz += v.Z
n = float(len(ids))
return Rhino.Geometry.Point3d(sx / n, sy / n, sz / n)
# =========================
# PREVIEW / OUTPUT
# =========================
def add_faces_to_new_mesh(mesh, face_ids):
out_mesh = Rhino.Geometry.Mesh()
vmap = {}
for fi in face_ids:
f = mesh.Faces[fi]
if f.IsTriangle:
src = [f.A, f.B, f.C]
else:
src = [f.A, f.B, f.C, f.D]
dst = []
for vid in src:
if vid not in vmap:
v = mesh.Vertices[vid]
new_id = out_mesh.Vertices.Add(v.X, v.Y, v.Z)
vmap[vid] = new_id
dst.append(vmap[vid])
if f.IsTriangle:
out_mesh.Faces.AddFace(dst[0], dst[1], dst[2])
else:
out_mesh.Faces.AddFace(dst[0], dst[1], dst[2], dst[3])
out_mesh.Normals.ComputeNormals()
out_mesh.Compact()
return out_mesh
# =========================
# STEP 1: BOUNDARY FACES
# =========================
def collect_faces_from_point_on_edge(mesh, face_index, pt, tol):
result = set([face_index])
top_edges = mesh.TopologyEdges
face_edge_ids = top_edges.GetEdgesForFace(face_index)
if not face_edge_ids:
return result
for ei in face_edge_ids:
tv = top_edges.GetTopologyVertices(ei)
a = mesh.TopologyVertices[tv.I]
b = mesh.TopologyVertices[tv.J]
d = point_segment_distance(pt, a, b)
if d <= tol:
connected = top_edges.GetConnectedFaces(ei)
for fi in connected:
result.add(fi)
return result
def get_boundary_faces_by_polyline_sampling(mesh, curve, samples_per_segment=10, edge_tol=None):
ok, pl = curve.TryGetPolyline()
if not ok:
return None, "Curve must be a polyline."
pts = list(pl)
if len(pts) < 2:
return None, "Invalid polyline."
if edge_tol is None:
edge_tol = sc.doc.ModelAbsoluteTolerance * 2.0
closed = pts[0].DistanceTo(pts[-1]) <= sc.doc.ModelAbsoluteTolerance
if not closed:
return None, "Polyline must be closed."
face_ids = set()
count = len(pts) - 1
for i in range(count):
a = pts[i]
b = pts[i + 1]
for j in range(samples_per_segment + 1):
t = float(j) / float(samples_per_segment)
p = Rhino.Geometry.Point3d(
a.X + (b.X - a.X) * t,
a.Y + (b.Y - a.Y) * t,
a.Z + (b.Z - a.Z) * t
)
mp = mesh.ClosestMeshPoint(p, 0.0)
if not mp:
continue
found = collect_faces_from_point_on_edge(mesh, mp.FaceIndex, p, edge_tol)
face_ids.update(found)
return sorted(face_ids), None
# =========================
# STEP 2: FACE ADJACENCY
# =========================
def build_face_adjacency(mesh):
face_count = mesh.Faces.Count
adjacency = [set() for _ in range(face_count)]
top_edges = mesh.TopologyEdges
for ei in range(top_edges.Count):
connected = top_edges.GetConnectedFaces(ei)
if connected and len(connected) >= 2:
for i in range(len(connected)):
for j in range(i + 1, len(connected)):
a = connected[i]
b = connected[j]
adjacency[a].add(b)
adjacency[b].add(a)
return adjacency
def connected_components_excluding_boundary(mesh, boundary_faces):
adjacency = build_face_adjacency(mesh)
face_count = mesh.Faces.Count
boundary = set(boundary_faces)
visited = set()
components = []
for start in range(face_count):
if start in visited or start in boundary:
continue
comp = []
q = deque([start])
visited.add(start)
while q:
f = q.popleft()
comp.append(f)
for nb in adjacency[f]:
if nb in visited or nb in boundary:
continue
visited.add(nb)
q.append(nb)
components.append(comp)
return components, adjacency
# =========================
# STEP 3: CHOOSE INSIDE
# =========================
def choose_inside_component(mesh, boundary_faces, components, adjacency):
"""
Heuristic:
pick the smallest component that touches the boundary.
"""
boundary = set(boundary_faces)
touching = []
for comp in components:
comp_set = set(comp)
touches = False
for fi in comp:
for nb in adjacency[fi]:
if nb in boundary:
touches = True
break
if touches:
break
if touches:
touching.append(comp)
if not touching:
return None, "No inside region found."
touching.sort(key=len)
return touching[0], None
# =========================
# MAIN
# =========================
def main():
tol = sc.doc.ModelAbsoluteTolerance
mesh_id = rs.GetObject("Select mesh", rs.filter.mesh, preselect=True)
if not mesh_id:
return
crv_id = rs.GetObject("Select closed polyline on mesh", rs.filter.curve, preselect=True)
if not crv_id:
return
mesh = rs.coercemesh(mesh_id)
curve = rs.coercecurve(crv_id)
if not mesh:
print("Invalid mesh.")
return
if not curve:
print("Invalid curve.")
return
if not curve.IsClosed:
print("Curve must be closed.")
return
boundary_faces, err = get_boundary_faces_by_polyline_sampling(
mesh,
curve,
samples_per_segment=SAMPLES_PER_SEGMENT,
edge_tol=tol * EDGE_TOL_FACTOR
)
if err:
print(err)
return
if not boundary_faces:
print("No boundary faces found.")
return
components, adjacency = connected_components_excluding_boundary(mesh, boundary_faces)
inside_faces, err = choose_inside_component(mesh, boundary_faces, components, adjacency)
if err:
print(err)
return
if INCLUDE_BOUNDARY:
final_faces = sorted(set(boundary_faces).union(set(inside_faces)))
else:
final_faces = sorted(set(inside_faces))
out_mesh = add_faces_to_new_mesh(mesh, final_faces)
sc.doc.Objects.AddMesh(out_mesh)
sc.doc.Views.Redraw()
print("Boundary faces: {}".format(len(boundary_faces)))
print("Inside faces: {}".format(len(inside_faces)))
print("Output faces: {}".format(len(final_faces)))
if __name__ == "__main__":
main()