perfect target area thing
This commit is contained in:
parent
664252e0fc
commit
5bcf22e458
23 changed files with 668 additions and 432 deletions
|
|
@ -51,6 +51,52 @@
|
|||
(is (= [4 8] (node/placed-span (get-in made [:symbols :main :nodes id]))))
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest multi-paste-has-one-destination-and-preserves-root-timing
|
||||
(let [doc (update-in (fixture/document) [:symbols :main] dissoc :display)
|
||||
payload (:clipboard
|
||||
(clipboard/snapshot doc [(address :main :a [:a])
|
||||
(address :main :b [:b])]))
|
||||
r (clipboard/paste doc payload :main 20 {:fresh-id (ids)})
|
||||
made (:clip r)
|
||||
[a2 b2] (:roots r)]
|
||||
(is (= [20 24] (node/placed-span (get-in made [:symbols :main :nodes a2]))))
|
||||
(is (= [24 28] (node/placed-span (get-in made [:symbols :main :nodes b2]))))
|
||||
(is (= 2 (count (:roots r))))
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest multi-paste-into-a-lane-claims-all-intervals-atomically
|
||||
(let [doc (fixture/document)
|
||||
payload (:clipboard
|
||||
(clipboard/snapshot doc [(address :main :a [:a])
|
||||
(address :main :insert [:insert])]))
|
||||
r (clipboard/paste doc payload :main 4 {:fresh-id (ids)})
|
||||
made (:clip r)
|
||||
[a2 insert2] (:roots r)]
|
||||
(is (= [4 8] (node/placed-span (get-in made [:symbols :main :nodes a2]))))
|
||||
(is (= [12 16] (node/placed-span (get-in made [:symbols :main :nodes insert2]))))
|
||||
(is (nil? (get-in made [:symbols :main :nodes :b])))
|
||||
(is (= [8 12] (node/placed-span (get-in made [:symbols :main :nodes :insert]))))
|
||||
(is (= 16 (get-in made [:symbols :main :frames])))
|
||||
(is (empty? (clip/problems made)))))
|
||||
|
||||
(deftest overlapping-multi-paste-into-a-lane-is-all-or-nothing
|
||||
(let [base (fixture/document)
|
||||
doc (-> base
|
||||
(assoc-in [:symbols :source]
|
||||
{:id :source :frames 8
|
||||
:nodes {:a (get-in base [:symbols :main :nodes :a])
|
||||
:b (assoc-in (get-in base [:symbols :main :nodes :b])
|
||||
[:time :at] 2)}})
|
||||
(assoc-in [:symbols :empty-lane]
|
||||
{:id :empty-lane :frames 8 :display :lane :nodes {}}))
|
||||
payload (:clipboard
|
||||
(clipboard/snapshot doc [(address :source :a [:a])
|
||||
(address :source :b [:b])]))
|
||||
r (clipboard/paste doc payload :empty-lane 0 {:fresh-id (ids)})]
|
||||
(is (= "overlapping copied things cannot be pasted into one lane" (:refused r)))
|
||||
(is (nil? (:clip r)))
|
||||
(is (= {} (get-in doc [:symbols :empty-lane :nodes])))))
|
||||
|
||||
(deftest lane-duplicate-is-the-forward-repeat-and-ripples-later-cels
|
||||
(let [doc (fixture/document)
|
||||
payload (:clipboard
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
(ns arthur.events.lane-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.creation :as creation]
|
||||
[arthur.domain.history :as history]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.node :as node]
|
||||
|
|
@ -42,22 +43,61 @@
|
|||
(node/placed-span (get-in fitted [:symbols :main :nodes instance-id])))
|
||||
"the lane follows a later change to its parent's extent")
|
||||
(is (= 300 (clip/frames fitted lane-id))))
|
||||
(is (some? (get-in lane-db [:ui :target]))
|
||||
"the new lane is aimed so drawing and pool drops can go into it"))))
|
||||
(is (= :lane (:kind (creation/target document {} :main
|
||||
(get-in lane-db [:ui :selection]) 6)))
|
||||
"the new lane is the active creation row"))))
|
||||
|
||||
(deftest aiming-the-root-clears-selection-and-drawing-target
|
||||
(deftest a-new-symbol-on-a-lane-is-a-one-frame-cel
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "one-frame-new-symbol")]
|
||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main
|
||||
:selection [:node :main nil []]}
|
||||
:playback {:frame 5}})
|
||||
(rf/dispatch-sync [::ui/new-symbol :inside])
|
||||
(let [db @rf-db/app-db
|
||||
saved (:clip (store/entry id))
|
||||
[_ sid instance-id] (get-in db [:ui :selection])
|
||||
instance (get-in saved [:symbols sid :nodes instance-id])
|
||||
source (node/source instance)]
|
||||
(is (= :main sid))
|
||||
(is (= [5 6] (node/placed-span instance)))
|
||||
(is (= 1 (clip/frames saved source)))
|
||||
(is (every? (fn [other]
|
||||
(or (= instance-id (:id other))
|
||||
(let [[a b] (node/placed-span other)]
|
||||
(or (<= b 5) (<= 6 a)))))
|
||||
(filter node/placed-span
|
||||
(vals (get-in saved [:symbols :main :nodes]))))
|
||||
"claiming the frame leaves no overlapping cel"))))
|
||||
|
||||
(deftest a-new-lane-uses-the-symbol-selected-at-the-playhead
|
||||
(let [doc (clip/blank)
|
||||
id (store/install! {:clip doc :store {}} "nested-new-lane")]
|
||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main} :playback {:frame 6}})
|
||||
(rf/dispatch-sync [::ui/new-symbol :inside])
|
||||
(let [parent-instance (nth (get-in @rf-db/app-db [:ui :selection]) 2)
|
||||
parent-sid (node/source (get-in (:clip (store/entry id))
|
||||
[:symbols :main :nodes parent-instance]))]
|
||||
(rf/dispatch-sync [::ui/new-lane])
|
||||
(let [db @rf-db/app-db
|
||||
saved (:clip (store/entry id))
|
||||
[_ host lane-instance] (get-in db [:ui :selection])
|
||||
lane-sid (node/source (get-in saved [:symbols host :nodes lane-instance]))]
|
||||
(is (= parent-sid host) "the lane is created inside the selected symbol")
|
||||
(is (= :lane (get-in saved [:symbols lane-sid :display])))
|
||||
(is (= [0 (clip/frames saved host)]
|
||||
(node/placed-span (get-in saved [:symbols host :nodes lane-instance]))))
|
||||
(is (nil? (get-in saved [:symbols :main :nodes lane-instance]))
|
||||
"it does not bypass the target and land in the tab root")))))
|
||||
|
||||
(deftest selecting-the-root-clears-the-active-row
|
||||
(let [db {:ui {:selection [:node :main :shape [:lane :shape]]
|
||||
:target {:sid :main :id :lane :path [:lane]}}}
|
||||
after (ui/aimed db nil)]
|
||||
(is (nil? (get-in after [:ui :selection])))
|
||||
(is (nil? (get-in after [:ui :target])))))
|
||||
|
||||
(deftest clearing-selection-also-clears-the-creation-target
|
||||
(let [db {:ui {:selection [:symbol :drawing]
|
||||
:target {:sid :main :id :lane :path [:lane]}}}
|
||||
:selections [[:node :main :shape [:lane :shape]]]}}
|
||||
after (ui/selected db nil)]
|
||||
(is (nil? (get-in after [:ui :selection])))
|
||||
(is (nil? (get-in after [:ui :target])))))
|
||||
(is (empty? (get-in after [:ui :selections])))))
|
||||
|
||||
(deftest an-explicit-lane-is-one-row-of-clips
|
||||
(let [doc (fixture/document)
|
||||
|
|
@ -111,7 +151,34 @@
|
|||
(is (= 12 (ui/selection-frame doc nil :main
|
||||
[:node :main :a [:a]] 12)))))
|
||||
|
||||
(deftest paste-targeting-continues-an-instance-clock-past-its-window
|
||||
(deftest creation-walks-to-the-nearest-valid-parent-at-the-playhead
|
||||
(let [doc (-> (fixture/document)
|
||||
(assoc-in [:symbols :shot]
|
||||
{:id :shot :frames 12 :fps 24
|
||||
:nodes {:girl {:id :girl :kind :instance :z "a"
|
||||
:span [0 12] :time {:mode :map :at 0 :rate 1}
|
||||
:source {:symbol :main}
|
||||
:playback {:in 0 :speed 1 :end :stop}}}})
|
||||
(assoc-in [:symbols :outer]
|
||||
{:id :outer :frames 30 :fps 24
|
||||
:nodes {:take {:id :take :kind :instance :z "a"
|
||||
:span [0 12] :time {:mode :map :at 10 :rate 1}
|
||||
:source {:symbol :shot}
|
||||
:playback {:in 0 :speed 1 :end :stop}}}}))
|
||||
selection [:node :main :a [:take :girl :a]]
|
||||
at #(select-keys (creation/target doc nil :outer selection %)
|
||||
[:kind :sid :path :frame])]
|
||||
(is (= {:kind :symbol :sid :drawing-a :path [:take :girl :a] :frame 0}
|
||||
(at 12))
|
||||
"the selected cel is the parent while every enclosing occurrence is active")
|
||||
(is (= {:kind :lane :sid :main :path [:take :girl] :frame 5}
|
||||
(at 15))
|
||||
"past the cel, its containing lane is the nearest valid insertion surface")
|
||||
(is (= {:kind :symbol :sid :outer :path [] :frame 25}
|
||||
(at 25))
|
||||
"a lane behind an inactive parent occurrence is not valid")))
|
||||
|
||||
(deftest creation-walks-out-of-an-instance-past-its-window
|
||||
(let [doc {:fps 24 :width 20 :height 20
|
||||
:symbols
|
||||
{:main {:id :main :frames 40
|
||||
|
|
@ -121,16 +188,40 @@
|
|||
:playback {:in 0 :speed 1 :end :stop}}}}
|
||||
:inside {:id :inside :frames 10 :nodes {}}}}
|
||||
db {:ui {:open :main
|
||||
:target {:sid :main :id :drawing :path [:drawing]}}
|
||||
:selection [:node :main :drawing [:drawing]]}
|
||||
:playback {:frame 20}}]
|
||||
(is (= {:sid :inside :path [:drawing] :frame 20}
|
||||
(ui/paste-destination db doc nil))
|
||||
"pasting outside a target symbol's window authors cropped content")))
|
||||
(is (= {:sid :main :path [] :frame 20}
|
||||
(select-keys (ui/paste-destination db doc nil) [:sid :path :frame]))
|
||||
"an inactive selected symbol falls back to the open symbol")))
|
||||
|
||||
(deftest multi-paste-uses-one-current-primary-target-and-selects-the-batch
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "multi-paste-target")
|
||||
a [:node :main :a [:a]]
|
||||
insert [:node :main :insert [:insert]]]
|
||||
(reset! rf-db/app-db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :selection insert :selections [a insert]}
|
||||
:playback {:frame 4}})
|
||||
(rf/dispatch-sync [::ui/copy])
|
||||
;; Changing selection after copy changes the destination, not the payload.
|
||||
(rf/dispatch-sync [::ui/select [:node :main nil []]])
|
||||
(rf/dispatch-sync [::ui/paste])
|
||||
(let [db @rf-db/app-db
|
||||
saved (:clip (store/entry id))
|
||||
selections (get-in db [:ui :selections])
|
||||
[a2 insert2] (map #(nth % 2) selections)]
|
||||
(is (= 2 (count selections)))
|
||||
(is (= (peek selections) (get-in db [:ui :selection]))
|
||||
"the last pasted root is the new primary creation anchor")
|
||||
(is (= [4 8] (node/placed-span (get-in saved [:symbols :main :nodes a2]))))
|
||||
(is (= [12 16] (node/placed-span (get-in saved [:symbols :main :nodes insert2]))))
|
||||
(is (= 1 (count (get-in (store/entry id) [:history :done])))
|
||||
"the complete batch is one edit"))))
|
||||
|
||||
(deftest polygon-landing-obeys-the-destination-symbol-mode
|
||||
(let [lane (fixture/document)
|
||||
ordinary (update-in lane [:symbols :main] dissoc :display)
|
||||
db {:ui {:open :main :target {:sid :main :id :plate :path [:plate]}}
|
||||
db {:ui {:open :main :selection [:node :main :plate [:plate]]}
|
||||
:playback {:frame 5}}]
|
||||
(is (true? (:lane? (ui/polygon-landing lane {} db))))
|
||||
(is (false? (:lane? (ui/polygon-landing ordinary {} db))))))
|
||||
|
|
@ -148,6 +239,21 @@
|
|||
(is (nil? (get-in saved [:symbols :main :display])))
|
||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))))
|
||||
|
||||
(deftest drawing-with-the-lane-row-active-creates-a-new-cel
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "draw-new-lane-cel")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :selection [:node :main nil []]}
|
||||
:playback {:frame 1}}
|
||||
after (ui/beginning-polygon db)
|
||||
[_ sid cel-id] (get-in after [:ui :selection])
|
||||
saved (:clip (store/entry id))]
|
||||
(is (= :main sid))
|
||||
(is (not= :a cel-id) "the occupied cel is not reused")
|
||||
(is (= [1 2] (node/placed-span (get-in saved [:symbols sid :nodes cel-id]))))
|
||||
(is (= 1 (clip/frames saved (node/source (get-in saved [:symbols sid :nodes cel-id])))))
|
||||
(is (= :polygon (get-in after [:ui :tool])))))
|
||||
|
||||
(deftest sequence-commands-use-isolated-history-transactions
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "sequence-command-test")
|
||||
|
|
@ -184,24 +290,22 @@
|
|||
(is (= [:vo] (mapv :id (:cels lane))))
|
||||
(is (empty? (timeline/sound-rows doc :main #{})))))
|
||||
|
||||
(deftest aiming-the-open-lanes-own-row-is-a-selection-and-not-a-crash
|
||||
(deftest selecting-the-open-lanes-own-row-is-the-lane-target
|
||||
;; `rows` names the open symbol's own lane row `[:node sid nil []]`, and
|
||||
;; `selected` used to open the rows above a selection with `(pop path)` —
|
||||
;; which THROWS on `[]`, aborting the whole event. Nothing was selected,
|
||||
;; nothing was aimed, and the next polygon went wherever the stale target
|
||||
;; still pointed, which is most of what made aiming a lane feel random.
|
||||
;; nothing was selected, and the next polygon used stale creation state.
|
||||
(let [doc (fixture/document)
|
||||
id (store/install! {:clip doc :store {}} "aim-the-open-lane")
|
||||
id (store/install! {:clip doc :store {}} "select-the-open-lane")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :target {:sid :main :id :a :path [:a]}}
|
||||
:ui {:open :main}
|
||||
:playback {:frame 0}}
|
||||
lane (first (filter :lane? (timeline/rows doc :main #{})))
|
||||
after (ui/aimed db (:select lane))]
|
||||
after (ui/selected db (:select lane))]
|
||||
(is (= [:node :main nil []] (:select lane)))
|
||||
(is (= [:node :main nil []] (get-in after [:ui :selection])))
|
||||
(is (nil? (get-in after [:ui :target]))
|
||||
"the open symbol IS the place, so aiming its own row clears the target")
|
||||
(is (= [] (ui/where-new-goes doc after)))
|
||||
(is (= :lane (:kind (creation/target doc {} :main
|
||||
(get-in after [:ui :selection]) 0))))
|
||||
(is (empty? (get-in after [:ui :expanded]))
|
||||
"and there are no rows above the top to open")))
|
||||
|
||||
|
|
@ -229,7 +333,7 @@
|
|||
id (store/install! {:clip doc :store {}} "finish-opens-nothing")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :main :tool :polygon
|
||||
:target {:sid :main :id :a :path [:a]}
|
||||
:selection [:node :main :a [:a]]
|
||||
:draft [10 10 40 10 40 40]}
|
||||
:playback {:frame 1}}]
|
||||
(reset! rf-db/app-db db)
|
||||
|
|
@ -237,7 +341,7 @@
|
|||
(let [after @rf-db/app-db
|
||||
[kind sid shape-id path] (get-in after [:ui :selection])]
|
||||
(is (= :node kind))
|
||||
(is (= :drawing-a sid) "the shape went into the drawing the aimed clip places")
|
||||
(is (= :drawing-a sid) "the shape went into the drawing the selected clip places")
|
||||
(is (= [:a shape-id] path))
|
||||
(is (empty? (get-in after [:ui :expanded])))
|
||||
(is (nil? (get-in after [:ui :tool]))))))
|
||||
|
|
@ -260,7 +364,7 @@
|
|||
:playback {:in 0 :speed 1 :end :stop}}}}))
|
||||
id (store/install! {:clip doc :store {}} "clip-selected-where-it-lives")
|
||||
db {:clip/current id :paint/revision 0
|
||||
:ui {:open :shot :target {:sid :shot :id :girl :path [:girl]}}
|
||||
:ui {:open :shot :selection [:node :shot :girl [:girl]]}
|
||||
:playback {:frame 9}}
|
||||
after (ui/beginning-polygon db)
|
||||
[_ sid clip-id path] (get-in after [:ui :selection])
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ try {
|
|||
assert.equal(await evaluate('document.querySelectorAll(".tl-track:not(.palette)").length'), 1,
|
||||
'an ordinary symbol is an ordinary timeline row');
|
||||
|
||||
// The new symbol is now the active creation parent. Return to the tab root
|
||||
// to ask for a sibling lane in `main`; leaving it selected would correctly
|
||||
// create the lane inside that symbol.
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
|
||||
cljs.core.keyword('arthur.events.ui/select'), null))`);
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
placed = mainInstances(s);
|
||||
|
|
@ -178,8 +183,11 @@ try {
|
|||
`the dropped clip remains in the explicit lane: ${JSON.stringify(s)}`);
|
||||
assert.equal(laneSymbols(s).length, 1, 'the drop creates no extra lane');
|
||||
|
||||
// A second explicit lane is a sibling in the open symbol even though the
|
||||
// first remains aimed. Move the clip between their linear tracks.
|
||||
// Return to the root active row, then make a sibling lane there. Lane
|
||||
// creation follows the selected target; the unit suite separately asserts
|
||||
// that leaving a symbol selected creates the lane inside that symbol.
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
|
||||
cljs.core.keyword('arthur.events.ui/select'), null))`);
|
||||
await clickNew('lane');
|
||||
s = await shot();
|
||||
assert.equal(laneSymbols(s).length, 2, 'a second explicit command creates a second lane');
|
||||
|
|
@ -239,7 +247,7 @@ try {
|
|||
await shortcut('z', 'KeyZ');
|
||||
assert.equal(mainInstances(await shot()).length, 2, 'one undo restores the cut');
|
||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
|
||||
cljs.core.keyword('arthur.events.ui/aim'), null))`);
|
||||
cljs.core.keyword('arthur.events.ui/select'), null))`);
|
||||
await sleep(80);
|
||||
await shortcut('v', 'KeyV');
|
||||
assert.equal(mainInstances(await shot()).length, 3, 'paste uses the copied snapshot');
|
||||
|
|
|
|||
|
|
@ -501,6 +501,17 @@ async function main() {
|
|||
// the authored node, the raster preview and the document round trip.
|
||||
await page.eval(SEEK(0));
|
||||
await sleep(150);
|
||||
// A lane row means "new one-frame cel". This test authors drawing keys
|
||||
// across the existing take, so make that cel the active row first; creation
|
||||
// then enters the symbol it places, exactly as a person clicking its block.
|
||||
await page.eval(`(() => {
|
||||
const cel = document.querySelector('.tl-cel');
|
||||
if (!cel?.arthurCel?.select) return false;
|
||||
re_frame.core.dispatch_sync(cljs.core.vector(
|
||||
cljs.core.keyword('arthur.events.ui/select'), cel.arthurCel.select));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(150);
|
||||
const beforePaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
const d = c.getContext('2d').getImageData(25, 20, 1, 1).data;
|
||||
|
|
@ -513,7 +524,7 @@ async function main() {
|
|||
// A person cannot click twice inside one microtask; a test should not either.
|
||||
const armed = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||
.find(b => b.textContent === 'polygon');
|
||||
.find(b => b.textContent === 'pen');
|
||||
if (!button) return false;
|
||||
button.click();
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue