GameCult.Geometry.Csg/spikes/boolean-weld/py/cases.py
Meta Vens 7539c3fb7b Spike boolean-weld round 2: owner-defines-silhouette weld, joint split guard, notch across the crease
Ruling weld-owner-defines-silhouette implemented (owner by curvature along the loop; yielding spare
vertices merge; hull-owned inserted loop vertices snap into nearby hull vertices; splits welded jointly).
Ports and the crease notch clean up; the panel split keeps its worst angle (in the cap triangulation),
gets no crack, and its seam kinks where loop vertices snap to hull vertices. Guards at 5 and 10 deg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:03:20 +02:00

99 lines
6 KiB
Python

"""Pick placements on the hull and write cutter meshes plus a case manifest (cases.txt)."""
import sys, os, json, numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from geo import *
fx, outd = sys.argv[1], sys.argv[2]
os.makedirs(outd, exist_ok=True)
V, F = load_obj(fx + '/hull_nobevel.obj'); T = triangulate(F)
FN, FA = face_normals(V, T)
S = np.load(fx + '/../_curv.npy')
VN = vertex_normals(V, T)
def ray_hits(o, d):
a, b, c = V[T[:, 0]], V[T[:, 1]], V[T[:, 2]]
e1, e2 = b - a, c - a; p = np.cross(d, e2); det = e1 @ np.zeros(3) + np.einsum('ij,ij->i', e1, p)
inv = 1 / np.where(np.abs(det) < 1e-15, np.nan, det); s = o - a
u = np.einsum('ij,ij->i', s, p) * inv; q = np.cross(s, e1); v = (q @ d) * inv; t = np.einsum('ij,ij->i', e2, q) * inv
ok = (u >= 0) & (v >= 0) & (u + v <= 1) & (t > 0)
idx = np.where(ok)[0]; return sorted((t[i], i) for i in idx)
def surf_point(x, y, top=True):
o = np.array([x, y, 1.0 if top else -1.0]); d = np.array([0, 0, -1.0 if top else 1.0])
h = ray_hits(o, d); t, fi = h[0]; p = o + t * d
w = V[T[fi]]; n = FN[fi]
hs = [hh for hh in h if hh[0] - t > 1e-6]
thick = hs[0][0] - t if hs else None
return p, n, thick
def cylinder(p, n, r, depth, height, seg):
a = n / np.linalg.norm(n); e1 = np.cross(a, [1, 0, 0] if abs(a[0]) < .9 else [0, 1, 0]); e1 /= np.linalg.norm(e1); e2 = np.cross(a, e1)
ang = np.arange(seg) * 2 * np.pi / seg
ring = np.cos(ang)[:, None] * e1 + np.sin(ang)[:, None] * e2
bot = p - depth * a + r * ring; top = p + height * a + r * ring
Vc = np.vstack([bot, top, [p - depth * a], [p + height * a]]); cb, ct = 2 * seg, 2 * seg + 1
Tc = []
for i in range(seg):
j = (i + 1) % seg
Tc += [[i, j, seg + j], [i, seg + j, seg + i], [cb, j, i], [ct, seg + i, seg + j]]
return Vc, np.array(Tc)
def box(lo, hi):
lo, hi = np.array(lo, float), np.array(hi, float)
Vb = np.array([[x, y, z] for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])])
Q = [[0, 1, 3, 2], [4, 6, 7, 5], [0, 4, 5, 1], [2, 3, 7, 6], [0, 2, 6, 4], [1, 5, 7, 3]]
return Vb, np.array([[q[0], q[1], q[2]] for q in Q] + [[q[0], q[2], q[3]] for q in Q])
def orient_out(Vc, Tc):
vol = np.einsum('ij,ij->i', Vc[Tc[:, 0]], np.cross(Vc[Tc[:, 1]], Vc[Tc[:, 2]])).sum()
return Tc if vol > 0 else Tc[:, ::-1]
cases = []
def add_cyl(name, x, y, r, segs, top=True, note=''):
p, n, th = surf_point(x, y, top); dep = min(0.02, 0.6 * th) if th else 0.02
for sg in segs:
Vc, Tc = cylinder(p, n, r, dep, 0.03, sg); Tc = orient_out(Vc, Tc)
fn = f'{outd}/{name}_s{sg}.obj'; save_obj(fn, Vc, Tc)
cases.append(dict(name=f'{name}_s{sg}', kind='cyl', op='sub', cutter=fn, p=p.tolist(), n=n.tolist(), r=r, seg=sg, thick=th, depth=dep, note=note))
# reference: same cylinder, 1024 segments, for the true intersection curve
Vc, Tc = cylinder(p, n, r, dep, 0.03, 1024); Tc = orient_out(Vc, Tc); save_obj(f'{outd}/{name}_ref.obj', Vc, Tc)
def add_split(name, lo, hi, note=''):
Vb, Tb = box(lo, hi); Tb = orient_out(Vb, Tb); fn = f'{outd}/{name}.obj'; save_obj(fn, Vb, Tb)
for op in ('sub', 'int'):
cases.append(dict(name=f'{name}_{op}', kind='box', op=op, cutter=fn, lo=list(lo), hi=list(hi), note=note))
# 1. gun port on the upper flank
add_cyl('port_flank', 0.17, 0.0, 0.015, (16, 48), note='flank gun port')
# 2. transverse panel split round the forward body
add_split('split_fwd', (-0.8, 0.22, -0.6), (0.8, 0.8, 0.6), note='transverse split, forward body (crosses the rim crease twice)')
# probe 1: equal curvature. Non-crease, upward-facing vertex whose max |k| is nearest 1/0.02; the cylinder radius
# is 1/|k| there, so |k_hull| and |k_cutter| (around) match at the placement.
E0 = np.loadtxt(fx + '/hull_creases.txt', dtype=int); cv0 = set(np.unique(E0).tolist()); adj0 = vertex_adj(T, len(V)); nr0 = set(cv0)
for c in cv0: nr0 |= adj0[c]
for c in list(nr0): nr0 |= adj0[c]
km = np.abs(np.linalg.eigvalsh(S)).max(1); score = np.abs(km - 50.0); score[list(nr0)] = 1e9; score[VN[:, 2] < 0.5] = 1e9
i = int(np.argmin(score)); kr = km[i]
add_cyl('probe_equalk', float(V[i, 0]), float(V[i, 1]), float(1 / kr), (16, 48), note=f'vertex {i}, r=1/|k|max={1/kr:.4f}, |k|={kr:.2f}')
# probe 1b: planar split near-flat region (longitudinal plane through the lower belly)
# probe 2: saddle, strongest opposite-sign principal curvatures away from creases
E = np.loadtxt(fx + '/hull_creases.txt', dtype=int); cv = set(np.unique(E).tolist())
adj = vertex_adj(T, len(V)); near = set(cv)
for c in cv: near |= adj[c]
for c in list(near): near |= adj[c]
kk = np.sort(np.linalg.eigvalsh(S), 1); sad = np.minimum(-kk[:, 0], kk[:, 2])
sad[list(near)] = -1; sad[VN[:, 2] < 0.3] = -1
si = int(np.argmax(sad)); sp = V[si]
add_cyl('probe_saddle', float(sp[0]), float(sp[1]), 0.015, (16, 48), note=f'saddle vertex {si} k1={kk[si,0]:.2f} k2={kk[si,2]:.2f}')
# probe 3: split across the wing-rim crease (chordwise plane through the wing)
add_split('probe_crease', (0.33, -0.8, -0.6), (0.9, 0.8, 0.6), note='chordwise split x>0.33 crosses the rim crease')
# probe 4: big cylinder across the dorsal ridge to the flank (ownership switch along one loop)
add_cyl('probe_switch', 0.0, -0.05, 0.08, (48,), note='r=0.08 across the dorsal ridge')
# round 2 probe: curved cutter across the wing-rim crease (vertical cylinder through the rim, a notch)
cvl = sorted(cv0, key=lambda i: np.linalg.norm(V[i, :2] - [0.38, -0.05]))[0]; pc = V[cvl]
for sg in (16, 48):
Vc, Tc = cylinder(pc, np.array([0, 0, 1.0]), 0.03, 0.15, 0.15, sg); Tc = orient_out(Vc, Tc)
fn = f'{outd}/probe_crease_cyl_s{sg}.obj'; save_obj(fn, Vc, Tc)
cases.append(dict(name=f'probe_crease_cyl_s{sg}', kind='cyl', op='sub', cutter=fn, p=pc.tolist(), n=[0, 0, 1.0], r=0.03, seg=sg, depth=0.15, note=f'vertical cylinder r 0.03 through rim crease vertex {cvl}'))
Vc, Tc = cylinder(pc, np.array([0, 0, 1.0]), 0.03, 0.15, 0.15, 1024); Tc = orient_out(Vc, Tc); save_obj(f'{outd}/probe_crease_cyl_ref.obj', Vc, Tc)
json.dump(cases, open(outd + '/cases.json', 'w'), indent=1)
for c in cases: print(c['name'], c.get('thick'), c['note'])