Skip to main content

geop_ops_model/parameters/
program.rs

1//! The parameters of a [`Program`], as its steps define them (see
2//! [`crate::parameters::steps`]): read and set.
3
4use std::collections::BTreeMap;
5
6use geop_core_math::geop_error::{GeopError, GeopResult};
7use geop_core_program::{
8    numeric::{ParameterValue, read_parameter_values},
9    operation_registry::OperationRegistry,
10    part::Part,
11    program::{Program, ProgramStep},
12    runner::Runner,
13};
14use serde::Serialize;
15
16use crate::parameters::{
17    definition::{NumberParameter, Parameter, ParameterKind, Parameters},
18    steps::{
19        DefineParameters, DefineParametersArgs, LOOKUP, LookupTable, LookupTableArgs, PARAMETERS,
20        PROPERTIES, PartPropertiesArgs,
21    },
22};
23
24/// The parameters and properties of `program`, as its steps define them
25/// (see [`crate::parameters::steps`]): its tables, as its [`LookupTable`] steps look
26/// them up, after its numbers — none, where it has no such step.
27pub fn parameters_of(program: &Program) -> Parameters {
28    let args = |kind: &str| program.steps.iter().find(|s| s.operation == kind);
29    let tables = (program.steps.iter())
30        .filter(|s| s.operation == LOOKUP)
31        .filter_map(|step| serde_json::from_value::<LookupTableArgs>(step.args.clone()).ok())
32        .map(|table| Parameter {
33            name: table.input,
34            kind: ParameterKind::Table {
35                columns: table.columns,
36                rows: table.rows,
37                selected: table.selected,
38            },
39        });
40    let numbers = (args(PARAMETERS))
41        .and_then(|step| serde_json::from_value::<DefineParametersArgs>(step.args.clone()).ok())
42        .unwrap_or_default()
43        .values
44        .into_iter()
45        .map(|p| Parameter {
46            name: p.name,
47            kind: ParameterKind::Number {
48                expression: p.expression,
49                min: p.min,
50                max: p.max,
51            },
52        });
53    let properties = (args(PROPERTIES))
54        .and_then(|step| serde_json::from_value::<PartPropertiesArgs>(step.args.clone()).ok())
55        .unwrap_or_default();
56    Parameters {
57        color: properties.color,
58        material: properties.material,
59        title: properties.title,
60        designation: properties.designation,
61        values: numbers.chain(tables).collect(),
62    }
63}
64
65/// `program` with its parameters and properties set to `parameters`: in
66/// the steps that define them, put first where it has none yet — and none,
67/// where there is nothing to define. Each table is a [`LookupTable`] step
68/// of its own, `table_<name>`, before the numbers, whose formulas may read
69/// its columns. The steps check them as they run. The colours of faces,
70/// which are no parameters of a part, stay as the program has them.
71pub fn set_parameters(program: &mut Program, parameters: Parameters) {
72    let face_colors = (program
73        .steps
74        .iter()
75        .find(|step| step.operation == PROPERTIES))
76    .and_then(|step| serde_json::from_value::<PartPropertiesArgs>(step.args.clone()).ok())
77    .map(|properties| properties.face_colors)
78    .unwrap_or_default();
79    let properties = PartPropertiesArgs {
80        title: parameters.title,
81        designation: parameters.designation,
82        color: parameters.color,
83        material: parameters.material,
84        face_colors,
85    };
86    let mut numbers = Vec::new();
87    let mut tables = Vec::new();
88    for p in parameters.values {
89        match p.kind {
90            ParameterKind::Number {
91                expression,
92                min,
93                max,
94            } => numbers.push(NumberParameter {
95                name: p.name,
96                expression,
97                min,
98                max,
99            }),
100            ParameterKind::Table {
101                columns,
102                rows,
103                selected,
104            } => tables.push(LookupTableArgs {
105                input: p.name,
106                columns,
107                rows,
108                selected,
109            }),
110        }
111    }
112    let values = DefineParametersArgs { values: numbers };
113    let empty = properties == PartPropertiesArgs::default();
114    set_step(program, PROPERTIES, (!empty).then_some(&properties));
115    set_step(
116        program,
117        PARAMETERS,
118        (!values.values.is_empty()).then_some(&values),
119    );
120    program.steps.retain(|step| step.operation != LOOKUP);
121    for (k, table) in tables.iter().enumerate() {
122        program.steps.insert(k, lookup_step(table));
123    }
124}
125
126/// The step looking `table` up.
127#[allow(
128    clippy::expect_used,
129    reason = "the arguments of a lookup step are plain data"
130)]
131fn lookup_step(table: &LookupTableArgs) -> ProgramStep {
132    ProgramStep {
133        id: format!("table_{}", table.input),
134        operation: LOOKUP.to_string(),
135        args: serde_json::to_value(table).expect("a step's arguments are plain data"),
136        active: None,
137    }
138}
139
140/// The step of `program` of the operation `kind` holding `args`: put first
141/// where there is none, under the kind's name; taken out, for none.
142#[allow(
143    clippy::expect_used,
144    reason = "the arguments of a parameters or properties step are plain data"
145)]
146fn set_step<A: Serialize>(program: &mut Program, kind: &str, args: Option<&A>) {
147    let steps = &mut program.steps;
148    let at = steps.iter().position(|s| s.operation == kind);
149    let Some(args) = args else {
150        if let Some(at) = at {
151            steps.remove(at);
152        }
153        return;
154    };
155    let args = serde_json::to_value(args).expect("a step's arguments are plain data");
156    match at {
157        Some(at) => steps[at].args = args,
158        None => {
159            let taken = |id: &str| steps.iter().any(|s| s.id == id);
160            let id = (std::iter::once(kind.to_string()))
161                .chain((1..).map(|n| format!("{kind}{n}")))
162                .find(|id| !taken(id))
163                .unwrap_or_else(|| kind.to_string());
164            let step = ProgramStep {
165                id,
166                operation: kind.to_string(),
167                args,
168                active: None,
169            };
170            steps.insert(0, step);
171        }
172    }
173}
174
175/// What the parameters of `program` come to as it defines them, by the names a
176/// formula reads them by: a number by its name, a table's columns as
177/// `table.column` — its steps defining them run on their own (see
178/// [`crate::parameters::steps::DefineParameters`], [`LookupTable`]). Fails naming one
179/// that does not come to anything.
180pub fn parameter_numbers(program: &Program) -> GeopResult<BTreeMap<String, f64>> {
181    let steps = (program.steps.iter())
182        .filter(|step| step.operation == PARAMETERS || step.operation == LOOKUP)
183        .cloned()
184        .collect();
185    let registry = (OperationRegistry::default().with(DefineParameters))
186        .and_then(|registry| registry.with(LookupTable))?;
187    let runner = Runner::new(registry);
188    let mut part = Part::default();
189    let defining = Program::from_steps(steps);
190    let output = runner.run(&mut part, &defining, None);
191    if let Some((step, e)) = output.step_errors.into_iter().next() {
192        return Err(e.with_context(format!("step {step:?}")));
193    }
194    let reader = part.current();
195    let values = read_parameter_values(&reader)?;
196    for p in &parameters_of(program).values {
197        if !values.0.iter().any(|(name, _)| *name == p.name) {
198            return Err(GeopError::new(format!(
199                "the parameter {:?} comes to nothing",
200                p.name
201            )));
202        }
203    }
204    Ok((values.0.iter())
205        .filter_map(|(name, value)| match value {
206            ParameterValue::Number(n) => Some((name.clone(), *n)),
207            ParameterValue::Text(_) => None,
208        })
209        .collect())
210}