1pub mod dimension;
36pub mod frame;
37pub mod hlr;
38pub mod ordinate;
39pub mod pdf;
40pub mod pdf3d;
41pub mod print;
42pub mod project;
43pub mod svg;
44pub mod u3d;
45
46use serde::{Deserialize, Serialize};
47use serde_json::{json, Value};
48
49pub const SHEETS: &str = "sheets";
51
52pub const ORTHOGRAPHIC: &str = "orthographic";
54
55pub const PAPER_SIZES: &[(&str, f64, f64)] = &[
59 ("A4", 297.0, 210.0),
60 ("A3", 420.0, 297.0),
61 ("A2", 594.0, 420.0),
62 ("A1", 841.0, 594.0),
63 ("A0", 1189.0, 841.0),
64 ("Letter", 279.4, 215.9),
65 ("Tabloid", 431.8, 279.4),
66];
67
68pub const CUSTOM: &str = "Custom";
70
71pub const DEFAULT_SIZE: &str = "A3";
73
74pub const DEFAULT_SCALE: f64 = 1.0;
76
77pub const DEFAULT_BORDER_INSET_MM: f64 = 10.0;
79
80pub fn paper_size(key: &str) -> Option<(f64, f64)> {
82 PAPER_SIZES
83 .iter()
84 .find(|(name, _, _)| name.eq_ignore_ascii_case(key))
85 .map(|(_, w, h)| (*w, *h))
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
90pub struct SheetState {
91 #[serde(default)]
92 pub sheets: Vec<Sheet>,
93 #[serde(default, rename = "idCounter")]
98 pub id_counter: u64,
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct Sheet {
104 pub id: String,
105 #[serde(default)]
106 pub name: String,
107 #[serde(default = "default_size")]
109 pub size: String,
110 #[serde(default, rename = "widthMm")]
111 pub width_mm: f64,
112 #[serde(default, rename = "heightMm")]
113 pub height_mm: f64,
114 #[serde(default = "default_true")]
117 pub border: bool,
118 #[serde(default = "default_border_inset", rename = "borderInsetMm")]
122 pub border_inset_mm: f64,
123 #[serde(default = "default_true", rename = "titleBlock")]
126 pub title_block: bool,
127 #[serde(default)]
129 pub notes: String,
130 #[serde(default)]
131 pub views: Vec<PlacedView>,
132 #[serde(default)]
136 pub dimensions: Vec<dimension::SheetDimension>,
137 #[serde(default)]
143 pub ordinates: Vec<ordinate::SheetOrdinate>,
144 #[serde(default, skip_serializing_if = "Vec::is_empty")]
148 pub revisions: Vec<Revision>,
149 #[serde(default, rename = "bomTable", skip_serializing_if = "Option::is_none")]
150 pub bom_table: Option<BomTable>,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct BomTable {
157 pub position: [f64; 2],
158 pub column_width_mm: f64,
159 pub columns: Vec<String>,
160}
161impl Default for BomTable {
162 fn default() -> Self {
163 Self {
164 position: [20., 20.],
165 column_width_mm: 32.,
166 columns: vec![
167 "occurrence.Find_Number".into(),
168 "partName".into(),
169 "occurrence.Quantity".into(),
170 ],
171 }
172 }
173}
174
175#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
180pub struct Revision {
181 #[serde(default)]
182 pub rev: String,
183 #[serde(default)]
184 pub date: String,
185 #[serde(default)]
186 pub description: String,
187}
188
189impl Revision {
190 pub const FIELDS: &'static [&'static str] = &["rev", "date", "description"];
192
193 pub fn apply(&mut self, params: &Value) -> Result<(), String> {
196 let Some(object) = params.as_object() else {
197 return Err("a revision is an object of rev, date and description".into());
198 };
199 for (key, value) in object {
200 let slot = match key.as_str() {
201 "rev" => &mut self.rev,
202 "date" => &mut self.date,
203 "description" => &mut self.description,
204 other => return Err(format!("a revision has no field '{other}'")),
205 };
206 *slot = value
207 .as_str()
208 .ok_or_else(|| format!("revision {key}: {value} is not text"))?
209 .to_string();
210 }
211 Ok(())
212 }
213}
214
215fn default_size() -> String {
216 DEFAULT_SIZE.to_string()
217}
218
219fn default_true() -> bool {
220 true
221}
222
223fn is_false(value: &bool) -> bool {
224 !*value
225}
226
227fn default_border_inset() -> f64 {
228 DEFAULT_BORDER_INSET_MM
229}
230
231#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct PlacedView {
234 pub id: String,
235 #[serde(default)]
237 pub view: String,
238 #[serde(default)]
242 pub position: [f64; 2],
243 #[serde(default = "default_scale")]
245 pub scale: f64,
246 #[serde(default = "default_projection")]
248 pub projection: String,
249 #[serde(default = "default_true", rename = "flattenText")]
256 pub flatten_text: bool,
257 #[serde(default, rename = "threeD", skip_serializing_if = "is_false")]
264 pub three_d: bool,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
270 pub section: Option<SectionCut>,
271 #[serde(default, skip_serializing_if = "Option::is_none")]
276 pub detail: Option<DetailCircle>,
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct SectionCut {
296 #[serde(default)]
300 pub source: String,
301 #[serde(default)]
303 pub from: String,
304 #[serde(default)]
305 pub to: String,
306 #[serde(default = "default_label")]
309 pub label: String,
310 #[serde(default)]
315 pub flip: bool,
316}
317
318fn default_label() -> String {
319 "A".to_string()
320}
321
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
332pub struct DetailCircle {
333 #[serde(default)]
336 pub source: String,
337 #[serde(default)]
339 pub centre: String,
340 #[serde(default)]
344 pub rim: String,
345 #[serde(default = "default_label")]
349 pub label: String,
350}
351
352pub const DETAIL_SCALE_FACTOR: f64 = 2.0;
355
356impl SectionCut {
357 pub const LETTERS: &'static [char] = &[
360 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'U',
361 'V', 'W', 'X', 'Y', 'Z',
362 ];
363}
364
365fn first_free_letter(taken: &[String]) -> String {
367 SectionCut::LETTERS
368 .iter()
369 .map(|letter| letter.to_string())
370 .find(|letter| !taken.iter().any(|used| used == letter))
371 .unwrap_or_else(|| SectionCut::LETTERS[SectionCut::LETTERS.len() - 1].to_string())
372}
373
374fn default_scale() -> f64 {
375 DEFAULT_SCALE
376}
377
378fn default_projection() -> String {
379 ORTHOGRAPHIC.to_string()
380}
381
382impl Sheet {
383 pub fn millimetres(&self) -> (f64, f64) {
386 match paper_size(&self.size) {
387 Some((w, h)) => (w, h),
388 None => (self.width_mm.max(1.0), self.height_mm.max(1.0)),
389 }
390 }
391
392 pub fn border_inset(&self) -> f64 {
395 let (w, h) = self.millimetres();
396 self.border_inset_mm.max(0.0).min(w.min(h) * 0.25)
397 }
398
399 pub fn find_view(&self, id: &str) -> Option<&PlacedView> {
400 self.views.iter().find(|view| view.id == id)
401 }
402
403 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PlacedView> {
404 self.views.iter_mut().find(|view| view.id == id)
405 }
406
407 pub fn find_dimension(&self, id: &str) -> Option<&dimension::SheetDimension> {
408 self.dimensions.iter().find(|d| d.id == id)
409 }
410
411 pub fn find_ordinate(&self, id: &str) -> Option<&ordinate::SheetOrdinate> {
412 self.ordinates.iter().find(|o| o.id == id)
413 }
414
415 pub fn next_view_letter(&self, views: &[brep_kernel::PmiView]) -> String {
424 let taken: Vec<String> = self.view_letters(views).into_iter().flatten().collect();
425 first_free_letter(&taken)
426 }
427
428 pub fn view_letters(&self, views: &[brep_kernel::PmiView]) -> Vec<Option<String>> {
435 let stored = |placed: &PlacedView| {
436 placed
437 .section
438 .as_ref()
439 .map(|cut| cut.label.clone())
440 .or_else(|| placed.detail.as_ref().map(|circle| circle.label.clone()))
441 };
442 let mut taken: Vec<String> = self.views.iter().filter_map(stored).collect();
443 self.views
444 .iter()
445 .map(|placed| {
446 if let Some(letter) = stored(placed) {
447 return Some(letter);
448 }
449 let sectioned = views
450 .iter()
451 .find(|view| view.id == placed.view)
452 .is_some_and(|view| view.display.section.is_some());
453 sectioned.then(|| {
454 let letter = first_free_letter(&taken);
455 taken.push(letter.clone());
456 letter
457 })
458 })
459 .collect()
460 }
461}
462
463impl SheetState {
464 pub fn next_id(&mut self, prefix: &str) -> String {
467 let seen = self.max_numeric_suffix();
468 if seen > self.id_counter {
469 self.id_counter = seen;
470 }
471 loop {
472 self.id_counter += 1;
473 let candidate = format!("{prefix}{}", self.id_counter);
474 if self.find_sheet(&candidate).is_none()
475 && self.locate_view(&candidate).is_none()
476 && self.locate_dimension(&candidate).is_none()
477 && self.locate_ordinate(&candidate).is_none()
478 {
479 return candidate;
480 }
481 }
482 }
483
484 fn max_numeric_suffix(&self) -> u64 {
485 let mut best = 0u64;
486 let mut consider = |id: &str| {
487 let digits = id.bytes().rev().take_while(u8::is_ascii_digit).count();
488 if digits > 0 {
489 if let Ok(value) = id[id.len() - digits..].parse::<u64>() {
490 best = best.max(value);
491 }
492 }
493 };
494 for sheet in &self.sheets {
495 consider(&sheet.id);
496 for view in &sheet.views {
497 consider(&view.id);
498 }
499 for dimension in &sheet.dimensions {
500 consider(&dimension.id);
501 }
502 for set in &sheet.ordinates {
503 consider(&set.id);
504 }
505 }
506 best
507 }
508
509 pub fn find_sheet(&self, id: &str) -> Option<&Sheet> {
510 self.sheets.iter().find(|sheet| sheet.id == id)
511 }
512
513 pub fn find_sheet_mut(&mut self, id: &str) -> Option<&mut Sheet> {
514 self.sheets.iter_mut().find(|sheet| sheet.id == id)
515 }
516
517 pub fn locate_view(&self, id: &str) -> Option<(String, usize)> {
519 self.sheets.iter().find_map(|sheet| {
520 sheet
521 .views
522 .iter()
523 .position(|view| view.id == id)
524 .map(|index| (sheet.id.clone(), index))
525 })
526 }
527
528 pub fn find_view_mut(&mut self, id: &str) -> Option<&mut PlacedView> {
529 self.sheets
530 .iter_mut()
531 .find_map(|sheet| sheet.views.iter_mut().find(|view| view.id == id))
532 }
533
534 pub fn locate_dimension(&self, id: &str) -> Option<(String, usize)> {
536 self.sheets.iter().find_map(|sheet| {
537 sheet
538 .dimensions
539 .iter()
540 .position(|dimension| dimension.id == id)
541 .map(|index| (sheet.id.clone(), index))
542 })
543 }
544
545 pub fn find_dimension(&self, id: &str) -> Option<&dimension::SheetDimension> {
546 self.sheets.iter().find_map(|sheet| sheet.find_dimension(id))
547 }
548
549 pub fn find_dimension_mut(&mut self, id: &str) -> Option<&mut dimension::SheetDimension> {
550 self.sheets
551 .iter_mut()
552 .find_map(|sheet| sheet.dimensions.iter_mut().find(|d| d.id == id))
553 }
554
555 pub fn locate_ordinate(&self, id: &str) -> Option<(String, usize)> {
557 self.sheets.iter().find_map(|sheet| {
558 sheet
559 .ordinates
560 .iter()
561 .position(|set| set.id == id)
562 .map(|index| (sheet.id.clone(), index))
563 })
564 }
565
566 pub fn find_ordinate(&self, id: &str) -> Option<&ordinate::SheetOrdinate> {
567 self.sheets.iter().find_map(|sheet| sheet.find_ordinate(id))
568 }
569
570 pub fn find_ordinate_mut(&mut self, id: &str) -> Option<&mut ordinate::SheetOrdinate> {
571 self.sheets
572 .iter_mut()
573 .find_map(|sheet| sheet.ordinates.iter_mut().find(|o| o.id == id))
574 }
575
576 pub fn is_empty(&self) -> bool {
578 self.sheets.is_empty() && self.id_counter == 0
579 }
580}
581
582pub fn schema_catalogue(view_ids: &[String]) -> Value {
592 json!({
593 "objects": [
594 sheet_schema(),
595 sheet_view_schema(view_ids),
596 dimension::sheet_dimension_schema(),
597 ordinate::sheet_ordinate_schema(),
598 section_view_schema(),
599 detail_view_schema(),
600 ]
601 })
602}
603
604pub fn section_view_schema() -> Value {
609 json!({
610 "type": "sectionView",
611 "shortName": "SV",
612 "longName": "Section view",
613 "inputParamsSchema": {
614 "id": { "type": "string", "default_value": null },
615 "cut": { "type": "reference_selection", "label": "Cutting line", "selectionFilter": ["sheetAnchor"], "multiple": true, "hint": "Two anchors on one placed view" },
616 "sectionLabel": { "type": "string", "label": "Section letter", "default_value": "A" },
617 "sectionFlip": { "type": "boolean", "label": "Section looks the other way", "default_value": false },
618 "positionXMm": { "type": "number", "label": "X (mm)", "default_value": 0.0 },
619 "positionYMm": { "type": "number", "label": "Y (mm)", "default_value": 0.0 },
620 "scale": { "type": "number", "label": "Scale (mm per model unit)", "default_value": DEFAULT_SCALE },
621 "flattenText": { "type": "boolean", "label": "Flatten text to sheet", "default_value": true },
622 }
623 })
624}
625
626pub fn detail_view_schema() -> Value {
630 json!({
631 "type": "detailView",
632 "shortName": "SV",
633 "longName": "Detail view",
634 "inputParamsSchema": {
635 "id": { "type": "string", "default_value": null },
636 "centre": { "type": "reference_selection", "label": "Centre", "selectionFilter": ["sheetAnchor"], "multiple": false, "hint": "The circle's centre, an anchor on a placed view" },
637 "rim": { "type": "reference_selection", "label": "Point on the rim", "selectionFilter": ["sheetAnchor"], "multiple": false, "hint": "An anchor on the same view, on the circle" },
638 "detailLabel": { "type": "string", "label": "Detail letter", "default_value": "A" },
639 "positionXMm": { "type": "number", "label": "X (mm)", "default_value": 0.0 },
640 "positionYMm": { "type": "number", "label": "Y (mm)", "default_value": 0.0 },
641 "scale": { "type": "number", "label": "Scale (mm per model unit)", "default_value": DEFAULT_SCALE },
642 "flattenText": { "type": "boolean", "label": "Flatten text to sheet", "default_value": true },
643 }
644 })
645}
646
647pub fn sheet_schema() -> Value {
649 let sizes: Vec<&str> = PAPER_SIZES
650 .iter()
651 .map(|(key, _, _)| *key)
652 .chain(std::iter::once(CUSTOM))
653 .collect();
654 json!({
655 "type": "sheet",
656 "shortName": "SHT",
657 "longName": "Sheet",
658 "inputParamsSchema": {
659 "id": { "type": "string", "default_value": null },
660 "name": { "type": "string", "default_value": "Sheet 1" },
661 "size": { "type": "options", "label": "Paper size", "options": sizes, "default_value": DEFAULT_SIZE },
662 "widthMm": { "type": "number", "label": "Width (mm)", "default_value": 420.0 },
663 "heightMm": { "type": "number", "label": "Height (mm)", "default_value": 297.0 },
664 "border": { "type": "boolean", "label": "Border", "default_value": true },
665 "borderInsetMm": { "type": "number", "label": "Border inset (mm)", "default_value": DEFAULT_BORDER_INSET_MM },
666 "titleBlock": { "type": "boolean", "label": "Title block", "default_value": true },
667 "notes": { "type": "string", "label": "Notes", "default_value": "" },
668 "revisions": { "type": "revision_table", "label": "Revisions", "columns": Revision::FIELDS, "default_value": [] },
672 }
673 })
674}
675
676pub fn sheet_view_schema(view_ids: &[String]) -> Value {
679 json!({
680 "type": "sheetView",
681 "shortName": "SV",
682 "longName": "Placed view",
683 "inputParamsSchema": {
684 "id": { "type": "string", "default_value": null },
685 "view": { "type": "options", "label": "PMI view", "options": view_ids, "default_value": view_ids.first().cloned().unwrap_or_default() },
686 "positionXMm": { "type": "number", "label": "X (mm)", "default_value": 0.0 },
687 "positionYMm": { "type": "number", "label": "Y (mm)", "default_value": 0.0 },
688 "scale": { "type": "number", "label": "Scale (mm per model unit)", "default_value": DEFAULT_SCALE },
689 "projection": { "type": "options", "label": "Projection", "options": [ORTHOGRAPHIC], "default_value": ORTHOGRAPHIC },
690 "flattenText": { "type": "boolean", "label": "Flatten text to sheet", "default_value": true },
691 "threeD": { "type": "boolean", "label": "Live 3D in PDF", "default_value": false, "hint": "In the Sheets + 3D (PDF) export, Acrobat and Foxit turn and zoom this view; other readers show the drawing" },
692 }
693 })
694}
695
696impl Sheet {
697 pub fn params(&self) -> Value {
699 let (w, h) = self.millimetres();
700 json!({
701 "id": self.id,
702 "name": self.name,
703 "size": self.size,
704 "widthMm": w,
705 "heightMm": h,
706 "border": self.border,
707 "borderInsetMm": self.border_inset_mm,
708 "titleBlock": self.title_block,
709 "notes": self.notes,
710 "revisions": self.revisions,
711 "bomTable": self.bom_table,
712 })
713 }
714
715 pub fn apply(&mut self, params: &Value) -> Result<(), String> {
718 if let Some(name) = params.get("name").and_then(Value::as_str) {
719 let name = name.trim();
720 if name.is_empty() {
721 return Err("a sheet needs a name".into());
722 }
723 self.name = name.to_string();
724 }
725 if let Some(size) = params.get("size").and_then(Value::as_str) {
726 let size = size.trim();
727 if !size.eq_ignore_ascii_case(CUSTOM) && paper_size(size).is_none() {
728 return Err(format!("no paper size '{size}'"));
729 }
730 self.size = if size.eq_ignore_ascii_case(CUSTOM) {
731 CUSTOM.to_string()
732 } else {
733 size.to_string()
734 };
735 }
736 let dimension = |key: &str, current: f64| -> Result<f64, String> {
737 match params.get(key).and_then(Value::as_f64) {
738 None => Ok(current),
739 Some(value) if value.is_finite() && value > 0.0 => Ok(value),
740 Some(value) => Err(format!("{key}: {value} is not a positive length")),
741 }
742 };
743 self.width_mm = dimension("widthMm", self.width_mm)?;
744 self.height_mm = dimension("heightMm", self.height_mm)?;
745 if let Some((w, h)) = paper_size(&self.size) {
748 self.width_mm = w;
749 self.height_mm = h;
750 }
751 if let Some(border) = params.get("border").and_then(Value::as_bool) {
755 self.border = border;
756 }
757 if let Some(inset) = params.get("borderInsetMm").and_then(Value::as_f64) {
758 let (w, h) = self.millimetres();
759 if !inset.is_finite() || inset < 0.0 {
760 return Err(format!("borderInsetMm: {inset} is not a length"));
761 }
762 if inset > w.min(h) * 0.25 {
763 return Err(format!(
764 "borderInsetMm: {inset} leaves no drawing area on a {w:.0} × {h:.0} mm sheet"
765 ));
766 }
767 self.border_inset_mm = inset;
768 }
769 if let Some(title_block) = params.get("titleBlock").and_then(Value::as_bool) {
770 self.title_block = title_block;
771 }
772 if let Some(notes) = params.get("notes").and_then(Value::as_str) {
775 self.notes = notes.to_string();
776 }
777 if let Some(value) = params.get("bomTable") {
778 self.bom_table = if value.is_null() { None } else {
779 let table: BomTable = serde_json::from_value(value.clone()).map_err(|e| format!("BOM table: {e}"))?;
780 if table.columns.is_empty() || table.columns.len() > 32 { return Err("choose between 1 and 32 BOM columns".into()); }
781 if !table.position.iter().all(|v| v.is_finite()) || !table.column_width_mm.is_finite() || table.column_width_mm < 8. { return Err("BOM position must be finite and column width at least 8 mm".into()); }
782 Some(table)
783 };
784 }
785 if let Some(rows) = params.get("revisions") {
789 let rows = rows.as_array().ok_or("revisions: not a list of rows")?;
790 let mut revisions = Vec::with_capacity(rows.len());
791 for (index, row) in rows.iter().enumerate() {
792 let mut revision = Revision::default();
793 revision.apply(row).map_err(|error| format!("revisions[{index}]: {error}"))?;
794 revisions.push(revision);
795 }
796 self.revisions = revisions;
797 }
798 Ok(())
799 }
800}
801
802impl PlacedView {
803 pub fn params(&self) -> Value {
808 let mut params = json!({
809 "id": self.id,
810 "view": self.view,
811 "positionXMm": self.position[0],
812 "positionYMm": self.position[1],
813 "scale": self.scale,
814 "projection": self.projection,
815 "flattenText": self.flatten_text,
816 });
817 if self.schema_type() == "sheetView" {
818 params.as_object_mut().expect("an object").insert("threeD".into(), json!(self.three_d));
819 }
820 let object = params.as_object_mut().expect("an object");
821 if let Some(cut) = &self.section {
822 object.insert("cut".into(), json!(self.picked("cut")));
823 object.insert("sectionLabel".into(), json!(cut.label));
824 object.insert("sectionFlip".into(), json!(cut.flip));
825 }
826 if let Some(circle) = &self.detail {
827 object.insert("centre".into(), json!(circle.centre));
828 object.insert("rim".into(), json!(circle.rim));
829 object.insert("detailLabel".into(), json!(circle.label));
830 }
831 params
832 }
833
834 pub fn picked(&self, field: &str) -> Vec<String> {
838 let list: Vec<&String> = match (field, &self.section, &self.detail) {
839 ("cut", Some(cut), _) => vec![&cut.from, &cut.to],
840 ("centre", _, Some(circle)) => vec![&circle.centre],
841 ("rim", _, Some(circle)) => vec![&circle.rim],
842 _ => Vec::new(),
843 };
844 list.into_iter().filter(|anchor| !anchor.is_empty()).cloned().collect()
845 }
846
847 pub fn schema_type(&self) -> &'static str {
850 match (&self.section, &self.detail) {
851 (Some(_), _) => "sectionView",
852 (_, Some(_)) => "detailView",
853 _ => "sheetView",
854 }
855 }
856
857 pub fn apply(&mut self, params: &Value, view_ids: &[String]) -> Result<(), String> {
859 if let Some(view) = params.get("view").and_then(Value::as_str) {
860 let view = view.trim();
861 if !view.is_empty() && !view_ids.iter().any(|id| id == view) {
862 return Err(format!("no PMI view '{view}'"));
863 }
864 self.view = view.to_string();
865 }
866 for (key, slot) in [("positionXMm", 0usize), ("positionYMm", 1usize)] {
867 if let Some(value) = params.get(key).and_then(Value::as_f64) {
868 if !value.is_finite() {
869 return Err(format!("{key}: not a finite length"));
870 }
871 self.position[slot] = value;
872 }
873 }
874 if let Some(scale) = params.get("scale").and_then(Value::as_f64) {
875 if !scale.is_finite() || scale <= 0.0 {
876 return Err(format!("scale: {scale} is not a positive ratio"));
877 }
878 self.scale = scale;
879 }
880 if let Some(projection) = params.get("projection").and_then(Value::as_str) {
881 if !projection.eq_ignore_ascii_case(ORTHOGRAPHIC) {
882 return Err(format!(
883 "projection '{projection}' is not drawn on a sheet yet — this slice is orthographic only"
884 ));
885 }
886 self.projection = ORTHOGRAPHIC.to_string();
887 }
888 if let Some(flatten) = params.get("flattenText").and_then(Value::as_bool) {
889 self.flatten_text = flatten;
890 }
891 if let Some(three_d) = params.get("threeD").and_then(Value::as_bool) {
895 if three_d && self.schema_type() != "sheetView" {
896 return Err(format!("'{}' is a {}; live 3D is for a plain placed view", self.id, self.schema_type()));
897 }
898 self.three_d = three_d;
899 }
900 let label = params.get("sectionLabel").and_then(Value::as_str).map(str::trim);
906 let flip = params.get("sectionFlip").and_then(Value::as_bool);
907 match self.section.as_mut() {
908 Some(cut) => {
909 if let Some(label) = label.filter(|l| !l.is_empty()) {
910 cut.label = label.to_string();
911 }
912 if let Some(flip) = flip {
913 cut.flip = flip;
914 }
915 }
916 None => {
917 if label.is_some_and(|l| !l.is_empty()) || flip == Some(true) {
918 return Err(format!(
919 "'{}' is not a section view; it has no cutting plane to letter or turn round",
920 self.id
921 ));
922 }
923 }
924 }
925 let detail_label = params.get("detailLabel").and_then(Value::as_str).map(str::trim);
928 match self.detail.as_mut() {
929 Some(circle) => {
930 if let Some(label) = detail_label.filter(|l| !l.is_empty()) {
931 circle.label = label.to_string();
932 }
933 }
934 None => {
935 if detail_label.is_some_and(|l| !l.is_empty()) {
936 return Err(format!(
937 "'{}' is not a detail view; it has no circle to letter",
938 self.id
939 ));
940 }
941 }
942 }
943 Ok(())
944 }
945}
946