mesh_outline_projection #1
2 changed files with 326 additions and 6 deletions
219
Blender/addons/Mesh Outline Projector/README.md
Normal file
219
Blender/addons/Mesh Outline Projector/README.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# Mesh Outline Projector
|
||||
|
||||
A Blender addon that projects the vertical outline of selected objects onto a target mesh and transfers materials, perfect for architectural visualization and terrain mapping.
|
||||
|
||||
## Overview
|
||||
|
||||
Mesh Outline Projector takes the 2D silhouette of objects (when viewed from above) and "stamps" them onto a mesh below, automatically cutting the mesh and applying the source object's material to the projected area. Think of it as a cookie-cutter that works vertically through 3D space.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Vertical Projection**: Projects object outlines straight down (Z-axis) onto target surfaces
|
||||
- **Material Transfer**: Automatically applies source object materials to projected regions
|
||||
- **Clean Cutting**: Uses Blender's knife project for precise edge creation
|
||||
- **Smart Material Assignment**: Point-in-polygon detection with edge tolerance for accurate coverage
|
||||
- **Mesh Cleanup**: Automatically removes previous cuts with Limit Dissolve
|
||||
- **Intersection Detection**: Validates that objects actually overlap before processing
|
||||
- **Multiple Objects**: Process multiple source objects in a single operation
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download `mesh_outline_projector.py`
|
||||
2. Open Blender → Edit → Preferences → Add-ons
|
||||
3. Click "Install..." and select the downloaded file
|
||||
4. Enable "Mesh: Mesh Outline Projector" in the add-ons list
|
||||
5. The panel appears in the 3D Viewport sidebar under the "Outline Projector" tab
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Workflow
|
||||
|
||||
1. **Setup Scene**
|
||||
- Position your source objects (e.g., building footprints) above your target mesh (e.g., terrain)
|
||||
- Objects can be at any height - only horizontal (XY) overlap matters
|
||||
|
||||
2. **Select Objects**
|
||||
- Select one or more source objects (the objects whose outlines you want to project)
|
||||
- Select the target mesh **last** (it becomes the active object - shown in orange)
|
||||
|
||||
3. **Set View**
|
||||
- Press **Numpad 7** to switch to top orthographic view
|
||||
- This is required for knife project to work correctly
|
||||
|
||||
4. **Run Operator**
|
||||
- Open the sidebar (N key) → "Outline Projector" tab
|
||||
- Adjust settings if needed (see below)
|
||||
- Click "Project Outline"
|
||||
|
||||
### What Happens
|
||||
|
||||
The addon performs these steps automatically:
|
||||
|
||||
1. **Validates** that source objects overlap with target in XY plane
|
||||
2. **Cleans up** the target mesh (Limit Dissolve + Quads to Tris)
|
||||
3. **Resets materials** to the first material slot
|
||||
4. **For each source object**:
|
||||
- Extracts the horizontal perimeter edges (2D outline)
|
||||
- Creates a temporary curve from these edges
|
||||
- Uses knife project to cut the target mesh
|
||||
- Assigns the source material to faces within the projected area
|
||||
5. **Reports** results with detailed console output
|
||||
|
||||
## Settings
|
||||
|
||||
### Dissolve Angle
|
||||
|
||||
- **Default**: 0.04 degrees
|
||||
- **Range**: 0.0 - 5.0 degrees
|
||||
- **Purpose**: Controls how aggressively Limit Dissolve removes edges from previous operations
|
||||
|
||||
**When to adjust:**
|
||||
- **Increase** (0.5-2.0°) if working with curved or organic surfaces
|
||||
- **Keep low** (0.01-0.1°) for architectural/flat surfaces
|
||||
- Higher values = more aggressive cleanup, fewer edges
|
||||
|
||||
### Edge Tolerance
|
||||
|
||||
- **Default**: 0.05 units
|
||||
- **Range**: 0.0 - 1.0 units
|
||||
- **Purpose**: How close a vertex must be to a polygon edge to be considered "on the edge"
|
||||
|
||||
**When to adjust:**
|
||||
- **Increase** (0.1-0.5) if missing faces at the boundaries
|
||||
- **Decrease** (0.01-0.03) if getting material bleeding outside the outline
|
||||
- Affects the precision of material assignment at polygon boundaries
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Architectural Site Plans
|
||||
|
||||
Project building footprints onto terrain meshes:
|
||||
- Import IFC building models
|
||||
- Create terrain mesh from topography data
|
||||
- Project building outlines to create material zones
|
||||
|
||||
### Terrain Texturing
|
||||
|
||||
Stamp different material zones onto landscapes:
|
||||
- Roads, paths, and sidewalks
|
||||
- Water bodies and wetlands
|
||||
- Vegetation zones
|
||||
- Building pads
|
||||
|
||||
### Urban Planning
|
||||
|
||||
Visualize zoning and land use:
|
||||
- Different materials for residential/commercial/industrial zones
|
||||
- Overlay multiple boundary shapes
|
||||
- Iterate quickly as plans change
|
||||
|
||||
## Tips & Best Practices
|
||||
|
||||
### Before Running
|
||||
|
||||
✅ **Do:**
|
||||
- Ensure source objects are above or at the same level as target mesh
|
||||
- Use simple, clean geometry for source objects
|
||||
- Press Numpad 7 (top view) before running
|
||||
- Save your work before first use
|
||||
|
||||
❌ **Don't:**
|
||||
- Use extremely complex source meshes (simplify first)
|
||||
- Forget to set top view
|
||||
- Run on unsaved files (though Undo works)
|
||||
|
||||
### Iteration Workflow
|
||||
|
||||
The addon is designed for iteration:
|
||||
|
||||
1. Run projection with objects A and B
|
||||
2. Adjust position/scale of object A
|
||||
3. Run projection again - mesh is automatically cleaned up
|
||||
4. Materials from both objects are preserved
|
||||
|
||||
**The mesh reset happens automatically** - you can run the operator multiple times without manual cleanup.
|
||||
|
||||
### Material Management
|
||||
|
||||
- The addon preserves the **first material slot** as the "base" material
|
||||
- Source materials are added as new slots (slot 1, 2, 3, etc.)
|
||||
- Each run resets everything to the base material before applying projections
|
||||
- If you want a different base material, assign it to slot 0 before running
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No source objects intersect with target mesh"
|
||||
|
||||
**Problem**: Objects don't overlap in the XY plane (when viewed from above)
|
||||
|
||||
**Solutions**:
|
||||
- Check bounding boxes in debug output
|
||||
- Move objects horizontally to overlap
|
||||
- Objects can be at different Z heights - that's fine
|
||||
- Ensure you're not selecting the target as a source object
|
||||
|
||||
### Materials not appearing correctly
|
||||
|
||||
**Problem**: Materials bleeding outside boundaries or missing at edges
|
||||
|
||||
**Solutions**:
|
||||
- Adjust **Edge Tolerance** setting
|
||||
- Increase if missing edge faces (try 0.1-0.2)
|
||||
- Decrease if bleeding outside (try 0.02-0.03)
|
||||
- Check that source objects have materials assigned
|
||||
|
||||
### Blender crashes
|
||||
|
||||
**Problem**: Crash during operation (rare)
|
||||
|
||||
**Solutions**:
|
||||
- Ensure you're in **top orthographic view** (Numpad 7)
|
||||
- Simplify source mesh geometry
|
||||
- Update to latest Blender version
|
||||
- Check console for error messages
|
||||
|
||||
### Unexpected results after multiple runs
|
||||
|
||||
**Problem**: Results look different on subsequent runs
|
||||
|
||||
**Solutions**:
|
||||
- This is normal - each run subdivides the mesh differently
|
||||
- Knife project creates new edges each time
|
||||
- Undo (Ctrl+Z) to return to previous state
|
||||
- The dissolve/cleanup helps but won't make mesh identical
|
||||
|
||||
## Technical Details
|
||||
|
||||
### How Outline Detection Works
|
||||
|
||||
The addon uses three methods to find outline edges:
|
||||
|
||||
1. **Boundary edges**: Edges with only one connected face
|
||||
2. **Silhouette edges**: Edges where connected faces have different vertical orientations
|
||||
3. **Perimeter edges**: Vertical edges at the outer boundary
|
||||
|
||||
These are combined to form the complete 2D outline when viewed from above.
|
||||
|
||||
### Material Assignment Algorithm
|
||||
|
||||
1. **Extract horizontal edges** from the 3D outline
|
||||
2. **Build a 2D polygon** from these edges (in world XY space)
|
||||
3. **For each face** in the target mesh:
|
||||
- Check if **all vertices** are inside the polygon OR near an edge
|
||||
- Uses ray-casting for point-in-polygon test
|
||||
- Uses perpendicular distance for edge proximity
|
||||
4. **Assign material** only if all vertices pass
|
||||
|
||||
This ensures clean boundaries without bleeding.
|
||||
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Requires **top orthographic view** (Numpad 7) to work correctly
|
||||
- Works best with **relatively flat** target meshes
|
||||
- Very **dense meshes** may slow down processing
|
||||
- **Complex source geometry** may produce unexpected outlines
|
||||
- Knife project can create **overlapping edges** in some cases
|
||||
- **Z-axis projection only** - cannot project at angles
|
||||
|
||||
|
||||
|
|
@ -69,6 +69,50 @@ def point_near_polygon_edge(point, polygon_verts, tolerance=0.05):
|
|||
return False
|
||||
|
||||
|
||||
def check_bounding_box_intersection(obj1, obj2):
|
||||
"""Check if two objects' bounding boxes intersect in the XY plane (horizontal)
|
||||
|
||||
Note: Only checks X and Y overlap, not Z, because this is a vertical projection.
|
||||
The source objects are expected to be above or below the target.
|
||||
"""
|
||||
# Get world space bounding box corners
|
||||
bbox1 = [obj1.matrix_world @ Vector(corner) for corner in obj1.bound_box]
|
||||
bbox2 = [obj2.matrix_world @ Vector(corner) for corner in obj2.bound_box]
|
||||
|
||||
# Get min/max for each axis
|
||||
min1 = Vector((min(v.x for v in bbox1), min(v.y for v in bbox1), min(v.z for v in bbox1)))
|
||||
max1 = Vector((max(v.x for v in bbox1), max(v.y for v in bbox1), max(v.z for v in bbox1)))
|
||||
|
||||
min2 = Vector((min(v.x for v in bbox2), min(v.y for v in bbox2), min(v.z for v in bbox2)))
|
||||
max2 = Vector((max(v.x for v in bbox2), max(v.y for v in bbox2), max(v.z for v in bbox2)))
|
||||
|
||||
# Debug output
|
||||
print(f"\n Bounding Box Check: {obj1.name} vs {obj2.name}")
|
||||
print(f" {obj1.name} bounds:")
|
||||
print(f" X: [{min1.x:.2f}, {max1.x:.2f}]")
|
||||
print(f" Y: [{min1.y:.2f}, {max1.y:.2f}]")
|
||||
print(f" Z: [{min1.z:.2f}, {max1.z:.2f}]")
|
||||
print(f" {obj2.name} bounds:")
|
||||
print(f" X: [{min2.x:.2f}, {max2.x:.2f}]")
|
||||
print(f" Y: [{min2.y:.2f}, {max2.y:.2f}]")
|
||||
print(f" Z: [{min2.z:.2f}, {max2.z:.2f}]")
|
||||
|
||||
# Check for overlap on X and Y axes only (horizontal plane)
|
||||
# Z is ignored because we're projecting vertically
|
||||
x_overlap = max1.x >= min2.x and max2.x >= min1.x
|
||||
y_overlap = max1.y >= min2.y and max2.y >= min1.y
|
||||
|
||||
print(f" Overlap check (XY plane only - vertical projection):")
|
||||
print(f" X overlap: {x_overlap} (max1:{max1.x:.2f} >= min2:{min2.x:.2f} and max2:{max2.x:.2f} >= min1:{min1.x:.2f})")
|
||||
print(f" Y overlap: {y_overlap} (max1:{max1.y:.2f} >= min2:{min2.y:.2f} and max2:{max2.y:.2f} >= min1:{min1.y:.2f})")
|
||||
print(f" Z check: SKIPPED (vertical projection - objects can be above/below each other)")
|
||||
|
||||
result = x_overlap and y_overlap
|
||||
print(f" Result: {'INTERSECTS (XY)' if result else 'NO INTERSECTION'}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
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} ===")
|
||||
|
|
@ -230,7 +274,7 @@ def get_object_outline_edges_2d(obj, context):
|
|||
|
||||
# View manipulation functions removed - now using direct bisect projection
|
||||
|
||||
def project_outline_to_mesh(context, source_obj, target_obj, outline_edges):
|
||||
def project_outline_to_mesh(context, source_obj, target_obj, outline_edges, edge_tolerance=0.05):
|
||||
"""Project outline edges vertically (Z-axis) onto target mesh using direct cutting"""
|
||||
|
||||
print(f"\n=== Projecting outline from {source_obj.name} to {target_obj.name} ===")
|
||||
|
|
@ -418,7 +462,7 @@ 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)")
|
||||
print(f" Material assignment: ALL vertices inside or on edge (tolerance: {edge_tolerance})")
|
||||
|
||||
faces_assigned = 0
|
||||
faces_rejected_outside_polygon = 0
|
||||
|
|
@ -447,7 +491,7 @@ def project_outline_to_mesh(context, source_obj, target_obj, outline_edges):
|
|||
|
||||
# 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)
|
||||
on_edge = point_near_polygon_edge((vert_world.x, vert_world.y), polygon_2d_world, tolerance=edge_tolerance)
|
||||
|
||||
if not (inside or on_edge):
|
||||
all_verts_inside = False
|
||||
|
|
@ -509,6 +553,16 @@ class MESH_OT_outline_project(bpy.types.Operator):
|
|||
precision=3
|
||||
)
|
||||
|
||||
edge_tolerance: bpy.props.FloatProperty(
|
||||
name="Edge Tolerance",
|
||||
description="Distance tolerance for vertices near polygon edges (world units)",
|
||||
default=0.05,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
step=0.01,
|
||||
precision=3
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (context.active_object is not None and
|
||||
|
|
@ -534,6 +588,38 @@ class MESH_OT_outline_project(bpy.types.Operator):
|
|||
self.report({'WARNING'}, "No source mesh objects selected")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Check which source objects actually intersect with the target
|
||||
print(f"\nChecking bounding box intersections...")
|
||||
intersecting_objects = []
|
||||
non_intersecting_objects = []
|
||||
|
||||
for source_obj in source_objects:
|
||||
intersects = check_bounding_box_intersection(source_obj, target_obj)
|
||||
if intersects:
|
||||
intersecting_objects.append(source_obj)
|
||||
print(f" ✓ {source_obj.name} intersects with target")
|
||||
else:
|
||||
non_intersecting_objects.append(source_obj)
|
||||
print(f" ✗ {source_obj.name} does NOT intersect with target")
|
||||
|
||||
# Report non-intersecting objects
|
||||
if non_intersecting_objects:
|
||||
non_intersecting_names = ", ".join([obj.name for obj in non_intersecting_objects])
|
||||
print(f"\nWARNING: {len(non_intersecting_objects)} object(s) do not intersect: {non_intersecting_names}")
|
||||
self.report({'WARNING'}, f"{len(non_intersecting_objects)} object(s) do not intersect with target")
|
||||
|
||||
# If no objects intersect, cancel the operation
|
||||
if not intersecting_objects:
|
||||
print("\nERROR: No source objects intersect with the target mesh!")
|
||||
print("Make sure your source objects overlap with the target mesh in 3D space.")
|
||||
self.report({'ERROR'}, "No source objects intersect with target mesh - nothing to project!")
|
||||
return {'CANCELLED'}
|
||||
|
||||
print(f"\nProceeding with {len(intersecting_objects)} intersecting object(s)")
|
||||
|
||||
# Use only intersecting objects from here on
|
||||
source_objects = intersecting_objects
|
||||
|
||||
# Store original mode
|
||||
original_mode = context.mode
|
||||
print(f"Original mode: {original_mode}")
|
||||
|
|
@ -541,6 +627,7 @@ class MESH_OT_outline_project(bpy.types.Operator):
|
|||
# MESH CLEANUP: Remove previous knife cuts
|
||||
print(f"\nCleaning up target mesh...")
|
||||
print(f"Dissolve angle: {self.dissolve_angle} degrees")
|
||||
print(f"Edge tolerance: {self.edge_tolerance} units")
|
||||
|
||||
# Switch to edit mode for cleanup operations
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
|
|
@ -601,7 +688,7 @@ class MESH_OT_outline_project(bpy.types.Operator):
|
|||
continue
|
||||
|
||||
# Project onto target
|
||||
num_verts = project_outline_to_mesh(context, source_obj, target_obj, outline_edges)
|
||||
num_verts = project_outline_to_mesh(context, source_obj, target_obj, outline_edges, self.edge_tolerance)
|
||||
total_verts += num_verts
|
||||
|
||||
print(f"Projected {len(outline_edges)} edges from {source_obj.name}")
|
||||
|
|
@ -622,7 +709,7 @@ class VIEW3D_PT_outline_project(bpy.types.Panel):
|
|||
bl_idname = "VIEW3D_PT_outline_project"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'Edit'
|
||||
bl_category = 'Outline Projector'
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
|
@ -636,13 +723,15 @@ class VIEW3D_PT_outline_project(bpy.types.Panel):
|
|||
box = layout.box()
|
||||
box.label(text="Settings:")
|
||||
box.prop(context.scene, 'outline_dissolve_angle', text="Dissolve Angle", slider=True)
|
||||
box.prop(context.scene, 'outline_edge_tolerance', text="Edge Tolerance", slider=True)
|
||||
|
||||
row = layout.row()
|
||||
row.scale_y = 1.5
|
||||
op = row.operator("mesh.outline_project", icon='MOD_UVPROJECT')
|
||||
|
||||
# Pass the scene value to the operator
|
||||
# Pass the scene values to the operator
|
||||
op.dissolve_angle = context.scene.outline_dissolve_angle
|
||||
op.edge_tolerance = context.scene.outline_edge_tolerance
|
||||
|
||||
|
||||
def register():
|
||||
|
|
@ -659,6 +748,16 @@ def register():
|
|||
step=0.01,
|
||||
precision=3
|
||||
)
|
||||
|
||||
bpy.types.Scene.outline_edge_tolerance = bpy.props.FloatProperty(
|
||||
name="Edge Tolerance",
|
||||
description="Distance tolerance for vertices near polygon edges (world units)",
|
||||
default=0.05,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
step=0.01,
|
||||
precision=3
|
||||
)
|
||||
|
||||
|
||||
def unregister():
|
||||
|
|
@ -668,6 +767,8 @@ def unregister():
|
|||
# Unregister scene property
|
||||
if hasattr(bpy.types.Scene, 'outline_dissolve_angle'):
|
||||
del bpy.types.Scene.outline_dissolve_angle
|
||||
if hasattr(bpy.types.Scene, 'outline_edge_tolerance'):
|
||||
del bpy.types.Scene.outline_edge_tolerance
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in a new issue