mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-06 10:31:05 +00:00
Merge remote-tracking branch 'upstream/main' into perf/exr-output-options
This commit is contained in:
commit
32871be132
6 files changed
+405
-56
No files matched your search
Generated
+76
-39
@@ -1412,7 +1412,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"craft-update",
|
||||
@@ -1438,7 +1438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-automation"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-host",
|
||||
@@ -1451,14 +1451,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-av1enc"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"filmcraft-av1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-cli"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"cpu-time",
|
||||
"effectcraft-automation",
|
||||
@@ -1477,7 +1477,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-color"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1485,7 +1485,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-effects"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"effectcraft-color",
|
||||
@@ -1503,7 +1503,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-engine"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-docpath",
|
||||
@@ -1541,7 +1541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-export"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-av1enc",
|
||||
"effectcraft-color",
|
||||
@@ -1572,7 +1572,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-expr"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
"effectcraft-color",
|
||||
@@ -1589,7 +1589,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-geom"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1597,7 +1597,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-gpu"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"effectcraft-color",
|
||||
@@ -1619,14 +1619,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-hevcenc"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"filmcraft-hevc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-host"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-export",
|
||||
@@ -1651,7 +1651,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-interchange"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-keyframe",
|
||||
@@ -1670,7 +1670,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-keyframe"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1682,7 +1682,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-lottie"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-docpath",
|
||||
"effectcraft-color",
|
||||
@@ -1700,7 +1700,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-media"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-docpath",
|
||||
"effectcraft-color",
|
||||
@@ -1732,7 +1732,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-model"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"image",
|
||||
@@ -1745,26 +1745,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-opusenc"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"filmcraft-opus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-path"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"effectcraft-keyframe",
|
||||
"effectcraft-raster",
|
||||
"kurbo",
|
||||
"linesweeper",
|
||||
"tiny-skia",
|
||||
"tiny-skia 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-pdf"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-pdf",
|
||||
"effectcraft-raster",
|
||||
@@ -1780,7 +1780,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-plugin"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-effects",
|
||||
"effectcraft-keyframe",
|
||||
@@ -1792,7 +1792,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-project"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"craft-ecproj-format",
|
||||
"effectcraft-color",
|
||||
@@ -1806,14 +1806,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-psd"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"miniz_oxide 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-raster"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-geom",
|
||||
@@ -1822,7 +1822,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-render"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-color",
|
||||
"effectcraft-effects",
|
||||
@@ -1844,7 +1844,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-sam2"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"candle-core",
|
||||
@@ -1859,7 +1859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-script"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
"effectcraft-engine",
|
||||
@@ -1877,7 +1877,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-segment"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"miniz_oxide 0.9.1",
|
||||
"ndarray",
|
||||
@@ -1886,7 +1886,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-svg"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-geom",
|
||||
"effectcraft-path",
|
||||
@@ -1898,7 +1898,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-text"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-keyframe",
|
||||
"harfrust",
|
||||
@@ -1910,7 +1910,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-time"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1918,7 +1918,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-track"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-raster",
|
||||
"effectcraft-segment",
|
||||
@@ -1930,7 +1930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-ui-egui"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-quit",
|
||||
@@ -1966,11 +1966,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-vp9enc"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
|
||||
[[package]]
|
||||
name = "effectcraft-web"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"effectcraft-engine",
|
||||
"effectcraft-export",
|
||||
@@ -5381,6 +5381,17 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "sctk-adwaita"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec"
|
||||
dependencies = [
|
||||
"log",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"tiny-skia 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
@@ -6110,6 +6121,20 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tiny-skia-path 0.11.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.12.0"
|
||||
@@ -6121,7 +6146,18 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"tiny-skia-path",
|
||||
"tiny-skia-path 0.12.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7430,6 +7466,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"redox_syscall 0.4.1",
|
||||
"rustix 0.38.44",
|
||||
"sctk-adwaita",
|
||||
"smithay-client-toolkit 0.19.2",
|
||||
"smol_str",
|
||||
"tracing",
|
||||
@@ -7587,7 +7624,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"pkg-config",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.20.0"
|
||||
version = "0.21.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -27,6 +27,37 @@ The shared patched syntax source is centralized in storytold/craft-libs, transfe
|
||||
PdfCraft revision 9ed3abe4907a127d36b12fbfeee71d7e26248da1. This parser-only use embeds no
|
||||
Hayro fonts, CMaps or image codecs; EffectCraft keeps its existing fonts and drawing filters.
|
||||
|
||||
Linux Wayland window decorations
|
||||
-------------------------------
|
||||
sctk-adwaita 0.10.1 (https://github.com/PolyMeilex/sctk-adwaita,
|
||||
commit d5f343c45c04d4a026db57f76760819b7325db00) is used by winit on Linux
|
||||
for vector window controls. Its title-font features are disabled: no Cantarell
|
||||
font is embedded by this dependency and no font file is added to this repo.
|
||||
|
||||
sctk-adwaita's MIT licence:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Bartłomiej Maryńczak
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
SIL Open Font License 1.1
|
||||
|
||||
Applies to Inter (assets/fonts/Inter-*.ttf), Noto Serif (assets/fonts/NotoSerif-Regular.ttf),
|
||||
|
||||
@@ -27,6 +27,9 @@ cpal = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Native GNOME/Wayland controls without a bundled title font.
|
||||
winit = { version = "0.30", default-features = false, features = ["wayland-csd-adwaita-notitle"] }
|
||||
|
||||
# Settings ▸ Appearance ▸ Sync with System: the XDG desktop portal's colour scheme. Already in
|
||||
# Cargo.lock (accesskit, through eframe), so it adds no new crates.
|
||||
zbus = "5.19"
|
||||
|
||||
+292
-16
@@ -543,25 +543,74 @@ impl Chain {
|
||||
}
|
||||
None
|
||||
};
|
||||
self.carry(f, None, frame, cancel, &lookup, &mut |g, seg| {
|
||||
let k = *self.keys.get(&g)?;
|
||||
store(k, sc, seg.clone());
|
||||
if let Some(st) = seg_store() {
|
||||
st.put_seg(k, sc, seg);
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
}
|
||||
|
||||
/// [`Chain::ensure`]'s walk: the matte carried to `f` from the nearest frame towards the base that `lookup` has,
|
||||
/// else from the root cut afresh from its strokes. `object: None` carries every stroke from the base frame (the shown
|
||||
/// matte); `Some(o)` carries only `o`'s, from its foreground-stroked frame nearest the base on the way. `keep`
|
||||
/// gets every frame made on the way.
|
||||
fn carry(
|
||||
&self,
|
||||
f: i64,
|
||||
object: Option<u32>,
|
||||
frame: &mut dyn FnMut(i64) -> Option<Arc<Image>>,
|
||||
cancel: &dyn Fn() -> bool,
|
||||
lookup: &dyn Fn(i64) -> Option<Arc<FrameSeg>>,
|
||||
keep: &mut dyn FnMut(i64, &Arc<FrameSeg>) -> Option<()>,
|
||||
) -> Option<Arc<FrameSeg>> {
|
||||
let strokes = |g: i64| match object {
|
||||
Some(o) => self.data.strokes_of(g, o),
|
||||
None => self.data.strokes_at(g),
|
||||
};
|
||||
if let Some(s) = lookup(f) {
|
||||
return Some(s);
|
||||
}
|
||||
let base = self.data.base?;
|
||||
let root = match object {
|
||||
Some(o) => {
|
||||
// one pass over the strokes, not one per frame on the way
|
||||
let on_the_way = |g: i64| (g == base || g.cmp(&base) == f.cmp(&base)) && g.abs_diff(base) <= f.abs_diff(base);
|
||||
let fg = self.data.strokes.iter().filter(|s| s.object == o && s.kind == StrokeKind::Fg && on_the_way(s.frame));
|
||||
fg.map(|s| s.frame).min_by_key(|g| g.abs_diff(base)).unwrap_or(f)
|
||||
}
|
||||
None => base,
|
||||
};
|
||||
if !self.data.in_span(f) {
|
||||
return None;
|
||||
}
|
||||
// Walk towards the base to the nearest available frame.
|
||||
// A new first stroke can still have a valid tracked matte nearer the base. Look for it
|
||||
// without reading those frames; without one, start afresh at the object's first stroke.
|
||||
let mut path = vec![f];
|
||||
let mut untracked_len = 1;
|
||||
let mut g = f;
|
||||
let mut start: Option<Arc<FrameSeg>> = None;
|
||||
while let Some(src) = self.data.source_of(g) {
|
||||
while g != base
|
||||
&& let Some(src) = self.data.source_of(g)
|
||||
{
|
||||
if cancel() {
|
||||
return None;
|
||||
}
|
||||
if let Some(s) = lookup(src) {
|
||||
start = Some(s);
|
||||
break;
|
||||
}
|
||||
path.push(src);
|
||||
if src == root {
|
||||
untracked_len = path.len();
|
||||
}
|
||||
g = src;
|
||||
}
|
||||
if start.is_none() {
|
||||
path.truncate(untracked_len);
|
||||
}
|
||||
path.reverse();
|
||||
let mut prev_img: Option<Arc<Image>> = None;
|
||||
let mut cur = start;
|
||||
@@ -570,8 +619,8 @@ impl Chain {
|
||||
return None;
|
||||
}
|
||||
let img = frame(g)?;
|
||||
let strokes = self.data.strokes_at(g);
|
||||
let seg = match (&cur, g == base) {
|
||||
let strokes = strokes(g);
|
||||
let seg = match (&cur, object.is_none() && g == root) {
|
||||
(_, true) | (None, _) => rb::segment_with(&img, &strokes, None, &self.opts, self.refine_radius, self.model.as_deref()),
|
||||
(Some(p), false) => {
|
||||
let src = self.data.source_of(g)?;
|
||||
@@ -587,11 +636,7 @@ impl Chain {
|
||||
}
|
||||
};
|
||||
let seg = Arc::new(seg);
|
||||
let k = *self.keys.get(&g)?;
|
||||
store(k, sc, seg.clone());
|
||||
if let Some(st) = seg_store() {
|
||||
st.put_seg(k, sc, &seg);
|
||||
}
|
||||
keep(g, &seg)?;
|
||||
cur = Some(seg);
|
||||
prev_img = Some(img);
|
||||
}
|
||||
@@ -674,7 +719,7 @@ impl Chain {
|
||||
max_pending_bytes: usize,
|
||||
) -> Result<Option<TrackSummary>, String> {
|
||||
let Some(tracker) = self.model.as_ref().and_then(|m| m.tracker()) else { return Ok(None) };
|
||||
let (Some(_), true) = (self.data.base, self.data.span[1] >= self.data.span[0]) else { return Err("no base frame".into()) };
|
||||
let (Some(base), true) = (self.data.base, self.data.span[1] >= self.data.span[0]) else { return Err("no base frame".into()) };
|
||||
// Frames of the span that already have a tracked result (this session's, or the saved
|
||||
// tracking) are kept: only the rest is tracked. A correction stroke changes the keys of the
|
||||
// frames from it on (both directions), so only those come back here.
|
||||
@@ -686,26 +731,48 @@ impl Chain {
|
||||
let (Some(&need_lo), Some(&need_hi)) = (needed.first(), needed.last()) else {
|
||||
return Ok(Some(TrackSummary::default()));
|
||||
};
|
||||
// Each object has its own prompts: a few clicks from its strokes and a box around what the classic engine
|
||||
// makes of them on that frame (clicks alone pick a part of the object, not all of it).
|
||||
// Each object has its own prompts: a few clicks from its strokes and a box saying how far it reaches (clicks
|
||||
// alone pick a part of the object). SAM 2 fills the box it is given, so the box surrounds the object's matte
|
||||
// on that frame: the one shown for a lone object; with several, each object's own, carried with its own strokes
|
||||
// as the shown one is (from its first prompted frame, or the nearest tracked or prompted frame), so a correction stroke on
|
||||
// part of an object still boxes all of it. Those per-object mattes are not cached: their keys mean tracked results.
|
||||
let objects = self.data.objects();
|
||||
let several = objects.len() > 1;
|
||||
let mut by_object: Vec<Vec<(i64, effectcraft_segment::Prompt)>> = vec![];
|
||||
for &obj in &objects {
|
||||
let mut prompts = vec![];
|
||||
for f in self.data.stroke_frames().into_iter().filter(|f| self.data.in_span(*f)) {
|
||||
let mut carried: Option<(i64, Arc<FrameSeg>)> = None;
|
||||
// from the base outwards on each side, so each carry goes on from the one before
|
||||
let mut frames: Vec<i64> = self.data.stroke_frames().into_iter().filter(|f| self.data.in_span(*f)).collect();
|
||||
frames.sort_by_key(|f| (*f < base, f.abs_diff(base)));
|
||||
for f in frames {
|
||||
let strokes = self.data.strokes_of(f, obj);
|
||||
if !strokes.iter().any(|s| s.kind == StrokeKind::Fg) {
|
||||
continue;
|
||||
}
|
||||
let p = match frame(f) {
|
||||
Some(img) => rb::tracker_prompt_boxed(&strokes, self.opts.scale, &rb::segment(&img, &strokes, None, &self.opts, self.refine_radius)),
|
||||
None => return Err(format!("cannot read Roto Brush prompt frame {f}")),
|
||||
let own = if several {
|
||||
// a later correction frame carries on from the one before it (only the last can be on its way)
|
||||
let lookup = |g| self.object_seg(g, obj).or_else(|| carried.as_ref().filter(|(k, _)| *k == g).map(|(_, s)| s.clone()));
|
||||
let own = self.carry(f, Some(obj), &mut *frame, cancel, &lookup, &mut |_, _| Some(()));
|
||||
carried = own.clone().map(|s| (f, s));
|
||||
own
|
||||
} else {
|
||||
self.ensure(f, &mut *frame, cancel)
|
||||
};
|
||||
let matte = match own {
|
||||
Some(own) => own,
|
||||
None if cancel() => return Err("tracking cancelled".into()),
|
||||
None => match frame(f) {
|
||||
Some(img) => Arc::new(rb::segment_with(&img, &strokes, None, &self.opts, self.refine_radius, self.model.as_deref())),
|
||||
None => return Err(format!("cannot read Roto Brush prompt frame {f}")),
|
||||
},
|
||||
};
|
||||
let p = rb::tracker_prompt_boxed(&strokes, self.opts.scale, &matte);
|
||||
if p.points.iter().any(|(_, fg)| *fg) {
|
||||
prompts.push((f, p));
|
||||
}
|
||||
}
|
||||
prompts.sort_by_key(|q| q.0);
|
||||
by_object.push(prompts);
|
||||
}
|
||||
// objects without a foreground prompt cannot be followed
|
||||
@@ -1470,6 +1537,215 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A model that sees only the left part of the disk, and records the tracker's box prompts.
|
||||
#[derive(Default)]
|
||||
struct LeftPart(std::sync::Mutex<Vec<(i64, Option<[f32; 4]>)>>);
|
||||
impl effectcraft_segment::MaskModel for LeftPart {
|
||||
fn info(&self) -> &'static effectcraft_segment::ModelInfo {
|
||||
&effectcraft_segment::SAM2_TINY
|
||||
}
|
||||
fn segment(&self, _: &[[f32; 3]], w: usize, h: usize, _: &effectcraft_segment::Prompt) -> effectcraft_segment::Result<Vec<f32>> {
|
||||
Ok((0..w * h).map(|i| f32::from((26..36).contains(&(i % w)) && (30..50).contains(&(i / w)))).collect())
|
||||
}
|
||||
fn tracker(&self) -> Option<&dyn effectcraft_segment::ObjectTracker> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
impl effectcraft_segment::ObjectTracker for LeftPart {
|
||||
fn track(
|
||||
&self,
|
||||
req: &effectcraft_segment::TrackRequest,
|
||||
_: &mut dyn FnMut(i64) -> Option<Arc<ClipFrame>>,
|
||||
_: &dyn Fn() -> bool,
|
||||
_: &mut dyn FnMut(usize, i64, effectcraft_segment::TrackedFrame),
|
||||
) -> effectcraft_segment::Result<()> {
|
||||
*self.0.lock().unwrap() = req.objects.iter().flatten().map(|(f, p)| (*f, p.bbox)).collect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tracker_box_surrounds_the_matte_shown_on_each_stroked_frame() {
|
||||
let mut data = RotoData::default();
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[30.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 2, radius: 3.0, points: vec![[50.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
data.span = [0, 3];
|
||||
let mut params = params_with(&data);
|
||||
params.values.insert(INPUT_KEY.into(), Value::Str("tracker-box-from-shown-matte".into()));
|
||||
let mut chain = Chain::new(¶ms, [100.0, 80.0], 25.0, 1.0);
|
||||
let fake = Arc::new(LeftPart::default());
|
||||
chain.model = Some(fake.clone());
|
||||
chain.keys = data.chain_keys(seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id));
|
||||
let mut get = |f: i64| Some(Arc::new(frame(f as f64 / 25.0)));
|
||||
let expected: Vec<(i64, Option<[f32; 4]>)> = [0, 2]
|
||||
.into_iter()
|
||||
.map(|f| (f, rb::tracker_prompt_boxed(&data.strokes_of(f, 0), 1.0, &chain.ensure(f, &mut get, &|| false).expect("a shown matte")).bbox))
|
||||
.collect();
|
||||
assert!(expected.iter().all(|(_, b)| b.is_some()));
|
||||
assert!(expected[0].1.unwrap()[2] < 44.0, "the base frame shows the model's part of the disk: {expected:?}");
|
||||
chain.track(&mut get, &|| false, &mut |_| {}).unwrap();
|
||||
assert_eq!(*fake.0.lock().unwrap(), expected, "the base frame and the correction frame are boxed around what they show");
|
||||
}
|
||||
|
||||
/// A model that cuts only a small square around each foreground click, and records every object's box prompts.
|
||||
#[derive(Default)]
|
||||
struct ClickSquares(std::sync::Mutex<Vec<Vec<(i64, Option<[f32; 4]>)>>>);
|
||||
impl effectcraft_segment::MaskModel for ClickSquares {
|
||||
fn info(&self) -> &'static effectcraft_segment::ModelInfo {
|
||||
&effectcraft_segment::SAM2_TINY
|
||||
}
|
||||
fn segment(&self, _: &[[f32; 3]], w: usize, h: usize, p: &effectcraft_segment::Prompt) -> effectcraft_segment::Result<Vec<f32>> {
|
||||
let near = |x: f32, y: f32| p.points.iter().any(|(c, fg)| *fg && (c[0] - x).abs() <= 4.0 && (c[1] - y).abs() <= 4.0);
|
||||
Ok((0..w * h).map(|i| f32::from(near((i % w) as f32 + 0.5, (i / w) as f32 + 0.5))).collect())
|
||||
}
|
||||
fn tracker(&self) -> Option<&dyn effectcraft_segment::ObjectTracker> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
impl effectcraft_segment::ObjectTracker for ClickSquares {
|
||||
fn track(
|
||||
&self,
|
||||
req: &effectcraft_segment::TrackRequest,
|
||||
_: &mut dyn FnMut(i64) -> Option<Arc<ClipFrame>>,
|
||||
_: &dyn Fn() -> bool,
|
||||
_: &mut dyn FnMut(usize, i64, effectcraft_segment::TrackedFrame),
|
||||
) -> effectcraft_segment::Result<()> {
|
||||
*self.0.lock().unwrap() = req.objects.iter().map(|o| o.iter().map(|(f, p)| (*f, p.bbox)).collect()).collect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_correction_stroke_on_part_of_one_of_several_objects_boxes_the_whole_object() {
|
||||
let mut data = RotoData::default();
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[30.0, 40.0], [50.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[88.0, 72.0]], object: 1 }, 3, [0, 3]);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 2, radius: 2.0, points: vec![[54.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
data.span = [0, 3];
|
||||
let mut params = params_with(&data);
|
||||
params.values.insert(INPUT_KEY.into(), Value::Str("object-correction-box".into()));
|
||||
let mut chain = Chain::new(¶ms, [100.0, 80.0], 25.0, 1.0);
|
||||
let fake = Arc::new(ClickSquares::default());
|
||||
chain.model = Some(fake.clone());
|
||||
chain.keys = data.chain_keys(seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id));
|
||||
chain.track(&mut |f| Some(Arc::new(frame(f as f64 / 25.0))), &|| false, &mut |_| {}).unwrap();
|
||||
let boxes = fake.0.lock().unwrap().clone();
|
||||
let box_of = |o: usize, f: i64| boxes[o].iter().find(|(g, _)| *g == f).and_then(|(_, b)| *b).expect("a box prompt");
|
||||
let (base, correction, other) = (box_of(0, 0), box_of(0, 2), box_of(1, 0));
|
||||
assert!(correction[0] <= base[0] + 6.0, "the correction frame boxes the object carried from frame 0 {base:?}, not only the stroke: {correction:?}");
|
||||
assert!(correction[2] >= 54.0 && correction[2] - correction[0] >= base[2] - base[0], "the box holds the whole object and the stroke: {correction:?}");
|
||||
assert!(correction[2] < other[0], "object 1 {other:?} stays out of object 0's box {correction:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_correction_after_propagate_carries_each_object_on_from_its_tracked_matte() {
|
||||
let mut data = RotoData::default();
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[30.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 0, radius: 3.0, points: vec![[15.0, 15.0]], object: 1 }, 3, [0, 3]);
|
||||
data.span = [0, 3];
|
||||
let mut params = params_with(&data);
|
||||
params.values.insert(INPUT_KEY.into(), Value::Str("object-correction-after-propagate".into()));
|
||||
let mut chain = Chain::new(¶ms, [100.0, 80.0], 25.0, 1.0);
|
||||
chain.model = Some(Arc::new(TwoObjects(Default::default())));
|
||||
chain.keys = data.chain_keys(seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id));
|
||||
assert_eq!(chain.track(&mut |f| Some(Arc::new(frame(f as f64 / 25.0))), &|| false, &mut |_| {}).unwrap().unwrap().frames, 4);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 2, radius: 2.0, points: vec![[56.0, 40.0]], object: 0 }, 3, [0, 3]);
|
||||
chain.data = Arc::new(data.clone());
|
||||
chain.keys = data.chain_keys(seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id));
|
||||
let fake = Arc::new(ClickSquares::default());
|
||||
chain.model = Some(fake.clone());
|
||||
let mut prompt_reads = vec![];
|
||||
let mut get = |f: i64| {
|
||||
if fake.0.lock().unwrap().is_empty() {
|
||||
prompt_reads.push(f);
|
||||
}
|
||||
Some(Arc::new(frame(f as f64 / 25.0)))
|
||||
};
|
||||
chain.track(&mut get, &|| false, &mut |_| {}).unwrap();
|
||||
assert_eq!(prompt_reads, [2, 1], "frame 2 carries on from the tracked frame 1; tracked frame 0 boxes both objects as it is");
|
||||
let boxes = fake.0.lock().unwrap().clone();
|
||||
let correction = boxes[0].iter().find(|(g, _)| *g == 2).and_then(|(_, b)| *b).expect("a box prompt");
|
||||
// the tracked disk carried to frame 2 spans x 29..59
|
||||
assert!(correction[0] <= 29.0 && correction[2] >= 59.0, "the box holds the whole disk: {correction:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_correction_before_an_objects_first_stroke_keeps_its_tracked_matte() {
|
||||
for base in [0, 4] {
|
||||
let first = if base == 0 { 3 } else { 1 };
|
||||
let prior = if base == 0 { 1 } else { 3 };
|
||||
let mut data = RotoData::default();
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: base, radius: 3.0, points: vec![[40.0, 40.0]], object: 0 }, 4, [0, 4]);
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: first, radius: 3.0, points: vec![[15.0, 15.0]], object: 1 }, 4, [0, 4]);
|
||||
data.span = [0, 4];
|
||||
let mut params = params_with(&data);
|
||||
params.values.insert(INPUT_KEY.into(), Value::Str(format!("earlier-object-correction-{base}")));
|
||||
let mut chain = Chain::new(¶ms, [100.0, 80.0], 25.0, 1.0);
|
||||
chain.model = Some(Arc::new(TwoObjects(Default::default())));
|
||||
let seed = seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id);
|
||||
chain.keys = data.chain_keys(seed);
|
||||
let image = |f: i64| {
|
||||
let mut img = frame(f as f64 / 25.0);
|
||||
// Match the second object's tracked square, so the carry has a real image edge.
|
||||
for y in 8..22 {
|
||||
for x in 8..22 {
|
||||
img.set(x, y, [0.95, 0.9, 0.1, 1.0]);
|
||||
}
|
||||
}
|
||||
Arc::new(img)
|
||||
};
|
||||
assert_eq!(chain.track(&mut |f| Some(image(f)), &|| false, &mut |_| {}).unwrap().unwrap().frames, 5);
|
||||
let prior_key = chain.keys[&prior];
|
||||
assert!(chain.object_seg(prior, 1).is_some());
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame: 2, radius: 1.0, points: vec![[19.0, 15.0]], object: 1 }, 4, [0, 4]);
|
||||
chain.data = Arc::new(data.clone());
|
||||
chain.keys = data.chain_keys(seed);
|
||||
assert_eq!(chain.keys[&prior], prior_key, "the nearer tracked frame is still valid");
|
||||
let fake = Arc::new(ClickSquares::default());
|
||||
chain.model = Some(fake.clone());
|
||||
let mut prompt_reads = vec![];
|
||||
let mut get = |f: i64| {
|
||||
if fake.0.lock().unwrap().is_empty() {
|
||||
prompt_reads.push(f);
|
||||
}
|
||||
Some(image(f))
|
||||
};
|
||||
chain.track(&mut get, &|| false, &mut |_| {}).unwrap();
|
||||
assert_eq!(prompt_reads, [2, prior, first, 2], "corrections carry from the valid tracked matte in either direction");
|
||||
let boxes = fake.0.lock().unwrap().clone();
|
||||
let correction = boxes[1].iter().find(|(g, _)| *g == 2).and_then(|(_, b)| *b).expect("a correction box");
|
||||
assert!(correction[0] <= 10.0 && correction[2] >= 22.0, "the box keeps the whole tracked square, not only the correction: {correction:?}");
|
||||
assert!(correction[2] < 30.0, "the other object's disk stays outside this box: {correction:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrections_before_the_base_carry_on_from_each_other() {
|
||||
let mut data = RotoData::default();
|
||||
for (frame, object) in [(3, 0), (3, 1), (2, 0), (1, 0)] {
|
||||
data.add(Stroke { kind: StrokeKind::Fg, frame, radius: 3.0, points: vec![[35.0, 40.0]], object }, 3, [0, 3]);
|
||||
}
|
||||
data.span = [0, 3];
|
||||
assert_eq!(data.base, Some(3));
|
||||
let mut params = params_with(&data);
|
||||
params.values.insert(INPUT_KEY.into(), Value::Str("corrections-before-the-base".into()));
|
||||
let mut chain = Chain::new(¶ms, [100.0, 80.0], 25.0, 1.0);
|
||||
let fake = Arc::new(ClickSquares::default());
|
||||
chain.model = Some(fake.clone());
|
||||
chain.keys = data.chain_keys(seed_with(¶ms, [100.0, 80.0], 25.0, effectcraft_segment::SAM2_TINY.id));
|
||||
let mut prompt_reads = vec![];
|
||||
let mut get = |f: i64| {
|
||||
if fake.0.lock().unwrap().is_empty() {
|
||||
prompt_reads.push(f);
|
||||
}
|
||||
Some(Arc::new(frame(f as f64 / 25.0)))
|
||||
};
|
||||
chain.track(&mut get, &|| false, &mut |_| {}).unwrap();
|
||||
assert_eq!(prompt_reads, [3, 2, 3, 1, 2, 3], "object 0: the base, then 2 from 3, then 1 from 2; object 1: the base");
|
||||
let frames: Vec<Vec<i64>> = fake.0.lock().unwrap().iter().map(|o| o.iter().map(|q| q.0).collect()).collect();
|
||||
assert_eq!(frames, [vec![1, 2, 3], vec![3]], "prompts reach the tracker in frame order");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_segmentation_reads_only_frames_with_that_objects_foreground() {
|
||||
let mut data = RotoData::default();
|
||||
|
||||
@@ -130,6 +130,8 @@ if has appimage; then
|
||||
fi
|
||||
APPDIR="$WORK/EffectCraft.AppDir"
|
||||
cp -R "$STAGE" "$APPDIR"
|
||||
# Keep sctk-adwaita's MIT notice in the AppImage.
|
||||
install -Dm644 "$ROOT/NOTICE" "$APPDIR/usr/share/doc/effectcraft/NOTICE"
|
||||
ln -s usr/bin/effectcraft "$APPDIR/AppRun"
|
||||
cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop"
|
||||
cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png"
|
||||
|
||||
Reference in new issue
Block a user