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>
|
|
@ -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
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 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
|
||||
|
|
|
|||
BIN
docs/research/spike-boolean-weld/r3_longinus_vs_split.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3boxflank.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3boxridge.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3clamp0.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3crease.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3port16.jpg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3split.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3splitwide.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/research/spike-boolean-weld/r3_r3switch.jpg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
20
spikes/boolean-weld/cutters/probe_box_flank.obj
Normal 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
|
||||
20
spikes/boolean-weld/cutters/probe_box_ridge.obj
Normal 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
|
||||
20
spikes/boolean-weld/py/case_obox.py
Normal 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')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
280
spikes/boolean-weld/py/round3.py
Normal 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())
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
24
spikes/boolean-weld/results/round3/table2-owner-r3a5.md
Normal 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 |
|
||||
6
spikes/boolean-weld/results/round3/table3-boxes-flip.md
Normal 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 | |
|
||||
|
|
@ -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 | |
|
||||
20
spikes/boolean-weld/results/round3/table3-splits-flip-cdt.md
Normal 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 |
|
||||