diff --git a/Cargo.lock b/Cargo.lock
index 79498923d..232fa6b18 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1094,6 +1094,16 @@ name = "craft-docpath"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
+[[package]]
+name = "craft-ecproj-format"
+version = "0.1.0"
+source = "git+https://github.com/storytold/craft-libs?rev=ca1b5e2edb8568335c6c14f390307bd5beae8238#ca1b5e2edb8568335c6c14f390307bd5beae8238"
+dependencies = [
+ "serde",
+ "serde_json",
+ "thiserror 2.0.21",
+]
+
[[package]]
name = "craft-fonts-build"
version = "0.1.0"
@@ -1402,7 +1412,7 @@ dependencies = [
[[package]]
name = "effectcraft"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"cpal",
"craft-update",
@@ -1428,7 +1438,7 @@ dependencies = [
[[package]]
name = "effectcraft-automation"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-engine",
"effectcraft-host",
@@ -1441,14 +1451,14 @@ dependencies = [
[[package]]
name = "effectcraft-av1enc"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"filmcraft-av1",
]
[[package]]
name = "effectcraft-cli"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"cpu-time",
"effectcraft-automation",
@@ -1467,7 +1477,7 @@ dependencies = [
[[package]]
name = "effectcraft-color"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"proptest",
"serde",
@@ -1475,7 +1485,7 @@ dependencies = [
[[package]]
name = "effectcraft-effects"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"chrono",
"effectcraft-color",
@@ -1493,7 +1503,7 @@ dependencies = [
[[package]]
name = "effectcraft-engine"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"chrono",
"craft-docpath",
@@ -1531,7 +1541,7 @@ dependencies = [
[[package]]
name = "effectcraft-export"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-av1enc",
"effectcraft-color",
@@ -1545,6 +1555,7 @@ dependencies = [
"effectcraft-render",
"effectcraft-time",
"effectcraft-vp9enc",
+ "exr",
"filmcraft-aac",
"filmcraft-h264enc",
"filmcraft-isobmff",
@@ -1561,7 +1572,7 @@ dependencies = [
[[package]]
name = "effectcraft-expr"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"boa_engine",
"effectcraft-color",
@@ -1578,7 +1589,7 @@ dependencies = [
[[package]]
name = "effectcraft-geom"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"proptest",
"serde",
@@ -1586,7 +1597,7 @@ dependencies = [
[[package]]
name = "effectcraft-gpu"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"bytemuck",
"effectcraft-color",
@@ -1601,6 +1612,7 @@ dependencies = [
"js-sys",
"log",
"pollster 1.0.1",
+ "rayon",
"wasm-bindgen",
"web-time",
"wgpu",
@@ -1608,14 +1620,14 @@ dependencies = [
[[package]]
name = "effectcraft-hevcenc"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"filmcraft-hevc",
]
[[package]]
name = "effectcraft-host"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-engine",
"effectcraft-export",
@@ -1640,7 +1652,7 @@ dependencies = [
[[package]]
name = "effectcraft-interchange"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-color",
"effectcraft-keyframe",
@@ -1659,7 +1671,7 @@ dependencies = [
[[package]]
name = "effectcraft-keyframe"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-color",
"effectcraft-geom",
@@ -1671,7 +1683,7 @@ dependencies = [
[[package]]
name = "effectcraft-lottie"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"craft-docpath",
"effectcraft-color",
@@ -1689,7 +1701,7 @@ dependencies = [
[[package]]
name = "effectcraft-media"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"craft-docpath",
"effectcraft-color",
@@ -1721,7 +1733,7 @@ dependencies = [
[[package]]
name = "effectcraft-model"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-geom",
"image",
@@ -1734,26 +1746,26 @@ dependencies = [
[[package]]
name = "effectcraft-opusenc"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"filmcraft-opus",
]
[[package]]
name = "effectcraft-path"
-version = "0.19.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.19.0"
+version = "0.21.0"
dependencies = [
"craft-pdf",
"effectcraft-raster",
@@ -1769,7 +1781,7 @@ dependencies = [
[[package]]
name = "effectcraft-plugin"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-effects",
"effectcraft-keyframe",
@@ -1781,8 +1793,9 @@ dependencies = [
[[package]]
name = "effectcraft-project"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
+ "craft-ecproj-format",
"effectcraft-color",
"effectcraft-geom",
"effectcraft-keyframe",
@@ -1794,14 +1807,14 @@ dependencies = [
[[package]]
name = "effectcraft-psd"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"miniz_oxide 0.9.1",
]
[[package]]
name = "effectcraft-raster"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-color",
"effectcraft-geom",
@@ -1810,7 +1823,7 @@ dependencies = [
[[package]]
name = "effectcraft-render"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-color",
"effectcraft-effects",
@@ -1832,7 +1845,7 @@ dependencies = [
[[package]]
name = "effectcraft-sam2"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"bytemuck",
"candle-core",
@@ -1847,7 +1860,7 @@ dependencies = [
[[package]]
name = "effectcraft-script"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"boa_engine",
"effectcraft-engine",
@@ -1865,7 +1878,7 @@ dependencies = [
[[package]]
name = "effectcraft-segment"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"miniz_oxide 0.9.1",
"ndarray",
@@ -1874,7 +1887,7 @@ dependencies = [
[[package]]
name = "effectcraft-svg"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-geom",
"effectcraft-path",
@@ -1886,7 +1899,7 @@ dependencies = [
[[package]]
name = "effectcraft-text"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-keyframe",
"harfrust",
@@ -1898,7 +1911,7 @@ dependencies = [
[[package]]
name = "effectcraft-time"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"proptest",
"serde",
@@ -1906,7 +1919,7 @@ dependencies = [
[[package]]
name = "effectcraft-track"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-raster",
"effectcraft-segment",
@@ -1918,7 +1931,7 @@ dependencies = [
[[package]]
name = "effectcraft-ui-egui"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"chrono",
"craft-quit",
@@ -1954,11 +1967,11 @@ dependencies = [
[[package]]
name = "effectcraft-vp9enc"
-version = "0.19.0"
+version = "0.21.0"
[[package]]
name = "effectcraft-web"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"effectcraft-engine",
"effectcraft-export",
@@ -5369,6 +5382,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"
@@ -6098,6 +6122,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"
@@ -6109,7 +6147,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]]
@@ -7418,6 +7467,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",
@@ -7575,7 +7625,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
-version = "0.19.0"
+version = "0.21.0"
dependencies = [
"chrono",
"pkg-config",
diff --git a/Cargo.toml b/Cargo.toml
index eae73bbca..f5f84775f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/*"]
[workspace.package]
-version = "0.19.0"
+version = "0.21.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -14,6 +14,7 @@ repository = "https://github.com/storytold/effectcraft"
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false, features = ["read"] }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
+craft-ecproj-format = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "ca1b5e2edb8568335c6c14f390307bd5beae8238" }
# craft-update: app-specific pretend versions apply to startup and manual checks.
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "c5a1cbfb5f9049f9270955caffb5bee5997aebf7", features = ["egui"] }
# internal
diff --git a/NOTICE b/NOTICE
index 5cf18a553..4a79e69fd 100644
--- a/NOTICE
+++ b/NOTICE
@@ -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),
diff --git a/README.md b/README.md
index 12970b8ba..81b5a0ba5 100644
--- a/README.md
+++ b/README.md
@@ -344,8 +344,17 @@ stand on its own.
|
| **FilmCraft** | Video editing, color and sound | [GitHub](https://github.com/storytold/filmcraft) | [Website](https://getartcraft.com/apps/filmcraft) |
|
| **LightCraft** | Photo library and raw development | [GitHub](https://github.com/storytold/lightcraft) | [Website](https://getartcraft.com/apps/lightcraft) |
|
| **PdfCraft** | Reading, organizing and protecting PDFs | [GitHub](https://github.com/storytold/pdfcraft) | [Website](https://getartcraft.com/apps/pdfcraft) |
-|
| **EffectCraft** | **Motion graphics and visual effects · you are here** | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
+|
| **EffectCraft** | Motion graphics and visual effects | [GitHub](https://github.com/storytold/effectcraft) | [Website](https://getartcraft.com/apps/effectcraft) |
|
| **DesignCraft** | Page layout and publishing | [GitHub](https://github.com/storytold/designcraft) | [Website](https://getartcraft.com/apps/designcraft) |
+|
| **FigCraft** | Interface design and prototyping | [GitHub](https://github.com/storytold/figcraft) | [Website](https://getartcraft.com/apps/figcraft) |
+|
| **WordCraft** | Word processing and documents | [GitHub](https://github.com/storytold/wordcraft) | [Website](https://getartcraft.com/apps/wordcraft) |
+|
| **SoundCraft** | Audio recording and editing | [GitHub](https://github.com/storytold/soundcraft) | [Website](https://getartcraft.com/apps/soundcraft) |
+|
| **DeckCraft** | Presentations and slide decks | [GitHub](https://github.com/storytold/deckcraft) | [Website](https://getartcraft.com/apps/deckcraft) |
+|
| **CADCraft** | Computer-aided design | [GitHub](https://github.com/storytold/cadcraft) | [Website](https://getartcraft.com/apps/cadcraft) |
+|
| **GridCraft** | Spreadsheets and data | [GitHub](https://github.com/storytold/gridcraft) | [Website](https://getartcraft.com/apps/gridcraft) |
+|
| **TextureCraft** | 3D texture painting | [GitHub](https://github.com/storytold/texturecraft) | [Website](https://getartcraft.com/apps/texturecraft) |
+|
| **Craft Anything** | See how any software works, then build your own | [GitHub](https://github.com/storytold/craft-anything) | [Website](https://getartcraft.com/apps/craft-anything) |
+|
| **SolidCraft** | Parametric 3D CAD by Tangency Labs (community app) | [GitHub](https://github.com/tangency-labs/solidcraft) | |
And [**ArtCraft**](https://getartcraft.com/) itself, our AI image and video studio for artists who want real control.
diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs
index 6ba352247..98bcbb598 100644
--- a/apps/effectcraft-cli/src/main.rs
+++ b/apps/effectcraft-cli/src/main.rs
@@ -47,6 +47,7 @@
use std::io::Write;
+mod project_lock;
mod template;
use effectcraft_automation::tools::{self, Reply};
@@ -508,6 +509,7 @@ fn script_cmd(args: &Args, json_out: bool) -> Result<(), Failure> {
(None, Some(f)) => (std::fs::read_to_string(f).map_err(|e| Failure::Error(format!("cannot read {f}: {e}")))?, f.clone()),
(None, None) => return usage_err("script needs a .jsx/.js file or --eval CODE"),
};
+ let _lock = project_lock::for_save(args)?;
let mut b = backend(args, false)?;
let r = b.exec("script.run", json!({"code": code, "name": name}))?;
let saved = maybe_save(&mut b, args)?;
@@ -611,6 +613,7 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
(Some(p), _) | (None, Some(p)) => serde_json::from_str(p).map_err(|e| Failure::Usage(format!("params are not valid JSON: {e}")))?,
(None, None) => json!({}),
};
+ let _lock = project_lock::for_save(args)?;
let mut b = backend(args, true)?;
let r = b.exec(&id, params)?;
if let Some(e) = failed_render(&id, &r) {
@@ -654,6 +657,7 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
if steps.is_empty() {
return Err(Failure::Usage(format!("--commands {}: no commands", sources.join(", "))));
}
+ let _lock = project_lock::for_save(args)?;
let mut b = backend(args, true)?;
let mut results = vec![];
// The step's index closes the message: a generated list repeats ids, and callers match
@@ -690,7 +694,7 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
if let Some(e) = p.get("expression").and_then(Value::as_str) {
extra += &format!(" expr: {e}");
}
- say!(" {:<44} {:<8} {}{extra}", p["path"].as_str().unwrap_or(""), p["type"].as_str().unwrap_or(""), p["value"]);
+ say!(" {:<44} {:<8} {}{extra}", p["path"].as_str().unwrap_or(""), p["type"].as_str().unwrap_or(""), value_text(p));
}
}
}
@@ -708,6 +712,7 @@ fn run(cmd: &str, args: &Args, json_out: bool) -> Result<(), Failure> {
}
let (comp, layer, path) = (args.pos[0].clone(), args.pos[1].clone(), args.pos[2].clone());
let value = args.pos.get(3).map(|v| value_arg(v));
+ let _lock = project_lock::for_save(args)?;
let mut b = backend(args, true)?;
let a =
json!({"comp": reference(&comp), "layer": reference(&layer), "path": path, "value": value, "time": args.num("--time")?, "expression": expr});
@@ -1539,6 +1544,16 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
Ok(())
}
+/// A flat property's value for the text listing: an enum's number with the label `options` gives it
+/// (#2928), since the numbers are 0-based where After Effects scripting counts from 1.
+fn value_text(p: &Value) -> String {
+ let label = p["options"].as_array().and_then(|o| o.iter().find(|o| o["value"] == p["value"])).and_then(|o| o["label"].as_str());
+ match label {
+ Some(l) => format!("{} ({l})", p["value"]),
+ None => p["value"].to_string(),
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/apps/effectcraft-cli/src/project_lock.rs b/apps/effectcraft-cli/src/project_lock.rs
new file mode 100644
index 000000000..234bbfd33
--- /dev/null
+++ b/apps/effectcraft-cli/src/project_lock.rs
@@ -0,0 +1,150 @@
+//! One saving run at a time per project file.
+//!
+//! A `--save` run reads the project, edits it and writes the whole file back, so two at once would
+//! silently drop the edits of whichever saved first (#2851). Each run holds an OS lock from before
+//! its read until it exits; the next one waits, then reads what the first one saved. This is how
+//! filmcraft-cli does it (storytold/filmcraft#3926).
+//!
+//! The lock is on a file next to the project, not on the project: a save replaces the project's
+//! inode, and on Windows a locked file can't be read by other processes. The OS drops the lock when
+//! the process ends, however it ends, so a crash leaves nothing stale. The file itself stays, since
+//! removing it would let one run wait on the old file while another locks a new one.
+
+use crate::{Args, Failure};
+use std::fs::{File, TryLockError};
+use std::io::Write;
+use std::path::{Path, PathBuf};
+
+/// Most bytes of the project's file name kept in the lock's name; the rest of the 255-byte limit
+/// is left for the leading dot and `.lock`.
+const MAX_NAME_BYTES: usize = 200;
+
+fn lock_path(project: &Path) -> PathBuf {
+ let name = project.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
+ project.with_file_name(format!(".{}.lock", &name[..name.floor_char_boundary(MAX_NAME_BYTES)]))
+}
+
+/// Do two paths name one existing file?
+fn same_file(a: &str, b: &str) -> bool {
+ a == b || matches!((std::fs::canonicalize(a), std::fs::canonicalize(b)), (Ok(a), Ok(b)) if a == b)
+}
+
+/// The path this run saves over, when that is the file it opens. `--save-as` somewhere else replaces
+/// that file whatever it held, so there is no edit of its to lose.
+///
+/// The lock goes by this path as written, not by where a symlink points: runs that name the
+/// project the same way share one lock.
+///
+/// A missing project is an error here, before anything else, the same one opening it would give.
+/// If the run went on, another one could create the project before it is read, and this run would
+/// edit and save it with no lock. Checking first also means the paths compared below exist, so
+/// `canonicalize` tells exactly whether they are one file, however they are spelt. A project that
+/// exists but isn't a file gets no lock file beside it, and opening it reports why.
+fn saved_in_place(a: &Args) -> Result