Adaptive isogrid: min triangle area filtering and circular hole bosses
This commit is contained in:
@@ -7,7 +7,7 @@ respects plate boundary, hole keepouts, and density-driven spacing.
|
||||
|
||||
import numpy as np
|
||||
import triangle as tr
|
||||
from shapely.geometry import Polygon, LinearRing
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from .density_field import evaluate_density, density_to_spacing
|
||||
|
||||
@@ -33,6 +33,13 @@ def offset_polygon(coords, distance, inward=True):
|
||||
return coords
|
||||
|
||||
|
||||
def sample_circle(center, radius, num_points=32):
|
||||
"""Sample a circle as a polygon with `num_points` vertices."""
|
||||
cx, cy = center
|
||||
angles = np.linspace(0.0, 2.0 * np.pi, num_points, endpoint=False)
|
||||
return [[cx + radius * np.cos(a), cy + radius * np.sin(a)] for a in angles]
|
||||
|
||||
|
||||
def build_pslg(geometry, params):
|
||||
"""
|
||||
Build Planar Straight Line Graph for Triangle library.
|
||||
@@ -62,18 +69,28 @@ def build_pslg(geometry, params):
|
||||
for i in range(n):
|
||||
segments.append([v_start + i, v_start + (i + 1) % n])
|
||||
|
||||
# Each hole with keepout offset
|
||||
# Each hole with boss keepout reservation
|
||||
for hole in geometry['holes']:
|
||||
keepout_dist = d_keep * (hole.get('diameter', 10.0) or 10.0) / 2.0
|
||||
hole_boundary = offset_polygon(hole['boundary'], keepout_dist, inward=False)
|
||||
|
||||
diameter = float(hole.get('diameter', 10.0) or 10.0)
|
||||
keepout_dist = d_keep * diameter / 2.0
|
||||
|
||||
if hole.get('is_circular', False) and 'center' in hole:
|
||||
# Circular boss reservation around hole:
|
||||
# r_boss = r_hole + d_keep * hole_diameter / 2
|
||||
hole_radius = diameter / 2.0
|
||||
boss_radius = hole_radius + keepout_dist
|
||||
keepout_boundary = sample_circle(hole['center'], boss_radius, num_points=32)
|
||||
else:
|
||||
# Fallback for non-circular holes
|
||||
keepout_boundary = offset_polygon(hole['boundary'], keepout_dist, inward=False)
|
||||
|
||||
v_start = len(vertices)
|
||||
vertices.extend(hole_boundary)
|
||||
n_h = len(hole_boundary)
|
||||
vertices.extend(keepout_boundary)
|
||||
n_h = len(keepout_boundary)
|
||||
for i in range(n_h):
|
||||
segments.append([v_start + i, v_start + (i + 1) % n_h])
|
||||
|
||||
# Marker inside hole tells Triangle to leave it empty
|
||||
|
||||
# Marker inside hole tells Triangle to leave this keepout region empty
|
||||
hole_markers.append(hole['center'])
|
||||
|
||||
result = {
|
||||
@@ -107,6 +124,22 @@ def compute_centroids(vertices, triangles):
|
||||
return (v0 + v1 + v2) / 3.0
|
||||
|
||||
|
||||
def filter_small_triangles(result, min_triangle_area):
|
||||
"""Remove triangles smaller than the manufacturing threshold."""
|
||||
triangles = result.get('triangles')
|
||||
vertices = result.get('vertices')
|
||||
if triangles is None or vertices is None or len(triangles) == 0:
|
||||
return result
|
||||
|
||||
areas = compute_triangle_areas(vertices, triangles)
|
||||
keep_mask = areas >= float(min_triangle_area)
|
||||
|
||||
result['triangle_areas'] = areas
|
||||
result['small_triangle_mask'] = ~keep_mask
|
||||
result['triangles'] = triangles[keep_mask]
|
||||
return result
|
||||
|
||||
|
||||
def generate_triangulation(geometry, params, max_refinement_passes=3):
|
||||
"""
|
||||
Generate density-adaptive constrained Delaunay triangulation.
|
||||
@@ -137,10 +170,10 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
|
||||
for iteration in range(max_refinement_passes):
|
||||
verts = result['vertices']
|
||||
tris = result['triangles']
|
||||
|
||||
|
||||
areas = compute_triangle_areas(verts, tris)
|
||||
centroids = compute_centroids(verts, tris)
|
||||
|
||||
|
||||
# Compute target area for each triangle based on density at centroid
|
||||
target_areas = np.array([
|
||||
(np.sqrt(3) / 4.0) * density_to_spacing(
|
||||
@@ -148,13 +181,15 @@ def generate_triangulation(geometry, params, max_refinement_passes=3):
|
||||
)**2
|
||||
for cx, cy in centroids
|
||||
])
|
||||
|
||||
|
||||
# Check if all triangles satisfy constraints (20% tolerance)
|
||||
if np.all(areas <= target_areas * 1.2):
|
||||
break
|
||||
|
||||
|
||||
# Set per-triangle max area and refine
|
||||
result['triangle_max_area'] = target_areas
|
||||
result = tr.triangulate(result, 'rpq30D')
|
||||
|
||||
|
||||
min_triangle_area = params.get('min_triangle_area', 20.0)
|
||||
result = filter_small_triangles(result, min_triangle_area)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user