more fixes
This commit is contained in:
parent
7812074151
commit
75486a4ab7
2 changed files with 161822 additions and 58358 deletions
|
|
@ -20,11 +20,11 @@ import os
|
|||
A_FIXED = -3.0 # butterfly unfolding parameter (must be negative)
|
||||
|
||||
# ── Tuning ──────────────────────────────────────────────────────────────────
|
||||
GRID = 50 # control-space resolution — increase to 80–100 for print
|
||||
GRID = 80 # 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
|
||||
MAX_EDGE_DZ = 0.6 # max z-jump per quad edge (filters branch mismatches)
|
||||
MAX_MATCH_DZ = 0.8 # max z-gap for inter-row branch matching
|
||||
|
||||
# ── 1. Root finding ──────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -58,53 +58,127 @@ def find_roots(c, d, n_starts=80):
|
|||
roots.append(x)
|
||||
return sorted(roots)
|
||||
|
||||
# ── 2. Build mesh ────────────────────────────────────────────────────────────
|
||||
# ── 2. Branch tracking ───────────────────────────────────────────────────────
|
||||
|
||||
def track_branches(roots_along_axis):
|
||||
"""
|
||||
Track branches along one axis (fixed d, varying c) using greedy
|
||||
nearest-neighbour matching. Returns a list of tracks; each track is a
|
||||
list of length GRID where entry i is the root value at column i, or None
|
||||
when the branch does not exist there.
|
||||
|
||||
Sorting-index matching (branch 0 always = branch 0) breaks at folds
|
||||
because two adjacent branches coalesce, shifting every higher index by
|
||||
one. Nearest-neighbour tracking follows each physical sheet through the
|
||||
fold correctly: the two merging branches each get None past the fold, and
|
||||
the surviving sheet keeps its track unbroken.
|
||||
"""
|
||||
n = len(roots_along_axis)
|
||||
if n == 0:
|
||||
return []
|
||||
|
||||
tracks = [[r] for r in roots_along_axis[0]]
|
||||
|
||||
for i in range(1, n):
|
||||
curr = roots_along_axis[i]
|
||||
|
||||
prev_live = [(ti, t[-1]) for ti, t in enumerate(tracks)
|
||||
if t[-1] is not None]
|
||||
prev_matched, curr_matched = set(), set()
|
||||
matches = {} # track_idx → curr_root_idx
|
||||
|
||||
cands = sorted(
|
||||
[(abs(pv - curr[ci]), ti, ci)
|
||||
for ti, pv in prev_live
|
||||
for ci in range(len(curr))],
|
||||
key=lambda x: x[0]
|
||||
)
|
||||
for _, ti, ci in cands:
|
||||
if ti not in prev_matched and ci not in curr_matched:
|
||||
matches[ti] = ci
|
||||
prev_matched.add(ti)
|
||||
curr_matched.add(ci)
|
||||
|
||||
for ti, t in enumerate(tracks):
|
||||
t.append(curr[matches[ti]] if ti in matches else None)
|
||||
|
||||
# Branches that appear for the first time at this column
|
||||
for ci in range(len(curr)):
|
||||
if ci not in curr_matched:
|
||||
tracks.append([None] * i + [curr[ci]])
|
||||
|
||||
return tracks
|
||||
|
||||
# ── 3. Build mesh ────────────────────────────────────────────────────────────
|
||||
|
||||
def build_mesh():
|
||||
c_vals = np.linspace(*C_RANGE, GRID)
|
||||
d_vals = np.linspace(*D_RANGE, GRID)
|
||||
|
||||
print(f' Computing roots on {GRID}×{GRID} grid…')
|
||||
# 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)
|
||||
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
|
||||
|
||||
triangles = []
|
||||
|
||||
for i in range(GRID - 1):
|
||||
for j in range(GRID - 1):
|
||||
for j in range(GRID - 1):
|
||||
d0, d1 = d_vals[j], d_vals[j+1]
|
||||
tracks_j = row_tracks[j]
|
||||
tracks_j1 = row_tracks[j + 1]
|
||||
|
||||
for i in range(GRID - 1):
|
||||
c0, c1 = c_vals[i], c_vals[i+1]
|
||||
d0, d1 = d_vals[j], d_vals[j+1]
|
||||
|
||||
r00 = roots_grid[i ][j ]
|
||||
r10 = roots_grid[i+1][j ]
|
||||
r11 = roots_grid[i+1][j+1]
|
||||
r01 = roots_grid[i ][j+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)
|
||||
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)
|
||||
if t[i] is not None and t[i+1] is not None]
|
||||
|
||||
# Only draw branch k when ALL four corners have it.
|
||||
# The rs[-1] fallback used previously created false triangles at
|
||||
# fold edges by connecting unrelated branches — this is the main
|
||||
# cause of the messy, self-intersecting geometry.
|
||||
n = min(len(r00), len(r10), len(r11), len(r01))
|
||||
if not segs_j or not segs_j1:
|
||||
continue
|
||||
|
||||
for k in range(n):
|
||||
p0 = (c0, d0, r00[k])
|
||||
p1 = (c1, d0, r10[k])
|
||||
p2 = (c1, d1, r11[k])
|
||||
p3 = (c0, d1, r01[k])
|
||||
# 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
|
||||
for x01, x11, k1 in segs_j1:
|
||||
if k1 in j1_used:
|
||||
continue
|
||||
dist = max(abs(x00 - x01), abs(x10 - x11))
|
||||
if dist < best_dist:
|
||||
best_dist, best_k1 = dist, k1
|
||||
best_x01, best_x11 = x01, x11
|
||||
|
||||
# Reject quads where any edge has a large z-jump. A large
|
||||
# jump indicates a branch-index mismatch near a fold line
|
||||
# (sorted order is preserved within a branch but can
|
||||
# "swap" across folds where adjacent branches coalesce).
|
||||
if any(abs(a[2] - b[2]) > MAX_EDGE_DZ
|
||||
for a, b in [(p0,p1),(p1,p2),(p2,p3),(p3,p0)]):
|
||||
if best_k1 is None:
|
||||
continue
|
||||
j1_used.add(best_k1)
|
||||
|
||||
triangles.append((p0, p1, p2))
|
||||
triangles.append((p0, p2, p3))
|
||||
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))
|
||||
|
||||
return triangles
|
||||
|
||||
# ── 3. Flat base ─────────────────────────────────────────────────────────────
|
||||
# ── 4. Flat base ─────────────────────────────────────────────────────────────
|
||||
|
||||
def add_base(triangles, z_base=-2.8):
|
||||
c0, c1 = C_RANGE
|
||||
|
|
@ -127,7 +201,7 @@ def add_base(triangles, z_base=-2.8):
|
|||
]
|
||||
return triangles
|
||||
|
||||
# ── 4. ASCII STL output ───────────────────────────────────────────────────────
|
||||
# ── 5. ASCII STL output ───────────────────────────────────────────────────────
|
||||
|
||||
def normal(v0, v1, v2):
|
||||
a = np.subtract(v1, v0)
|
||||
|
|
@ -153,7 +227,7 @@ def write_ascii_stl(triangles, filename):
|
|||
print(f' Triangles : {len(triangles):,}')
|
||||
print(f' File size : {size_kb:.0f} KB → {filename}')
|
||||
|
||||
# ── 5. Main ───────────────────────────────────────────────────────────────────
|
||||
# ── 6. Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(f'Building butterfly catastrophe mesh (grid={GRID})…')
|
||||
|
|
|
|||
220044
butterfly_catastrophe.stl
220044
butterfly_catastrophe.stl
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue