arthur/frontend/test/arthur/domain/outline_test.cljs

63 lines
3 KiB
Text
Raw Normal View History

Draw with tools: a pen, a brush that makes polygons, and knockouts One tool at a time, picked from a strip down the left of the stage or by its letter, as in Photoshop, Illustrator and Flash: V selects and transforms, P is the pen, B the brush, E the eraser. The options bar above the stage holds the tool's settings, and the palette is a grid under the tools with the colour a new shape gets above it, as Deluxe Paint's was. The pen's draft is filled into the picture as it is drawn, so the edge on screen is the pixels the shape will be. Click the first point, Enter, Esc or leaving the pen closes it; the pen stays the tool. Editing a shape's points is the pen ON that shape — Figma's vector edit mode — so the separate points mode is gone: click an edge to add a point, ⌥-click one to delete it, on every key at once so tweens keep meaning something. The brush paints a mask, and what it will be is shown while painting: the stroke is traced round its pixel edges, holes and all, and simplified to a point every so many pixels of outline by the same function the saved shapes come from. A hole is bridged into the one ring along a whole-pixel row, which the fill never samples. Blender's Adjust Last Operation re-traces the last stroke from what it was made of. A shape coloured CLEAR is a knockout: its symbol is drawn into a layer of its own and the knockout clears it, every colour or one. The eraser makes one in the symbol it starts on, of the colour it starts on, or of every colour with ⌥. A click goes through a knockout to what shows. Previews are drawn in the stacking context of what they will land in. The polygon's points no longer stay behind when the shape is moved: they were read off the saved document while the box handles read the one mid-drag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:43:28 -04:00
(ns arthur.domain.outline-test
(:require [cljs.test :refer [deftest is]]
[arthur.domain.outline :as outline]
[arthur.domain.raster :as r]))
(defn- covered [{:keys [buf]}] (count (filter #(= 1 %) (array-seq buf))))
(deftest a-traced-ring-fills-back-to-exactly-the-mask
(let [m (outline/stamp! (outline/mask 40 30) [8 8] [30 20] 6)
[ring & more] (outline/rings m)
back (r/fill-poly-buf! (r/make 40 30) ring (quot (count ring) 2) 1)]
(is (empty? more) "one stroke is one piece")
(is (= (vec (array-seq (:buf m))) (vec (array-seq (:buf back)))))))
(deftest a-single-pixel-is-a-square
(let [m (outline/mask 5 5)]
(aset (:buf m) 12 1)
(is (= [[2 2 3 2 3 3 2 3]] (outline/rings m 0)))))
(deftest pieces-are-traced-apart-biggest-first
(let [m (-> (outline/mask 40 20)
(outline/stamp! [5 10] [5 10] 4)
(outline/stamp! [20 10] [34 10] 6))]
(is (= 2 (count (outline/rings m))))
(is (< 8 (covered m)))
(is (< 15 (apply min (take-nth 2 (first (outline/rings m))))) "the long one first")))
(deftest simplify-keeps-exactly-the-count-asked-for
(let [ring (first (outline/rings (outline/stamp! (outline/mask 60 60) [30 30] [30 30] 40)))]
(is (= 24 (count (outline/simplify ring 12))))
(is (= 6 (count (outline/simplify ring 1))) "never fewer than three points")
(is (= ring (outline/simplify ring 10000)))))
(deftest a-loop-keeps-its-hole-in-one-ring
;; A ring of brush round a gap: one piece, one hole, and the one polygon it
;; makes fills back to the ring and leaves the gap empty.
(let [m (reduce (fn [m a]
(let [p #(vector (+ 30 (* 15 (js/Math.cos %))) (+ 30 (* 15 (js/Math.sin %))))]
(outline/stamp! m (p a) (p (+ a 0.3)) 5)))
(outline/mask 60 60) (range 0 6.6 0.3))
[piece & more] (outline/pieces m)
fill (fn [ring] (:buf (r/fill-poly-buf! (r/make 60 60) ring (quot (count ring) 2) 1)))
middle (+ 30 (* 30 60))]
(is (empty? more))
(is (= 1 (count (:holes piece))))
(is (= (vec (array-seq (:buf m))) (vec (array-seq (fill (outline/polygon piece 0.01)))))
"unsimplified, the bridged ring is the stroke exactly")
(is (zero? (aget (fill (outline/polygon piece 0.01)) middle)) "the middle is a hole")
(is (zero? (aget (fill (outline/polygon piece 8)) middle)) "and simplified, still a hole")))
(deftest points-go-by-length
(let [dab (first (outline/pieces (outline/stamp! (outline/mask 80 40) [10 20] [10 20] 8)))
long (first (outline/pieces (outline/stamp! (outline/mask 80 40) [10 20] [70 20] 8)))]
(is (< (count (outline/polygon dab 6)) (count (outline/polygon long 6))))
(is (= 6 (count (outline/polygon dab 1000))) "never fewer than three")))
(deftest a-gap-open-to-the-outside-is-not-a-hole
(let [m (-> (outline/mask 40 40)
(outline/stamp! [5 5] [30 5] 4)
(outline/stamp! [30 5] [30 30] 4)
(outline/stamp! [30 30] [5 30] 4))]
(is (= [] (:holes (first (outline/pieces m)))))))