mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 11:17:12 -04:00
M0.1: cargo xtask (layers, assets, wasm, ci); attribute ArtCraft brand marks
Layer table matches the real dependency graph; wasm check passes for all L0-L4 crates and the egui UI. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
dee16181fe
commit
9f0cfb3373
14 files changed
+348
-1
No files matched your search
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --quiet --package xtask --"
|
||||
@@ -43,6 +43,11 @@ reproduce an image (for example, point lists traced from someone else's icon).
|
||||
`crates/engine/src/demo.rs`, generators in `crates/effects`) are original work under the project licence
|
||||
and are listed in `ATTRIBUTION.md`.
|
||||
7. **When in doubt, leave it out** and draw or generate it yourself.
|
||||
8. **The one exception: first-party ArtCraft brand marks.** The ArtCraft name and logos in `docs/brand/`
|
||||
(supplied by the ArtCraft team) are trademarks, all rights reserved, used with permission. They still
|
||||
need a sidecar and an `ATTRIBUTION.md` row (licence `LicenseRef-ArtCraft-Trademark`). No other
|
||||
non-open asset is allowed, and this exception never covers third-party marks (Adobe, Discord, GitHub
|
||||
and other logos stay out; draw a generic icon instead).
|
||||
|
||||
## 2. Clean-room code
|
||||
|
||||
|
||||
@@ -21,3 +21,19 @@ iconography, images or other Adobe assets; open licences only; every asset attri
|
||||
|
||||
- UI icons drawn in code: `crates/ui-egui/src/icons.rs`.
|
||||
- Procedural demo content (solids, shapes, gradients, text animations): `crates/engine/src/demo.rs`.
|
||||
|
||||
## First-party brand marks
|
||||
|
||||
The ArtCraft name and logo are trademarks of the ArtCraft team, used in EffectCraft with permission. They are not
|
||||
openly licensed and are not covered by the project licence; forks must remove or replace them (AGENTS.md §1.8).
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| `docs/brand/artcraft-logo-white.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-logo-white.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark, light ink for dark backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-logo.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-logo.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-mark-black.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-mark-black.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), black) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-mark.png` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
| `docs/brand/artcraft-mark.svg` | ArtCraft team | Original work: first-party ArtCraft brand mark (ArtCraft symbol ("A" mark), blue) | ArtCraft trademark, all rights reserved; used with permission (not MIT/Apache) |
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/*", "apps/*"]
|
||||
members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/*"]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-logo-white.png
|
||||
title: ArtCraft wordmark, light ink for dark backgrounds
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-logo-white.svg
|
||||
title: ArtCraft wordmark, light ink for dark backgrounds
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-logo.png
|
||||
title: ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-logo.svg
|
||||
title: ArtCraft wordmark (symbol + "ARTCRAFT"), dark ink for light backgrounds
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-mark-black.png
|
||||
title: ArtCraft symbol ("A" mark), black
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-mark-black.svg
|
||||
title: ArtCraft symbol ("A" mark), black
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-mark.png
|
||||
title: ArtCraft symbol ("A" mark), blue
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,8 @@
|
||||
asset: artcraft-mark.svg
|
||||
title: ArtCraft symbol ("A" mark), blue
|
||||
author: ArtCraft team (Storyteller)
|
||||
source: original work (first-party ArtCraft brand asset supplied by the ArtCraft team)
|
||||
license: LicenseRef-ArtCraft-Trademark — ArtCraft name and logo are trademarks of the ArtCraft team, all rights reserved; used in EffectCraft with permission. Not covered by the project's MIT OR Apache-2.0 licence.
|
||||
license-file:
|
||||
added: 2026-10-01 by Claude (agent) for Brandon Thomas
|
||||
notes: First-party ArtCraft brand mark kept in docs/brand for the README and brand use: the one exception to the open-licence rule (AGENTS.md §1.8). Forks and redistributions must remove or replace it unless they have permission.
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
@@ -0,0 +1,251 @@
|
||||
//! Workspace automation: `cargo xtask <layers|assets|wasm|ci>`.
|
||||
//!
|
||||
//! - `layers`: enforces the dependency layering of `docs/architecture.md` §1 (downward-only edges,
|
||||
//! listed same-layer edges, no UI/OS crates below L5).
|
||||
//! - `wasm`: `cargo check --target wasm32-unknown-unknown` for every crate in L0–L4 and the egui UI.
|
||||
//! - `assets`: every asset file (image, icon, font, LUT, audio, video…) has a complete
|
||||
//! `<file>.attribution` sidecar and an entry in `ATTRIBUTION.md` (AGENTS.md §1).
|
||||
//! - `ci`: fmt check, clippy -D warnings, tests, layers, assets, wasm.
|
||||
|
||||
use std::process::{Command, ExitCode};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// (crate name without the `effectcraft-` prefix, layer). See `docs/architecture.md` §1.
|
||||
const LAYERS: &[(&str, u8)] = &[
|
||||
("time", 0),
|
||||
("geom", 0),
|
||||
("color", 0),
|
||||
("testkit", 0),
|
||||
("raster", 1),
|
||||
("keyframe", 1),
|
||||
("path", 1),
|
||||
("project", 2),
|
||||
("text", 2),
|
||||
("effects", 2),
|
||||
("render", 3),
|
||||
("media", 3),
|
||||
("expr", 3),
|
||||
("export", 3),
|
||||
("gpu", 3),
|
||||
("lottie", 3),
|
||||
("format", 3),
|
||||
("engine", 4),
|
||||
("host", 4),
|
||||
("ui-egui", 5),
|
||||
("automation", 5),
|
||||
("effectcraft", 6),
|
||||
("cli", 6),
|
||||
("web", 6),
|
||||
];
|
||||
|
||||
/// Allowed same-layer edges (from, to).
|
||||
const SAME_LAYER: &[(&str, &str)] = &[
|
||||
("path", "keyframe"),
|
||||
("path", "raster"),
|
||||
("text", "path"),
|
||||
("effects", "project"),
|
||||
("effects", "text"),
|
||||
("effects", "path"),
|
||||
("media", "render"),
|
||||
("expr", "render"),
|
||||
("export", "render"),
|
||||
("export", "media"),
|
||||
("gpu", "render"),
|
||||
("lottie", "format"),
|
||||
("host", "engine"),
|
||||
("cli", "effectcraft"),
|
||||
];
|
||||
|
||||
/// Crates that must not appear below L5 (UI toolkits, windowing, OS audio/menus).
|
||||
const UI_ONLY: &[&str] = &["egui", "eframe", "egui-wgpu", "winit", "rfd", "cpal", "muda"];
|
||||
|
||||
fn short(name: &str) -> &str {
|
||||
name.strip_prefix("effectcraft-").unwrap_or(name)
|
||||
}
|
||||
|
||||
fn layer_of(name: &str) -> Option<u8> {
|
||||
LAYERS.iter().find(|(n, _)| *n == short(name)).map(|(_, l)| *l)
|
||||
}
|
||||
|
||||
fn metadata() -> Result<Value, String> {
|
||||
let out = Command::new(env!("CARGO")).args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).into());
|
||||
}
|
||||
serde_json::from_slice(&out.stdout).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn workspace_crates(md: &Value) -> Vec<(String, Vec<String>)> {
|
||||
md["packages"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|p| p["name"] != "xtask")
|
||||
.map(|p| {
|
||||
let deps = p["dependencies"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|d| d["kind"].is_null() || d["kind"] == "build")
|
||||
.filter_map(|d| d["name"].as_str().map(str::to_string))
|
||||
.collect();
|
||||
(p["name"].as_str().unwrap_or_default().to_string(), deps)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn layers() -> Result<(), String> {
|
||||
let md = metadata()?;
|
||||
let mut errors = Vec::new();
|
||||
for (name, deps) in workspace_crates(&md) {
|
||||
let Some(l) = layer_of(&name) else {
|
||||
errors.push(format!("{name}: not assigned a layer (add it to xtask LAYERS and docs/architecture.md §1)"));
|
||||
continue;
|
||||
};
|
||||
for d in &deps {
|
||||
if l < 5 && UI_ONLY.contains(&d.as_str()) {
|
||||
errors.push(format!("{name} (L{l}) depends on UI/OS crate `{d}`"));
|
||||
}
|
||||
if !d.starts_with("effectcraft-") {
|
||||
continue;
|
||||
}
|
||||
let Some(dl) = layer_of(d) else { continue };
|
||||
let (a, b) = (short(&name), short(d));
|
||||
if dl > l {
|
||||
errors.push(format!("{a} (L{l}) depends upward on {b} (L{dl})"));
|
||||
} else if dl == l && l > 0 && !SAME_LAYER.contains(&(a, b)) {
|
||||
errors.push(format!("{a} → {b}: same-layer edge (L{l}) not in the allowed list"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
println!("layers: ok");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
/// File extensions that count as assets (AGENTS.md §1).
|
||||
const ASSET_EXT: &[&str] = &[
|
||||
"png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "icns", "bmp", "tif", "tiff", "heic", "avif", "exr", "ttf", "otf", "ttc", "woff", "woff2", "cube",
|
||||
"3dl", "lut", "look", "wav", "mp3", "aac", "flac", "ogg", "opus", "m4a", "aif", "aiff", "mp4", "mov", "m4v", "mkv", "webm", "avi", "mxf", "psd", "ai",
|
||||
"eps", "pdf", "prproj", "ffx", "prfpset", "mogrt", "aep",
|
||||
];
|
||||
|
||||
/// Sidecar fields that must be present and non-empty.
|
||||
const REQUIRED_FIELDS: &[&str] = &["asset", "title", "author", "source", "license", "added"];
|
||||
|
||||
fn repo_files() -> Result<Vec<String>, String> {
|
||||
let out = Command::new("git").args(["ls-files", "--cached", "--others", "--exclude-standard"]).output().map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(String::from_utf8_lossy(&out.stderr).into());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&out.stdout).lines().map(str::to_string).filter(|f| std::path::Path::new(f).exists()).collect())
|
||||
}
|
||||
|
||||
fn is_asset(path: &str) -> bool {
|
||||
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| ASSET_EXT.contains(&e.to_ascii_lowercase().as_str()))
|
||||
}
|
||||
|
||||
fn assets() -> Result<(), String> {
|
||||
let files = repo_files()?;
|
||||
let index = std::fs::read_to_string("ATTRIBUTION.md").map_err(|e| format!("ATTRIBUTION.md: {e}"))?;
|
||||
let mut errors = Vec::new();
|
||||
let mut n = 0;
|
||||
for f in files.iter().filter(|f| is_asset(f)) {
|
||||
n += 1;
|
||||
let lower = f.to_ascii_lowercase();
|
||||
if lower.contains("adobe") || lower.contains("aftereffects") || lower.contains("after-effects") {
|
||||
errors.push(format!("{f}: asset paths must not reference Adobe/After Effects (AGENTS.md §1)"));
|
||||
}
|
||||
let side = format!("{f}.attribution");
|
||||
match std::fs::read_to_string(&side) {
|
||||
Err(_) => errors.push(format!("{f}: missing attribution sidecar {side}")),
|
||||
Ok(text) => {
|
||||
for field in REQUIRED_FIELDS {
|
||||
let ok = text.lines().any(|l| l.split_once(':').is_some_and(|(k, v)| k.trim() == *field && !v.trim().is_empty()));
|
||||
if !ok {
|
||||
errors.push(format!("{side}: field `{field}` missing or empty"));
|
||||
}
|
||||
}
|
||||
let lic = text.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == "license").map(|(_, v)| v.trim().to_ascii_lowercase()));
|
||||
if lic.is_some_and(|l| l.contains("-nc") || l.contains("-nd") || l.contains("adobe") || l.contains("proprietary")) {
|
||||
errors.push(format!("{side}: licence not allowed (no NC/ND, Adobe or proprietary licences)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !index.contains(&format!("`{f}`")) {
|
||||
errors.push(format!("{f}: not listed in ATTRIBUTION.md"));
|
||||
}
|
||||
}
|
||||
for f in files.iter().filter(|f| f.ends_with(".attribution")) {
|
||||
let asset = f.trim_end_matches(".attribution");
|
||||
if !std::path::Path::new(asset).exists() {
|
||||
errors.push(format!("{f}: sidecar for a file that does not exist"));
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
println!("assets: ok ({n} assets attributed)");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join("\n"))
|
||||
}
|
||||
}
|
||||
|
||||
fn run(cmd: &mut Command) -> Result<(), String> {
|
||||
eprintln!("$ {cmd:?}");
|
||||
let st = cmd.status().map_err(|e| e.to_string())?;
|
||||
if st.success() { Ok(()) } else { Err(format!("failed: {cmd:?}")) }
|
||||
}
|
||||
|
||||
/// Crates above L4 that must also build for the web.
|
||||
const WEB_CRATES: &[&str] = &["effectcraft-ui-egui"];
|
||||
|
||||
fn wasm() -> Result<(), String> {
|
||||
let md = metadata()?;
|
||||
let mut cmd = Command::new(env!("CARGO"));
|
||||
cmd.args(["check", "--target", "wasm32-unknown-unknown"]);
|
||||
let mut n = 0;
|
||||
for (name, _) in workspace_crates(&md) {
|
||||
if layer_of(&name).is_some_and(|l| l <= 4) || WEB_CRATES.contains(&name.as_str()) {
|
||||
cmd.args(["-p", &name]);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
run(&mut cmd)?;
|
||||
println!("wasm: ok ({n} crates)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ci() -> Result<(), String> {
|
||||
let cargo = env!("CARGO");
|
||||
run(Command::new(cargo).args(["fmt", "--check"]))?;
|
||||
run(Command::new(cargo).args(["clippy", "--workspace", "--all-targets", "--release", "--", "-D", "warnings"]))?;
|
||||
run(Command::new(cargo).args(["test", "--workspace", "--release"]))?;
|
||||
layers()?;
|
||||
assets()?;
|
||||
wasm()
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let task = std::env::args().nth(1).unwrap_or_default();
|
||||
let r = match task.as_str() {
|
||||
"layers" => layers(),
|
||||
"wasm" => wasm(),
|
||||
"assets" => assets(),
|
||||
"ci" => ci(),
|
||||
_ => Err("usage: cargo xtask <layers|assets|wasm|ci>".into()),
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user