Plane_Trimmer #2
1 changed files with 185 additions and 171 deletions
v11
commit
3bcf101083
|
|
@ -1,11 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Miter Planes — Trim at Intersection
|
Miter Planes — Trim at Intersection (v11)
|
||||||
======================================
|
============================================
|
||||||
Select 2+ mesh objects in Object Mode, then run this script.
|
Select 2+ mesh objects in Object Mode, then run this script.
|
||||||
|
|
||||||
Each original is cut at all intersection lines, then only the
|
Each plane's polygon is clipped by every other plane's infinite plane,
|
||||||
largest face is kept — like a miter/trim tool for planes.
|
keeping the side toward the global centroid. Uses Sutherland-Hodgman
|
||||||
Collinear vertices (both mid-edge and fold-back) are removed.
|
polygon clipping — no mesh.intersect, no temp objects, no face analysis.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
1. Select your plane objects in Object Mode
|
1. Select your plane objects in Object Mode
|
||||||
|
|
@ -18,51 +18,116 @@ from mathutils import Vector
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
def get_world_verts(obj):
|
# ─────────────────────────────────────────────
|
||||||
return [obj.matrix_world @ v.co for v in obj.data.vertices]
|
# Geometry helpers
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_world_verts_ordered(obj):
|
||||||
|
"""Get world-space vertices in face winding order (polygon[0])."""
|
||||||
|
mesh = obj.data
|
||||||
|
if len(mesh.polygons) == 0:
|
||||||
|
return [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||||
|
|
||||||
|
# If multiple faces (triangulated), dissolve first to get single polygon
|
||||||
|
if len(mesh.polygons) > 1:
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(mesh)
|
||||||
|
bmesh.ops.dissolve_limit(
|
||||||
|
bm,
|
||||||
|
angle_limit=math.radians(5.0),
|
||||||
|
use_dissolve_boundaries=False,
|
||||||
|
verts=bm.verts[:],
|
||||||
|
edges=bm.edges[:],
|
||||||
|
)
|
||||||
|
bm.faces.ensure_lookup_table()
|
||||||
|
if len(bm.faces) > 0:
|
||||||
|
face = bm.faces[0]
|
||||||
|
world_verts = [obj.matrix_world @ v.co for v in face.verts]
|
||||||
|
else:
|
||||||
|
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||||
|
bm.free()
|
||||||
|
return world_verts
|
||||||
|
|
||||||
|
face = mesh.polygons[0]
|
||||||
|
return [obj.matrix_world @ mesh.vertices[i].co for i in face.vertices]
|
||||||
|
|
||||||
|
|
||||||
def compute_plane(world_verts):
|
def compute_plane(world_verts):
|
||||||
|
"""Compute plane normal and point from vertices."""
|
||||||
if len(world_verts) < 3:
|
if len(world_verts) < 3:
|
||||||
return None, None
|
return None, None
|
||||||
v0, v1, v2 = world_verts[0], world_verts[1], world_verts[2]
|
for i in range(len(world_verts)):
|
||||||
normal = (v1 - v0).cross(v2 - v0)
|
for j in range(i + 1, len(world_verts)):
|
||||||
if normal.length < 1e-8:
|
for k in range(j + 1, len(world_verts)):
|
||||||
return None, None
|
normal = (world_verts[j] - world_verts[i]).cross(world_verts[k] - world_verts[i])
|
||||||
normal.normalize()
|
if normal.length > 1e-8:
|
||||||
return normal, v0
|
normal.normalize()
|
||||||
|
return normal, world_verts[i]
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def distance_to_plane(point, plane_normal, plane_point):
|
def distance_to_plane(point, plane_normal, plane_point):
|
||||||
|
"""Signed distance from point to plane."""
|
||||||
return (point - plane_point).dot(plane_normal)
|
return (point - plane_point).dot(plane_normal)
|
||||||
|
|
||||||
|
|
||||||
def bmesh_face_area(face):
|
def clip_polygon_by_plane(polygon_verts, plane_normal, plane_point, keep_side_point):
|
||||||
verts = [v.co for v in face.verts]
|
"""
|
||||||
if len(verts) < 3:
|
Sutherland-Hodgman: clip polygon, keeping the side that contains keep_side_point.
|
||||||
return 0.0
|
Returns clipped polygon vertices (list of Vector), or empty list if fully clipped away.
|
||||||
total = 0.0
|
"""
|
||||||
v0 = verts[0]
|
if len(polygon_verts) < 3:
|
||||||
for i in range(1, len(verts) - 1):
|
return polygon_verts
|
||||||
total += (verts[i] - v0).cross(verts[i + 1] - v0).length / 2.0
|
|
||||||
return total
|
# Which side to keep?
|
||||||
|
keep_dist = distance_to_plane(keep_side_point, plane_normal, plane_point)
|
||||||
|
if abs(keep_dist) < 1e-9:
|
||||||
|
# Centroid is ON the plane — can't determine side, skip this clip
|
||||||
|
return polygon_verts
|
||||||
|
|
||||||
|
clipped = []
|
||||||
|
n = len(polygon_verts)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
curr = polygon_verts[i]
|
||||||
|
next_v = polygon_verts[(i + 1) % n]
|
||||||
|
|
||||||
|
curr_dist = distance_to_plane(curr, plane_normal, plane_point)
|
||||||
|
next_dist = distance_to_plane(next_v, plane_normal, plane_point)
|
||||||
|
|
||||||
|
curr_inside = (curr_dist * keep_dist >= -1e-9) # same side as keep point (with tolerance)
|
||||||
|
next_inside = (next_dist * keep_dist >= -1e-9)
|
||||||
|
|
||||||
|
if curr_inside:
|
||||||
|
clipped.append(curr)
|
||||||
|
if not next_inside:
|
||||||
|
# Exiting — compute intersection point
|
||||||
|
denom = curr_dist - next_dist
|
||||||
|
if abs(denom) > 1e-12:
|
||||||
|
t = curr_dist / denom
|
||||||
|
intersection = curr.lerp(next_v, t)
|
||||||
|
clipped.append(intersection)
|
||||||
|
else:
|
||||||
|
if next_inside:
|
||||||
|
# Entering — compute intersection point
|
||||||
|
denom = curr_dist - next_dist
|
||||||
|
if abs(denom) > 1e-12:
|
||||||
|
t = curr_dist / denom
|
||||||
|
intersection = curr.lerp(next_v, t)
|
||||||
|
clipped.append(intersection)
|
||||||
|
|
||||||
|
return clipped
|
||||||
|
|
||||||
|
|
||||||
def remove_collinear_verts(verts, tolerance_degrees=1.0):
|
def remove_collinear_verts(verts, tolerance_degrees=1.0):
|
||||||
"""
|
"""Remove collinear vertices — catches both mid-edge (≈180°) and fold-back (≈0°)."""
|
||||||
Remove collinear vertices from an ordered polygon vert list.
|
|
||||||
Catches TWO cases:
|
|
||||||
- angle ≈ 180°: vert is between its neighbors on a line (mid-edge point)
|
|
||||||
- angle ≈ 0°: vert is a fold-back where both edges go same direction
|
|
||||||
Both mean the 3 consecutive verts are collinear.
|
|
||||||
"""
|
|
||||||
if len(verts) < 3:
|
if len(verts) < 3:
|
||||||
return verts
|
return verts
|
||||||
|
|
||||||
tolerance_rad = math.radians(tolerance_degrees)
|
tolerance_rad = math.radians(tolerance_degrees)
|
||||||
cleaned = []
|
cleaned = []
|
||||||
|
|
||||||
n = len(verts)
|
n = len(verts)
|
||||||
|
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
prev_v = verts[(i - 1) % n]
|
prev_v = verts[(i - 1) % n]
|
||||||
curr_v = verts[i]
|
curr_v = verts[i]
|
||||||
|
|
@ -72,52 +137,62 @@ def remove_collinear_verts(verts, tolerance_degrees=1.0):
|
||||||
edge_out = next_v - curr_v
|
edge_out = next_v - curr_v
|
||||||
|
|
||||||
if edge_in.length < 1e-8 or edge_out.length < 1e-8:
|
if edge_in.length < 1e-8 or edge_out.length < 1e-8:
|
||||||
# Degenerate (duplicate vert), skip
|
continue # duplicate vertex
|
||||||
print(f" vert {i}: DEGENERATE (zero-length edge), removing")
|
|
||||||
continue
|
|
||||||
|
|
||||||
edge_in_n = edge_in.normalized()
|
dot = max(-1.0, min(1.0, edge_in.normalized().dot(edge_out.normalized())))
|
||||||
edge_out_n = edge_out.normalized()
|
|
||||||
|
|
||||||
dot = max(-1.0, min(1.0, edge_in_n.dot(edge_out_n)))
|
|
||||||
angle = math.acos(dot)
|
angle = math.acos(dot)
|
||||||
angle_deg = math.degrees(angle)
|
|
||||||
|
|
||||||
# Collinear if angle is near 0° (fold-back) or near 180° (mid-edge)
|
|
||||||
deviation = min(angle, abs(math.pi - angle))
|
deviation = min(angle, abs(math.pi - angle))
|
||||||
deviation_deg = math.degrees(deviation)
|
|
||||||
|
|
||||||
is_collinear = deviation < tolerance_rad
|
if deviation < tolerance_rad:
|
||||||
|
continue # collinear
|
||||||
print(f" vert {i}: angle={angle_deg:.2f}°, deviation from straight={deviation_deg:.2f}°, collinear={is_collinear}")
|
|
||||||
|
|
||||||
if is_collinear:
|
|
||||||
continue
|
|
||||||
else:
|
else:
|
||||||
cleaned.append(curr_v)
|
cleaned.append(curr_v)
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
def remove_duplicate_verts(verts, tolerance=1e-6):
|
||||||
|
"""Remove consecutive duplicate vertices."""
|
||||||
|
if len(verts) < 2:
|
||||||
|
return verts
|
||||||
|
cleaned = [verts[0]]
|
||||||
|
for i in range(1, len(verts)):
|
||||||
|
if (verts[i] - cleaned[-1]).length > tolerance:
|
||||||
|
cleaned.append(verts[i])
|
||||||
|
# Check last vs first
|
||||||
|
if len(cleaned) > 1 and (cleaned[-1] - cleaned[0]).length <= tolerance:
|
||||||
|
cleaned.pop()
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Logging
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
def log_object_verts(label, obj):
|
def log_object_verts(label, obj):
|
||||||
local_verts = [v.co.copy() for v in obj.data.vertices]
|
mesh = obj.data
|
||||||
world_verts = get_world_verts(obj)
|
local_verts = [v.co.copy() for v in mesh.vertices]
|
||||||
|
world_verts = [obj.matrix_world @ v.co for v in mesh.vertices]
|
||||||
print(f"\n [{label}] '{obj.name}'")
|
print(f"\n [{label}] '{obj.name}'")
|
||||||
print(f" Location: {obj.location}")
|
print(f" Location: {obj.location}")
|
||||||
print(f" Rotation: {obj.rotation_euler}")
|
print(f" Rotation: {obj.rotation_euler}")
|
||||||
print(f" Verts: {len(local_verts)} | Edges: {len(obj.data.edges)} | Faces: {len(obj.data.polygons)}")
|
print(f" Verts: {len(local_verts)} | Edges: {len(mesh.edges)} | Faces: {len(mesh.polygons)}")
|
||||||
print(f" {'idx':<5} {'LOCAL (x, y, z)':<35} {'WORLD (x, y, z)':<35}")
|
print(f" {'idx':<5} {'LOCAL (x, y, z)':<35} {'WORLD (x, y, z)':<35}")
|
||||||
print(f" {'---':<5} {'---------------':<35} {'---------------':<35}")
|
print(f" {'---':<5} {'---------------':<35} {'---------------':<35}")
|
||||||
for i, (lv, wv) in enumerate(zip(local_verts, world_verts)):
|
for i, (lv, wv) in enumerate(zip(local_verts, world_verts)):
|
||||||
print(f" {i:<5} ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f}) ({wv.x:9.4f}, {wv.y:9.4f}, {wv.z:9.4f})")
|
print(f" {i:<5} ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f}) ({wv.x:9.4f}, {wv.y:9.4f}, {wv.z:9.4f})")
|
||||||
print(f" Faces:")
|
print(f" Faces:")
|
||||||
for i, poly in enumerate(obj.data.polygons):
|
for i, poly in enumerate(mesh.polygons):
|
||||||
print(f" face[{i}] verts: {list(poly.vertices)} normal: ({poly.normal.x:.4f}, {poly.normal.y:.4f}, {poly.normal.z:.4f})")
|
print(f" face[{i}] verts: {list(poly.vertices)} normal: ({poly.normal.x:.4f}, {poly.normal.y:.4f}, {poly.normal.z:.4f})")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
# Main
|
||||||
|
# ─────────────────────────────────────────────
|
||||||
|
|
||||||
def miter_planes():
|
def miter_planes():
|
||||||
print("\n" + "=" * 70)
|
print("\n" + "=" * 70)
|
||||||
print("MITER PLANES — TRIM AT INTERSECTION")
|
print("MITER PLANES — TRIM AT INTERSECTION (v11 — Sutherland-Hodgman)")
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|
||||||
# --- Validate selection ---
|
# --- Validate selection ---
|
||||||
|
|
@ -138,182 +213,121 @@ def miter_planes():
|
||||||
log_object_verts("BEFORE", obj)
|
log_object_verts("BEFORE", obj)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# STEP 1: Store original data + plane equations
|
# STEP 1: Gather data + compute centroid
|
||||||
# =========================================================
|
# =========================================================
|
||||||
print(f"\n[STEP 1] Storing original data...")
|
print(f"\n[STEP 1] Storing original data...")
|
||||||
orig_data = {}
|
orig_data = {}
|
||||||
|
all_world_verts = []
|
||||||
|
|
||||||
for obj in selected:
|
for obj in selected:
|
||||||
wv = get_world_verts(obj)
|
wv_ordered = get_world_verts_ordered(obj)
|
||||||
plane_normal, plane_point = compute_plane(wv)
|
all_world_verts.extend(wv_ordered)
|
||||||
|
plane_normal, plane_point = compute_plane(wv_ordered)
|
||||||
orig_data[obj.name] = {
|
orig_data[obj.name] = {
|
||||||
'object': obj,
|
'object': obj,
|
||||||
'world_verts': wv,
|
'world_verts': wv_ordered,
|
||||||
'matrix_world': obj.matrix_world.copy(),
|
|
||||||
'matrix_world_inv': obj.matrix_world.inverted(),
|
'matrix_world_inv': obj.matrix_world.inverted(),
|
||||||
'plane_normal': plane_normal,
|
'plane_normal': plane_normal,
|
||||||
'plane_point': plane_point,
|
'plane_point': plane_point,
|
||||||
}
|
}
|
||||||
if plane_normal:
|
if plane_normal:
|
||||||
print(f" '{obj.name}' plane: normal=({plane_normal.x:.4f}, {plane_normal.y:.4f}, {plane_normal.z:.4f})")
|
print(f" '{obj.name}' plane: normal=({plane_normal.x:.4f}, {plane_normal.y:.4f}, {plane_normal.z:.4f})")
|
||||||
|
print(f" '{obj.name}' polygon ({len(wv_ordered)} verts):")
|
||||||
|
for i, v in enumerate(wv_ordered):
|
||||||
|
print(f" {i}: ({v.x:.4f}, {v.y:.4f}, {v.z:.4f})")
|
||||||
|
|
||||||
|
centroid = sum(all_world_verts, Vector((0, 0, 0))) / len(all_world_verts)
|
||||||
|
print(f" Global centroid: ({centroid.x:.4f}, {centroid.y:.4f}, {centroid.z:.4f})")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# STEP 2: Duplicate, apply transforms, join, intersect
|
# STEP 2: Clip each polygon by all other planes
|
||||||
# =========================================================
|
# =========================================================
|
||||||
print(f"\n[STEP 2] Duplicating, joining, intersecting...")
|
print(f"\n[STEP 2] Clipping polygons...")
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
clipped_polys = {}
|
||||||
duplicates = []
|
|
||||||
for obj in selected:
|
|
||||||
new_mesh = obj.data.copy()
|
|
||||||
new_obj = obj.copy()
|
|
||||||
new_obj.data = new_mesh
|
|
||||||
bpy.context.collection.objects.link(new_obj)
|
|
||||||
new_obj.name = f"_temp_{obj.name}"
|
|
||||||
duplicates.append(new_obj)
|
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
for name_a, data_a in orig_data.items():
|
||||||
for dup in duplicates:
|
polygon = list(data_a['world_verts'])
|
||||||
dup.select_set(True)
|
print(f"\n Clipping '{name_a}' (starting with {len(polygon)} verts):")
|
||||||
bpy.context.view_layer.objects.active = duplicates[0]
|
|
||||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
|
||||||
|
|
||||||
bpy.ops.object.join()
|
for name_b, data_b in orig_data.items():
|
||||||
temp_joined = bpy.context.active_object
|
if name_b == name_a:
|
||||||
print(f" Joined → {len(temp_joined.data.vertices)} verts, {len(temp_joined.data.polygons)} faces")
|
continue
|
||||||
|
|
||||||
bpy.ops.object.mode_set(mode='EDIT')
|
pn = data_b['plane_normal']
|
||||||
bpy.ops.mesh.select_all(action='SELECT')
|
pp = data_b['plane_point']
|
||||||
result = bpy.ops.mesh.intersect(mode='SELECT', separate_mode='ALL', threshold=1e-06)
|
|
||||||
print(f" intersect result: {result}")
|
|
||||||
|
|
||||||
# =========================================================
|
|
||||||
# STEP 3: Iterate faces in bmesh — assign and find largest
|
|
||||||
# =========================================================
|
|
||||||
print(f"\n[STEP 3] Analyzing faces...")
|
|
||||||
bm = bmesh.from_edit_mesh(temp_joined.data)
|
|
||||||
bm.verts.ensure_lookup_table()
|
|
||||||
bm.faces.ensure_lookup_table()
|
|
||||||
|
|
||||||
print(f" Total faces after intersect: {len(bm.faces)}")
|
|
||||||
|
|
||||||
face_data = {name: [] for name in orig_data}
|
|
||||||
|
|
||||||
for fi, face in enumerate(bm.faces):
|
|
||||||
face_verts_co = [v.co.copy() for v in face.verts]
|
|
||||||
area = bmesh_face_area(face)
|
|
||||||
|
|
||||||
matches = []
|
|
||||||
for name, data in orig_data.items():
|
|
||||||
pn, pp = data['plane_normal'], data['plane_point']
|
|
||||||
if pn is None:
|
if pn is None:
|
||||||
continue
|
continue
|
||||||
distances = [abs(distance_to_plane(v, pn, pp)) for v in face_verts_co]
|
|
||||||
max_dist = max(distances)
|
|
||||||
avg_dist = sum(distances) / len(distances)
|
|
||||||
is_coplanar = max_dist <= 0.001
|
|
||||||
print(f" face[{fi}] area={area:.6f} vs '{name}': max_dist={max_dist:.6f}, coplanar={is_coplanar}")
|
|
||||||
if is_coplanar:
|
|
||||||
matches.append((name, avg_dist))
|
|
||||||
|
|
||||||
if len(matches) == 1:
|
before_count = len(polygon)
|
||||||
face_data[matches[0][0]].append((fi, area, face_verts_co))
|
polygon = clip_polygon_by_plane(polygon, pn, pp, centroid)
|
||||||
elif len(matches) > 1:
|
polygon = remove_duplicate_verts(polygon)
|
||||||
best = min(matches, key=lambda x: x[1])
|
print(f" vs '{name_b}': {before_count} → {len(polygon)} verts")
|
||||||
face_data[best[0]].append((fi, area, face_verts_co))
|
|
||||||
|
if len(polygon) < 3:
|
||||||
|
print(f" WARNING: polygon degenerate after clipping by '{name_b}'")
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(polygon) >= 3:
|
||||||
|
print(f" '{name_a}' clipped polygon ({len(polygon)} verts):")
|
||||||
|
for i, v in enumerate(polygon):
|
||||||
|
print(f" {i}: ({v.x:.4f}, {v.y:.4f}, {v.z:.4f})")
|
||||||
|
clipped_polys[name_a] = polygon
|
||||||
else:
|
else:
|
||||||
best_name = None
|
print(f" '{name_a}': fully clipped away — skipping")
|
||||||
best_avg = float('inf')
|
|
||||||
for name, data in orig_data.items():
|
|
||||||
pn, pp = data['plane_normal'], data['plane_point']
|
|
||||||
if pn is None:
|
|
||||||
continue
|
|
||||||
avg = sum(abs(distance_to_plane(v, pn, pp)) for v in face_verts_co) / len(face_verts_co)
|
|
||||||
if avg < best_avg:
|
|
||||||
best_avg = avg
|
|
||||||
best_name = name
|
|
||||||
if best_name:
|
|
||||||
face_data[best_name].append((fi, area, face_verts_co))
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# STEP 4: Keep only the largest face per original
|
# STEP 3: Rebuild meshes
|
||||||
# =========================================================
|
# =========================================================
|
||||||
print(f"\n[STEP 4] Selecting largest face per original...")
|
print(f"\n[STEP 3] Rebuilding original meshes...")
|
||||||
winning_faces = {}
|
|
||||||
|
|
||||||
for name, faces in face_data.items():
|
|
||||||
if not faces:
|
|
||||||
print(f" '{name}': no faces — skipping")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f" '{name}' has {len(faces)} faces:")
|
|
||||||
for fi, area, verts in faces:
|
|
||||||
print(f" face[{fi}]: area={area:.6f}")
|
|
||||||
|
|
||||||
largest = max(faces, key=lambda x: x[1])
|
|
||||||
print(f" → keeping face[{largest[0]}] (area: {largest[1]:.6f})")
|
|
||||||
winning_faces[name] = largest[2]
|
|
||||||
|
|
||||||
bm.free()
|
|
||||||
bpy.ops.object.mode_set(mode='OBJECT')
|
|
||||||
|
|
||||||
# =========================================================
|
|
||||||
# STEP 5: Rebuild each original, removing collinear verts
|
|
||||||
# =========================================================
|
|
||||||
print(f"\n[STEP 5] Rebuilding original meshes...")
|
|
||||||
|
|
||||||
for name, data in orig_data.items():
|
for name, data in orig_data.items():
|
||||||
if name not in winning_faces:
|
if name not in clipped_polys:
|
||||||
print(f" '{name}': no winning face — skipping")
|
print(f" '{name}': no clipped polygon — skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
orig_obj = data['object']
|
orig_obj = data['object']
|
||||||
mat_inv = data['matrix_world_inv']
|
mat_inv = data['matrix_world_inv']
|
||||||
world_verts = winning_faces[name]
|
world_verts = clipped_polys[name]
|
||||||
local_verts = [mat_inv @ wv for wv in world_verts]
|
local_verts = [mat_inv @ wv for wv in world_verts]
|
||||||
|
|
||||||
print(f"\n '{name}' — raw verts ({len(local_verts)}):")
|
print(f"\n '{name}' — raw verts ({len(local_verts)}):")
|
||||||
for i, (wv, lv) in enumerate(zip(world_verts, local_verts)):
|
for i, (wv, lv) in enumerate(zip(world_verts, local_verts)):
|
||||||
print(f" {i:<3} world: ({wv.x:9.4f}, {wv.y:9.4f}, {wv.z:9.4f}) local: ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f})")
|
print(f" {i:<3} world: ({wv.x:9.4f}, {wv.y:9.4f}, {wv.z:9.4f}) local: ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f})")
|
||||||
|
|
||||||
# Remove collinear verts (catches both mid-edge and fold-back)
|
# Clean up
|
||||||
print(f" Collinear check:")
|
cleaned = remove_collinear_verts(local_verts, tolerance_degrees=1.0)
|
||||||
cleaned_local = remove_collinear_verts(local_verts, tolerance_degrees=1.0)
|
cleaned = remove_duplicate_verts(cleaned)
|
||||||
|
|
||||||
removed = len(local_verts) - len(cleaned_local)
|
removed = len(local_verts) - len(cleaned)
|
||||||
print(f" '{name}' — cleaned: {len(local_verts)} → {len(cleaned_local)} verts (removed {removed})")
|
print(f" '{name}' — cleaned: {len(local_verts)} → {len(cleaned)} verts (removed {removed})")
|
||||||
for i, lv in enumerate(cleaned_local):
|
for i, lv in enumerate(cleaned):
|
||||||
print(f" {i:<3} local: ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f})")
|
print(f" {i:<3} local: ({lv.x:9.4f}, {lv.y:9.4f}, {lv.z:9.4f})")
|
||||||
|
|
||||||
if len(cleaned_local) < 3:
|
if len(cleaned) < 3:
|
||||||
print(f" WARNING: fewer than 3 verts after cleanup — skipping")
|
print(f" WARNING: fewer than 3 verts after cleanup — skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build mesh
|
# Rebuild mesh
|
||||||
bm = bmesh.new()
|
bm_out = bmesh.new()
|
||||||
bm_verts = [bm.verts.new(v) for v in cleaned_local]
|
bm_verts = [bm_out.verts.new(v) for v in cleaned]
|
||||||
bm.verts.ensure_lookup_table()
|
bm_out.verts.ensure_lookup_table()
|
||||||
try:
|
try:
|
||||||
bm.faces.new(bm_verts)
|
bm_out.faces.new(bm_verts)
|
||||||
print(f" Face created OK")
|
print(f" Face created OK")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f" Face creation FAILED: {e}")
|
print(f" Face creation FAILED: {e}")
|
||||||
bm.free()
|
bm_out.free()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
bm.to_mesh(orig_obj.data)
|
bm_out.to_mesh(orig_obj.data)
|
||||||
bm.free()
|
bm_out.free()
|
||||||
orig_obj.data.update()
|
orig_obj.data.update()
|
||||||
|
|
||||||
print(f" '{name}': → {len(orig_obj.data.vertices)} verts, {len(orig_obj.data.edges)} edges, {len(orig_obj.data.polygons)} faces")
|
print(f" '{name}': → {len(orig_obj.data.vertices)} verts, {len(orig_obj.data.edges)} edges, {len(orig_obj.data.polygons)} faces")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# STEP 6: Delete temp object
|
# Reselect originals
|
||||||
# =========================================================
|
# =========================================================
|
||||||
print(f"\n[STEP 6] Cleaning up...")
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
|
||||||
temp_joined.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = temp_joined
|
|
||||||
bpy.ops.object.delete()
|
|
||||||
|
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
for name, data in orig_data.items():
|
for name, data in orig_data.items():
|
||||||
data['object'].select_set(True)
|
data['object'].select_set(True)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue