forked from 1b-agent/opencadstudio-upstream
IMPORT: read SAT, STEP, IGES and WMF files; ACISIN and WMFIN
ACIS text files come in one object per body (3DSOLID, REGION or SURFACE). STEP B-rep models become a block per product with component inserts at their placements, scaled to the drawing's units; IGES manifold solids come in as 3DSOLIDs scaled to the drawing's units with their colours. Splines that are exactly analytic shapes are kept as those shapes. Windows metafiles become a WMF<n> block of polylines and text fitted to half the view, placed at its top-left corner or by WMFIN's insertion prompts. The Import File window lists the four formats as supported. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3c9404510f
commit
659bb06cd6
38 files changed
+2400
-113
No files matched your search
@@ -8,6 +8,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::io::model_import::ModelFormat;
|
||||
use crate::modules::insert::dgn_import::{self, DgnImportCommand, DgnImportRequest};
|
||||
use crate::ui::window::import_dialogs::{
|
||||
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
|
||||
@@ -18,7 +19,7 @@ const RECENT: usize = 5;
|
||||
|
||||
/// Picks a file and registers its bytes (the web build reads them only
|
||||
/// here); `None` when the picker is cancelled.
|
||||
async fn pick(title: String, filters: Vec<(String, Vec<String>)>) -> Option<String> {
|
||||
pub(super) async fn pick(title: String, filters: Vec<(String, Vec<String>)>) -> Option<String> {
|
||||
let mut dialog = crate::sys::file_dialog().set_title(title.as_str());
|
||||
for (name, extensions) in &filters {
|
||||
let extensions: Vec<&str> = extensions.iter().map(String::as_str).collect();
|
||||
@@ -62,6 +63,8 @@ fn is_dgn(path: &str) -> bool {
|
||||
enum Route {
|
||||
Pdf,
|
||||
Dgn,
|
||||
Model(ModelFormat),
|
||||
Wmf,
|
||||
}
|
||||
|
||||
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
|
||||
@@ -75,6 +78,21 @@ fn route(path: &str, format: Option<usize>) -> Result<Route, String> {
|
||||
}
|
||||
let ext = extension(path);
|
||||
let by_extension = f.is_none_or(|f| f.support == Support::ByExtension);
|
||||
// The chosen format, or the one whose patterns name the extension.
|
||||
let named = if by_extension {
|
||||
FORMATS
|
||||
.iter()
|
||||
.find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)))
|
||||
} else {
|
||||
f
|
||||
};
|
||||
match named.map(|f| f.name) {
|
||||
Some("ACIS") => return Ok(Route::Model(ModelFormat::Sat)),
|
||||
Some("STEP") => return Ok(Route::Model(ModelFormat::Step)),
|
||||
Some("IGES") => return Ok(Route::Model(ModelFormat::Iges)),
|
||||
Some("Metafile") => return Ok(Route::Wmf),
|
||||
_ => {}
|
||||
}
|
||||
let pdf = if by_extension { ext == "pdf" } else { f.is_some_and(|f| f.name == "PDF Files") };
|
||||
if pdf {
|
||||
return Ok(Route::Pdf);
|
||||
@@ -111,8 +129,8 @@ impl OpenCADStudio {
|
||||
Some(Task::none())
|
||||
}
|
||||
("IMPORT", true) => {
|
||||
use crate::command::{CadCommand, ValuePromptCommand};
|
||||
let command = ValuePromptCommand::new("_IMPORTFILE", "Enter import file name:");
|
||||
use crate::command::{CadCommand, FreeTextValuePromptCommand};
|
||||
let command = FreeTextValuePromptCommand::new("_IMPORTFILE", "Enter import file name:");
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
@@ -217,6 +235,16 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.dispatch_command(&format!("-DGNIMPORT {path}"))
|
||||
}
|
||||
Ok(Route::Model(format)) => {
|
||||
self.remember_import(path);
|
||||
let i = self.active_tab;
|
||||
self.import_model(i, path, format)
|
||||
}
|
||||
Ok(Route::Wmf) => {
|
||||
self.remember_import(path);
|
||||
let i = self.active_tab;
|
||||
self.import_wmf(i, path, false)
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
Task::none()
|
||||
@@ -324,6 +352,16 @@ impl OpenCADStudio {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
Ok(Route::Model(kind)) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
return self.import_model(i, &path, kind);
|
||||
}
|
||||
Ok(Route::Wmf) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
return self.import_wmf(i, &path, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── DGN Import ───────────────────────────────────────────────
|
||||
|
||||
@@ -20,6 +20,7 @@ mod fileops;
|
||||
mod inquiry;
|
||||
mod layerprops;
|
||||
mod layers;
|
||||
mod model_import;
|
||||
mod pc_colormap;
|
||||
mod pc_extract;
|
||||
mod pdf_dialogs;
|
||||
@@ -393,6 +394,9 @@ impl OpenCADStudio {
|
||||
if let Some(t) = self.dispatch_import(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_model_import(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_bvisibility(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
@@ -994,6 +998,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"QNEW",
|
||||
"OBJIMPORT",
|
||||
"IMPORT",
|
||||
"ACISIN",
|
||||
"WMFIN",
|
||||
"DGNIMPORT",
|
||||
"-DGNIMPORT",
|
||||
"OPEN",
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
//! 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<Task<Message>> {
|
||||
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<Message> {
|
||||
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<n>`, 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<Message> {
|
||||
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 font, named WMF-<n> after the first free number.
|
||||
let mut styles: Vec<String> = Vec::new();
|
||||
for _ in &picture.fonts {
|
||||
let mut n = 0;
|
||||
let name = loop {
|
||||
let name = format!("WMF-{n}");
|
||||
if !scene.document.text_styles.contains(&name) && !styles.contains(&name) {
|
||||
break name;
|
||||
}
|
||||
n += 1;
|
||||
};
|
||||
let mut style = codec::tables::TextStyle::new(name.as_str());
|
||||
style.handle = scene.document.allocate_handle();
|
||||
// No font of its own, as the metafile names the face only.
|
||||
style.font_file = String::new();
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user