mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
IO-2/SHP-5/PRV-2: PSD import, SVG import + vector/text conversions, persistent disk cache
- effectcraft-psd (L1): PSD/PSB reader from Adobe's published file format spec (layers, groups, masks, blend modes, text engine data, lfx2 effects, fill/adjustment layers, vector masks; 1/8/16/32-bit, RGB/CMYK/Gray/Lab/Indexed, raw/RLE/ZIP) and a minimal writer for fixtures. - File > Import: importAs footage|composition|compositionLayerSizes, layer (Choose Layer); groups -> precomps (pass-through collapses), text -> editable text, styles -> Layer Styles, adjustments -> adjustment layers with effects; UI asks the Import Kind for Photoshop files. - effectcraft-svg (L2): SVG parser (shapes, paths, transforms, use, CSS, gradients) and rasteriser; .svg imports as footage, continuously rasterised when the switch is on. - layer.create: shapesFromVector, shapesFromText, masksFromText, editableText (stub removed). - Disk cache (render::disk_cache): LZ4 content-keyed frames and layer buffers, LRU size limit from prefs, atomic writes, restart persistence; edit.purge clears it, cache.diskStats query; viewer frames read/write it and the timeline shows disk-only frames in blue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e911cc5c93
commit
94e1c86d89
@@ -0,0 +1,121 @@
|
||||
//! The persistent disk cache (PRV-2) through the session: settings, layer cache persistence
|
||||
//! across restarts, invalidation on edits, purging.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
use crate::render::RenderOpts;
|
||||
|
||||
fn folder(name: &str) -> String {
|
||||
let d = std::env::temp_dir().join(format!("effectcraft-session-disk-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
d.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
fn session(dir: &str) -> Session {
|
||||
let mut s = Session::default();
|
||||
s.execute_checked("prefs.set", json!({"values": {"disk.diskCacheFolder": dir, "disk.diskCacheEnabled": true}})).unwrap();
|
||||
// Write every layer buffer, however quick to render.
|
||||
s.disk_cache.as_ref().unwrap().set_min_layer_ms(0);
|
||||
s
|
||||
}
|
||||
|
||||
fn project(s: &mut Session) -> effectcraft_project::ItemId {
|
||||
s.execute("comp.new", json!({"name": "D", "width": 160, "height": 90, "frameRate": 30, "duration": 1})).unwrap();
|
||||
s.execute("layer.newSolid", json!({"color": "#3080ff", "width": 80, "height": 40})).unwrap();
|
||||
s.execute("effect.apply", json!({"layer": "#1", "effect": "Gaussian Blur"})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": "#1", "path": "effects/#1/blurriness", "value": 12})).unwrap();
|
||||
s.active_comp_id().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disk_cache_settings_persistence_and_invalidation() {
|
||||
let dir = folder("persist");
|
||||
let mut s = session(&dir);
|
||||
let st = s.execute_checked("cache.diskStats", json!({})).unwrap();
|
||||
assert_eq!(st["enabled"], json!(true));
|
||||
assert_eq!(st["maxBytes"], json!(100u64 << 30));
|
||||
let cid = project(&mut s);
|
||||
let t = s.time();
|
||||
let first = s.render(cid, t, RenderOpts::default());
|
||||
let dc = s.disk_cache.clone().unwrap();
|
||||
dc.flush();
|
||||
let written = dc.stats().entries;
|
||||
assert!(written > 0, "layer buffers written");
|
||||
// "Restart": a new session on the same folder and project renders from disk.
|
||||
let saved = s.project.clone();
|
||||
drop(s);
|
||||
let mut s2 = session(&dir);
|
||||
s2.project = saved;
|
||||
s2.bump();
|
||||
s2.open_comp(cid);
|
||||
let dc2 = s2.disk_cache.clone().unwrap();
|
||||
assert_eq!(dc2.stats().entries, written);
|
||||
let again = s2.render(cid, t, RenderOpts::default());
|
||||
assert!(dc2.stats().hits > 0, "{:?}", dc2.stats());
|
||||
assert_eq!(again.data, first.data, "disk-cached pixels are exact");
|
||||
// An edit changes the keys: a miss, and new entries.
|
||||
let hits = dc2.stats().hits;
|
||||
s2.execute("prop.set", json!({"layer": "#1", "path": "effects/#1/blurriness", "value": 20})).unwrap();
|
||||
let edited = s2.render(cid, t, RenderOpts::default());
|
||||
assert_ne!(edited.data, first.data);
|
||||
assert_eq!(dc2.stats().hits, hits);
|
||||
dc2.flush();
|
||||
assert!(dc2.stats().entries > written);
|
||||
// Purge ▸ All Disk Cache empties it; memory purge leaves the disk alone.
|
||||
let r = s2.execute_checked("edit.purge", json!({"what": "memory"})).unwrap();
|
||||
assert_eq!(r["diskEntries"], json!(0));
|
||||
assert!(dc2.stats().entries > 0);
|
||||
let r = s2.execute_checked("edit.purge", json!({"what": "disk"})).unwrap();
|
||||
assert!(r["diskEntries"].as_u64().unwrap() > 0);
|
||||
assert_eq!(dc2.stats().entries, 0);
|
||||
// Disabling detaches it.
|
||||
s2.execute_checked("prefs.set", json!({"key": "disk.diskCacheEnabled", "value": false})).unwrap();
|
||||
assert!(s2.disk_cache.is_none());
|
||||
assert!(s2.layer_cache.disk().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn footage_changes_change_the_salt() {
|
||||
let dir = folder("salt");
|
||||
let mut s = session(&dir);
|
||||
let foot = std::env::temp_dir().join(format!("effectcraft-salt-{}.png", std::process::id()));
|
||||
std::fs::write(&foot, b"one").unwrap();
|
||||
s.edit("add", None, |p, _| {
|
||||
let f = effectcraft_project::Footage {
|
||||
path: foot.to_string_lossy().to_string(),
|
||||
kind: effectcraft_project::FootageKind::Still,
|
||||
width: 1,
|
||||
height: 1,
|
||||
pixel_aspect: 1.0,
|
||||
frame_rate: effectcraft_time::FrameRate::FPS_30,
|
||||
native_rate: None,
|
||||
duration: effectcraft_time::Tick::ZERO,
|
||||
has_video: true,
|
||||
has_audio: false,
|
||||
alpha: effectcraft_project::AlphaMode::Ignore,
|
||||
premul_color: [0.0; 3],
|
||||
loop_count: 1,
|
||||
codec: "PNG".into(),
|
||||
missing: false,
|
||||
sequence: vec![],
|
||||
color_profile: None,
|
||||
layer: None,
|
||||
};
|
||||
p.add_item("f.png", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f));
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let a = effectcraft_render::disk_cache::footage_salt(&s.project);
|
||||
std::fs::write(&foot, b"two, longer").unwrap();
|
||||
let b = effectcraft_render::disk_cache::footage_salt(&s.project);
|
||||
assert_ne!(a, b);
|
||||
let cid = project(&mut s);
|
||||
let k1 = effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
||||
s.execute("prop.set", json!({"layer": "#1", "path": "transform/opacity", "value": 50})).unwrap();
|
||||
let k2 = effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
||||
assert_ne!(k1, k2);
|
||||
// Unrelated comps do not change it.
|
||||
s.execute("comp.new", json!({"name": "Other", "width": 10, "height": 10, "frameRate": 30, "duration": 1})).unwrap();
|
||||
assert_eq!(effectcraft_render::disk_cache::comp_content_key(&s.project, cid), k2);
|
||||
}
|
||||
Reference in New Issue
Block a user