One word for one thing: it is a cel

Four words had accumulated for a node that puts a symbol inside another symbol.
`instance` was the document's, from the model. `placement` was the stage and
export work's. `occurrence` came in with the lane model. `exposure` came in with
me, because it is what an animator would say. Three bodies of work each brought
a word and none of them retired anybody else's, which is how you get a codebase
that reads like three people describing the same object over each other.

It is a CEL. One drawing, held for some duration. `cel` was already the view's
word — `.tl-cel`, the cel strip — so choosing it was also the smallest change,
and the app already says "drawing" for the content, which is what frees the word
up: historically a cel IS the celluloid with the drawing on it, and that sense
has somewhere else to live here.

  instance   the `:kind`. The general thing, anywhere in a document.
  cel        an instance in a lane. UI labels, command names, prose.
  lane       the group with `:layout :sequence`.
  drawing    the content a cel names.
  placement  kept ONLY for where a node sits — `nest/placement` and the
             transform that puts a face on the stage. Retired as a noun for the
             node itself.
  occurrence gone.

AND IT SETTLES A COLLISION I SHOULD HAVE SEEN EARLIER. `:time :expose` already
existed and means something else entirely: how many frames each step of a
subtree lasts, which is what shooting on twos is. Had the block been called an
exposure too, `node/expose`, `clock/exposed-frame` and `subs/render ::exposure`
would have been permanently confusable with it. Choosing `cel` lets the word
`exposure` keep the thing it actually names, and every remaining use of it in
`src` is now that one.

`:layout :sequence` stays as the field, and it is the one place two words are
kept deliberately: the layout names the RULE — children follow one another and
may not overlap — and a group carrying it is called a lane. `node/lane?` says so
where the two meet.

The second view is traditionally the exposure sheet. It will be the CEL SHEET,
for one vocabulary.

Renamed with a script and then read, because a blind pass does real damage: it
produced "an cel" thirty times, renamed the `::exposure` sub that is about the
`:expose` grid, and turned an "exposure grid" into a "cel grid" in two
docstrings. All three classes are fixed. `arthur.domain.sequence` is now
`arthur.domain.lane`, which is what its test file was already called.

424 tests, 5,749 assertions, and both browser flows — `test/browser/lane.mjs`,
renamed too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-30 19:43:18 -04:00
parent 76106d36ee
commit 598c186c4f
21 changed files with 427 additions and 419 deletions

View file

@ -9,7 +9,7 @@
[arthur.domain.node :as node]
[arthur.domain.palette :as pal]
[arthur.domain.pick :as pick]
[arthur.domain.sequence :as sequence]
[arthur.domain.lane :as lane]
[arthur.domain.symbol :as symbol]))
(defn drawing [id x frames]
@ -18,17 +18,17 @@
:channels {[:geom :size] (ch/framed 4)
[:xform :pos] (ch/framed [x 0])}}}})
(defn occurrence [id source at duration speed]
(defn cel [id source at duration speed]
{:id id :kind :instance :parent :girl :z (name id)
:source {:symbol source} :playback {:in 0 :speed speed :end :stop}
:time {:at at :rate 1} :span [0 duration]})
(defn document []
(let [a (occurrence :a :drawing-a 0 4 0)
b (assoc-in (occurrence :b :drawing-b 4 4 0)
(let [a (cel :a :drawing-a 0 4 0)
b (assoc-in (cel :b :drawing-b 4 4 0)
[:channels [:xform :pos]] (ch/keyed {0 [0 0] 1 [2 0]} :hold))
insert (assoc-in (occurrence :insert :wave 8 4 1) [:playback :in] 3)]
{:name "exposures" :fps 24 :width 320 :height 200
insert (assoc-in (cel :insert :wave 8 4 1) [:playback :in] 3)]
{:name "cels" :fps 24 :width 320 :height 200
:symbols
{:main {:id :main :frames 12
:nodes {:girl {:id :girl :kind :group :layout :sequence :z "b"
@ -58,9 +58,9 @@
(into {} (map (fn [[f ops]] [f (js/Math.round (:plate ops))])) at)))
(is (nil? (get-in at [4 [:a :mark]])) "half-open cuts have a single owner")))
(deftest exposure-ripple-keeps-lane-keys-and-moves-occurrence-corrections
(deftest cel-ripple-keeps-lane-keys-and-moves-cel-corrections
(let [doc (document)
result (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol})
result (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
after (:clip result)
nodes (get-in after [:symbols :main :nodes])]
(is (= :a (:selection result)))
@ -78,19 +78,19 @@
(is (= 320 (get-in at [10 [:insert :mark]])) "insert starts on source frame 3"))
(is (empty? (clip/problems after)))
(is (= (assoc-in doc [:symbols :main :frames] 14)
(:clip (sequence/extend-hold after :main :a -2 {})))
(:clip (lane/extend-hold after :main :a -2 {})))
"shrinking restores content, except the explicitly grown shot")))
(deftest overflow-and-invalid-edits-are-atomic
(let [doc (document)
result (sequence/extend-hold doc :main :a 2 {})]
result (lane/extend-hold doc :main :a 2 {})]
(is (:refused result))
(is (= 14 (:required-frames result)))
(is (not (contains? result :clip)))
(doseq [delta [0 -4 0.5 js/NaN]]
(is (:refused (sequence/extend-hold doc :main :a delta {}))))
(is (:refused (sequence/extend-hold doc :main :insert 1 {})))
(is (:refused (sequence/extend-hold doc :main :missing 1 {})))))
(is (:refused (lane/extend-hold doc :main :a delta {}))))
(is (:refused (lane/extend-hold doc :main :insert 1 {})))
(is (:refused (lane/extend-hold doc :main :missing 1 {})))))
(deftest a-gap-is-an-uncovered-interval
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
@ -148,8 +148,8 @@
(is (= 99 (get-in (sample edited [0 4]) [0 [:a :mark]])))
(is (= 139 (get-in (sample edited [0 4]) [4 [:b :mark]]))))))
(deftest occurrence-identities-and-playback-round-trip
(let [doc (:clip (sequence/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
(deftest cel-identities-and-playback-round-trip
(let [doc (:clip (lane/extend-hold (document) :main :a 2 {:extent :grow-symbol}))
leaves (leaf/leaves :project doc)]
(is (= doc (leaf/clip :project leaves)))
(is (contains? leaves "clip/project/symbol/main/node/a"))
@ -163,7 +163,7 @@
(deftest one-transaction-undoes-the-ripple-and-shot-extension
(let [doc (document)
after (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
before-leaves (leaf/leaves :p doc)
after-leaves (leaf/leaves :p after)
h (-> nil history/hold (history/record before-leaves after-leaves 0) history/settle)
@ -174,19 +174,19 @@
(is (= after-leaves (:leaves redo)))))
(deftest create-lane-and-append-drawings
(let [doc (:clip (sequence/add-lane (clip/blank) :main :girl))
a (:clip (sequence/append-drawing doc :main :girl :a :drawing-a {}))
b (:clip (sequence/append-drawing a :main :girl :b :drawing-b {}))]
(let [doc (:clip (lane/add-lane (clip/blank) :main :girl))
a (:clip (lane/append-drawing doc :main :girl :a :drawing-a {}))
b (:clip (lane/append-drawing a :main :girl :b :drawing-b {}))]
(is (empty? (clip/problems b)))
(is (= [1 2] (node/placed-span (get-in b [:symbols :main :nodes :b]))))
(is (= 0 (get-in b [:symbols :main :nodes :b :playback :speed])))
(is (:refused (sequence/append-drawing b :main :girl :a :new {})))))
(is (:refused (lane/append-drawing b :main :girl :a :new {})))))
(deftest fractional-placement-rates-convert-the-hold-delta
(let [doc (-> (document)
(assoc-in [:symbols :main :nodes :a :time :rate] 2)
(assoc-in [:symbols :main :nodes :a :span] [0 8]))
after (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
after (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
(is (= [0 12] (get-in after [:symbols :main :nodes :a :span])))
(is (= [6 10] (node/placed-span (get-in after [:symbols :main :nodes :b]))))))
@ -196,7 +196,7 @@
((symbol/resolver sym nil pal/index-of nil) 0))
"omitted style colour must not crash a missing cursor")))
(deftest audio-follows-only-the-playing-occurrence
(deftest audio-follows-only-the-playing-cel
(let [voice {:id :voice :kind :audio :z "a" :source {:sound "voice"}
:span [0 10]
:channels {[:audio :gain] (ch/keyed {0 0 5 1} :linear)}}
@ -208,7 +208,7 @@
(is (= [8 12] (node/placed-span track)))
(is (= [3 7] (:span track)) "the source in-point trims the audio too")
(is (= {5 0 10 1} (get-in track [:channels [:audio :gain] :keys])))
(let [moved (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))
(let [moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))
[track] (nest/audio-tracks moved :main)]
(is (= [10 14] (node/placed-span track)))
(is (= [3 7] (:span track))))
@ -231,56 +231,56 @@
(deftest enclosing-retiming-is-respected-when-extending-the-shot
(let [doc (assoc-in (document) [:symbols :main :nodes :girl :time] {:at 8 :rate 2})
result (sequence/extend-hold doc :main :a 2 {})]
result (lane/extend-hold doc :main :a 2 {})]
(is (= 15 (:required-frames result)))
(is (nil? (:clip result)))
(is (= 15 (get-in (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol})
(is (= 15 (get-in (lane/extend-hold doc :main :a 2 {:extent :grow-symbol})
[:clip :symbols :main :frames])))))
(deftest reuse-shares-content-and-make-unique-decouples-one-exposure
(deftest reuse-shares-content-and-make-unique-decouples-one-cel
(let [doc (document)
shared (:clip (sequence/reuse-drawing doc :main :girl :c :drawing-a
shared (:clip (lane/reuse-drawing doc :main :girl :c :drawing-a
{:extent :grow-symbol}))
edit (fn [c sym x]
(assoc-in c [:symbols sym :nodes :mark :channels [:xform :pos]]
(ch/framed [x 0])))]
(is (:refused (sequence/reuse-drawing doc :main :girl :c :drawing-a {}))
(is (:refused (lane/reuse-drawing doc :main :girl :c :drawing-a {}))
"the shot has to be extended on purpose")
(is (= :drawing-a (node/source (get-in shared [:symbols :main :nodes :c]))))
(is (= [12 13] (node/placed-span (get-in shared [:symbols :main :nodes :c]))))
(is (empty? (clip/problems shared)))
;; One drawing, two exposures: the edit arrives at both.
;; One drawing, two cels: the edit arrives at both.
(let [at (sample (edit shared :drawing-a 99) [0 12])]
(is (= 99 (get-in at [0 [:a :mark]])))
(is (= 99 (get-in at [12 [:c :mark]]))))
(let [unique (:clip (sequence/make-unique shared :main :c {}))]
(let [unique (:clip (lane/make-unique shared :main :c {}))]
(is (= :drawing-a-2 (node/source (get-in unique [:symbols :main :nodes :c]))))
(is (= (:nodes (get-in shared [:symbols :drawing-a]))
(:nodes (get-in unique [:symbols :drawing-a-2])))
"a copy of the same drawing, not an empty one")
(is (= :drawing-a (node/source (get-in unique [:symbols :main :nodes :a])))
"the other exposure keeps the original")
"the other cel keeps the original")
(let [at (sample (edit unique :drawing-a 99) [0 12])]
(is (= 99 (get-in at [0 [:a :mark]])))
(is (= 10 (get-in at [12 [:c :mark]])) "the exposure made unique is untouched"))
(is (= 10 (get-in at [12 [:c :mark]])) "the cel made unique is untouched"))
(let [at (sample (edit unique :drawing-a-2 99) [0 12])]
(is (= 10 (get-in at [0 [:a :mark]])) "and does not reach back"))
(is (empty? (clip/problems unique))))
;; Nothing else places drawing-b, so there is nothing to decouple from.
(is (:refused (sequence/make-unique doc :main :b {})))
(is (:refused (sequence/make-unique doc :main :girl {}))
(is (:refused (lane/make-unique doc :main :b {})))
(is (:refused (lane/make-unique doc :main :girl {}))
"a lane places nothing itself")))
(deftest duplicate-copies-the-drawing-and-not-the-exposure
(deftest duplicate-copies-the-drawing-and-not-the-cel
(let [doc (document)
made (:clip (sequence/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
made (:clip (lane/duplicate-drawing doc :main :b :d {:extent :grow-symbol}))
n (get-in made [:symbols :main :nodes :d])]
(is (= :drawing-b-2 (node/source n)))
(is (= (:nodes (get-in doc [:symbols :drawing-b]))
(:nodes (get-in made [:symbols :drawing-b-2]))))
(is (= [12 13] (node/placed-span n)))
(is (= {:in 0 :speed 0 :end :stop} (:playback n)))
(is (nil? (:channels n)) "B's own position correction belongs to B's exposure")
(is (nil? (:channels n)) "B's own position correction belongs to B's cel")
(is (= (get-in doc [:symbols :main :nodes :b])
(get-in made [:symbols :main :nodes :b]))
"the drawing duplicated is left as it was")
@ -293,7 +293,7 @@
{:id :part :kind :instance :z "b" :span [0 1]
:time {:at 0 :rate 1} :source {:symbol :wave}
:playback {:in 0 :speed 0 :end :stop}})
copy (fn [opts] (:clip (sequence/duplicate-drawing
copy (fn [opts] (:clip (lane/duplicate-drawing
doc :main :a :d (merge {:extent :grow-symbol} opts))))
shallow (copy {})
deep (copy {:deep? true})]
@ -306,19 +306,19 @@
(deftest reuse-refuses-what-would-not-be-a-document
(let [doc (document)]
(is (:refused (sequence/reuse-drawing doc :main :girl :c :nothing-here {})))
(is (:refused (sequence/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
(is (:refused (lane/reuse-drawing doc :main :girl :c :nothing-here {})))
(is (:refused (lane/reuse-drawing doc :main :girl :c :main {:extent :grow-symbol}))
"a symbol cannot go inside itself")
(is (:refused (sequence/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
"an occurrence ID in use is not free")
(is (:refused (sequence/reuse-drawing doc :main :plate :c :drawing-a {})))
(is (:refused (sequence/duplicate-drawing doc :main :girl :d {})))))
(is (:refused (lane/reuse-drawing doc :main :girl :a :drawing-a {:extent :grow-symbol}))
"a cel ID in use is not free")
(is (:refused (lane/reuse-drawing doc :main :plate :c :drawing-a {})))
(is (:refused (lane/duplicate-drawing doc :main :girl :d {})))))
(deftest drawing-on-twos-does-not-quantize-the-lane-transform
;; Exposure length IS the drawing cadence, and it is the only thing on twos
;; Cel length IS the drawing cadence, and it is the only thing on twos
;; here: the lane's transform has its own clock and keeps moving every frame.
;; Stepping it would be the cel cadence leaking into continuous motion.
(let [cel (fn [id source at] (occurrence id source at 2 0))
(let [cel (fn [id source at] (cel id source at 2 0))
doc (-> (document)
(update-in [:symbols :main :nodes] dissoc :a :b :insert)
(update-in [:symbols :main :nodes] merge
@ -337,7 +337,7 @@
(defn- drawn
"What every frame draws, as sorted values, so a picture can be compared
without naming the occurrences that produced it."
without naming the cels that produced it."
[doc fs]
(let [at (sample doc fs)]
(mapv #(sort (vals (get at %))) fs)))
@ -347,7 +347,7 @@
keys-of #(get-in % [:symbols :main :nodes :girl :channels [:xform :pos] :keys])
spans #(mapv (fn [id] (node/placed-span (get-in % [:symbols :main :nodes id])))
[:a :n :b :insert])
r (sequence/append-drawing doc :main :girl :n :drawing-n
r (lane/append-drawing doc :main :girl :n :drawing-n
{:at 4 :extent :grow-symbol})]
(is (= [[0 4] [4 5] [5 9] [9 13]] (spans (:clip r))))
(is (= 13 (get-in r [:clip :symbols :main :frames])))
@ -356,45 +356,45 @@
(is (= 4 (:frame r)))
(is (empty? (clip/problems (:clip r))))
;; The same command with no room refuses, and says how much it needs.
(is (= 13 (:required-frames (sequence/append-drawing doc :main :girl :n :drawing-n {:at 4}))))
(is (= 13 (:required-frames (lane/append-drawing doc :main :girl :n :drawing-n {:at 4}))))
;; At the very front everything moves.
(is (= [[1 5] [0 1] [5 9] [9 13]]
(spans (:clip (sequence/append-drawing doc :main :girl :n :drawing-n
(spans (:clip (lane/append-drawing doc :main :girl :n :drawing-n
{:at 0 :extent :grow-symbol})))))
;; Inside an exposure is not a position for another one.
;; Inside a cel is not a position for another one.
(is (re-find #"split it first"
(:refused (sequence/append-drawing doc :main :girl :n :drawing-n
(:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at 2 :extent :grow-symbol}))))
(is (:refused (sequence/append-drawing doc :main :girl :n :drawing-n
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at -1 :extent :grow-symbol})))
(is (:refused (sequence/append-drawing doc :main :girl :n :drawing-n
(is (:refused (lane/append-drawing doc :main :girl :n :drawing-n
{:at ##Inf :extent :grow-symbol})))
;; Reuse and duplicate take a position too; it is one placement rule.
(is (= [4 5] (node/placed-span
(get-in (sequence/reuse-drawing doc :main :girl :n :drawing-b
(get-in (lane/reuse-drawing doc :main :girl :n :drawing-b
{:at 4 :extent :grow-symbol})
[:clip :symbols :main :nodes :n]))))
(is (= [4 5] (node/placed-span
(get-in (sequence/duplicate-drawing doc :main :b :n
(get-in (lane/duplicate-drawing doc :main :b :n
{:at 4 :extent :grow-symbol})
[:clip :symbols :main :nodes :n]))))))
(deftest splitting-an-exposure-changes-nothing-that-is-drawn
(deftest splitting-an-cel-changes-nothing-that-is-drawn
(let [doc (document)
fs (range 12)
before (drawn doc fs)]
(doseq [[label id cut] [["a held drawing" :a 2]
["an exposure with a correction of its own" :b 6]
["a cel with a correction of its own" :b 6]
["a playing insert" :insert 10]]]
(testing label
(let [r (sequence/split doc :main id cut :right)
(let [r (lane/split doc :main id cut :right)
after (:clip r)]
(is (= :right (:selection r)))
(is (= before (drawn after fs)) "the same picture, frame for frame")
(is (= (node/placed-span (get-in doc [:symbols :main :nodes id]))
[(first (node/placed-span (get-in after [:symbols :main :nodes id])))
(second (node/placed-span (get-in after [:symbols :main :nodes :right])))])
"the pieces occupy the frames the exposure did")
"the pieces occupy the frames the cel did")
(is (= cut (second (node/placed-span (get-in after [:symbols :main :nodes id])))
(first (node/placed-span (get-in after [:symbols :main :nodes :right])))))
(is (= (:time (get-in doc [:symbols :main :nodes id]))
@ -405,19 +405,19 @@
(is (= 12 (get-in after [:symbols :main :frames])) "and no shot-length question")
(is (empty? (clip/problems after))))))))
(deftest split-refuses-anything-that-is-not-one-cut-inside-one-exposure
(deftest split-refuses-anything-that-is-not-one-cut-inside-one-cel
(let [doc (document)]
(doseq [cut [0 4 8 12 -1 2.5 ##NaN nil]]
(is (:refused (sequence/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
(is (:refused (sequence/split doc :main :girl 2 :right)) "a lane is not an exposure")
(is (:refused (sequence/split doc :main :plate 2 :right)) "nor is a shape outside one")
(is (:refused (sequence/split doc :main :a 2 :b)) "the new ID has to be free")))
(is (:refused (lane/split doc :main :b cut :right)) (str "cut at " (pr-str cut))))
(is (:refused (lane/split doc :main :girl 2 :right)) "a lane is not a cel")
(is (:refused (lane/split doc :main :plate 2 :right)) "nor is a shape outside one")
(is (:refused (lane/split doc :main :a 2 :b)) "the new ID has to be free")))
(deftest split-then-place-puts-a-drawing-inside-a-hold
;; The two commands the doc asks for, composed: neither one guesses.
(let [doc (document)
cut (:clip (sequence/split doc :main :a 2 :right))
r (sequence/append-drawing cut :main :girl :n :drawing-n
cut (:clip (lane/split doc :main :a 2 :right))
r (lane/append-drawing cut :main :girl :n :drawing-n
{:at 2 :extent :grow-symbol})
after (:clip r)]
(is (= [[0 2] [2 3] [3 5] [5 9] [9 13]]
@ -459,14 +459,14 @@
(is (= b (:leaves (history/undo h a))))
(is (= c (leaf/clip :p a)) "a correction needs no codec of its own"))))
(deftest a-correction-on-one-exposure-travels-with-it
;; The other half of ownership: a layer on an occurrence is in that
;; occurrence's own frames, so moving the exposure moves the correction and
(deftest a-correction-on-one-cel-travels-with-it
;; The other half of ownership: a layer on a cel is in that
;; cel's own frames, so moving the cel moves the correction and
;; nothing has to say so.
(let [beat (ch/layer :beat [0 2] :offset (ch/framed [7 0]))
doc (update-in (document) [:symbols :main :nodes :b :channels [:xform :pos] :over]
(fnil conj []) beat)
moved (:clip (sequence/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
moved (:clip (lane/extend-hold doc :main :a 2 {:extent :grow-symbol}))]
;; Stated as the difference from the same document without the correction,
;; so the claim is about WHERE the layer applies and not about arithmetic.
(let [nudge (fn [with without f]
@ -475,7 +475,7 @@
(is (= [7 7 0 0] (mapv #(nudge doc (document) %) [4 5 6 7]))
"B's first two frames, which are lane frames 4 and 5")
(is (= [7 7 0 0]
(mapv #(nudge moved (:clip (sequence/extend-hold (document) :main :a 2
(mapv #(nudge moved (:clip (lane/extend-hold (document) :main :a 2
{:extent :grow-symbol}))
%)
[6 7 8 9]))
@ -488,9 +488,9 @@
(defn- spans [clip ids]
(mapv #(node/placed-span (get-in clip [:symbols :main :nodes %])) ids))
(deftest trimming-narrows-one-exposure-and-moves-nothing-else
(deftest trimming-narrows-one-cel-and-moves-nothing-else
(let [doc (document)
r (sequence/trim doc :main :b :out 6)
r (lane/trim doc :main :b :out 6)
after (:clip r)]
(is (= [[0 4] [4 6] [8 12]] (spans after [:a :b :insert])))
(is (= :b (:selection r)))
@ -505,7 +505,7 @@
;; were, so the frames that survive show exactly what they showed.
(let [doc (document)
before (sample doc [10 11])
after (:clip (sequence/trim doc :main :insert :in 10))]
after (:clip (lane/trim doc :main :insert :in 10))]
(is (= [10 12] (node/placed-span (get-in after [:symbols :main :nodes :insert]))))
(is (= (:playback (get-in doc [:symbols :main :nodes :insert]))
(:playback (get-in after [:symbols :main :nodes :insert]))))
@ -520,13 +520,13 @@
["past its end" :out 9]
["before its start" :in 2]
["off a whole frame" :out 5.5]]]
(is (:refused (sequence/trim doc :main :b edge to)) label))
(is (:refused (sequence/trim doc :main :b :middle 6)))
(is (:refused (sequence/trim doc :main :girl :out 6)) "a lane is not an exposure")))
(is (:refused (lane/trim doc :main :b edge to)) label))
(is (:refused (lane/trim doc :main :b :middle 6)))
(is (:refused (lane/trim doc :main :girl :out 6)) "a lane is not a cel")))
(deftest moving-an-exposure-keeps-its-length-and-its-source-origin
(deftest moving-an-cel-keeps-its-length-and-its-source-origin
(let [doc (update-in (document) [:symbols :main :nodes] dissoc :b)
r (sequence/move doc :main :insert 4)
r (lane/move doc :main :insert 4)
after (:clip r)]
(is (= [[0 4] [4 8]] (spans after [:a :insert])))
(is (= :insert (:selection r)))
@ -539,19 +539,19 @@
(deftest a-move-onto-an-occupied-frame-is-refused-rather-than-rippled
(let [doc (document)]
(is (:refused (sequence/move doc :main :insert 6)) "it would overlap B")
(is (:refused (sequence/move doc :main :insert 4.5)))
(is (:refused (sequence/move doc :main :girl 2)))
(is (:refused (lane/move doc :main :insert 6)) "it would overlap B")
(is (:refused (lane/move doc :main :insert 4.5)))
(is (:refused (lane/move doc :main :girl 2)))
;; Clearing the room first is the composition, and then it goes.
(let [cleared (:clip (sequence/blank doc :main :girl [4 8] {}))]
(is (= [[0 4] [4 8]] (spans (:clip (sequence/move cleared :main :insert 4))
(let [cleared (:clip (lane/blank doc :main :girl [4 8] {}))]
(is (= [[0 4] [4 8]] (spans (:clip (lane/move cleared :main :insert 4))
[:a :insert]))))))
(deftest blanking-leaves-a-gap-and-does-not-close-it
(let [doc (document)
r (sequence/blank doc :main :girl [5 7] {:id :rest})
r (lane/blank doc :main :girl [5 7] {:id :rest})
after (:clip r)]
;; B spanned the range, so it became two exposures with a hole between them.
;; B spanned the range, so it became two cels with a hole between them.
(is (= [[0 4] [4 5] [7 8] [8 12]] (spans after [:a :b :rest :insert])))
(is (= :rest (:selection r)))
(let [at (sample after [4 5 6 7])]
@ -562,9 +562,9 @@
(is (= 12 (get-in after [:symbols :main :frames])))
(is (empty? (clip/problems after)))))
(deftest blanking-a-whole-exposure-removes-it-and-keeps-its-drawing
(deftest blanking-a-whole-cel-removes-it-and-keeps-its-drawing
(let [doc (document)
after (:clip (sequence/blank doc :main :girl [4 8] {}))]
after (:clip (lane/blank doc :main :girl [4 8] {}))]
(is (nil? (get-in after [:symbols :main :nodes :b])))
(is (= [[0 4] [8 12]] (spans after [:a :insert])) "and moves nothing")
(is (= (get-in doc [:symbols :drawing-b]) (get-in after [:symbols :drawing-b]))
@ -573,7 +573,7 @@
(deftest blanking-a-range-trims-what-it-only-partly-covers
(let [doc (document)
after (:clip (sequence/blank doc :main :girl [3 9] {}))]
after (:clip (lane/blank doc :main :girl [3 9] {}))]
(is (= [[0 3] [9 12]] (spans after [:a :insert])))
(is (nil? (get-in after [:symbols :main :nodes :b])))
(is (= (get-in (sample doc [9]) [9 [:insert :mark]])
@ -583,28 +583,28 @@
(deftest blank-refuses-what-it-cannot-do-in-one-piece
(let [doc (document)]
(is (re-find #"free ID" (:refused (sequence/blank doc :main :girl [5 7] {})))
"splitting an exposure needs an ID for the remainder")
(is (:refused (sequence/blank doc :main :girl [5 7] {:id :a})) "and a free one")
(is (:refused (sequence/blank doc :main :girl [7 5] {})))
(is (:refused (sequence/blank doc :main :girl [5 5] {})))
(is (:refused (sequence/blank doc :main :girl [5 6.5] {})))
(is (:refused (sequence/blank doc :main :plate [0 2] {})))))
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
"splitting a cel needs an ID for the remainder")
(is (:refused (lane/blank doc :main :girl [5 7] {:id :a})) "and a free one")
(is (:refused (lane/blank doc :main :girl [7 5] {})))
(is (:refused (lane/blank doc :main :girl [5 5] {})))
(is (:refused (lane/blank doc :main :girl [5 6.5] {})))
(is (:refused (lane/blank doc :main :plate [0 2] {})))))
(deftest the-shot-length-is-authored-and-emptying-a-lane-does-not-shorten-it
;; The window and the occupied extent are two facts. A shot with nothing in
;; the last half is a shot somebody authored that long, and deleting the last
;; drawing must not quietly shorten the film.
(let [doc (document)
empty-lane (:clip (sequence/blank doc :main :girl [0 12] {}))]
(is (empty? (symbol/sequence-members (get-in empty-lane [:symbols :main :nodes]) :girl)))
empty-lane (:clip (lane/blank doc :main :girl [0 12] {}))]
(is (empty? (symbol/lane-cels (get-in empty-lane [:symbols :main :nodes]) :girl)))
(is (= 12 (get-in empty-lane [:symbols :main :frames])))
(is (empty? (clip/problems empty-lane)))
;; Growing is still the caller's word, and only ever grows.
(is (:refused (sequence/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
(is (= 21 (get-in (sequence/append-drawing empty-lane :main :girl :n :drawing-n
(is (:refused (lane/append-drawing empty-lane :main :girl :n :drawing-n {:at 20})))
(is (= 21 (get-in (lane/append-drawing empty-lane :main :girl :n :drawing-n
{:at 20 :extent :grow-symbol})
[:clip :symbols :main :frames])))
(is (= 12 (get-in (:clip (sequence/trim doc :main :insert :out 9))
(is (= 12 (get-in (:clip (lane/trim doc :main :insert :out 9))
[:symbols :main :frames]))
"and trimming the last exposure leaves the window where it was")))
"and trimming the last cel leaves the window where it was")))

View file

@ -1,14 +1,14 @@
(ns arthur.events.sequence-test
(ns arthur.events.lane-test
(:require [cljs.test :refer [deftest is]]
[arthur.domain.lane-test :as fixture]
[arthur.domain.sequence :as sequence]
[arthur.domain.lane :as lane]
[arthur.events.ui :as ui]
[arthur.domain.history :as history]
[arthur.domain.leaf :as leaf]
[arthur.footage.store :as store]
[arthur.ui.timeline :as timeline]))
(deftest one-row-projects-all-occurrences-and-keeps-selection-addresses
(deftest one-row-projects-all-cels-and-keeps-selection-addresses
(let [doc (fixture/document)
rows (timeline/rows doc :main #{})
lane (first (filter :cels rows))]
@ -24,15 +24,15 @@
id (store/install! {:clip doc :store {}} "sequence-test")
db {:clip/current id :paint/revision 0
:ui {:open :main :selection [:node :main :a [:a]]}}
refused (ui/apply-sequence-command db :main
(sequence/extend-hold doc :main :a 1 {}) [:retry])]
refused (ui/apply-lane-command db :main
(lane/extend-hold doc :main :a 1 {}) [:retry])]
(is (= doc (:clip (store/entry id))))
(is (nil? (:history (store/entry id))))
(is (= [:retry] (get-in refused [:ui :sequence-retry])))
(let [r1 (sequence/extend-hold doc :main :a 1 {:extent :grow-symbol})
db1 (ui/apply-sequence-command db :main r1 nil)
r2 (sequence/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
db2 (ui/apply-sequence-command db1 :main r2 nil)
(is (= [:retry] (get-in refused [:ui :lane-retry])))
(let [r1 (lane/extend-hold doc :main :a 1 {:extent :grow-symbol})
db1 (ui/apply-lane-command db :main r1 nil)
r2 (lane/extend-hold (:clip r1) :main :a 1 {:extent :grow-symbol})
db2 (ui/apply-lane-command db1 :main r2 nil)
h (:history (store/entry id))
undo (history/undo h (leaf/leaves "u" (:clip r2)))
undo2 (history/undo (:history undo) (:leaves undo))]

View file

@ -59,7 +59,7 @@ try {
await evaluate(`(() => {
const k = cljs.core.keyword;
cljs.core.swap_BANG_(re_frame.db.app_db, db => cljs.core.assoc(db, k('route'), k('local-test')));
window.sequenceSnapshot = () => {
window.laneSnapshot = () => {
const db = cljs.core.deref(re_frame.db.app_db);
const entry = arthur.footage.store.entry(cljs.core.get(db, k('clip/current')));
return cljs.core.clj__GT_js(entry);
@ -75,7 +75,7 @@ try {
})()`), `enabled button: ${label}`);
await sleep(180);
};
const shot = async () => (await evaluate('sequenceSnapshot()'));
const shot = async () => (await evaluate('laneSnapshot()'));
const instances = s => Object.values(s.clip.symbols.main.nodes).filter(n => n.kind === 'instance')
.sort((a, b) => a.time.at - b.time.at);
await click('+ lane');
@ -90,7 +90,7 @@ try {
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1);
assert.equal(await evaluate(`cljs.core.get_in(cljs.core.deref(re_frame.db.app_db),
cljs.core.vector(cljs.core.keyword('playback'), cljs.core.keyword('frame')))`), 4,
'new drawing seeks to its occurrence');
'new drawing seeks to its cel');
// Shorten this test shot to the occupied extent, purely in memory.
await evaluate(`(() => {
@ -112,9 +112,9 @@ try {
assert.equal(s.history.done.length, before.history.done.length + 1);
await evaluate(`document.dispatchEvent(new KeyboardEvent('keydown', {key:'z', ctrlKey:true, bubbles:true}))`);
await sleep(250);
assert.deepEqual((await shot()).clip, before.clip, 'one undo restores exposure, ripple, and shot length');
assert.deepEqual((await shot()).clip, before.clip, 'one undo restores cel, ripple, and shot length');
// Sharing: one drawing exposed twice, then one exposure decoupled. Room is
// Sharing: one drawing exposed twice, then one cel decoupled. Room is
// made first so these assertions are about content and not about overflow.
await evaluate(`(() => {
const k = cljs.core.keyword, db = cljs.core.deref(re_frame.db.app_db);
@ -135,19 +135,19 @@ try {
assert.equal(cels[2].source.symbol, cels[0].source.symbol, 'reuse exposes the same drawing');
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 3);
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1,
'three exposures, still one row');
'three cels, still one row');
assert.equal(await enabled('make unique'), true);
await click('make unique');
s = await shot();
cels = instances(s);
assert.notEqual(cels[2].source.symbol, cels[0].source.symbol, 'that exposure has its own drawing');
assert.notEqual(cels[2].source.symbol, cels[0].source.symbol, 'that cel has its own drawing');
assert.equal(await enabled('make unique'), false, 'and is not shared any more');
await click('duplicate');
s = await shot();
cels = instances(s);
assert.equal(cels.length, 4);
assert.equal(new Set(cels.map(n => n.source.symbol)).size, 4,
'four exposures of four drawings: nothing is shared once every copy is made');
'four cels of four drawings: nothing is shared once every copy is made');
assert.equal(s.history.done.length, before.history.done.length + 3, 'three more commands, three more steps');
// A drawing into the middle of a hold: split, then insert. Both act at the
@ -158,25 +158,25 @@ try {
assert.deepEqual(placed(s), [[0, 4], [4, 5], [5, 6], [6, 7]]);
await evaluate(`document.querySelector('.tl-cel').click()`);
await sleep(200);
assert.equal(await enabled('split'), false, 'the start of an exposure is not inside it');
assert.equal(await enabled('split'), false, 'the start of a cel is not inside it');
await click('+1');
await click('+1');
assert.equal(await enabled('split'), true);
await click('split');
s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 4], [4, 5], [5, 6], [6, 7]],
'one exposure became two, over the frames it had');
'one cel became two, over the frames it had');
await click('insert');
s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 5], [5, 6], [6, 7], [7, 8]],
'the new drawing took frame 2 and everything from there rippled later');
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 6);
assert.equal(await evaluate('document.querySelectorAll(".tl-label:not(.tl-corner)").length'), 1,
'six exposures, still one row');
'six cels, still one row');
assert.equal(s.history.done.length, before.history.done.length + 5);
// Trim, move and blank: three gestures that move nothing but their own
// exposure, and a shot whose length does not follow what is in it.
// cel, and a shot whose length does not follow what is in it.
await evaluate(`[...document.querySelectorAll('.tl-cel')][2].click()`);
await sleep(200);
assert.deepEqual(placed(await shot()).slice(2, 4), [[3, 5], [5, 6]]);
@ -185,7 +185,7 @@ try {
await click('trim out');
s = await shot();
assert.deepEqual(placed(s), [[0, 2], [2, 3], [3, 4], [5, 6], [6, 7], [7, 8]],
'it ends at the playhead and every other exposure stayed');
'it ends at the playhead and every other cel stayed');
assert.equal(await enabled('move here'), true);
await click('move here');
s = await shot();