We present pip_mouth, a novel algorithm for point-in-polygon testing on simple polygons. The algorithm is the geometric dual of ear clipping: where ear clipping iteratively removes ears — convex vertices whose triangles lie entirely inside the polygon — pip_mouth iteratively removes mouths — reflex vertices whose triangles lie entirely outside.
A point is tested against each mouth triangle as it is removed. Since mouth triangles are guaranteed external, a point inside such a triangle is immediately classified as outside the polygon. After all mouths are consumed, the reduced polygon is convex and a direct convex test is applied.
Correctness rests on the Mouth Lemma: every simple polygon with at least one reflex vertex has at least two valid mouths. We prove this by duality — the reversed polygon R is also simple, its ears correspond exactly to the mouths of P, and Meisters' Theorem (1975) guarantees R has at least two ears.
1. The concept of a mouth as the geometric dual of an ear.
2. The Mouth Lemma: every simple polygon with ≥1 reflex vertex has ≥2 mouths.
3. A proof of the Mouth Lemma via the reversed polygon and Meisters' Theorem.
4. The pip_mouth algorithm with formal correctness argument.
5. Identification that chord_ok is necessary for mouths but redundant for ears — an asymmetry not previously noted explicitly.
— Degenerate cases: collinear vertices, vertices on polygon edges.
— Can O(n²) be achieved by maintaining a mouth list dynamically?
— Does an analogous "mouth-based triangulation" exist with useful properties?
Let P be a simple polygon with vertices listed in CCW order. The signed area
(shoelace formula) is positive for CCW polygons. A vertex V with neighbors A (prev) and B (next)
is reflex if the interior angle exceeds 180°, equivalently if
cross(A, V, B) < 0 for CCW orientation.
A vertex V of a simple polygon P is a mouth if: (1) V is reflex; (2) the chord AB does not properly intersect any non-adjacent edge of P; (3) no vertex of P other than A, V, B lies inside or on the triangle △AVB.
If V is a mouth of simple polygon P, then the triangle △AVB lies entirely outside P (its interior does not intersect the interior of P).
Every simple polygon P with n ≥ 3 vertices and at least one reflex vertex has at least two mouths.
cross(A,V,B) < 0 in P (reflex) has
cross(B,V,A) > 0 in R (convex). So:
reflex vertices of P = convex vertices of R.pip_mouth correctly classifies a query point p as inside or outside any simple polygon P.
false is correct. ✓// pip_mouth: point-in-polygon via mouth removal // Input: p — query point // P — simple polygon, vertices in any order // Output: true if p is strictly inside P function pip_mouth(p, P): P ← ensure_ccw(P) // normalize via shoelace area while |P| ≥ 3: M ← first_mouth(P) // find first valid mouth if M = null: // no reflex vertices remain return in_convex(p, P) // P is now convex A, B ← neighbors(M, P) if in_triangle(p, A, M, B): return false // p is in external triangle → outside P ← P \ {M} // remove mouth, polygon stays simple return false
// is_mouth: three necessary and sufficient conditions function is_mouth(V, P): A ← prev(V, P), B ← next(V, P) // 1. V is reflex (CCW: interior cross product negative) if cross(A, V, B) ≥ 0: return false // 2. Chord AB does not cross any non-adjacent edge of P // (NOT implied by condition 3 for mouths) if ¬chord_ok(V, P): return false // 3. No other vertex of P lies inside △AVB for each vertex Q ∈ P \ {A, V, B}: if in_triangle(Q, A, V, B): return false return true
// cross product: (A-O)×(B-O) function cross(O, A, B) { return (A.x-O.x)*(B.y-O.y) - (A.y-O.y)*(B.x-O.x); } // ensure CCW orientation via shoelace area function ensure_ccw(poly) { let a = 0; for (let i=0; i<poly.length; i++) { const j = (i+1) % poly.length; a += poly[i].x*poly[j].y - poly[j].x*poly[i].y; } return a > 0 ? [...poly] : [...poly].reverse(); } function is_mouth(vi, poly) { const n=poly.length, I=i=>((i%n)+n)%n; const A=poly[I(vi-1)], V=poly[vi], B=poly[I(vi+1)]; if (cross(A,V,B) >= 0) return false; // not reflex if (!chord_ok(vi, poly)) return false; // chord crosses edge for (let j=0; j<n; j++) { if (j===I(vi-1)||j===vi||j===I(vi+1)) continue; if (in_tri(poly[j],A,V,B)) return false; // vertex inside △ } return true; } function pip_mouth(p, poly) { poly = ensure_ccw([...poly]); while (poly.length >= 3) { const n=poly.length, I=i=>((i%n)+n)%n; let mi = -1; for (let i=0; i<n; i++) if (is_mouth(i,poly)){mi=i;break;} if (mi === -1) return in_convex_ccw(p, poly); if (in_tri(p, poly[I(mi-1)], poly[mi], poly[I(mi+1)])) return false; poly.splice(mi, 1); } return false; }