kittycad_execution_plan/
import_files.rs

1//! Instruction for importing a file.
2//! This will do all the file related operations, and return a
3//! kittycad_modeling_cmds::ImportFiles to be passed to Endpoint::ImportFiles.
4
5use crate::Result;
6use crate::{memory::Memory, Destination, ExecutionError};
7use kittycad_execution_plan_traits::{InMemory, Primitive, ReadMemory, Value};
8use kittycad_modeling_cmds::{coord, format, shared, units};
9use serde::{Deserialize, Serialize};
10use std::fs;
11use std::path::Path;
12use std::str::FromStr;
13
14/// Data required to import a file
15#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
16pub struct ImportFiles {
17    /// Which address should the imported files names be stored in, if any?
18    /// Written after `format_destination`.
19    pub files_destination: Option<Destination>,
20    /// Which address should the imported files format be stored in, if any?
21    /// Written before `files_destination`.
22    pub format_destination: Option<Destination>,
23    /// Look up each parameter at this address.
24    /// 1: file path
25    /// 2: options (file format)
26    pub arguments: Vec<InMemory>,
27}
28
29impl ImportFiles {
30    /// Import a file!
31    pub async fn execute(self, mem: &mut Memory) -> Result<()> {
32        let Self {
33            files_destination,
34            format_destination,
35            arguments,
36        } = self;
37
38        let file_path_prim = match arguments[0] {
39            InMemory::Address(addr) => mem.get_ok(&addr),
40            InMemory::StackPop => mem.stack_pop(),
41            InMemory::StackPeek => mem.stack_peek(),
42        };
43
44        let Ok([Primitive::String(file_path_str)]) = file_path_prim.as_deref() else {
45            return Err(ExecutionError::BadArg {
46                reason: "first arg must be a string".to_string(),
47            });
48        };
49
50        let file_path = Path::new(&file_path_str);
51        let Ok(file_contents) = fs::read(file_path) else {
52            return Err(ExecutionError::General {
53                reason: "Can't read file".to_string(),
54            });
55        };
56
57        let ext_format = get_import_format_from_extension(file_path_str.split('.').last().ok_or_else(|| {
58            ExecutionError::General {
59                reason: format!("No file extension found for `{}`", file_path_str),
60            }
61        })?)
62        .map_err(|e| ExecutionError::General { reason: e.to_string() })?;
63
64        let options_prim = match arguments.get(1) {
65            Some(InMemory::Address(addr)) => mem.get_ok(addr),
66            Some(InMemory::StackPop) => mem.stack_pop(),
67            Some(InMemory::StackPeek) => mem.stack_peek(),
68            None => Ok(vec![]),
69        };
70
71        let maybe_opt_format = match options_prim.as_deref() {
72            Ok(format_values) => from_vec_prim_to_res_opt_input_format(format_values.to_vec()),
73            _ => {
74                return Err(ExecutionError::General {
75                    reason: "invalid format option passed".to_string(),
76                });
77            }
78        };
79
80        // If the "format option" was passed, check if it matches that of the file.
81        let format = if let Ok(Some(opt_format)) = maybe_opt_format {
82            // Validate the given format with the extension format.
83            validate_extension_format(ext_format, opt_format.clone())?;
84            opt_format
85        } else {
86            ext_format
87        };
88
89        // Get the base name (no path)
90        let file_name = file_path
91            .file_name()
92            .map(|p| p.to_string_lossy().to_string())
93            .ok_or_else(|| ExecutionError::General {
94                reason: "couldn't extract file name from file path".to_string(),
95            })?;
96
97        // We're going to return possibly many files. This is because some
98        // file formats have multiple side-car files.
99        let mut import_files = vec![kittycad_modeling_cmds::ImportFile {
100            path: file_name,
101            data: file_contents.clone(),
102        }];
103
104        // In the case of a gltf importing a bin file we need to handle that! and figure out where the
105        // file is relative to our current file.
106        if let format::InputFormat::Gltf(format::gltf::import::Options {}) = format {
107            // Check if the file is a binary gltf file, in that case we don't need to import the bin
108            // file.
109            if !file_contents.starts_with(b"glTF") {
110                let json = gltf_json::Root::from_slice(&file_contents)
111                    .map_err(|e| ExecutionError::General { reason: e.to_string() })?;
112
113                // Read the gltf file and check if there is a bin file.
114                for buffer in json.buffers {
115                    if let Some(uri) = &buffer.uri {
116                        if !uri.starts_with("data:") {
117                            // We want this path relative to the file_path given.
118                            let bin_path = std::path::Path::new(&file_path)
119                                .parent()
120                                .map(|p| p.join(uri))
121                                .map(|p| p.to_string_lossy().to_string())
122                                .ok_or_else(|| ExecutionError::General {
123                                    reason: format!("Could not get the parent path of the file `{}`", file_path_str),
124                                })?;
125
126                            let bin_contents =
127                                fs::read(&bin_path).map_err(|e| ExecutionError::General { reason: e.to_string() })?;
128
129                            import_files.push(kittycad_modeling_cmds::ImportFile {
130                                path: uri.to_string(),
131                                data: bin_contents,
132                            });
133                        }
134                    }
135                }
136            }
137        }
138
139        // Write out to memory.
140        if let Some(memory_area) = format_destination {
141            match memory_area {
142                Destination::Address(addr) => {
143                    mem.set_composite(addr, format);
144                }
145                Destination::StackPush => {
146                    mem.stack.push(format.into_parts());
147                }
148                Destination::StackExtend => {
149                    mem.stack.extend(format.into_parts())?;
150                }
151            }
152        }
153        if let Some(memory_area) = files_destination {
154            match memory_area {
155                Destination::Address(addr) => {
156                    mem.set_composite(addr, import_files);
157                }
158                Destination::StackPush => {
159                    mem.stack.push(import_files.into_parts());
160                }
161                Destination::StackExtend => {
162                    mem.stack.extend(import_files.into_parts())?;
163                }
164            }
165        }
166
167        Ok(())
168    }
169}
170
171/// Zoo co-ordinate system.
172///
173/// * Forward: -Y
174/// * Up: +Z
175/// * Handedness: Right
176pub const ZOO_COORD_SYSTEM: coord::System = coord::System {
177    forward: coord::AxisDirectionPair {
178        axis: coord::Axis::Y,
179        direction: coord::Direction::Negative,
180    },
181    up: coord::AxisDirectionPair {
182        axis: coord::Axis::Z,
183        direction: coord::Direction::Positive,
184    },
185};
186
187fn from_vec_prim_to_res_opt_input_format(values: Vec<Primitive>) -> Result<Option<format::InputFormat>> {
188    let mut iter = values.iter();
189
190    let str_type = match iter.next() {
191        None => {
192            return Ok(None);
193        }
194        Some(Primitive::Nil) => {
195            return Ok(None);
196        }
197        Some(Primitive::String(str)) => str,
198        _ => {
199            return Err(ExecutionError::General {
200                reason: "missing type".to_string(),
201            });
202        }
203    };
204
205    return match str_type.as_str() {
206        "stl" => {
207            let Some(Primitive::String(str_units)) = iter.next() else {
208                return Err(ExecutionError::General {
209                    reason: "missing units".to_string(),
210                });
211            };
212            let Some(Primitive::String(str_coords_forward_axis)) = iter.next() else {
213                return Err(ExecutionError::General {
214                    reason: "missing coords.forward.axis".to_string(),
215                });
216            };
217            let Some(Primitive::String(str_coords_forward_direction)) = iter.next() else {
218                return Err(ExecutionError::General {
219                    reason: "missing coords.forward.direction".to_string(),
220                });
221            };
222            let Some(Primitive::String(str_coords_up_axis)) = iter.next() else {
223                return Err(ExecutionError::General {
224                    reason: "missing coords.up.axis".to_string(),
225                });
226            };
227            let Some(Primitive::String(str_coords_up_direction)) = iter.next() else {
228                return Err(ExecutionError::General {
229                    reason: "missing coords.up.direction".to_string(),
230                });
231            };
232            Ok(Some(format::InputFormat::Stl(format::stl::import::Options {
233                coords: coord::System {
234                    forward: coord::AxisDirectionPair {
235                        axis: coord::Axis::from_str(str_coords_forward_axis).unwrap(),
236                        direction: coord::Direction::from_str(str_coords_forward_direction).unwrap(),
237                    },
238                    up: coord::AxisDirectionPair {
239                        axis: coord::Axis::from_str(str_coords_up_axis).unwrap(),
240                        direction: coord::Direction::from_str(str_coords_up_direction).unwrap(),
241                    },
242                },
243                units: units::UnitLength::from_str(str_units).unwrap(),
244            })))
245        }
246        _ => Err(ExecutionError::General {
247            reason: "unknown type".to_string(),
248        }),
249    };
250}
251
252// Implemented here so we don't have to mess with kittycad::types...
253fn from_input_format(type_: format::InputFormat) -> String {
254    match type_ {
255        format::InputFormat::Fbx(_) => "fbx".to_string(),
256        format::InputFormat::Gltf(_) => "gltf".to_string(),
257        format::InputFormat::Obj(_) => "obj".to_string(),
258        format::InputFormat::Ply(_) => "ply".to_string(),
259        format::InputFormat::Sldprt(_) => "sldprt".to_string(),
260        format::InputFormat::Step(_) => "step".to_string(),
261        format::InputFormat::Stl(_) => "stl".to_string(),
262    }
263}
264
265fn validate_extension_format(ext: format::InputFormat, given: format::InputFormat) -> Result<()> {
266    if let format::InputFormat::Stl(format::stl::import::Options { coords: _, units: _ }) = ext {
267        if let format::InputFormat::Stl(format::stl::import::Options { coords: _, units: _ }) = given {
268            return Ok(());
269        }
270    }
271
272    if let format::InputFormat::Obj(format::obj::import::Options { coords: _, units: _ }) = ext {
273        if let format::InputFormat::Obj(format::obj::import::Options { coords: _, units: _ }) = given {
274            return Ok(());
275        }
276    }
277
278    if let format::InputFormat::Ply(format::ply::import::Options { coords: _, units: _ }) = ext {
279        if let format::InputFormat::Ply(format::ply::import::Options { coords: _, units: _ }) = given {
280            return Ok(());
281        }
282    }
283
284    if ext == given {
285        return Ok(());
286    }
287
288    Err(ExecutionError::General {
289        reason: format!(
290            "The given format does not match the file extension. Expected: `{}`, Given: `{}`",
291            from_input_format(ext),
292            from_input_format(given)
293        ),
294    })
295}
296
297/// Get the source format from the extension.
298fn get_import_format_from_extension(ext: &str) -> Result<format::InputFormat> {
299    let format = match shared::FileImportFormat::from_str(ext) {
300        Ok(format) => format,
301        Err(_) => {
302            if ext == "stp" {
303                shared::FileImportFormat::Step
304            } else if ext == "glb" {
305                shared::FileImportFormat::Gltf
306            } else {
307                return Err(ExecutionError::General {
308                    reason: format!(
309                      "unknown source format for file extension: {}. Try setting the `--src-format` flag explicitly or use a valid format.",
310                      ext
311                    )
312                });
313            }
314        }
315    };
316
317    // Make the default units millimeters.
318    let ul = units::UnitLength::Millimeters;
319
320    // Zoo co-ordinate system.
321    //
322    // * Forward: -Y
323    // * Up: +Z
324    // * Handedness: Right
325    match format {
326        shared::FileImportFormat::Step => Ok(format::InputFormat::Step(format::step::ImportOptions {})),
327        shared::FileImportFormat::Stl => Ok(format::InputFormat::Stl(format::stl::import::Options {
328            coords: ZOO_COORD_SYSTEM,
329            units: ul,
330        })),
331        shared::FileImportFormat::Obj => Ok(format::InputFormat::Obj(format::obj::import::Options {
332            coords: ZOO_COORD_SYSTEM,
333            units: ul,
334        })),
335        shared::FileImportFormat::Gltf => Ok(format::InputFormat::Gltf(format::gltf::import::Options::default())),
336        shared::FileImportFormat::Ply => Ok(format::InputFormat::Ply(format::ply::import::Options {
337            coords: ZOO_COORD_SYSTEM,
338            units: ul,
339        })),
340        shared::FileImportFormat::Fbx => Ok(format::InputFormat::Fbx(format::fbx::import::Options::default())),
341        shared::FileImportFormat::Sldprt => Ok(format::InputFormat::Sldprt(format::sldprt::import::Options::default())),
342    }
343}