geop_ops_drawing/
export.rs1use geop_core_editor::output::{Content, Export, Exporter, ExporterInfo, Output};
6use geop_core_math::geop_error::{GeopError, GeopResult, WithContext};
7use geop_core_program::{
8 element::{ElementReference, HasElements, Kind, whole},
9 reader::Reader,
10 target::{Target, TargetReference},
11};
12use geop_lib_sheet::Format;
13use geop_ops_model::parameters::{read_properties, stem};
14use serde::Deserialize;
15
16use crate::{DrawingArgs, Projection, SheetSize, ViewKind, compose, parts_list};
17
18pub fn drawing(step: &str) -> TargetReference<DrawingArgs> {
21 TargetReference::new(format!("{PREFIX}{step}"))
22}
23
24const PREFIX: &str = "drawing:";
26
27pub(crate) fn described(_: &Reader, args: &DrawingArgs) -> GeopResult<DrawingArgs> {
29 Ok(args.clone())
30}
31
32impl Target for DrawingArgs {}
34
35impl Kind for DrawingArgs {
37 const NAME: &'static str = "drawing";
38}
39
40impl HasElements<DrawingArgs> for DrawingArgs {
41 type Element<'a> = &'a DrawingArgs;
42
43 fn element(&self, name: &str) -> GeopResult<&DrawingArgs> {
44 whole(self, name)
45 }
46
47 fn names_target(name: &str) -> bool {
48 name.starts_with(PREFIX)
49 }
50}
51
52pub fn drawing_steps<'a>(reader: &Reader<'a>) -> GeopResult<Vec<(String, &'a DrawingArgs)>> {
55 let mut out = Vec::new();
56 for target in reader.targets_of::<DrawingArgs>() {
57 let Some(step) = target.name().strip_prefix(PREFIX) else {
58 continue;
59 };
60 let step = step.to_string();
61 out.push((
62 step,
63 reader.retrieve_element(&ElementReference::whole(target))?,
64 ));
65 }
66 Ok(out)
67}
68
69pub struct DrawingExporter;
71
72#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
75#[serde(default)]
76pub struct DrawingOptions {
77 pub step: Option<String>,
81 pub views: Vec<String>,
84 pub first_angle: bool,
86 pub scale: Option<String>,
88 pub sheet: Option<String>,
90 pub name: Option<String>,
92 pub material: Option<String>,
94 pub bom: bool,
96}
97
98fn parse_scale(text: &str) -> GeopResult<f64> {
100 let bad = || GeopError::new(format!("the scale {text:?} is not like 1:2 or 5:1"));
101 let (a, b) = text.split_once(':').ok_or_else(bad)?;
102 let (a, b): (f64, f64) = (
103 a.trim().parse().map_err(|_| bad())?,
104 b.trim().parse().map_err(|_| bad())?,
105 );
106 match a > 0.0 && b > 0.0 {
107 true => Ok(a / b),
108 false => Err(bad()),
109 }
110}
111
112impl DrawingOptions {
113 fn apply(&self, mut args: DrawingArgs) -> GeopResult<DrawingArgs> {
115 if !self.views.is_empty() {
116 args.views = (self.views.iter())
117 .map(|v| {
118 ViewKind::from_name(v.trim()).ok_or_else(|| {
119 let known: Vec<&str> = ViewKind::ALL.iter().map(|k| k.name()).collect();
120 GeopError::new(format!(
121 "no view is named {v:?}; the views are {}",
122 known.join(", ")
123 ))
124 })
125 })
126 .collect::<GeopResult<_>>()?;
127 }
128 if self.first_angle {
129 args.projection = Projection::FirstAngle;
130 }
131 if let Some(scale) = &self.scale {
132 args.scale = Some(parse_scale(scale)?);
133 }
134 if let Some(sheet) = &self.sheet {
135 args.sheet = (SheetSize::ALL.into_iter())
136 .find(|s| s.name() == sheet.to_ascii_lowercase())
137 .ok_or_else(|| GeopError::new(format!("no paper is named {sheet:?}: a4 to a0")))?;
138 }
139 if let Some(name) = &self.name {
140 args.name = name.clone();
141 }
142 if let Some(material) = &self.material {
143 args.material = material.clone();
144 }
145 args.bom |= self.bom;
146 Ok(args)
147 }
148}
149
150impl Exporter for DrawingExporter {
151 const INFO: ExporterInfo = ExporterInfo {
152 kind: "drawing",
153 label: "Drawing",
154 doc: "2-D drawing: views, dimensions, title block",
155 formats: &["svg", "dxf"],
156 };
157 type Options = DrawingOptions;
158
159 fn export(output: &mut Output, format: &str, options: DrawingOptions) -> GeopResult<Export> {
163 let format = Format::of_extension(format)
164 .ok_or_else(|| GeopError::new(format!("a drawing is no .{format} file")))?;
165 let current = output.part();
166 let steps = drawing_steps(¤t)?;
167 let found = match &options.step {
168 Some(step) => Some(
169 (steps.iter().find(|(s, _)| s == step))
170 .ok_or_else(|| GeopError::new(format!("the step {step:?} is no drawing")))?,
171 ),
172 None => (output.editing)
173 .and_then(|editing| steps.iter().find(|(s, _)| s == editing))
174 .or(steps.last()),
175 };
176 let (step, args) = match found {
177 Some((step, args)) => (Some(step.to_string()), (*args).clone()),
178 None => (None, DrawingArgs::default()),
179 };
180 let mut args = options.apply(args)?;
181 let properties = read_properties(&output.part())?.into_owned();
182 if args.name.is_empty() {
183 args.name = properties.name(stem(output.file_name())).to_string();
184 }
185 if let Some(material) = properties.material().filter(|_| args.material.is_empty()) {
186 args.material = material.name.clone();
187 }
188 let reader = match &step {
189 Some(step) => output.part_before(step)?,
190 None => output.part(),
191 };
192 let ctx = |e: GeopError| match &step {
193 Some(step) => e.with_context(format!("the drawing {step:?}")),
194 None => e.with_context("the drawing of the whole part"),
195 };
196 let parts = parts_list(&reader, output.file_name(), &args).with_context(&ctx)?;
197 let sheet = compose(&reader, &args, output.date, &parts).with_context(&ctx)?;
198 Ok(Export {
199 name: format!("{}.{}", stem(output.file_name()), format.extension()),
200 media_type: format.media_type().into(),
201 content: Content::Text(sheet.write(format)),
202 })
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn scales_are_read_as_written() {
212 assert_eq!(parse_scale("1:5").unwrap(), 0.2);
213 assert_eq!(parse_scale("2:1").unwrap(), 2.0);
214 assert!(parse_scale("0:1").is_err());
215 assert!(parse_scale("half").is_err());
216 }
217}