108 lines
5.2 KiB
Text
108 lines
5.2 KiB
Text
|
|
(ns arthur.domain.ring-test
|
||
|
|
"The ring-simplicity check exists because \"fixed topology\" is load-bearing in
|
||
|
|
docs/design.md: because hold parts CUT between poses rather than
|
||
|
|
interpolating, a ring whose vertex order is wrong self-intersects and renders
|
||
|
|
as blocks meeting at corners. It is invisible at some vertex counts and obvious
|
||
|
|
at others, so it needs an assertion rather than an eyeball."
|
||
|
|
(:require [cljs.test :refer [deftest is testing]]
|
||
|
|
[arthur.domain.landmarks :as lm]
|
||
|
|
[arthur.domain.ring :as ring]
|
||
|
|
[arthur.synth :as synth]))
|
||
|
|
|
||
|
|
(def dense (delay (synth/synth-dense 72)))
|
||
|
|
|
||
|
|
(defn- first-bad-budget
|
||
|
|
"The first [budget frame edge] at which `table`'s subsampled ring
|
||
|
|
self-intersects on the synthetic track, or nil. Reported rather than asserted
|
||
|
|
per-frame so a failure names the case instead of only the count."
|
||
|
|
[table budgets]
|
||
|
|
(first
|
||
|
|
(for [n budgets
|
||
|
|
:let [slots (ring/subsample-slots (count table) n)]
|
||
|
|
[f fr] (map-indexed vector @dense)
|
||
|
|
:let [pts (mapv #(nth fr (nth table %)) slots)
|
||
|
|
hits (ring/self-intersections pts)]
|
||
|
|
:when (seq hits)]
|
||
|
|
{:verts n :frame f :edges (first hits)})))
|
||
|
|
|
||
|
|
(deftest subsample-preserves-order-and-count
|
||
|
|
(doseq [n (range 4 17 2)]
|
||
|
|
(let [s (ring/subsample-slots 20 n)]
|
||
|
|
(is (= n (count s)) (str "subsample-slots(20," n ") returns n slots"))
|
||
|
|
(is (apply < s) (str "subsample-slots(20," n ") is strictly increasing: " (pr-str s))))))
|
||
|
|
|
||
|
|
(deftest subsample-ring-agrees-with-subsample-slots
|
||
|
|
(is (= (ring/subsample-ring lm/LIPS-OUTER 8)
|
||
|
|
(mapv #(nth lm/LIPS-OUTER %) (ring/subsample-slots 20 8)))))
|
||
|
|
|
||
|
|
(deftest lid-ring-carries-its-own-corners
|
||
|
|
;; Subsampling a 16-slot ring to any even budget must keep the two corners at
|
||
|
|
;; output indices 0 and n/2. That is what lets the socket be read back off the
|
||
|
|
;; drawn polygon instead of measured separately, which is what stops the iris
|
||
|
|
;; drifting relative to the eye it sits in.
|
||
|
|
(doseq [n (range 4 13 2)]
|
||
|
|
(let [sl (ring/subsample-slots 16 n)]
|
||
|
|
(is (and (= 0 (nth sl 0)) (= 8 (nth sl (quot n 2))))
|
||
|
|
(str "n=" n " -> " (pr-str sl))))))
|
||
|
|
|
||
|
|
(deftest lip-rings-are-simple-at-every-vertex-budget
|
||
|
|
(doseq [[label table] [["outer" lm/LIPS-OUTER] ["inner" lm/LIPS-INNER]]]
|
||
|
|
(is (nil? (first-bad-budget table (range 4 17 2)))
|
||
|
|
(str label " ring self-intersects: "
|
||
|
|
(pr-str (first-bad-budget table (range 4 17 2)))))))
|
||
|
|
|
||
|
|
(deftest face-oval-is-a-simple-ring-on-every-frame
|
||
|
|
;; A wrong ordering here shows up as a lumpy plate rather than an obvious
|
||
|
|
;; bowtie, so it needs asserting.
|
||
|
|
(is (nil? (first-bad-budget lm/FACE-OVAL [(count lm/FACE-OVAL)]))))
|
||
|
|
|
||
|
|
(deftest eye-rings-are-simple-at-every-vertex-budget
|
||
|
|
;; Checked on blink frames too, where the ring is nearly degenerate.
|
||
|
|
(doseq [[label table] [["right" lm/EYE-R-RING] ["left" lm/EYE-L-RING]]]
|
||
|
|
(is (nil? (first-bad-budget table (range 4 13 2)))
|
||
|
|
(str label " eye ring self-intersects: "
|
||
|
|
(pr-str (first-bad-budget table (range 4 13 2)))))))
|
||
|
|
|
||
|
|
(deftest brow-rings-are-simple-at-every-vertex-budget
|
||
|
|
(doseq [[label table] [["A" lm/BROW-A-RING] ["B" lm/BROW-B-RING]]]
|
||
|
|
(is (nil? (first-bad-budget table (range 4 11 2)))
|
||
|
|
(str "brow " label " ring self-intersects: "
|
||
|
|
(pr-str (first-bad-budget table (range 4 11 2)))))))
|
||
|
|
|
||
|
|
(deftest offset-ring-grows-by-a-fixed-amount
|
||
|
|
;; offset-ring must grow by a FIXED amount and survive a degenerate ring - the
|
||
|
|
;; shut eyelid is exactly the degenerate case, and it is the frame where the
|
||
|
|
;; lash line is the entire drawing.
|
||
|
|
(let [sq [{:x -1 :y 0} {:x 0 :y -1} {:x 1 :y 0} {:x 0 :y 1}]
|
||
|
|
g (ring/offset-ring sq 2)]
|
||
|
|
(is (every? true?
|
||
|
|
(map (fn [p q] (< (abs (- (js/Math.hypot (:x p) (:y p))
|
||
|
|
(+ (js/Math.hypot (:x q) (:y q)) 2)))
|
||
|
|
1e-9))
|
||
|
|
g sq))
|
||
|
|
"offset-ring pushes every vertex out by exactly d")
|
||
|
|
(is (identical? sq (ring/offset-ring sq 0)) "offset-ring(0) is identity")))
|
||
|
|
|
||
|
|
(deftest offset-ring-survives-a-shut-lid
|
||
|
|
(let [shut-lid [{:x -10 :y 0} {:x 0 :y -0.02} {:x 10 :y 0} {:x 0 :y 0.02}]
|
||
|
|
band (ring/offset-ring shut-lid 1.5)
|
||
|
|
h (- (apply max (map :y band)) (apply min (map :y band)))]
|
||
|
|
(is (> h 2.9) (str "shut lid gets a visible lash band, height " h))
|
||
|
|
(is (ring/simple? band) "offset-ring keeps the shut lid simple")))
|
||
|
|
|
||
|
|
(deftest offset-ring-tolerates-a-vertex-on-the-centroid
|
||
|
|
;; A vertex sitting exactly on the centroid has no outward direction. Leave it
|
||
|
|
;; where it is rather than emitting NaN and poisoning the whole ring.
|
||
|
|
(let [degenerate [{:x 0 :y 0} {:x 0 :y 0} {:x 0 :y 0}]]
|
||
|
|
(is (every? #(and (not (js/isNaN (:x %))) (not (js/isNaN (:y %))))
|
||
|
|
(ring/offset-ring degenerate 3)))))
|
||
|
|
|
||
|
|
(deftest a-bowtie-is-detected
|
||
|
|
;; The detector has to actually fire, or every simplicity assertion above is
|
||
|
|
;; asserting nothing. Swapping two opposite vertices of a square is exactly
|
||
|
|
;; the bowtie the lip rings grew.
|
||
|
|
(let [square [{:x 0 :y 0} {:x 10 :y 0} {:x 10 :y 10} {:x 0 :y 10}]
|
||
|
|
bowtie [{:x 0 :y 0} {:x 10 :y 0} {:x 0 :y 10} {:x 10 :y 10}]]
|
||
|
|
(is (ring/simple? square))
|
||
|
|
(is (not (ring/simple? bowtie)) "a swapped pair of opposite vertices is caught")))
|