Skip to main content

geop_ops_step/
operation.rs

1//! [`ImportStep`]: add the bodies of a STEP file to the part.
2
3use 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/// Adds every body of the STEP file `file` — its solids, and its sheets of
31/// faces — to the part, where the file has them: the parts of an assembly
32/// each where it is placed, in millimetres.
33///
34/// Named `import(I,sK)` for the `K`-th body of the file and the operation
35/// `I`, what it is made of `import(I,sK,X)` after its entity `X` in the
36/// file: `vN`, `eN`, `fN` for the file's `N`-th vertex, edge and face,
37/// counted as the body has them. A face going all the way round an axis
38/// is cut into sectors `fN,qM`, along meridians `fN,mM` — one going round
39/// a torus' tube likewise into pieces `fN,qM` along parallels `fN,mM`, and a
40/// whole torus first into bands `fN,bK` along parallels `fN,mK` starting at
41/// `fN,mK,v`, and a strip turning more than once into pieces `fN,qM` along
42/// meridians `fN,mM`; an edge split where a cut crosses it, or because it
43/// closes on itself, into pieces `eN,pM` at the vertices `eN,cM`.
44///
45/// The file's bodies, as read, are the step's target [`imported`]: read
46/// once, and again only when the file changes. Where the file disagrees
47/// with itself further than the kernel can carry as one point or curve,
48/// what is rebuilt from its surfaces — and how far it moved — is kept with
49/// its body there ([`ImportedBody::healed`]) and shown in the dialog.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
51pub struct ImportStep;
52
53#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
54pub struct ImportStepArgs {
55    /// The STEP file, relative to the program.
56    pub file: String,
57}
58
59/// The extensions of a STEP file.
60pub const STEP_EXTENSIONS: [&str; 2] = ["step", "stp"];
61
62/// Whether `path` names a STEP file.
63pub 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
70/// Adds the bodies `bodies` to `model`, named by `namer` as [`ImportStep`]
71/// names them, after checking they are well formed on their own.
72pub fn add_bodies(
73    model: &mut NamedModel,
74    namer: &Namer,
75    bodies: &[ImportedBody],
76) -> GeopResult<()> {
77    // Checked on their own first, named as the file names them: a body the
78    // file describes badly is refused by name, before it is mixed into the
79    // part.
80    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/// The bodies of a STEP file, as read: the value of an import's target
126/// [`imported`], shared with every other build reading the same file (see
127/// [`read`]).
128#[derive(Clone, Debug, PartialEq)]
129pub struct Imported(pub Rc<Vec<ImportedBody>>);
130
131/// Each import step's own.
132impl Target for Imported {}
133
134/// Read whole.
135impl 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
148/// The bodies the import step `step` read.
149pub fn imported(step: &str) -> TargetReference<Imported> {
150    TargetReference::new(format!("import:{step}"))
151}
152
153/// The bodies the import step `step` of the part `reader` reads read.
154pub fn read_imported<'a>(reader: &Reader<'a>, step: &str) -> GeopResult<&'a Imported> {
155    reader.retrieve_element(&ElementReference::whole(imported(step)))
156}
157
158/// What a STEP file reads as, as its workspace keeps it (see
159/// [`geop_core_program::cache`]).
160const STEP_BODIES: &str = "step_bodies";
161
162/// The bodies of the file `file` the step `step` reads, the read recorded:
163/// read again once the file changes. A text is read once for every build
164/// of the workspace — a step's preview, the program it is committed to, a
165/// part placed from it — and, kept on disk, for every run of the kernel.
166fn 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    /// No file yet.
191    fn new_args(_: &Context) -> ImportStepArgs {
192        ImportStepArgs::default()
193    }
194
195    /// Reads the file, then adds its bodies.
196    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    /// The file: a STEP file of the workspace, chosen in its explorer, or
208    /// one the user adds from elsewhere, which the front end stores next to
209    /// the program — and
210    /// what the import rebuilt, body by body, as the step last read them.
211    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    /// Adds the bodies the step read (see [`imported`]).
252    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}