Merge branch 'fix/sec-model-import-warnings' into fix/sec-fbx-parse

This commit is contained in:
Dylan M. Taylor
2026-11-11 23:39:18 +00:00
8 changed files with 52 additions and 5 deletions
+7 -3
View File
@@ -319,15 +319,19 @@ impl Prober {
return f.map(|footage| Probed { name, footage, ask_alpha: false, warning: None }).map_err(fail);
}
let importer = self.importer.as_ref().ok_or_else(|| fail("media import is not available in this build".into()))?;
let mut f = crate::sequence::probe(importer.as_ref(), src, self.sequence_rate).map_err(fail)?;
let mut warning = None;
// What the file's own import left out (a model's textures over the limits) is a warning too.
let (mut f, mut warning) = match src {
Source::File(_) => importer.probe_noting(path).map_err(fail)?,
_ => (crate::sequence::probe(importer.as_ref(), src, self.sequence_rate).map_err(fail)?, None),
};
// Settings ▸ Import ▸ Report Missing Frames (numbered sequences).
if f.kind == FootageKind::Sequence
&& !f.alphabetical
&& self.report_missing_frames
&& let Some(gaps) = crate::sequence::missing_report(&f.sequence)
{
warning = Some(format!("{name}: {gaps} (the previous frame is held)"));
let gaps = format!("{name}: {gaps} (the previous frame is held)");
warning = Some(warning.map_or(gaps.clone(), |w| format!("{w}; {gaps}")));
}
// Settings ▸ Import ▸ Interpret Unlabeled Alpha As: images only (a 3D model renders its
// own alpha, and audio and data have none).
+5
View File
@@ -106,6 +106,11 @@ pub trait Importer: Send + Sync {
/// Probe one file and return footage metadata. Image sequences are the engine's
/// ([`sequence`]): it groups numbered stills and probes their first file here.
fn probe(&self, path: &str) -> std::result::Result<effectcraft_project::Footage, String>;
/// [`probe`](Self::probe), and what the import left out of the file (a 3D model's textures
/// over the limits), which the import shows the user.
fn probe_noting(&self, path: &str) -> std::result::Result<(effectcraft_project::Footage, Option<String>), String> {
self.probe(path).map(|f| (f, None))
}
/// A footage file's bytes are now at `path` (extracted from a template): hosts whose media
/// layer reads from memory (the browser) register them; file-based ones need nothing.
fn register(&self, _path: &str, _data: &[u8]) {}
+19
View File
@@ -96,6 +96,25 @@ fn imported_files_go_into_the_comp_where_they_were_dropped() {
assert!(s.execute("layer.addItem", json!({"item": "a.png", "position": [1.0]})).is_err(), "position needs x and y");
}
/// What an import left out of a file (a 3D model's textures over the limits) comes back in the
/// import's `warnings`.
#[test]
fn what_an_import_left_out_is_a_warning() {
use effectcraft_project::{Footage, FootageKind};
struct Probe;
impl crate::Importer for Probe {
fn probe(&self, path: &str) -> Result<Footage, String> {
Ok(Footage { path: path.into(), kind: FootageKind::Model, ..Default::default() })
}
fn probe_noting(&self, path: &str) -> Result<(Footage, Option<String>), String> {
Ok((self.probe(path)?, Some(format!("{path}: texture `a.png` left out"))))
}
}
let mut s = Session { importer: Some(std::sync::Arc::new(Probe)), ..Default::default() };
let r = s.execute("file.import", json!({"paths": ["/m/scene.obj"]})).unwrap();
assert_eq!(r["warnings"], json!(["/m/scene.obj: texture `a.png` left out"]));
}
/// #270: files dropped or picked in the Import dialog (`file.import {background}`) are probed in
/// a background job that says which file it is reading and how many are left; then they arrive
/// in one undo step, all selected, with a count. The same file imported again arrives again.
+3
View File
@@ -20,6 +20,9 @@ impl Importer for MediaImporter {
fn probe(&self, path: &str) -> Result<Footage, String> {
effectcraft_media::probe(path).map_err(|e| e.to_string())
}
fn probe_noting(&self, path: &str) -> Result<(Footage, Option<String>), String> {
effectcraft_media::probe_noting(path).map_err(|e| e.to_string())
}
}
/// Render Queue export via `effectcraft-export`: to the file system, or to `sink` (the web app
+1 -1
View File
@@ -33,7 +33,7 @@ mod webp_anim;
/// Video decoder counters (time in decoders, samples decoded, seeks, cache hits), for benchmarks.
pub use filmcraft_codecs::{GopStats, gop_stats};
pub use pool::{DEFAULT_BUDGET, MediaPool, PoolStats};
pub use probe::{DEFAULT_SEQUENCE_RATE, probe, probe_bytes, probe_model};
pub use probe::{DEFAULT_SEQUENCE_RATE, probe, probe_bytes, probe_model, probe_noting};
/// Errors from probing or decoding footage.
#[derive(Debug, thiserror::Error)]
+14
View File
@@ -43,11 +43,25 @@ fn side_file_reader(model: &Path) -> impl Fn(&str) -> Option<Vec<u8>> + use<> {
}
}
thread_local! {
/// What the last model probed on this thread left out (see [`probe_noting`]).
static MODEL_WARNING: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
}
/// [`probe`], and what the import left out of the file (a 3D model's textures over the limits),
/// for the user.
pub fn probe_noting(path: impl AsRef<Path>) -> Result<(Footage, Option<String>)> {
MODEL_WARNING.with(|w| w.borrow_mut().take());
let f = probe(path)?;
Ok((f, MODEL_WARNING.with(|w| w.borrow_mut().take())))
}
/// Probe a 3D model (glTF 2.0 / OBJ / STL / FBX): a [`FootageKind::Model`] item whose duration is its
/// longest animation. `resolve` reads sibling resources.
pub fn probe_model(path: &str, bytes: &[u8], resolve: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Footage> {
let m = effectcraft_model::load(path, bytes, resolve).map_err(|e| MediaError::Decode(format!("{path}: {e}")))?;
let s = m.summary();
MODEL_WARNING.with(|w| *w.borrow_mut() = (!m.warnings.is_empty()).then(|| format!("{path}: {}", m.warnings.join("; "))));
let dur = s.animations.iter().map(|a| a.1).fold(0.0f64, f64::max);
let codec = match effectcraft_model::format_of(path) {
Some("obj") => "OBJ",
+1 -1
View File
@@ -536,7 +536,7 @@ pub(crate) fn parse_with_budget(bytes: &[u8], resolve: &dyn Fn(&str) -> Option<V
.and_then(|e| e.get("KHR_lights_punctual"))
.map(|e| arr(e, "lights").iter().enumerate().map(|(i, l)| light(l, i)).collect())
.unwrap_or_default();
Ok(Model { meshes, nodes, roots, materials, textures, skins, animations, cameras, lights })
Ok(Model { meshes, nodes, roots, materials, textures, skins, animations, cameras, lights, warnings: Vec::new() })
}
fn name_or(v: &Value, d: String) -> String {
+2
View File
@@ -517,6 +517,8 @@ pub struct Model {
pub cameras: Vec<ModelCamera>,
/// Punctual lights embedded in the file (glTF `KHR_lights_punctual`).
pub lights: Vec<ModelLight>,
/// Parts of the file the import left out (textures over the limits), for the user.
pub warnings: Vec<String>,
}
/// A camera embedded in a model file.