mesh_outline_projection #1
1 changed files with 234 additions and 75 deletions
|
|
@ -14,6 +14,61 @@ from mathutils import Vector
|
|||
from mathutils.bvhtree import BVHTree
|
||||
|
||||
|
||||
def point_in_polygon_2d(point, polygon_verts):
|
||||
"""Check if a 2D point is inside a 2D polygon using ray casting algorithm"""
|
||||
x, y = point[0], point[1]
|
||||
n = len(polygon_verts)
|
||||
inside = False
|
||||
|
||||
p1x, p1y = polygon_verts[0]
|
||||
for i in range(1, n + 1):
|
||||
p2x, p2y = polygon_verts[i % n]
|
||||
if y > min(p1y, p2y):
|
||||
if y <= max(p1y, p2y):
|
||||
if x <= max(p1x, p2x):
|
||||
if p1y != p2y:
|
||||
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
||||
if p1x == p2x or x <= xinters:
|
||||
inside = not inside
|
||||
p1x, p1y = p2x, p2y
|
||||
|
||||
return inside
|
||||
|
||||
|
||||
def point_near_polygon_edge(point, polygon_verts, tolerance=0.05):
|
||||
"""Check if a point is very close to any polygon edge"""
|
||||
x, y = point[0], point[1]
|
||||
n = len(polygon_verts)
|
||||
|
||||
for i in range(n):
|
||||
p1x, p1y = polygon_verts[i]
|
||||
p2x, p2y = polygon_verts[(i + 1) % n]
|
||||
|
||||
# Calculate distance from point to line segment
|
||||
# Vector from p1 to p2
|
||||
dx = p2x - p1x
|
||||
dy = p2y - p1y
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
# Degenerate edge
|
||||
dist = ((x - p1x)**2 + (y - p1y)**2)**0.5
|
||||
else:
|
||||
# Parameter t for closest point on line segment
|
||||
t = max(0, min(1, ((x - p1x) * dx + (y - p1y) * dy) / (dx * dx + dy * dy)))
|
||||
|
||||
# Closest point on line segment
|
||||
closest_x = p1x + t * dx
|
||||
closest_y = p1y + t * dy
|
||||
|
||||
# Distance to closest point
|
||||
dist = ((x - closest_x)**2 + (y - closest_y)**2)**0.5
|
||||
|
||||
if dist < tolerance:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_object_outline_edges_2d(obj, context):
|
||||
"""Get the outline edges of an object when viewed from above (Z-axis)"""
|
||||
print(f"\n=== Getting outline edges for: {obj.name} ===")
|
||||
|
|
@ -232,94 +287,157 @@ def project_outline_to_mesh(context, source_obj, target_obj, outline_edges):
|
|||
bm.faces.ensure_lookup_table()
|
||||
bm.edges.ensure_lookup_table()
|
||||
|
||||
print(f" Creating vertical cutting planes from outline edges...")
|
||||
print(f" Creating outline curve for knife projection...")
|
||||
|
||||
# Get transformation matrix to convert from world to target local space
|
||||
target_matrix_inv = target_obj.matrix_world.inverted()
|
||||
|
||||
# Identify horizontal edges (those in the XY plane that form the perimeter)
|
||||
# These are edges where both vertices have similar Z values
|
||||
# Get horizontal perimeter edges
|
||||
horizontal_edges = []
|
||||
for v1, v2 in outline_edges:
|
||||
z_diff = abs(v1.z - v2.z)
|
||||
xy_dist = ((v1.x - v2.x)**2 + (v1.y - v2.y)**2)**0.5
|
||||
|
||||
# If edge is mostly horizontal (small Z difference, significant XY distance)
|
||||
if z_diff < 0.01 and xy_dist > 0.01:
|
||||
horizontal_edges.append((v1, v2))
|
||||
|
||||
print(f" Found {len(horizontal_edges)} horizontal perimeter edges")
|
||||
print(f" Found {len(horizontal_edges)} horizontal perimeter edges (before deduplication)")
|
||||
|
||||
# For each horizontal edge, create a vertical cutting plane
|
||||
cuts_performed = 0
|
||||
for i, (v1_world, v2_world) in enumerate(horizontal_edges):
|
||||
# Transform edge vertices to target's local space
|
||||
v1_local = target_matrix_inv @ v1_world
|
||||
v2_local = target_matrix_inv @ v2_world
|
||||
# Deduplicate edges
|
||||
unique_edges_normalized = []
|
||||
tolerance = 0.01
|
||||
|
||||
# Edge vector in XY (local space)
|
||||
edge_vec = Vector((v2_local.x - v1_local.x, v2_local.y - v1_local.y, 0))
|
||||
edge_vec.normalize()
|
||||
for v1, v2 in horizontal_edges:
|
||||
if v1.x < v2.x or (abs(v1.x - v2.x) < tolerance and v1.y < v2.y):
|
||||
edge_normalized = (v1, v2)
|
||||
else:
|
||||
edge_normalized = (v2, v1)
|
||||
|
||||
# Plane normal perpendicular to edge (in XY, pointing inward/outward)
|
||||
plane_normal = Vector((-edge_vec.y, edge_vec.x, 0))
|
||||
plane_normal.normalize()
|
||||
is_duplicate = False
|
||||
for existing_v1, existing_v2 in unique_edges_normalized:
|
||||
if (abs(edge_normalized[0].x - existing_v1.x) < tolerance and
|
||||
abs(edge_normalized[0].y - existing_v1.y) < tolerance and
|
||||
abs(edge_normalized[1].x - existing_v2.x) < tolerance and
|
||||
abs(edge_normalized[1].y - existing_v2.y) < tolerance):
|
||||
is_duplicate = True
|
||||
break
|
||||
|
||||
# Plane point (use v1 in local space)
|
||||
plane_co = v1_local.copy()
|
||||
if not is_duplicate:
|
||||
unique_edges_normalized.append(edge_normalized)
|
||||
|
||||
if i < 3:
|
||||
print(f" Cut {i}: plane at ({plane_co.x:.2f}, {plane_co.y:.2f}) [local], normal ({plane_normal.x:.2f}, {plane_normal.y:.2f})")
|
||||
print(f" Deduplicated to {len(unique_edges_normalized)} unique edges")
|
||||
|
||||
# Perform bisect
|
||||
geom = bm.verts[:] + bm.edges[:] + bm.faces[:]
|
||||
result = bmesh.ops.bisect_plane(
|
||||
bm,
|
||||
geom=geom,
|
||||
dist=0.001,
|
||||
plane_co=plane_co,
|
||||
plane_no=plane_normal,
|
||||
clear_outer=False,
|
||||
clear_inner=False
|
||||
)
|
||||
cuts_performed += 1
|
||||
# Build ordered polygon from edges for point-in-polygon testing
|
||||
# Start with first edge
|
||||
polygon_verts_2d = [unique_edges_normalized[0][0], unique_edges_normalized[0][1]]
|
||||
used_edges = {0}
|
||||
|
||||
print(f" Performed {cuts_performed} cutting plane operations")
|
||||
# Connect edges to form a closed loop
|
||||
for _ in range(len(unique_edges_normalized) - 1):
|
||||
last_point = polygon_verts_2d[-1]
|
||||
|
||||
bmesh.update_edit_mesh(target_obj.data)
|
||||
# Find next connected edge
|
||||
for i, (v1, v2) in enumerate(unique_edges_normalized):
|
||||
if i in used_edges:
|
||||
continue
|
||||
|
||||
# Refresh bmesh references after cuts
|
||||
# Check if this edge connects to our last point
|
||||
v1_2d = (v1.x, v1.y)
|
||||
v2_2d = (v2.x, v2.y)
|
||||
last_2d = (last_point.x, last_point.y)
|
||||
|
||||
tolerance = 0.01
|
||||
if abs(v1_2d[0] - last_2d[0]) < tolerance and abs(v1_2d[1] - last_2d[1]) < tolerance:
|
||||
polygon_verts_2d.append(v2)
|
||||
used_edges.add(i)
|
||||
break
|
||||
elif abs(v2_2d[0] - last_2d[0]) < tolerance and abs(v2_2d[1] - last_2d[1]) < tolerance:
|
||||
polygon_verts_2d.append(v1)
|
||||
used_edges.add(i)
|
||||
break
|
||||
|
||||
# Remove duplicate endpoint if polygon is closed
|
||||
if len(polygon_verts_2d) > 0:
|
||||
first = polygon_verts_2d[0]
|
||||
last = polygon_verts_2d[-1]
|
||||
if abs(first.x - last.x) < 0.01 and abs(first.y - last.y) < 0.01:
|
||||
polygon_verts_2d = polygon_verts_2d[:-1]
|
||||
|
||||
# Convert to 2D coordinates in world space for polygon testing
|
||||
polygon_2d_world = [(v.x, v.y) for v in polygon_verts_2d]
|
||||
|
||||
print(f" Built polygon with {len(polygon_2d_world)} vertices for point-in-polygon testing")
|
||||
print(f" Polygon vertices (world space):")
|
||||
for i, (x, y) in enumerate(polygon_2d_world):
|
||||
print(f" Vertex {i}: ({x:.2f}, {y:.2f})")
|
||||
|
||||
# Create curve object from unique edges
|
||||
curve_data = bpy.data.curves.new(name=f"{source_obj.name}_outline", type='CURVE')
|
||||
curve_data.dimensions = '3D'
|
||||
|
||||
for v1, v2 in unique_edges_normalized:
|
||||
spline = curve_data.splines.new('POLY')
|
||||
spline.points.add(1)
|
||||
spline.points[0].co = (v1.x, v1.y, v1.z, 1)
|
||||
spline.points[1].co = (v2.x, v2.y, v2.z, 1)
|
||||
|
||||
curve_obj = bpy.data.objects.new(f"{source_obj.name}_outline", curve_data)
|
||||
context.collection.objects.link(curve_obj)
|
||||
|
||||
print(f" Created curve with {len(unique_edges_normalized)} splines")
|
||||
|
||||
# Set view to top orthographic
|
||||
original_view_data = None
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for space in area.spaces:
|
||||
if space.type == 'VIEW_3D':
|
||||
from mathutils import Quaternion
|
||||
original_view_data = {
|
||||
'rotation': space.region_3d.view_rotation.copy(),
|
||||
'perspective': space.region_3d.view_perspective,
|
||||
'distance': space.region_3d.view_distance
|
||||
}
|
||||
space.region_3d.view_rotation = Quaternion((1.0, 0.0, 0.0, 0.0))
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
break
|
||||
|
||||
# Switch back to object mode to select objects
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Deselect all
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
# Select curve and target
|
||||
curve_obj.select_set(True)
|
||||
target_obj.select_set(True)
|
||||
context.view_layer.objects.active = target_obj
|
||||
|
||||
# Switch to edit mode for knife_project
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.mesh.select_all(action='DESELECT')
|
||||
|
||||
# Use knife project
|
||||
print(f" Running knife_project...")
|
||||
bpy.ops.mesh.knife_project(cut_through=False)
|
||||
|
||||
# Restore view
|
||||
if original_view_data:
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
for space in area.spaces:
|
||||
if space.type == 'VIEW_3D':
|
||||
space.region_3d.view_rotation = original_view_data['rotation']
|
||||
space.region_3d.view_perspective = original_view_data['perspective']
|
||||
space.region_3d.view_distance = original_view_data['distance']
|
||||
break
|
||||
|
||||
# Get bmesh for material assignment
|
||||
bm = bmesh.from_edit_mesh(target_obj.data)
|
||||
bm.faces.ensure_lookup_table()
|
||||
|
||||
print(f" Mesh cutting complete, now assigning materials...")
|
||||
|
||||
# Calculate 2D bounding box of the outline (in world space)
|
||||
min_x_world = min(min(v1.x, v2.x) for v1, v2 in outline_edges)
|
||||
max_x_world = max(max(v1.x, v2.x) for v1, v2 in outline_edges)
|
||||
min_y_world = min(min(v1.y, v2.y) for v1, v2 in outline_edges)
|
||||
max_y_world = max(max(v1.y, v2.y) for v1, v2 in outline_edges)
|
||||
|
||||
print(f" Outline 2D bounds (world space): X=[{min_x_world:.2f}, {max_x_world:.2f}], Y=[{min_y_world:.2f}, {max_y_world:.2f}]")
|
||||
|
||||
# Transform bounds to target object's local space (reuse matrix from cutting)
|
||||
# Transform the 4 corners of the bounding box
|
||||
corner_min_min = target_matrix_inv @ Vector((min_x_world, min_y_world, 0))
|
||||
corner_min_max = target_matrix_inv @ Vector((min_x_world, max_y_world, 0))
|
||||
corner_max_min = target_matrix_inv @ Vector((max_x_world, min_y_world, 0))
|
||||
corner_max_max = target_matrix_inv @ Vector((max_x_world, max_y_world, 0))
|
||||
|
||||
# Get new bounds in local space
|
||||
all_x = [corner_min_min.x, corner_min_max.x, corner_max_min.x, corner_max_max.x]
|
||||
all_y = [corner_min_min.y, corner_min_max.y, corner_max_min.y, corner_max_max.y]
|
||||
|
||||
min_x_local = min(all_x)
|
||||
max_x_local = max(all_x)
|
||||
min_y_local = min(all_y)
|
||||
max_y_local = max(all_y)
|
||||
|
||||
print(f" Outline 2D bounds (target local space): X=[{min_x_local:.2f}, {max_x_local:.2f}], Y=[{min_y_local:.2f}, {max_y_local:.2f}]")
|
||||
|
||||
# Assign material to faces within bounds
|
||||
if material_index >= 0:
|
||||
source_min_z_world = min(min(v1.z, v2.z) for v1, v2 in outline_edges)
|
||||
|
|
@ -327,24 +445,60 @@ def project_outline_to_mesh(context, source_obj, target_obj, outline_edges):
|
|||
source_max_z_local = (target_matrix_inv @ Vector((0, 0, source_max_z_world))).z
|
||||
|
||||
print(f" Source Z max: {source_max_z_world:.2f} (world), {source_max_z_local:.2f} (target local)")
|
||||
print(f" Material assignment: ALL vertices inside or on edge (tolerance: 0.05)")
|
||||
|
||||
faces_assigned = 0
|
||||
faces_rejected_outside_polygon = 0
|
||||
faces_rejected_z = 0
|
||||
|
||||
for face in bm.faces:
|
||||
face_center = face.calc_center_median()
|
||||
|
||||
# Face center is already in target's local space (we're in edit mode)
|
||||
# Check if within XY bounds and below source
|
||||
if (min_x_local <= face_center.x <= max_x_local and
|
||||
min_y_local <= face_center.y <= max_y_local and
|
||||
face_center.z < source_max_z_local):
|
||||
# First check Z (quick rejection)
|
||||
below_z = face_center.z < source_max_z_local
|
||||
if not below_z:
|
||||
faces_rejected_z += 1
|
||||
continue
|
||||
|
||||
# Check if ALL vertices are inside or on/near the polygon boundary
|
||||
all_verts_inside = True
|
||||
verts_checked = 0
|
||||
|
||||
for vert in face.verts:
|
||||
vert_world = target_obj.matrix_world @ vert.co
|
||||
|
||||
# Check Z first
|
||||
if vert_world.z >= source_max_z_world:
|
||||
all_verts_inside = False
|
||||
break
|
||||
|
||||
# Check if vertex is inside polygon OR on/near the edge (with tolerance)
|
||||
inside = point_in_polygon_2d((vert_world.x, vert_world.y), polygon_2d_world)
|
||||
on_edge = point_near_polygon_edge((vert_world.x, vert_world.y), polygon_2d_world, tolerance=0.05)
|
||||
|
||||
if not (inside or on_edge):
|
||||
all_verts_inside = False
|
||||
break
|
||||
|
||||
verts_checked += 1
|
||||
|
||||
if all_verts_inside and verts_checked > 0:
|
||||
face.material_index = material_index
|
||||
faces_assigned += 1
|
||||
|
||||
if faces_assigned <= 3:
|
||||
print(f" Assigned material to face at ({face_center.x:.2f}, {face_center.y:.2f}, {face_center.z:.2f}) [local]")
|
||||
if faces_assigned <= 5:
|
||||
face_center_world = target_obj.matrix_world @ face_center
|
||||
print(f" ✓ Face {faces_assigned}: ({face_center.x:.2f}, {face_center.y:.2f}, {face_center.z:.2f}) [local] -> ({face_center_world.x:.2f}, {face_center_world.y:.2f}) [world]")
|
||||
else:
|
||||
faces_rejected_outside_polygon += 1
|
||||
if faces_rejected_outside_polygon <= 5:
|
||||
face_center_world = target_obj.matrix_world @ face_center
|
||||
print(f" ✗ Outside polygon: ({face_center.x:.2f}, {face_center.y:.2f}, {face_center.z:.2f}) [local] -> ({face_center_world.x:.2f}, {face_center_world.y:.2f}) [world]")
|
||||
|
||||
print(f" Assigned material to {faces_assigned} faces")
|
||||
print(f" Material assignment results:")
|
||||
print(f" Assigned: {faces_assigned}")
|
||||
print(f" Rejected - Outside polygon: {faces_rejected_outside_polygon}")
|
||||
print(f" Rejected - Z too high: {faces_rejected_z}")
|
||||
|
||||
bmesh.update_edit_mesh(target_obj.data)
|
||||
|
||||
|
|
@ -352,6 +506,11 @@ def project_outline_to_mesh(context, source_obj, target_obj, outline_edges):
|
|||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
print(f" Returned to OBJECT mode")
|
||||
|
||||
# Clean up temporary curve object
|
||||
bpy.data.objects.remove(curve_obj, do_unlink=True)
|
||||
bpy.data.curves.remove(curve_data)
|
||||
print(f" Cleaned up temporary curve")
|
||||
|
||||
# Verify material assignment
|
||||
if material_index >= 0:
|
||||
print(f"\n Post-assignment verification:")
|
||||
|
|
|
|||
Loading…
Reference in a new issue