ikd mn
This commit is contained in:
parent
208dddee07
commit
78fb120edd
9 changed files with 102 additions and 48 deletions
1
.claude/worktrees/tracing-layers
Submodule
1
.claude/worktrees/tracing-layers
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f4dd04764204506fc275180364d0366d693036f4
|
||||
|
|
@ -57,8 +57,9 @@
|
|||
THE EDITOR'S, NOT THE DOCUMENT'S. Showing a reference is a way of looking at
|
||||
the stage, like solo and zoom: not an undo step, not sent to collaborators,
|
||||
and it cannot reach an export. A layer is shown unless it is in `:hidden`, so
|
||||
footage brought in shows without being found and switched on first."
|
||||
{:on? true :opacity 0.5 :hidden #{}})
|
||||
footage brought in shows without being found and switched on first — once
|
||||
tracing itself is on, which it is not until asked for."
|
||||
{:on? false :opacity 0.5 :hidden #{}})
|
||||
|
||||
(def default
|
||||
{;; --- the document ---
|
||||
|
|
|
|||
|
|
@ -25,13 +25,14 @@
|
|||
another clip is `arthur.domain.bring`. This namespace is the document and the
|
||||
operations that only need the document.
|
||||
|
||||
NO SYMBOL IS SPECIAL. There is no reserved root and no pointer to one: which
|
||||
symbol is on screen is the editor's state, not the document's, and every
|
||||
function here that needs a symbol is told which. A new document has one symbol
|
||||
called `:main` because it has to be called something, and that is all the name
|
||||
means — it can be renamed, placed inside another symbol or deleted like any of
|
||||
them. `unplaced` answers the question a reserved root used to: which symbols
|
||||
nothing else places, and so which ones a person opening the document wants.
|
||||
NO SYMBOL IS SPECIAL. There is no reserved root id: which symbol is on screen
|
||||
is the editor's state, not the document's, and every function here that needs
|
||||
a symbol is told which. A new document has one symbol called `:main` because
|
||||
it has to be called something, and that is all the name means — it can be
|
||||
renamed or placed inside another symbol like any of them. What the document
|
||||
does say is `:root`, which symbol it opens on: a pointer, not a kind of
|
||||
symbol, the way a Flash file names its scene. See `opens-on` for why that
|
||||
cannot be worked out instead.
|
||||
|
||||
:fps is the output grid. A symbol's optional :fps names the native grid its
|
||||
frames were authored or measured on; absent means the document's grid."
|
||||
|
|
@ -52,7 +53,7 @@
|
|||
layer must not be able to have. Add the field here and to `leaf/leaves` and
|
||||
`leaf/clip` in the same commit."
|
||||
#{:name :fps :analyses :subjects :features :groups :width :height :symbols
|
||||
:palettes :default-palette})
|
||||
:palettes :default-palette :root})
|
||||
|
||||
(defn symbol
|
||||
"One of the clip's symbols, by id."
|
||||
|
|
@ -201,18 +202,39 @@
|
|||
(let [placed (into #{} (mapcat #(places clip %)) (keys (:symbols clip)))]
|
||||
(vec (sort-by str (remove placed (keys (:symbols clip)))))))
|
||||
|
||||
(defn opens-on
|
||||
"The symbol a document opens on: the longest one nothing else places, ties
|
||||
broken by id. The symbol that contains everything else is the longest of the
|
||||
unplaced ones in every document made so far, and a reserved name is what this
|
||||
replaces.
|
||||
|
||||
Never a tracing symbol: it is footage nobody has placed yet, and as long as
|
||||
the take it came from, so it would otherwise win."
|
||||
(defn- longest-unplaced
|
||||
"The longest symbol nothing else places, ties broken by id, and never a
|
||||
tracing symbol: that is footage nobody has placed yet, as long as its take."
|
||||
[clip]
|
||||
(first (sort-by (fn [sid] [(- (or (frames clip sid) 0)) (str sid)])
|
||||
(remove #(trace? (symbol clip %)) (unplaced clip)))))
|
||||
|
||||
(defn opens-on
|
||||
"The symbol a document opens on: its `:root`.
|
||||
|
||||
IT IS STORED, NOT WORKED OUT. It used to be the longest unplaced symbol, on
|
||||
the theory that the one containing everything else is always that. A take
|
||||
disproves it: imported footage is a symbol a thousand frames long, and the
|
||||
moment its instance is deleted, or the drop lands somewhere other than the
|
||||
root, nothing places it and it outranks a 120-frame `:main`. The document then
|
||||
opened on the take, and `set-root-fps` rewrote the take's rate to the
|
||||
project's on the way in.
|
||||
|
||||
A document with no `:root` — a demo, a fixture — still gets the old answer."
|
||||
[clip]
|
||||
(let [root (:root clip)]
|
||||
(if (contains? (:symbols clip) root) root (longest-unplaced clip))))
|
||||
|
||||
(defn pin-root
|
||||
"Give a document saved before `:root` existed the root it was made with.
|
||||
Every such document started as `blank`, whose root is `:main`, and ids never
|
||||
change, so `:main` is the answer wherever it survives; the old rule is only
|
||||
for documents that never had one."
|
||||
[clip]
|
||||
(cond-> clip
|
||||
(not (:root clip))
|
||||
(assoc :root (if (contains? (:symbols clip) :main) :main (longest-unplaced clip)))))
|
||||
|
||||
(defn set-root-fps
|
||||
"Set the document/output rate and the root symbol's editing rate together.
|
||||
|
||||
|
|
@ -250,6 +272,7 @@
|
|||
:width 320 :height 200
|
||||
:palettes {pal/default-id pal/default-palette}
|
||||
:default-palette pal/default-id
|
||||
:root :main
|
||||
;; No native fps yet: an untouched canvas follows the project grid. Imported
|
||||
;; and generated symbols carry their own rate explicitly.
|
||||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
||||
|
|
@ -723,6 +746,9 @@
|
|||
(when (and (contains? clip :default-palette) (map? (:palettes clip))
|
||||
(not (contains? (:palettes clip) (:default-palette clip))))
|
||||
[":default-palette must name a project palette"])
|
||||
(when (and (contains? clip :root) (map? (:symbols clip))
|
||||
(not (contains? (:symbols clip) (:root clip))))
|
||||
[(str ":root names missing symbol " (pr-str (:root clip)))])
|
||||
(for [[id p] (:palettes clip)
|
||||
:when (or (not= id (:id p)) (not (pal/valid-palette? p)))]
|
||||
(str "palette " (pr-str id) " is invalid or has a different :id"))
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
clip/<cid>/stage width, height
|
||||
clip/<cid>/palette/<pid> a named indexed palette asset
|
||||
clip/<cid>/palette-default the project fallback palette id
|
||||
clip/<cid>/root the symbol the document opens on
|
||||
clip/<cid>/source the analysis record this came out of
|
||||
clip/<cid>/subject/<subj> a tracked subject and its params
|
||||
clip/<cid>/feature/<fid> one feature: area, nodes, params
|
||||
|
|
@ -138,6 +139,7 @@
|
|||
(some-leaf (at "timing") (select-keys clip [:fps]))
|
||||
(some-leaf (at "stage") (select-keys clip [:width :height]))
|
||||
(some-leaf (at "palette-default") (select-keys clip [:default-palette]))
|
||||
(some-leaf (at "root") (select-keys clip [:root]))
|
||||
(some-leaf (at "analyses") (:analyses clip))
|
||||
(concat
|
||||
(for [[id v] (:subjects clip)] {(at "subject" (segment id)) v})
|
||||
|
|
@ -198,6 +200,7 @@
|
|||
"stage" (merge acc v)
|
||||
"analyses" (assoc acc :analyses v)
|
||||
"palette-default" (merge acc v)
|
||||
"root" (merge acc v)
|
||||
"palette" (assoc-in acc [:palettes (unsegment a)] v)
|
||||
"subject" (assoc-in acc [:subjects (unsegment a)] v)
|
||||
"feature" (assoc-in acc [:features (unsegment a)] v)
|
||||
|
|
@ -239,7 +242,7 @@
|
|||
false)
|
||||
(case (count p)
|
||||
;; The clip's own facts carry no id.
|
||||
3 (#{"name" "timing" "stage" "analyses" "palette-default"} (nth p 2))
|
||||
3 (#{"name" "timing" "stage" "analyses" "palette-default" "root"} (nth p 2))
|
||||
4 (#{"subject" "feature" "group" "palette"} (nth p 2))
|
||||
false))))]
|
||||
(vec
|
||||
|
|
|
|||
|
|
@ -113,7 +113,9 @@
|
|||
;; also normalizes documents saved by the earlier model,
|
||||
;; where changing project FPS left the root on its old
|
||||
;; editing grid.
|
||||
built (clip/set-root-fps (:clip loaded) (:fps (:clip loaded)))]
|
||||
built (-> (:clip loaded)
|
||||
clip/pin-root
|
||||
(clip/set-root-fps (:fps (:clip loaded))))]
|
||||
(let [entry (merge (select-keys built [:fps :width :height])
|
||||
{:label (str (or (.-name clip-json) cid) " (saved)")
|
||||
:cid cid
|
||||
|
|
|
|||
|
|
@ -841,6 +841,7 @@
|
|||
:width (first stage) :height (second stage)
|
||||
:palettes {pal/default-id pal/default-palette}
|
||||
:default-palette pal/default-id
|
||||
:root :main
|
||||
:subjects (into {} (map (fn [[id _]]
|
||||
[id {:id id :params {}
|
||||
:analysis (:id analysis)
|
||||
|
|
|
|||
|
|
@ -1241,10 +1241,9 @@
|
|||
(let [by-frame (group-by :at (concat key-items (mapcat :key-items cels)))]
|
||||
(for [[index f] (map-indexed vector (sort (distinct (concat keys (mapcat :keys cels)))))
|
||||
:when (and (<= 0 f) (< f frames))]
|
||||
^{:key index}
|
||||
(if-let [items (and (= :channel kind) (seq (get by-frame f)))]
|
||||
[key-dot (vec items) frames key-selection key-preview key-anchor key-state key-elements]
|
||||
[:div.tl-key {:aria-hidden true :style {:left (at% f frames)}}])))))]))
|
||||
^{:key index} [key-dot (vec items) frames key-selection key-preview key-anchor key-state key-elements]
|
||||
^{:key index} [:div.tl-key {:aria-hidden true :style {:left (at% f frames)}}])))))]))
|
||||
|
||||
(defn- cursor-hint
|
||||
"What the drag in flight would do, beside the pointer.
|
||||
|
|
|
|||
|
|
@ -216,10 +216,19 @@
|
|||
|
||||
(deftest no-symbol-is-special
|
||||
(let [c (nested)]
|
||||
(testing "a document opens on the longest symbol nothing places"
|
||||
(testing "a document opens on its root, however long its unplaced symbols are"
|
||||
(is (= [:loose :main :outer] (clip/unplaced c)))
|
||||
(is (= :outer (clip/opens-on c)))
|
||||
(is (= :main (clip/opens-on (clip/blank)))))
|
||||
(is (= :main (clip/opens-on c)))
|
||||
(is (= :main (clip/opens-on (clip/blank))))
|
||||
(is (= :loose (clip/opens-on (assoc c :root :loose))))
|
||||
(is (= c (leaf/clip "u" (leaf/leaves "u" c))))
|
||||
(is (seq (clip/problems (assoc c :root :gone)))))
|
||||
(testing "without a root, the longest symbol nothing places"
|
||||
(is (= :outer (clip/opens-on (dissoc c :root)))))
|
||||
(testing "a document saved before roots opens on :main, not a longer take"
|
||||
(is (= :main (:root (clip/pin-root (dissoc c :root)))))
|
||||
(is (= :outer (:root (clip/pin-root (-> c (dissoc :root) (update :symbols dissoc :main))))))
|
||||
(is (= :loose (:root (clip/pin-root (assoc c :root :loose))))))
|
||||
(testing "an instance can go into any symbol, and spans that symbol's frames"
|
||||
(let [[n] (vals (get-in c [:symbols :outer :nodes]))]
|
||||
(is (= #{:inner} (node/sources n)))
|
||||
|
|
|
|||
|
|
@ -523,32 +523,32 @@ async function main() {
|
|||
// same synchronous block arrives while the tool is still unset and is dropped.
|
||||
// A person cannot click twice inside one microtask; a test should not either.
|
||||
const armed = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||
.find(b => b.textContent === 'pen');
|
||||
const button = document.querySelector('.toolbox button.tool[title^="Pen"]');
|
||||
if (!button) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
const painted = armed && await page.eval(`(() => {
|
||||
const svg = document.querySelector('.paint-overlay');
|
||||
const box = svg.getBoundingClientRect();
|
||||
for (const [x, y] of [[10, 10], [40, 10], [25, 40]]) {
|
||||
// The overlay reaches past the stage on every side, so a stage pixel is
|
||||
// found through the SVG's own transform, as the stage finds it coming back.
|
||||
// Each point is its own eval: a click is a re-frame event, and the next one
|
||||
// reads the draft it added to.
|
||||
const PEN_AT = (x, y) => `(() => {
|
||||
const svg = document.querySelector('.paint-overlay.tool-pen');
|
||||
if (!svg) return false;
|
||||
const p = new DOMPoint(${x}, ${y}).matrixTransform(svg.getScreenCTM());
|
||||
svg.dispatchEvent(new PointerEvent('pointerdown', {
|
||||
bubbles: true, clientX: box.left + x * box.width / 320,
|
||||
clientY: box.top + y * box.height / 200,
|
||||
bubbles: true, clientX: p.x, clientY: p.y,
|
||||
}));
|
||||
return true;
|
||||
})()`;
|
||||
let painted = armed;
|
||||
for (const [x, y] of [[10, 10], [40, 10], [25, 40]]) {
|
||||
painted = painted && await page.eval(PEN_AT(x, y));
|
||||
await sleep(60);
|
||||
}
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
const finished = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||
.find(b => b.textContent === 'finish');
|
||||
if (!button || button.disabled) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`);
|
||||
// Closed as a person closes it: on the first point.
|
||||
const finished = painted && await page.eval(PEN_AT(10, 10));
|
||||
await sleep(250);
|
||||
const afterPaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
|
|
@ -556,7 +556,8 @@ async function main() {
|
|||
return [...d];
|
||||
})()`);
|
||||
check(painted && finished && beforePaint.join(',') !== afterPaint.join(','),
|
||||
'a polygon drawn with the paint controls appears on the canvas');
|
||||
'a polygon drawn with the paint controls appears on the canvas',
|
||||
`${beforePaint} -> ${afterPaint}`);
|
||||
for (const f of [8, 16]) {
|
||||
await page.eval(SEEK(f));
|
||||
await sleep(100);
|
||||
|
|
@ -597,7 +598,18 @@ async function main() {
|
|||
return [...c.getContext('2d').getImageData(25, 20, 1, 1).data];
|
||||
})()`);
|
||||
check(afterPaint.join(',') === reopenedPaint.join(','),
|
||||
'the painted polygon survives the project round trip');
|
||||
'the painted polygon survives the project round trip', `${afterPaint} vs ${reopenedPaint}`);
|
||||
// A reopened document has nothing selected, so the shape's keys are not in
|
||||
// the inspector until it is selected again, as anybody would.
|
||||
await page.eval(`(() => {
|
||||
const c = cljs.core, k = c.keyword;
|
||||
const db = c.deref(re_frame.db.app_db);
|
||||
const nodes = c.get_in(c.get(arthur.footage.store.entry(c.get(db, k('clip/current'))), k('clip')),
|
||||
c.vector(k('symbols'), k('main'), k('nodes')));
|
||||
const id = c.first(c.filter((id) => c.name(id).startsWith('paint-'), c.keys(nodes)));
|
||||
re_frame.core.dispatch_sync(c.vector(k('arthur.events.ui/select'),
|
||||
c.vector(k('node'), k('main'), id, c.vector(id))));
|
||||
})()`);
|
||||
await page.eval(SEEK(8));
|
||||
await sleep(100);
|
||||
check(await page.eval(`(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue