use concinnity_cook::authoring::world::entry_handles;
use super::shader_list::{FileStatus, Row, RowKind};
use super::shader_source::{SourceKey, same_file};
use crate::debug::hot_reload::{ReloadOutcome, ReloadSubject, ReportBoard};
pub(crate) const SDF_VOLUME: &str = "SdfVolume";
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FieldVolume {
pub(crate) name: String,
pub(crate) volumetric: bool,
}
impl FieldVolume {
pub(crate) fn mode(&self) -> &'static str {
match self.volumetric {
true => "volumetric",
false => "surface",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FieldDecl {
pub(crate) declared: String,
pub(crate) path: String,
pub(crate) volumes: Vec<FieldVolume>,
}
impl FieldDecl {
pub(crate) fn names(&self) -> Vec<&str> {
self.volumes.iter().map(|v| v.name.as_str()).collect()
}
}
pub(crate) fn declared_fields(
entries: &[serde_json::Value],
mut resolve: impl FnMut(&str) -> String,
) -> Vec<FieldDecl> {
let names = entry_handles(entries);
let mut out: Vec<FieldDecl> = Vec::new();
for (i, e) in entries.iter().enumerate() {
if e.get("type").and_then(|t| t.as_str()) != Some(SDF_VOLUME) {
continue;
}
let args = e.get("args");
let Some(declared) = args
.and_then(|a| a.get("fragment_shader"))
.and_then(|v| v.as_str())
.filter(|d| !d.is_empty())
else {
continue;
};
let Some(name) = names[i].clone() else {
continue;
};
let volume = FieldVolume {
name,
volumetric: args
.and_then(|a| a.get("volumetric"))
.and_then(|v| v.as_bool())
.unwrap_or(false),
};
let path = resolve(declared);
match out.iter_mut().find(|f| f.path == path) {
Some(field) => field.volumes.push(volume),
None => out.push(FieldDecl {
declared: declared.to_string(),
path,
volumes: vec![volume],
}),
}
}
out
}
pub(crate) fn subjects(field: &FieldDecl) -> Vec<ReloadSubject> {
field
.volumes
.iter()
.map(|v| ReloadSubject::sdf_volume(&v.name))
.collect()
}
pub(crate) fn field_status(board: &ReportBoard, field: &FieldDecl) -> FileStatus {
subjects(field)
.iter()
.map(|subject| volume_status(board, subject, &field.path))
.max_by_key(|s| severity(*s))
.unwrap_or(FileStatus::Built)
}
fn volume_status(board: &ReportBoard, subject: &ReloadSubject, path: &str) -> FileStatus {
if board.is_live(subject) == Some(false) {
return FileStatus::NotLive;
}
let Some(latest) = board.latest(subject) else {
return FileStatus::Built;
};
let here = |warnings: &[concinnity_cook::compile::program::Diagnostic]| {
warnings.iter().filter(|w| same_file(&w.path, path)).count()
};
match &latest.outcome {
ReloadOutcome::Swapped { warnings, .. } => FileStatus::Ok(here(warnings)),
ReloadOutcome::AppliesOnLoad { warnings } => FileStatus::AppliesOnLoad(here(warnings)),
ReloadOutcome::Failed(_) => FileStatus::Failed,
}
}
fn severity(status: FileStatus) -> u8 {
match status {
FileStatus::Built => 0,
FileStatus::Ok(0) => 1,
FileStatus::AppliesOnLoad(0) => 2,
FileStatus::NotLive => 3,
FileStatus::Ok(_) | FileStatus::AppliesOnLoad(_) => 4,
FileStatus::SiblingFailed | FileStatus::Failed => 5,
}
}
pub(crate) fn field_rows(
fields: &[FieldDecl],
board: &ReportBoard,
open: Option<&SourceKey>,
) -> Vec<Row> {
if fields.is_empty() {
return Vec::new();
}
let mut out = vec![Row {
kind: RowKind::Section,
text: "SDF fields".to_string(),
badge: None,
indent: false,
selected: false,
}];
for (i, field) in fields.iter().enumerate() {
let status = field_status(board, field);
let key = SourceKey::Field {
path: field.path.clone(),
};
out.push(Row {
kind: RowKind::Field(i),
text: field.declared.clone(),
badge: Some((status.label(), status.tone())),
indent: false,
selected: open == Some(&key),
});
out.extend(field.volumes.iter().map(|v| Row {
kind: RowKind::FieldVolume(v.name.clone()),
text: format!("{} {}", v.name, v.mode()),
badge: None,
indent: true,
selected: false,
}));
}
out
}
#[cfg(test)]
mod tests;