mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 20:36:55 -06:00
A new L4 crate that writes layers as an Android Vector Drawable (XML): shapes, booleans and text as baked path data, solid and gradient fills (linear, radial and sweep, exact for any handle frame via a group and a 2x2 SVD), native strokes where Android draws them the same and filled stroke outlines otherwise, clip frames and masks as <clip-path> groups. What a drawable can't hold is governed by Compat (bestEffort / exact / ignore): layer opacity over overlapping shapes, even-odd fills and clips (rewritten to non-zero via pathops), diamond gradients, alpha masks; effects, image and pattern fills and blend modes are left out with a warning. Output is pretty-printed, numbers are finite and short, names (android:name) are optional, escaped and unique.
77 lines
2.8 KiB
Rust
77 lines
2.8 KiB
Rust
//! Crate layering: a crate may depend only on lower layers (or earlier crates of its own layer
|
|
//! listed in ORDER). Nothing below the UI depends on egui/eframe.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::process::Command;
|
|
|
|
/// (crate suffix, layer)
|
|
const TABLE: &[(&str, u8)] = &[
|
|
("geom", 0),
|
|
("color", 0),
|
|
("doc", 1),
|
|
("layout", 2),
|
|
("text", 2),
|
|
("pathops", 2),
|
|
("collab", 2),
|
|
("plugin", 2),
|
|
("render", 3),
|
|
("format", 3),
|
|
("svg", 4),
|
|
("pdf", 4),
|
|
("vectordrawable", 4),
|
|
("engine", 5),
|
|
("ui-egui", 6),
|
|
("mcp", 6),
|
|
// Live sessions over the network (WebSockets): above the engine, beside the UI.
|
|
("collab-net", 6),
|
|
];
|
|
|
|
/// Networking crates: only L6 (`collab-net`) may use them.
|
|
const NETWORK: &[&str] = &["tungstenite", "tokio", "tokio-tungstenite", "mio", "socket2"];
|
|
|
|
pub fn check(root: &std::path::Path) -> bool {
|
|
let out = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
|
|
.args(["metadata", "--format-version", "1", "--no-deps"])
|
|
.current_dir(root)
|
|
.output();
|
|
let Ok(out) = out else { return false };
|
|
let Ok(v) = serde_json::from_slice::<serde_json::Value>(&out.stdout) else { return false };
|
|
let layer: BTreeMap<String, u8> = TABLE.iter().map(|(n, l)| (format!("figcraft-{n}"), *l)).collect();
|
|
let mut ok = true;
|
|
for pkg in v["packages"].as_array().cloned().unwrap_or_default() {
|
|
let name = pkg["name"].as_str().unwrap_or("").to_string();
|
|
let Some(&l) = layer.get(&name) else { continue };
|
|
for dep in pkg["dependencies"].as_array().cloned().unwrap_or_default() {
|
|
let dn = dep["name"].as_str().unwrap_or("");
|
|
if dep["kind"].as_str() == Some("dev") {
|
|
continue;
|
|
}
|
|
if l < 6 && (dn == "egui" || dn == "eframe" || dn == "rfd" || dn == "winit") {
|
|
eprintln!("layers: {name} (L{l}) depends on UI crate {dn}");
|
|
ok = false;
|
|
}
|
|
if l < 6 && NETWORK.contains(&dn) {
|
|
eprintln!("layers: {name} (L{l}) depends on networking crate {dn}");
|
|
ok = false;
|
|
}
|
|
if let Some(&dl) = layer.get(dn)
|
|
&& dl >= l
|
|
&& !(dl == l && same_layer_ok(&name, dn))
|
|
{
|
|
eprintln!("layers: {name} (L{l}) may not depend on {dn} (L{dl})");
|
|
ok = false;
|
|
}
|
|
}
|
|
}
|
|
if ok {
|
|
eprintln!("layers: ok");
|
|
}
|
|
ok
|
|
}
|
|
|
|
fn same_layer_ok(from: &str, to: &str) -> bool {
|
|
// Within L0: color uses geom types; within L2: text/layout/pathops/collab are independent;
|
|
// within L6 the MCP server joins live sessions through collab-net.
|
|
matches!((from, to), ("figcraft-color", "figcraft-geom") | ("figcraft-render", "figcraft-format") | ("figcraft-mcp", "figcraft-collab-net"))
|
|
}
|