//! ACISIN and WMFIN, and the files IMPORT routes here: ACIS text (SAT), //! STEP and IGES models and Windows metafiles, read into the current drawing //! as one undo step. use super::*; use crate::io::model_import::{self, ModelFormat}; impl OpenCADStudio { /// ACISIN and WMFIN; `None` defers to the other families. pub(super) fn dispatch_model_import(&mut self, cmd: &str, i: usize) -> Option> { let (verb, rest) = match cmd.trim().split_once(char::is_whitespace) { Some((verb, rest)) => (verb.to_ascii_uppercase(), rest.trim().trim_matches('"')), None => (cmd.trim().to_ascii_uppercase(), ""), }; match (verb.as_str(), rest.is_empty()) { ("ACISIN", true) if crate::sys::file_dia() => { let filters = vec![("ACIS (*.sat)".to_string(), vec!["sat".to_string()])]; let title = crate::t!("Select ACIS File").into_owned(); Some(Task::perform(super::dgn_import::pick(title, filters), |path| match path { Some(path) => Message::Command(format!("_ACISINFILE {path}")), None => Message::Noop, })) } ("ACISIN", true) => { use crate::command::FreeTextValuePromptCommand; let command = FreeTextValuePromptCommand::new("_ACISINFILE", "Enter SAT file name:"); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Some(Task::none()) } ("ACISIN" | "_ACISINFILE", false) => { if crate::scene::model::pdf_raster::source_bytes(rest).is_none() { let name = std::path::Path::new(rest) .file_name() .map_or_else(|| rest.to_string(), |n| n.to_string_lossy().into_owned()); let here = std::env::current_dir() .map(|d| d.display().to_string()) .unwrap_or_default(); self.command_line.push_error(&format!( "\"{name}\": Can't find file in search path:\n {here}\\ (current directory)" )); return Some(Task::none()); } Some(self.import_model(i, rest, ModelFormat::Sat)) } ("_ACISINFILE", true) => Some(Task::none()), ("WMFIN", true) if crate::sys::file_dia() => { let filters = vec![("Metafile (*.wmf)".to_string(), vec!["wmf".to_string()])]; let title = crate::t!("Import WMF").into_owned(); Some(Task::perform(super::dgn_import::pick(title, filters), |path| match path { Some(path) => Message::Command(format!("_WMFINFILE {path}")), None => Message::Noop, })) } ("WMFIN", true) => { use crate::command::FreeTextValuePromptCommand; let command = FreeTextValuePromptCommand::new( "_WMFINFILE", "Enter name of WMF file to import (no extension):", ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Some(Task::none()) } ("WMFIN" | "_WMFINFILE", false) => { let path = if std::path::Path::new(rest).extension().is_some() { rest.to_string() } else { format!("{rest}.wmf") }; Some(self.import_wmf(i, &path, true)) } ("_WMFINFILE", true) => Some(Task::none()), _ => None, } } /// Reads a model file into drawing `i`. pub(in crate::app) fn import_model(&mut self, i: usize, path: &str, format: ModelFormat) -> Task { let Some(bytes) = crate::scene::model::pdf_raster::source_bytes(path) else { self.command_line .push_error(&format!("File {path} does not exist.")); return Task::none(); }; // The translated formats report the job as they run. let translated = format != ModelFormat::Sat; if translated { self.command_line .push_output("Import job in progress. Press Esc key to cancel."); } let drawing_mm = crate::app::properties::insunits_to_mm( self.tabs[i].scene.document.header.insertion_units, ); let Some(imported) = model_import::read(format, &bytes, drawing_mm) else { let shown = path.replace('/', "\\"); match format { ModelFormat::Sat => { self.command_line.push_error("Modeling Operation Error:"); self.command_line.push_error("Error Code Number is 58000"); self.command_line .push_error(&format!("Unable to read the ACIS file {shown}.")); } _ => self .command_line .push_error(&format!("Import failed: {shown} has no model the importer reads.")), } return Task::none(); }; self.push_undo_snapshot(i, "IMPORT"); let scene = &mut self.tabs[i].scene; // A block the drawing already has keeps its definition and is // inserted as it is. for (name, entities) in imported.blocks { if scene.document.block_records.get(&name).is_none() { let _ = scene.define_block_from_owned_entities(entities, &name, glam::DVec3::ZERO); } } for entity in imported.entities { scene.add_entity(entity); } self.tabs[i].dirty = true; if translated { self.command_line.push_output("Import Job Complete"); } self.refresh_properties(); Task::none() } /// A metafile as a new block `WMF`, fitted to half the current view: /// placed at the view's top-left corner (IMPORT), or by the insertion /// prompts (WMFIN). pub(in crate::app) fn import_wmf(&mut self, i: usize, path: &str, prompt: bool) -> Task { let Some(bytes) = crate::scene::model::pdf_raster::source_bytes(path) else { self.command_line .push_error(&format!("File {path} does not exist.")); return Task::none(); }; let (centre, size, pixel) = self.tabs[i].scene.view_window(); let Some(mut picture) = crate::io::wmf_import::read(&bytes, [0.5 * size[0], 0.5 * size[1]]) else { self.command_line .push_error(&format!("{} is not a valid Windows metafile.", path.replace('/', "\\"))); return Task::none(); }; self.push_undo_snapshot(i, if prompt { "WMFIN" } else { "IMPORT" }); let scene = &mut self.tabs[i].scene; // A text style per face, `WMF-` with `n` counting the // face's bold and italic variants; the style's font is the // installed file of that face, none when it is not installed. A // style the drawing already has is used as it is. let mut styles: Vec = Vec::new(); for (k, face) in picture.fonts.iter().enumerate() { let n = picture.fonts[..k].iter().filter(|f| f.name == face.name).count(); let name = format!("WMF-{}{n}", face.name); if !scene.document.text_styles.contains(&name) { let mut style = codec::tables::TextStyle::new(name.as_str()); style.handle = scene.document.allocate_handle(); style.font_file = crate::scene::text::sysfont::face_file(&face.name, face.bold, face.italic).unwrap_or_default(); let _ = scene.document.text_styles.add(style); } styles.push(name); } for (at, font) in &picture.text_fonts { if let Some(codec::EntityType::Text(text)) = picture.entities.get_mut(*at) { text.style = styles[*font].clone(); } } let mut n = 0; let name = loop { let name = format!("WMF{n}"); if scene.document.block_records.get(&name).is_none() { break name; } n += 1; }; if let Err(error) = scene.define_block_from_owned_entities(picture.entities, &name, glam::DVec3::ZERO) { self.command_line.push_error(&error); return Task::none(); } self.tabs[i].dirty = true; if prompt { use crate::command::CadCommand; use crate::modules::insert::insert_block::InsertBlockCommand; let preview = self.tabs[i].scene.block_preview_wires(&name); let command = InsertBlockCommand::classic_for_block(name, preview, String::new()); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } else { // The view's top-left corner, a pixel or two in. let corner = codec::types::Vector3::new( centre[0] - 0.5 * size[0] + pixel, centre[1] + 0.5 * size[1] - 2.0 * pixel, 0.0, ); let insert = codec::entities::Insert::new(&name, corner); self.tabs[i].scene.add_entity(codec::EntityType::Insert(insert)); self.refresh_properties(); } Task::none() } } impl OpenCADStudio { /// The 3D Studio Import window for a file; an error when it is no 3D /// Studio file. pub(in crate::app) fn open_tds_import_dialog(&mut self, path: &str) -> Result<(), String> { let scene = crate::scene::model::pdf_raster::source_bytes(path) .and_then(|bytes| crate::io::tds_import::read(&bytes)) .ok_or_else(|| format!("{} is not a valid 3D Studio file.", path.replace('/', "\\")))?; let mut objects: Vec<(String, &'static str, usize)> = scene .objects .iter() .map(|(name, object)| { let materials = match object { crate::io::tds_import::TdsObject::Mesh(mesh) => scene.mesh_materials(mesh).len(), _ => 0, }; (name.clone(), object.kind(), materials) }) .collect(); objects.sort_by_key(|o| o.0.to_lowercase()); objects.insert(0, ("Global Settings".to_string(), "Global", 0)); // The layer and material choices made last stay; nothing starts // checked. let previous = self.import_dialogs.tds.take(); self.import_dialogs.tds = Some(crate::ui::window::import_dialogs::TdsImportState { path: path.to_string(), checked: vec![false; objects.len()], objects, materials: scene.materials.len(), filter: 0, search: String::new(), layers: previous.as_ref().map_or(0, |s| s.layers), multi: previous.as_ref().map_or(0, |s| s.multi), }); self.active_modal = Some(crate::app::ModalKind::TdsImport); Ok(()) } /// The window's choices as an import: asked mesh by mesh first when set /// to ask and some checked mesh has several materials. pub(in crate::app) fn start_tds_import( &mut self, i: usize, state: crate::ui::window::import_dialogs::TdsImportState, ) -> Task { use crate::io::tds_import::{LayerMode, MultiMaterial, TdsObject}; use crate::modules::insert::tds_import::{TdsImportRequest, TdsMaterialCommand}; let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&state.path) .and_then(|bytes| crate::io::tds_import::read(&bytes)) else { return Task::none(); }; let selected: Vec = state .objects .iter() .zip(&state.checked) .filter(|(_, on)| **on) .map(|((name, _, _), _)| name.clone()) .collect(); let several: Vec<(String, Vec)> = scene .objects .iter() .filter(|(name, _)| selected.contains(name)) .filter_map(|(name, object)| match object { TdsObject::Mesh(mesh) => { let materials = scene.mesh_materials(mesh); (materials.len() > 1).then(|| (name.clone(), materials)) } _ => None, }) .collect(); let choice = match state.multi { 1 => Some(MultiMaterial::Split), 2 => Some(MultiMaterial::First), 3 => Some(MultiMaterial::Material(None)), _ => None, }; let mut request = TdsImportRequest { path: state.path.clone(), selected, layers: match state.layers { 1 => LayerMode::ByMaterial, 2 => LayerMode::ByObjectColor, 3 => LayerMode::Single, _ => LayerMode::ByObject, }, multi: Default::default(), compact: matches!(state.multi, 0 | 1), }; match choice { Some(choice) => { for (name, _) in several { request.multi.insert(name, choice.clone()); } } None if !several.is_empty() => { use crate::command::CadCommand; let command = TdsMaterialCommand::new(request, several); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); return Task::none(); } None => {} } self.run_tds_import(i, request); Task::none() } /// The import itself, one undo step: the meshes on their layers with /// their materials, the cameras as named views. pub(in crate::app) fn run_tds_import( &mut self, i: usize, request: crate::modules::insert::tds_import::TdsImportRequest, ) { use codec::objects::ObjectType; let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&request.path) .and_then(|bytes| crate::io::tds_import::read(&bytes)) else { self.command_line.push_error(&format!( "{} is not a valid 3D Studio file.", request.path.replace('/', "\\") )); return; }; let import = crate::io::tds_import::convert( &scene, &crate::io::tds_import::TdsOptions { selected: request.selected.clone(), layers: request.layers, multi: request.multi.clone(), compact: request.compact, }, ); self.push_undo_snapshot(i, "IMPORT"); let document = &mut self.tabs[i].scene.document; // Materials by name: the drawing's own when it has one, else one // made from the file's colours. let mut materials: std::collections::HashMap = Default::default(); for name in &import.materials { let existing = document.objects.iter().find_map(|(h, o)| match o { ObjectType::Material(m) if m.name.eq_ignore_ascii_case(name) => Some(*h), _ => None, }); let handle = match existing { Some(h) => h, None => { let source = scene.materials.iter().find(|m| m.name == *name); let mut material = codec::objects::Material::new(); material.handle = document.allocate_handle(); material.owner = document.header.acad_material_dict_handle; material.name = name.clone(); let color = |rgb: Option<[u8; 3]>| match rgb { Some([r, g, b]) => codec::objects::MaterialColor { flag: 1, factor: 1.0, rgb: Some( ((0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b)) as i32, ), }, None => codec::objects::MaterialColor::default(), }; material.ambient_color = color(source.and_then(|m| m.ambient)); material.diffuse_color = color(source.and_then(|m| m.diffuse)); material.specular_color = color(source.and_then(|m| m.specular)); let handle = material.handle; let dictionary = document.header.acad_material_dict_handle; if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) { dictionary.add_entry(name.clone(), handle); } document.objects.insert(handle, ObjectType::Material(material)); handle } }; materials.insert(name.clone(), handle); } for part in &import.parts { if !document.layers.contains(&part.layer) { let mut layer = codec::tables::Layer::new(part.layer.as_str()); layer.handle = document.allocate_handle(); let _ = document.layers.add(layer); } } for view in &import.views { let mut named = codec::tables::View::new(view.name.as_str()); named.target = codec::types::Vector3::new(view.target[0], view.target[1], view.target[2]); named.direction = codec::types::Vector3::new(view.direction[0], view.direction[1], view.direction[2]); named.height = view.height; named.width = view.height; named.lens_length = view.lens; named.handle = match document.views.get(&view.name) { Some(old) => old.handle, None => document.allocate_handle(), }; document.views.add_or_replace(named); } for part in import.parts { let mut mesh = part.mesh; mesh.common.layer = part.layer.clone(); mesh.common.material_handle = part.material.as_ref().and_then(|m| materials.get(m).copied()); if mesh.common.material_handle.is_some() { mesh.common.material_flags = 0b11; } self.tabs[i].scene.add_entity(codec::EntityType::PolyfaceMesh(mesh)); } for line in &import.messages { self.command_line.push_output(line); } self.tabs[i].dirty = true; self.refresh_properties(); } }