some improvement
This commit is contained in:
parent
75486a4ab7
commit
af1c43d41c
3 changed files with 1060175 additions and 93373 deletions
|
|
@ -73,7 +73,7 @@ Output: `butterfly_catastrophe.stl`
|
|||
| `C_RANGE` | `(-2.0, 7.0)` | Range of control parameter c |
|
||||
| `D_RANGE` | `(-6.0, 6.0)` | Range of control parameter d |
|
||||
| `X_RANGE` | `2.5` | Search window for equilibrium roots |
|
||||
| `MAX_EDGE_DZ` | `0.6` | Z-jump threshold for rejecting branch-mismatch triangles at fold edges |
|
||||
| `MAX_MATCH_DZ` | `0.8` | Max z-gap for inter-row branch matching in main surface |
|
||||
|
||||
For a final high-quality print, increase `grid` to `80`–`100`. The default of `40` is optimised for STL viewer compatibility.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import os
|
|||
A_FIXED = -3.0 # butterfly unfolding parameter (must be negative)
|
||||
|
||||
# ── Tuning ──────────────────────────────────────────────────────────────────
|
||||
GRID = 80 # control-space resolution — increase to 100–120 for print
|
||||
GRID = 200 # control-space resolution — increase to 100–120 for print
|
||||
C_RANGE = (-2.0, 7.0) # range of control parameter c
|
||||
D_RANGE = (-6.0, 6.0) # range of control parameter d
|
||||
X_RANGE = 2.5 # half-width of root search window
|
||||
|
|
@ -111,6 +111,31 @@ def track_branches(roots_along_axis):
|
|||
|
||||
# ── 3. Build mesh ────────────────────────────────────────────────────────────
|
||||
|
||||
def _emit_quad(triangles, p00, p10, p11, p01):
|
||||
triangles.append((p00, p10, p11))
|
||||
triangles.append((p00, p11, p01))
|
||||
|
||||
|
||||
def _fold_terminations(tracks, axis_vals):
|
||||
"""
|
||||
Scan tracks along one axis and return a list of fold-termination events.
|
||||
Each event is (axis_val, xa, xb) where axis_val is the last valid position,
|
||||
and xa < xb are the two branch values that die together at a fold.
|
||||
|
||||
Branches come in pairs at fold lines (two coalesce), so we pair adjacent
|
||||
sorted dying values. Events are indexed by the axis position of the last
|
||||
valid step.
|
||||
"""
|
||||
events = []
|
||||
n = len(axis_vals) - 1 # number of steps
|
||||
for step in range(n):
|
||||
dying = sorted(t[step] for t in tracks
|
||||
if t[step] is not None and t[step + 1] is None)
|
||||
for k in range(0, len(dying) - 1, 2):
|
||||
events.append((axis_vals[step], dying[k], dying[k + 1]))
|
||||
return events
|
||||
|
||||
|
||||
def build_mesh():
|
||||
c_vals = np.linspace(*C_RANGE, GRID)
|
||||
d_vals = np.linspace(*D_RANGE, GRID)
|
||||
|
|
@ -119,16 +144,16 @@ def build_mesh():
|
|||
# roots_grid[i][j] = sorted roots at (c_vals[i], d_vals[j])
|
||||
roots_grid = [[find_roots(c, d) for d in d_vals] for c in c_vals]
|
||||
|
||||
# Track branches along each row (fixed d, varying c)
|
||||
# Track branches along rows (fixed d, varying c) and columns (fixed c, varying d).
|
||||
print(' Tracking branches…')
|
||||
row_tracks = []
|
||||
for j in range(GRID):
|
||||
row = [roots_grid[i][j] for i in range(GRID)]
|
||||
row_tracks.append(track_branches(row))
|
||||
# row_tracks[j] = list of tracks; track[i] = root value at column i
|
||||
row_tracks = [track_branches([roots_grid[i][j] for i in range(GRID)])
|
||||
for j in range(GRID)]
|
||||
col_tracks = [track_branches([roots_grid[i][j] for j in range(GRID)])
|
||||
for i in range(GRID)]
|
||||
|
||||
triangles = []
|
||||
|
||||
# ── Main surface quads ────────────────────────────────────────────────────
|
||||
for j in range(GRID - 1):
|
||||
d0, d1 = d_vals[j], d_vals[j + 1]
|
||||
tracks_j = row_tracks[j]
|
||||
|
|
@ -137,23 +162,15 @@ def build_mesh():
|
|||
for i in range(GRID - 1):
|
||||
c0, c1 = c_vals[i], c_vals[i + 1]
|
||||
|
||||
# Collect valid quad-edge segments for both rows at this column
|
||||
# A segment is valid when both endpoints of the edge exist
|
||||
segs_j = [(t[i], t[i+1], k)
|
||||
for k, t in enumerate(tracks_j)
|
||||
segs_j = [(t[i], t[i + 1], k) for k, t in enumerate(tracks_j)
|
||||
if t[i] is not None and t[i + 1] is not None]
|
||||
segs_j1 = [(t[i], t[i+1], k)
|
||||
for k, t in enumerate(tracks_j1)
|
||||
segs_j1 = [(t[i], t[i + 1], k) for k, t in enumerate(tracks_j1)
|
||||
if t[i] is not None and t[i + 1] is not None]
|
||||
|
||||
if not segs_j or not segs_j1:
|
||||
continue
|
||||
|
||||
# Greedy nearest-neighbour matching between the two row-bands.
|
||||
# Each j1-segment is claimed by at most one j-segment so we never
|
||||
# emit overlapping quads.
|
||||
j1_used = set()
|
||||
# Sort j-segments by midpoint so iteration order is deterministic
|
||||
for x00, x10, _ in sorted(segs_j, key=lambda s: (s[0] + s[1]) / 2):
|
||||
best_k1 = best_x01 = best_x11 = None
|
||||
best_dist = MAX_MATCH_DZ
|
||||
|
|
@ -164,17 +181,74 @@ def build_mesh():
|
|||
if dist < best_dist:
|
||||
best_dist, best_k1 = dist, k1
|
||||
best_x01, best_x11 = x01, x11
|
||||
|
||||
if best_k1 is None:
|
||||
continue
|
||||
j1_used.add(best_k1)
|
||||
_emit_quad(triangles,
|
||||
(c0, d0, x00), (c1, d0, x10),
|
||||
(c1, d1, best_x11), (c0, d1, best_x01))
|
||||
|
||||
p00 = (c0, d0, x00)
|
||||
p10 = (c1, d0, x10)
|
||||
p11 = (c1, d1, best_x11)
|
||||
p01 = (c0, d1, best_x01)
|
||||
triangles.append((p00, p10, p11))
|
||||
triangles.append((p00, p11, p01))
|
||||
# ── C-direction fold caps ─────────────────────────────────────────────────
|
||||
# The fold line runs at an angle through the (c, d) grid, so the column
|
||||
# where two branches die can differ by ±1 between adjacent rows.
|
||||
# We collect all fold terminations per row, then match them across the
|
||||
# row pair regardless of exact column, connecting the dying edges with
|
||||
# (possibly trapezoidal) quads.
|
||||
c_terms = [_fold_terminations(row_tracks[j], c_vals) for j in range(GRID)]
|
||||
|
||||
for j in range(GRID - 1):
|
||||
d0, d1 = d_vals[j], d_vals[j + 1]
|
||||
terms_j = c_terms[j]
|
||||
terms_j1 = c_terms[j + 1]
|
||||
|
||||
j1_used = set()
|
||||
for c_j, xa, xb in terms_j:
|
||||
mid = (xa + xb) / 2
|
||||
best_k = None
|
||||
best_dist = 1.0 # max allowed x-midpoint distance between matched pairs
|
||||
for k1, (c_j1, xa1, xb1) in enumerate(terms_j1):
|
||||
if k1 in j1_used:
|
||||
continue
|
||||
dist = abs(mid - (xa1 + xb1) / 2)
|
||||
if dist < best_dist:
|
||||
best_dist, best_k = dist, k1
|
||||
if best_k is None:
|
||||
continue
|
||||
j1_used.add(best_k)
|
||||
c_j1, xa1, xb1 = terms_j1[best_k]
|
||||
# Cap quad: lies at the fold edge, spanning d0→d1 between the two
|
||||
# dying branches. c may differ slightly between the two rows if
|
||||
# the fold line is diagonal.
|
||||
_emit_quad(triangles,
|
||||
(c_j, d0, xa), (c_j, d0, xb),
|
||||
(c_j1, d1, xb1), (c_j1, d1, xa1))
|
||||
|
||||
# ── D-direction fold caps ─────────────────────────────────────────────────
|
||||
d_terms = [_fold_terminations(col_tracks[i], d_vals) for i in range(GRID)]
|
||||
|
||||
for i in range(GRID - 1):
|
||||
c0, c1 = c_vals[i], c_vals[i + 1]
|
||||
terms_i = d_terms[i]
|
||||
terms_i1 = d_terms[i + 1]
|
||||
|
||||
i1_used = set()
|
||||
for d_i, xa, xb in terms_i:
|
||||
mid = (xa + xb) / 2
|
||||
best_k = None
|
||||
best_dist = 1.0
|
||||
for k1, (d_i1, xa1, xb1) in enumerate(terms_i1):
|
||||
if k1 in i1_used:
|
||||
continue
|
||||
dist = abs(mid - (xa1 + xb1) / 2)
|
||||
if dist < best_dist:
|
||||
best_dist, best_k = dist, k1
|
||||
if best_k is None:
|
||||
continue
|
||||
i1_used.add(best_k)
|
||||
d_i1, xa1, xb1 = terms_i1[best_k]
|
||||
_emit_quad(triangles,
|
||||
(c0, d_i, xa), (c1, d_i1, xa1),
|
||||
(c1, d_i1, xb1), (c0, d_i, xb))
|
||||
|
||||
return triangles
|
||||
|
||||
|
|
|
|||
1153416
butterfly_catastrophe.stl
1153416
butterfly_catastrophe.stl
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue