perfect target area thing
This commit is contained in:
parent
664252e0fc
commit
5bcf22e458
23 changed files with 668 additions and 432 deletions
|
|
@ -211,8 +211,8 @@ def probe(path):
|
||||||
raise ValueError(f"the video reports a frame rate of {float(rate):g}, which is "
|
raise ValueError(f"the video reports a frame rate of {float(rate):g}, which is "
|
||||||
"not a rate footage can be measured at")
|
"not a rate footage can be measured at")
|
||||||
duration = float(data.get("format", {}).get("duration") or 0)
|
duration = float(data.get("format", {}).get("duration") or 0)
|
||||||
if duration > 0 and duration * float(rate) > 901:
|
# if duration > 0 and duration * float(rate) > 901:
|
||||||
raise ValueError("video is longer than the 900-frame footage limit")
|
# raise ValueError("video is longer than the 900-frame footage limit")
|
||||||
frames = video.get("nb_frames")
|
frames = video.get("nb_frames")
|
||||||
return {"fps": float(rate),
|
return {"fps": float(rate),
|
||||||
# The exact rate, for ffmpeg. 30000/1001 is not a float, and handing
|
# The exact rate, for ffmpeg. 30000/1001 is not a float, and handing
|
||||||
|
|
@ -338,8 +338,8 @@ def run(key):
|
||||||
proxy_facts = probe(proxy_path)
|
proxy_facts = probe(proxy_path)
|
||||||
_refuse_a_shifted_timeline(proxy_path)
|
_refuse_a_shifted_timeline(proxy_path)
|
||||||
frames = count_frames(proxy_path)
|
frames = count_frames(proxy_path)
|
||||||
if not 1 <= frames <= 900:
|
#if not 1 <= frames <= 900:
|
||||||
raise ValueError(f"the proxy holds {frames} frames; the limit is 1–900")
|
# raise ValueError(f"the proxy holds {frames} frames; the limit is 1–900")
|
||||||
# CHECKED AS A DURATION, not as a frame count. The page's clock is
|
# CHECKED AS A DURATION, not as a frame count. The page's clock is
|
||||||
# `frame = floor(audio.currentTime * fps)`, so what must not drift is
|
# `frame = floor(audio.currentTime * fps)`, so what must not drift is
|
||||||
# how long the picture lasts against how long the audio lasts — and
|
# how long the picture lasts against how long the audio lasts — and
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,11 @@ stage and timeline. A stage marquee replaces, or with Shift adds to, the same
|
||||||
set. Timeline rows, bars, and cel blocks render membership from that same set;
|
set. Timeline rows, bars, and cel blocks render membership from that same set;
|
||||||
the primary member gets the inspector/focus treatment.
|
the primary member gets the inspector/focus treatment.
|
||||||
|
|
||||||
Targeting remains separate. Clicking a timeline label both selects it and aims
|
Creation targeting is derived rather than stored. The primary (last-selected)
|
||||||
it. Shift-click changes the selection set and makes the clicked row the one
|
occurrence is the preferred row; the playhead validates it and, when necessary,
|
||||||
target. Clicking a bar/cel or the stage changes selection without silently
|
walks outward to the nearest valid containing occurrence. A target is singular
|
||||||
changing the target. A target is singular even when selection is plural.
|
even when selection is plural. See `docs/creating-in.md` for the resolver
|
||||||
|
contract.
|
||||||
|
|
||||||
## Clipboard value
|
## Clipboard value
|
||||||
|
|
||||||
|
|
@ -50,19 +51,13 @@ cross-project identity collision policy hidden inside it.
|
||||||
|
|
||||||
## Paste
|
## Paste
|
||||||
|
|
||||||
Paste always uses the explicit insertion target and the playhead:
|
Paste resolves one destination from the primary selection and playhead. An
|
||||||
|
ordinary occurrence is usable only while the playhead maps through every
|
||||||
- no target: paste at the top of the open symbol;
|
enclosing occurrence and lies within its extent. Otherwise resolution walks
|
||||||
- aimed ordinary node: paste beside it in the symbol that owns it;
|
outward, with the open symbol as the total fallback. A selected lane is an
|
||||||
- aimed instance, including an instance whose source is drawn as a lane: paste
|
insertion surface only while all occurrences enclosing its parent symbol are
|
||||||
inside the symbol it places;
|
valid. Multi-selection supplies one ordered payload, not several destinations;
|
||||||
- the open lane's own row: paste directly into that lane.
|
only its primary member anchors this resolution.
|
||||||
|
|
||||||
The destination is resolved structurally from the target. It does not require
|
|
||||||
the target instance to be visible at the playhead. Being outside the target
|
|
||||||
symbol's authored window is legal: content can exist there and be cropped by
|
|
||||||
the window. Lane paste may grow the lane/window because a sequence cannot store
|
|
||||||
an overlapping or unreachable tail and pretend the edit succeeded.
|
|
||||||
|
|
||||||
The earliest finite start among the copied roots is aligned with the playhead
|
The earliest finite start among the copied roots is aligned with the playhead
|
||||||
in the destination. All other root starts retain their offset from it. Roots
|
in the destination. All other root starts retain their offset from it. Roots
|
||||||
|
|
@ -118,13 +113,14 @@ After either duplicate command, the new roots replace the selection.
|
||||||
|
|
||||||
Cut, paste, duplicate, and duplicate unique each call one domain command and
|
Cut, paste, duplicate, and duplicate unique each call one domain command and
|
||||||
commit through one `edit/transaction`; each is exactly one undo/redo step no
|
commit through one `edit/transaction`; each is exactly one undo/redo step no
|
||||||
matter how many nodes or symbols it touches. Copy, aiming, and selection do not
|
matter how many nodes or symbols it touches. Copy and selection do not
|
||||||
touch history. A refusal changes no document leaves and creates no history step.
|
touch history. A refusal changes no document leaves and creates no history step.
|
||||||
|
|
||||||
Undo/redo filters the complete selection set against the restored document and
|
Undo/redo filters the complete selection set against the restored document and
|
||||||
repairs the primary selection. Clipboard payloads remain snapshots. Paste
|
repairs the primary selection. Clipboard payloads remain snapshots. Paste
|
||||||
validates the destination and all remapped references at commit time, so a stale
|
validates the resolved destination and all remapped references at commit time,
|
||||||
target or a newly impossible symbol cycle refuses rather than partially editing.
|
so a stale selection or a newly impossible symbol cycle refuses rather than
|
||||||
|
partially editing.
|
||||||
|
|
||||||
## Required tests
|
## Required tests
|
||||||
|
|
||||||
|
|
@ -133,7 +129,7 @@ normal shared references, deep unique graph remaps, multi-root relative timing,
|
||||||
composition overlap, lane overwrite, lane forward duplication and ripple,
|
composition overlap, lane overwrite, lane forward duplication and ripple,
|
||||||
mixed-owner duplication, cycle refusal, stale targets, and all-or-nothing
|
mixed-owner duplication, cycle refusal, stale targets, and all-or-nothing
|
||||||
failure. Event tests cover copy without history, atomic cut/paste/duplicates,
|
failure. Event tests cover copy without history, atomic cut/paste/duplicates,
|
||||||
resulting multi-selection, target resolution, pasting beyond a symbol window,
|
resulting multi-selection, target fallback at the playhead,
|
||||||
and one undo plus redo of each mutation. Browser tests cover mirrored stage and
|
and one undo plus redo of each mutation. Browser tests cover mirrored stage and
|
||||||
timeline selection, Shift-toggle on labels/bars/cels, singular aiming, and the
|
timeline selection, Shift-toggle on labels/bars/cels, singular derived creation
|
||||||
keyboard commands.
|
targeting, and the keyboard commands.
|
||||||
|
|
|
||||||
149
docs/creating-in.md
Normal file
149
docs/creating-in.md
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# Creating in
|
||||||
|
|
||||||
|
Revised 2026-10-02.
|
||||||
|
|
||||||
|
Arthur's creation model is a layer list plus a playhead. The primary active row
|
||||||
|
is the preferred place for a new thing. The playhead decides whether that row is
|
||||||
|
present in the current occurrence and supplies the frame at which the thing is
|
||||||
|
created.
|
||||||
|
|
||||||
|
This is editor behavior, not document structure. A saved clip has symbols,
|
||||||
|
instances and spans; it does not have timeline rows or a creation target.
|
||||||
|
|
||||||
|
## Selection and the active row
|
||||||
|
|
||||||
|
The selection set names the objects affected by copy, delete and transform. Its
|
||||||
|
primary member is also the active row, analogous to the active layer in a paint
|
||||||
|
program. There is no second targeting gesture for a user to maintain.
|
||||||
|
|
||||||
|
- Clicking a row label, its timeline body, or the same occurrence on the stage
|
||||||
|
makes that occurrence primary.
|
||||||
|
- Shift/marquee selection may retain several objects, but only the primary row
|
||||||
|
anchors creation.
|
||||||
|
- Clicking blank stage or timeline space returns the active row to the symbol in
|
||||||
|
the current tab.
|
||||||
|
- Row disclosure is independent. Selecting or creating never implicitly expands
|
||||||
|
a row.
|
||||||
|
|
||||||
|
The selection does not change merely because the playhead moves. An off-frame
|
||||||
|
object remains available for copying, deletion and inspection.
|
||||||
|
|
||||||
|
## Resolving the active row at the playhead
|
||||||
|
|
||||||
|
Resolution starts with the structural destination implied by the primary row:
|
||||||
|
|
||||||
|
- A lane row means the lane symbol itself.
|
||||||
|
- An ordinary symbol-instance row means the symbol placed by that occurrence.
|
||||||
|
- A non-instance row means the symbol containing that node.
|
||||||
|
- No row means the symbol in the current tab.
|
||||||
|
|
||||||
|
For an ordinary symbol occurrence, the playhead must be within that occurrence's
|
||||||
|
extent in the current nested context. Extents are tested after walking all parent
|
||||||
|
time maps and use the half-open interval `[in, out)`. If the preferred occurrence
|
||||||
|
is not present, resolution walks outward to the nearest parent whose occurrence
|
||||||
|
is present. A containing lane is therefore the natural fallback from an inactive
|
||||||
|
cel. If no nested occurrence is present, the current tab is the destination.
|
||||||
|
|
||||||
|
The preferred row is retained during fallback. Scrubbing back into its extent
|
||||||
|
makes it the effective destination again.
|
||||||
|
|
||||||
|
A lane differs only in what its row means. It is an insertion surface across its
|
||||||
|
containing timeline and does not require an existing cel under the playhead. A
|
||||||
|
cel within the lane is still an ordinary symbol occurrence with an extent.
|
||||||
|
|
||||||
|
For `A -> B -> lane C -> cel D`:
|
||||||
|
|
||||||
|
- over D, with D primary, creation happens inside D;
|
||||||
|
- past D but while C is available, creation inserts a new cel in C;
|
||||||
|
- outside C's containing occurrence but inside B, creation happens inside B;
|
||||||
|
- outside every nested occurrence, creation happens in A, the current tab.
|
||||||
|
|
||||||
|
## What creation does
|
||||||
|
|
||||||
|
Once resolved, every creation command follows the destination kind.
|
||||||
|
|
||||||
|
### Lane destination
|
||||||
|
|
||||||
|
- A new or dropped symbol is instantiated directly in the lane at the playhead.
|
||||||
|
- Its interval claims that time. Existing cels under the interval are removed or
|
||||||
|
trimmed by the lane's ordinary claim-time command.
|
||||||
|
- Beginning a drawing creates a new one-frame drawing symbol in the lane and the
|
||||||
|
finished shape is a child of that drawing.
|
||||||
|
- Selecting an existing cel changes the destination from the lane to the symbol
|
||||||
|
placed by that cel; subsequent symbols and shapes become children there.
|
||||||
|
|
||||||
|
Thus no separate "new cel" versus "add inside" mode is needed. Selecting the
|
||||||
|
lane header says new cel; selecting a cel says add inside.
|
||||||
|
|
||||||
|
### Ordinary symbol destination
|
||||||
|
|
||||||
|
- A new symbol is instantiated as a child at the mapped playhead frame.
|
||||||
|
- A new shape is authored directly in the symbol at that frame.
|
||||||
|
- Stage coordinates are transformed through the occurrence into the destination
|
||||||
|
symbol's local coordinates.
|
||||||
|
|
||||||
|
### Explicit timeline drop
|
||||||
|
|
||||||
|
A timeline drop uses the row and frame under the pointer, not the stored active
|
||||||
|
row and playhead. Dropping onto a lane therefore always instantiates in that lane
|
||||||
|
and claims the pointer's interval. A stage drop uses the resolved active row and
|
||||||
|
the playhead.
|
||||||
|
|
||||||
|
Paste, imported symbols and converted footage obey the same resolver as direct
|
||||||
|
creation. They must not each reconstruct nesting or extent fallback separately.
|
||||||
|
|
||||||
|
## Multi-selection and paste
|
||||||
|
|
||||||
|
Multi-selection does not create multiple insertion targets. Copy and cut take
|
||||||
|
the canonical forest of selected roots as one ordered payload; the primary
|
||||||
|
(last-selected) occurrence alone supplies the preferred row for a later paste.
|
||||||
|
At paste time that row and the current playhead resolve one effective
|
||||||
|
destination, and every root in the payload is inserted there in one transaction.
|
||||||
|
|
||||||
|
The earliest finite root start is aligned to the destination frame. Other roots
|
||||||
|
keep their timing offsets, hierarchy, and clipboard order. In an ordinary symbol
|
||||||
|
the roots may overlap. In a lane every root must have a finite span and the
|
||||||
|
payload's root spans must not overlap one another; valid spans claim their times
|
||||||
|
and trim or remove existing cels as a batch. An invalid member refuses the whole
|
||||||
|
paste rather than inserting a partial payload.
|
||||||
|
|
||||||
|
After paste, all new roots form the selection and the final root is primary, so
|
||||||
|
it becomes the preferred row for the next creation. Pasting one payload into
|
||||||
|
several selected destinations is intentionally not implicit: that would be a
|
||||||
|
separate distribute command. Duplicate is also distinct from paste—it stays
|
||||||
|
beside each source in its original owner and does not consult the playhead or
|
||||||
|
creation target.
|
||||||
|
|
||||||
|
## Palette lanes
|
||||||
|
|
||||||
|
Palette lanes use the same row-and-playhead resolution. Their content filter and
|
||||||
|
transition command remain palette-specific: only palettes and palette
|
||||||
|
transitions can be inserted there. This is a type restriction, not a second
|
||||||
|
targeting model.
|
||||||
|
|
||||||
|
## Implementation boundary
|
||||||
|
|
||||||
|
One pure resolver returns the effective destination:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
{:kind :lane | :symbol
|
||||||
|
:sid destination-symbol
|
||||||
|
:path effective-occurrence-path
|
||||||
|
:frame destination-local-frame
|
||||||
|
:matrix destination-to-current-tab-transform}
|
||||||
|
```
|
||||||
|
|
||||||
|
Callers may add the unchanged document as `:clip` or rename `:frame` to `:at`,
|
||||||
|
but they must not reinterpret the active row. Polygon creation, symbol creation,
|
||||||
|
stage drops, paste, import and footage conversion all consume this answer.
|
||||||
|
Explicit timeline drops use the same structural row rule with the row and frame
|
||||||
|
under the pointer; unlike playhead resolution, an invalid pointer destination is
|
||||||
|
refused rather than allowed to fall outward.
|
||||||
|
|
||||||
|
The invariants are:
|
||||||
|
|
||||||
|
1. The primary row is the preferred structural destination.
|
||||||
|
2. The playhead validates occurrences and supplies creation time.
|
||||||
|
3. Inactive targets fall outward; selection does not follow them.
|
||||||
|
4. A lane row inserts a cel, while a cel row enters its symbol.
|
||||||
|
5. Stage creation uses the playhead; timeline drops use pointer time.
|
||||||
|
|
@ -94,12 +94,9 @@ decision, not a cleanup.
|
||||||
existing root instance all produce the same child instance shape. The only
|
existing root instance all produce the same child instance shape. The only
|
||||||
difference is playback policy: a new empty drawing holds source frame zero;
|
difference is playback policy: a new empty drawing holds source frame zero;
|
||||||
a dropped library symbol plays at speed one.
|
a dropped library symbol plays at speed one.
|
||||||
- **Every symbol is born with a lane,** `clip/lane-node`, id `:lane`. A symbol
|
- **Lanes are explicit.** A symbol may contain ordinary overlapping children
|
||||||
with none had nowhere to drop a thing, which made the first drop into any
|
without a lane. Selecting a lane row opts creation into its claim-time
|
||||||
symbol a special case. An unaimed drop fills an EMPTY lane that is already
|
behavior; selecting a cel enters that cel's source symbol instead.
|
||||||
there and otherwise makes a new one; it never takes an occupied lane nobody
|
|
||||||
pointed at, because placement claims time and would trim or delete what was
|
|
||||||
in it.
|
|
||||||
- **Placement claims time.** Lanes never store overlaps. A new or extended clip
|
- **Placement claims time.** Lanes never store overlaps. A new or extended clip
|
||||||
trims, removes, or splits whatever previously owned the claimed interval.
|
trims, removes, or splits whatever previously owned the claimed interval.
|
||||||
Real compositing overlap uses another lane, where ordering remains explicit.
|
Real compositing overlap uses another lane, where ordering remains explicit.
|
||||||
|
|
@ -122,12 +119,12 @@ that is mostly refused today.
|
||||||
|
|
||||||
## Current timeline interaction
|
## Current timeline interaction
|
||||||
|
|
||||||
- Creating a symbol inside an aimed lane creates a one-frame held clip at the
|
- Creation follows the primary active row and the playhead; the complete rule is
|
||||||
playhead. With no aimed lane it first creates a lane. Drawing a polygon uses
|
in `docs/creating-in.md`. A lane row creates a new cel and claims its interval,
|
||||||
the existing clip there or creates the same one-frame clip when the frame is
|
while selecting a cel creates inside the symbol that cel places.
|
||||||
empty.
|
- Drawing with a lane row active creates a new one-frame drawing cel. Dropping a
|
||||||
- Dropping any library symbol into a lane creates a natural-duration playing
|
library symbol there creates a natural-duration playing cel. Explicit timeline
|
||||||
clip. Dropping it on unclaimed timeline or stage space first creates a lane.
|
drops use the row and frame under the pointer.
|
||||||
- Dragging a clip body moves it. A linked audio node follows a picture move;
|
- Dragging a clip body moves it. A linked audio node follows a picture move;
|
||||||
moving or trimming the audio itself remains independent.
|
moving or trimming the audio itself remains independent.
|
||||||
- Dragging a right edge changes its endpoint. Growth consumes adjacent spans
|
- Dragging a right edge changes its endpoint. Growth consumes adjacent spans
|
||||||
|
|
|
||||||
|
|
@ -65,8 +65,7 @@ code. `npm test` is green at each step either way.
|
||||||
fixtures and risks nothing.
|
fixtures and risks nothing.
|
||||||
7. **Lane creation stays explicit.** A blank document and `new symbol` create
|
7. **Lane creation stays explicit.** A blank document and `new symbol` create
|
||||||
ordinary symbols. The separate `new → lane` command creates and places a
|
ordinary symbols. The separate `new → lane` command creates and places a
|
||||||
symbol with `:display :lane`, aims it for immediate drawing or dropping, and
|
symbol with `:display :lane` in the effective creation target derived from
|
||||||
always adds it at the top of the open symbol rather than nesting it in the
|
selection and playhead; the new lane then becomes the primary selection.
|
||||||
previously aimed lane.
|
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
|
|
@ -384,9 +384,10 @@ content identity, and cel context when applicable. Show local time and
|
||||||
its project context where a useful mapping exists. Holds and loops need an honest
|
its project context where a useful mapping exists. Holds and loops need an honest
|
||||||
description instead of a fictitious unique global frame.
|
description instead of a fictitious unique global frame.
|
||||||
|
|
||||||
Creation controls next to the breadcrumb act in that explicit location. Selection
|
Creation follows the primary active row, resolved at the playhead as specified in
|
||||||
does not secretly change where a new symbol goes. A shared drawing indicates its
|
[`creating-in.md`](creating-in.md). The wider selection set still names what copy,
|
||||||
reuse and offers Make this cel unique. Names help identify content;
|
delete and transform affect; it is not a second list of creation destinations. A
|
||||||
|
shared drawing indicates its reuse and offers Make this cel unique. Names help identify content;
|
||||||
linked-use indicators must rely on IDs, because different drawings can share names.
|
linked-use indicators must rely on IDs, because different drawings can share names.
|
||||||
|
|
||||||
| Surface | Primary scope and controls |
|
| Surface | Primary scope and controls |
|
||||||
|
|
|
||||||
43
frontend/src/arthur/domain/creation.cljs
Normal file
43
frontend/src/arthur/domain/creation.cljs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
(ns arthur.domain.creation
|
||||||
|
"Resolve where a new thing goes from the primary selection and playhead.
|
||||||
|
|
||||||
|
This namespace owns no editor state. A row address is a preference; walking
|
||||||
|
the occurrence path at one frame answers which preferred or enclosing symbol
|
||||||
|
is actually available. See `docs/creating-in.md`."
|
||||||
|
(:require [arthur.domain.clip :as clip]
|
||||||
|
[arthur.domain.nest :as nest]
|
||||||
|
[arthur.domain.symbol :as symbol]))
|
||||||
|
|
||||||
|
(defn preferred-path
|
||||||
|
"The container path structurally implied by `selection`.
|
||||||
|
|
||||||
|
Selecting an instance means inside it. Selecting any other node means its
|
||||||
|
containing symbol. An empty or non-node selection means the open symbol."
|
||||||
|
[document selection]
|
||||||
|
(let [[kind sid id selected-path] selection
|
||||||
|
path (when (= :node kind)
|
||||||
|
(vec (or (seq selected-path) (when id [id]))))]
|
||||||
|
(cond
|
||||||
|
(empty? path) []
|
||||||
|
(= :instance (get-in document [:symbols sid :nodes id :kind])) path
|
||||||
|
:else (vec (butlast path)))))
|
||||||
|
|
||||||
|
(defn target
|
||||||
|
"The nearest creation context available at `frame` of `open`.
|
||||||
|
|
||||||
|
Every non-empty candidate is validated by `nest/inside`, so every enclosing
|
||||||
|
symbol occurrence must be under the playhead in the current context. Walking
|
||||||
|
outward stops at the first valid symbol. A lane needs no cel at that frame,
|
||||||
|
but the occurrence chain that reaches the lane must still be valid. The empty
|
||||||
|
path always resolves to the open symbol.
|
||||||
|
|
||||||
|
Returns `{:kind :lane|:symbol :sid :path :frame :matrix :time}`."
|
||||||
|
[document store open selection frame]
|
||||||
|
(let [preferred (preferred-path document selection)]
|
||||||
|
(some (fn [path]
|
||||||
|
(when-let [inside (nest/inside document store open path frame)]
|
||||||
|
(when-let [sid (:sid inside)]
|
||||||
|
(assoc inside :path path
|
||||||
|
:kind (if (symbol/lane? (clip/symbol document sid))
|
||||||
|
:lane :symbol)))))
|
||||||
|
(take (inc (count preferred)) (iterate pop preferred)))))
|
||||||
|
|
@ -509,7 +509,10 @@
|
||||||
st (merge (:store entry) (:store built))
|
st (merge (:store entry) (:store built))
|
||||||
imported-frames (clip/output-frames clip sid)
|
imported-frames (clip/output-frames clip sid)
|
||||||
source-fps (get-in built [:clip :fps])
|
source-fps (get-in built [:clip :fps])
|
||||||
where (ui/drop-destination db clip st frame target)
|
where (if point
|
||||||
|
(ui/creation-destination db clip st frame)
|
||||||
|
(ui/drop-destination db clip st frame target))
|
||||||
|
point (ui/destination-point where point)
|
||||||
result (if (:refused where)
|
result (if (:refused where)
|
||||||
where
|
where
|
||||||
(span/place-symbol (:clip where) st (:sid where)
|
(span/place-symbol (:clip where) st (:sid where)
|
||||||
|
|
@ -527,7 +530,7 @@
|
||||||
:source-inputs]))))]
|
:source-inputs]))))]
|
||||||
{:db (-> db
|
{:db (-> db
|
||||||
(update :ui dissoc :convert)
|
(update :ui dissoc :convert)
|
||||||
(assoc-in [:ui :selection]
|
(ui/selected
|
||||||
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
||||||
(update :footage merge
|
(update :footage merge
|
||||||
{:loading? false
|
{:loading? false
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@
|
||||||
:trace {:faces (trace/showing-for (:clip entry) sid #{})
|
:trace {:faces (trace/showing-for (:clip entry) sid #{})
|
||||||
:opacity (or (get-in db [:ui :trace :opacity])
|
:opacity (or (get-in db [:ui :trace :opacity])
|
||||||
trace/opacity-default)}})
|
trace/opacity-default)}})
|
||||||
|
;; Occurrence addresses belong to the document being left. Creation is
|
||||||
|
;; derived from the primary selection, so carrying one across documents
|
||||||
|
;; could otherwise make a coincidentally equal id a nested destination.
|
||||||
|
(update :ui dissoc :selection :selections :points)
|
||||||
(assoc-in [:playback :frame] 0)
|
(assoc-in [:playback :frame] 0)
|
||||||
(assoc-in [:playback :playing?] false))))
|
(assoc-in [:playback :playing?] false))))
|
||||||
|
|
||||||
|
|
@ -190,18 +194,13 @@
|
||||||
{:db (-> db
|
{:db (-> db
|
||||||
(update-in [:ui :tabs] #(if (some #{sid} %) % (conj (vec %) sid)))
|
(update-in [:ui :tabs] #(if (some #{sid} %) % (conj (vec %) sid)))
|
||||||
(assoc-in [:ui :open] sid)
|
(assoc-in [:ui :open] sid)
|
||||||
;; WHAT WAS SELECTED IS NOT IN HERE. A node selection and an
|
;; A node address is relative to the tab being left. Opening a
|
||||||
;; aimed target are paths in the symbol being left, and every
|
;; symbol therefore arrives at its root; a pool `[:symbol _]`
|
||||||
;; bar that reads one — the breadcrumb, the inspector, where a
|
;; selection is not an occurrence address and may survive.
|
||||||
;; new symbol would land — reads it against the open one. So
|
|
||||||
;; opening a symbol arrives with nothing selected, which is the
|
|
||||||
;; state the root crumb already means. A `[:symbol _]`
|
|
||||||
;; selection names a symbol rather than a place inside one and
|
|
||||||
;; survives.
|
|
||||||
(update :ui (fn [ui]
|
(update :ui (fn [ui]
|
||||||
(cond-> (dissoc ui :target)
|
(cond-> ui
|
||||||
(= :node (first (:selection ui)))
|
(= :node (first (:selection ui)))
|
||||||
(dissoc :selection))))
|
(dissoc :selection :selections))))
|
||||||
(update-in [:ui :trace :faces] #(trace/showing-for clip sid %))
|
(update-in [:ui :trace :faces] #(trace/showing-for clip sid %))
|
||||||
(assoc-in [:playback :frame] 0)
|
(assoc-in [:playback :frame] 0)
|
||||||
(assoc-in [:playback :playing?] false))
|
(assoc-in [:playback :playing?] false))
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,10 @@
|
||||||
st (merge (:store entry) (:store other))
|
st (merge (:store entry) (:store other))
|
||||||
;; A symbol from another project arrives as an ordinary clip, the same
|
;; A symbol from another project arrives as an ordinary clip, the same
|
||||||
;; as one from this project's pool.
|
;; as one from this project's pool.
|
||||||
where (ui/drop-destination db clip st frame target)
|
where (if point
|
||||||
|
(ui/creation-destination db clip st frame)
|
||||||
|
(ui/drop-destination db clip st frame target))
|
||||||
|
point (ui/destination-point where point)
|
||||||
result (if (:refused where)
|
result (if (:refused where)
|
||||||
where
|
where
|
||||||
(span/place-symbol (:clip where) st (:sid where)
|
(span/place-symbol (:clip where) st (:sid where)
|
||||||
|
|
@ -217,7 +220,7 @@
|
||||||
(if-let [why (or (:refused where) (:refused result))]
|
(if-let [why (or (:refused where) (:refused result))]
|
||||||
{:db (update db :project merge {:status why})}
|
{:db (update db :project merge {:status why})}
|
||||||
{:db (-> (edit/edit-entry db #(assoc % :clip (:clip result) :store st))
|
{:db (-> (edit/edit-entry db #(assoc % :clip (:clip result) :store st))
|
||||||
(assoc-in [:ui :selection]
|
(ui/selected
|
||||||
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
[:node (:sid where) uuid (conj (vec (:path where)) uuid)])
|
||||||
(update :project merge {:status (str "brought in " label)}))
|
(update :project merge {:status (str "brought in " label)}))
|
||||||
:dispatch [::pb/refresh-clock]}))))
|
:dispatch [::pb/refresh-clock]}))))
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,16 @@
|
||||||
(ns arthur.events.ui
|
(ns arthur.events.ui
|
||||||
"Selection, the TARGET, the active tone, the polygon being drawn, and which
|
"Selection, the active tone, the polygon being drawn, and which timeline rows
|
||||||
timeline rows are open.
|
are open.
|
||||||
|
|
||||||
TWO PIECES OF STATE AND NOT ONE. `[:ui :selection]` is what you are LOOKING
|
THE CREATION TARGET IS A FUNCTION, NOT EDITOR STATE. The primary selection is
|
||||||
at — what the inspector shows, what the stage puts handles on. `[:ui :target]`
|
the preferred row, like the active layer in a paint program. The playhead
|
||||||
is where a new thing would GO. They used to be one, so clicking a shape on the
|
resolves that row to the nearest occurrence creation can happen in: an active
|
||||||
stage silently re-aimed the next polygon at whatever held it, which
|
ordinary symbol, a lane insertion surface, an active ancestor, or the open
|
||||||
`lane-model.md` forbids in so many words: \"Selection does not secretly change
|
symbol. `docs/creating-in.md` is the contract.
|
||||||
where a new symbol goes.\"
|
|
||||||
|
|
||||||
THE TARGET MOVES ONLY WHEN SOMEBODY AIMS IT. A click on a timeline row label
|
`[:ui :selections]` remains the set copied, deleted or transformed together;
|
||||||
or a breadcrumb is aiming — each names a place in the
|
`[:ui :selection]` is its primary member and therefore the creation anchor.
|
||||||
document and nothing else. A click on the stage, or on a timeline bar, is not:
|
Moving the playhead never rewrites either one.
|
||||||
it says look at this. So `::select` never writes the target, and `::aim`
|
|
||||||
always writes both.
|
|
||||||
|
|
||||||
All of it is `assoc-in` under `:ui`. There is no effect in this namespace and
|
All of it is `assoc-in` under `:ui`. There is no effect in this namespace and
|
||||||
there should not be one: an editor's own state is the cheapest thing in the
|
there should not be one: an editor's own state is the cheapest thing in the
|
||||||
|
|
@ -22,11 +19,11 @@
|
||||||
[arthur.domain.clip :as clip]
|
[arthur.domain.clip :as clip]
|
||||||
[arthur.domain.clipboard :as clipboard]
|
[arthur.domain.clipboard :as clipboard]
|
||||||
[arthur.domain.correction :as correction]
|
[arthur.domain.correction :as correction]
|
||||||
|
[arthur.domain.creation :as creation]
|
||||||
[arthur.domain.gesture :as gesture]
|
[arthur.domain.gesture :as gesture]
|
||||||
[arthur.domain.nest :as nest]
|
[arthur.domain.nest :as nest]
|
||||||
[arthur.domain.node :as node]
|
[arthur.domain.node :as node]
|
||||||
[arthur.domain.span :as span]
|
[arthur.domain.span :as span]
|
||||||
[arthur.domain.symbol :as symbol]
|
|
||||||
[arthur.events.edit :as edit]
|
[arthur.events.edit :as edit]
|
||||||
[arthur.domain.paint :as paint]
|
[arthur.domain.paint :as paint]
|
||||||
[arthur.events.playback :as playback]
|
[arthur.events.playback :as playback]
|
||||||
|
|
@ -61,37 +58,17 @@
|
||||||
(update-in % [:symbols sid :nodes id] dissoc :name)))))))
|
(update-in % [:symbols sid :nodes id] dissoc :name)))))))
|
||||||
|
|
||||||
(defn selected
|
(defn selected
|
||||||
"`db` with `selection` selected, and nothing aimed.
|
"`db` with `selection` as its primary active row.
|
||||||
|
|
||||||
Selection and row disclosure are independent. A click or trim can select a
|
Selection and row disclosure are independent. A click or trim can select a
|
||||||
nested item, but only the row's twist button opens or closes it."
|
nested item, but only the row's twist button opens or closes it."
|
||||||
[db selection]
|
[db selection]
|
||||||
;; A captured trim gesture also selects on pointer-up. It must not acquire the
|
;; A captured trim gesture also selects on pointer-up. It must not acquire the
|
||||||
;; unrelated side effect of opening every row above the edited clip.
|
;; unrelated side effect of opening every row above the edited clip.
|
||||||
(cond-> (-> db
|
(-> db
|
||||||
(assoc-in [:ui :selection] selection)
|
(assoc-in [:ui :selection] selection)
|
||||||
(assoc-in [:ui :selections] (if selection [selection] []))
|
(assoc-in [:ui :selections] (if selection [selection] []))
|
||||||
(update :ui dissoc :points :retry))
|
(update :ui dissoc :points :retry)))
|
||||||
(nil? selection) (assoc-in [:ui :target] nil)))
|
|
||||||
|
|
||||||
(defn target-of
|
|
||||||
"The target `selection` names: `{:sid :id :path}`, or nil for the open symbol
|
|
||||||
itself.
|
|
||||||
|
|
||||||
All three parts, because a path alone cannot be looked up — one symbol placed
|
|
||||||
twice is two rows with the same node ids in them, so the row path says WHICH
|
|
||||||
and the sid says in which symbol's node map to find it. A selection made on
|
|
||||||
the stage carries no path; it names a node in the open symbol directly, and
|
|
||||||
the node's own id standing alone is that one-step path."
|
|
||||||
[selection]
|
|
||||||
(let [[kind sid id path] selection]
|
|
||||||
(when (and (= :node kind) id)
|
|
||||||
{:sid sid :id id :path (vec (or (seq path) [id]))})))
|
|
||||||
|
|
||||||
(defn aimed
|
|
||||||
"`db` with `selection` selected AND aimed: the target follows it."
|
|
||||||
[db selection]
|
|
||||||
(assoc-in (selected db selection) [:ui :target] (target-of selection)))
|
|
||||||
|
|
||||||
(rf/reg-event-db ::select (fn [db [_ selection]] (selected db selection)))
|
(rf/reg-event-db ::select (fn [db [_ selection]] (selected db selection)))
|
||||||
|
|
||||||
|
|
@ -118,34 +95,6 @@
|
||||||
(-> (selected db primary) (assoc-in [:ui :selections] xs))
|
(-> (selected db primary) (assoc-in [:ui :selections] xs))
|
||||||
(selected db nil)))))
|
(selected db nil)))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
|
||||||
::toggle-and-aim
|
|
||||||
;; A modified timeline-label click has two independent meanings: membership in
|
|
||||||
;; the shared selection set, and the one place subsequent creation/paste uses.
|
|
||||||
(fn [db [_ selection]]
|
|
||||||
(let [primary (get-in db [:ui :selection])
|
|
||||||
many (vec (get-in db [:ui :selections]))
|
|
||||||
old (if (some #{primary} many) many (if primary [primary] []))
|
|
||||||
xs (if (some #{selection} old)
|
|
||||||
(vec (remove #{selection} old))
|
|
||||||
(conj old selection))
|
|
||||||
db (if-let [primary (peek xs)]
|
|
||||||
(-> (selected db primary) (assoc-in [:ui :selections] xs))
|
|
||||||
(selected db nil))]
|
|
||||||
(assoc-in db [:ui :target] (target-of selection)))))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
|
||||||
::aim
|
|
||||||
;; The gestures that name a PLACE in the document rather than a thing on
|
|
||||||
;; screen: a timeline row label or a breadcrumb.
|
|
||||||
(fn [db [_ selection]] (aimed db selection)))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
|
||||||
::aim-at
|
|
||||||
;; Aiming without selecting: the lane becomes where drawings go while the
|
|
||||||
;; inspector selection remains independent.
|
|
||||||
(fn [db [_ selection]] (assoc-in db [:ui :target] (target-of selection))))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::set-tone
|
::set-tone
|
||||||
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
|
(fn [db [_ tone]] (assoc-in db [:ui :tone] tone)))
|
||||||
|
|
@ -450,42 +399,6 @@
|
||||||
(update-in db [:ui :smart] #(let [on? (boolean on)]
|
(update-in db [:ui :smart] #(let [on? (boolean on)]
|
||||||
((if on? conj disj) (set %) face)))))
|
((if on? conj disj) (set %) face)))))
|
||||||
|
|
||||||
(defn where-new-goes
|
|
||||||
"The row path, from the open symbol down, of the symbol a new thing goes into:
|
|
||||||
INSIDE the aimed instance, or BESIDE an aimed node of any other kind, or at
|
|
||||||
the top of the open symbol when nothing is aimed.
|
|
||||||
|
|
||||||
READ OFF THE TARGET, NEVER THE SELECTION. The two were one thing, and the
|
|
||||||
result was that clicking a shape on the stage to look at it re-aimed the next
|
|
||||||
polygon at whatever happened to hold that shape. `[:ui :target]` is a row
|
|
||||||
path, because one symbol placed twice is two rows and only the path says which
|
|
||||||
one was aimed at."
|
|
||||||
[clip db]
|
|
||||||
(let [{:keys [sid id path]} (get-in db [:ui :target])]
|
|
||||||
(cond
|
|
||||||
(empty? path) []
|
|
||||||
(= :instance (get-in clip [:symbols sid :nodes id :kind])) path
|
|
||||||
:else (vec (butlast path)))))
|
|
||||||
|
|
||||||
(defn aimed-symbol
|
|
||||||
"The symbol a command acts in and the path of rows down to it: `{:sid :path}`.
|
|
||||||
|
|
||||||
INSIDE the aimed instance, beside an aimed node of any other kind, the open
|
|
||||||
symbol when nothing is aimed — which is `where-new-goes`, resolved. There is
|
|
||||||
no lane to aim at any more: a lane is how a symbol is DRAWN, so what a gesture
|
|
||||||
names is a symbol, and whether that symbol is drawn as a lane is a separate
|
|
||||||
question `symbol/lane?` answers."
|
|
||||||
[clip st db frame]
|
|
||||||
(let [open (get-in db [:ui :open])
|
|
||||||
down (where-new-goes clip db)
|
|
||||||
inside (nest/inside clip st open down frame)]
|
|
||||||
;; A stale or presently invisible target is no target. Creation must remain
|
|
||||||
;; a total operation, so it falls back to the document's main symbol.
|
|
||||||
(if (:sid inside)
|
|
||||||
(assoc inside :path down)
|
|
||||||
{:sid (if (get-in clip [:symbols :main]) :main open)
|
|
||||||
:path [] :frame frame})))
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; clipboard and local duplication
|
;; clipboard and local duplication
|
||||||
|
|
||||||
|
|
@ -537,47 +450,11 @@
|
||||||
(edit/transaction (constantly (:clip r)))
|
(edit/transaction (constantly (:clip r)))
|
||||||
(select-results []))))))))
|
(select-results []))))))))
|
||||||
|
|
||||||
(defn- apply-time [{:keys [at rate]} f]
|
|
||||||
(* rate (- f at)))
|
|
||||||
|
|
||||||
(defn paste-destination
|
(defn paste-destination
|
||||||
"The aimed symbol, its row-path prefix, and the playhead in its frames.
|
"The effective creation context at the playhead, in the destination's frames."
|
||||||
|
|
||||||
Resolve the destination structurally. For an aimed instance, try the ordinary
|
|
||||||
visible walk first, then continue its authored clock beyond its span/window;
|
|
||||||
content outside the window is legal and is simply cropped by that window."
|
|
||||||
[db clip st]
|
[db clip st]
|
||||||
(let [open (get-in db [:ui :open])
|
(creation/target clip st (get-in db [:ui :open])
|
||||||
frame (editing-frame db clip)
|
(get-in db [:ui :selection]) (editing-frame db clip)))
|
||||||
{:keys [sid id path]} (get-in db [:ui :target])
|
|
||||||
n (when id (get-in clip [:symbols sid :nodes id]))
|
|
||||||
selection [:node sid id path]
|
|
||||||
owner-frame (when id (selection-frame clip st open selection frame))]
|
|
||||||
(cond
|
|
||||||
(nil? id)
|
|
||||||
{:sid open :path [] :frame frame}
|
|
||||||
|
|
||||||
(nil? n)
|
|
||||||
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}
|
|
||||||
|
|
||||||
(= :instance (:kind n))
|
|
||||||
(let [visible (nest/inside clip st open path frame)
|
|
||||||
local (when (number? owner-frame) (node/local-frame n owner-frame))
|
|
||||||
shown (when (number? local) (clip/placed-frame clip sid n local))
|
|
||||||
mapped (when-let [t (and (number? owner-frame)
|
|
||||||
(clip/source-time clip sid n))]
|
|
||||||
(apply-time (node/then-time (node/time-of n) t) owner-frame))
|
|
||||||
held (when (zero? (:speed (node/playback-of n)))
|
|
||||||
(:in (node/playback-of n)))
|
|
||||||
at (or (:frame visible) (:frame shown) mapped held)]
|
|
||||||
(if (number? at)
|
|
||||||
{:sid (node/source n) :path (vec path) :frame (js/Math.floor at)}
|
|
||||||
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}))
|
|
||||||
|
|
||||||
:else
|
|
||||||
(if (number? owner-frame)
|
|
||||||
{:sid sid :path (vec (butlast path)) :frame (js/Math.floor owner-frame)}
|
|
||||||
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}))))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::paste
|
::paste
|
||||||
|
|
@ -633,59 +510,46 @@
|
||||||
(update-in db [:ui :draft] into [x y])
|
(update-in db [:ui :draft] into [x y])
|
||||||
db)))
|
db)))
|
||||||
|
|
||||||
(defn into-the-sequence
|
(defn new-lane-drawing
|
||||||
"Where a polygon drawn into a symbol DRAWN AS A LANE goes: the path of the held
|
"Create the one-frame drawing cel implied by drawing with a lane row active.
|
||||||
clip that symbol exposes at the playhead, and the document containing it.
|
|
||||||
`{:clip :path}`, or `{:refused why}`.
|
|
||||||
|
|
||||||
A GAP IS NOT A REFUSAL, IT IS A NEW DRAWING. The frame under the playhead is
|
A lane is an insertion surface, so this always makes a new cel at the
|
||||||
where the drawing belongs, so drawing on an empty frame makes the one-frame
|
playhead. Existing content at that frame is claimed and trimmed by
|
||||||
held clip that was missing. The gesture already says what the removed drawing
|
`overwrite-drawing`. Selecting an existing cel resolves to that cel's ordinary
|
||||||
controls used to say.
|
source symbol before this function is reached, and therefore draws inside it.
|
||||||
|
|
||||||
`overwrite-drawing` rather than `append-drawing`, because a gap already has
|
`overwrite-drawing` rather than `append-drawing`, because a gap already has
|
||||||
the room. Appending RIPPLES everything after it later by the new clip's
|
the room. Appending RIPPLES everything after it later by the new clip's
|
||||||
duration — that is what `insert` means, and it is a different intention.
|
duration — that is what `insert` means, and it is a different intention.
|
||||||
Placed into a gap, overwrite clears nothing and moves nobody.
|
|
||||||
|
|
||||||
THE CLIP'S PATH IS THE SYMBOL'S PATH PLUS ONE STEP, because a clip of a lane
|
THE CLIP'S PATH IS THE SYMBOL'S PATH PLUS ONE STEP, because a clip of a lane
|
||||||
is an ordinary row of the symbol holding it — which is the addressing `rows`
|
is an ordinary row of the symbol holding it — which is the addressing `rows`
|
||||||
hands the timeline and `trail` reads back."
|
hands the timeline and `trail` reads back."
|
||||||
[clip db {:keys [sid frame path]}]
|
[clip {:keys [sid frame path]}]
|
||||||
(let [exposed (when (integer? frame)
|
(if-not (integer? frame)
|
||||||
(some (fn [c] (let [[lo hi] (node/placed-span c)]
|
|
||||||
(when (and (<= lo frame) (< frame hi)) c)))
|
|
||||||
(symbol/children (get-in clip [:symbols sid :nodes]))))]
|
|
||||||
(cond
|
|
||||||
(not (integer? frame))
|
|
||||||
{:refused "the playhead is not on one frame of this symbol"}
|
{:refused "the playhead is not on one frame of this symbol"}
|
||||||
exposed {:clip clip :sid sid :path (conj (vec path) (:id exposed))}
|
|
||||||
:else
|
|
||||||
(let [cel-id (random-uuid)
|
(let [cel-id (random-uuid)
|
||||||
made (span/overwrite-drawing clip sid cel-id (clip/fresh-id clip) frame
|
made (span/overwrite-drawing clip sid cel-id (clip/fresh-id clip) frame
|
||||||
{:extent :grow-symbol
|
{:extent :grow-symbol
|
||||||
:remainder-id (random-uuid)})]
|
:remainder-id (random-uuid)})]
|
||||||
(if (:refused made)
|
(if (:refused made)
|
||||||
made
|
made
|
||||||
{:clip (:clip made) :sid sid :path (conj (vec path) cel-id)})))))
|
{:clip (:clip made) :sid sid :path (conj (vec path) cel-id)}))))
|
||||||
|
|
||||||
(defn polygon-landing
|
(defn polygon-landing
|
||||||
"Choose the document and row path a finished polygon is drawn into.
|
"Choose the document and row path a finished polygon is drawn into.
|
||||||
|
|
||||||
WHERE IT GOES IS A SYMBOL, and what happens there follows how that symbol is
|
A resolved lane always gets a new drawing cel. A selected cel resolves to its
|
||||||
DRAWN: in one drawn as a lane an occupied frame lands in its clip and a gap
|
source symbol instead, so drawing there adds a child to that existing cel."
|
||||||
first becomes a one-frame drawing, because the sequence is what says which
|
|
||||||
drawing the playhead is on. In an ordinary composition the polygon goes
|
|
||||||
straight into the symbol, as it always did."
|
|
||||||
[clip st db]
|
[clip st db]
|
||||||
(let [into (aimed-symbol clip st db (editing-frame db clip))]
|
(let [into (creation/target clip st (get-in db [:ui :open])
|
||||||
(if (and (:sid into) (symbol/lane? (clip/symbol clip (:sid into))))
|
(get-in db [:ui :selection]) (editing-frame db clip))]
|
||||||
(assoc (into-the-sequence clip db into) :lane? true)
|
(if (= :lane (:kind into))
|
||||||
|
(assoc (new-lane-drawing clip into) :lane? true)
|
||||||
{:clip clip :sid (:sid into) :path (:path into) :lane? false})))
|
{:clip clip :sid (:sid into) :path (:path into) :lane? false})))
|
||||||
|
|
||||||
(defn beginning-polygon
|
(defn beginning-polygon
|
||||||
"Enter polygon mode, first materializing a drawing at the playhead when the
|
"Enter polygon mode, first materializing a new drawing at the playhead when
|
||||||
symbol it lands in is drawn as a lane and that frame is empty.
|
the effective target is a lane.
|
||||||
|
|
||||||
Creating here, rather than when the polygon is finished, means the drawing is
|
Creating here, rather than when the polygon is finished, means the drawing is
|
||||||
already the current cel while points are being placed. Cancelling the polygon
|
already the current cel while points are being placed. Cancelling the polygon
|
||||||
|
|
@ -708,8 +572,7 @@
|
||||||
;; left a selection that looked up to nothing, so the
|
;; left a selection that looked up to nothing, so the
|
||||||
;; inspector, the breadcrumb and every span command went
|
;; inspector, the breadcrumb and every span command went
|
||||||
;; blank on a drawing that had just been created.
|
;; blank on a drawing that had just been created.
|
||||||
(assoc-in [:ui :selection]
|
(selected [:node (:sid landing)
|
||||||
[:node (:sid landing)
|
|
||||||
(peek (:path landing)) (:path landing)]))
|
(peek (:path landing)) (:path landing)]))
|
||||||
|
|
||||||
:else db)]
|
:else db)]
|
||||||
|
|
@ -761,8 +624,8 @@
|
||||||
(-> db
|
(-> db
|
||||||
(edit/transaction
|
(edit/transaction
|
||||||
(fn [_] (paint/new-shape source sid id frame pts (get-in db [:ui :tone]))))
|
(fn [_] (paint/new-shape source sid id frame pts (get-in db [:ui :tone]))))
|
||||||
(update :ui merge {:tool nil :draft []
|
(update :ui merge {:tool nil :draft []})
|
||||||
:selection [:node sid id (conj down id)]})))))))
|
(selected [:node sid id (conj down id)])))))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::set-knob
|
::set-knob
|
||||||
|
|
@ -783,21 +646,22 @@
|
||||||
"Create and place one empty symbol. Lane creation is explicit: `lane?` marks
|
"Create and place one empty symbol. Lane creation is explicit: `lane?` marks
|
||||||
the new symbol for linear timeline display; ordinary symbol creation never
|
the new symbol for linear timeline display; ordinary symbol creation never
|
||||||
does. Both operations place the new symbol through the same span command, so
|
does. Both operations place the new symbol through the same span command, so
|
||||||
an explicitly aimed lane still applies its claim-time rule."
|
a selected lane still applies its claim-time rule."
|
||||||
[db where lane?]
|
[db where lane?]
|
||||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||||
open (get-in db [:ui :open])
|
open (get-in db [:ui :open])
|
||||||
frame (editing-frame db document)
|
playhead (editing-frame db document)
|
||||||
|
into (if (= :top where)
|
||||||
|
(creation/target document st open nil playhead)
|
||||||
|
(creation/target document st open (get-in db [:ui :selection]) playhead))
|
||||||
;; A lane is the open symbol's timeline, not an insert that happens to
|
;; A lane is the open symbol's timeline, not an insert that happens to
|
||||||
;; begin where the playhead was when it was made. Giving that wrapper
|
;; begin where the playhead was when it was made. Giving that wrapper
|
||||||
;; the whole open-symbol window makes the row's promise true: it is a
|
;; its whole parent window makes the row's promise true: it is a
|
||||||
;; destination at every frame. Ordinary symbols remain clips created at
|
;; destination at every frame. Resolve the parent at the ACTUAL playhead
|
||||||
;; the playhead.
|
;; first; using frame zero for resolution would incorrectly fall out of
|
||||||
frame (if lane? 0 frame)
|
;; every nested symbol that begins later in the open context.
|
||||||
into (if (= :top where)
|
{host :sid resolved-at :frame down :path destination-kind :kind} into
|
||||||
(assoc (nest/inside document st open [] frame) :path [])
|
at (if lane? 0 resolved-at)
|
||||||
(aimed-symbol document st db frame))
|
|
||||||
{host :sid at :frame down :path} into
|
|
||||||
sid (clip/fresh-id document)
|
sid (clip/fresh-id document)
|
||||||
instance-id (random-uuid)
|
instance-id (random-uuid)
|
||||||
end (clip/frames document host)
|
end (clip/frames document host)
|
||||||
|
|
@ -811,7 +675,13 @@
|
||||||
:else
|
:else
|
||||||
(let [new-symbol (cond-> {:id sid :name (name sid)
|
(let [new-symbol (cond-> {:id sid :name (name sid)
|
||||||
:fps (clip/fps document host)
|
:fps (clip/fps document host)
|
||||||
:frames (- end at) :nodes {}}
|
;; A fresh cel is one frame for
|
||||||
|
;; frame-by-frame work. In an
|
||||||
|
;; ordinary composition the new
|
||||||
|
;; symbol keeps the available window.
|
||||||
|
:frames (if (= :lane destination-kind)
|
||||||
|
1 (- end at))
|
||||||
|
:nodes {}}
|
||||||
lane? (assoc :display :lane))
|
lane? (assoc :display :lane))
|
||||||
seeded (assoc-in document [:symbols sid] new-symbol)]
|
seeded (assoc-in document [:symbols sid] new-symbol)]
|
||||||
(span/place-symbol seeded st host instance-id sid at
|
(span/place-symbol seeded st host instance-id sid at
|
||||||
|
|
@ -820,12 +690,11 @@
|
||||||
(if-let [why (:refused result)]
|
(if-let [why (:refused result)]
|
||||||
(update db :project merge {:status why})
|
(update db :project merge {:status why})
|
||||||
(let [selection [:node host instance-id (conj (vec down) instance-id)]]
|
(let [selection [:node host instance-id (conj (vec down) instance-id)]]
|
||||||
(cond-> (-> db
|
(-> db
|
||||||
(edit/transaction (constantly (:clip result)))
|
(edit/transaction (constantly (:clip result)))
|
||||||
(assoc-in [:ui :selection] selection)
|
(selected selection)
|
||||||
(update-in [:ui :expanded] (fnil into #{})
|
(update-in [:ui :expanded] (fnil into #{})
|
||||||
(rest (reductions conj [] down))))
|
(rest (reductions conj [] down))))))))
|
||||||
lane? (assoc-in [:ui :target] (target-of selection)))))))
|
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::new-symbol
|
::new-symbol
|
||||||
|
|
@ -835,11 +704,9 @@
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::new-lane
|
::new-lane
|
||||||
(fn [db _]
|
(fn [db _]
|
||||||
;; A lane is an explicit, persistent top-level track of the open symbol. It
|
;; A lane is an explicit persistent row across the effective target symbol.
|
||||||
;; must not become nested merely because the previously created lane is
|
;; The playhead validates that nested target; the row itself begins at zero.
|
||||||
;; still aimed, and it spans the open symbol rather than starting at the
|
(create-container db :inside true)))
|
||||||
;; current playhead.
|
|
||||||
(create-container db :top true)))
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; a drop in flight
|
;; a drop in flight
|
||||||
|
|
@ -875,31 +742,47 @@
|
||||||
pointer answered with — and `:at` is that frame carried down into the
|
pointer answered with — and `:at` is that frame carried down into the
|
||||||
destination's. Nothing on this path crosses the output grid.
|
destination's. Nothing on this path crosses the output grid.
|
||||||
|
|
||||||
`:clip` is handed back unchanged and is in the result only so the callers that
|
`:clip` is handed back unchanged so all placement callers consume the same
|
||||||
used to be given a document with a freshly made lane in it go on reading one
|
result shape."
|
||||||
thing."
|
|
||||||
[document st open frame target]
|
[document st open frame target]
|
||||||
(let [target (if (vector? target)
|
(let [[_ sid id selected-path] target
|
||||||
(let [[_ sid id path] target]
|
path0 (vec (or (seq selected-path) (when id [id])))
|
||||||
{:sid sid :id id :path path})
|
|
||||||
target)
|
|
||||||
path (cond
|
path (cond
|
||||||
(nil? target) []
|
(nil? target) []
|
||||||
(= :instance (get-in document [:symbols (:sid target)
|
(= :instance (get-in document [:symbols sid :nodes id :kind])) path0
|
||||||
:nodes (:id target) :kind]))
|
:else (vec (butlast path0)))
|
||||||
(vec (:path target))
|
{destination :sid at :frame matrix :matrix}
|
||||||
:else (vec (butlast (:path target))))
|
(nest/inside document st open path frame)]
|
||||||
{:keys [sid] at :frame} (nest/inside document st open path frame)]
|
|
||||||
(cond
|
(cond
|
||||||
(nil? sid) {:refused "what you are dropping into is not on screen at this frame"}
|
(nil? destination) {:refused "what you are dropping into is not on screen at this frame"}
|
||||||
(not (integer? at)) {:refused "the drop is not on one frame of that symbol"}
|
(not (integer? at)) {:refused "the drop is not on one frame of that symbol"}
|
||||||
:else {:clip document :sid sid :at at :path path})))
|
:else {:clip document :sid destination :at at :path path :matrix matrix})))
|
||||||
|
|
||||||
(defn drop-destination
|
(defn drop-destination
|
||||||
"`drop-destination-at` from the symbol currently open in `db`."
|
"`drop-destination-at` from the symbol currently open in `db`."
|
||||||
[db document st frame target]
|
[db document st frame target]
|
||||||
(drop-destination-at document st (get-in db [:ui :open]) frame target))
|
(drop-destination-at document st (get-in db [:ui :open]) frame target))
|
||||||
|
|
||||||
|
(defn creation-destination
|
||||||
|
"A stage creation destination from the primary selection and playhead."
|
||||||
|
[db document st frame]
|
||||||
|
(let [{:keys [sid path matrix] at :frame}
|
||||||
|
(creation/target document st (get-in db [:ui :open])
|
||||||
|
(get-in db [:ui :selection]) frame)]
|
||||||
|
(cond
|
||||||
|
(nil? sid) {:refused "nothing under the playhead can contain this"}
|
||||||
|
(not (integer? at)) {:refused "the playhead is not on one frame of that symbol"}
|
||||||
|
:else {:clip document :sid sid :at at :path path :matrix matrix})))
|
||||||
|
|
||||||
|
(defn destination-point
|
||||||
|
"A point in the current tab transformed into `where`'s local coordinates."
|
||||||
|
[{:keys [matrix]} point]
|
||||||
|
(if-let [inv (and point matrix (node/invert matrix))]
|
||||||
|
(let [out (js/Float64Array. 2)]
|
||||||
|
(node/apply-pt! out 0 inv (first point) (second point))
|
||||||
|
[(aget out 0) (aget out 1)])
|
||||||
|
point))
|
||||||
|
|
||||||
(defn landed
|
(defn landed
|
||||||
"`db` after a drop that produced `result`, with `uuid` selected."
|
"`db` after a drop that produced `result`, with `uuid` selected."
|
||||||
[db {:keys [sid path]} uuid result]
|
[db {:keys [sid path]} uuid result]
|
||||||
|
|
@ -908,7 +791,7 @@
|
||||||
(-> db
|
(-> db
|
||||||
(update :ui dissoc :drop)
|
(update :ui dissoc :drop)
|
||||||
(edit/transaction (constantly (:clip result)))
|
(edit/transaction (constantly (:clip result)))
|
||||||
(assoc-in [:ui :selection] [:node sid uuid (conj (vec path) uuid)]))))
|
(selected [:node sid uuid (conj (vec path) uuid)]))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
::drop-symbol
|
::drop-symbol
|
||||||
|
|
@ -918,7 +801,10 @@
|
||||||
;; a lane trims its neighbour and a drop into a composition does not.
|
;; a lane trims its neighbour and a drop into a composition does not.
|
||||||
(fn [db [_ source-id frame point target]]
|
(fn [db [_ source-id frame point target]]
|
||||||
(let [{document :clip st :store} (store/entry (:clip/current db))
|
(let [{document :clip st :store} (store/entry (:clip/current db))
|
||||||
where (drop-destination db document st frame target)
|
where (if point
|
||||||
|
(creation-destination db document st frame)
|
||||||
|
(drop-destination db document st frame target))
|
||||||
|
point (destination-point where point)
|
||||||
uuid (random-uuid)]
|
uuid (random-uuid)]
|
||||||
(if (:refused where)
|
(if (:refused where)
|
||||||
(-> db (update :ui dissoc :drop) (update :project merge {:status (:refused where)}))
|
(-> db (update :ui dissoc :drop) (update :project merge {:status (:refused where)}))
|
||||||
|
|
@ -943,7 +829,7 @@
|
||||||
;; `place-sound` positions in. It used to be multiplied by the grid rate
|
;; `place-sound` positions in. It used to be multiplied by the grid rate
|
||||||
;; again on the way in — a second conversion of an already-converted
|
;; again on the way in — a second conversion of an already-converted
|
||||||
;; number, which dropped a sound 2.5 frames late for every frame it was
|
;; number, which dropped a sound 2.5 frames late for every frame it was
|
||||||
;; aimed at in a 12fps project.
|
;; selected in a 12fps project.
|
||||||
(let [sid (:sid where)
|
(let [sid (:sid where)
|
||||||
seeded (clip/place-sound (:clip where) sid source label length rate
|
seeded (clip/place-sound (:clip where) sid source label length rate
|
||||||
(:at where) uuid)]
|
(:at where) uuid)]
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@
|
||||||
frames (js/Number (:frames m))
|
frames (js/Number (:frames m))
|
||||||
urls (:urls m)]
|
urls (:urls m)]
|
||||||
(when-not (and (js/Number.isFinite fps) (pos? fps)
|
(when-not (and (js/Number.isFinite fps) (pos? fps)
|
||||||
(js/Number.isInteger frames) (<= 1 frames 900)
|
|
||||||
(string? (:audio m)) (seq (:audio m))
|
(string? (:audio m)) (seq (:audio m))
|
||||||
(sequential? urls) (every? string? urls))
|
(sequential? urls) (every? string? urls))
|
||||||
(throw (ex-info "a footage manifest needs fps, frames (1–900), audio and a url per frame"
|
(throw (ex-info "a footage manifest needs fps, frames (1–900), audio and a url per frame"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
Cheap by construction, like `subs/playback`: each reads a path and returns a
|
Cheap by construction, like `subs/playback`: each reads a path and returns a
|
||||||
value, so clicking a swatch notifies the swatches and nothing else."
|
value, so clicking a swatch notifies the swatches and nothing else."
|
||||||
(:require [arthur.domain.nest :as nest]
|
(:require [arthur.domain.creation :as creation]
|
||||||
|
[arthur.domain.nest :as nest]
|
||||||
[arthur.domain.pick :as pick]
|
[arthur.domain.pick :as pick]
|
||||||
[arthur.footage.store :as store]
|
[arthur.footage.store :as store]
|
||||||
[arthur.subs.playback :as playback]
|
[arthur.subs.playback :as playback]
|
||||||
|
|
@ -23,7 +24,6 @@
|
||||||
(cond (nil? primary) []
|
(cond (nil? primary) []
|
||||||
(some #{primary} many) many
|
(some #{primary} many) many
|
||||||
:else [primary]))))
|
:else [primary]))))
|
||||||
(rf/reg-sub ::target (fn [db _] (get-in db [:ui :target])))
|
|
||||||
(rf/reg-sub ::clipboard (fn [db _] (get-in db [:ui :clipboard])))
|
(rf/reg-sub ::clipboard (fn [db _] (get-in db [:ui :clipboard])))
|
||||||
(rf/reg-sub ::retry (fn [db _] (get-in db [:ui :retry])))
|
(rf/reg-sub ::retry (fn [db _] (get-in db [:ui :retry])))
|
||||||
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||||
|
|
@ -67,33 +67,33 @@
|
||||||
(nest/inside clip st open (or path [id]) f)))))
|
(nest/inside clip st open (or path [id]) f)))))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::target-node
|
::creation-target
|
||||||
:<- [::target]
|
:<- [::selection]
|
||||||
:<- [::render/clip]
|
|
||||||
(fn [[{:keys [sid id]} clip] _]
|
|
||||||
;; The node a new thing would be parented to, for the outline that says so.
|
|
||||||
;; `[sid id node]` as `::selected-node` gives it, so the two read alike.
|
|
||||||
(when (and clip id)
|
|
||||||
(when-let [n (get-in clip [:symbols sid :nodes id])]
|
|
||||||
[sid id n]))))
|
|
||||||
|
|
||||||
(rf/reg-sub
|
|
||||||
::target-placement
|
|
||||||
:<- [::target]
|
|
||||||
:<- [::target-node]
|
|
||||||
:<- [::render/clip]
|
:<- [::render/clip]
|
||||||
:<- [::render/clip-id]
|
:<- [::render/clip-id]
|
||||||
:<- [::render/open]
|
:<- [::render/open]
|
||||||
:<- [::render/open-frame]
|
:<- [::render/open-frame]
|
||||||
(fn [[{:keys [path]} [_ id n] clip clip-id open f] _]
|
(fn [[selection clip clip-id open f] _]
|
||||||
;; `nest/placement` of the TARGET, with the bounds of what it draws — the
|
(when clip
|
||||||
;; same work `::selected-placement` does, kept separate because the two are
|
(creation/target clip (:store (store/entry clip-id)) open selection f))))
|
||||||
;; separate states and are drawn differently: the target gets an outline and
|
|
||||||
;; a tag, the selection gets handles.
|
(rf/reg-sub
|
||||||
(when n
|
::creation-placement
|
||||||
|
:<- [::creation-target]
|
||||||
|
:<- [::render/clip]
|
||||||
|
:<- [::render/clip-id]
|
||||||
|
:<- [::render/open]
|
||||||
|
:<- [::render/open-frame]
|
||||||
|
(fn [[{:keys [path]} clip clip-id open f] _]
|
||||||
|
;; The effective occurrence whose contents receive creation. A fallback may
|
||||||
|
;; therefore outline an ancestor while the off-frame primary selection stays
|
||||||
|
;; unchanged.
|
||||||
|
(when (and clip (seq path))
|
||||||
(let [st (:store (store/entry clip-id))]
|
(let [st (:store (store/entry clip-id))]
|
||||||
(when-let [pl (nest/placement clip st open (or path [id]) f)]
|
(when-let [{:keys [sid id frame] :as pl} (nest/placement clip st open path f)]
|
||||||
(assoc pl :node n :bounds ((pick/bounds-of clip st (:sid pl) n) (:frame pl))))))))
|
(let [n (get-in clip [:symbols sid :nodes id])]
|
||||||
|
(assoc pl :node n
|
||||||
|
:bounds ((pick/bounds-of clip st sid n) frame))))))))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::selected-placement
|
::selected-placement
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
(case kind
|
(case kind
|
||||||
:symbol (rf/dispatch [::ui/drop-symbol sid frame point target])
|
:symbol (rf/dispatch [::ui/drop-symbol sid frame point target])
|
||||||
;; Video is asked about before anything happens: which frames, and what
|
;; Video is asked about before anything happens: which frames, and what
|
||||||
;; the symbol they become is called. The lane it was aimed at travels
|
;; the symbol they become is called. The explicit timeline row travels
|
||||||
;; with the question, so the answer lands where the drop pointed.
|
;; with the question, so the answer lands where the drop pointed.
|
||||||
:footage (rf/dispatch [::footage/ask-convert c frame point target])
|
:footage (rf/dispatch [::footage/ask-convert c frame point target])
|
||||||
:import (rf/dispatch [::project/import c frame point target])
|
:import (rf/dispatch [::project/import c frame point target])
|
||||||
|
|
|
||||||
|
|
@ -141,12 +141,10 @@
|
||||||
last-i (dec (count crumbs))
|
last-i (dec (count crumbs))
|
||||||
shared (shared-with clip n)
|
shared (shared-with clip n)
|
||||||
says (whereabouts clip n inside)
|
says (whereabouts clip n inside)
|
||||||
;; WHAT IS AIMED, which is not what is selected: the menu has to name
|
;; The effective parent can be an active ancestor of the primary row
|
||||||
;; the place it would add to, and that place is the target. Named the
|
;; when the preferred occurrence is off the playhead.
|
||||||
;; way the trail names a crumb, so the menu and the outline's tag and
|
destination (let [{:keys [id node]} @(rf/subscribe [::sub/creation-placement])]
|
||||||
;; the breadcrumb all call one node by one name.
|
(when node (crumb-label clip id node)))]
|
||||||
aimed (let [[tsid tid tn] @(rf/subscribe [::sub/target-node])]
|
|
||||||
(when tn (crumb-label clip tid tn)))]
|
|
||||||
[:section.loc
|
[:section.loc
|
||||||
[:nav.crumbs {:aria-label "editing location"}
|
[:nav.crumbs {:aria-label "editing location"}
|
||||||
(doall
|
(doall
|
||||||
|
|
@ -161,9 +159,7 @@
|
||||||
:title (if (zero? i)
|
:title (if (zero? i)
|
||||||
"the open symbol — clear the selection"
|
"the open symbol — clear the selection"
|
||||||
(str label " · " (if lane? "lane" (name crumb-kind))))
|
(str label " · " (if lane? "lane" (name crumb-kind))))
|
||||||
;; A CRUMB AIMS, because this bar is the one that says where an
|
:on-click #(rf/dispatch [::ui/select (when (pos? i) select)])}
|
||||||
;; edit would land: going out to a level is going there to work.
|
|
||||||
:on-click #(rf/dispatch [::ui/aim (when (pos? i) select)])}
|
|
||||||
label]]))]
|
label]]))]
|
||||||
(when says
|
(when says
|
||||||
[:span.loc-fact {:title "the frame this selection is showing, in its own time"}
|
[:span.loc-fact {:title "the frame this selection is showing, in its own time"}
|
||||||
|
|
@ -184,20 +180,18 @@
|
||||||
[menu/view
|
[menu/view
|
||||||
{:label "new" :title "add to the document"
|
{:label "new" :title "add to the document"
|
||||||
:items [{:label "inside"
|
:items [{:label "inside"
|
||||||
:sub (if aimed
|
:sub (if destination
|
||||||
(str "a symbol in " aimed " — the outlined one")
|
(str "a symbol in " destination " — the outlined one")
|
||||||
(str "a symbol at the top of " (clip/symbol-name clip open)
|
(str "a symbol at the top of " (clip/symbol-name clip open)
|
||||||
", since nothing is aimed"))
|
", since no nested row is active here"))
|
||||||
:on-click #(rf/dispatch [::ui/new-symbol :inside])}
|
:on-click #(rf/dispatch [::ui/new-symbol :inside])}
|
||||||
;; THE SECOND ITEM IS THE POINT OF THE MENU. With one item it was
|
;; THE SECOND ITEM IS THE POINT OF THE MENU. With one item it was
|
||||||
;; impossible to add anything at the top of the open symbol
|
;; impossible to add anything at the top of the open symbol
|
||||||
;; without first clearing the aim, which meant the only way out
|
;; without first clearing the active row.
|
||||||
;; of a nesting was to leave it — and nobody could see the rule
|
|
||||||
;; they were working against.
|
|
||||||
{:label "at top"
|
{:label "at top"
|
||||||
:disabled? (nil? aimed)
|
:disabled? (nil? destination)
|
||||||
:sub (str "a symbol straight into " (clip/symbol-name clip open)
|
:sub (str "a symbol straight into " (clip/symbol-name clip open)
|
||||||
", ignoring what is aimed")
|
", ignoring the active row")
|
||||||
:on-click #(rf/dispatch [::ui/new-symbol :top])}
|
:on-click #(rf/dispatch [::ui/new-symbol :top])}
|
||||||
{:label "lane"
|
{:label "lane"
|
||||||
:sub (str "a row for symbol clips in " (clip/symbol-name clip open))
|
:sub (str "a row for symbol clips in " (clip/symbol-name clip open))
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,13 @@
|
||||||
(defn- select!
|
(defn- select!
|
||||||
"Select the node at row path `path` of the open symbol — the selection a
|
"Select the node at row path `path` of the open symbol — the selection a
|
||||||
timeline row makes, so the row, the inspector and the stage all show it — or
|
timeline row makes, so the row, the inspector and the stage all show it — or
|
||||||
the open symbol itself when `path` is empty. Blank stage is an explicit place,
|
the open symbol itself when `path` is empty."
|
||||||
not merely an absence of a picked shape, so it clears a stale drawing target
|
|
||||||
as well as the inspector selection."
|
|
||||||
[{:keys [open f] :as ctx} path]
|
[{:keys [open f] :as ctx} path]
|
||||||
(let [{document :clip st :store} (loaded ctx)]
|
(let [{document :clip st :store} (loaded ctx)]
|
||||||
(if-let [{:keys [sid id]} (when (seq path)
|
(if-let [{:keys [sid id]} (when (seq path)
|
||||||
(nest/placement document st open path f))]
|
(nest/placement document st open path f))]
|
||||||
(rf/dispatch [::ui/select [:node sid id path]])
|
(rf/dispatch [::ui/select [:node sid id path]])
|
||||||
(rf/dispatch [::ui/aim nil]))))
|
(rf/dispatch [::ui/select nil]))))
|
||||||
|
|
||||||
(defn- begin!
|
(defn- begin!
|
||||||
"Start dragging `kind` of the node at `path` from stage point `p`."
|
"Start dragging `kind` of the node at `path` from stage point `p`."
|
||||||
|
|
@ -305,13 +303,12 @@
|
||||||
(when-let [{:keys [sid id]} (and (seq path) (nest/placement document st open path f))]
|
(when-let [{:keys [sid id]} (and (seq path) (nest/placement document st open path f))]
|
||||||
[:node sid id path])))
|
[:node sid id path])))
|
||||||
|
|
||||||
(defn- aim-box
|
(defn- creation-box
|
||||||
"The TARGET's outline: where a new polygon or symbol would be parented, drawn
|
"The effective creation parent's outline, resolved from selection and playhead.
|
||||||
around the thing that would be its parent.
|
|
||||||
|
|
||||||
NOT THE SELECTION'S BOX AND DELIBERATELY NOT LIKE IT. The selection gets
|
NOT THE SELECTION'S BOX AND DELIBERATELY NOT LIKE IT. The selection gets
|
||||||
handles, because handles are how you change it; the target gets an outline
|
handles, because handles are how you change it; the target gets an outline
|
||||||
with nothing to grab, because there is nothing to change about being aimed at.
|
with nothing to grab, because the derived destination is not itself a handle.
|
||||||
Same geometry — the node's bounds through its own transform, so it turns with
|
Same geometry — the node's bounds through its own transform, so it turns with
|
||||||
what it is around — and a different statement.
|
what it is around — and a different statement.
|
||||||
|
|
||||||
|
|
@ -323,8 +320,9 @@
|
||||||
carries a `viewBox` scaled by `zoom`, which is also why the stroke widths
|
carries a `viewBox` scaled by `zoom`, which is also why the stroke widths
|
||||||
nearby are fractions."
|
nearby are fractions."
|
||||||
[]
|
[]
|
||||||
(let [{:keys [world bounds]} @(rf/subscribe [::sub/target-placement])
|
(let [{:keys [world bounds id node]}
|
||||||
[_ id n] @(rf/subscribe [::sub/target-node])
|
@(rf/subscribe [::sub/creation-placement])
|
||||||
|
n node
|
||||||
label (or (:name n) (some-> (node/source n) name)
|
label (or (:name n) (some-> (node/source n) name)
|
||||||
(when id (if (keyword? id) (subs (str id) 1) (subs (str id) 0 8))))]
|
(when id (if (keyword? id) (subs (str id) 1) (subs (str id) 0 8))))]
|
||||||
(when (and world bounds)
|
(when (and world bounds)
|
||||||
|
|
@ -333,16 +331,16 @@
|
||||||
;; Furthest bottom-right of the four as drawn: the corner a reader
|
;; Furthest bottom-right of the four as drawn: the corner a reader
|
||||||
;; would call "the bottom right" whatever the node has been turned to.
|
;; would call "the bottom right" whatever the node has been turned to.
|
||||||
[tx ty] (apply max-key (fn [[x y]] (+ x y)) corners)]
|
[tx ty] (apply max-key (fn [[x y]] (+ x y)) corners)]
|
||||||
[:g.aim {:pointer-events "none"}
|
[:g.creation-target {:pointer-events "none"}
|
||||||
[:polygon.aim-box {:points (points-text (flatten corners))}]
|
[:polygon.creation-target-box {:points (points-text (flatten corners))}]
|
||||||
(when label
|
(when label
|
||||||
[:g {:transform (str "translate(" (+ tx 1.5) " " (+ ty 1.5) ")")}
|
[:g {:transform (str "translate(" (+ tx 1.5) " " (+ ty 1.5) ")")}
|
||||||
;; The plate behind the text, sized off the string rather than
|
;; The plate behind the text, sized off the string rather than
|
||||||
;; measured: `textLength` would need a layout pass, and a tag that
|
;; measured: `textLength` would need a layout pass, and a tag that
|
||||||
;; is a little wide is better than one that reflows the frame.
|
;; is a little wide is better than one that reflows the frame.
|
||||||
[:rect.aim-tag-bg {:x 0 :y 0 :rx 0.8
|
[:rect.creation-target-tag-bg {:x 0 :y 0 :rx 0.8
|
||||||
:width (+ 2 (* 2.1 (count label))) :height 5}]
|
:width (+ 2 (* 2.1 (count label))) :height 5}]
|
||||||
[:text.aim-tag {:x 1 :y 3.8} label]])]))))
|
[:text.creation-target-tag {:x 1 :y 3.8} label]])]))))
|
||||||
|
|
||||||
(defn- overlay [w h zoom]
|
(defn- overlay [w h zoom]
|
||||||
(let [tool @(rf/subscribe [::sub/tool])
|
(let [tool @(rf/subscribe [::sub/tool])
|
||||||
|
|
@ -444,7 +442,7 @@
|
||||||
;; DRAWN WHILE DRAWING, unlike the handles. "Where will this polygon land"
|
;; DRAWN WHILE DRAWING, unlike the handles. "Where will this polygon land"
|
||||||
;; is the question the outline exists to answer, and the moment it is being
|
;; is the question the outline exists to answer, and the moment it is being
|
||||||
;; asked is mid-draft.
|
;; asked is mid-draft.
|
||||||
(when-not points? [aim-box])
|
(when-not points? [creation-box])
|
||||||
(when-not (or drawing? points?)
|
(when-not (or drawing? points?)
|
||||||
(if (> (count placements) 1) [group-handles ctx placements] [handles ctx]))
|
(if (> (count placements) 1) [group-handles ctx placements] [handles ctx]))
|
||||||
(when (and id pts (not drawing?) (not (channel/nothing? pts)))
|
(when (and id pts (not drawing?) (not (channel/nothing? pts)))
|
||||||
|
|
@ -475,12 +473,7 @@
|
||||||
;; unit a drop on the timeline's ruler lands in.
|
;; unit a drop on the timeline's ruler lands in.
|
||||||
frame @(rf/subscribe [::render/open-frame])
|
frame @(rf/subscribe [::render/open-frame])
|
||||||
clip @(rf/subscribe [::render/clip])
|
clip @(rf/subscribe [::render/clip])
|
||||||
clip-id @(rf/subscribe [::render/clip-id])
|
destination @(rf/subscribe [::sub/creation-target])
|
||||||
db-target @(rf/subscribe [::sub/target])
|
|
||||||
open @(rf/subscribe [::render/open])
|
|
||||||
destination (when clip
|
|
||||||
(ui/aimed-symbol clip (:store (store/entry clip-id))
|
|
||||||
{:ui {:open open :target db-target}} frame))
|
|
||||||
destination-name (when-let [sid (:sid destination)]
|
destination-name (when-let [sid (:sid destination)]
|
||||||
(clip/symbol-name clip sid))
|
(clip/symbol-name clip sid))
|
||||||
zoom @(rf/subscribe [::layout/zoom :stage])]
|
zoom @(rf/subscribe [::layout/zoom :stage])]
|
||||||
|
|
|
||||||
|
|
@ -585,14 +585,9 @@
|
||||||
(memoize (fn [_selection] (fn [el] (some-> el (.scrollIntoView #js {:block "nearest"}))))))
|
(memoize (fn [_selection] (fn [el] (some-> el (.scrollIntoView #js {:block "nearest"}))))))
|
||||||
|
|
||||||
(defn- label-cell [{:keys [path depth label kind node-kind lane? select expandable? expanded? of via]}
|
(defn- label-cell [{:keys [path depth label kind node-kind lane? select expandable? expanded? of via]}
|
||||||
selection selections target over solo tracing renaming draft]
|
selection selections target-path over solo tracing renaming draft]
|
||||||
(let [node? (= :node kind)
|
(let [node? (= :node kind)
|
||||||
selected? (contains? selections select)
|
selected? (contains? selections select)
|
||||||
;; AIMED IS NOT SELECTED, so it does not wear the selected class. The
|
|
||||||
;; target is where a new thing would go; the selection is what the
|
|
||||||
;; inspector is showing. One row is often both and must still say which
|
|
||||||
;; of the two it is being.
|
|
||||||
aimed? (and node? (= path (:path target)))
|
|
||||||
editing? (and lane? (= select @renaming))
|
editing? (and lane? (= select @renaming))
|
||||||
begin-rename! (fn [] (reset! draft label) (reset! renaming select))
|
begin-rename! (fn [] (reset! draft label) (reset! renaming select))
|
||||||
commit-rename! (fn []
|
commit-rename! (fn []
|
||||||
|
|
@ -603,7 +598,7 @@
|
||||||
[over-path where] @over]
|
[over-path where] @over]
|
||||||
[:div (cond-> {:class (str "tl-label" (when (and select selected?) " on")
|
[:div (cond-> {:class (str "tl-label" (when (and select selected?) " on")
|
||||||
(when (and select (= select selection)) " primary")
|
(when (and select (= select selection)) " primary")
|
||||||
(when aimed? " aimed")
|
(when (= path target-path) " target")
|
||||||
(when lane? " lane")
|
(when lane? " lane")
|
||||||
(when (= :ghost kind) " ghost")
|
(when (= :ghost kind) " ghost")
|
||||||
(when (= path over-path)
|
(when (= path over-path)
|
||||||
|
|
@ -614,16 +609,13 @@
|
||||||
:style {:padding-left (str (+ 4 (* 11 depth)) "px")}
|
:style {:padding-left (str (+ 4 (* 11 depth)) "px")}
|
||||||
:title label
|
:title label
|
||||||
:ref (when (and select (= select selection)) (reveal selection))
|
:ref (when (and select (= select selection)) (reveal selection))
|
||||||
;; A LABEL AIMS. Clicking a row's name says "I am working
|
;; Labels, bars and stage objects all select the same row.
|
||||||
;; here", which is a statement about a place in the document;
|
;; The primary row plus playhead resolves creation.
|
||||||
;; clicking its bar or one of its cels, below, says "show me
|
|
||||||
;; this", which is a statement about a thing on screen. Only
|
|
||||||
;; the first moves where new drawings and symbols go.
|
|
||||||
:on-click (fn [^js e]
|
:on-click (fn [^js e]
|
||||||
(when select
|
(when select
|
||||||
(rf/dispatch [(if (.-shiftKey e)
|
(rf/dispatch [(if (.-shiftKey e)
|
||||||
::ui/toggle-and-aim
|
::ui/toggle-selection
|
||||||
::ui/aim)
|
::ui/select)
|
||||||
select])))
|
select])))
|
||||||
;; An instance's row opens the symbol it places, as a tab.
|
;; An instance's row opens the symbol it places, as a tab.
|
||||||
:on-double-click (fn [^js e]
|
:on-double-click (fn [^js e]
|
||||||
|
|
@ -665,8 +657,10 @@
|
||||||
[:button.tl-twist
|
[:button.tl-twist
|
||||||
{:disabled (not expandable?)
|
{:disabled (not expandable?)
|
||||||
:on-click (fn [^js e]
|
:on-click (fn [^js e]
|
||||||
|
(println "heyyy")
|
||||||
(.stopPropagation e)
|
(.stopPropagation e)
|
||||||
(rf/dispatch [::ui/toggle-row path]))}
|
(rf/dispatch [::ui/toggle-row path]))
|
||||||
|
:class (println expandable?)}
|
||||||
(when expandable? (if expanded? "▾" "▸"))]
|
(when expandable? (if expanded? "▾" "▸"))]
|
||||||
;; A LANE IS NOT AN INSTANCE WEARING A DIFFERENT HAT, to read. Its one row
|
;; A LANE IS NOT AN INSTANCE WEARING A DIFFERENT HAT, to read. Its one row
|
||||||
;; holds blocks that follow one another in time, where every other row
|
;; holds blocks that follow one another in time, where every other row
|
||||||
|
|
@ -726,7 +720,7 @@
|
||||||
moved since. What it looks like mid-drag is `[:ui :sliding]`, which the clip
|
moved since. What it looks like mid-drag is `[:ui :sliding]`, which the clip
|
||||||
every row and the stage are drawn from already has in it."
|
every row and the stage are drawn from already has in it."
|
||||||
[{:keys [path span keys dense? kind node-kind select slides cels lane? of unmapped? owner]}
|
[{:keys [path span keys dense? kind node-kind select slides cels lane? of unmapped? owner]}
|
||||||
frames sliding hint {:keys [clip store open selection selections]}]
|
frames sliding hint {:keys [clip store open selection selections target-path]}]
|
||||||
(let [selected-set (set selections)
|
(let [selected-set (set selections)
|
||||||
active-row (:row @sliding)
|
active-row (:row @sliding)
|
||||||
slide (fn [^js e]
|
slide (fn [^js e]
|
||||||
|
|
@ -882,7 +876,8 @@
|
||||||
[:div.tl-track
|
[:div.tl-track
|
||||||
;; The track, not the bar, holds the pointer while a bar slides, so the drag
|
;; The track, not the bar, holds the pointer while a bar slides, so the drag
|
||||||
;; goes on when the bar has slid off the ruler and is no longer drawn.
|
;; goes on when the bar has slid off the ruler and is no longer drawn.
|
||||||
{:class (str (when lane? "lane") (when (= :palette kind) " palette"))
|
{:class (str (when lane? "lane") (when (= :palette kind) " palette")
|
||||||
|
(when (= path target-path) " target"))
|
||||||
:on-pointer-move slide
|
:on-pointer-move slide
|
||||||
:on-pointer-up (fn [e] (slide e) (done true))
|
:on-pointer-up (fn [e] (slide e) (done true))
|
||||||
:on-pointer-cancel (fn [_] (done false))
|
:on-pointer-cancel (fn [_] (done false))
|
||||||
|
|
@ -1000,6 +995,7 @@
|
||||||
:class (str (when ghost? "ghost")
|
:class (str (when ghost? "ghost")
|
||||||
(when (and select (contains? selected-set select)) " on")
|
(when (and select (contains? selected-set select)) " on")
|
||||||
(when (and select (= select selection)) " primary")
|
(when (and select (= select selection)) " primary")
|
||||||
|
(when (= (nth select 3 nil) target-path) " target")
|
||||||
(when (and select (= select (get-in @sliding [:nest :select])))
|
(when (and select (= select (get-in @sliding [:nest :select])))
|
||||||
" nest-target"))
|
" nest-target"))
|
||||||
:style {:position "absolute" :left (edge% in frames)
|
:style {:position "absolute" :left (edge% in frames)
|
||||||
|
|
@ -1107,7 +1103,12 @@
|
||||||
selection @(rf/subscribe [::sub/selection])
|
selection @(rf/subscribe [::sub/selection])
|
||||||
selections @(rf/subscribe [::sub/selections])
|
selections @(rf/subscribe [::sub/selections])
|
||||||
selection-set (set selections)
|
selection-set (set selections)
|
||||||
target @(rf/subscribe [::sub/target])
|
creation @(rf/subscribe [::sub/creation-target])
|
||||||
|
target-path (let [path (:path creation)]
|
||||||
|
(if (and (empty? path)
|
||||||
|
(= :lane (:kind creation)))
|
||||||
|
[::lane]
|
||||||
|
path))
|
||||||
expanded @(rf/subscribe [::sub/expanded])
|
expanded @(rf/subscribe [::sub/expanded])
|
||||||
drop @(rf/subscribe [::sub/drop])
|
drop @(rf/subscribe [::sub/drop])
|
||||||
solo (set @(rf/subscribe [::render/solo]))
|
solo (set @(rf/subscribe [::render/solo]))
|
||||||
|
|
@ -1123,7 +1124,7 @@
|
||||||
;; will resolve on release -- and with no row under the pointer it is a
|
;; will resolve on release -- and with no row under the pointer it is a
|
||||||
;; row of its own at the top of the section it will appear in.
|
;; row of its own at the top of the section it will appear in.
|
||||||
;;
|
;;
|
||||||
;; A SOUND USED TO BE THE EXCEPTION: aimed at a lane it drew no block
|
;; A SOUND USED TO BE THE EXCEPTION: over a lane it drew no block
|
||||||
;; there and a ghost row down in the audio section instead, so dragging
|
;; there and a ghost row down in the audio section instead, so dragging
|
||||||
;; a sound onto a lane looked exactly like a lane refusing it -- while
|
;; a sound onto a lane looked exactly like a lane refusing it -- while
|
||||||
;; the drop itself worked and put the sound in the lane. One rule
|
;; the drop itself worked and put the sound in the lane. One rule
|
||||||
|
|
@ -1175,18 +1176,16 @@
|
||||||
[:section.pane.time
|
[:section.pane.time
|
||||||
[transport]
|
[transport]
|
||||||
[:div.tl-body
|
[:div.tl-body
|
||||||
;; Blank timeline space means the open symbol. Use the same `aim`
|
;; Blank timeline space means the open symbol. Child controls stop their
|
||||||
;; gesture as its breadcrumb so both the visible selection and the
|
|
||||||
;; drawing destination return to the root. Child controls stop their
|
|
||||||
;; own events; the target check keeps ordinary row/bar clicks local.
|
;; own events; the target check keeps ordinary row/bar clicks local.
|
||||||
{:on-click (fn [^js e]
|
{:on-click (fn [^js e]
|
||||||
(when (= (.-target e) (.-currentTarget e))
|
(when (= (.-target e) (.-currentTarget e))
|
||||||
(rf/dispatch [::ui/aim nil])))}
|
(rf/dispatch [::ui/select nil])))}
|
||||||
[:div.tl-labels
|
[:div.tl-labels
|
||||||
;; Empty label space takes a row back out to the top of the open symbol.
|
;; Empty label space takes a row back out to the top of the open symbol.
|
||||||
{:on-click (fn [^js e]
|
{:on-click (fn [^js e]
|
||||||
(when (= (.-target e) (.-currentTarget e))
|
(when (= (.-target e) (.-currentTarget e))
|
||||||
(rf/dispatch [::ui/aim nil])))
|
(rf/dispatch [::ui/select nil])))
|
||||||
:on-drag-over (fn [^js e]
|
:on-drag-over (fn [^js e]
|
||||||
(when (drag/row)
|
(when (drag/row)
|
||||||
(.preventDefault e)
|
(.preventDefault e)
|
||||||
|
|
@ -1202,12 +1201,13 @@
|
||||||
(doall (for [row visible]
|
(doall (for [row visible]
|
||||||
(with-meta (if (= :section (:kind row))
|
(with-meta (if (= :section (:kind row))
|
||||||
[:div.tl-label.tl-section (:label row)]
|
[:div.tl-label.tl-section (:label row)]
|
||||||
[label-cell row selection selection-set target over solo tracing renaming draft])
|
[label-cell row selection selection-set target-path
|
||||||
|
over solo tracing renaming draft])
|
||||||
{:key (str (:path row))})))]
|
{:key (str (:path row))})))]
|
||||||
[:div.tl-tracks
|
[:div.tl-tracks
|
||||||
{:on-click (fn [^js e]
|
{:on-click (fn [^js e]
|
||||||
(when (= (.-target e) (.-currentTarget e))
|
(when (= (.-target e) (.-currentTarget e))
|
||||||
(rf/dispatch [::ui/aim nil])))
|
(rf/dispatch [::ui/select nil])))
|
||||||
:on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e)))
|
:on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e)))
|
||||||
:on-drag-over (fn [^js e]
|
:on-drag-over (fn [^js e]
|
||||||
(when (drag/accepts?)
|
(when (drag/accepts?)
|
||||||
|
|
@ -1265,7 +1265,8 @@
|
||||||
[:div.tl-track.tl-section]
|
[:div.tl-track.tl-section]
|
||||||
[track-cell row frames sliding hint
|
[track-cell row frames sliding hint
|
||||||
{:clip clip :store store :open open
|
{:clip clip :store store :open open
|
||||||
:selection selection :selections selections}])
|
:selection selection :selections selections
|
||||||
|
:target-path target-path}])
|
||||||
{:key (str (:path row))})))
|
{:key (str (:path row))})))
|
||||||
[:div.tl-empty "nothing in this symbol"])
|
[:div.tl-empty "nothing in this symbol"])
|
||||||
[playhead :div.tl-playhead frames]]]
|
[playhead :div.tl-playhead frames]]]
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,9 @@
|
||||||
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/copy])}
|
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/copy])}
|
||||||
{:label "cut" :sub "Cut selection · ⌘/Ctrl X"
|
{:label "cut" :sub "Cut selection · ⌘/Ctrl X"
|
||||||
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/cut])}
|
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/cut])}
|
||||||
{:label "paste" :sub "Paste at playhead into the aimed place · ⌘/Ctrl V"
|
{:label "paste" :sub "Paste at playhead into the active row · ⌘/Ctrl V"
|
||||||
:disabled? (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])}
|
:disabled? (nil? clipboard) :on-click #(rf/dispatch [::ui/paste])}
|
||||||
{:label "duplicate" :sub "Duplicate here; lanes repeat forward · ⌘/Ctrl D"
|
{:label "duplicate" :sub "Duplicate beside selection; lanes repeat forward · ⌘/Ctrl D"
|
||||||
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/duplicate])}
|
:disabled? (not selected?) :on-click #(rf/dispatch [::ui/duplicate])}
|
||||||
{:label "duplicate unique"
|
{:label "duplicate unique"
|
||||||
:sub "Duplicate with a private nested symbol graph · ⇧⌘/Ctrl D"
|
:sub "Duplicate with a private nested symbol graph · ⇧⌘/Ctrl D"
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,52 @@
|
||||||
(is (= [4 8] (node/placed-span (get-in made [:symbols :main :nodes id]))))
|
(is (= [4 8] (node/placed-span (get-in made [:symbols :main :nodes id]))))
|
||||||
(is (empty? (clip/problems made)))))
|
(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
|
(deftest lane-duplicate-is-the-forward-repeat-and-ripples-later-cels
|
||||||
(let [doc (fixture/document)
|
(let [doc (fixture/document)
|
||||||
payload (:clipboard
|
payload (:clipboard
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
(ns arthur.events.lane-test
|
(ns arthur.events.lane-test
|
||||||
(:require [cljs.test :refer [deftest is]]
|
(:require [cljs.test :refer [deftest is]]
|
||||||
[arthur.domain.clip :as clip]
|
[arthur.domain.clip :as clip]
|
||||||
|
[arthur.domain.creation :as creation]
|
||||||
[arthur.domain.history :as history]
|
[arthur.domain.history :as history]
|
||||||
[arthur.domain.leaf :as leaf]
|
[arthur.domain.leaf :as leaf]
|
||||||
[arthur.domain.node :as node]
|
[arthur.domain.node :as node]
|
||||||
|
|
@ -42,22 +43,61 @@
|
||||||
(node/placed-span (get-in fitted [:symbols :main :nodes instance-id])))
|
(node/placed-span (get-in fitted [:symbols :main :nodes instance-id])))
|
||||||
"the lane follows a later change to its parent's extent")
|
"the lane follows a later change to its parent's extent")
|
||||||
(is (= 300 (clip/frames fitted lane-id))))
|
(is (= 300 (clip/frames fitted lane-id))))
|
||||||
(is (some? (get-in lane-db [:ui :target]))
|
(is (= :lane (:kind (creation/target document {} :main
|
||||||
"the new lane is aimed so drawing and pool drops can go into it"))))
|
(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]]
|
(let [db {:ui {:selection [:node :main :shape [:lane :shape]]
|
||||||
:target {:sid :main :id :lane :path [:lane]}}}
|
:selections [[:node :main :shape [:lane :shape]]]}}
|
||||||
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]}}}
|
|
||||||
after (ui/selected db nil)]
|
after (ui/selected db nil)]
|
||||||
(is (nil? (get-in after [:ui :selection])))
|
(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
|
(deftest an-explicit-lane-is-one-row-of-clips
|
||||||
(let [doc (fixture/document)
|
(let [doc (fixture/document)
|
||||||
|
|
@ -111,7 +151,34 @@
|
||||||
(is (= 12 (ui/selection-frame doc nil :main
|
(is (= 12 (ui/selection-frame doc nil :main
|
||||||
[:node :main :a [:a]] 12)))))
|
[: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
|
(let [doc {:fps 24 :width 20 :height 20
|
||||||
:symbols
|
:symbols
|
||||||
{:main {:id :main :frames 40
|
{:main {:id :main :frames 40
|
||||||
|
|
@ -121,16 +188,40 @@
|
||||||
:playback {:in 0 :speed 1 :end :stop}}}}
|
:playback {:in 0 :speed 1 :end :stop}}}}
|
||||||
:inside {:id :inside :frames 10 :nodes {}}}}
|
:inside {:id :inside :frames 10 :nodes {}}}}
|
||||||
db {:ui {:open :main
|
db {:ui {:open :main
|
||||||
:target {:sid :main :id :drawing :path [:drawing]}}
|
:selection [:node :main :drawing [:drawing]]}
|
||||||
:playback {:frame 20}}]
|
:playback {:frame 20}}]
|
||||||
(is (= {:sid :inside :path [:drawing] :frame 20}
|
(is (= {:sid :main :path [] :frame 20}
|
||||||
(ui/paste-destination db doc nil))
|
(select-keys (ui/paste-destination db doc nil) [:sid :path :frame]))
|
||||||
"pasting outside a target symbol's window authors cropped content")))
|
"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
|
(deftest polygon-landing-obeys-the-destination-symbol-mode
|
||||||
(let [lane (fixture/document)
|
(let [lane (fixture/document)
|
||||||
ordinary (update-in lane [:symbols :main] dissoc :display)
|
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}}]
|
:playback {:frame 5}}]
|
||||||
(is (true? (:lane? (ui/polygon-landing lane {} db))))
|
(is (true? (:lane? (ui/polygon-landing lane {} db))))
|
||||||
(is (false? (:lane? (ui/polygon-landing ordinary {} 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 saved [:symbols :main :display])))
|
||||||
(is (nil? (get-in (store/entry (:clip/current after)) [:history :done])))))
|
(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
|
(deftest sequence-commands-use-isolated-history-transactions
|
||||||
(let [doc (fixture/document)
|
(let [doc (fixture/document)
|
||||||
id (store/install! {:clip doc :store {}} "sequence-command-test")
|
id (store/install! {:clip doc :store {}} "sequence-command-test")
|
||||||
|
|
@ -184,24 +290,22 @@
|
||||||
(is (= [:vo] (mapv :id (:cels lane))))
|
(is (= [:vo] (mapv :id (:cels lane))))
|
||||||
(is (empty? (timeline/sound-rows doc :main #{})))))
|
(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
|
;; `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)` —
|
;; `selected` used to open the rows above a selection with `(pop path)` —
|
||||||
;; which THROWS on `[]`, aborting the whole event. Nothing was selected,
|
;; which THROWS on `[]`, aborting the whole event. Nothing was selected,
|
||||||
;; nothing was aimed, and the next polygon went wherever the stale target
|
;; nothing was selected, and the next polygon used stale creation state.
|
||||||
;; still pointed, which is most of what made aiming a lane feel random.
|
|
||||||
(let [doc (fixture/document)
|
(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
|
db {:clip/current id :paint/revision 0
|
||||||
:ui {:open :main :target {:sid :main :id :a :path [:a]}}
|
:ui {:open :main}
|
||||||
:playback {:frame 0}}
|
:playback {:frame 0}}
|
||||||
lane (first (filter :lane? (timeline/rows doc :main #{})))
|
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 []] (:select lane)))
|
||||||
(is (= [:node :main nil []] (get-in after [:ui :selection])))
|
(is (= [:node :main nil []] (get-in after [:ui :selection])))
|
||||||
(is (nil? (get-in after [:ui :target]))
|
(is (= :lane (:kind (creation/target doc {} :main
|
||||||
"the open symbol IS the place, so aiming its own row clears the target")
|
(get-in after [:ui :selection]) 0))))
|
||||||
(is (= [] (ui/where-new-goes doc after)))
|
|
||||||
(is (empty? (get-in after [:ui :expanded]))
|
(is (empty? (get-in after [:ui :expanded]))
|
||||||
"and there are no rows above the top to open")))
|
"and there are no rows above the top to open")))
|
||||||
|
|
||||||
|
|
@ -229,7 +333,7 @@
|
||||||
id (store/install! {:clip doc :store {}} "finish-opens-nothing")
|
id (store/install! {:clip doc :store {}} "finish-opens-nothing")
|
||||||
db {:clip/current id :paint/revision 0
|
db {:clip/current id :paint/revision 0
|
||||||
:ui {:open :main :tool :polygon
|
:ui {:open :main :tool :polygon
|
||||||
:target {:sid :main :id :a :path [:a]}
|
:selection [:node :main :a [:a]]
|
||||||
:draft [10 10 40 10 40 40]}
|
:draft [10 10 40 10 40 40]}
|
||||||
:playback {:frame 1}}]
|
:playback {:frame 1}}]
|
||||||
(reset! rf-db/app-db db)
|
(reset! rf-db/app-db db)
|
||||||
|
|
@ -237,7 +341,7 @@
|
||||||
(let [after @rf-db/app-db
|
(let [after @rf-db/app-db
|
||||||
[kind sid shape-id path] (get-in after [:ui :selection])]
|
[kind sid shape-id path] (get-in after [:ui :selection])]
|
||||||
(is (= :node kind))
|
(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 (= [:a shape-id] path))
|
||||||
(is (empty? (get-in after [:ui :expanded])))
|
(is (empty? (get-in after [:ui :expanded])))
|
||||||
(is (nil? (get-in after [:ui :tool]))))))
|
(is (nil? (get-in after [:ui :tool]))))))
|
||||||
|
|
@ -260,7 +364,7 @@
|
||||||
:playback {:in 0 :speed 1 :end :stop}}}}))
|
:playback {:in 0 :speed 1 :end :stop}}}}))
|
||||||
id (store/install! {:clip doc :store {}} "clip-selected-where-it-lives")
|
id (store/install! {:clip doc :store {}} "clip-selected-where-it-lives")
|
||||||
db {:clip/current id :paint/revision 0
|
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}}
|
:playback {:frame 9}}
|
||||||
after (ui/beginning-polygon db)
|
after (ui/beginning-polygon db)
|
||||||
[_ sid clip-id path] (get-in after [:ui :selection])
|
[_ 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,
|
assert.equal(await evaluate('document.querySelectorAll(".tl-track:not(.palette)").length'), 1,
|
||||||
'an ordinary symbol is an ordinary timeline row');
|
'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');
|
await clickNew('lane');
|
||||||
s = await shot();
|
s = await shot();
|
||||||
placed = mainInstances(s);
|
placed = mainInstances(s);
|
||||||
|
|
@ -178,8 +183,11 @@ try {
|
||||||
`the dropped clip remains in the explicit lane: ${JSON.stringify(s)}`);
|
`the dropped clip remains in the explicit lane: ${JSON.stringify(s)}`);
|
||||||
assert.equal(laneSymbols(s).length, 1, 'the drop creates no extra lane');
|
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
|
// Return to the root active row, then make a sibling lane there. Lane
|
||||||
// first remains aimed. Move the clip between their linear tracks.
|
// 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');
|
await clickNew('lane');
|
||||||
s = await shot();
|
s = await shot();
|
||||||
assert.equal(laneSymbols(s).length, 2, 'a second explicit command creates a second lane');
|
assert.equal(laneSymbols(s).length, 2, 'a second explicit command creates a second lane');
|
||||||
|
|
@ -239,7 +247,7 @@ try {
|
||||||
await shortcut('z', 'KeyZ');
|
await shortcut('z', 'KeyZ');
|
||||||
assert.equal(mainInstances(await shot()).length, 2, 'one undo restores the cut');
|
assert.equal(mainInstances(await shot()).length, 2, 'one undo restores the cut');
|
||||||
await evaluate(`re_frame.core.dispatch_sync(cljs.core.vector(
|
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 sleep(80);
|
||||||
await shortcut('v', 'KeyV');
|
await shortcut('v', 'KeyV');
|
||||||
assert.equal(mainInstances(await shot()).length, 3, 'paste uses the copied snapshot');
|
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.
|
// the authored node, the raster preview and the document round trip.
|
||||||
await page.eval(SEEK(0));
|
await page.eval(SEEK(0));
|
||||||
await sleep(150);
|
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 beforePaint = await page.eval(`(() => {
|
||||||
const c = document.querySelector('canvas.stage');
|
const c = document.querySelector('canvas.stage');
|
||||||
const d = c.getContext('2d').getImageData(25, 20, 1, 1).data;
|
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.
|
// A person cannot click twice inside one microtask; a test should not either.
|
||||||
const armed = await page.eval(`(() => {
|
const armed = await page.eval(`(() => {
|
||||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||||
.find(b => b.textContent === 'polygon');
|
.find(b => b.textContent === 'pen');
|
||||||
if (!button) return false;
|
if (!button) return false;
|
||||||
button.click();
|
button.click();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -40,21 +40,16 @@
|
||||||
--sel: #2f6fc0;
|
--sel: #2f6fc0;
|
||||||
--sel-bg: #cfe0f5;
|
--sel-bg: #cfe0f5;
|
||||||
|
|
||||||
/* A SECOND accent, and the one exception to the rule above — stated here
|
/* A SECOND accent because selection and effective creation target are two
|
||||||
because SELECTED and AIMED are two different facts that are often true of
|
facts that are often true of one node at the same time.
|
||||||
one node at the same time, and one accent cannot say both.
|
|
||||||
Selected is what you are LOOKING at: the inspector's subject, the thing
|
|
||||||
with handles on it. Aimed is where a new drawing or symbol would GO. They
|
|
||||||
used to be the same state, which is how drawing a polygon ended up in
|
|
||||||
whatever you had last clicked on the stage.
|
|
||||||
Violet rather than another blue: in the chrome it has to be told apart
|
Violet rather than another blue: in the chrome it has to be told apart
|
||||||
from --sel, and on the stage from the warm gold the handles are drawn in.
|
from --sel, and on the stage from the warm gold the handles are drawn in.
|
||||||
Colour is not the only channel either — the aim outline is SOLID where the
|
Colour is not the only channel either — the target outline is SOLID where the
|
||||||
selection's box is dashed, and it carries no handles at all, because there
|
selection's box is dashed, and it carries no handles at all, because there
|
||||||
is nothing about being aimed at that you could drag. --aim-stage is the
|
is nothing about being a creation parent that you could drag. --target-stage is the
|
||||||
same hue lifted to read against --stage, which is the one dark surface. */
|
same hue lifted to read against --stage, which is the one dark surface. */
|
||||||
--aim: #8d4bd6;
|
--target: #8d4bd6;
|
||||||
--aim-stage: #c79bf2;
|
--target-stage: #c79bf2;
|
||||||
|
|
||||||
/* A THIRD, and the last. SELECTED IN THE TIMELINE had been --sel-bg, which is
|
/* A THIRD, and the last. SELECTED IN THE TIMELINE had been --sel-bg, which is
|
||||||
#cfe0f5 against spans drawn in #dfe8f4: two pale blues a hair apart, in the
|
#cfe0f5 against spans drawn in #dfe8f4: two pale blues a hair apart, in the
|
||||||
|
|
@ -62,13 +57,13 @@
|
||||||
is showing — an expanded lane opens exactly the selected clip. The fix is
|
is showing — an expanded lane opens exactly the selected clip. The fix is
|
||||||
not a stronger blue, which stays in the family the spans already own, but a
|
not a stronger blue, which stays in the family the spans already own, but a
|
||||||
different hue entirely. Ochre: warm against every cool surface in the pane,
|
different hue entirely. Ochre: warm against every cool surface in the pane,
|
||||||
clear of --aim's violet, --live's green and the playhead's red, and dark
|
clear of --target's violet, --live's green and the playhead's red, and dark
|
||||||
enough at full strength to carry white type on a 21px block. */
|
enough at full strength to carry white type on a 21px block. */
|
||||||
--pick: #a85c00;
|
--pick: #a85c00;
|
||||||
--pick-bg: #ffd9a0;
|
--pick-bg: #ffd9a0;
|
||||||
--pick-fg: #4a2800;
|
--pick-fg: #4a2800;
|
||||||
|
|
||||||
/* Auto-key is a recording state, distinct from selection and aiming. */
|
/* Auto-key is a recording state, distinct from selection and creation target. */
|
||||||
--live: #239447;
|
--live: #239447;
|
||||||
--live-bg: #e2f5e7;
|
--live-bg: #e2f5e7;
|
||||||
|
|
||||||
|
|
@ -1085,10 +1080,18 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||||||
color: var(--pick-fg);
|
color: var(--pick-fg);
|
||||||
}
|
}
|
||||||
.tl-label.on .kind { color: var(--pick); }
|
.tl-label.on .kind { color: var(--pick); }
|
||||||
.tl-label.aimed { box-shadow: inset 0 0 0 2px var(--aim); }
|
.tl-label.target { box-shadow: inset 0 0 0 2px var(--target); }
|
||||||
/* Both at once, and they must still be two readable facts: the aim's ring sits
|
/* Both at once, and they must still be two readable facts: the target ring sits
|
||||||
inside the pick's edge rather than replacing it. */
|
inside the pick's edge rather than replacing it. */
|
||||||
.tl-label.on.aimed { box-shadow: inset 3px 0 0 var(--pick), inset 0 0 0 2px var(--aim); }
|
.tl-label.on.target { box-shadow: inset 3px 0 0 var(--pick), inset 0 0 0 2px var(--target); }
|
||||||
|
.tl-track.target::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: 2px solid var(--target);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
.tl-label:hover:not(.on) { background: #fff; }
|
.tl-label:hover:not(.on) { background: #fff; }
|
||||||
.tl-label .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
.tl-label .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||||
.tl-label .kind { color: var(--dim); }
|
.tl-label .kind { color: var(--dim); }
|
||||||
|
|
@ -1213,6 +1216,7 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||||||
}
|
}
|
||||||
.tl-cel.on:hover:not(:disabled) { background: var(--pick); filter: brightness(1.12); }
|
.tl-cel.on:hover:not(:disabled) { background: var(--pick); filter: brightness(1.12); }
|
||||||
.tl-span.on { background: var(--pick-bg); border-color: var(--pick); }
|
.tl-span.on { background: var(--pick-bg); border-color: var(--pick); }
|
||||||
|
.tl-cel.target, .tl-span.target { outline: 2px solid var(--target); outline-offset: -2px; }
|
||||||
.tl-span.on.dense {
|
.tl-span.on.dense {
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
-45deg, var(--pick-bg) 0 3px, #f0bf77 3px 6px);
|
-45deg, var(--pick-bg) 0 3px, #f0bf77 3px 6px);
|
||||||
|
|
@ -1397,14 +1401,14 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
||||||
.paint-overlay .handles .pivot { stroke: #fff1be; stroke-width: 0.6; pointer-events: none; }
|
.paint-overlay .handles .pivot { stroke: #fff1be; stroke-width: 0.6; pointer-events: none; }
|
||||||
.paint-overlay .marquee { fill: rgba(230, 202, 139, 0.12); stroke: #e6ca8b; stroke-width: 0.6; stroke-dasharray: 2 1; pointer-events: none; }
|
.paint-overlay .marquee { fill: rgba(230, 202, 139, 0.12); stroke: #e6ca8b; stroke-width: 0.6; stroke-dasharray: 2 1; pointer-events: none; }
|
||||||
|
|
||||||
/* The aim outline and its name tag. Solid, no handles, and a tag pinned just
|
/* The creation-target outline and its name tag. Solid, no handles, and a tag pinned just
|
||||||
outside the bottom-right corner — see `ui/stage/aim-box` for why the box
|
outside the bottom-right corner — see `ui/stage/creation-box` for why the box
|
||||||
rotates with the node and the tag does not. Sizes are in user units because
|
rotates with the node and the tag does not. Sizes are in user units because
|
||||||
this SVG's viewBox is the stage's 320x200 scaled by `zoom`. */
|
this SVG's viewBox is the stage's 320x200 scaled by `zoom`. */
|
||||||
.paint-overlay .aim { pointer-events: none; }
|
.paint-overlay .creation-target { pointer-events: none; }
|
||||||
.paint-overlay .aim .aim-box { fill: none; stroke: var(--aim-stage); stroke-width: 0.7; }
|
.paint-overlay .creation-target .creation-target-box { fill: none; stroke: var(--target-stage); stroke-width: 0.7; }
|
||||||
.paint-overlay .aim .aim-tag-bg { fill: var(--aim-stage); }
|
.paint-overlay .creation-target .creation-target-tag-bg { fill: var(--target-stage); }
|
||||||
.paint-overlay .aim .aim-tag {
|
.paint-overlay .creation-target .creation-target-tag {
|
||||||
fill: #1b1030;
|
fill: #1b1030;
|
||||||
font: 600 3.4px ui-sans-serif, system-ui, sans-serif;
|
font: 600 3.4px ui-sans-serif, system-ui, sans-serif;
|
||||||
letter-spacing: 0.04px;
|
letter-spacing: 0.04px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue