Skip to main content

brep_app/
template.rs

1//! A template (`.tbrep`): a seed whose MARKED expressions are the user's
2//! inputs. Inserting one never places the template: it asks for a new name
3//! and the input values, spins out a unique normal part with those values
4//! baked into its expressions, and places that.
5//!
6//! The marks live in the template document as the top-level `templateInputs`
7//! block — one entry per input expression, with an optional label, limits or
8//! fixed list of choices. The spun-out copy KEEPS its expressions (it is an
9//! ordinary part the user can keep tweaking) and records where it came from in
10//! `templateSource`: the template's file name and a content hash, for
11//! traceability only. Nothing links back; changing the template never touches
12//! a copy.
13
14use crate::family_table::{part_number_problem, rewrite_expressions};
15use brep_render::engine_state::EngineState;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19/// The document key the input marks are stored under.
20pub const TEMPLATE_INPUTS_KEY: &str = "templateInputs";
21/// The document key a spun-out copy records its template under.
22pub const TEMPLATE_SOURCE_KEY: &str = brep_render::brep_kernel::family::TEMPLATE_SOURCE_KEY;
23
24/// One marked input: an expression the user sets when inserting.
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(feature = "automation", derive(schemars::JsonSchema))]
27#[serde(rename_all = "camelCase")]
28pub struct TemplateInput {
29    /// The expression name (`height`).
30    pub name: String,
31    /// What the insert prompt calls it; empty = the name.
32    #[serde(default)]
33    pub label: String,
34    /// Inclusive limits, when set.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub min: Option<f64>,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub max: Option<f64>,
39    /// A fixed list of allowed values (expression source text). Non-empty =
40    /// the prompt offers exactly these.
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub choices: Vec<String>,
43}
44
45impl TemplateInput {
46    /// The label the prompt shows.
47    pub fn shown(&self) -> &str {
48        if self.label.trim().is_empty() {
49            &self.name
50        } else {
51            &self.label
52        }
53    }
54}
55
56/// What a spun-out copy records about its template.
57#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct TemplateSource {
60    /// The template's file name (`bracket.tbrep`).
61    pub template: String,
62    /// [`template_hash`] of the template when the copy was made.
63    pub content_hash: String,
64}
65
66/// The inputs a template document marks (empty when none, or unreadable).
67pub fn inputs_of(document: &serde_json::Value) -> Vec<TemplateInput> {
68    document
69        .get(TEMPLATE_INPUTS_KEY)
70        .and_then(|block| serde_json::from_value(block.clone()).ok())
71        .unwrap_or_default()
72}
73
74/// The inputs the engine's document marks. One map lookup and a small parse.
75pub fn engine_inputs(engine: &EngineState) -> Vec<TemplateInput> {
76    engine
77        .history
78        .document_block(TEMPLATE_INPUTS_KEY)
79        .and_then(|block| serde_json::from_value(block.clone()).ok())
80        .unwrap_or_default()
81}
82
83/// Replace the engine document's input marks as a USER edit (one undo step;
84/// `coalesce` groups a typing run). An empty list removes the block.
85pub fn apply_inputs(engine: &mut EngineState, inputs: &[TemplateInput], coalesce: Option<&str>) {
86    let block = (!inputs.is_empty())
87        .then(|| serde_json::to_value(inputs).unwrap_or(serde_json::Value::Null));
88    engine
89        .history
90        .set_document_block(TEMPLATE_INPUTS_KEY, block, coalesce);
91}
92
93/// The copy's record of its template, if the document is a spun-out copy.
94pub fn template_source(document: &serde_json::Value) -> Option<TemplateSource> {
95    document
96        .get(TEMPLATE_SOURCE_KEY)
97        .and_then(|block| serde_json::from_value(block.clone()).ok())
98}
99
100/// A content hash of the whole template document less the view state it
101/// reopens in.
102pub fn template_hash(template: &serde_json::Value) -> String {
103    let mut content = template.clone();
104    if let Some(object) = content.as_object_mut() {
105        object.remove("workbench");
106    }
107    format!("{:016x}", brep_render::brep_kernel::stable_json_hash(&content))
108}
109
110/// The `name = rhs` assignments of an expressions source, in order.
111pub fn assignments(expressions: &str) -> Vec<(String, String)> {
112    let mut cleaned = String::with_capacity(expressions.len());
113    for line in expressions.lines() {
114        cleaned.push_str(line.split("//").next().unwrap_or(""));
115        cleaned.push('\n');
116    }
117    cleaned
118        .split(';')
119        .filter_map(|statement| {
120            let (name, rhs) = statement.split_once('=')?;
121            let (name, rhs) = (name.trim(), rhs.trim());
122            let identifier = name
123                .chars()
124                .next()
125                .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
126                && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
127            (identifier && !rhs.is_empty()).then(|| (name.to_string(), rhs.to_string()))
128        })
129        .collect()
130}
131
132/// The template's own value for `name` (its defining expression's source),
133/// what the insert prompt starts the field from.
134pub fn default_value(template: &serde_json::Value, input: &TemplateInput) -> String {
135    let expressions = expressions_of(template);
136    if let Some(first) = input.choices.first() {
137        return assignments(expressions)
138            .into_iter()
139            .find(|(name, _)| *name == input.name)
140            .map(|(_, rhs)| rhs)
141            .filter(|rhs| input.choices.iter().any(|choice| choice.trim() == rhs))
142            .unwrap_or_else(|| first.clone());
143    }
144    assignments(expressions)
145        .into_iter()
146        .find(|(name, _)| *name == input.name)
147        .map(|(_, rhs)| rhs)
148        .unwrap_or_default()
149}
150
151fn expressions_of(document: &serde_json::Value) -> &str {
152    document
153        .get("expressions")
154        .and_then(serde_json::Value::as_str)
155        .unwrap_or_default()
156}
157
158/// Why `text` is not an acceptable value for `input`, if it is not: it must
159/// evaluate (against the template's own expressions, so `width * 2` works) to
160/// a finite number inside the limits, and be one of the choices when there
161/// are choices.
162pub fn value_problem(template: &serde_json::Value, input: &TemplateInput, text: &str) -> Option<String> {
163    let text = text.trim();
164    let shown = input.shown();
165    if text.is_empty() {
166        return Some(format!("{shown}: enter a value"));
167    }
168    if !input.choices.is_empty() && !input.choices.iter().any(|choice| choice.trim() == text) {
169        return Some(format!("{shown}: choose one of {}", input.choices.join(", ")));
170    }
171    let configurator = template.get("configurator").cloned().unwrap_or(serde_json::Value::Null);
172    let mut values = BTreeMap::new();
173    values.insert(input.name.clone(), text.to_string());
174    let source = rewrite_expressions(expressions_of(template), &values);
175    let value = match brep_render::brep_kernel::eval_expression(&source, &configurator, &input.name) {
176        Ok(value) if value.is_finite() => value,
177        Ok(value) => return Some(format!("{shown}: evaluates to {value}")),
178        Err(error) => return Some(format!("{shown}: {error}")),
179    };
180    if let Some(min) = input.min.filter(|min| value < *min) {
181        return Some(format!("{shown}: {value} is below the minimum {min}"));
182    }
183    if let Some(max) = input.max.filter(|max| value > *max) {
184        return Some(format!("{shown}: {value} is above the maximum {max}"));
185    }
186    None
187}
188
189/// The specialised copy of `template` (file `template_file`) with `values`
190/// (input name -> source text) baked into its expressions: a normal part that
191/// keeps its expressions, drops the input marks and the class, and records
192/// its template. Every value is checked first; the first problem is the error.
193pub fn spin_out(
194    template: &serde_json::Value,
195    template_file: &str,
196    values: &BTreeMap<String, String>,
197) -> Result<serde_json::Value, String> {
198    let inputs = inputs_of(template);
199    for input in &inputs {
200        let text = values.get(&input.name).map(String::as_str).unwrap_or("");
201        if let Some(problem) = value_problem(template, input, text) {
202            return Err(problem);
203        }
204    }
205    let values: BTreeMap<String, String> = inputs
206        .iter()
207        .filter_map(|input| {
208            values
209                .get(&input.name)
210                .map(|text| (input.name.clone(), text.trim().to_string()))
211        })
212        .collect();
213    let mut copy = template.clone();
214    let object = copy.as_object_mut().ok_or("the template is not a JSON object")?;
215    object.insert(
216        "expressions".into(),
217        serde_json::Value::String(rewrite_expressions(expressions_of(template), &values)),
218    );
219    object.remove(TEMPLATE_INPUTS_KEY);
220    object.remove(crate::document_class::DOCUMENT_CLASS_KEY);
221    object.remove(crate::family_table::FAMILY_TABLE_KEY);
222    object.remove(crate::family_table::FAMILY_SOURCE_KEY);
223    // A new document from a template is no relative of the template's own
224    // origin (two parts from one template are unrelated parts).
225    object.remove(brep_render::engine_state::replace_component::DERIVED_FROM_KEY);
226    let source = TemplateSource {
227        template: template_file.to_string(),
228        content_hash: template_hash(template),
229    };
230    object.insert(
231        TEMPLATE_SOURCE_KEY.into(),
232        serde_json::to_value(source).map_err(|e| e.to_string())?,
233    );
234    Ok(copy)
235}
236
237/// Why `name` cannot be the spun-out part's name, if it cannot: the same file
238/// name rules as a family member's part number.
239pub fn name_problem(name: &str) -> Option<String> {
240    part_number_problem(name).map(|problem| problem.replace("part number", "name"))
241}
242