Turn the lane sideways without changing what it means
The cel sheet is a second projection of the same lane rows: frames run down, lanes run across, and each occupied cell carries the timeline cel's exact selection address. The shared action strip proves the point in the browser test by selecting a cell and issuing the existing hold command. Before exposing that second entrance, fix the boundary mistakes it revealed. Nested commands now convert the open playhead through their enclosing instance path. Overwrite composes blanking with non-rippling placement as one transaction. Picture-rate and pose sampling select only the generated base frame while hand corrections retain the node's authored frame. Stack validation follows covering replacement layers so a document accepted by the validator cannot throw solely because a later offset sees a different shape. 429 tests, 5,767 assertions; both browser flows; 56 Django tests; optimized frontend build.
This commit is contained in:
parent
7a54bfca56
commit
2f1c9b9c02
15 changed files with 387 additions and 100 deletions
|
|
@ -1,14 +1,14 @@
|
||||||
# Lane and cel handoff
|
# Lane and cel handoff
|
||||||
|
|
||||||
Status (2026-09-30): the lane model is implemented through its commands. Cels are
|
Status (2026-09-30): the lane model is implemented through its commands and its
|
||||||
ordinary nodes with their own playback clock, a lane draws them as one row, the
|
first two views. Cels are ordinary nodes with their own playback clock, the
|
||||||
cel commands all exist, and correction layers evaluate and survive regeneration.
|
timeline draws them as one row, the cel sheet draws frames down and lanes across,
|
||||||
What is missing is a second view and the commands that make a correction.
|
and both views issue the same commands. Correction layers evaluate and survive
|
||||||
|
regeneration. What is missing is the commands that make a correction.
|
||||||
|
|
||||||
Seven commits on branch `lanes`, off `624242b`. `master` is untouched and can be
|
The commits beginning at `3d3c1bb` are the argument for the model and are worth
|
||||||
fast-forwarded. Each commit message is the argument for its change and is worth
|
reading before touching what they did — they are the design record, more than
|
||||||
reading before touching what it did — they are the design record, more than this
|
this file is.
|
||||||
file is.
|
|
||||||
|
|
||||||
3d3c1bb An occurrence is a node, with a clock of its own
|
3d3c1bb An occurrence is a node, with a clock of its own
|
||||||
9446829 Reuse, duplicate and make unique: deciding what is shared
|
9446829 Reuse, duplicate and make unique: deciding what is shared
|
||||||
|
|
@ -65,10 +65,9 @@ decision, not a cleanup.
|
||||||
and never shrinks it. Blanking the end of a shot leaves empty frames at the
|
and never shrinks it. Blanking the end of a shot leaves empty frames at the
|
||||||
end, because deriving the window from the extent would make deleting the last
|
end, because deriving the window from the extent would make deleting the last
|
||||||
drawing silently shorten the film. `lane/finish`.
|
drawing silently shorten the film. `lane/finish`.
|
||||||
- **Placing ripples; overwrite is `blank` then `place`.** Overwrite is not a
|
- **Placing ripples; overwrite is `blank` then non-rippling placement.**
|
||||||
policy argument and should not become one until there is a reason `blank`
|
`lane/overwrite-drawing` composes those pieces as one transaction. Insertion
|
||||||
cannot serve. An argument whose second value is unimplemented is worse than
|
retains its ripple rule; overwrite does not move any surviving cel.
|
||||||
no argument.
|
|
||||||
- **A position inside a cel refuses and names `split`.** One command must not
|
- **A position inside a cel refuses and names `split`.** One command must not
|
||||||
quietly perform two. The UI offers the retry.
|
quietly perform two. The UI offers the retry.
|
||||||
- **A correction has no time space of its own.** Its `:support` and its values'
|
- **A correction has no time space of its own.** Its `:support` and its values'
|
||||||
|
|
@ -88,27 +87,19 @@ decision, not a cleanup.
|
||||||
|
|
||||||
## Next steps, in order
|
## Next steps, in order
|
||||||
|
|
||||||
1. **The cel sheet.** The doc's own test that the document is separate from its
|
1. **The commands that make a correction** — Constant adjustment, Ramp, Return
|
||||||
presentation: frames down, a column per lane, one cell per frame naming the
|
|
||||||
drawing. It needs NO new model — `symbol/lane-cels` and the existing commands
|
|
||||||
are the whole API. The obligation is that the same command issued from the
|
|
||||||
sheet and from the timeline produces identical document changes and the same
|
|
||||||
selection. This is the recommended next piece: it is self-contained, it is
|
|
||||||
specified, and it is the first real evidence the model is not shaped by the
|
|
||||||
timeline that grew up with it.
|
|
||||||
2. **The commands that make a correction** — Constant adjustment, Ramp, Return
|
|
||||||
motion over a selected range, per `lane-model.md`. The evaluator is done and
|
motion over a selected range, per `lane-model.md`. The evaluator is done and
|
||||||
has no opinion about how a range or a motion shape is chosen, which is now a
|
has no opinion about how a range or a motion shape is chosen, which is now a
|
||||||
view question. A panel also needs to offer `clip/conflicts` for resolution.
|
view question. A panel also needs to offer `clip/conflicts` for resolution.
|
||||||
Note the one open question: a correction needs a stable `:id` from somewhere,
|
Note the one open question: a correction needs a stable `:id` from somewhere,
|
||||||
and cel ids come from the caller because this namespace is pure.
|
and cel ids come from the caller because this namespace is pure.
|
||||||
3. **Slip source and retime.** Both have real design questions open and the doc
|
2. **Slip source and retime.** Both have real design questions open and the doc
|
||||||
says to refuse rather than approximate: retime needs a defined warp and
|
says to refuse rather than approximate: retime needs a defined warp and
|
||||||
interpolation behaviour, and is not moving keys whose numbers happen to fall
|
interpolation behaviour, and is not moving keys whose numbers happen to fall
|
||||||
inside a selection.
|
inside a selection.
|
||||||
4. **Deleting reused content.** Reference discovery exists (`node/sources`,
|
3. **Deleting reused content.** Reference discovery exists (`node/sources`,
|
||||||
`clip/places`, `clip/contains-symbol?`); the policy does not.
|
`clip/places`, `clip/contains-symbol?`); the policy does not.
|
||||||
5. **Collaboration.** `lane-model.md` is explicit that one leaf per channel does
|
4. **Collaboration.** `lane-model.md` is explicit that one leaf per channel does
|
||||||
NOT solve two people editing different keys of the same channel. No conflict
|
NOT solve two people editing different keys of the same channel. No conflict
|
||||||
policy exists for that.
|
policy exists for that.
|
||||||
|
|
||||||
|
|
@ -129,9 +120,12 @@ decision, not a cleanup.
|
||||||
- **`lane/lane-frame`** converts a symbol frame to a lane frame, or returns nil
|
- **`lane/lane-frame`** converts a symbol frame to a lane frame, or returns nil
|
||||||
through a stepped or looping lane where there is no single answer. Nil refuses;
|
through a stepped or looping lane where there is no single answer. Nil refuses;
|
||||||
it never snaps.
|
it never snaps.
|
||||||
- **`channel/conflict-with`** is the single rule for "can this layer apply to
|
- **`channel/conflict-with`** is the rule for whether one offset fits a base,
|
||||||
this base", used by the validator, by `conflicts`, and by the regeneration
|
used by `conflicts` and regeneration. Validation additionally follows prior
|
||||||
that marks them.
|
replacement layers, so it cannot approve a stack that throws when read.
|
||||||
|
- **Generated sampling applies to the base, not the hand correction.** Picture
|
||||||
|
rate and pose selection may choose an earlier generated frame; correction
|
||||||
|
support and values still read the node's current authored frame.
|
||||||
- **Two test patterns worth copying.** `the-cursor-agrees-with-the-specification-in-any-frame-order`
|
- **Two test patterns worth copying.** `the-cursor-agrees-with-the-specification-in-any-frame-order`
|
||||||
holds the optimized cursor to `value-at` in forward, backward and random order
|
holds the optimized cursor to `value-at` in forward, backward and random order
|
||||||
— add a case to it for any new channel shape. And `drawn` in `lane_test`
|
— add a case to it for any new channel shape. And `drawn` in `lane_test`
|
||||||
|
|
@ -171,7 +165,7 @@ decision, not a cleanup.
|
||||||
|
|
||||||
From `frontend/`:
|
From `frontend/`:
|
||||||
|
|
||||||
npx shadow-cljs compile test && node out/node-tests.js # 424 tests, 5,749 assertions
|
npx shadow-cljs compile test && node out/node-tests.js # 429 tests, 5,767 assertions
|
||||||
npx shadow-cljs compile app # the bundle Django serves
|
npx shadow-cljs compile app # the bundle Django serves
|
||||||
npx shadow-cljs release app # then `compile app` again — see above
|
npx shadow-cljs release app # then `compile app` again — see above
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# The Lane Model
|
# The Lane Model
|
||||||
|
|
||||||
Revised 2026-09-30. Target design. Cel ownership, source playback, the
|
Revised 2026-09-30. Target design. Cel ownership, source playback, the
|
||||||
content and cel commands, placement anywhere in a lane, a one-row cel strip
|
content and cel commands, placement anywhere in a lane, overwrite, a one-row cel
|
||||||
and the correction-layer evaluator are implemented; the commands that produce a
|
strip, a frame-down cel sheet and the correction-layer evaluator are implemented;
|
||||||
correction, overwrite, the retiming commands and the remaining views are not.
|
the commands that produce a correction and the retiming commands are not.
|
||||||
See the status note under
|
See the status note under
|
||||||
[Proof obligations](#proof-obligations-and-implementation-order).
|
[Proof obligations](#proof-obligations-and-implementation-order).
|
||||||
|
|
||||||
|
|
@ -530,22 +530,23 @@ re-measurement for good; and an incompatible offset threw in the read path,
|
||||||
which would have taken the stage down on exactly the case the model says to
|
which would have taken the stage down on exactly the case the model says to
|
||||||
report.
|
report.
|
||||||
|
|
||||||
Still unbuilt: overwrite as a placement policy (which is now `blank` then
|
Overwrite is `blank` followed by non-rippling placement, composed inside one
|
||||||
`place`, composed inside one transaction), slip source, retime, and deleting
|
transaction; insertion keeps its ripple rule. Still unbuilt: slip source,
|
||||||
reused content. A lane cannot hold AUDIO cels — `lane-problems`
|
retime, and deleting reused content. A lane cannot hold AUDIO cels — `lane-problems`
|
||||||
requires visual ones, though this document says a lane may hold either and
|
requires visual ones, though this document says a lane may hold either and
|
||||||
should reject only a mixture.
|
should reject only a mixture.
|
||||||
|
|
||||||
What is NOT implemented is a command that produces a layer — the doc's Constant
|
What is NOT implemented is a command that produces a layer — the doc's Constant
|
||||||
adjustment, Ramp and Return motion — and with it the question of how a view
|
adjustment, Ramp and Return motion — and with it the question of how a view
|
||||||
offers those three over a selected range, and how it offers a conflict for
|
offers those three over a selected range, and how it offers a conflict for
|
||||||
resolution. Overwrite, the range and retiming
|
resolution. Slip source and retime are also not implemented; a refusal is the
|
||||||
commands (blank, trim, move, slip source, retime) and the cel-sheet view
|
current behavior where the model demands an explicit choice nobody has made yet.
|
||||||
are also not implemented; a refusal is the current behavior where the model
|
The cel sheet is the same projected cels and selection addresses with its axes
|
||||||
demands an explicit choice nobody has made yet. The suite stands at 424 tests
|
turned: frames down and lanes across, so commands selected there and in the
|
||||||
and 5,749 assertions, with `frontend/test/browser/sequence.mjs` driving the
|
timeline have identical targets. The suite stands at 429 tests and 5,767
|
||||||
editor through create, hold, overflow, undo, reuse, make unique, duplicate,
|
assertions, with `frontend/test/browser/lane.mjs` driving the editor through
|
||||||
split, insert, trim, move and blank. Rewrite tests that encode superseded
|
create, hold, overflow, undo, reuse, make unique, duplicate, split, insert,
|
||||||
|
trim, move and blank. Rewrite tests that encode superseded
|
||||||
behavior rather than preserving behavior to keep them green.
|
behavior rather than preserving behavior to keep them green.
|
||||||
|
|
||||||
Build small adversarial documents and test their domain operations before
|
Build small adversarial documents and test their domain operations before
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@
|
||||||
:ui {:open nil
|
:ui {:open nil
|
||||||
:tabs []
|
:tabs []
|
||||||
:selection nil
|
:selection nil
|
||||||
|
:time-view :timeline
|
||||||
:tone :skin-base
|
:tone :skin-base
|
||||||
:tool nil
|
:tool nil
|
||||||
:draft []
|
:draft []
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,16 @@
|
||||||
(seq (:keys ch)) (shape (val (first (:keys ch))))
|
(seq (:keys ch)) (shape (val (first (:keys ch))))
|
||||||
:else nil))
|
:else nil))
|
||||||
|
|
||||||
|
(defn- shape-conflict [base-shape correction-shape]
|
||||||
|
(cond
|
||||||
|
(or (nil? base-shape) (nil? correction-shape)) nil
|
||||||
|
(= :opaque base-shape) "the base is not a number or a row of components"
|
||||||
|
(= :opaque correction-shape) "the correction is not a number or a row of components"
|
||||||
|
(not= base-shape correction-shape)
|
||||||
|
(str "the base has " (pr-str base-shape) " and the correction "
|
||||||
|
(pr-str correction-shape) " — a correction cannot offset a value of"
|
||||||
|
" a different shape")))
|
||||||
|
|
||||||
(defn conflict-with
|
(defn conflict-with
|
||||||
"Why correction `l` cannot apply to base channel `base`, or nil.
|
"Why correction `l` cannot apply to base channel `base`, or nil.
|
||||||
|
|
||||||
|
|
@ -166,14 +176,29 @@
|
||||||
disagreement, it is a channel with nothing in it."
|
disagreement, it is a channel with nothing in it."
|
||||||
[base l]
|
[base l]
|
||||||
(when (= :offset (:op l))
|
(when (= :offset (:op l))
|
||||||
(let [b (value-shape base) v (value-shape (:values l))]
|
(shape-conflict (value-shape base) (value-shape (:values l)))))
|
||||||
(cond
|
|
||||||
(or (nil? b) (nil? v)) nil
|
(defn- stack-conflict
|
||||||
(= :opaque b) "the base is not a number or a row of components"
|
"Why layer `i` can encounter a value of the wrong shape after the layers
|
||||||
(= :opaque v) "the correction is not a number or a row of components"
|
before it. A replace covering all of this layer's support becomes the only
|
||||||
(not= b v) (str "the base has " (pr-str b) " and the correction "
|
possible input; a partly overlapping replace adds another possible input."
|
||||||
(pr-str v) " — a correction cannot offset a value of"
|
[ch i l]
|
||||||
" a different shape")))))
|
(when (and (= :offset (:op l))
|
||||||
|
(vector? (:support l)) (= 2 (count (:support l))))
|
||||||
|
(let [[a b] (:support l)
|
||||||
|
shapes (reduce
|
||||||
|
(fn [possible prior]
|
||||||
|
(let [[c d] (when (and (vector? (:support prior))
|
||||||
|
(= 2 (count (:support prior))))
|
||||||
|
(:support prior))]
|
||||||
|
(if (and c d (not (:conflict prior)) (= :replace (:op prior))
|
||||||
|
(< a d) (< c b))
|
||||||
|
(let [s (value-shape (:values prior))]
|
||||||
|
(if (and (<= c a) (<= b d)) #{s} (conj possible s)))
|
||||||
|
possible)))
|
||||||
|
#{(value-shape ch)} (take i (:over ch)))
|
||||||
|
v (value-shape (:values l))]
|
||||||
|
(some #(shape-conflict % v) shapes))))
|
||||||
|
|
||||||
(defn conflicts
|
(defn conflicts
|
||||||
"Corrections on `ch` that cannot apply to its base, as `[{:id :why}]`.
|
"Corrections on `ch` that cannot apply to its base, as `[{:id :why}]`.
|
||||||
|
|
@ -330,18 +355,20 @@
|
||||||
dense, and throw the first time a selection landed on one that was. That is
|
dense, and throw the first time a selection landed on one that was. That is
|
||||||
how `gesture/values` took the stage down on an iris. A caller with no store
|
how `gesture/values` took the stage down on an iris. A caller with no store
|
||||||
says `nil` and means it."
|
says `nil` and means it."
|
||||||
[ch f store]
|
([ch f store] (value-at ch f f store))
|
||||||
(let [base (cond
|
([ch base-f correction-f store]
|
||||||
(not (:animated? ch)) (:value ch)
|
(let [base (cond
|
||||||
(:dense ch) (dense-at (:dense ch) f store nil)
|
(not (:animated? ch)) (:value ch)
|
||||||
(:keys ch) (let [ks (:keys ch)]
|
(:dense ch) (dense-at (:dense ch) base-f store nil)
|
||||||
(if (empty? ks) absent (keyed-at ch f)))
|
(:keys ch) (let [ks (:keys ch)]
|
||||||
:else
|
(if (empty? ks) absent (keyed-at ch base-f)))
|
||||||
(throw (ex-info "animated channel has neither :keys nor :dense"
|
:else
|
||||||
{:channel ch})))]
|
(throw (ex-info "animated channel has neither :keys nor :dense"
|
||||||
(if (seq (:over ch))
|
{:channel ch})))]
|
||||||
(over-at ch f base (fn [_ values f] (value-at values f store)))
|
(if (seq (:over ch))
|
||||||
base)))
|
(over-at ch correction-f base
|
||||||
|
(fn [_ values f] (value-at values f store)))
|
||||||
|
base))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; the playback path
|
;; the playback path
|
||||||
|
|
@ -428,12 +455,14 @@
|
||||||
A correction layer is sampled through its OWN cursor, so a stacked channel is
|
A correction layer is sampled through its OWN cursor, so a stacked channel is
|
||||||
still one reading head per key map and `value-at` stays the specification for
|
still one reading head per key map and `value-at` stays the specification for
|
||||||
the blending as well as for the base."
|
the blending as well as for the base."
|
||||||
[^Cursor cur f]
|
([cur f] (sample! cur f f))
|
||||||
(let [ch (.-ch cur)
|
([^Cursor cur base-f correction-f]
|
||||||
base (base-sample! cur ch (.-ks cur) f)]
|
(let [ch (.-ch cur)
|
||||||
(if (seq (:over ch))
|
base (base-sample! cur ch (.-ks cur) base-f)]
|
||||||
(over-at ch f base (fn [i _ f] (sample! (nth (.-overs cur) i) f)))
|
(if (seq (:over ch))
|
||||||
base)))
|
(over-at ch correction-f base
|
||||||
|
(fn [i _ f] (sample! (nth (.-overs cur) i) f)))
|
||||||
|
base))))
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -527,9 +556,9 @@
|
||||||
;; reports those. The distinction is what keeps a topology change from
|
;; reports those. The distinction is what keeps a topology change from
|
||||||
;; making a document that will not load.
|
;; making a document that will not load.
|
||||||
(and (map? ch) (vector? (:over ch)))
|
(and (map? ch) (vector? (:over ch)))
|
||||||
(into (for [l (:over ch)
|
(into (for [[i l] (map-indexed vector (:over ch))
|
||||||
:when (not (:conflict l))
|
:when (not (:conflict l))
|
||||||
:let [why (conflict-with ch l)]
|
:let [why (stack-conflict ch i l)]
|
||||||
:when why]
|
:when why]
|
||||||
(str "correction " (pr-str (:id l)) " " why)))
|
(str "correction " (pr-str (:id l)) " " why)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,13 +295,14 @@
|
||||||
|
|
||||||
`:frame` in the result is where it landed, in the open symbol's time, for a
|
`:frame` in the result is where it landed, in the open symbol's time, for a
|
||||||
caller that wants to look at what it just made."
|
caller that wants to look at what it just made."
|
||||||
[clip sid lane-id id drawing-id at extent]
|
[clip sid lane-id id drawing-id at extent ripple?]
|
||||||
(let [nodes (get-in clip [:symbols sid :nodes])
|
(let [nodes (get-in clip [:symbols sid :nodes])
|
||||||
at (if (= :end at) (lane-end nodes lane-id) at)
|
at (if (= :end at) (lane-end nodes lane-id) at)
|
||||||
n (held id lane-id drawing-id at)
|
n (held id lane-id drawing-id at)
|
||||||
[lo hi] (node/placed-span n)
|
[lo hi] (node/placed-span n)
|
||||||
later (filter #(>= (first (node/placed-span %)) lo)
|
later (when ripple?
|
||||||
(symbol/lane-cels nodes lane-id))
|
(filter #(>= (first (node/placed-span %)) lo)
|
||||||
|
(symbol/lane-cels nodes lane-id)))
|
||||||
nodes (reduce (fn [ns sibling]
|
nodes (reduce (fn [ns sibling]
|
||||||
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
|
(update-in ns [(:id sibling) :time :at] (fnil + 0) (- hi lo)))
|
||||||
(assoc nodes id n) later)
|
(assoc nodes id n) later)
|
||||||
|
|
@ -344,7 +345,7 @@
|
||||||
{:refused why}
|
{:refused why}
|
||||||
(place (assoc-in clip [:symbols drawing-id]
|
(place (assoc-in clip [:symbols drawing-id]
|
||||||
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
||||||
sid lane-id id drawing-id at extent)))
|
sid lane-id id drawing-id at extent true)))
|
||||||
|
|
||||||
(defn reuse-drawing
|
(defn reuse-drawing
|
||||||
"Append a held cel of content the document ALREADY has, so the same
|
"Append a held cel of content the document ALREADY has, so the same
|
||||||
|
|
@ -361,7 +362,7 @@
|
||||||
(when (clip/contains-symbol? clip drawing-id sid)
|
(when (clip/contains-symbol? clip drawing-id sid)
|
||||||
"a symbol cannot go inside itself"))]
|
"a symbol cannot go inside itself"))]
|
||||||
{:refused why}
|
{:refused why}
|
||||||
(place clip sid lane-id id drawing-id at extent)))
|
(place clip sid lane-id id drawing-id at extent true)))
|
||||||
|
|
||||||
(defn- copied
|
(defn- copied
|
||||||
"A copy of symbol `from`, as `{:clip :id}`.
|
"A copy of symbol `from`, as `{:clip :id}`.
|
||||||
|
|
@ -396,7 +397,34 @@
|
||||||
(placeable clip sid (:parent n) new-id at))]
|
(placeable clip sid (:parent n) new-id at))]
|
||||||
{:refused why}
|
{:refused why}
|
||||||
(let [{c :clip copy :id} (copied clip from deep?)]
|
(let [{c :clip copy :id} (copied clip from deep?)]
|
||||||
(place c sid (:parent n) new-id copy at extent)))))
|
(place c sid (:parent n) new-id copy at extent true)))))
|
||||||
|
|
||||||
|
(defn overwrite-drawing
|
||||||
|
"Put a fresh one-frame drawing at lane frame `at`, replacing whatever was
|
||||||
|
there and leaving every other cel where it was.
|
||||||
|
|
||||||
|
This is `blank` and placement composed in ONE command and therefore one undo
|
||||||
|
step. `remainder-id` is used only when clearing the frame cuts one cel into
|
||||||
|
two; ids still come from the caller because this namespace is pure."
|
||||||
|
[clip sid lane-id id drawing-id at {:keys [extent remainder-id]
|
||||||
|
:or {extent :keep}}]
|
||||||
|
(let [nodes (get-in clip [:symbols sid :nodes])]
|
||||||
|
(if-let [why (cond
|
||||||
|
(not (and (integer? at) (not (neg? at))))
|
||||||
|
"a position is a nonnegative whole lane frame"
|
||||||
|
(contains? nodes id) "the new cel ID is already used"
|
||||||
|
(or (= id remainder-id) (contains? nodes remainder-id))
|
||||||
|
"the remainder cel needs a free ID different from the new cel"
|
||||||
|
(clip/symbol clip drawing-id) "the new drawing ID is already used"
|
||||||
|
(nil? (lane-map nodes lane-id))
|
||||||
|
"drawing creation through a stepped or looping lane is not supported")]
|
||||||
|
{:refused why}
|
||||||
|
(let [cleared (blank clip sid lane-id [at (inc at)] {:id remainder-id})]
|
||||||
|
(if (:refused cleared)
|
||||||
|
cleared
|
||||||
|
(place (assoc-in (:clip cleared) [:symbols drawing-id]
|
||||||
|
{:id drawing-id :name (name drawing-id) :frames 1 :nodes {}})
|
||||||
|
sid lane-id id drawing-id at extent false))))))
|
||||||
|
|
||||||
(defn make-unique
|
(defn make-unique
|
||||||
"Point cel `id` at a private copy of its content, leaving every other
|
"Point cel `id` at a private copy of its content, leaving every other
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,7 @@
|
||||||
{:keys (vec (sort-by str (keys sym)))})))
|
{:keys (vec (sort-by str (keys sym)))})))
|
||||||
(into {} (remove #(= :audio (:kind (val %)))) nodes)))
|
(into {} (remove #(= :audio (:kind (val %)))) nodes)))
|
||||||
|
|
||||||
(defn- channel-frame
|
(defn- base-channel-frame
|
||||||
"A trace selects the measured frames its node reads; marked channels read
|
"A trace selects the measured frames its node reads; marked channels read
|
||||||
instance pose choices."
|
instance pose choices."
|
||||||
[choices traces nodes source-fps picture-fps id c lf]
|
[choices traces nodes source-fps picture-fps id c lf]
|
||||||
|
|
@ -471,9 +471,9 @@
|
||||||
traces (prepared-traces nodes)
|
traces (prepared-traces nodes)
|
||||||
ord (order nodes)]
|
ord (order nodes)]
|
||||||
(eval-into {:read (fn [id path c lf]
|
(eval-into {:read (fn [id path c lf]
|
||||||
(ch/value-at c (channel-frame choices traces nodes
|
(ch/value-at c (base-channel-frame choices traces nodes
|
||||||
source-fps picture-fps id c lf)
|
source-fps picture-fps id c lf)
|
||||||
store))
|
lf store))
|
||||||
:palette palette
|
:palette palette
|
||||||
:mat-for (fn [_id] (node/mat))
|
:mat-for (fn [_id] (node/mat))
|
||||||
:pinv-for (fn [id] (node/pinv (get nodes id)))
|
:pinv-for (fn [id] (node/pinv (get nodes id)))
|
||||||
|
|
@ -559,8 +559,9 @@
|
||||||
ctx {:read (fn [id path c lf]
|
ctx {:read (fn [id path c lf]
|
||||||
(when-let [cursor (get-in cursors [id path])]
|
(when-let [cursor (get-in cursors [id path])]
|
||||||
(ch/sample! cursor
|
(ch/sample! cursor
|
||||||
(channel-frame choices traces nodes
|
(base-channel-frame choices traces nodes
|
||||||
source-fps picture-fps id c lf))))
|
source-fps picture-fps id c lf)
|
||||||
|
lf)))
|
||||||
:palette palette
|
:palette palette
|
||||||
:mat-for (fn [id] (get mats id))
|
:mat-for (fn [id] (get mats id))
|
||||||
:on-place (fn [id p] (vswap! placed assoc id p))
|
:on-place (fn [id p] (vswap! placed assoc id p))
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@
|
||||||
::set-tone
|
::set-tone
|
||||||
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
|
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
|
||||||
|
|
||||||
|
(rf/reg-event-db
|
||||||
|
::set-time-view
|
||||||
|
(fn [db [_ view]]
|
||||||
|
(if (#{:timeline :cel-sheet} view)
|
||||||
|
(assoc-in db [:ui :time-view] view)
|
||||||
|
db)))
|
||||||
|
|
||||||
(defn apply-lane-command
|
(defn apply-lane-command
|
||||||
"Commit a successful domain command as one history step. A refused command
|
"Commit a successful domain command as one history step. A refused command
|
||||||
leaves the document and history untouched; an overflow offers an explicit retry."
|
leaves the document and history untouched; an overflow offers an explicit retry."
|
||||||
|
|
@ -82,6 +89,18 @@
|
||||||
(let [n (get-in clip [:symbols sid :nodes id])]
|
(let [n (get-in clip [:symbols sid :nodes id])]
|
||||||
(if (node/lane? n) id (:parent n))))
|
(if (node/lane? n) id (:parent n))))
|
||||||
|
|
||||||
|
(defn selection-frame
|
||||||
|
"The playhead as a frame of the symbol that owns `selection`.
|
||||||
|
|
||||||
|
A timeline selection carries its path from the open symbol. Walking to the
|
||||||
|
parent of the selected node crosses every enclosing instance clock before a
|
||||||
|
lane command converts that owning-symbol frame into lane time."
|
||||||
|
[clip st open selection frame]
|
||||||
|
(let [[_ sid _ path] selection]
|
||||||
|
(if (or (= sid open) (not (seq path)))
|
||||||
|
frame
|
||||||
|
(:frame (nest/inside clip st open (pop path) frame)))))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
::append-drawing
|
::append-drawing
|
||||||
(fn [{:keys [db]} [_ extent]]
|
(fn [{:keys [db]} [_ extent]]
|
||||||
|
|
@ -117,10 +136,13 @@
|
||||||
;; that is stepped or retimed off whole frames has no single lane frame for a
|
;; that is stepped or retimed off whole frames has no single lane frame for a
|
||||||
;; symbol frame, and `lane-frame` says so rather than snapping to one.
|
;; symbol frame, and `lane-frame` says so rather than snapping to one.
|
||||||
(fn [{:keys [db]} [_ extent]]
|
(fn [{:keys [db]} [_ extent]]
|
||||||
(let [clip (:clip (store/entry (:clip/current db)))
|
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||||
[_ sid id] (get-in db [:ui :selection])
|
selection (get-in db [:ui :selection])
|
||||||
|
[_ sid id] selection
|
||||||
lane (selected-lane clip sid id)
|
lane (selected-lane clip sid id)
|
||||||
at (lane/lane-frame clip sid lane (get-in db [:playback :frame]))
|
owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||||
|
(get-in db [:playback :frame]))
|
||||||
|
at (when (number? owner-frame) (lane/lane-frame clip sid lane owner-frame))
|
||||||
result (if at
|
result (if at
|
||||||
(lane/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip)
|
(lane/append-drawing clip sid lane (random-uuid) (clip/fresh-id clip)
|
||||||
{:at at :extent (or extent :keep)})
|
{:at at :extent (or extent :keep)})
|
||||||
|
|
@ -130,10 +152,14 @@
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::split-cel
|
::split-cel
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
(let [clip (:clip (store/entry (:clip/current db)))
|
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||||
[_ sid id] (get-in db [:ui :selection])
|
selection (get-in db [:ui :selection])
|
||||||
cut (lane/lane-frame clip sid (:parent (get-in clip [:symbols sid :nodes id]))
|
[_ sid id] selection
|
||||||
(get-in db [:playback :frame]))]
|
owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||||
|
(get-in db [:playback :frame]))
|
||||||
|
cut (when (number? owner-frame)
|
||||||
|
(lane/lane-frame clip sid (:parent (get-in clip [:symbols sid :nodes id]))
|
||||||
|
owner-frame))]
|
||||||
(apply-lane-command
|
(apply-lane-command
|
||||||
db sid (if cut
|
db sid (if cut
|
||||||
(lane/split clip sid id cut (random-uuid))
|
(lane/split clip sid id cut (random-uuid))
|
||||||
|
|
@ -144,11 +170,31 @@
|
||||||
"The selected cel, its lane, and the playhead as a frame of that lane's
|
"The selected cel, its lane, and the playhead as a frame of that lane's
|
||||||
own time — or a refusal in place of the frame where there is no single one."
|
own time — or a refusal in place of the frame where there is no single one."
|
||||||
[db]
|
[db]
|
||||||
(let [clip (:clip (store/entry (:clip/current db)))
|
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||||
[_ sid id] (get-in db [:ui :selection])
|
selection (get-in db [:ui :selection])
|
||||||
|
[_ sid id] selection
|
||||||
n (get-in clip [:symbols sid :nodes id])]
|
n (get-in clip [:symbols sid :nodes id])]
|
||||||
{:clip clip :sid sid :id id :node n
|
{:clip clip :sid sid :id id :node n
|
||||||
:at (lane/lane-frame clip sid (:parent n) (get-in db [:playback :frame]))}))
|
:at (when-let [owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||||
|
(get-in db [:playback :frame]))]
|
||||||
|
(lane/lane-frame clip sid (:parent n) owner-frame))}))
|
||||||
|
|
||||||
|
(rf/reg-event-fx
|
||||||
|
::overwrite-drawing
|
||||||
|
(fn [{:keys [db]} [_ extent]]
|
||||||
|
(let [{clip :clip st :store} (store/entry (:clip/current db))
|
||||||
|
selection (get-in db [:ui :selection])
|
||||||
|
[_ sid id] selection
|
||||||
|
lane-id (selected-lane clip sid id)
|
||||||
|
owner-frame (selection-frame clip st (get-in db [:ui :open]) selection
|
||||||
|
(get-in db [:playback :frame]))
|
||||||
|
at (when (number? owner-frame) (lane/lane-frame clip sid lane-id owner-frame))
|
||||||
|
result (if (integer? at)
|
||||||
|
(lane/overwrite-drawing clip sid lane-id (random-uuid) (clip/fresh-id clip) at
|
||||||
|
{:extent (or extent :keep)
|
||||||
|
:remainder-id (random-uuid)})
|
||||||
|
{:refused "this lane's frames are not the open symbol's"})]
|
||||||
|
(committed db sid result [::overwrite-drawing :grow-symbol]))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::trim-cel
|
::trim-cel
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
[re-frame.core :as rf]))
|
[re-frame.core :as rf]))
|
||||||
|
|
||||||
(rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection])))
|
(rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection])))
|
||||||
|
(rf/reg-sub ::time-view (fn [db _] (get-in db [:ui :time-view] :timeline)))
|
||||||
(rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry])))
|
(rf/reg-sub ::lane-retry (fn [db _] (get-in db [:ui :lane-retry])))
|
||||||
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||||
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
[arthur.domain.trace :as trace]
|
[arthur.domain.trace :as trace]
|
||||||
[arthur.events.playback :as pb]
|
[arthur.events.playback :as pb]
|
||||||
[arthur.events.ui :as ui]
|
[arthur.events.ui :as ui]
|
||||||
|
[arthur.footage.store :as store]
|
||||||
[arthur.subs.playback :as playback]
|
[arthur.subs.playback :as playback]
|
||||||
[arthur.subs.render :as render]
|
[arthur.subs.render :as render]
|
||||||
[arthur.subs.ui :as sub]
|
[arthur.subs.ui :as sub]
|
||||||
|
|
@ -226,7 +227,11 @@
|
||||||
frames @(rf/subscribe [::render/frames])
|
frames @(rf/subscribe [::render/frames])
|
||||||
{:keys [fps drop]} @player/meter
|
{:keys [fps drop]} @player/meter
|
||||||
clip @(rf/subscribe [::render/clip])
|
clip @(rf/subscribe [::render/clip])
|
||||||
[_ sid id] @(rf/subscribe [::sub/selection])
|
clip-id @(rf/subscribe [::render/clip-id])
|
||||||
|
selection @(rf/subscribe [::sub/selection])
|
||||||
|
[_ sid id] selection
|
||||||
|
open @(rf/subscribe [::render/open])
|
||||||
|
st (:store (store/entry clip-id))
|
||||||
n (get-in clip [:symbols sid :nodes id])
|
n (get-in clip [:symbols sid :nodes id])
|
||||||
lane (if (node/lane? n) n (get-in clip [:symbols sid :nodes (:parent n)]))
|
lane (if (node/lane? n) n (get-in clip [:symbols sid :nodes (:parent n)]))
|
||||||
lane? (node/lane? lane)
|
lane? (node/lane? lane)
|
||||||
|
|
@ -240,7 +245,9 @@
|
||||||
other))))
|
other))))
|
||||||
;; Both act at the playhead, so both are offered only where the playhead
|
;; Both act at the playhead, so both are offered only where the playhead
|
||||||
;; is somewhere they mean something.
|
;; is somewhere they mean something.
|
||||||
at (when lane? (lane/lane-frame clip sid (:id lane) frame))
|
owner-frame (ui/selection-frame clip st open selection frame)
|
||||||
|
at (when (and lane? (number? owner-frame))
|
||||||
|
(lane/lane-frame clip sid (:id lane) owner-frame))
|
||||||
insertable? (and lane? (integer? at))
|
insertable? (and lane? (integer? at))
|
||||||
splittable? (and cel? (integer? at)
|
splittable? (and cel? (integer? at)
|
||||||
(let [[lo hi] (node/placed-span n)] (< lo at hi)))]
|
(let [[lo hi] (node/placed-span n)] (< lo at hi)))]
|
||||||
|
|
@ -253,6 +260,10 @@
|
||||||
:on-click #(rf/dispatch [::pb/toggle-loop])} "loop"]
|
:on-click #(rf/dispatch [::pb/toggle-loop])} "loop"]
|
||||||
[:button {:class (when @(rf/subscribe [::playback/muted?]) "on")
|
[:button {:class (when @(rf/subscribe [::playback/muted?]) "on")
|
||||||
:on-click #(rf/dispatch [::pb/toggle-mute])} "mute"]
|
:on-click #(rf/dispatch [::pb/toggle-mute])} "mute"]
|
||||||
|
[:button {:class (when (= :timeline @(rf/subscribe [::sub/time-view])) "on")
|
||||||
|
:on-click #(rf/dispatch [::ui/set-time-view :timeline])} "timeline"]
|
||||||
|
[:button {:class (when (= :cel-sheet @(rf/subscribe [::sub/time-view])) "on")
|
||||||
|
:on-click #(rf/dispatch [::ui/set-time-view :cel-sheet])} "cel sheet"]
|
||||||
(doall
|
(doall
|
||||||
(for [r [0.25 0.5 1.0 2.0 4.0]]
|
(for [r [0.25 0.5 1.0 2.0 4.0]]
|
||||||
^{:key r}
|
^{:key r}
|
||||||
|
|
@ -272,6 +283,9 @@
|
||||||
[:button {:disabled (not insertable?)
|
[:button {:disabled (not insertable?)
|
||||||
:title "a new drawing at the playhead; later drawings ripple later"
|
:title "a new drawing at the playhead; later drawings ripple later"
|
||||||
:on-click #(rf/dispatch [::ui/insert-drawing])} "insert"]
|
:on-click #(rf/dispatch [::ui/insert-drawing])} "insert"]
|
||||||
|
[:button {:disabled (not insertable?)
|
||||||
|
:title "replace the drawing at the playhead; later drawings stay put"
|
||||||
|
:on-click #(rf/dispatch [::ui/overwrite-drawing])} "overwrite"]
|
||||||
[:button {:disabled (not splittable?)
|
[:button {:disabled (not splittable?)
|
||||||
:title "cut this cel in two at the playhead; the picture does not change"
|
:title "cut this cel in two at the playhead; the picture does not change"
|
||||||
:on-click #(rf/dispatch [::ui/split-cel])} "split"]
|
:on-click #(rf/dispatch [::ui/split-cel])} "split"]
|
||||||
|
|
@ -492,7 +506,7 @@
|
||||||
(for [f keys :when (and (<= 0 f) (< f frames))]
|
(for [f keys :when (and (<= 0 f) (< f frames))]
|
||||||
^{:key f} [:div.tl-key {:style {:left (at% f frames)}}])))]))
|
^{:key f} [:div.tl-key {:style {:left (at% f frames)}}])))]))
|
||||||
|
|
||||||
(defn view []
|
(defn- timeline-view []
|
||||||
(r/with-let [scrubbing (r/atom false)
|
(r/with-let [scrubbing (r/atom false)
|
||||||
;; The row a carried row is over and which part of it, for the
|
;; The row a carried row is over and which part of it, for the
|
||||||
;; highlight.
|
;; highlight.
|
||||||
|
|
@ -603,3 +617,56 @@
|
||||||
{:key (str (:path row))})))
|
{:key (str (:path row))})))
|
||||||
[:div.tl-empty "nothing in this symbol"])
|
[:div.tl-empty "nothing in this symbol"])
|
||||||
[:div.tl-playhead {:style {:left (at% frame frames)}}]]]])))
|
[:div.tl-playhead {:style {:left (at% frame frames)}}]]]])))
|
||||||
|
|
||||||
|
(defn cel-sheet
|
||||||
|
"The cel-sheet projection of one symbol: frames down, one column per lane.
|
||||||
|
It reuses `rows`, so its spans and selection addresses are exactly the ones
|
||||||
|
the timeline presents rather than a second interpretation of the document."
|
||||||
|
[clip sid frames]
|
||||||
|
(mapv (fn [{:keys [path label cels]}]
|
||||||
|
{:id (peek path)
|
||||||
|
:label label
|
||||||
|
:cells (mapv (fn [f]
|
||||||
|
(let [cel (some (fn [{[in out] :span :as cel}]
|
||||||
|
(when (and (<= in f) (< f out)) cel))
|
||||||
|
cels)]
|
||||||
|
{:frame f :lane (peek path) :cel cel}))
|
||||||
|
(range frames))})
|
||||||
|
(filter :cels (rows clip sid #{}))))
|
||||||
|
|
||||||
|
(defn- cel-sheet-view []
|
||||||
|
(let [clip @(rf/subscribe [::render/clip])
|
||||||
|
sid @(rf/subscribe [::render/open])
|
||||||
|
frames (max 1 (or @(rf/subscribe [::render/frames]) 1))
|
||||||
|
frame @(rf/subscribe [::playback/frame])
|
||||||
|
selection @(rf/subscribe [::sub/selection])
|
||||||
|
columns (cel-sheet clip sid frames)
|
||||||
|
style {:grid-template-columns
|
||||||
|
(str "52px repeat(" (max 1 (count columns)) ", minmax(110px, 1fr))")}]
|
||||||
|
[:div.cel-sheet {:style style}
|
||||||
|
[:div.cs-head.cs-frame "frame"]
|
||||||
|
(doall (for [{:keys [id label]} columns]
|
||||||
|
^{:key (str "head-" id)} [:div.cs-head label]))
|
||||||
|
(doall
|
||||||
|
(for [f (range frames)
|
||||||
|
item (cons {:frame-label? true}
|
||||||
|
(map #(get-in % [:cells f]) columns))]
|
||||||
|
(if (:frame-label? item)
|
||||||
|
^{:key (str "frame-" f)}
|
||||||
|
[:button.cs-frame {:class (when (= f frame) "on")
|
||||||
|
:on-click #(rf/dispatch [::pb/seek f])} f]
|
||||||
|
(let [{:keys [id label select]} (:cel item)]
|
||||||
|
^{:key (str f "-" (:lane item) "-" (or id "gap"))}
|
||||||
|
[:button.cs-cell
|
||||||
|
{:class (str (when (= f frame) " current")
|
||||||
|
(when (and select (= select selection)) " selected"))
|
||||||
|
:title (if id (str label " · frame " f) (str "gap · frame " f))
|
||||||
|
:on-click (fn []
|
||||||
|
(rf/dispatch [::pb/seek f])
|
||||||
|
(when select (rf/dispatch [::ui/select select])))}
|
||||||
|
(or label "—")]))))]))
|
||||||
|
|
||||||
|
(defn view []
|
||||||
|
(if (= :cel-sheet @(rf/subscribe [::sub/time-view]))
|
||||||
|
[:section.pane.time [transport] [cel-sheet-view]]
|
||||||
|
[timeline-view]))
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,26 @@
|
||||||
(is (= [3 9 9 3] (two add2 put9)) "the later layer sees the earlier one's result")
|
(is (= [3 9 9 3] (two add2 put9)) "the later layer sees the earlier one's result")
|
||||||
(is (= [3 11 11 3] (two put9 add2)) "and order therefore matters")))
|
(is (= [3 11 11 3] (two put9 add2)) "and order therefore matters")))
|
||||||
|
|
||||||
|
(deftest validation-follows-the-shape-produced-by-an-ordered-stack
|
||||||
|
(let [base (ch/framed [0 0])
|
||||||
|
put3 (ch/layer :put3 [0 2] :replace (ch/framed [1 2 3]))
|
||||||
|
add2 (ch/layer :add2 [0 2] :offset (ch/framed [1 1]))
|
||||||
|
add3 (ch/layer :add3 [0 2] :offset (ch/framed [1 1 1]))]
|
||||||
|
(is (seq (ch/problems (corrected base put3 add2)))
|
||||||
|
"validation rejects a stack that would throw while reading")
|
||||||
|
(is (empty? (ch/problems (corrected base put3 add3)))
|
||||||
|
"a covering replacement establishes the shape seen by later layers")
|
||||||
|
(is (= [2 3 4] (ch/value-at (corrected base put3 add3) 0 nil)))))
|
||||||
|
|
||||||
|
(deftest generated-base-time-and-authored-correction-time-can-differ
|
||||||
|
(let [c (corrected (ch/keyed {0 0, 2 20} :hold)
|
||||||
|
(ch/layer :nudge [1 2] :offset (ch/framed 3)))
|
||||||
|
cursor (ch/cursor c nil)]
|
||||||
|
(is (= 3 (ch/value-at c 0 1 nil)))
|
||||||
|
(is (= 3 (ch/sample! cursor 0 1)))
|
||||||
|
(is (= 23 (ch/value-at c 2 1 nil)))
|
||||||
|
(is (= 20 (ch/sample! cursor 2 0)))))
|
||||||
|
|
||||||
(deftest replace-can-supply-a-value-where-offset-has-nothing-to-add-to
|
(deftest replace-can-supply-a-value-where-offset-has-nothing-to-add-to
|
||||||
;; An absent value is not zero. `replace` states a pose; `offset` cannot
|
;; An absent value is not zero. `replace` states a pose; `offset` cannot
|
||||||
;; offset a pose that was never measured, and must not invent one.
|
;; offset a pose that was never measured, and must not invent one.
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,8 @@
|
||||||
visible (ch/keyed {0 true 20 true 21 false} :hold)
|
visible (ch/keyed {0 true 20 true 21 false} :hold)
|
||||||
dense {:animated? true :interp :hold
|
dense {:animated? true :interp :hold
|
||||||
:dense {:store "sizes" :offset 0 :stride 1 :frames 30}
|
:dense {:store "sizes" :offset 0 :stride 1 :frames 30}
|
||||||
:pose-sampled? true}
|
:pose-sampled? true
|
||||||
|
:over [(ch/layer :nudge [7 8] :offset (ch/framed 100))]}
|
||||||
shape (fn [id z group]
|
shape (fn [id z group]
|
||||||
{:id id :kind :rect :parent :root :z z :pose-group group
|
{:id id :kind :rect :parent :root :z z :pose-group group
|
||||||
:channels {[:xform :pos] (ch/keyed {0 [0 0] 8 [8 0]} :hold)
|
:channels {[:xform :pos] (ch/keyed {0 [0 0] 8 [8 0]} :hold)
|
||||||
|
|
@ -86,18 +87,18 @@
|
||||||
low-at (fn [f] (into {} (map (fn [op] [(:node op) op])) (low-resolve f)))]
|
low-at (fn [f] (into {} (map (fn [op] [(:node op) op])) (low-resolve f)))]
|
||||||
(is (empty? (clip/problems document)))
|
(is (empty? (clip/problems document)))
|
||||||
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
(is (= document (leaf/clip "stage" (leaf/leaves "stage" document))))
|
||||||
(is (= 2 (:size (get (at 7) [:first :mouth]))) "eight static frames")
|
(is (= 102 (:size (get (at 7) [:first :mouth]))) "eight static frames, plus its correction")
|
||||||
(is (= 22 (:size (get (at 8) [:first :mouth]))) "cut to source pose 20")
|
(is (= 22 (:size (get (at 8) [:first :mouth]))) "cut to source pose 20")
|
||||||
(is (= 6 (:size (get (at 8) [:first :mouth-detail])))
|
(is (= 6 (:size (get (at 8) [:first :mouth-detail])))
|
||||||
"one node may depart from its shared mouth group")
|
"one node may depart from its shared mouth group")
|
||||||
(is (= 10 (:size (get (at 8) [:second :mouth]))) "other instance chooses pose 8")
|
(is (= 10 (:size (get (at 8) [:second :mouth]))) "other instance chooses pose 8")
|
||||||
(is (= 6 (:size (get (at 7) [:first :eye]))) "eye has its own timing")
|
(is (= 106 (:size (get (at 7) [:first :eye]))) "eye has its own timing")
|
||||||
(is (= 8 (:cx (get (at 8) [:first :eye]))) "authored position still reads stage time")
|
(is (= 8 (:cx (get (at 8) [:first :eye]))) "authored position still reads stage time")
|
||||||
(is (nil? (get (at 9) [:first :mouth]))
|
(is (nil? (get (at 9) [:first :mouth]))
|
||||||
"generated visibility is read from the same selected pose")
|
"generated visibility is read from the same selected pose")
|
||||||
(is (some? (get (at 9) [:second :mouth])))
|
(is (some? (get (at 9) [:second :mouth])))
|
||||||
(is (= 5 (:size (get (low-at 7) [:first :brow])))
|
(is (= 105 (:size (get (low-at 7) [:first :brow])))
|
||||||
"picture rate samples only generated motion")
|
"picture rate samples the generated base and leaves correction time alone")
|
||||||
(is (= 22 (:size (get (low-at 8) [:first :mouth])))
|
(is (= 22 (:size (get (low-at 8) [:first :mouth])))
|
||||||
"an explicit cut occurs at its exact local frame, even off the picture grid")
|
"an explicit cut occurs at its exact local frame, even off the picture grid")
|
||||||
(is (= 8 (:cx (get (low-at 8) [:first :eye])))
|
(is (= 8 (:cx (get (low-at 8) [:first :eye])))
|
||||||
|
|
|
||||||
|
|
@ -581,6 +581,18 @@
|
||||||
"the insert kept its own frames, so frame 9 shows what it showed")
|
"the insert kept its own frames, so frame 9 shows what it showed")
|
||||||
(is (empty? (clip/problems after)))))
|
(is (empty? (clip/problems after)))))
|
||||||
|
|
||||||
|
(deftest overwrite-clears-one-frame-and-does-not-ripple-what-follows
|
||||||
|
(let [r (lane/overwrite-drawing (document) :main :girl :n :drawing-n 5
|
||||||
|
{:extent :keep :remainder-id :right})
|
||||||
|
after (:clip r)
|
||||||
|
nodes (get-in after [:symbols :main :nodes])]
|
||||||
|
(is (= :n (:selection r)))
|
||||||
|
(is (= [[0 4] [4 5] [5 6] [6 8] [8 12]]
|
||||||
|
(mapv #(node/placed-span (get nodes %)) [:a :b :n :right :insert])))
|
||||||
|
(is (= :drawing-b (node/source (:right nodes))))
|
||||||
|
(is (= 12 (get-in after [:symbols :main :frames])))
|
||||||
|
(is (empty? (clip/problems after)))))
|
||||||
|
|
||||||
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
(deftest blank-refuses-what-it-cannot-do-in-one-piece
|
||||||
(let [doc (document)]
|
(let [doc (document)]
|
||||||
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
|
(is (re-find #"free ID" (:refused (lane/blank doc :main :girl [5 7] {})))
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,31 @@
|
||||||
(is (= [0 6 12] (:keys lane)))
|
(is (= [0 6 12] (:keys lane)))
|
||||||
(is (= 1 (count (filter :cels (timeline/rows doc :main #{[:girl]})))))))
|
(is (= 1 (count (filter :cels (timeline/rows doc :main #{[:girl]})))))))
|
||||||
|
|
||||||
|
(deftest the-cel-sheet-is-the-same-cels-with-the-axes-turned
|
||||||
|
(let [doc (fixture/document)
|
||||||
|
column (first (timeline/cel-sheet doc :main 12))
|
||||||
|
cells (:cells column)]
|
||||||
|
(is (= :girl (:id column)))
|
||||||
|
(is (= [:a :b :insert] (mapv #(get-in cells [% :cel :id]) [0 4 8])))
|
||||||
|
(is (= [[:node :main :a [:a]]
|
||||||
|
[:node :main :b [:b]]
|
||||||
|
[:node :main :insert [:insert]]]
|
||||||
|
(mapv #(get-in cells [% :cel :select]) [0 4 8])))
|
||||||
|
(is (= (mapv :select (:cels (first (filter :cels (timeline/rows doc :main #{})))))
|
||||||
|
(mapv #(get-in cells [% :cel :select]) [0 4 8])))))
|
||||||
|
|
||||||
|
(deftest a-nested-selection-converts-the-open-playhead-to-its-owning-symbol
|
||||||
|
(let [doc (assoc-in (fixture/document) [:symbols :outer]
|
||||||
|
{:id :outer :frames 30
|
||||||
|
:nodes {:take {:id :take :kind :instance :z "a"
|
||||||
|
:time {:at 10 :rate 1} :span [0 12]
|
||||||
|
:source {:symbol :main}
|
||||||
|
:playback {:in 0 :speed 1 :end :stop}}}})]
|
||||||
|
(is (= 2 (ui/selection-frame doc nil :outer
|
||||||
|
[:node :main :a [:take :a]] 12)))
|
||||||
|
(is (= 12 (ui/selection-frame doc nil :main
|
||||||
|
[:node :main :a [:a]] 12)))))
|
||||||
|
|
||||||
(deftest sequence-commands-use-isolated-history-transactions
|
(deftest sequence-commands-use-isolated-history-transactions
|
||||||
(let [doc (fixture/document)
|
(let [doc (fixture/document)
|
||||||
id (store/install! {:clip doc :store {}} "sequence-test")
|
id (store/install! {:clip doc :store {}} "sequence-test")
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,25 @@ try {
|
||||||
'blanked: a gap where it was, and nothing closed it');
|
'blanked: a gap where it was, and nothing closed it');
|
||||||
assert.equal(s.clip.symbols.main.frames, 20, 'the shot is as long as it was authored');
|
assert.equal(s.clip.symbols.main.frames, 20, 'the shot is as long as it was authored');
|
||||||
assert.equal(s.history.done.length, before.history.done.length + 8);
|
assert.equal(s.history.done.length, before.history.done.length + 8);
|
||||||
|
|
||||||
|
// The same cels with the axes turned. Selecting a sheet cell feeds the same
|
||||||
|
// action strip and therefore the same domain command and undo transaction.
|
||||||
|
await click('cel sheet');
|
||||||
|
assert.equal(await evaluate('document.querySelectorAll(".cs-head:not(.cs-frame)").length'), 1,
|
||||||
|
'one lane is one cel-sheet column');
|
||||||
|
assert.equal(await evaluate('document.querySelectorAll(".cs-cell").length'), 20,
|
||||||
|
'one cell per authored frame');
|
||||||
|
await evaluate(`document.querySelector('.cs-cell').click()`);
|
||||||
|
await sleep(180);
|
||||||
|
await click('hold +');
|
||||||
|
s = await shot();
|
||||||
|
assert.deepEqual(placed(s), [[0, 3], [3, 4], [6, 7], [7, 8], [8, 9]],
|
||||||
|
'a command selected in the sheet has the timeline command semantics');
|
||||||
|
assert.equal(s.history.done.length, before.history.done.length + 9);
|
||||||
|
await click('timeline');
|
||||||
|
assert.equal(await evaluate('document.querySelectorAll(".tl-cel").length'), 5);
|
||||||
assert.equal(errors.length, 0, JSON.stringify(errors));
|
assert.equal(errors.length, 0, JSON.stringify(errors));
|
||||||
console.log('PASS: create lane/drawings, one-row cels, hold ripple, seek, explicit overflow, atomic undo, reuse/make unique/duplicate, split, insert, trim, move and blank; no server writes');
|
console.log('PASS: lane commands agree from timeline and cel sheet; no server writes');
|
||||||
} finally {
|
} finally {
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ id: 999999, method: 'Browser.close' }));
|
ws.send(JSON.stringify({ id: 999999, method: 'Browser.close' }));
|
||||||
|
|
|
||||||
|
|
@ -772,6 +772,50 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||||||
|
|
||||||
.tl-empty { padding: 9px; color: var(--dim); }
|
.tl-empty { padding: 9px; color: var(--dim); }
|
||||||
|
|
||||||
|
/* The second temporal view. Its cells carry the same selection addresses as
|
||||||
|
the cel blocks above; only the axes change. */
|
||||||
|
.cel-sheet {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
background: var(--line);
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-head,
|
||||||
|
.cs-frame,
|
||||||
|
.cs-cell {
|
||||||
|
min-width: 0;
|
||||||
|
height: 24px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--fg);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-head {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--chrome);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-head.cs-frame { z-index: 3; }
|
||||||
|
.cs-frame { position: sticky; left: 0; z-index: 1; color: var(--dim); text-align: right; }
|
||||||
|
.cs-frame.on, .cs-cell.current { box-shadow: inset 3px 0 0 var(--playhead); }
|
||||||
|
.cs-cell { text-align: left; cursor: pointer; }
|
||||||
|
.cs-cell:hover { background: var(--sel-bg); }
|
||||||
|
.cs-cell.selected { background: var(--sel-bg); color: var(--sel); font-weight: 600; }
|
||||||
|
|
||||||
/* --------------------------------------------------------------------------
|
/* --------------------------------------------------------------------------
|
||||||
the video -> symbol dialog */
|
the video -> symbol dialog */
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue