Skip to main content

geop_ops_model/parameters/
steps.rs

1//! The steps a program says what its part is by, besides its shape: its
2//! parameters ([`DefineParameters`]), the tables a part family looks its
3//! size up in ([`LookupTable`]) and its properties ([`PartProperties`]).
4//! Each produces targets as any step does: a formula reads a parameter's
5//! target, and what lists the part reads its properties'. Every step that
6//! defines parameters adds them to the part's [`ParameterValues`], a chain.
7//!
8//! What a parameter is set to where the part is built is the run's input
9//! of its name — a [`Setting`] a program placing the part gives — and its
10//! own definition where none is given. The colour is set the same way, by
11//! the input [`COLOR`], and the part is drawn in it ([`PartColor`]).
12
13use std::borrow::Cow;
14
15use geop_core_editor::{
16    edit_form::{EditForm, FormState},
17    editable::{Context, OperationGroup, OperationInfo, OperationTier},
18    editing::{
19        Action, Cell, Choice, Column, ColumnKind, Control, Layout, Table, TableEdit, TableRow,
20        Tone, Unit,
21    },
22    part_operation,
23    part_operation::PartOperation,
24    registry::{Drawer, StructureItem},
25    view::PartView,
26};
27use geop_core_math::geop_error::{GeopError, GeopResult, WithContext};
28use geop_core_program::{
29    element::{ElementReference, HasElements, Kind, whole},
30    numeric::{
31        ParameterValue, ParameterValues, evaluate, names_in, parameter, parameter_values,
32        read_parameter, read_parameter_values,
33    },
34    part::StepContext,
35    reader::Reader,
36    target::{Target, TargetReference},
37    values::Setting,
38};
39use serde::{Deserialize, Serialize};
40
41use crate::{
42    Face, NamedModel,
43    parameters::definition::{
44        COLOR, Material, NumberParameter, Row, validate_color, validate_name,
45    },
46};
47
48/// The kind of the step defining a program's parameters, and its id.
49pub const PARAMETERS: &str = "parameters";
50
51/// The kind of the step defining a part's properties, and its id.
52pub const PROPERTIES: &str = "properties";
53
54/// The kind of the step looking a part family's size up in a table.
55pub const LOOKUP: &str = "lookup_table";
56
57/// Defines the program's number parameters, each a target of its own
58/// ([`parameter`]): what the run's input of its name sets it to, else what
59/// it is defined as. A formula may read any other parameter of the step,
60/// defined before it or after, and those the steps before defined — a
61/// table's columns; one reading one that reads it back fails, saying so.
62#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
63pub struct DefineParameters;
64
65#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
66pub struct DefineParametersArgs {
67    #[serde(default)]
68    pub values: Vec<NumberParameter>,
69}
70
71part_operation!(DefineParameters(DefineParametersArgs));
72
73impl PartOperation for DefineParameters {
74    const KIND: &'static str = PARAMETERS;
75    const INFO: OperationInfo = OperationInfo::new(
76        "Parameters",
77        "The numbers the design is given by, which a program placing the part may set.",
78        OperationGroup::Part,
79        OperationTier::Small,
80    )
81    .shortcut("Shift+V")
82    .icon(crate::parameters::icons::PARAMETERS);
83    type Args = DefineParametersArgs;
84    type Session = ();
85
86    fn new_args(_: &Context) -> DefineParametersArgs {
87        DefineParametersArgs::default()
88    }
89
90    /// Each parameter, in an order in which a formula's parameters come
91    /// before it, and then the step's entries of the part's parameter
92    /// values.
93    fn run(ctx: &mut StepContext, args: &DefineParametersArgs) -> GeopResult<()> {
94        let step = ctx.step();
95        for (i, p) in args.values.iter().enumerate() {
96            validate_name(&p.name).map_err(|e| e.with_context(format!("parameters({step})")))?;
97            if args.values[..i].iter().any(|q| q.name == p.name) {
98                return Err(GeopError::new(format!(
99                    "there is more than one parameter {:?}",
100                    p.name
101                )));
102            }
103        }
104        for p in in_order(&args.values) {
105            ctx.produce(&parameter(&p.name), p, value_of)?;
106        }
107        let names: Vec<String> = args.values.iter().map(|p| p.name.clone()).collect();
108        ctx.produce(&parameter_values(), &names, add_values)
109    }
110
111    /// The parameters as a table, in a popup: a row each, of its name, its
112    /// value — a number or a formula, and what it comes to in the part as
113    /// the step built it — and what a slider offers, from and to. A row
114    /// added is a fresh `length` of 10. A name typed here renames the
115    /// definition alone: a formula still reading the old name fails,
116    /// naming it, until it is changed too.
117    fn form<'a>(
118        ctx: &'a Context<'a>,
119        args: &DefineParametersArgs,
120        _: &FormState<()>,
121    ) -> EditForm<'a, DefineParametersArgs> {
122        let mut f = EditForm::<DefineParametersArgs>::default();
123        f.layout(Layout::Popup);
124        let reader = ctx.built.as_ref().unwrap_or(&ctx.before);
125        let columns = vec![
126            Column::new("Name", ColumnKind::Text),
127            Column::new("Value", ColumnKind::Formula),
128            Column::new("Slider from", ColumnKind::Number { unit: Unit::Length }),
129            Column::new("to", ColumnKind::Number { unit: Unit::Length }),
130        ];
131        let optional = |n: Option<f64>| Cell::new(n.map(|n| n.to_string()).unwrap_or_default());
132        let rows = (args.values.iter())
133            .map(|p| {
134                let value = match evaluate(&p.expression, |name| read_parameter(reader, name)) {
135                    Ok(value) => Cell::new(&p.expression).detail(format!("= {value}"), Tone::Hint),
136                    Err(e) => Cell::new(&p.expression).detail(e.root_message(), Tone::Error),
137                };
138                TableRow::new(vec![
139                    Cell::new(&p.name),
140                    value,
141                    optional(p.min),
142                    optional(p.max),
143                ])
144            })
145            .collect();
146        let table = Table::new(columns, rows, "No parameters yet: add a row.");
147        f.table("parameters", table, |edit, what| {
148            let values = &mut edit.args.values;
149            match what {
150                TableEdit::Cell { row, column, text } => {
151                    let Some(p) = values.get_mut(row) else {
152                        return;
153                    };
154                    match column {
155                        0 => p.name = text.trim().to_string(),
156                        1 => p.expression = text.trim().to_string(),
157                        2 => p.min = text.trim().parse().ok(),
158                        3 => p.max = text.trim().parse().ok(),
159                        _ => {}
160                    }
161                }
162                TableEdit::AddRow => {
163                    let names: Vec<String> = values.iter().map(|p| p.name.clone()).collect();
164                    values.push(NumberParameter {
165                        name: fresh_name(&names, "length"),
166                        expression: "10".into(),
167                        min: None,
168                        max: None,
169                    });
170                }
171                TableEdit::RemoveRow { row } => {
172                    if row < values.len() {
173                        values.remove(row);
174                    }
175                }
176                TableEdit::MoveRow { row, to } => move_row(values, row, to),
177                // No row is chosen, and the columns are the step's.
178                TableEdit::SelectRow { .. }
179                | TableEdit::Column { .. }
180                | TableEdit::AddColumn
181                | TableEdit::RemoveColumn { .. }
182                | TableEdit::Paste { .. } => {}
183            }
184        });
185        f
186    }
187}
188
189/// The row `row` of `rows` moved to be the row `to`; nothing, for a row
190/// there is none of.
191fn move_row<R>(rows: &mut Vec<R>, row: usize, to: usize) {
192    if row < rows.len() {
193        let moved = rows.remove(row);
194        rows.insert(to.min(rows.len()), moved);
195    }
196}
197
198/// The part's parameter values as the steps before left them, and the
199/// parameters `names` as they come to — a parameter that failed left out.
200/// The make of a step's entries of the [`ParameterValues`] chain.
201#[allow(clippy::ptr_arg, reason = "a make takes its arguments as produced")]
202fn add_values(reader: &Reader, names: &Vec<String>) -> GeopResult<ParameterValues> {
203    let mut values: ParameterValues = reader.previous()?;
204    values.0.extend((names.iter()).filter_map(|name| {
205        let value = reader.retrieve_element(&ElementReference::whole(parameter(name)));
206        Some((name.clone(), value.ok()?.clone()))
207    }));
208    Ok(values)
209}
210
211/// `values` in an order in which a number comes after every parameter of
212/// the step its formula names: the numbers as their formulas' names are
213/// defined, then — reading what is not there, or reading each other — the
214/// rest, which fail.
215fn in_order(values: &[NumberParameter]) -> Vec<&NumberParameter> {
216    let mut ordered: Vec<&NumberParameter> = Vec::with_capacity(values.len());
217    let mut left: Vec<&NumberParameter> = values.iter().collect();
218    loop {
219        let ready = left.iter().position(|p| {
220            names_in(&p.expression).iter().all(|name| {
221                ordered.iter().any(|q| q.name == *name) || !values.iter().any(|q| q.name == *name)
222            })
223        });
224        match ready {
225            Some(at) => ordered.push(left.remove(at)),
226            None => break,
227        }
228    }
229    ordered.extend(left);
230    ordered
231}
232
233/// What the parameter `p` is set to: the run's input of its name if it is
234/// given a number, else its formula of the other parameters.
235fn value_of(reader: &Reader, p: &NumberParameter) -> GeopResult<ParameterValue> {
236    match reader.retrieve_input(&p.name) {
237        Some(Setting::Number(value)) => Ok(ParameterValue::Number(value)),
238        _ => Ok(ParameterValue::Number(evaluate(&p.expression, |name| {
239            read_parameter(reader, name)
240        })?)),
241    }
242}
243
244/// Looks the row a part family is built in up in a table: the row the
245/// run's text input `input` names — `size = "M4"` from a program placing
246/// the part — else the row `selected`. Defines the parameter `input` as the
247/// row's name, and `input.column` as its value in each column (`size.d`),
248/// each a target of its own ([`parameter`]), and adds them to the part's
249/// [`ParameterValues`]. A text that names no row fails, naming the input
250/// and the rows.
251#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
252pub struct LookupTable;
253
254/// A table of the variants of a part family, one per row, looked up by the
255/// input `input`.
256#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
257pub struct LookupTableArgs {
258    pub input: String,
259    pub columns: Vec<String>,
260    pub rows: Vec<Row>,
261    /// The row built where the run names none.
262    pub selected: String,
263}
264
265impl LookupTableArgs {
266    /// Checks the names of the input and the columns, that every row has a
267    /// value per column, and that the row `selected` is there.
268    pub fn validate(&self) -> GeopResult<()> {
269        let ctx = |e: GeopError| e.with_context(format!("the table {:?}", self.input));
270        validate_name(&self.input).map_err(ctx)?;
271        for c in &self.columns {
272            validate_name(c).map_err(|e| ctx(e.with_context("a column")))?;
273        }
274        for row in &self.rows {
275            if row.values.len() != self.columns.len() {
276                return Err(ctx(GeopError::new(format!(
277                    "row {:?} has {} values for {} columns",
278                    row.name,
279                    row.values.len(),
280                    self.columns.len()
281                ))));
282            }
283        }
284        if !self.rows.iter().any(|r| r.name == self.selected) {
285            return Err(ctx(GeopError::new(format!(
286                "there is no row {:?} to select",
287                self.selected
288            ))));
289        }
290        Ok(())
291    }
292
293    /// The name the value of `column` is read by: `size.d`.
294    pub fn column(&self, column: &str) -> String {
295        format!("{}.{column}", self.input)
296    }
297}
298
299part_operation!(LookupTable(LookupTableArgs));
300
301impl PartOperation for LookupTable {
302    const KIND: &'static str = LOOKUP;
303    const INFO: OperationInfo = OperationInfo::new(
304        "Table",
305        "A table of the variants of a part family — screw sizes, say — of which a program placing the part picks one by name.",
306        OperationGroup::Part,
307        OperationTier::Small,
308    )
309    .icon(crate::parameters::icons::LOOKUP_TABLE);
310    type Args = LookupTableArgs;
311    type Session = ();
312
313    fn new_args(_: &Context) -> LookupTableArgs {
314        LookupTableArgs {
315            input: "size".into(),
316            columns: vec!["d".into()],
317            rows: vec![Row {
318                name: "A".into(),
319                values: vec![1.0],
320            }],
321            selected: "A".into(),
322        }
323    }
324
325    /// The row, then each column of it, then the step's entries of the
326    /// part's parameter values.
327    fn run(ctx: &mut StepContext, args: &LookupTableArgs) -> GeopResult<()> {
328        args.validate()
329            .map_err(|e| e.with_context(format!("lookup_table({})", ctx.step())))?;
330        ctx.produce(&parameter(&args.input), args, row_of)?;
331        let mut names = vec![args.input.clone()];
332        for (k, column) in args.columns.iter().enumerate() {
333            let name = args.column(column);
334            ctx.produce(
335                &parameter(&name),
336                &(args.input.clone(), k, args.rows.clone()),
337                column_of,
338            )?;
339            names.push(name);
340        }
341        ctx.produce(&parameter_values(), &names, add_values)
342    }
343
344    /// The table, in a popup: the input it is looked up by, and its rows —
345    /// each named in the first column, the one built where the run names
346    /// none chosen — with a value per column; the columns are the user's,
347    /// named in their headers. A column or a row added is named afresh and
348    /// its values are 0; the last row is not taken out, and taking out the
349    /// row chosen chooses the first left.
350    fn form<'a>(
351        _: &'a Context<'a>,
352        args: &LookupTableArgs,
353        _: &FormState<()>,
354    ) -> EditForm<'a, LookupTableArgs> {
355        let mut f = EditForm::<LookupTableArgs>::default();
356        f.layout(Layout::Popup);
357        f.input(
358            "input",
359            "Looked up by",
360            &args.input,
361            Some("size"),
362            |edit, name| edit.args.input = name.trim().to_string(),
363        );
364        let mut columns = vec![Column::new("Row", ColumnKind::Text)];
365        columns.extend((args.columns.iter()).map(|column| {
366            Column::new(column, ColumnKind::Number { unit: Unit::Length }).editable()
367        }));
368        let rows = (args.rows.iter())
369            .map(|row| TableRow {
370                cells: (std::iter::once(Cell::new(&row.name)))
371                    .chain(row.values.iter().map(|v| Cell::number(*v)))
372                    .collect(),
373                selected: row.name == args.selected,
374                removable: args.rows.len() > 1,
375            })
376            .collect();
377        let table = Table::new(columns, rows, "No rows: add one.")
378            .columns_editable()
379            .selectable();
380        f.table("table", table, |edit, what| {
381            let args = &mut *edit.args;
382            match what {
383                TableEdit::Cell {
384                    row,
385                    column: 0,
386                    text,
387                } => {
388                    let Some(r) = args.rows.get_mut(row) else {
389                        return;
390                    };
391                    let name = text.trim().to_string();
392                    if args.selected == r.name {
393                        args.selected = name.clone();
394                    }
395                    r.name = name;
396                }
397                TableEdit::Cell { row, column, text } => {
398                    let cell = (args.rows.get_mut(row)).and_then(|r| r.values.get_mut(column - 1));
399                    if let (Ok(n), Some(cell)) = (text.trim().parse::<f64>(), cell) {
400                        *cell = n;
401                    }
402                }
403                TableEdit::AddRow => {
404                    let names: Vec<String> = args.rows.iter().map(|r| r.name.clone()).collect();
405                    args.rows.push(Row {
406                        name: fresh_name(&names, "row"),
407                        values: vec![0.0; args.columns.len()],
408                    });
409                }
410                TableEdit::RemoveRow { row } => {
411                    if args.rows.len() > 1 && row < args.rows.len() {
412                        let removed = args.rows.remove(row);
413                        if removed.name == args.selected {
414                            args.selected = args.rows[0].name.clone();
415                        }
416                    }
417                }
418                TableEdit::MoveRow { row, to } => move_row(&mut args.rows, row, to),
419                TableEdit::SelectRow { row } => {
420                    if let Some(r) = args.rows.get(row) {
421                        args.selected = r.name.clone();
422                    }
423                }
424                // The first column names the rows, and is the step's.
425                TableEdit::Column { column: 0, .. } | TableEdit::RemoveColumn { column: 0 } => {}
426                TableEdit::Column { column, text } => {
427                    if let Some(c) = args.columns.get_mut(column - 1) {
428                        *c = text.trim().to_string();
429                    }
430                }
431                TableEdit::AddColumn => {
432                    args.columns.push(fresh_name(&args.columns, "value"));
433                    for row in &mut args.rows {
434                        row.values.push(0.0);
435                    }
436                }
437                TableEdit::RemoveColumn { column } => {
438                    if column - 1 < args.columns.len() {
439                        args.columns.remove(column - 1);
440                        for row in &mut args.rows {
441                            if column - 1 < row.values.len() {
442                                row.values.remove(column - 1);
443                            }
444                        }
445                    }
446                }
447                TableEdit::Paste { .. } => {}
448            }
449        });
450        f
451    }
452}
453
454/// `base`, or `base2`, `base3`, ... — the first not among `names`.
455fn fresh_name(names: &[String], base: &str) -> String {
456    let taken = |name: &str| names.iter().any(|n| n == name);
457    let mut name = base.to_string();
458    let mut n = 1;
459    while taken(&name) {
460        n += 1;
461        name = format!("{base}{n}");
462    }
463    name
464}
465
466/// The row the table `args` is set to: the one the run's input names, else
467/// its own selection. Fails for an input naming no row, or no text.
468fn row_of(reader: &Reader, args: &LookupTableArgs) -> GeopResult<ParameterValue> {
469    let wanted = match reader.retrieve_input(&args.input) {
470        None => args.selected.clone(),
471        Some(Setting::Text(row)) => row,
472        Some(Setting::Number(n)) => {
473            return Err(GeopError::new(format!(
474                "the input {:?} is the number {n}: it names a row of its table by text",
475                args.input
476            )));
477        }
478    };
479    match args.rows.iter().any(|row| row.name == wanted) {
480        true => Ok(ParameterValue::Text(wanted)),
481        false => {
482            let rows: Vec<&str> = args.rows.iter().map(|row| row.name.as_str()).collect();
483            Err(GeopError::new(format!(
484                "the input {:?} is {wanted:?}, which names no row of its table: {}",
485                args.input,
486                rows.join(", ")
487            )))
488        }
489    }
490}
491
492/// The value in the column at `k` of the row the table looked up by
493/// `input`, of `rows`, is set to.
494fn column_of(
495    reader: &Reader,
496    (input, k, rows): &(String, usize, Vec<Row>),
497) -> GeopResult<ParameterValue> {
498    let row = reader.retrieve_element(&ElementReference::whole(parameter(input)))?;
499    let ParameterValue::Text(row) = row else {
500        return Err(GeopError::new(format!(
501            "the parameter {input:?} names no row"
502        )));
503    };
504    let values = (rows.iter().find(|r| r.name == *row))
505        .map(|r| &r.values)
506        .ok_or_else(|| GeopError::new(format!("the table {input:?} has no row {row:?}")))?;
507    (values.get(*k).copied())
508        .map(ParameterValue::Number)
509        .ok_or_else(|| {
510            GeopError::new(format!(
511                "row {row:?} of the table {input:?} has no column {k}"
512            ))
513        })
514}
515
516/// Defines what the part is called and ordered as, its colours, and what
517/// it is made of: its [`properties`]. The colour is the run's input
518/// [`COLOR`] where it is given one.
519///
520/// Its colours of faces name faces later steps make, and it comes first in
521/// most programs, so it reads none of them: it holds their names, and the
522/// viewer asks for a face's colour by the face's name. A name no face has
523/// now fails nothing, and its dialog says so; a colour changed builds
524/// nothing again. It is edited with the whole part built and picks in that
525/// ([`OperationInfo::picks_whole`]).
526#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
527pub struct PartProperties;
528
529/// What a part is, besides its shape.
530#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
531pub struct PartPropertiesArgs {
532    /// What the part is, in words — `ISO 4762 socket head cap screw` —
533    /// where it is listed: a bill of materials, a drawing's. None for a
534    /// part known by its file's name.
535    #[serde(default, skip_serializing_if = "Option::is_none")]
536    pub title: Option<String>,
537    /// What the part is ordered as, before the parameters it is built with
538    /// (see [`Properties::designation`]): a norm — `ISO 4762` — or a
539    /// catalogue or part number. None for a part that is made rather than
540    /// bought.
541    #[serde(default, skip_serializing_if = "Option::is_none")]
542    pub designation: Option<String>,
543    /// The part's colour, `#rrggbb`; none for the viewer's own.
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub color: Option<String>,
546    /// What the part is made of; none for a part whose material is not
547    /// given, which has a volume and a centroid but no mass.
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub material: Option<Material>,
550    /// Faces in a colour of their own, over the part's: of a face named
551    /// twice, the later.
552    #[serde(default, skip_serializing_if = "Vec::is_empty")]
553    pub face_colors: Vec<FaceColor>,
554}
555
556/// Faces of the part in one colour.
557#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
558pub struct FaceColor {
559    pub faces: Vec<ElementReference<NamedModel, Face>>,
560    /// `#rrggbb`.
561    pub color: String,
562}
563
564impl FaceColor {
565    /// The list of `args`' colours of faces, each its faces picked, its
566    /// colour and a button taking it out, and a button adding one. Which
567    /// of the faces picked no longer are in `whole`, the part as built, or
568    /// are not the part's own, is said beside each; `whole` is only asked
569    /// for a dialog open (`open`), not for a list of steps.
570    fn fields<'a>(
571        f: &mut EditForm<'a, PartPropertiesArgs>,
572        whole: &Reader<'a>,
573        args: &PartPropertiesArgs,
574        open: bool,
575    ) {
576        f.form.dialog.heading("face_colors", "Colours of faces");
577        for (i, entry) in args.face_colors.iter().enumerate() {
578            let key = face_color_key(i, "faces");
579            f.pick(&key, "Faces", whole, &entry.faces, true, move |e, faces| {
580                if let Some(entry) = e.args.face_colors.get_mut(i) {
581                    entry.faces = faces;
582                }
583            });
584            let field = f.form.dialog.0.iter_mut().find(|field| field.key == key);
585            if let Some(Control::Reference(reference)) = field.map(|field| &mut field.control) {
586                for (picked, face) in reference.value.iter_mut().zip(&entry.faces) {
587                    // The face in its colour says that it is held: lit,
588                    // the colour would not show.
589                    picked.drawn.clear();
590                    if let Some(why) = open.then(|| not_drawn(face, whole)).flatten() {
591                        picked.tone = Tone::Error;
592                        picked.detail = Some(why);
593                    }
594                }
595            }
596            let color = face_color_key(i, "color");
597            f.color(&color, "Colour", &entry.color, move |e, c| {
598                if let Some(entry) = e.args.face_colors.get_mut(i) {
599                    entry.color = c.trim().to_string();
600                }
601            });
602            let actions = vec![Action::new(REMOVE, "Take out")];
603            f.actions(&face_color_key(i, "remove"), actions, move |e, _| {
604                if i < e.args.face_colors.len() {
605                    e.args.face_colors.remove(i);
606                }
607            });
608        }
609        let actions = vec![Action::new(ADD, "Add a colour of faces")];
610        f.actions("face_color_add", actions, |e, _| {
611            e.args.face_colors.push(FaceColor {
612                faces: Vec::new(),
613                color: NEW_FACE_COLOR.to_string(),
614            });
615        });
616    }
617}
618
619/// The key of the field `field` of the `i`th colour of faces.
620fn face_color_key(i: usize, field: &str) -> String {
621    format!("face_color:{i}:{field}")
622}
623
624/// The value of the button taking a colour of faces out.
625const REMOVE: &str = "remove";
626
627/// The value of the button adding a colour of faces.
628const ADD: &str = "add";
629
630/// The colour a new colour of faces starts in: one a face picked shows
631/// itself in at once, whatever the part's.
632const NEW_FACE_COLOR: &str = "#d9483b";
633
634/// Why the face `face` is drawn in no colour of the properties of the part
635/// `reader` reads, if it is not: it is not a face of that part.
636fn not_drawn(face: &ElementReference<NamedModel, Face>, reader: &Reader) -> Option<String> {
637    if !face.instance().is_empty() {
638        return Some(format!(
639            "of the placed part `{}`: give it a colour in that part's properties",
640            face.instance()
641        ));
642    }
643    (reader.retrieve_element(face).err())
644        .map(|_| "no face of the part has this name now: take it out, or pick another".into())
645}
646
647part_operation!(PartProperties(PartPropertiesArgs));
648
649impl PartOperation for PartProperties {
650    const KIND: &'static str = PROPERTIES;
651    const INFO: OperationInfo = OperationInfo::new(
652        "Properties",
653        "What the part is called and ordered as, its colour, and what it is made of.",
654        OperationGroup::Part,
655        OperationTier::Small,
656    )
657    .icon(crate::parameters::icons::PROPERTIES)
658    .picks_whole();
659    type Args = PartPropertiesArgs;
660    type Session = ();
661
662    fn new_args(_: &Context) -> PartPropertiesArgs {
663        PartPropertiesArgs::default()
664    }
665
666    fn run(ctx: &mut StepContext, args: &PartPropertiesArgs) -> GeopResult<()> {
667        let step = ctx.step();
668        if let Some(color) = &args.color {
669            validate_color(color).map_err(|e| e.with_context(format!("properties({step})")))?;
670        }
671        for (i, entry) in args.face_colors.iter().enumerate() {
672            validate_color(&entry.color)
673                .map_err(|e| e.with_context(format!("properties({step}), colour of faces {i}")))
674                .concerning(&face_color_key(i, "color"))?;
675        }
676        if let Some(Material { name, density }) = &args.material
677            && !(density.is_finite() && *density > 0.0)
678        {
679            return Err(GeopError::new(format!(
680                "the material {name:?} has a density of {density} kg/m³: it must be a positive number"
681            )));
682        }
683        ctx.produce(&properties(), args, |reader, args| {
684            let color = match reader.retrieve_input(COLOR) {
685                Some(Setting::Text(color)) if validate_color(&color).is_ok() => Some(color),
686                _ => args.color.clone(),
687            };
688            let designation = (args.designation.clone()).map(|designation| {
689                let values = read_parameter_values(reader).unwrap_or_default();
690                designate(&designation, &values)
691            });
692            Ok(Properties {
693                title: args.title.clone(),
694                designation,
695                color,
696                material: args.material.clone(),
697                face_colors: args.face_colors.clone(),
698            })
699        })
700    }
701
702    /// Its title and designation typed, its colour picked, some faces in
703    /// colours of their own, and its material chosen from the list
704    /// ([`Material::listed`]), or none, or any other by its name and
705    /// density.
706    fn form<'a>(
707        context: &'a Context<'a>,
708        args: &PartPropertiesArgs,
709        _: &FormState<()>,
710    ) -> EditForm<'a, PartPropertiesArgs> {
711        let mut f = EditForm::<PartPropertiesArgs>::default();
712        let text = |text: &str| (!text.trim().is_empty()).then(|| text.trim().to_string());
713        let title = args.title.clone().unwrap_or_default();
714        f.input(
715            "title",
716            "Title",
717            title,
718            Some("the file's name"),
719            move |e, t| {
720                e.args.title = text(t);
721            },
722        );
723        let designation = args.designation.clone().unwrap_or_default();
724        f.input(
725            "designation",
726            "Designation",
727            designation,
728            Some("none"),
729            move |e, t| {
730                e.args.designation = text(t);
731            },
732        );
733        let color = args.color.clone().unwrap_or_default();
734        f.color("color", "Colour", color, move |e, c| e.args.color = text(c));
735        FaceColor::fields(&mut f, &context.whole, args, context.built.is_some());
736        let listed = |m: &Material| Material::named(&m.name).as_ref() == Some(m);
737        let chosen = match &args.material {
738            None => NO_MATERIAL,
739            Some(m) if listed(m) => m.name.as_str(),
740            Some(_) => OTHER_MATERIAL,
741        };
742        let options = (std::iter::once(Choice::new(NO_MATERIAL, "None")))
743            .chain(Material::listed().map(|m| {
744                let label = format!("{} — {} kg/m³", m.name, m.density);
745                Choice::new(m.name, label)
746            }))
747            .chain(std::iter::once(Choice::new(
748                OTHER_MATERIAL,
749                "Other, by its density…",
750            )))
751            .collect();
752        f.select(
753            "material",
754            "Material",
755            chosen,
756            options,
757            true,
758            move |e, choice| {
759                e.args.material = match choice {
760                    NO_MATERIAL => None,
761                    // One not listed already stays as it is; one listed gives
762                    // its density to start from.
763                    OTHER_MATERIAL => match e.args.material.take() {
764                        Some(m) if !listed(&m) => Some(m),
765                        m => Some(Material {
766                            name: "Other".into(),
767                            density: m.map_or(OTHER_DENSITY, |m| m.density),
768                        }),
769                    },
770                    name => Material::named(name),
771                };
772            },
773        );
774        if let Some(material) = args.material.as_ref().filter(|m| !listed(m)) {
775            f.input(
776                "material_name",
777                "Name",
778                material.name.clone(),
779                None,
780                move |e, t| {
781                    if let (Some(material), Some(name)) = (&mut e.args.material, text(t)) {
782                        material.name = name;
783                    }
784                },
785            );
786            let density = material.density.to_string();
787            f.input("density", "Density (kg/m³)", density, None, |e, t| {
788                if let (Some(material), Ok(density)) = (&mut e.args.material, t.trim().parse()) {
789                    material.density = density;
790                }
791            });
792        }
793        if args.material.is_none() {
794            let hint = "No material: the part has a volume, but no mass.";
795            f.form.dialog.text("no_material", hint, Tone::Hint);
796        }
797        f
798    }
799}
800
801/// What the material field sends for none.
802const NO_MATERIAL: &str = "";
803
804/// What the material field sends for one not listed, given by its density.
805const OTHER_MATERIAL: &str = "other";
806
807/// The density (kg/m³) a material not listed starts from when no material
808/// was chosen before, until its own is typed.
809const OTHER_DENSITY: f64 = 1000.0;
810
811/// What the part designated `designation` built with `values` is ordered
812/// as: the designation, then each parameter in order — a table's row by its
813/// name, a number after its own, a table's columns not at all —
814/// `ISO 4762 M4x12`, `T-slot 2020 length 500`.
815fn designate(designation: &str, values: &ParameterValues) -> String {
816    let mut words = vec![designation.to_string()];
817    for (name, value) in &values.0 {
818        match value {
819            ParameterValue::Text(row) => words.push(row.clone()),
820            ParameterValue::Number(_) if name.contains('.') => {}
821            ParameterValue::Number(n) => words.push(format!("{name} {n}")),
822        }
823    }
824    words.join(" ")
825}
826
827/// What a part is, besides its shape, as its program says: what it is
828/// called and ordered as, its colour and what it is made of.
829#[derive(Clone, Debug, Default, PartialEq)]
830pub struct Properties {
831    pub title: Option<String>,
832    /// What it is ordered as, with what it was built with: none for a part
833    /// made rather than bought.
834    pub designation: Option<String>,
835    pub color: Option<String>,
836    pub material: Option<Material>,
837    pub face_colors: Vec<FaceColor>,
838}
839
840/// Defined once, by the program's [`PartProperties`] step.
841impl Target for Properties {}
842
843/// Read whole.
844impl Kind for Properties {
845    const NAME: &'static str = "properties";
846}
847
848impl HasElements<Properties> for Properties {
849    type Element<'a> = &'a Properties;
850    const SINGLETON: Option<&'static str> = Some(PROPERTIES);
851
852    fn element(&self, name: &str) -> GeopResult<&Properties> {
853        whole(self, name)
854    }
855}
856
857/// The part's properties, defined by its [`PartProperties`] step.
858pub fn properties() -> TargetReference<Properties> {
859    TargetReference::new(PROPERTIES)
860}
861
862/// The properties of the part `reader` reads: none set, for a part whose
863/// program says nothing of them.
864pub fn read_properties<'a>(reader: &Reader<'a>) -> GeopResult<Cow<'a, Properties>> {
865    if !reader.defines(&properties()) {
866        return Ok(Cow::Owned(Properties::default()));
867    }
868    Ok(Cow::Borrowed(reader.retrieve_element(
869        &ElementReference::whole(properties()),
870    )?))
871}
872
873/// Draws a part in the colour its properties give it (see
874/// [`PartProperties`]).
875pub struct PartColor;
876
877impl Drawer for PartColor {
878    fn draw(&self, reader: &Reader, view: &mut PartView) -> GeopResult<()> {
879        read_properties(reader)?.draw(view);
880        Ok(())
881    }
882
883    fn structure(&self, _: &Reader) -> GeopResult<Vec<StructureItem>> {
884        Ok(Vec::new())
885    }
886}
887
888impl Properties {
889    /// What its part is made of: none for a part whose material is not
890    /// given, which then has no mass.
891    pub fn material(&self) -> Option<&Material> {
892        self.material.as_ref()
893    }
894
895    /// Its part's colour, `#rrggbb`: none for the viewer's own.
896    pub fn color(&self) -> Option<&str> {
897        self.color.as_deref()
898    }
899
900    /// Its colours drawn: its part's, and its faces', by name, of the part's
901    /// own faces: nothing of the faces is read, so changing one draws
902    /// them again, and meshes nothing.
903    pub fn draw(&self, view: &mut PartView) {
904        view.color = self.color.clone();
905        view.face_colors = (self.face_colors.iter())
906            .flat_map(|entry| entry.faces.iter().map(|face| (face, &entry.color)))
907            .filter(|(face, _)| face.instance().is_empty())
908            .map(|(face, color)| (face.name().to_string(), color.clone()))
909            .collect();
910    }
911
912    /// What its part is called where it is listed: its title, else `stem`
913    /// — the name of the file it is placed from.
914    pub fn name<'a>(&'a self, stem: &'a str) -> &'a str {
915        self.title.as_deref().unwrap_or(stem)
916    }
917}
918
919/// The name of the file `file`, without folders or `.geop`: what a part
920/// placed from it is listed as without a title, and what a file written
921/// from it is named after — `part` if that leaves nothing.
922pub fn stem(file: &str) -> &str {
923    let file = file.rsplit(['/', ':']).next().unwrap_or_default();
924    match file.trim_end_matches(".geop") {
925        "" => "part",
926        stem => stem,
927    }
928}