Skip to main content

geop_core_editor/output/
mod.rs

1//! What is made *of* a part rather than in it: files written from what a
2//! program built ([`Exporter`]), and questions answered about it
3//! ([`Inspector`]). Neither is a step: exporting and inspecting change no
4//! program and are nothing to undo.
5//!
6//! A crate that writes a format, or answers a question, implements one of
7//! the traits on a unit struct, as an operation does; an editor offers
8//! those registered in its [`Outputs`], by their infos, and knows nothing
9//! more of them. The options each takes are its own, sent as JSON.
10
11mod export;
12mod inspect;
13
14pub use export::{Content, ErasedExporter, Export, Exporter, ExporterInfo};
15pub use inspect::{
16    Answer, AnswerAction, Block, Does, ErasedInspector, Inspection, Inspector, InspectorInfo,
17    Picks, Row,
18};
19
20use geop_core_math::{
21    geop_error::{GeopError, GeopResult},
22    scalars::Scalar,
23};
24use geop_core_program::{part::Part, program::Program, reader::Reader, runner::Runner};
25use serde::{Deserialize, Serialize, de::DeserializeOwned};
26
27/// A number as a report shows it — a value, and how far the truth may be
28/// from it: the midpoint of an enclosure, and its half-width.
29#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
30pub struct Bounded {
31    pub value: f64,
32    pub error: f64,
33}
34
35impl Bounded {
36    /// The enclosure `x`, as shown.
37    pub fn of(x: Scalar) -> Self {
38        let (lo, hi) = (x.lower().to_f64(), x.upper().to_f64());
39        Self {
40            value: 0.5 * (lo + hi),
41            error: 0.5 * (hi - lo),
42        }
43    }
44
45    /// It times `n`, as shown: a unit's amount for `n` units, or a value
46    /// in a unit `n` times smaller.
47    pub fn times(self, n: f64) -> Self {
48        Self {
49            value: self.value * n,
50            error: self.error * n,
51        }
52    }
53
54    /// Whether `exact` lies within the bounds.
55    pub fn contains(&self, exact: f64) -> bool {
56        (exact - self.value).abs() <= self.error
57    }
58
59    /// As a report shows it: its value to `digits` decimals, and `± error`
60    /// when the error shows at that precision.
61    pub fn show(&self, digits: usize) -> String {
62        let value = format!("{:.digits$}", self.value);
63        match self.error >= 0.5 * 10f64.powi(-(digits as i32)) {
64            true => format!("{value} ± {:.1e}", self.error),
65            false => value,
66        }
67    }
68
69    /// A mass in kilograms as a report shows it: in grams below one
70    /// kilogram.
71    pub fn show_mass(&self) -> String {
72        match self.value < 1.0 {
73            true => format!("{} g", self.times(1000.0).show(3)),
74            false => format!("{} kg", self.show(3)),
75        }
76    }
77}
78
79/// What a program built, as an exporter or an inspector is given it: the
80/// part as far as the program runs, the parts it places included, and the
81/// runner and the program it ran, with its state — to ask how its parts
82/// can move by running it again.
83pub struct Output<'a> {
84    pub runner: &'a Runner,
85    pub part: &'a mut Part,
86    pub file: &'a Program,
87    /// The step being edited, if one is: what an export of one step's
88    /// result (a drawing) writes unless told which.
89    pub editing: Option<&'a str>,
90    /// The day it is written, `YYYY-MM-DD`, as the front end says: what a
91    /// file is dated with. Empty if not told.
92    pub date: &'a str,
93}
94
95impl<'a> Output<'a> {
96    /// The part as far as the program runs.
97    pub fn part(&self) -> Reader<'_> {
98        self.part.current()
99    }
100
101    /// The part as it was before the step `step`: what a step that
102    /// describes an output of the part — a drawing — is of.
103    pub fn part_before(&self, step: &str) -> GeopResult<Reader<'_>> {
104        self.file.index_of(step)?;
105        Ok(self.part.before(step))
106    }
107
108    /// The program's file, as the runner ran it.
109    pub fn file_name(&self) -> &str {
110        &self.runner.location.file
111    }
112}
113
114/// The options of an exporter or inspector that takes none. Like any
115/// options, it ignores those it does not take, so a front end may send
116/// every exporter the same: a unit `()` would refuse them.
117#[derive(Clone, Copy, Debug, Default, Deserialize)]
118pub struct NoOptions {}
119
120/// The options `json` gives, as `T`; its defaults where it gives none
121/// (`null`).
122fn read_options<T: DeserializeOwned + Default>(
123    kind: &str,
124    json: &serde_json::Value,
125) -> GeopResult<T> {
126    match json {
127        serde_json::Value::Null => Ok(T::default()),
128        json => T::deserialize(json)
129            .map_err(|e| GeopError::new(format!("the options of `{kind}`: {e}"))),
130    }
131}
132
133/// The exporters and inspectors an editor offers, each by its kind, in the
134/// order they were added.
135#[derive(Default)]
136pub struct Outputs {
137    exporters: Vec<Box<dyn ErasedExporter>>,
138    inspectors: Vec<Box<dyn ErasedInspector>>,
139}
140
141impl Outputs {
142    /// Adds `exporter`. Fails if one of its kind is there already: a front
143    /// end could not tell the two apart.
144    pub fn exporter<E: Exporter>(mut self, exporter: E) -> GeopResult<Self> {
145        if self.exporters.iter().any(|e| e.info().kind == E::INFO.kind) {
146            return Err(GeopError::new(format!(
147                "exporter `{}` is registered twice",
148                E::INFO.kind
149            )));
150        }
151        self.exporters.push(Box::new(exporter));
152        Ok(self)
153    }
154
155    /// Adds `inspector`. Fails if one of its kind is there already.
156    pub fn inspector<I: Inspector>(mut self, inspector: I) -> GeopResult<Self> {
157        if self
158            .inspectors
159            .iter()
160            .any(|i| i.info().kind == I::INFO.kind)
161        {
162            return Err(GeopError::new(format!(
163                "inspector `{}` is registered twice",
164                I::INFO.kind
165            )));
166        }
167        self.inspectors.push(Box::new(inspector));
168        Ok(self)
169    }
170
171    /// What each exporter is, in the order they were added.
172    pub fn exporters(&self) -> Vec<&'static ExporterInfo> {
173        self.exporters.iter().map(|e| e.info()).collect()
174    }
175
176    /// What each inspector is, in the order they were added.
177    pub fn inspectors(&self) -> Vec<&'static InspectorInfo> {
178        self.inspectors.iter().map(|i| i.info()).collect()
179    }
180
181    /// The exporter `kind`.
182    pub fn get_exporter(&self, kind: &str) -> GeopResult<&dyn ErasedExporter> {
183        of_kind(&self.exporters, kind, "exporter", |e| e.info().kind)
184    }
185
186    /// The exporter that writes `format` — a file's extension, `stl` —
187    /// the first one added if several do.
188    pub fn exporter_for(&self, format: &str) -> GeopResult<&dyn ErasedExporter> {
189        let format = format.to_ascii_lowercase();
190        (self.exporters.iter())
191            .find(|e| e.info().formats.contains(&format.as_str()))
192            .map(Box::as_ref)
193            .ok_or_else(|| {
194                let known: Vec<&str> = (self.exporters.iter())
195                    .flat_map(|e| e.info().formats.iter().copied())
196                    .collect();
197                GeopError::new(format!(
198                    "no exporter writes `.{format}` files; they write: {}",
199                    known.join(", ")
200                ))
201            })
202    }
203
204    /// The inspector `kind`.
205    pub fn get_inspector(&self, kind: &str) -> GeopResult<&dyn ErasedInspector> {
206        of_kind(&self.inspectors, kind, "inspector", |i| i.info().kind)
207    }
208}
209
210/// The one of `entries` whose kind, as `kind_of` reads it, is `kind`; if
211/// there is none, an error naming the kinds there are, each a `what`.
212fn of_kind<'a, T: ?Sized>(
213    entries: &'a [Box<T>],
214    kind: &str,
215    what: &str,
216    kind_of: impl Fn(&T) -> &'static str,
217) -> GeopResult<&'a T> {
218    (entries
219        .iter()
220        .map(Box::as_ref)
221        .find(|&e| kind_of(e) == kind))
222    .ok_or_else(|| {
223        let known: Vec<&str> = entries.iter().map(Box::as_ref).map(&kind_of).collect();
224        GeopError::new(format!(
225            "there is no {what} `{kind}`; there are: {}",
226            known.join(", ")
227        ))
228    })
229}