use crate::family_table::{part_number_problem, rewrite_expressions};
use brep_render::engine_state::EngineState;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const TEMPLATE_INPUTS_KEY: &str = "templateInputs";
pub const TEMPLATE_SOURCE_KEY: &str = brep_render::brep_kernel::family::TEMPLATE_SOURCE_KEY;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "automation", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase")]
pub struct TemplateInput {
pub name: String,
#[serde(default)]
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub choices: Vec<String>,
}
impl TemplateInput {
pub fn shown(&self) -> &str {
if self.label.trim().is_empty() {
&self.name
} else {
&self.label
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateSource {
pub template: String,
pub content_hash: String,
}
pub fn inputs_of(document: &serde_json::Value) -> Vec<TemplateInput> {
document
.get(TEMPLATE_INPUTS_KEY)
.and_then(|block| serde_json::from_value(block.clone()).ok())
.unwrap_or_default()
}
pub fn engine_inputs(engine: &EngineState) -> Vec<TemplateInput> {
engine
.history
.document_block(TEMPLATE_INPUTS_KEY)
.and_then(|block| serde_json::from_value(block.clone()).ok())
.unwrap_or_default()
}
pub fn apply_inputs(engine: &mut EngineState, inputs: &[TemplateInput], coalesce: Option<&str>) {
let block = (!inputs.is_empty())
.then(|| serde_json::to_value(inputs).unwrap_or(serde_json::Value::Null));
engine
.history
.set_document_block(TEMPLATE_INPUTS_KEY, block, coalesce);
}
pub fn template_source(document: &serde_json::Value) -> Option<TemplateSource> {
document
.get(TEMPLATE_SOURCE_KEY)
.and_then(|block| serde_json::from_value(block.clone()).ok())
}
pub fn template_hash(template: &serde_json::Value) -> String {
let mut content = template.clone();
if let Some(object) = content.as_object_mut() {
object.remove("workbench");
}
format!("{:016x}", brep_render::brep_kernel::stable_json_hash(&content))
}
pub fn assignments(expressions: &str) -> Vec<(String, String)> {
let mut cleaned = String::with_capacity(expressions.len());
for line in expressions.lines() {
cleaned.push_str(line.split("//").next().unwrap_or(""));
cleaned.push('\n');
}
cleaned
.split(';')
.filter_map(|statement| {
let (name, rhs) = statement.split_once('=')?;
let (name, rhs) = (name.trim(), rhs.trim());
let identifier = name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
(identifier && !rhs.is_empty()).then(|| (name.to_string(), rhs.to_string()))
})
.collect()
}
pub fn default_value(template: &serde_json::Value, input: &TemplateInput) -> String {
let expressions = expressions_of(template);
if let Some(first) = input.choices.first() {
return assignments(expressions)
.into_iter()
.find(|(name, _)| *name == input.name)
.map(|(_, rhs)| rhs)
.filter(|rhs| input.choices.iter().any(|choice| choice.trim() == rhs))
.unwrap_or_else(|| first.clone());
}
assignments(expressions)
.into_iter()
.find(|(name, _)| *name == input.name)
.map(|(_, rhs)| rhs)
.unwrap_or_default()
}
fn expressions_of(document: &serde_json::Value) -> &str {
document
.get("expressions")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
}
pub fn value_problem(template: &serde_json::Value, input: &TemplateInput, text: &str) -> Option<String> {
let text = text.trim();
let shown = input.shown();
if text.is_empty() {
return Some(format!("{shown}: enter a value"));
}
if !input.choices.is_empty() && !input.choices.iter().any(|choice| choice.trim() == text) {
return Some(format!("{shown}: choose one of {}", input.choices.join(", ")));
}
let configurator = template.get("configurator").cloned().unwrap_or(serde_json::Value::Null);
let mut values = BTreeMap::new();
values.insert(input.name.clone(), text.to_string());
let source = rewrite_expressions(expressions_of(template), &values);
let value = match brep_render::brep_kernel::eval_expression(&source, &configurator, &input.name) {
Ok(value) if value.is_finite() => value,
Ok(value) => return Some(format!("{shown}: evaluates to {value}")),
Err(error) => return Some(format!("{shown}: {error}")),
};
if let Some(min) = input.min.filter(|min| value < *min) {
return Some(format!("{shown}: {value} is below the minimum {min}"));
}
if let Some(max) = input.max.filter(|max| value > *max) {
return Some(format!("{shown}: {value} is above the maximum {max}"));
}
None
}
pub fn spin_out(
template: &serde_json::Value,
template_file: &str,
values: &BTreeMap<String, String>,
) -> Result<serde_json::Value, String> {
let inputs = inputs_of(template);
for input in &inputs {
let text = values.get(&input.name).map(String::as_str).unwrap_or("");
if let Some(problem) = value_problem(template, input, text) {
return Err(problem);
}
}
let values: BTreeMap<String, String> = inputs
.iter()
.filter_map(|input| {
values
.get(&input.name)
.map(|text| (input.name.clone(), text.trim().to_string()))
})
.collect();
let mut copy = template.clone();
let object = copy.as_object_mut().ok_or("the template is not a JSON object")?;
object.insert(
"expressions".into(),
serde_json::Value::String(rewrite_expressions(expressions_of(template), &values)),
);
object.remove(TEMPLATE_INPUTS_KEY);
object.remove(crate::document_class::DOCUMENT_CLASS_KEY);
object.remove(crate::family_table::FAMILY_TABLE_KEY);
object.remove(crate::family_table::FAMILY_SOURCE_KEY);
object.remove(brep_render::engine_state::replace_component::DERIVED_FROM_KEY);
let source = TemplateSource {
template: template_file.to_string(),
content_hash: template_hash(template),
};
object.insert(
TEMPLATE_SOURCE_KEY.into(),
serde_json::to_value(source).map_err(|e| e.to_string())?,
);
Ok(copy)
}
pub fn name_problem(name: &str) -> Option<String> {
part_number_problem(name).map(|problem| problem.replace("part number", "name"))
}