1use 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
48pub const PARAMETERS: &str = "parameters";
50
51pub const PROPERTIES: &str = "properties";
53
54pub const LOOKUP: &str = "lookup_table";
56
57#[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 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(¶meter(&p.name), p, value_of)?;
106 }
107 let names: Vec<String> = args.values.iter().map(|p| p.name.clone()).collect();
108 ctx.produce(¶meter_values(), &names, add_values)
109 }
110
111 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 TableEdit::SelectRow { .. }
179 | TableEdit::Column { .. }
180 | TableEdit::AddColumn
181 | TableEdit::RemoveColumn { .. }
182 | TableEdit::Paste { .. } => {}
183 }
184 });
185 f
186 }
187}
188
189fn 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#[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
211fn 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
233fn 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
252pub struct LookupTable;
253
254#[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 pub selected: String,
263}
264
265impl LookupTableArgs {
266 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 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 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(¶meter(&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 ¶meter(&name),
336 &(args.input.clone(), k, args.rows.clone()),
337 column_of,
338 )?;
339 names.push(name);
340 }
341 ctx.produce(¶meter_values(), &names, add_values)
342 }
343
344 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 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
454fn 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
466fn 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
492fn 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
527pub struct PartProperties;
528
529#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
531pub struct PartPropertiesArgs {
532 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub title: Option<String>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub designation: Option<String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub color: Option<String>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
549 pub material: Option<Material>,
550 #[serde(default, skip_serializing_if = "Vec::is_empty")]
553 pub face_colors: Vec<FaceColor>,
554}
555
556#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
558pub struct FaceColor {
559 pub faces: Vec<ElementReference<NamedModel, Face>>,
560 pub color: String,
562}
563
564impl FaceColor {
565 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 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
619fn face_color_key(i: usize, field: &str) -> String {
621 format!("face_color:{i}:{field}")
622}
623
624const REMOVE: &str = "remove";
626
627const ADD: &str = "add";
629
630const NEW_FACE_COLOR: &str = "#d9483b";
633
634fn 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 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 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
801const NO_MATERIAL: &str = "";
803
804const OTHER_MATERIAL: &str = "other";
806
807const OTHER_DENSITY: f64 = 1000.0;
810
811fn 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#[derive(Clone, Debug, Default, PartialEq)]
830pub struct Properties {
831 pub title: Option<String>,
832 pub designation: Option<String>,
835 pub color: Option<String>,
836 pub material: Option<Material>,
837 pub face_colors: Vec<FaceColor>,
838}
839
840impl Target for Properties {}
842
843impl 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
857pub fn properties() -> TargetReference<Properties> {
859 TargetReference::new(PROPERTIES)
860}
861
862pub 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
873pub 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 pub fn material(&self) -> Option<&Material> {
892 self.material.as_ref()
893 }
894
895 pub fn color(&self) -> Option<&str> {
897 self.color.as_deref()
898 }
899
900 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 pub fn name<'a>(&'a self, stem: &'a str) -> &'a str {
915 self.title.as_deref().unwrap_or(stem)
916 }
917}
918
919pub 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}