forked from 1b-agent/opencadstudio-upstream
Merge review/1844: PR #1844 IMPORT with fixes
This commit is contained in:
commit
ea72ffad5f
50 files changed
+5120
-120
No files matched your search
@@ -664,6 +664,11 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
return self.run_dgn_import(i, request);
|
||||
}
|
||||
CmdResult::TdsImport(request) => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.run_tds_import(i, request);
|
||||
}
|
||||
CmdResult::PdfImportFile(import) => {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
|
||||
@@ -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,9 @@ fn is_dgn(path: &str) -> bool {
|
||||
enum Route {
|
||||
Pdf,
|
||||
Dgn,
|
||||
Model(ModelFormat),
|
||||
Wmf,
|
||||
Tds,
|
||||
}
|
||||
|
||||
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
|
||||
@@ -75,6 +79,22 @@ 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),
|
||||
Some("3D Studio") => return Ok(Route::Tds),
|
||||
_ => {}
|
||||
}
|
||||
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 +131,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 +237,24 @@ 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)
|
||||
}
|
||||
// The options window opens whatever FILEDIA is.
|
||||
Ok(Route::Tds) => {
|
||||
self.remember_import(path);
|
||||
if let Err(error) = self.open_tds_import_dialog(path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
Task::none()
|
||||
@@ -232,6 +270,7 @@ impl OpenCADStudio {
|
||||
"dgn" => crate::t!(
|
||||
"Imports a DGN model as drawing objects: levels as layers, cells as blocks."
|
||||
),
|
||||
"3ds" => std::borrow::Cow::Owned(crate::tr!("import-3ds", "help")),
|
||||
_ => crate::t!(
|
||||
"Imports a file of the chosen format with the format's importer."
|
||||
),
|
||||
@@ -324,8 +363,81 @@ 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);
|
||||
}
|
||||
Ok(Route::Tds) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
if let Err(error) = self.open_tds_import_dialog(&path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── 3D Studio Import ─────────────────────────────────────────
|
||||
ImportDialogMsg::TdsChange => {
|
||||
let title = crate::t!("Import File").into_owned();
|
||||
let filters = vec![("3D Studio (*.3ds)".to_string(), vec!["3ds".to_string()])];
|
||||
return Task::perform(pick(title, filters), |path| {
|
||||
Message::ImportDialog(ImportDialogMsg::TdsPicked(path))
|
||||
});
|
||||
}
|
||||
ImportDialogMsg::TdsPicked(Some(path)) => {
|
||||
if let Err(error) = self.open_tds_import_dialog(&path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsPicked(None) => {}
|
||||
ImportDialogMsg::TdsCheck(k, on) => {
|
||||
if let Some(c) = self.import_dialogs.tds.as_mut().and_then(|s| s.checked.get_mut(k)) {
|
||||
*c = on;
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsAll | ImportDialogMsg::TdsClear => {
|
||||
let on = matches!(message, ImportDialogMsg::TdsAll);
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.checked.iter_mut().for_each(|c| *c = on);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsFilter(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.filter = k.min(4);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsSearch(text) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.search = text;
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsLayers(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.layers = k.min(3);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsMulti(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.multi = k.min(3);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsOk => {
|
||||
let Some(state) = self.import_dialogs.tds.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
if !state.checked.iter().any(|c| *c) {
|
||||
self.import_dialogs.tds = Some(state);
|
||||
return Task::none();
|
||||
}
|
||||
self.close_active_modal();
|
||||
return self.start_tds_import(i, state);
|
||||
}
|
||||
// ── DGN Import ───────────────────────────────────────────────
|
||||
ImportDialogMsg::DgnChange => return self.pick_dgn_file(),
|
||||
ImportDialogMsg::DgnPicked(Some(path)) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ mod fileops;
|
||||
mod inquiry;
|
||||
mod layerprops;
|
||||
mod layers;
|
||||
mod model_import;
|
||||
mod pc_colormap;
|
||||
mod pc_extract;
|
||||
mod pdf_dialogs;
|
||||
@@ -415,6 +416,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);
|
||||
}
|
||||
@@ -1016,6 +1020,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"QNEW",
|
||||
"OBJIMPORT",
|
||||
"IMPORT",
|
||||
"ACISIN",
|
||||
"WMFIN",
|
||||
"DGNIMPORT",
|
||||
"-DGNIMPORT",
|
||||
"OPEN",
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
//! 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 std::collections::HashMap;
|
||||
|
||||
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}\": file not found in the current directory, {here}"));
|
||||
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();
|
||||
};
|
||||
let translated = format != ModelFormat::Sat;
|
||||
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 {
|
||||
self.command_line
|
||||
.push_error(&format!("Import failed: {path} has no model the importer reads."));
|
||||
return Task::none();
|
||||
};
|
||||
self.push_undo_snapshot(i, "IMPORT");
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
// Each block goes under a name the drawing does not use yet (a
|
||||
// product named like a block already there gets a new name, not the
|
||||
// old definition), and the inserts are pointed at the names the
|
||||
// blocks got. An insert of a block that could not be made is left
|
||||
// out.
|
||||
fn retarget(entities: Vec<codec::EntityType>, defined: &HashMap<String, String>) -> Vec<codec::EntityType> {
|
||||
entities
|
||||
.into_iter()
|
||||
.filter_map(|entity| match entity {
|
||||
codec::EntityType::Insert(mut insert) => {
|
||||
insert.block_name = defined.get(&insert.block_name)?.clone();
|
||||
Some(codec::EntityType::Insert(insert))
|
||||
}
|
||||
other => Some(other),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let mut taken: Vec<String> = scene.document.block_records.iter().map(|b| b.name.clone()).collect();
|
||||
let mut defined: HashMap<String, String> = HashMap::new();
|
||||
for (name, entities) in imported.blocks {
|
||||
let unique = crate::modules::insert::xattach::unique_block_name(&name, &taken);
|
||||
let entities = retarget(entities, &defined);
|
||||
match scene.define_block_from_owned_entities(entities, &unique, glam::DVec3::ZERO) {
|
||||
Ok(_) => {
|
||||
taken.push(unique.clone());
|
||||
defined.insert(name, unique);
|
||||
}
|
||||
Err(error) => self.command_line.push_error(&format!("{name}: {error}")),
|
||||
}
|
||||
}
|
||||
for entity in retarget(imported.entities, &defined) {
|
||||
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!("{path} is not a valid Windows metafile."));
|
||||
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-<face><n>` 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<String> = Vec::new();
|
||||
for (k, face) in picture.fonts.iter().enumerate() {
|
||||
let n = picture.fonts[..k].iter().filter(|f| f.name == face.name).count();
|
||||
// The face name comes from the file.
|
||||
let name = crate::scene::sanitized_table_name(&format!("WMF-{}{n}", face.name), "WMF");
|
||||
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!("{path} is not a valid 3D Studio file."))?;
|
||||
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<Message> {
|
||||
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<String> = state
|
||||
.objects
|
||||
.iter()
|
||||
.zip(&state.checked)
|
||||
.filter(|(_, on)| **on)
|
||||
.map(|((name, _, _), _)| name.clone())
|
||||
.collect();
|
||||
let several: Vec<(String, Vec<String>)> = 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));
|
||||
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,
|
||||
single_layer: self.tabs[i].scene.document.header.current_layer_name.clone(),
|
||||
},
|
||||
);
|
||||
// Named views take the current view's proportions.
|
||||
let (_, size, _) = self.tabs[i].scene.view_window();
|
||||
let aspect = if size[0] > 0.0 && size[1] > 0.0 { size[0] / size[1] } else { 1.0 };
|
||||
self.push_undo_snapshot(i, "IMPORT");
|
||||
let document = &mut self.tabs[i].scene.document;
|
||||
// Materials live in the material dictionary; a drawing without one
|
||||
// imports the meshes without materials.
|
||||
let dictionary = document.header.acad_material_dict_handle;
|
||||
let has_dictionary = matches!(document.objects.get(&dictionary), Some(ObjectType::Dictionary(_)));
|
||||
// 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<String, codec::Handle> = 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 if !has_dictionary => continue,
|
||||
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 = dictionary;
|
||||
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(crate::scene::convert::acis_import::packed_true_colour(r, g, b)),
|
||||
},
|
||||
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;
|
||||
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 * aspect;
|
||||
named.lens_length = view.lens;
|
||||
named.perspective = true;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,7 @@ pub(super) const NAMES: &[&str] = &[
|
||||
"pdf_import_pick",
|
||||
"pdf_import_dialog",
|
||||
"dgn_import_dialog",
|
||||
"tds_import_dialog",
|
||||
"refedit_dialog",
|
||||
"pdf_shx_settings",
|
||||
"pdf_layer_toggle",
|
||||
@@ -2234,6 +2235,45 @@ impl OpenCADStudio {
|
||||
_ => return Err(bad()),
|
||||
})
|
||||
}
|
||||
// The 3D Studio Import window, one field per call: "check=<name>"
|
||||
// and "uncheck=<name>" (the object's name, or "Global Settings"),
|
||||
// "all", "clear", "filter=all|mesh|light|camera|general",
|
||||
// "search=<text>", "layers=object|material|color|single",
|
||||
// "multi=ask|split|first|none", "path=<file>" (as Change… picks
|
||||
// it), "ok" or "cancel".
|
||||
"tds_import_dialog" => {
|
||||
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
|
||||
if self.active_modal != Some(crate::app::ModalKind::TdsImport) {
|
||||
return Err(failure("no_dialog", "The 3D Studio Import window is not open"));
|
||||
}
|
||||
let spec = string(req, "value")?;
|
||||
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
||||
let bad = || failure("invalid_value", spec);
|
||||
if key == "cancel" {
|
||||
return Ok(Task::done(Message::CloseModal));
|
||||
}
|
||||
let pick = |options: &[&str]| options.iter().position(|o| *o == value).ok_or_else(bad);
|
||||
Message::ImportDialog(match key {
|
||||
"ok" => I::TdsOk,
|
||||
"all" => I::TdsAll,
|
||||
"clear" => I::TdsClear,
|
||||
"check" | "uncheck" => {
|
||||
let k = self
|
||||
.import_dialogs
|
||||
.tds
|
||||
.as_ref()
|
||||
.and_then(|s| s.objects.iter().position(|(name, _, _)| name.eq_ignore_ascii_case(value)))
|
||||
.ok_or_else(bad)?;
|
||||
I::TdsCheck(k, key == "check")
|
||||
}
|
||||
"filter" => I::TdsFilter(pick(&["all", "mesh", "light", "camera", "general"])?),
|
||||
"search" => I::TdsSearch(value.to_string()),
|
||||
"layers" => I::TdsLayers(pick(&["object", "material", "color", "single"])?),
|
||||
"multi" => I::TdsMulti(pick(&["ask", "split", "first", "none"])?),
|
||||
"path" => I::TdsPicked(Some(value.to_string())),
|
||||
_ => return Err(bad()),
|
||||
})
|
||||
}
|
||||
// The DGN Import window, one field per call: "path=<file>" (as
|
||||
// Change… picks it), "model=<n>", "target=new|current",
|
||||
// "conflict=prefix|ignore", "units=master|sub", "explode=on|off",
|
||||
|
||||
@@ -2340,6 +2340,8 @@ pub enum ModalKind {
|
||||
ImportFile,
|
||||
/// DGNIMPORT's DGN Import window.
|
||||
DgnImport,
|
||||
/// IMPORT's 3D Studio Import window.
|
||||
TdsImport,
|
||||
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
|
||||
/// save awaits a restore/discard decision. Distinct from the corrupt-file
|
||||
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
|
||||
|
||||
@@ -255,6 +255,7 @@ impl OpenCADStudio {
|
||||
Some(PdfImportFile) => self.pdf_import_file = None,
|
||||
Some(ImportFile) => self.import_dialogs.file = None,
|
||||
Some(DgnImport) => self.import_dialogs.dgn = None,
|
||||
Some(TdsImport) => self.import_dialogs.tds = None,
|
||||
Some(XrefAttach) => self.xref_attach = None,
|
||||
Some(WriteBlock) => self.wblock = None,
|
||||
Some(Hyperlink) => {
|
||||
|
||||
@@ -51,6 +51,7 @@ impl OpenCADStudio {
|
||||
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
|
||||
Some(K::ImportFile) => crate::t!("Import File").into_owned(),
|
||||
Some(K::DgnImport) => crate::t!("DGN Import").into_owned(),
|
||||
Some(K::TdsImport) => crate::tr!("import-3ds", "title"),
|
||||
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
|
||||
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
|
||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||
@@ -620,6 +621,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::import_dialogs::view_import_file(state, recent, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::TdsImport => {
|
||||
let state = self.import_dialogs.tds.as_ref()?;
|
||||
sized_flow(ex, 860, 560, |flow| {
|
||||
crate::ui::window::import_dialogs::view_tds_import(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::DgnImport => {
|
||||
let state = self.import_dialogs.dgn.as_ref()?;
|
||||
sized_flow(ex, 900, 680, |flow| {
|
||||
|
||||
Reference in new issue
Block a user