1use crate::family_table::{part_number_problem, rewrite_expressions};
15use brep_render::engine_state::EngineState;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19pub const TEMPLATE_INPUTS_KEY: &str = "templateInputs";
21pub const TEMPLATE_SOURCE_KEY: &str = "templateSource";
23
24#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TemplateInput {
28 pub name: String,
30 #[serde(default)]
32 pub label: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub min: Option<f64>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub max: Option<f64>,
38 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub choices: Vec<String>,
42}
43
44impl TemplateInput {
45 pub fn shown(&self) -> &str {
47 if self.label.trim().is_empty() {
48 &self.name
49 } else {
50 &self.label
51 }
52 }
53}
54
55#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TemplateSource {
59 pub template: String,
61 pub content_hash: String,
63}
64
65pub fn inputs_of(document: &serde_json::Value) -> Vec<TemplateInput> {
67 document
68 .get(TEMPLATE_INPUTS_KEY)
69 .and_then(|block| serde_json::from_value(block.clone()).ok())
70 .unwrap_or_default()
71}
72
73pub fn engine_inputs(engine: &EngineState) -> Vec<TemplateInput> {
75 engine
76 .history
77 .document_block(TEMPLATE_INPUTS_KEY)
78 .and_then(|block| serde_json::from_value(block.clone()).ok())
79 .unwrap_or_default()
80}
81
82pub fn apply_inputs(engine: &mut EngineState, inputs: &[TemplateInput], coalesce: Option<&str>) {
85 let block = (!inputs.is_empty())
86 .then(|| serde_json::to_value(inputs).unwrap_or(serde_json::Value::Null));
87 engine
88 .history
89 .set_document_block(TEMPLATE_INPUTS_KEY, block, coalesce);
90}
91
92pub fn template_source(document: &serde_json::Value) -> Option<TemplateSource> {
94 document
95 .get(TEMPLATE_SOURCE_KEY)
96 .and_then(|block| serde_json::from_value(block.clone()).ok())
97}
98
99pub fn template_hash(template: &serde_json::Value) -> String {
102 let mut content = template.clone();
103 if let Some(object) = content.as_object_mut() {
104 object.remove("workbench");
105 }
106 format!("{:016x}", brep_render::brep_kernel::stable_json_hash(&content))
107}
108
109pub fn assignments(expressions: &str) -> Vec<(String, String)> {
111 let mut cleaned = String::with_capacity(expressions.len());
112 for line in expressions.lines() {
113 cleaned.push_str(line.split("//").next().unwrap_or(""));
114 cleaned.push('\n');
115 }
116 cleaned
117 .split(';')
118 .filter_map(|statement| {
119 let (name, rhs) = statement.split_once('=')?;
120 let (name, rhs) = (name.trim(), rhs.trim());
121 let identifier = name
122 .chars()
123 .next()
124 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
125 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
126 (identifier && !rhs.is_empty()).then(|| (name.to_string(), rhs.to_string()))
127 })
128 .collect()
129}
130
131pub fn default_value(template: &serde_json::Value, input: &TemplateInput) -> String {
134 let expressions = expressions_of(template);
135 if let Some(first) = input.choices.first() {
136 return assignments(expressions)
137 .into_iter()
138 .find(|(name, _)| *name == input.name)
139 .map(|(_, rhs)| rhs)
140 .filter(|rhs| input.choices.iter().any(|choice| choice.trim() == rhs))
141 .unwrap_or_else(|| first.clone());
142 }
143 assignments(expressions)
144 .into_iter()
145 .find(|(name, _)| *name == input.name)
146 .map(|(_, rhs)| rhs)
147 .unwrap_or_default()
148}
149
150fn expressions_of(document: &serde_json::Value) -> &str {
151 document
152 .get("expressions")
153 .and_then(serde_json::Value::as_str)
154 .unwrap_or_default()
155}
156
157pub fn value_problem(template: &serde_json::Value, input: &TemplateInput, text: &str) -> Option<String> {
162 let text = text.trim();
163 let shown = input.shown();
164 if text.is_empty() {
165 return Some(format!("{shown}: enter a value"));
166 }
167 if !input.choices.is_empty() && !input.choices.iter().any(|choice| choice.trim() == text) {
168 return Some(format!("{shown}: choose one of {}", input.choices.join(", ")));
169 }
170 let configurator = template.get("configurator").cloned().unwrap_or(serde_json::Value::Null);
171 let mut values = BTreeMap::new();
172 values.insert(input.name.clone(), text.to_string());
173 let source = rewrite_expressions(expressions_of(template), &values);
174 let value = match brep_render::brep_kernel::eval_expression(&source, &configurator, &input.name) {
175 Ok(value) if value.is_finite() => value,
176 Ok(value) => return Some(format!("{shown}: evaluates to {value}")),
177 Err(error) => return Some(format!("{shown}: {error}")),
178 };
179 if let Some(min) = input.min.filter(|min| value < *min) {
180 return Some(format!("{shown}: {value} is below the minimum {min}"));
181 }
182 if let Some(max) = input.max.filter(|max| value > *max) {
183 return Some(format!("{shown}: {value} is above the maximum {max}"));
184 }
185 None
186}
187
188pub fn spin_out(
193 template: &serde_json::Value,
194 template_file: &str,
195 values: &BTreeMap<String, String>,
196) -> Result<serde_json::Value, String> {
197 let inputs = inputs_of(template);
198 for input in &inputs {
199 let text = values.get(&input.name).map(String::as_str).unwrap_or("");
200 if let Some(problem) = value_problem(template, input, text) {
201 return Err(problem);
202 }
203 }
204 let values: BTreeMap<String, String> = inputs
205 .iter()
206 .filter_map(|input| {
207 values
208 .get(&input.name)
209 .map(|text| (input.name.clone(), text.trim().to_string()))
210 })
211 .collect();
212 let mut copy = template.clone();
213 let object = copy.as_object_mut().ok_or("the template is not a JSON object")?;
214 object.insert(
215 "expressions".into(),
216 serde_json::Value::String(rewrite_expressions(expressions_of(template), &values)),
217 );
218 object.remove(TEMPLATE_INPUTS_KEY);
219 object.remove(crate::document_class::DOCUMENT_CLASS_KEY);
220 object.remove(crate::family_table::FAMILY_TABLE_KEY);
221 object.remove(crate::family_table::FAMILY_SOURCE_KEY);
222 object.remove(brep_render::engine_state::replace_component::DERIVED_FROM_KEY);
225 let source = TemplateSource {
226 template: template_file.to_string(),
227 content_hash: template_hash(template),
228 };
229 object.insert(
230 TEMPLATE_SOURCE_KEY.into(),
231 serde_json::to_value(source).map_err(|e| e.to_string())?,
232 );
233 Ok(copy)
234}
235
236pub fn name_problem(name: &str) -> Option<String> {
239 part_number_problem(name).map(|problem| problem.replace("part number", "name"))
240}
241