Merge remote-tracking branch 'upstream/main' into perf/exr-output-options

This commit is contained in:
vangel76 committed 2026-11-03 01:52:11 +01:00
commit 32871be132
6 files changed
+405 -56

No files matched your search

Generated
+76 -39
View File
@@ -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
View File
@@ -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"
+31
View File
@@ -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),
+3
View File
@@ -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
View File
@@ -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(&params, [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(&params, [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(&params, [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(&params, [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(&params, [100.0, 80.0], 25.0, 1.0);
chain.model = Some(Arc::new(TwoObjects(Default::default())));
chain.keys = data.chain_keys(seed_with(&params, [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(&params, [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(&params, [100.0, 80.0], 25.0, 1.0);
chain.model = Some(Arc::new(TwoObjects(Default::default())));
let seed = seed_with(&params, [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(&params, [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(&params, [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();
+2
View File
@@ -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"