Merge main into #840

# Conflicts:
#	apps/figcraft-cli/src/files.rs
#	apps/figcraft-cli/src/main.rs
#	crates/engine/src/cmd/file.rs
#	crates/engine/src/lib.rs
#	crates/ui-egui/locales/en.toml
#	crates/ui-egui/locales/es.toml
#	crates/ui-egui/src/export_panel.rs
#	crates/ui-egui/src/export_preview_tests.rs
#	docs/file-format-parity.md
#	docs/mcp.md
#	docs/roadmap.md
#	docs/ui-parity.md
This commit is contained in:
bflatastic
2026-10-26 23:23:07 +07:00
581 changed files with 66949 additions and 5373 deletions
+6 -2
View File
@@ -67,11 +67,15 @@ pub fn generate(root: &Path) -> bool {
for png in &pngs {
ico.extend_from_slice(png);
}
ok &= std::fs::write(dir.join("figcraft.ico"), ico).is_ok();
let ico_path = dir.join("figcraft.ico");
let _ = std::fs::remove_file(&ico_path);
ok &= std::fs::write(&ico_path, ico).is_ok();
// The lighter trace keeps the scalable theme icon cheap to render.
let sd = dir.join("hicolor/scalable/apps");
let _ = std::fs::create_dir_all(&sd);
ok &= std::fs::copy(dir.join("figcraft-small.svg"), sd.join("ai.storyteller.figcraft.svg")).is_ok();
let target = sd.join("ai.storyteller.figcraft.svg");
let _ = std::fs::remove_file(&target);
ok &= std::fs::copy(dir.join("figcraft-small.svg"), &target).is_ok();
eprintln!("icon: wrote {}", dir.display());
ok
}
+44 -15
View File
@@ -1,6 +1,9 @@
//! Project tasks: `cargo xtask <task>`.
//!
//! - `ci` fmt check, clippy -D warnings, tests, layers, assets, wasm check
//! - `ci` fmt check, clippy -D warnings, tests, the translation report, layers, assets, wasm check
//! - `test` the workspace tests (nextest when installed) and doctests
//! - `i18n-report` the UI strings, command labels and menu wordings each language lacks (never fails
//! on them: a missing translation reads English)
//! - `layers` crate dependency layering check
//! - `assets` every file under assets/ and docs/ has a row in ATTRIBUTION.md
//! - `parity` menu-catalog coverage → docs/parity-checklist.md
@@ -16,7 +19,7 @@
mod icon;
mod layers;
use std::process::{Command, ExitCode};
use std::process::{Command, ExitCode, Stdio};
fn run(cmd: &mut Command) -> bool {
eprintln!("$ {cmd:?}");
@@ -28,7 +31,7 @@ fn cargo() -> Command {
}
fn root() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(|p| p.to_path_buf()).unwrap_or_default()
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(std::path::Path::to_path_buf).unwrap_or_default()
}
fn main() -> ExitCode {
@@ -36,6 +39,8 @@ fn main() -> ExitCode {
let task = args.first().map(String::as_str).unwrap_or("help");
let ok = match task {
"ci" => ci(),
"test" => test(),
"i18n-report" => i18n_report(),
"layers" => layers::check(&root()),
"assets" => assets(),
"parity" => parity(),
@@ -45,7 +50,7 @@ fn main() -> ExitCode {
"plugins" => plugins(),
"bench" => bench(args.get(1).map_or("1000,10000,50000", String::as_str)),
_ => {
eprintln!("tasks: ci, layers, assets, parity, version [set X.Y.Z], icon, wasm, plugins, bench [SIZES]");
eprintln!("tasks: ci, test, i18n-report, layers, assets, parity, version [set X.Y.Z], icon, wasm, plugins, bench [SIZES]");
task == "help"
}
};
@@ -54,16 +59,38 @@ fn main() -> ExitCode {
fn ci() -> bool {
run(cargo().args(["fmt", "--all", "--check"]))
&& run(cargo().args(["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]))
&& run(cargo().args(["test", "--workspace"]))
&& run(cargo().args(["clippy", "--workspace", "--all-targets", "--keep-going", "--", "-D", "warnings"]))
&& test()
&& i18n_report()
&& layers::check(&root())
&& assets()
&& wasm()
}
/// Unit and integration tests with nextest when it is installed (`cargo install cargo-nextest`),
/// else with `cargo test`; then the doctests, which nextest does not run. Examples are not built:
/// clippy's `--all-targets` already checks them.
fn test() -> bool {
let nextest = cargo().args(["nextest", "--version"]).stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok_and(|s| s.success());
let units = if nextest {
run(cargo().args(["nextest", "run", "--workspace", "--lib", "--bins", "--tests"]))
} else {
eprintln!("xtask: cargo-nextest not installed; running unit and integration tests with `cargo test`");
run(cargo().args(["test", "--workspace", "--lib", "--bins", "--tests"]))
};
units && run(cargo().args(["test", "--workspace", "--doc"]))
}
/// Prints what each language's catalog lacks, from the test that lists it. `--workspace --lib`
/// reuses the binaries `test` built.
fn i18n_report() -> bool {
run(cargo().args(["test", "-q", "--workspace", "--lib", "--", "--exact", "i18n::tests::missing_translations_report", "--nocapture"]))
}
fn wasm() -> bool {
run(cargo().args([
"check",
"--keep-going",
"--target",
"wasm32-unknown-unknown",
"-p",
@@ -81,30 +108,31 @@ fn wasm() -> bool {
/// The example plugins in `plugins/examples` (each folder's name is its package; its module is
/// `<name with _>.wasm`).
const EXAMPLE_PLUGINS: &[&str] = &["tidy-names", "swatch-grid", "counter"];
const EXAMPLE_PLUGINS: &[&str] = &["tidy-names", "swatch-grid", "counter", "export-names"];
/// The plugin workspace: fmt, clippy and build for wasm32, host tests, then each example's manifest
/// and module side by side in `<target>/plugins/dist/<example>/`.
fn plugins() -> bool {
let root = root();
let manifest = root.join("plugins").join("Cargo.toml");
let target = std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| root.join("target")).join("plugins");
let target = std::env::var_os("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|| root.join("target"));
let ws = |args: &[&str]| {
let mut c = cargo();
c.args(args).arg("--manifest-path").arg(&manifest);
// Keep the caller's target; Cargo separates host and wasm artifacts itself.
c.args(args).arg("--manifest-path").arg(&manifest).env("CARGO_TARGET_DIR", &target);
c
};
let wasm = ["--target", "wasm32-unknown-unknown", "--locked"];
let ok = run(&mut ws(&["fmt", "--all", "--check"]))
&& run(ws(&["clippy", "--workspace", "--release"]).args(wasm).arg("--target-dir").arg(&target).args(["--", "-D", "warnings"]))
&& run(ws(&["test", "--workspace", "--locked"]).arg("--target-dir").arg(&target))
&& run(ws(&["build", "--workspace", "--release"]).args(wasm).arg("--target-dir").arg(&target));
&& run(ws(&["clippy", "--workspace", "--release"]).args(wasm).args(["--", "-D", "warnings"]))
&& run(&mut ws(&["test", "--workspace", "--locked"]))
&& run(ws(&["build", "--workspace", "--release"]).args(wasm));
if !ok {
return false;
}
for name in EXAMPLE_PLUGINS {
let module = format!("{}.wasm", name.replace('-', "_"));
let dist = target.join("dist").join(name);
let dist = target.join("plugins/dist").join(name);
let copied = std::fs::create_dir_all(&dist)
.and_then(|()| std::fs::copy(root.join("plugins/examples").join(name).join("figcraft-plugin.json"), dist.join("figcraft-plugin.json")))
.and_then(|_| std::fs::copy(target.join("wasm32-unknown-unknown/release").join(&module), dist.join(&module)));
@@ -174,10 +202,11 @@ fn assets() -> bool {
fn walk(dir: &std::path::Path, f: &mut dyn FnMut(&std::path::Path)) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let Ok(ft) = e.file_type() else { continue };
let p = e.path();
if p.is_dir() {
if ft.is_dir() && !ft.is_symlink() {
walk(&p, f);
} else {
} else if !ft.is_dir() {
f(&p);
}
}