codex/geometry/convex_hull.cat
# Convex hull 2D (scan de Graham)
#
# Entrée : liste de points [x, y]
# Sortie : enveloppe convexe dans le sens anti-horaire
# Complexité : O(n log n) (tri polaire) + O(n) (scan)
builtins = import("builtins")
math = import("math")
# Produit vectoriel orienté (OA x OB)
cross = (o, a, b) => {
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
}
sqdist = (a, b) => {
dx = a[0] - b[0]
dy = a[1] - b[1]
dx * dx + dy * dy
}
# Point pivot : y minimal, puis x minimal
find_pivot = (points) => {
p = points[0]
i = 1
while i < len(points) {
q = points[i]
if q[1] < p[1] or (q[1] == p[1] and q[0] < p[0]) {
p = q
}
i = i + 1
}
p
}
graham_hull = (points) => {
if len(points) <= 1 {
points
} else {
pivot = find_pivot(points)
# Tri par angle polaire puis distance au pivot.
# Pour points colinéaires au même angle, le plus éloigné reste après le scan.
sorted_pts = builtins.sorted(
points,
key=(p) => {
tuple(
math.atan2(p[1] - pivot[1], p[0] - pivot[0]),
sqdist(pivot, p)
)
}
)
stack = list()
i = 0
while i < len(sorted_pts) {
p = sorted_pts[i]
# Ignore les duplicatas exacts consécutifs
if len(stack) > 0 {
last = stack[len(stack) - 1]
if last[0] == p[0] and last[1] == p[1] {
i = i + 1
continue
}
}
while len(stack) >= 2 and cross(stack[len(stack) - 2], stack[len(stack) - 1], p) <= 0 {
stack = stack[:len(stack) - 1]
}
stack = stack + list(p)
i = i + 1
}
stack
}
}
# Démo
points = list(
list(0, 0),
list(1, 1),
list(2, 2),
list(2, 0),
list(2, 4),
list(3, 3),
list(4, 2),
list(0, 3),
list(1, 2),
list(2, 2), # doublon
list(4, 2) # doublon
)
hull = graham_hull(points)
print("Points d'entrée :", len(points))
print("Hull (CCW) :", hull)
print("Taille hull :", len(hull))