Fix IMPORT naming, messages and 3D Studio limits

STEP assemblies: a link that closes a loop in the assembly is dropped,
so no block inserts itself; the walk is iterative. Each imported block
gets a name the drawing does not use yet instead of silently reusing an
existing definition, inserts are pointed at the names the blocks got,
and an insert of a block that could not be defined is left out with
the error reported.

Names that come from files (STEP products, 3D Studio objects, materials
and cameras, WMF font faces) go through a new shared
scene::sanitized_table_name, which applies the block-name rules
(reserved and control characters, 255 characters).

3D Studio: meshes with more vertices than a polyface mesh can index are
split over several meshes. Single-layer mode uses the current layer.
Materials are only created when the drawing has a material dictionary.
Camera views get the current view's proportions and the perspective
flag.

Messages show paths as given, the file-not-found and read-failure
messages are plain text of our own, and the "Press Esc" line is gone
since the import cannot be cancelled. The true-colour packing is
shared. "Select ACIS File" and "Import WMF" get catalog entries and
keys in all 22 locales.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-16 23:20:03 +03:00
1 parent dbc428b237
commit a0ed20df28
28 files changed
+205 -88

No files matched your search

+53 -39
View File
@@ -3,6 +3,8 @@
//! as one undo step.
use super::*;
use std::collections::HashMap;
use crate::io::model_import::{self, ModelFormat};
impl OpenCADStudio {
@@ -36,9 +38,8 @@ impl OpenCADStudio {
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)"
));
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))
@@ -82,40 +83,48 @@ impl OpenCADStudio {
.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.")),
}
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;
// A block the drawing already has keeps its definition and is
// inserted as it is.
// 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 {
if scene.document.block_records.get(&name).is_none() {
let _ = scene.define_block_from_owned_entities(entities, &name, glam::DVec3::ZERO);
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 imported.entities {
for entity in retarget(imported.entities, &defined) {
scene.add_entity(entity);
}
self.tabs[i].dirty = true;
@@ -140,7 +149,7 @@ impl OpenCADStudio {
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('/', "\\")));
.push_error(&format!("{path} is not a valid Windows metafile."));
return Task::none();
};
self.push_undo_snapshot(i, if prompt { "WMFIN" } else { "IMPORT" });
@@ -152,7 +161,8 @@ impl OpenCADStudio {
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();
let name = format!("WMF-{}{n}", face.name);
// 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();
@@ -210,7 +220,7 @@ impl OpenCADStudio {
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('/', "\\")))?;
.ok_or_else(|| format!("{path} is not a valid 3D Studio file."))?;
let mut objects: Vec<(String, &'static str, usize)> = scene
.objects
.iter()
@@ -322,10 +332,8 @@ impl OpenCADStudio {
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('/', "\\")
));
self.command_line
.push_error(&format!("{} is not a valid 3D Studio file.", request.path));
return;
};
let import = crate::io::tds_import::convert(
@@ -335,10 +343,18 @@ impl OpenCADStudio {
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();
@@ -349,20 +365,18 @@ impl OpenCADStudio {
});
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 = document.header.acad_material_dict_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(
((0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b))
as i32,
),
rgb: Some(crate::scene::convert::acis_import::packed_true_colour(r, g, b)),
},
None => codec::objects::MaterialColor::default(),
};
@@ -370,7 +384,6 @@ impl OpenCADStudio {
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);
}
@@ -393,8 +406,9 @@ impl OpenCADStudio {
named.direction =
codec::types::Vector3::new(view.direction[0], view.direction[1], view.direction[2]);
named.height = view.height;
named.width = 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(),