1use geop_core_editor::{
4 edit_form::{Edit, EditForm, FormState},
5 editable::{Context, OperationGroup, OperationInfo, OperationTier},
6 editing::Tone,
7 part_operation,
8 part_operation::PartOperation,
9};
10use geop_core_program::{
11 element::{ElementReference, HasElements, Kind, whole},
12 part::StepContext,
13 reader::Reader,
14 target::{Target, TargetReference},
15};
16
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult, WithContext},
19 with_context,
20};
21use geop_core_topology::validation::{ValidationParameters, validate_fast};
22use geop_ops_model::parameters::file_choices;
23use geop_ops_model::{BodyNames, ModelOperation, NamedModel, Namer, build_model};
24use std::rc::Rc;
25
26use serde::{Deserialize, Serialize};
27
28use crate::import::{ImportedBody, read_step};
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
51pub struct ImportStep;
52
53#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
54pub struct ImportStepArgs {
55 pub file: String,
57}
58
59pub const STEP_EXTENSIONS: [&str; 2] = ["step", "stp"];
61
62pub fn is_step_file(path: &str) -> bool {
64 let lower = path.to_ascii_lowercase();
65 STEP_EXTENSIONS
66 .iter()
67 .any(|e| lower.ends_with(&format!(".{e}")))
68}
69
70pub fn add_bodies(
73 model: &mut NamedModel,
74 namer: &Namer,
75 bodies: &[ImportedBody],
76) -> GeopResult<()> {
77 let mut alone = NamedModel::new();
81 for (k, body) in bodies.iter().enumerate() {
82 let named = |names: &[Vec<String>]| -> Vec<String> {
83 (names.iter())
84 .map(|n| format!("s{k},{}", n.join(",")))
85 .collect()
86 };
87 let names = BodyNames {
88 vertices: named(&body.vertex_names),
89 edges: named(&body.edge_names),
90 faces: named(&body.face_names),
91 solid: body.spec.solid.then(|| format!("s{k}")),
92 };
93 (alone.build_body(body.spec.clone(), names))
94 .map_err(|e| e.with_context(format!("building the body {:?}", body.label)))?;
95 }
96 if let Err(errors) = validate_fast(&ValidationParameters::default(), alone.topology()) {
97 let shown: Vec<String> = errors.iter().take(5).map(|e| e.to_string()).collect();
98 let shown = shown.join("; ");
99 let mentioned = alone.names_mentioned(&shown);
100 return Err(GeopError::new(format!(
101 "the file's bodies are not valid in geop ({} problem{}): {shown} — where {}",
102 errors.len(),
103 if errors.len() == 1 { "" } else { "s" },
104 mentioned.join(", ")
105 )));
106 }
107 for (k, body) in bodies.iter().enumerate() {
108 let solid = format!("s{k}");
109 let named = |args: &[String]| {
110 let mut all: Vec<&str> = vec![&solid];
111 all.extend(args.iter().map(String::as_str));
112 namer.name(&all)
113 };
114 let names = BodyNames {
115 vertices: body.vertex_names.iter().map(|n| named(n)).collect(),
116 edges: body.edge_names.iter().map(|n| named(n)).collect(),
117 faces: body.face_names.iter().map(|n| named(n)).collect(),
118 solid: body.spec.solid.then(|| namer.name(&[&solid])),
119 };
120 model.build_body(body.spec.clone(), names)?;
121 }
122 Ok(())
123}
124
125#[derive(Clone, Debug, PartialEq)]
129pub struct Imported(pub Rc<Vec<ImportedBody>>);
130
131impl Target for Imported {}
133
134impl Kind for Imported {
136 const NAME: &'static str = "imported";
137}
138
139impl HasElements<Imported> for Imported {
140 type Element<'a> = &'a Imported;
141 const SINGLETON: Option<&'static str> = None;
142
143 fn element(&self, name: &str) -> GeopResult<&Imported> {
144 whole(self, name)
145 }
146}
147
148pub fn imported(step: &str) -> TargetReference<Imported> {
150 TargetReference::new(format!("import:{step}"))
151}
152
153pub fn read_imported<'a>(reader: &Reader<'a>, step: &str) -> GeopResult<&'a Imported> {
155 reader.retrieve_element(&ElementReference::whole(imported(step)))
156}
157
158const STEP_BODIES: &str = "step_bodies";
161
162fn read(reader: &Reader, (step, file): &(String, String)) -> GeopResult<Imported> {
167 let ctx = with_context!("import_step({step}, file={file:?})");
168 let (_, bodies) = (reader.read_file_as(file, STEP_BODIES, read_step)).with_context(ctx)?;
169 if bodies.is_empty() {
170 return Err(GeopError::new(format!("{file} has no solids or sheets"))).with_context(ctx);
171 }
172 Ok(Imported(bodies))
173}
174
175part_operation!(ImportStep(ImportStepArgs));
176
177impl PartOperation for ImportStep {
178 const KIND: &'static str = "import_step";
179 const INFO: OperationInfo = OperationInfo::new(
180 "Import STEP",
181 "Add the solids and sheets of a STEP file next to the program.",
182 OperationGroup::Output,
183 OperationTier::Small,
184 )
185 .reads(&STEP_EXTENSIONS)
186 .icon(crate::icons::IMPORT_STEP);
187 type Args = ImportStepArgs;
188 type Session = ();
189
190 fn new_args(_: &Context) -> ImportStepArgs {
192 ImportStepArgs::default()
193 }
194
195 fn run(ctx: &mut StepContext, args: &ImportStepArgs) -> GeopResult<()> {
197 let step = ctx.step();
198 let context = with_context!("import_step({step}, {args:?})");
199 if args.file.is_empty() {
200 return Err(GeopError::new("choose a STEP file to import")).with_context(context);
201 }
202 let read_from = (step.to_string(), args.file.clone());
203 ctx.produce(&imported(step), &read_from, read)?;
204 build_model::<Self>(ctx, args)
205 }
206
207 fn form<'a>(
212 context: &'a Context<'a>,
213 args: &ImportStepArgs,
214 _: &FormState<()>,
215 ) -> EditForm<'a, ImportStepArgs> {
216 let mut f = EditForm::<ImportStepArgs>::default();
217 let files = file_choices(context.runner, is_step_file);
218 f.file(
219 "file",
220 "file",
221 args.file.clone(),
222 files,
223 &STEP_EXTENSIONS,
224 |Edit { args, .. }, file| args.file = file.to_string(),
225 );
226 let read =
227 (context.built.as_ref()).and_then(|built| read_imported(built, context.step).ok());
228 if let (Some(Imported(bodies)), Ok(namer)) = (read, Namer::new("import", context.step)) {
229 for (k, body) in bodies.iter().enumerate() {
230 if body.healed.is_empty() {
231 continue;
232 }
233 let solid = namer.name(&[&format!("s{k}")]);
234 f.form.dialog.text(
235 &format!("healed_{k}"),
236 format!(
237 "Healed {} ({}): {}.",
238 body.label,
239 solid,
240 body.healed.summary()
241 ),
242 Tone::Hint,
243 );
244 }
245 }
246 f
247 }
248}
249
250impl ModelOperation for ImportStep {
251 fn build(
253 model: &mut NamedModel,
254 reader: &Reader,
255 step: &str,
256 _: &ImportStepArgs,
257 ) -> GeopResult<()> {
258 let namer = Namer::new("import", step)?;
259 let Imported(bodies) = read_imported(reader, step)?;
260 add_bodies(model, &namer, bodies)
261 }
262}