Skip to main content

geop_core_program/
program.rs

1//! [`Program`]: what a program is, and what a program file holds — its
2//! steps, each an operation and its saved arguments, its state: where its
3//! placed parts are and how its joints are turned, what solving it
4//! changes — and how it is shown ([`Display`]). Serialized as
5//! `{"steps": [...], "state": {...}, "display": {...}}`.
6//!
7//! A step's id names everything the step builds, so ids are unique in a
8//! program and spelled in an alphabet a name can be built from
9//! ([`validate_operation_id`]).
10
11use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
12
13use geop_core_math::geop_error::{GeopError, GeopResult};
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    operation::{Operation, StepArgs, saved},
18    operation_registry::OperationRegistry,
19    part::Part,
20    placed::{INSTANCE_SEPARATOR, Located, TARGET_SEPARATOR},
21    runner::Runner,
22    values::StateValues,
23};
24
25/// One step of a program: which operation, with which arguments, as saved,
26/// and whether it runs. The operation reads its arguments from their saved
27/// form each time it runs or is edited, so a program is plain data: it is
28/// copied for undo, compared for change, and saved as it is.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct ProgramStep {
31    pub id: String,
32    pub operation: String,
33    pub args: serde_json::Value,
34    /// Whether the step runs, as a formula of the program's parameters
35    /// ([`crate::numeric::evaluate`]): it runs where the formula is not 0
36    /// — always, for none. A step that does not run is suppressed: it
37    /// produces nothing, and the steps after it see the part as the steps
38    /// before it left it (see [`crate::runner::Runner::run`]). `"0"`
39    /// suppresses it outright; a column of the table a program's
40    /// configurations are looked up in, `configuration.fillet`, suppresses
41    /// it in the configurations whose row has 0 there.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub active: Option<String>,
44}
45
46impl ProgramStep {
47    /// The step `id` of the operation `args` are for.
48    pub fn of<A: StepArgs>(id: impl Into<String>, args: &A) -> GeopResult<Self> {
49        let id = id.into();
50        let args = serde_json::to_value(args).map_err(|e| {
51            GeopError::new(format!("arguments of operation `{}`: {e}", A::KIND))
52                .with_context(format!("step `{id}`"))
53        })?;
54        Ok(Self {
55            id,
56            operation: A::KIND.to_string(),
57            args,
58            active: None,
59        })
60    }
61
62    pub fn new<O: Operation>(id: impl Into<String>, args: &O::Args) -> GeopResult<Self> {
63        let id = id.into();
64        let args = saved::<O>(args).map_err(|e| e.with_context(format!("step `{id}`")))?;
65        Ok(Self {
66            id,
67            operation: O::KIND.to_string(),
68            args,
69            active: None,
70        })
71    }
72}
73
74/// A program: its steps, the state it was saved with — what it is run
75/// with: what a solve moves (see [`Program::run`]) — and how what it
76/// builds is shown.
77#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
78pub struct Program {
79    #[serde(default)]
80    pub steps: Vec<ProgramStep>,
81    #[serde(default, skip_serializing_if = "StateValues::is_empty")]
82    pub state: StateValues,
83    #[serde(default, skip_serializing_if = "Display::is_empty")]
84    pub display: Display,
85}
86
87/// How what a program builds is shown, as the user chose, over what an
88/// editor shows by itself: a display state, saved with the program as the
89/// camera is not. Nothing a run reads: it builds the same part however it
90/// is shown.
91#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
92pub struct Display {
93    /// What is shown or hidden — a datum, a sketch, a solid, a placed part
94    /// — by name.
95    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
96    pub visibility: BTreeMap<String, bool>,
97    /// The colours placed parts are drawn in over their own, `#rrggbb`, by
98    /// the instance's path: `bolt`, `asm/bolt`.
99    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
100    pub colors: BTreeMap<String, String>,
101    /// The placed parts shown alone, if any: with the parts placed in them
102    /// and those they are placed in.
103    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
104    pub isolated: BTreeSet<String>,
105}
106
107impl Display {
108    pub fn is_empty(&self) -> bool {
109        self.visibility.is_empty() && self.colors.is_empty() && self.isolated.is_empty()
110    }
111}
112
113/// Checks that `id` can be an operation id: non-empty, and only ASCII
114/// letters, digits, `_`, `-` and `.`. Nothing that could be mistaken for the
115/// `(`, `)` and `,` that structure a name, so the names built from it stay
116/// unambiguous — nor dots alone: a part placed as `..` would be mistaken
117/// for the part placing it ([`crate::placed::OUTSIDE`]).
118pub fn validate_operation_id(id: &str) -> GeopResult<()> {
119    if !id.is_empty()
120        && !id.chars().all(|c| c == '.')
121        && id
122            .chars()
123            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
124    {
125        Ok(())
126    } else {
127        Err(GeopError::new(format!(
128            "{id:?} is not a valid operation id: use only ASCII letters, digits, '_', '-' and '.'"
129        )))
130    }
131}
132
133impl Program {
134    /// A program of no steps and no state.
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// The program of `steps`, with no state.
140    pub fn from_steps(steps: Vec<ProgramStep>) -> Self {
141        Self {
142            steps,
143            ..Self::default()
144        }
145    }
146
147    /// The program the file `text` holds, its ids checked (see
148    /// [`Program::validate_ids`]). What its steps are is checked against
149    /// the operations they are run in ([`Program::validate`]). An empty
150    /// file, as one is when just made, is the empty program.
151    pub fn from_json(text: &str) -> GeopResult<Self> {
152        if text.trim().is_empty() {
153            return Ok(Self::new());
154        }
155        let program: Self = serde_json::from_str(text)
156            .map_err(|e| GeopError::new(format!("reading a program: {e}")))?;
157        program.validate_ids()?;
158        Ok(program)
159    }
160
161    pub fn to_json(&self) -> GeopResult<String> {
162        serde_json::to_string_pretty(self)
163            .map_err(|e| GeopError::new(format!("writing a program: {e}")))
164    }
165
166    /// The step `id`.
167    pub fn step(&self, id: &str) -> GeopResult<&ProgramStep> {
168        (self.steps.iter().find(|s| s.id == id)).ok_or_else(|| no_step(id))
169    }
170
171    pub fn step_mut(&mut self, id: &str) -> GeopResult<&mut ProgramStep> {
172        (self.steps.iter_mut().find(|s| s.id == id)).ok_or_else(|| no_step(id))
173    }
174
175    /// The position of the step `id`.
176    pub fn index_of(&self, id: &str) -> GeopResult<usize> {
177        (self.steps.iter().position(|s| s.id == id)).ok_or_else(|| no_step(id))
178    }
179
180    /// Appends the step `id`, `args` for the operation they are for: for a
181    /// program written in Rust, whose arguments are plain data.
182    #[allow(
183        clippy::expect_used,
184        reason = "arguments that do not serialize are a bug of the program written"
185    )]
186    pub fn push<A: StepArgs>(&mut self, id: impl Into<String>, args: A) {
187        let step = ProgramStep::of(id, &args).expect("a step's arguments are plain data");
188        self.steps.push(step);
189    }
190
191    /// An id no step has yet, for a new step of an operation labelled
192    /// `label`: the label, lowercased, and the lowest number that makes it
193    /// unique — `sketch1`, `extrude2`.
194    pub fn fresh_id(&self, label: &str) -> String {
195        let base = label.to_lowercase().replace(' ', "_");
196        (1..)
197            .map(|n| format!("{base}{n}"))
198            .find(|id| self.steps.iter().all(|s| &s.id != id))
199            .unwrap_or(base)
200    }
201
202    /// Runs it all on `part`, which keeps what earlier runs built, by
203    /// `runner` (see [`Runner::run`]). Fails naming the first step that
204    /// does.
205    pub fn run(&self, runner: &Runner, part: &mut Part) -> GeopResult<()> {
206        let output = runner.run(part, self, None);
207        match output.step_errors.into_iter().next() {
208            Some((step, error)) => Err(error.with_context(format!("step {step:?}"))),
209            None => Ok(()),
210        }
211    }
212
213    /// The step `id` as a program of its own, as a copy of it is kept: the
214    /// step, and the state that is its, every key naming it (`names_step`):
215    /// where a placed part is, how its joints are turned.
216    pub fn excerpt(&self, id: &str) -> GeopResult<Self> {
217        let step = self.step(id)?.clone();
218        let state = (self.state.iter())
219            .filter(|(name, _)| names_step(name, id))
220            .map(|(name, value)| (name.clone(), value.clone()))
221            .collect();
222        Ok(Self {
223            steps: vec![step],
224            state,
225            display: Display::default(),
226        })
227    }
228
229    /// Every text in its steps' arguments that `rewrite` rewrites,
230    /// rewritten: the files they name, as the steps move to another file.
231    pub fn rewrite_texts(&mut self, rewrite: impl Fn(&str) -> Option<String>) {
232        fn walk(value: &mut serde_json::Value, rewrite: &dyn Fn(&str) -> Option<String>) {
233            match value {
234                serde_json::Value::String(text) => {
235                    if let Some(rewritten) = rewrite(text) {
236                        *text = rewritten;
237                    }
238                }
239                serde_json::Value::Array(values) => {
240                    values.iter_mut().for_each(|v| walk(v, rewrite));
241                }
242                serde_json::Value::Object(fields) => {
243                    fields.values_mut().for_each(|v| walk(v, rewrite));
244                }
245                _ => {}
246            }
247        }
248        for step in &mut self.steps {
249            walk(&mut step.args, &rewrite);
250        }
251    }
252
253    /// Inserts the steps of `pasted` at `index`, in order, with the state
254    /// that is theirs (see [`Program::excerpt`]): each under its own id
255    /// where no step has it yet, else under a fresh one — `label` naming a
256    /// step's operation. The ids they got, in order. `pasted` is checked
257    /// against `registry` first (see [`Program::validate`]).
258    ///
259    /// A step pasted under a fresh id is the step the steps pasted with it,
260    /// itself too, name by that id: every name of theirs that names a step
261    /// pasted, in their arguments and their state, is renamed with it
262    /// (`rename_steps`). A sketch and its extrusion pasted twice are two
263    /// extrusions, each of its own sketch; a part placed and mated, pasted,
264    /// is a second part, mated as itself.
265    pub fn paste(
266        &mut self,
267        pasted: Self,
268        index: usize,
269        registry: &OperationRegistry,
270        label: impl Fn(&ProgramStep) -> String,
271    ) -> GeopResult<Vec<String>> {
272        pasted
273            .validate(registry)
274            .map_err(|e| e.with_context("pasting a program"))?;
275        if index > self.steps.len() {
276            return Err(GeopError::new(format!(
277                "cannot paste at {index}: the program has {} steps",
278                self.steps.len()
279            )));
280        }
281        let own: Vec<String> = pasted.steps.iter().map(|s| s.id.clone()).collect();
282        let mut renamed = HashMap::new();
283        let mut ids = Vec::with_capacity(pasted.steps.len());
284        for (k, step) in pasted.steps.into_iter().enumerate() {
285            let id = match self.steps.iter().any(|s| s.id == step.id) {
286                true => self.fresh_id(&label(&step)),
287                false => step.id.clone(),
288            };
289            if id != step.id {
290                renamed.insert(step.id.clone(), id.clone());
291            }
292            self.steps.insert(
293                index + k,
294                ProgramStep {
295                    id: id.clone(),
296                    ..step
297                },
298            );
299            ids.push(id);
300        }
301        for step in self.steps.iter_mut().skip(index).take(ids.len()) {
302            rename_in_args(&mut step.args, &renamed);
303        }
304        for (name, value) in pasted.state {
305            if own.iter().any(|id| names_step(&name, id)) {
306                self.state
307                    .insert(rename_steps(&name, &renamed, true), value);
308            }
309        }
310        Ok(ids)
311    }
312
313    /// Checks that every step id is a valid operation id
314    /// ([`validate_operation_id`]) and unique: every name a step creates is
315    /// built from its id.
316    pub fn validate_ids(&self) -> GeopResult<()> {
317        let mut ids = HashSet::new();
318        for step in &self.steps {
319            validate_operation_id(&step.id)?;
320            if !ids.insert(step.id.as_str()) {
321                return Err(GeopError::new(format!(
322                    "the program has more than one step with id {:?}",
323                    step.id
324                )));
325            }
326        }
327        Ok(())
328    }
329
330    /// Checks the program against the operations of `registry`, which it is
331    /// run in: its ids (see [`Program::validate_ids`]), and that every step
332    /// is of an operation there is, with arguments that operation reads.
333    /// An error names the step.
334    pub fn validate(&self, registry: &OperationRegistry) -> GeopResult<()> {
335        self.validate_ids()?;
336        for step in &self.steps {
337            (registry.get(&step.operation))
338                .and_then(|operation| operation.check_args(&step.args))
339                .map_err(|e| e.with_context(format!("step `{}`", step.id)))?;
340        }
341        Ok(())
342    }
343}
344
345/// What separates the names a name is built of: `extrude(box,end)`,
346/// `sketch:base` — the steps a name names are among the pieces between.
347const NAME_SEPARATORS: [char; 4] = ['(', ')', ',', TARGET_SEPARATOR];
348
349/// `name`, a name a part spells, with every step `renamed` maps renamed:
350/// a target's or an element's name, located (`/hand:model`) or of the
351/// part itself (`/:sketch:base`, `extrude(box,end)`) — or, a `key`, a key
352/// of its state (`hand.pose`, `add_part(hand,m1).turn`). Behind an
353/// instance's path only that instance is a step of the part: the rest of
354/// the path, and the name, are the part placed's.
355fn rename_steps(name: &str, renamed: &HashMap<String, String>, key: bool) -> String {
356    map_steps(name, key, &mut |id| renamed.get(id).cloned())
357}
358
359/// Whether the key of the state `key` (see [`rename_steps`]) names the
360/// step `id`: is of it.
361fn names_step(key: &str, id: &str) -> bool {
362    let mut named = false;
363    map_steps(key, true, &mut |step| {
364        named |= step == id;
365        None
366    });
367    named
368}
369
370/// `name` (see [`rename_steps`]), each step it names as `map` maps it,
371/// where it maps it.
372fn map_steps(name: &str, key: bool, map: &mut dyn FnMut(&str) -> Option<String>) -> String {
373    if name.starts_with(INSTANCE_SEPARATOR) {
374        let Ok(located) = Located::parse(name) else {
375            return name.to_string();
376        };
377        let path = located.path();
378        if path.is_empty() {
379            return Located::new(map_steps(located.name(), key, map)).to_string();
380        }
381        let (instance, rest) = path.split_once(INSTANCE_SEPARATOR).unwrap_or((path, ""));
382        let Some(to) = map(instance) else {
383            return name.to_string();
384        };
385        let path = match rest.is_empty() {
386            true => to,
387            false => format!("{to}{INSTANCE_SEPARATOR}{rest}"),
388        };
389        return Located::in_instance(path, located.name()).to_string();
390    }
391    let mut spelled = String::with_capacity(name.len());
392    let mut rest = name;
393    let mut first = true;
394    loop {
395        let end = rest.find(NAME_SEPARATORS).unwrap_or(rest.len());
396        let (piece, after) = rest.split_at(end);
397        // A key of the state begins with what it is of, then `.` and what
398        // of it: `hand.pose`.
399        let mapped = match map(piece) {
400            Some(to) => Some(to),
401            None if key && first => (piece.match_indices('.'))
402                .find_map(|(at, _)| map(&piece[..at]).map(|to| format!("{to}{}", &piece[at..]))),
403            None => None,
404        };
405        spelled.push_str(mapped.as_deref().unwrap_or(piece));
406        let Some(separator) = after.chars().next() else {
407            return spelled;
408        };
409        spelled.push(separator);
410        rest = &after[separator.len_utf8()..];
411        first = false;
412    }
413}
414
415/// `args`, a step's arguments, every step `renamed` names in them renamed
416/// ([`rename_steps`]), but in the name of an element behind an instance's
417/// path, which is the part placed's: `{"target": "/hand:model", "name":
418/// "extrude(box,end)"}` names the hand's step `box`, not the part's.
419fn rename_in_args(args: &mut serde_json::Value, renamed: &HashMap<String, String>) {
420    if renamed.is_empty() {
421        return;
422    }
423    match args {
424        serde_json::Value::String(text) => *text = rename_steps(text, renamed, false),
425        serde_json::Value::Array(values) => {
426            values.iter_mut().for_each(|v| rename_in_args(v, renamed));
427        }
428        serde_json::Value::Object(fields) => {
429            let placed = (fields.get("target").and_then(|t| t.as_str()))
430                .and_then(|t| Located::parse(t).ok())
431                .is_some_and(|t| !t.path().is_empty());
432            for (key, value) in fields.iter_mut() {
433                if !(placed && key == "name") {
434                    rename_in_args(value, renamed);
435                }
436            }
437        }
438        _ => {}
439    }
440}
441
442fn no_step(id: &str) -> GeopError {
443    GeopError::new(format!("the program has no step `{id}`"))
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449
450    /// A step renamed is renamed wherever a name of the part names it — a
451    /// target of its own, an instance, an element built from it, a key of
452    /// its state — and nowhere a name is the part placed's or merely
453    /// begins like it.
454    #[test]
455    fn a_step_renamed_is_renamed_in_every_name_of_it() {
456        let renamed = HashMap::from([("hand".to_string(), "part1".to_string())]);
457        let name = |text: &str| rename_steps(text, &renamed, false);
458        assert_eq!(name("/:sketch:hand"), "/:sketch:part1");
459        assert_eq!(name("/hand:model"), "/part1:model");
460        assert_eq!(name("/hand/hand:model"), "/part1/hand:model");
461        assert_eq!(name("/fore:hand"), "/fore:hand");
462        assert_eq!(name("extrude(hand,end)"), "extrude(part1,end)");
463        assert_eq!(name("hand.geop"), "hand.geop");
464        assert_eq!(name("handle"), "handle");
465        let key = |text: &str| rename_steps(text, &renamed, true);
466        assert_eq!(key("hand.pose"), "part1.pose");
467        assert_eq!(key("add_part(hand,m1).turn"), "add_part(part1,m1).turn");
468        assert_eq!(key("/hand:b.pose"), "/part1:b.pose");
469        assert!(names_step("add_part(hand,m1).turn", "hand"));
470        assert!(!names_step("add_part(fore,m1).turn", "hand"));
471        let mut args = serde_json::json!({
472            "entities": [{"target": "/hand:model", "name": "block(hand,c1)"},
473                         {"target": "/:model", "name": "block(hand,c1)"}],
474        });
475        rename_in_args(&mut args, &renamed);
476        let expected = serde_json::json!({
477            "entities": [{"target": "/part1:model", "name": "block(hand,c1)"},
478                         {"target": "/:model", "name": "block(part1,c1)"}],
479        });
480        assert_eq!(args, expected);
481    }
482}