Spike boolean-weld round 3: seam on the plane, hull flips, cap CDT, local ownership with corners kept

Self's calls (flat seams stay on the cutter plane, hull vertices survive, hull-side edge flips, cap CDT,
5 deg guard) and ruling weld-owner-local-corners-kept (per-vertex ownership, cutter and crease corners
never merged or moved). Box cut-ins added. Seam off plane 3e-16, crack 0, cap worst angle 0.058 to
5.5 deg; the hull-side worst angle is pinned by near-coincident seam/hull vertex pairs. All corners kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-12 19:23:16 +02:00
commit db2c162a79
23 changed files with 659 additions and 6 deletions

View file

@ -7,6 +7,123 @@ Agent `spike-bt-boolean-weld`, 2026-10-12. A spike, not a cut: the code under `s
Her words, the spec of the rule: "the surface with the tighter curve defines the silhouette of the cut, and the spare
vertices that come from the flatter surface all get merged away."
## Round 3: Self's calls and ruling weld-owner-local-corners-kept
Inputs to this round:
- Self's calls: a flat split keeps its seam exactly on the cutter's plane; hull vertices survive; hull-side slivers
along a flat seam are repaired by edge flips; the cap faces a cut leaves are ours and get a constrained Delaunay
(CDT); worst-angle guard 5 deg.
- Ruling `weld-owner-local-corners-kept` (operator): ownership is local along the loop, and a hard-edged cutter's
corners always survive.
How it was run:
- **Code:** `py/round3.py` for the flat seams; `py/owner_weld.py` with `NOSNAP_FLAT=1 MIN_ANGLE=5` for the curved
cutters.
- **Ownership:** per loop vertex, by curvature along the loop. Nothing in the code takes a loop-wide vote.
- **Protected corners:** loop vertices on a sharp cutter edge (dihedral > 30 deg) or on a hull crease edge. They are
never a merge source and never moved.
- **Edge flips:** greedy. A flip must raise the pair's worst angle and keep both normals within 20 deg. Seam and
crease edges are constrained.
- **CDT:** Shewchuk's Triangle through the `triangle` package (Python wrapper LGPL-3.0; the Triangle core's licence
bars commercial use, so it is fine for this spike and not shippable). Variants:
- `cdt`: plain CDT, loop as the boundary.
- `cdt-q`: CDT with a 20 deg quality bound, interior Steiner points only (`pq20Y`), so the seam is never split.
- **Short seam merge (`short`):** consecutive seam vertices closer than the 0.001 bevel width merge, jointly on both
pieces. Both endpoints lie on the plane, so the seam stays on it. Only boolean-inserted vertices are merged away.
- **New probes:** a box cut into the curved flank (`probe_box_flank`, half-width 0.02) and a box across the dorsal
ridge (`probe_box_ridge`, half-width 0.06). Each is rotated off the tessellation, with its sharp vertical edges on
the loop.
Tables: `spikes/boolean-weld/results/round3/`.
### Flat seams (seam on the plane; hull side and cap reported separately)
| case | variant | hull slivers | hull worst deg | hull p5 deg | cap slivers | cap worst deg | flips | seam off plane | seam crack | seam-adjacent edges < bevel width |
|---|---|---|---|---|---|---|---|---|---|---|
| panel split, subtract piece | naive | 37 | 0.676 | 3.86 | 85 / 122 | 0.058 | 0 | 3e-16 | 0 | 15 |
| | flip + cdt | 28 | 0.676 | 4.69 | 80 / 122 | 0.122 | 0 | 3e-16 | 0 | 15 |
| | flip + cdt-q | 28 | 0.676 | 4.69 | 0 / 332 | 6.76 | 0 | 3e-16 | 0 | 23 |
| | short + flip + cdt-q | 26 | 0.676 | 5.30 | 1 / 313 | 5.50 | 0 | 3e-16 | 0 | 16 |
| panel split, intersect piece | naive | 32 | 0.539 | 4.36 | 85 / 122 | 0.056 | 0 | 3e-16 | 0 | 14 |
| | short + flip + cdt-q | 17 | 0.539 | 6.69 | 1 / 313 | 5.50 | 0 | 3e-16 | 0 | 16 |
| box across the crease, subtract piece | naive | 22 | 0.377 | 3.73 | 22 / 82 | 0.033 | 0 | 7e-16 | 0 | 12 |
| | short + flip + cdt-q | 12 | 2.41 | 6.64 | 3 / 147 | 0.714 | 0 | 7e-16 | 0 | 6 |
| box across the crease, intersect piece | naive | 25 | 0.0405 | 5.61 | 22 / 82 | 0.033 | 0 | 7e-16 | 0 | 11 |
| | short + flip + cdt-q | 10 | 0.0405 | 7.14 | 3 / 147 | 0.714 | 0 | 7e-16 | 0 | 5 |
- **Flips applied:** 138 and 99 on the split; 65 and 79 on the crease box.
- **Short seam merges:** 3 on the split, 4 refused by the 5 deg guard; 5 on the crease box, 1 refused. They move the
seam polyline by at most 0.0069 % and 0.0040 % of the diagonal, within the plane.
- **The seam stays on the plane to float** (3e-16), the two pieces' seams coincide (crack 0), and nothing flips.
- **The cap CDT with interior refinement removes the cap's slivers.** Cap worst angle 0.058 to 5.5 deg (6.8 deg
without the short merge); the crease box's cap reaches only 0.71 deg. Plain CDT cannot: the cap is a long thin
polygon, and with no interior points its worst angle only reaches 0.12 deg.
- **The hull side's worst angle does not move** (0.676, 0.539 and 0.0405 deg). It is a triangle whose two vertices
are a boolean-inserted seam vertex and a hull vertex almost on top of it. Flips cannot remove the edge between
them: two nearly coincident points stay connected in any Delaunay-like triangulation. With both points pinned (the
seam on the plane, hull vertices surviving), that angle stays. The seam-adjacent edges shorter than the bevel width
remain for the same reason (15 to 16).
- **Render** (`r3_longinus_vs_split.jpg`, `r3_r3split.jpg`): the round-3 seam is straight, crisp and continuous for
most of its length. That is better than round 2's kinks, and its seam no longer shows round 1's sliver highlight.
Where the short seam-adjacent edges sit, the bevel narrows and a light wedge appears: Bevel's overlap clamp (on in
the cage's own modifier) caps the width at the shortest adjacent edge. Turning the clamp off (`r3_r3clamp0.jpg`)
gives a constant-width groove but overlapping bevel geometry: a spike on the round-3 seam, sawtooth on the naive
one. So her uniform Longinus groove is not reached yet. Reaching it needs those near-coincident seam/hull vertex
pairs resolved. Either the hull vertex moves onto the seam within the hull's own surface (it survives, displaced
by less than the bevel width), or Self's no-move call is relaxed for vertices within the bevel width.
### Curved and hard-edged cutters (owner rule, local ownership, corners kept, 5 deg)
| case | variant | slivers | worst deg | p5 deg | flips | silhouette dev max | corners kept / total |
|---|---|---|---|---|---|---|---|
| gun port 16 (regression) | naive | 32 | 0.007 | 1.14 | 0 | 0.0198 % | 0 / 0 |
| | owner r3 | 5 | 5.09 | 6.68 | 0 | 0.0198 % | 0 / 0 |
| ownership switch r 0.08 | naive | 112 | 1.2e-6 | 0.62 | 0 | 0.0128 % | |
| | owner r3 | 53 | 1.2e-6 | 2.65 | 0 | 0.323 % | 4 / 4 |
| crease notch 16 | owner r3 | 10 | 4.31 | 6.83 | 0 | 0.061 % | 2 / 2 |
| box in flank (corners on loop) | naive | 3 | 0.285 | 6.62 | 0 | 0 | |
| | owner r3 | 2 | 0.285 | 9.61 | 0 | 0.0108 % | 4 / 4 |
| | hull-side flips (24) | hull 0 (was 2) | hull 6.0 (was 0.95) | hull 12.1 | 0 | 0 | 4 / 4 |
| box across ridge | owner r3 | 66 | 0.070 | 1.41 | 0 | 0.0014 % | 6 / 6 |
| | hull-side flips (118) | hull 7 (was 19) | hull 1.75 (was 0.72) | hull 7.24 | 0 | 0 | 6 / 6 |
- **Corners:** no corner was moved or lost in any case. `corners_kept_in_place` equals `corners` everywhere, and the
guard turned away 4 merges on the flank box and 6 on the ridge box (`protected_corner`).
- On the ownership-switch probe, the 4 corners are where the r 0.08 cylinder's floor rim breaks through the ridge.
- On the crease notch they are the 2 rim-crease crossings; on the flat splits, the 4 crease crossings.
- **Ownership on the box cut-ins:** the hull owns every stretch (box faces have zero curvature along the loop), no
vertex snaps (flat), and the box contributes its corners. That matches her "keep the first's corners".
- **The ridge box's pocket walls keep 48 slivers.** They are the cutter's own faces, trimmed by the hull. They are
not a planar cap, so this round did not re-triangulate them.
- **The port regression is unchanged** from round 2: slivers 5, worst 5.09 deg, silhouette unchanged.
- **The ownership switch** keeps its owner-side 1.2e-6 deg needle and round 2's 0.32 % loop movement on the
hull-owned stretches, where curved-cutter snapping is still on.
### Renders, round 3
![Longinus vs split, round 3](spike-boolean-weld/r3_longinus_vs_split.jpg)
![split seam](spike-boolean-weld/r3_r3split.jpg)
![split wide](spike-boolean-weld/r3_r3splitwide.jpg)
![clamp off](spike-boolean-weld/r3_r3clamp0.jpg)
![crease box](spike-boolean-weld/r3_r3crease.jpg)
![box in flank](spike-boolean-weld/r3_r3boxflank.jpg)
![box across ridge](spike-boolean-weld/r3_r3boxridge.jpg)
![port regression](spike-boolean-weld/r3_r3port16.jpg)
![switch](spike-boolean-weld/r3_r3switch.jpg)
### Questions for Self (round 3, text only)
1. A flat seam's residual worst angle and short bevel-clamped edges come from a boolean-inserted seam vertex lying
almost on a hull vertex. Under "seam on the plane, hull vertices survive" both are pinned. Should a hull vertex
within the bevel width of the seam be allowed to slide onto the seam inside the hull's surface? It still survives,
moved by less than 0.001.
2. Curved-cutter snapping on hull-owned stretches still moves the loop (switch probe 0.32 %). Should the round-3
no-snap apply to every hull-owned stretch, curved cutter or not?
3. The CDT here uses Triangle, whose licence bars commercial use. A shippable cap CDT needs an owned implementation
or a permissive crate (for example `spade`, a Rust CDT; not evaluated here).
## Round 2: ruling weld-owner-defines-silhouette
The operator answered round 1's question (ruling `weld-owner-defines-silhouette`, superseding

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -253,5 +253,93 @@
"seg": 48,
"depth": 0.15,
"note": "vertical cylinder r 0.03 through rim crease vertex 35"
},
{
"name": "probe_box_flank",
"kind": "obox",
"op": "sub",
"cutter": "cutters/probe_box_flank.obj",
"p": [
0.17,
0.0,
0.10491874328928341
],
"n": [
0.16691628816232545,
0.2153707407240696,
0.9621613153656081
],
"R": [
[
-0.7999919715756821,
-0.5763261213588748,
0.16691628816232545
],
[
0.5999939786817615,
-0.770469123057891,
0.2153707407240696
],
[
0.004480062508814215,
0.27244363133286853,
0.9621613153656081
]
],
"lo": [
-0.02,
-0.02,
-0.02
],
"hi": [
0.02,
0.02,
0.03
],
"note": "oriented box half-width 0.02"
},
{
"name": "probe_box_ridge",
"kind": "obox",
"op": "sub",
"cutter": "cutters/probe_box_ridge.obj",
"p": [
0.0,
-0.05,
0.1680719189037999
],
"n": [
-0.09574350308538447,
0.1106194787646127,
0.9892403714642789
],
"R": [
[
-0.7917740521194535,
-0.6032636504939445,
-0.09574350308538447
],
[
0.5938305390895902,
-0.7969495729106153,
0.1106194787646127
],
[
-0.14303545446814095,
0.03073021689327954,
0.9892403714642789
]
],
"lo": [
-0.06,
-0.06,
-0.02
],
"hi": [
0.06,
0.06,
0.03
],
"note": "oriented box half-width 0.06"
}
]

View file

@ -0,0 +1,20 @@
v 0.194188036 -0.000897912 0.080137043
v 0.202533851 0.009870625 0.128245109
v 0.171134991 -0.031716677 0.091034788
v 0.179480806 -0.020948140 0.139142854
v 0.162188357 0.023101847 0.080316246
v 0.170534172 0.033870384 0.128424311
v 0.139135312 -0.007716918 0.091213991
v 0.147481127 0.003051619 0.139322057
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

View file

@ -0,0 +1,20 @@
v 0.085617132 -0.040025248 0.155025426
v 0.080829957 -0.034494274 0.204487444
v 0.013225494 -0.135659196 0.158713052
v 0.008438319 -0.130128222 0.208175070
v -0.009395754 0.031234417 0.137861171
v -0.014182929 0.036765391 0.187323190
v -0.081787392 -0.064399532 0.141548797
v -0.086574567 -0.058868558 0.191010816
f 1 2 4
f 5 7 8
f 1 5 6
f 3 4 8
f 1 3 7
f 2 6 8
f 1 4 3
f 5 8 6
f 1 6 2
f 3 8 7
f 1 7 5
f 2 8 4

View file

@ -0,0 +1,20 @@
"""Round 3 case: an oriented box cut into the curved flank (a square port; its 4 vertical edges cross the hull,
putting 4 cutter corners on the loop). Appends to cases.json. Usage: case_obox.py <cases.json> <cutters-dir>"""
import sys, os, json, numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from geo import save_obj
cf, outd = sys.argv[1:3]
cases = [c for c in json.load(open(cf)) if c['kind'] != 'obox']
base = next(c for c in cases if c['name'] == 'port_flank_s16')
for name, src, half in (('probe_box_flank', base, 0.02), ('probe_box_ridge', next(c for c in cases if c['name'] == 'probe_switch_s48'), 0.06)):
p, n = np.array(src['p']), np.array(src['n']); n /= np.linalg.norm(n)
a = np.cross(n, [0.6, 0.8, 0]); a /= np.linalg.norm(a); b = np.cross(n, a) # rotated off the tessellation
R = np.stack([a, b, n], 1); lo, hi = np.array([-half, -half, -0.02]), np.array([half, half, 0.03])
Vl = 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])])
Vb = p + Vl @ R.T
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]]
Tb = np.array([[q[0], q[1], q[2]] for q in Q] + [[q[0], q[2], q[3]] for q in Q])
if np.einsum('ij,ij->i', Vb[Tb[:, 0]], np.cross(Vb[Tb[:, 1]], Vb[Tb[:, 2]])).sum() < 0: Tb = Tb[:, ::-1]
fn = f'{outd}/{name}.obj'; save_obj(fn, Vb, Tb)
cases.append(dict(name=name, kind='obox', op='sub', cutter=fn, p=p.tolist(), n=n.tolist(), R=R.tolist(), lo=lo.tolist(), hi=hi.tolist(), note=f'oriented box half-width {half}'))
json.dump(cases, open(cf, 'w'), indent=1); print('ok')

View file

@ -17,6 +17,29 @@ MIN_ANGLE = float(os.environ.get('MIN_ANGLE', '10'))
SNAP = float(os.environ.get('SNAP', '0.25'))
# SNAPDIR=reverse: the hull vertex slides onto the loop vertex instead (the loop stays on the cutter; spike variant)
SNAPDIR = os.environ.get('SNAPDIR', 'loop-to-hull')
# Round 3 (ruling weld-owner-local-corners-kept + Self's calls): no snapping where the cutter is planar (the seam stays on
# the cutter's plane), and feature corners on the loop always survive: loop vertices on a sharp cutter edge (dihedral
# > CORNER_DEG) or on a hull crease edge are never a merge source and never moved.
NOSNAP_FLAT = os.environ.get('NOSNAP_FLAT', '0') == '1'
CORNER_DEG = 30.0
def sharp_cutter_edges(cut):
from geo import edge_faces, face_normals
FN, _ = face_normals(cut.V, cut.T); out = set()
for e, fs in edge_faces(cut.T).items():
if len(fs) == 2 and FN[fs[0]] @ FN[fs[1]] < np.cos(np.radians(CORNER_DEG)):
out.add(e)
return out
def corners_of(P, info, hull, cut, tol):
sharp = sharp_cutter_edges(cut); out = []
for v, inf in info.items():
ce = cut.edge_of(P.V[v], tol)
if (ce is not None and (min(ce), max(ce)) in sharp) or inf['crease']:
out.append(P.V[v].copy())
return out
class Piece:
@ -91,7 +114,7 @@ class Piece:
return s.V, np.array([s.faces[i] for i in keep]), np.array([s.prov[i] for i in keep])
def candidates(P, info, loops, hull, tol, scope_ring=True):
def candidates(P, info, loops, hull, tol, scope_ring=True, cut_is_flat=False):
"""Operations as (dist, v_pos, w_pos, kind): v merges into w."""
cand = []; loopset = set(info)
for L in loops:
@ -116,7 +139,7 @@ def candidates(P, info, loops, hull, tol, scope_ring=True):
d = np.linalg.norm(P.V[x] - P.V[v])
if own == 1 and scope_ring:
cand.append((d, P.V[x].copy(), P.V[v].copy(), 'ring'))
elif own == 0 and inf['origin'] == 'H' and hull.kd.query(P.V[v])[0] > tol and hull.kd.query(P.V[x])[0] < tol:
elif own == 0 and not (NOSNAP_FLAT and cut_is_flat) and inf['origin'] == 'H' and hull.kd.query(P.V[v])[0] > tol and hull.kd.query(P.V[x])[0] < tol:
if d < SNAP * hull.vlen[hull.kd.query(P.V[x])[1]]:
if SNAPDIR == 'reverse':
cand.append((d, P.V[x].copy(), P.V[v].copy(), 'snap'))
@ -129,10 +152,11 @@ def owner_weld(pieces, hull, cut, tol):
"""pieces: list of (V, T, prov); one for a cut-in, two for a split. Returns welded pieces, stats, done ops, info."""
Ps = [Piece(*p) for p in pieces]
infos = []
cand = []
cand = []; corners = []
for P in Ps:
loops, le, info = analyse_loops(P.V, np.array([f for f in P.faces]), np.array(P.prov), hull, cut, 'along', tol)
infos.append((loops, info)); cand += candidates(P, info, loops, hull, tol)
infos.append((loops, info)); cand += candidates(P, info, loops, hull, tol, cut_is_flat=cut.kind in ('box', 'obox'))
corners.extend(corners_of(P, info, hull, cut, tol))
cand.sort(key=lambda z: z[0])
stats = defaultdict(int); done = []; seen = set()
for d, pv, pw, kind in cand:
@ -140,6 +164,8 @@ def owner_weld(pieces, hull, cut, tol):
if key in seen:
continue
seen.add(key); stats['cand_' + kind] += 1
if any(np.linalg.norm(pv - q) < tol for q in corners):
stats['protected_corner'] += 1; continue
plan, reason = [], None
for P in Ps:
v = P.at(pv, tol); w = P.at(pw, tol)
@ -165,4 +191,9 @@ def owner_weld(pieces, hull, cut, tol):
else:
P.apply_move(v, w)
stats['applied_' + kind] += 1; done.append((pv, pw))
return [P.result() for P in Ps], dict(stats), done, infos
res = [P.result() for P in Ps]
kept = 0
for q in corners:
kept += int(any(np.linalg.norm(Vr[np.unique(Tr)] - q, axis=1).min() < tol for Vr, Tr, _ in res))
stats['corners'] = len(corners); stats['corners_kept_in_place'] = kept
return res, dict(stats), done, infos

View file

@ -22,7 +22,7 @@ for e in me.edges:
w = att.data[e.index].value if att else 0.0
if w > 0:
segs.append((cage.matrix_world @ me.vertices[e.vertices[0]].co, cage.matrix_world @ me.vertices[e.vertices[1]].co, w))
print('CAGE weighted edges', len(segs), 'smooth', [p.use_smooth for p in me.polygons][:1])
print('CAGE clamp_overlap', bev_src.use_clamp_overlap, 'harden', bev_src.harden_normals, 'WN', wn_src.mode, wn_src.keep_sharp, 'weighted edges', len(segs), 'smooth', [p.use_smooth for p in me.polygons][:1])
# Diagnostic: compare the cage's bevel-weight edges with the crease edges exported with the hull fixture.
fx = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'fixtures')
HV = [Vector(tuple(map(float, l.split()[1:4]))) for l in open(fx + '/hull_nobevel.obj') if l.startswith('v ')]
@ -102,6 +102,7 @@ def build(stem, tubes):
for k in ('width', 'segments', 'limit_method', 'offset_type', 'profile', 'harden_normals', 'miter_outer', 'miter_inner', 'clamp_overlap', 'loop_slide'):
if hasattr(bev_src, k):
setattr(b, k, getattr(bev_src, k))
b.use_clamp_overlap = bev_src.use_clamp_overlap if os.environ.get('CLAMP0') != '1' else False
w = ob.modifiers.new('WeightedNormal', 'WEIGHTED_NORMAL')
for k in ('mode', 'weight', 'thresh', 'keep_sharp', 'use_face_influence'):
if hasattr(wn_src, k):

View file

@ -0,0 +1,280 @@
"""Round 3 (Self's calls): a flat split keeps its seam exactly on the cutter plane. Hull vertices survive; hull-side
slivers along the seam get edge flips only (loop and crease edges constrained); each cap polygon is re-triangulated
with a constrained Delaunay (loop as boundary), plain ('p') or with interior-only Steiner refinement ('pq<deg>Y').
Usage: round3.py <fixtures> <cases.json> <runs> <out-dir>. Scratch. Uses the `triangle` package (Shewchuk's Triangle)."""
import sys, os, json, numpy as np
from collections import defaultdict
from scipy.spatial import cKDTree
import triangle
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from geo import *
from weld import Hull, Cutter, loops_of, densify, angles
fx, casesf, runs, outd = sys.argv[1:5]
os.makedirs(outd, exist_ok=True)
hull = Hull(fx); TOL = 1e-6 * hull.diag
cases = {c['name']: c for c in json.load(open(casesf))}
FLIP_DIHEDRAL_COS = np.cos(np.radians(20))
def load_run(name):
V, F = load_obj(f'{runs}/boolmesh_{name}.obj'); T = triangulate(F)
return V, T, np.array([int(x) for x in open(f'{runs}/boolmesh_{name}.prov').read().split()])
def tri_min_angle(V, t):
return min(angles(V[list(t)]))
def flip_hull(V, T, prov, loopedges, band_faces):
"""Greedy Delaunay-style flips on hull-provenance faces near the loop. Constrained: loop edges, hull crease edges.
A flip must raise the pair's worst angle, keep both normals within 20 deg of the old ones, and not duplicate an edge."""
T = [list(t) for t in T]; nflip = 0
crease_pos = hull.crease
for _ in range(20):
ef = defaultdict(list)
for fi, t in enumerate(T):
for k in range(3):
a, b = t[k], t[(k + 1) % 3]; ef[(min(a, b), max(a, b))].append(fi)
changed = False
for e, fs in ef.items():
if len(fs) != 2 or e in loopedges:
continue
f1, f2 = fs
if prov[f1] or prov[f2] or not (f1 in band_faces or f2 in band_faces):
continue
he = hull.edge_of(V[e[0]], TOL), hull.edge_of(V[e[1]], TOL)
if any(h is not None and h in crease_pos for h in he) and he[0] == he[1]:
continue
a, b = e; c = [x for x in T[f1] if x not in e][0]; d = [x for x in T[f2] if x not in e][0]
if (min(c, d), max(c, d)) in ef:
continue
old = min(tri_min_angle(V, T[f1]), tri_min_angle(V, T[f2]))
# keep orientation: T[f1] has a->b in some order; build new faces with the same winding
t1 = T[f1]; i = t1.index(c); n1 = [t1[i], t1[(i + 1) % 3], t1[(i + 2) % 3]] # c, p, q
p, q = n1[1], n1[2]
new1, new2 = [c, p, d], [c, d, q]
new = min(tri_min_angle(V, new1), tri_min_angle(V, new2))
if new <= old + 1e-9:
continue
on = [np.cross(V[t[1]] - V[t[0]], V[t[2]] - V[t[0]]) for t in (T[f1], T[f2])]
nn = [np.cross(V[t[1]] - V[t[0]], V[t[2]] - V[t[0]]) for t in (new1, new2)]
on = [x / np.linalg.norm(x) for x in on]; nn = [x / max(np.linalg.norm(x), 1e-300) for x in nn]
if min(o @ n for o in on for n in nn) < FLIP_DIHEDRAL_COS:
continue
T[f1], T[f2] = new1, new2; nflip += 1; changed = True
break # edge map is stale after a flip
if not changed:
break
return np.array(T), nflip
def flip_hull_all(V, T, prov, loopedges, band_faces, cap=4000):
total = 0
for _ in range(cap // 20):
T, n = flip_hull(V, T, prov, loopedges, band_faces); total += n
if n == 0:
break
return T, total
def cdt_caps(V, T, prov, cut, opts):
"""Replace every cap (cutter-provenance) face set with a CDT of its boundary loop in the cutter plane."""
loops, _ = loops_of(T, prov)
capF = np.where(prov == 1)[0]
old_n = face_normals(V, T[capF])[0].mean(0)
keep = [i for i in range(len(T)) if prov[i] == 0]
newV = [V]; newT = []; nv = len(V)
for L in loops:
P = V[L]; n, _ = cut.frame(P.mean(0))
e1 = np.cross(n, [1, 0, 0] if abs(n[0]) < .9 else [0, 1, 0]); e1 /= np.linalg.norm(e1); e2 = np.cross(n, e1)
o = P.mean(0); P2 = np.stack([(P - o) @ e1, (P - o) @ e2], 1)
seg = np.array([[i, (i + 1) % len(L)] for i in range(len(L))])
r = triangle.triangulate({'vertices': P2, 'segments': seg}, opts)
rv, rt = r['vertices'], r['triangles']
assert np.allclose(rv[:len(L)], P2), 'Triangle moved a boundary vertex'
idx = list(L) + list(range(nv, nv + len(rv) - len(L)))
if len(rv) > len(L):
newV.append(o + rv[len(L):, :1] * e1 + rv[len(L):, 1:2] * e2); nv += len(rv) - len(L)
for t in rt:
tt = [idx[k] for k in t]; newT.append(tt)
V2 = np.vstack(newV); NT = np.array(newT)
nn = face_normals(V2, NT)[0].mean(0)
if nn @ old_n < 0:
NT = NT[:, ::-1]
T2 = np.vstack([T[keep], NT]); P2 = np.concatenate([prov[keep], np.ones(len(NT), int)])
return V2, T2, P2
def side_measures(V, T, prov, ref_loop_pts, plane=None):
loops, le = loops_of(T, prov); lv = sorted({v for L in loops for v in L})
h = float(np.median(hull.vlen[hull.kd.query(V[lv])[1]]))
kd = cKDTree(densify(V, loops)); cen = V[T].mean(1); inb = kd.query(cen)[0] < 2 * h
FN, FA = face_normals(V, T); lmax = np.linalg.norm(V[T] - V[np.roll(T, -1, 1)], axis=2).max(1)
sl = (lmax ** 2 / np.maximum(2 * FA, 1e-300) > 10) | (FA < 1e-4 * FA[prov == 0].mean())
ang = np.array([tri_min_angle(V, t) for t in T])
hs, cs = inb & (prov == 0), prov == 1
flips = 0
for fi in np.where(hs)[0]:
n, _, _ = hull.frame(cen[fi]); flips += int(FN[fi] @ n < 0)
out = dict(h=h, hull_band_tris=int(hs.sum()), hull_slivers=int((sl & hs).sum()), hull_min_angle=float(ang[hs].min()),
hull_p5=float(np.percentile(ang[hs], 5)), cap_tris=int(cs.sum()), cap_slivers=int((sl & cs).sum()),
cap_min_angle=float(ang[cs].min()) if cs.any() else None, flipped_hull=flips,
loop_verts=len(lv), loop_to_naive_max=float(cKDTree(ref_loop_pts).query(V[lv])[0].max()))
if plane is not None:
n, d = plane; out['seam_off_plane_max'] = float(np.abs(V[lv] @ n - d).max())
ls = [np.linalg.norm(V[a] - V[b]) for a, b in edge_faces(T) if a in set(lv) or b in set(lv)]
out['loop_edges_under_bevel'] = int(sum(l < 0.001 for l in ls))
return out
def save(stem, V, T, prov):
save_obj(f'{outd}/{stem}.obj', V, T)
_, le = loops_of(T, prov)
with open(f'{outd}/{stem}.loop', 'w') as f:
for a, b in le:
f.write(f'{a} {b} 0\n')
def boundary_loops(T):
nxt = {}
ef = edge_faces(T)
for t in T:
for k in range(3):
a, b = int(t[k]), int(t[(k + 1) % 3])
if len(ef[(min(a, b), max(a, b))]) == 1:
nxt[a] = b
loops, seen = [], set()
for s0 in list(nxt):
if s0 in seen:
continue
L = [s0]; seen.add(s0); x = nxt[s0]
while x != s0 and x not in seen:
L.append(x); seen.add(x); x = nxt[x]
loops.append(L)
return loops
def short_loop_merge(pieces, bevel_w=0.001):
"""Split only: merge consecutive seam vertices closer than the bevel width, both pieces jointly. Both endpoints lie
on the cutter plane, so the seam stays on it; only boolean-inserted seam vertices are merge sources (hull vertices
survive). Caps are dropped first (rebuilt afterwards by the CDT), so the guard sees hull faces only."""
import owner_weld as OW
Ps = [OW.Piece(V, T[p == 0], np.zeros(int((p == 0).sum()), int)) for V, T, p in pieces]
L = boundary_loops(np.array([f for f in Ps[0].faces]))
st = defaultdict(int); n = 0
for lp in L:
for i in range(len(lp)):
a, b = lp[i], lp[(i + 1) % len(lp)]
pa, pb = Ps[0].V[a], Ps[0].V[b]
if not (Ps[0].alive[a] and Ps[0].alive[b]) or np.linalg.norm(pa - pb) >= bevel_w:
continue
src, dst = (pb, pa) if hull.kd.query(pb)[0] > TOL else (pa, pb)
if hull.kd.query(src)[0] < TOL:
st['refused_both_hull_vertices'] += 1; continue
plan = []
for P in Ps:
v, w = P.at(src, TOL), P.at(dst, TOL)
if v is None or w is None:
plan = None; st['refused_missing'] += 1; break
r = P.check_collapse(v, w)
if r:
plan = None; st['refused_' + r] += 1; break
plan.append((P, v, w))
if plan:
for P, v, w in plan:
P.apply_collapse(v, w)
n += 1
out = []
for P in Ps:
V, T, _ = P.result(); out.append((V, T))
return out, n, dict(st)
def cdt_boundary(V, T, cut, opts, flip_orient):
newV, newT, nv = [V], [], len(V)
for L in boundary_loops(T):
P = V[L]; n, _ = cut.frame(P.mean(0))
e1 = np.cross(n, [1, 0, 0] if abs(n[0]) < .9 else [0, 1, 0]); e1 /= np.linalg.norm(e1); e2 = np.cross(n, e1)
o = P.mean(0); P2 = np.stack([(P - o) @ e1, (P - o) @ e2], 1)
r = triangle.triangulate({'vertices': P2, 'segments': np.array([[i, (i + 1) % len(L)] for i in range(len(L))])}, opts)
idx = list(L) + list(range(nv, nv + len(r['vertices']) - len(L)))
if len(r['vertices']) > len(L):
newV.append(o + r['vertices'][len(L):, :1] * e1 + r['vertices'][len(L):, 1:2] * e2); nv += len(r['vertices']) - len(L)
# boundary runs a->b in the hull faces, so the cap must run b->a: orient each cap triangle against the loop
tri = [[idx[k] for k in t] for t in r['triangles']]
newT += tri
V2 = np.vstack(newV); NT = np.array(newT)
# orientation: the cap must use each boundary edge in the opposite direction to the hull face that owns it
hd = {(int(t[k]), int(t[(k + 1) % 3])) for t in T for k in range(3)}
bad = sum(1 for t in NT for k in range(3) if (int(t[k]), int(t[(k + 1) % 3])) in hd)
if bad:
NT = NT[:, ::-1]
return V2, np.vstack([T, NT]), np.concatenate([np.zeros(len(T), int), np.ones(len(NT), int)])
rows = []
for base in [b for b in ('split_fwd', 'probe_crease') if os.environ.get('SKIP_SPLITS') != '1']:
c = cases[base + '_sub']; cut = Cutter(c)
lo, hi = np.array(c['lo']), np.array(c['hi'])
pieces = {'sub': load_run(base + '_sub'), 'int': load_run(base + '_int')}
V0, T0, p0 = pieces['sub']; L0, _ = loops_of(T0, p0); refl = V0[sorted({v for L in L0 for v in L})]
n, _ = cut.frame(refl.mean(0)); plane = (n, float(refl.mean(0) @ n))
out = {}
for variant, opts in ((('naive', None), ('flip+cdt', 'p'), ('flip+cdt-q', 'pq20Y')) if os.environ.get('ONLY_SHORT') != '1' else (('naive', None),)):
for pk, (V, T, p) in pieces.items():
if variant != 'naive':
loops, le = loops_of(T, p); lset = {(min(a, b), max(a, b)) for a, b in le}
kd = cKDTree(densify(V, loops)); h = float(np.median(hull.vlen))
band = set(np.where(kd.query(V[T].mean(1))[0] < 2 * 0.0256)[0].tolist())
T1, nf = flip_hull_all(V, T, p, lset, band)
V2, T2, P2 = cdt_caps(V, T1, p, cut, opts)
else:
V2, T2, P2, nf = V, T, p, 0
m = side_measures(V2, T2, P2, refl, plane)
out[(variant, pk)] = (V2, T2, P2)
rows.append(dict(case=f'{base}_{pk}', variant=variant, flips_applied=nf, **m))
save(f'{base}_{pk}_{variant}', V2, T2, P2)
A, B = out[(variant, 'sub')], out[(variant, 'int')]
la = A[0][sorted({v for L in loops_of(A[1], A[2])[0] for v in L})]; lb = B[0][sorted({v for L in loops_of(B[1], B[2])[0] for v in L})]
rows.append(dict(case=base, variant=f'{variant} seam crack', crack=float(max(cKDTree(lb).query(la)[0].max(), cKDTree(la).query(lb)[0].max()))))
merged, nm, stm = short_loop_merge([pieces['sub'], pieces['int']])
naive_dense = cKDTree(densify(V0, L0, 64)); res = {}
for pk, (Vm, Tm) in zip(('sub', 'int'), merged):
p0 = np.zeros(len(Tm), int); L = boundary_loops(Tm)
lset = {(min(a, b), max(a, b)) for lp in L for a, b in zip(lp, lp[1:] + lp[:1])}
kd = cKDTree(densify(Vm, L)); band = set(np.where(kd.query(Vm[Tm].mean(1))[0] < 2 * 0.0256)[0].tolist())
T1, nf = flip_hull_all(Vm, Tm, p0, lset, band)
V2, T2, P2 = cdt_boundary(Vm, T1, cut, 'pq20Y', None)
m = side_measures(V2, T2, P2, refl, plane)
chord = float(naive_dense.query(densify(V2, loops_of(T2, P2)[0], 16))[0].max())
rows.append(dict(case=f'{base}_{pk}', variant='short+flip+cdt-q', flips_applied=nf, merges=nm, merge_stats=stm,
seam_chord_dev=chord, seam_chord_dev_pct=chord / hull.diag * 100, **m))
save(f'{base}_{pk}_short+flip+cdt-q', V2, T2, P2); res[pk] = (V2, T2, P2)
A, B = res['sub'], res['int']
la = A[0][sorted({v for L in loops_of(A[1], A[2])[0] for v in L})]; lb = B[0][sorted({v for L in loops_of(B[1], B[2])[0] for v in L})]
rows.append(dict(case=base, variant='short+flip+cdt-q seam crack', crack=float(max(cKDTree(lb).query(la)[0].max(), cKDTree(la).query(lb)[0].max()))))
print('done', base, nm, stm, flush=True)
# Box cut-ins into the curved hull: the flat faces own nothing (the hull owns every stretch, corners kept), so the
# hull-side repair is the same edge flips; the pocket walls and floor are the cutter's own faces, not a cap.
for base in [b for b in ('probe_box_flank', 'probe_box_ridge') if b in cases]:
c = cases[base]; cut = Cutter(c); V, T, p = load_run(base)
L0, le = loops_of(T, p); refl = V[sorted({v for L in L0 for v in L})]
lset = {(min(a, b), max(a, b)) for a, b in le}
kd = cKDTree(densify(V, L0)); band = set(np.where(kd.query(V[T].mean(1))[0] < 2 * 0.0256)[0].tolist())
for variant in ('naive', 'flip'):
T2, nf = (T, 0) if variant == 'naive' else flip_hull_all(V, T, p, lset, band)
m = side_measures(V, T2, p, refl)
rows.append(dict(case=base, variant=variant, flips_applied=nf, **m)); save(f'{base}_{variant}', V, T2, p)
print('done', base, flush=True)
json.dump(rows, open(f'{outd}/measures3.json', 'w'), indent=1, default=float)
cols = ['case', 'variant', 'merges', 'seam_chord_dev_pct', 'flips_applied', 'hull_band_tris', 'hull_slivers', 'hull_min_angle', 'hull_p5', 'cap_tris', 'cap_slivers',
'cap_min_angle', 'flipped_hull', 'seam_off_plane_max', 'loop_to_naive_max', 'loop_edges_under_bevel', 'crack']
f = lambda v: '' if v is None else (f'{v:.3g}' if isinstance(v, float) else str(v))
with open(f'{outd}/table3.md', 'w') as o:
o.write('| ' + ' | '.join(cols) + ' |\n|' + '---|' * len(cols) + '\n')
for r in rows:
o.write('| ' + ' | '.join(f(r.get(k)) for k in cols) + ' |\n')
print(open(f'{outd}/table3.md').read())

View file

@ -46,6 +46,8 @@ def row(name, variant, V, T, prov, c, ref, extra):
for name, c in cases.items():
if os.environ.get('CASES') and name not in os.environ['CASES'].split(','):
continue
if c['kind'] == 'box' and c['op'] == 'int':
continue
cut = Cutter(c)

View file

@ -87,6 +87,8 @@ class Cutter:
def __init__(s, c):
s.c = c; s.kind = c['kind']
Vc, Fc = load_obj(c['cutter']); s.V, s.T = Vc, triangulate(Fc); s.adj = vertex_adj(s.T, len(Vc))
if s.kind == 'obox':
s.p = np.array(c['p']); s.R = np.array(c['R']); s.lo = np.array(c['lo']); s.hi = np.array(c['hi'])
if s.kind == 'cyl':
s.p, s.r = np.array(c['p']), c['r']
s.a = np.array(c['n']) / np.linalg.norm(c['n'])
@ -107,6 +109,10 @@ class Cutter:
d = x - s.p; er = d - (d @ s.a) * s.a; er = er / np.linalg.norm(er)
et = np.cross(s.a, er)
return er, np.outer(et, et) / s.r
if s.kind == 'obox':
xl = s.R.T @ (x - s.p); dd = np.concatenate([xl - s.lo, s.hi - xl]); i = int(np.argmin(np.abs(dd)))
nl = np.zeros(3); nl[i % 3] = -1 if i < 3 else 1
return s.R @ nl, np.zeros((3, 3))
lo, hi = np.array(s.c['lo']), np.array(s.c['hi'])
dd = np.concatenate([x - lo, hi - x]); i = int(np.argmin(np.abs(dd)))
n = np.zeros(3); n[i % 3] = -1 if i < 3 else 1

View file

@ -0,0 +1,24 @@
| case | variant | applied | tris_band | slivers_band | min_angle_band | p5_angle_band | flipped_hull_band | dev_max_pct | dev_max_h | outside_band_moved | shortest_loop_edge | loop_edges_under_bevel | hull_owned | cutter_owned | split_loop_gap | hull_vertex_surface_dev_h |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| port_flank_s16 | naive | | 157 | 32 | 0.00707 | 1.14 | 0 | 0.0198 | 0.00961 | 0 | 5.29e-05 | 6 | | | | |
| port_flank_s16 | owner-weld[r3a5] | 12 | 133 | 5 | 5.09 | 6.68 | 0 | 0.0198 | 0.00961 | 0 | 0.00226 | 0 | 0 | 36 | | 0.0175 |
| split_fwd_sub | naive | | 507 | 97 | 0.0584 | 1.46 | 0 | 0 | 0 | 0 | 0.000263 | 15 | | | | |
| split_fwd_sub | owner-weld[r3a5] | 0 | 507 | 97 | 0.0584 | 1.46 | 0 | 0 | 0 | 0 | 0.000263 | 15 | 124 | 0 | | 0 |
| split_fwd_int | naive | | 445 | 92 | 0.0555 | 1.52 | 0 | 0 | 0 | 0 | 0.000263 | 14 | | | | |
| split_fwd_int | owner-weld[r3a5] | 0 | 445 | 92 | 0.0555 | 1.52 | 0 | 0 | 0 | 0 | 0.000263 | 14 | 124 | 0 | | 0 |
| split_fwd_sub | seam-gap-naive[r3a5] | | | | | | | | | | | | | | 0 | |
| split_fwd_sub | seam-gap-welded[r3a5] | | | | | | | | | | | | | | 0 | |
| probe_crease_sub | naive | | 342 | 44 | 0.0326 | 2.41 | 0 | 0 | 0 | 0 | 1.46e-05 | 12 | | | | |
| probe_crease_sub | owner-weld[r3a5] | 0 | 342 | 44 | 0.0326 | 2.41 | 0 | 0 | 0 | 0 | 1.46e-05 | 12 | 84 | 0 | | 0 |
| probe_crease_int | naive | | 319 | 47 | 0.0326 | 2.7 | 0 | 0 | 0 | 0 | 1.46e-05 | 11 | | | | |
| probe_crease_int | owner-weld[r3a5] | 0 | 319 | 47 | 0.0326 | 2.7 | 0 | 0 | 0 | 0 | 1.46e-05 | 11 | 84 | 0 | | 0 |
| probe_crease_sub | seam-gap-naive[r3a5] | | | | | | | | | | | | | | 0 | |
| probe_crease_sub | seam-gap-welded[r3a5] | | | | | | | | | | | | | | 0 | |
| probe_switch_s48 | naive | | 488 | 112 | 1.21e-06 | 0.624 | 0 | 0.0128 | 0.00624 | 0 | 6.38e-06 | 31 | | | | |
| probe_switch_s48 | owner-weld[r3a5] | 65 | 364 | 53 | 1.21e-06 | 2.65 | 0 | 0.323 | 0.157 | 0 | 4.71e-05 | 16 | 24 | 108 | | 0.021 |
| probe_crease_cyl_s16 | naive | | 206 | 27 | 0.208 | 2.6 | 0 | 0.0434 | 0.0146 | 0 | 0.000247 | 4 | | | | |
| probe_crease_cyl_s16 | owner-weld[r3a5] | 27 | 133 | 10 | 4.31 | 6.83 | 0 | 0.0609 | 0.0229 | 0 | 0.00135 | 0 | 6 | 35 | | 0.00665 |
| probe_box_flank | naive | | 105 | 3 | 0.285 | 6.62 | 0 | 0 | 0 | 0 | 0.0025 | 0 | | | | |
| probe_box_flank | owner-weld[r3a5] | 2 | 103 | 2 | 0.285 | 9.61 | 0 | 0.0108 | 0.00534 | 0 | 0.0025 | 0 | 16 | 0 | | 0 |
| probe_box_ridge | naive | | 314 | 67 | 0.0703 | 1.36 | 0 | 0 | 0 | 0 | 0.000637 | 4 | | | | |
| probe_box_ridge | owner-weld[r3a5] | 1 | 312 | 66 | 0.0703 | 1.41 | 0 | 0.00142 | 0.000687 | 0 | 0.000637 | 3 | 69 | 0 | | 0 |

View file

@ -0,0 +1,6 @@
| case | variant | flips_applied | hull_band_tris | hull_slivers | hull_min_angle | hull_p5 | cap_tris | cap_slivers | cap_min_angle | flipped_hull | seam_off_plane_max | loop_to_naive_max | loop_edges_under_bevel | crack |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| probe_box_flank | naive | 0 | 83 | 2 | 0.952 | 8.55 | 22 | 1 | 0.285 | 0 | | 0 | 0 | |
| probe_box_flank | flip | 24 | 84 | 0 | 6 | 12.1 | 22 | 1 | 0.285 | 0 | | 0 | 0 | |
| probe_box_ridge | naive | 0 | 243 | 19 | 0.715 | 4.9 | 71 | 48 | 0.0703 | 0 | | 0 | 4 | |
| probe_box_ridge | flip | 118 | 243 | 7 | 1.75 | 7.24 | 71 | 48 | 0.0703 | 0 | | 0 | 4 | |

View file

@ -0,0 +1,18 @@
| case | variant | merges | seam_chord_dev_pct | flips_applied | hull_band_tris | hull_slivers | hull_min_angle | hull_p5 | cap_tris | cap_slivers | cap_min_angle | flipped_hull | seam_off_plane_max | loop_to_naive_max | loop_edges_under_bevel | crack |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| split_fwd_sub | naive | | | 0 | 410 | 37 | 0.676 | 3.86 | 122 | 85 | 0.0584 | 0 | 3.05e-16 | 0 | 15 | |
| split_fwd_int | naive | | | 0 | 348 | 32 | 0.539 | 4.36 | 122 | 85 | 0.0555 | 0 | 3.05e-16 | 0 | 14 | |
| split_fwd | naive seam crack | | | | | | | | | | | | | | | 0 |
| split_fwd_sub | short+flip+cdt-q | 3 | 0.00692 | 138 | 407 | 26 | 0.676 | 5.3 | 313 | 1 | 5.5 | 0 | 3.05e-16 | 0 | 16 | |
| split_fwd_int | short+flip+cdt-q | 3 | 0.00692 | 99 | 343 | 17 | 0.539 | 6.69 | 313 | 1 | 5.5 | 0 | 3.05e-16 | 0 | 16 | |
| split_fwd | short+flip+cdt-q seam crack | | | | | | | | | | | | | | | 0 |
| probe_crease_sub | naive | | | 0 | 260 | 22 | 0.377 | 3.73 | 82 | 22 | 0.0326 | 0 | 7.22e-16 | 0 | 12 | |
| probe_crease_int | naive | | | 0 | 237 | 25 | 0.0405 | 5.61 | 82 | 22 | 0.0326 | 0 | 7.22e-16 | 0 | 11 | |
| probe_crease | naive seam crack | | | | | | | | | | | | | | | 0 |
| probe_crease_sub | short+flip+cdt-q | 5 | 0.00404 | 65 | 255 | 12 | 2.41 | 6.64 | 147 | 3 | 0.714 | 0 | 7.22e-16 | 0 | 6 | |
| probe_crease_int | short+flip+cdt-q | 5 | 0.00404 | 79 | 234 | 10 | 0.0405 | 7.14 | 147 | 3 | 0.714 | 0 | 7.22e-16 | 0 | 5 | |
| probe_crease | short+flip+cdt-q seam crack | | | | | | | | | | | | | | | 0 |
| probe_box_flank | naive | | | 0 | 83 | 2 | 0.952 | 8.55 | 22 | 1 | 0.285 | 0 | | 0 | 0 | |
| probe_box_flank | flip | | | 24 | 84 | 0 | 6 | 12.1 | 22 | 1 | 0.285 | 0 | | 0 | 0 | |
| probe_box_ridge | naive | | | 0 | 243 | 19 | 0.715 | 4.9 | 71 | 48 | 0.0703 | 0 | | 0 | 4 | |
| probe_box_ridge | flip | | | 118 | 243 | 7 | 1.75 | 7.24 | 71 | 48 | 0.0703 | 0 | | 0 | 4 | |

View file

@ -0,0 +1,20 @@
| case | variant | flips_applied | hull_band_tris | hull_slivers | hull_min_angle | hull_p5 | cap_tris | cap_slivers | cap_min_angle | flipped_hull | seam_off_plane_max | loop_to_naive_max | loop_edges_under_bevel | crack |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| split_fwd_sub | naive | 0 | 410 | 37 | 0.676 | 3.86 | 122 | 85 | 0.0584 | 0 | 3.05e-16 | 0 | 15 | |
| split_fwd_int | naive | 0 | 348 | 32 | 0.539 | 4.36 | 122 | 85 | 0.0555 | 0 | 3.05e-16 | 0 | 14 | |
| split_fwd | naive seam crack | | | | | | | | | | | | | 0 |
| split_fwd_sub | flip+cdt | 138 | 410 | 28 | 0.676 | 4.69 | 122 | 80 | 0.122 | 0 | 3.05e-16 | 0 | 15 | |
| split_fwd_int | flip+cdt | 99 | 346 | 18 | 0.539 | 5.97 | 122 | 80 | 0.122 | 0 | 3.05e-16 | 0 | 14 | |
| split_fwd | flip+cdt seam crack | | | | | | | | | | | | | 0 |
| split_fwd_sub | flip+cdt-q | 138 | 410 | 28 | 0.676 | 4.69 | 332 | 0 | 6.76 | 0 | 3.05e-16 | 0 | 23 | |
| split_fwd_int | flip+cdt-q | 99 | 346 | 18 | 0.539 | 5.97 | 332 | 0 | 6.76 | 0 | 3.05e-16 | 0 | 22 | |
| split_fwd | flip+cdt-q seam crack | | | | | | | | | | | | | 0 |
| probe_crease_sub | naive | 0 | 260 | 22 | 0.377 | 3.73 | 82 | 22 | 0.0326 | 0 | 7.22e-16 | 0 | 12 | |
| probe_crease_int | naive | 0 | 237 | 25 | 0.0405 | 5.61 | 82 | 22 | 0.0326 | 0 | 7.22e-16 | 0 | 11 | |
| probe_crease | naive seam crack | | | | | | | | | | | | | 0 |
| probe_crease_sub | flip+cdt | 67 | 260 | 19 | 0.377 | 4.25 | 82 | 21 | 0.575 | 0 | 7.22e-16 | 0 | 13 | |
| probe_crease_int | flip+cdt | 79 | 238 | 11 | 0.0405 | 7.1 | 82 | 21 | 0.575 | 0 | 7.22e-16 | 0 | 12 | |
| probe_crease | flip+cdt seam crack | | | | | | | | | | | | | 0 |
| probe_crease_sub | flip+cdt-q | 67 | 260 | 19 | 0.377 | 4.25 | 162 | 4 | 0.714 | 0 | 7.22e-16 | 0 | 18 | |
| probe_crease_int | flip+cdt-q | 79 | 238 | 11 | 0.0405 | 7.1 | 162 | 4 | 0.714 | 0 | 7.22e-16 | 0 | 17 | |
| probe_crease | flip+cdt-q seam crack | | | | | | | | | | | | | 0 |