use crate::asset::BuildCtx;
use crate::authoring::source_args::sdf_volume_source_path;
use concinnity_core::components::SdfVolume;
pub(super) fn resolve_source_path(raw: &str, ctx: &BuildCtx<'_>) -> Option<String> {
let raw_path = std::path::Path::new(raw);
let mut candidates: Vec<String> = Vec::new();
if raw_path.is_absolute() {
candidates.push(raw.to_string());
} else {
if let Some(assets) = ctx.assets_dir {
candidates.push(assets.join(raw).to_string_lossy().into_owned());
}
if raw_path
.parent()
.map(|d| d.as_os_str().is_empty())
.unwrap_or(true)
&& let Some(found) = ctx
.assets_dir
.and_then(|dir| concinnity_host::store::source::find_in(dir, raw))
{
candidates.push(found);
}
if let Some(dir) = ctx.artifacts_dir {
candidates.push(format!("{dir}/{raw}"));
}
candidates.push(format!("assets/{raw}"));
candidates.push(raw.to_string());
}
candidates
.into_iter()
.find(|p| std::path::Path::new(p).exists())
}
impl crate::asset::BuildAsset for SdfVolume {
fn compile_payload(
args: &serde_json::Value,
ctx: &crate::asset::BuildCtx<'_>,
) -> std::io::Result<Vec<u8>> {
let raw = sdf_volume_source_path(args).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"SdfVolume '{}': no distance field declared (set `fragment_shader` \
to a `.slang` path declaring map + shade, or sampleVolume)",
ctx.name
),
)
})?;
let source_path = resolve_source_path(&raw, ctx).unwrap_or_else(|| raw.clone());
let field = std::fs::read_to_string(&source_path).map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"SdfVolume '{}': failed to read distance field '{}': {}",
ctx.name, source_path, e
),
)
})?;
let flag = |k: &str| args.get(k).and_then(serde_json::Value::as_bool);
let volumetric = flag("volumetric").unwrap_or(false);
let cast_shadows = flag("cast_shadows").unwrap_or(false);
let programs =
super::sdf_field::compile(ctx.name, &field, ctx.platform, volumetric, cast_shadows)?;
postcard::to_allocvec(&programs).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("SdfVolume '{}': encoding compiled field: {e}", ctx.name),
)
})
}
const TARGET_DEPENDENT: bool = true;
fn source_files(
args: &serde_json::Value,
ctx: &crate::asset::BuildCtx<'_>,
) -> crate::asset::SourceFiles {
use crate::asset::SourceFiles;
let Some(raw) = sdf_volume_source_path(args) else {
return SourceFiles::Only(Vec::new());
};
SourceFiles::Only(resolve_source_path(&raw, ctx).into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::{BuildAsset, SourceFiles};
use concinnity_core::platform::Platform;
fn args(source: &str) -> serde_json::Value {
serde_json::json!({ "fragment_shader": source })
}
fn ctx<'a>(artifacts_dir: Option<&'a str>) -> BuildCtx<'a> {
BuildCtx {
name: "blob",
platform: Platform::Metal,
assets_dir: None,
artifacts_dir,
all_assets: &[],
}
}
const FIELD: &str = r#"
float map(float3 p, SdfParams params, float time) { return sdSphere(p, 0.5); }
SdfSurface shade(float3 p, float3 n, SdfParams params, float time, float2 uv) {
SdfSurface s;
s.albedo = float3(1.0, 1.0, 1.0);
s.roughness = 0.5;
s.metallic = 0.0;
s.emissive = float3(0.0, 0.0, 0.0);
s.transmitted = float3(0.0, 0.0, 0.0);
return s;
}
"#;
#[test]
fn an_absolute_path_resolves_only_when_it_exists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("chrome.slang");
std::fs::write(&path, FIELD).unwrap();
let raw = path.to_string_lossy().into_owned();
assert_eq!(resolve_source_path(&raw, &ctx(None)), Some(raw.clone()));
let missing = dir
.path()
.join("absent.slang")
.to_string_lossy()
.into_owned();
assert_eq!(resolve_source_path(&missing, &ctx(None)), None);
}
#[test]
fn a_relative_path_resolves_under_the_artifacts_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("shaders")).unwrap();
std::fs::write(dir.path().join("shaders/chrome.slang"), FIELD).unwrap();
let artifacts = dir.path().to_string_lossy().into_owned();
assert_eq!(
resolve_source_path("shaders/chrome.slang", &ctx(Some(&artifacts))),
Some(format!("{artifacts}/shaders/chrome.slang"))
);
}
#[test]
fn a_bare_filename_resolves_under_the_artifacts_dir() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("chrome.slang"), FIELD).unwrap();
let artifacts = dir.path().to_string_lossy().into_owned();
assert_eq!(
resolve_source_path("chrome.slang", &ctx(Some(&artifacts))),
Some(format!("{artifacts}/chrome.slang"))
);
}
#[test]
fn an_unresolvable_relative_path_returns_none() {
let dir = tempfile::tempdir().unwrap();
let artifacts = dir.path().to_string_lossy().into_owned();
assert_eq!(
resolve_source_path("cn_no_such_field.slang", &ctx(Some(&artifacts))),
None
);
assert_eq!(
resolve_source_path("cn_no_such_field.slang", &ctx(None)),
None
);
}
#[test]
fn a_missing_source_file_names_the_asset_and_the_path() {
let err =
SdfVolume::compile_payload(&args("/no/such/chrome.slang"), &ctx(None)).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
assert!(
err.to_string().contains(
"SdfVolume 'blob': failed to read distance field '/no/such/chrome.slang'"
),
"got: {err}"
);
}
#[test]
fn no_declared_field_is_a_hard_error() {
let err = SdfVolume::compile_payload(&serde_json::json!({}), &ctx(None)).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("no distance field declared"),
"got: {err}"
);
}
#[test]
fn source_files_reports_only_the_declared_field() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("chrome.slang");
std::fs::write(&path, FIELD).unwrap();
let raw = path.to_string_lossy().into_owned();
assert_eq!(
SdfVolume::source_files(&args(&raw), &ctx(None)),
SourceFiles::Only(vec![raw])
);
assert_eq!(
SdfVolume::source_files(&serde_json::json!({}), &ctx(None)),
SourceFiles::Only(Vec::new())
);
assert_eq!(
SdfVolume::source_files(&args("/no/such/chrome.slang"), &ctx(None)),
SourceFiles::Only(Vec::new())
);
const { assert!(SdfVolume::TARGET_DEPENDENT) };
}
}