more fixes

This commit is contained in:
Bruno Postle 2026-03-25 19:05:53 +00:00
parent 7812074151
commit 75486a4ab7
2 changed files with 161822 additions and 58358 deletions

View file

@ -20,11 +20,11 @@ import os
A_FIXED = -3.0 # butterfly unfolding parameter (must be negative)
# ── Tuning ──────────────────────────────────────────────────────────────────
GRID = 50 # control-space resolution — increase to 80100 for print
GRID = 80 # control-space resolution — increase to 100120 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):
c0, c1 = c_vals[i], c_vals[i+1]
d0, d1 = d_vals[j], d_vals[j+1]
tracks_j = row_tracks[j]
tracks_j1 = row_tracks[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]
for i in range(GRID - 1):
c0, c1 = c_vals[i], c_vals[i+1]
# 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))
# 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]
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])
# 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 not segs_j or not segs_j1:
continue
triangles.append((p0, p1, p2))
triangles.append((p0, p2, p3))
# 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
if best_k1 is None:
continue
j1_used.add(best_k1)
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})…')

File diff suppressed because it is too large Load diff