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 = "templateSource";
23
24/// One marked input: an expression the user sets when inserting.
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TemplateInput {
28    /// The expression name (`height`).
29    pub name: String,
30    /// What the insert prompt calls it; empty = the name.
31    #[serde(default)]
32    pub label: String,
33    /// Inclusive limits, when set.
34    #[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    /// A fixed list of allowed values (expression source text). Non-empty =
39    /// the prompt offers exactly these.
40    #[serde(default, skip_serializing_if = "Vec::is_empty")]
41    pub choices: Vec<String>,
42}
43
44impl TemplateInput {
45    /// The label the prompt shows.
46    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/// What a spun-out copy records about its template.
56#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct TemplateSource {
59    /// The template's file name (`bracket.tbrep`).
60    pub template: String,
61    /// [`template_hash`] of the template when the copy was made.
62    pub content_hash: String,
63}
64
65/// The inputs a template document marks (empty when none, or unreadable).
66pub 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
73/// The inputs the engine's document marks. One map lookup and a small parse.
74pub 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
82/// Replace the engine document's input marks as a USER edit (one undo step;
83/// `coalesce` groups a typing run). An empty list removes the block.
84pub 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
92/// The copy's record of its template, if the document is a spun-out copy.
93pub 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
99/// A content hash of the whole template document less the view state it
100/// reopens in.
101pub 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
109/// The `name = rhs` assignments of an expressions source, in order.
110pub 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
131/// The template's own value for `name` (its defining expression's source),
132/// what the insert prompt starts the field from.
133pub 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
157/// Why `text` is not an acceptable value for `input`, if it is not: it must
158/// evaluate (against the template's own expressions, so `width * 2` works) to
159/// a finite number inside the limits, and be one of the choices when there
160/// are choices.
161pub 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
188/// The specialised copy of `template` (file `template_file`) with `values`
189/// (input name -> source text) baked into its expressions: a normal part that
190/// keeps its expressions, drops the input marks and the class, and records
191/// its template. Every value is checked first; the first problem is the error.
192pub 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    let source = TemplateSource {
223        template: template_file.to_string(),
224        content_hash: template_hash(template),
225    };
226    object.insert(
227        TEMPLATE_SOURCE_KEY.into(),
228        serde_json::to_value(source).map_err(|e| e.to_string())?,
229    );
230    Ok(copy)
231}
232
233/// Why `name` cannot be the spun-out part's name, if it cannot: the same file
234/// name rules as a family member's part number.
235pub fn name_problem(name: &str) -> Option<String> {
236    part_number_problem(name).map(|problem| problem.replace("part number", "name"))
237}
238