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

+39 -21
View File
@@ -30,19 +30,14 @@ fn ratio(file_mm: f64, drawing_mm: Option<f64>) -> f64 {
/// A block name from a product's, with the characters names cannot hold
/// replaced.
fn block_name(name: &str) -> String {
let name: String = name
.trim()
.chars()
.map(|c| if "<>/\\\":;?*|,=`".contains(c) { '_' } else { c })
.collect();
if name.is_empty() { "Part".to_string() } else { name }
crate::scene::sanitized_table_name(name, "Part")
}
/// A STEP presentation colour as the drawing colour it imports as: the
/// standard index colour it is close to — red, yellow, green, cyan, blue,
/// magenta, white, or the dark grey 250 (which black also becomes) — and
/// otherwise the true colour.
// ponytail: the closeness bands are fitted to measured imports (saturation
// ponytail: the closeness bands are fitted to sample files (saturation
// at least 0.75, value at least 0.825 and hue within 7.5 degrees for the six
// hues; greys from 0.16 to 0.39 for 250 and from 0.935 for white); refit
// them if a colour on a band edge comes out differently.
@@ -114,22 +109,43 @@ fn step_blocks(model: crate::io::step_import::StepModel, scale: f64) -> Imported
}
names.push(name);
}
// Children before parents.
// Children before parents. A link that closes a loop in the file's
// assembly (a product that would contain itself) is dropped, so no
// block ends up inserting itself.
let mut order = Vec::new();
let mut dropped: std::collections::HashSet<(usize, usize)> = Default::default();
let mut state = vec![0u8; model.products.len()];
fn visit(k: usize, model: &crate::io::step_import::StepModel, state: &mut [u8], order: &mut Vec<usize>) {
if state[k] != 0 {
return;
}
state[k] = 1;
for (child, _) in &model.products[k].children {
visit(*child, model, state, order);
}
state[k] = 2;
order.push(k);
}
for &root in &model.roots {
visit(root, &model, &mut state, &mut order);
if state[root] != 0 {
continue;
}
state[root] = 1;
// Each product being walked and the next of its children to visit.
let mut stack = vec![(root, 0usize)];
while let Some((k, next)) = stack.last_mut() {
let k = *k;
match model.products[k].children.get(*next) {
Some(&(child, _)) => {
let link = (k, *next);
*next += 1;
match state[child] {
0 => {
state[child] = 1;
stack.push((child, 0));
}
1 => {
dropped.insert(link);
}
_ => {}
}
}
None => {
state[k] = 2;
order.push(k);
stack.pop();
}
}
}
}
let blocks = order
.iter()
@@ -147,7 +163,9 @@ fn step_blocks(model: crate::io::step_import::StepModel, scale: f64) -> Imported
product
.children
.iter()
.map(|(child, placement)| {
.enumerate()
.filter(|(n, _)| !dropped.contains(&(k, *n)))
.map(|(_, (child, placement))| {
let mut insert = placed_insert(&names[*child], placement);
insert.common_mut().color = Color::ByBlock;
insert
+42 -21
View File
@@ -8,6 +8,8 @@ use std::collections::HashMap;
use codec::entities::{PolyfaceFace, PolyfaceMesh, PolyfaceVertex};
use codec::types::Vector3;
use crate::scene::sanitized_table_name;
pub struct TdsMaterial {
pub name: String,
pub ambient: Option<[u8; 3]>,
@@ -258,6 +260,8 @@ pub struct TdsOptions {
/// they first use them — as they do when the import splits by
/// material or asks — rather than the file's vertex list.
pub compact: bool,
/// The layer every mesh goes on with [`LayerMode::Single`].
pub single_layer: String,
}
/// One imported mesh.
@@ -284,14 +288,24 @@ pub struct TdsImport {
pub messages: Vec<String>,
}
/// A polyface mesh of `faces` of `mesh`: its vertices in file order, or
/// Polyface meshes of `faces` of `mesh`: its vertices in file order, or
/// (`compact`) only those the faces use, in the order they first use them.
fn polyface(mesh: &TdsMesh, faces: &[usize], compact: bool) -> PolyfaceMesh {
let mut out = PolyfaceMesh::new();
/// A polyface mesh numbers its vertices in 16 bits, so a mesh with more
/// vertices than that is always compact, and the faces that need further
/// vertices go on further meshes.
fn polyfaces(mesh: &TdsMesh, faces: &[usize], compact: bool) -> Vec<PolyfaceMesh> {
const MAX_VERTICES: usize = i16::MAX as usize;
let compact = compact || mesh.vertices.len() > MAX_VERTICES;
let vertex = |v: usize| {
let p = mesh.vertices[v];
PolyfaceVertex::new(Vector3::new(p[0], p[1], p[2]))
};
let mut out = Vec::new();
let mut current = PolyfaceMesh::new();
let mut index: HashMap<usize, i16> = HashMap::new();
if !compact {
for (k, v) in mesh.vertices.iter().enumerate() {
index.insert(k, out.add_vertex(PolyfaceVertex::new(Vector3::new(v[0], v[1], v[2]))));
for k in 0..mesh.vertices.len() {
index.insert(k, current.add_vertex(vertex(k)));
}
}
for &f in faces {
@@ -299,14 +313,15 @@ fn polyface(mesh: &TdsMesh, faces: &[usize], compact: bool) -> PolyfaceMesh {
if face.iter().any(|v| *v >= mesh.vertices.len()) {
continue;
}
let ids = face.map(|v| {
*index.entry(v).or_insert_with(|| {
let p = mesh.vertices[v];
out.add_vertex(PolyfaceVertex::new(Vector3::new(p[0], p[1], p[2])))
})
});
out.add_face(PolyfaceFace::triangle(ids[0], ids[1], ids[2]));
let new = face.iter().filter(|v| !index.contains_key(v)).count();
if current.vertices.len() + new > MAX_VERTICES {
out.push(std::mem::replace(&mut current, PolyfaceMesh::new()));
index.clear();
}
let ids = face.map(|v| *index.entry(v).or_insert_with(|| current.add_vertex(vertex(v))));
current.add_face(PolyfaceFace::triangle(ids[0], ids[1], ids[2]));
}
out.push(current);
out
}
@@ -365,24 +380,30 @@ pub fn convert(scene: &TdsScene, options: &TdsOptions) -> TdsImport {
out.materials.push(m.clone());
}
}
// Object and material names come from the file, so they
// are made fit to name a layer.
let layer = match options.layers {
LayerMode::ByObject => name.to_uppercase(),
LayerMode::ByMaterial => material.as_deref().unwrap_or("0").to_uppercase(),
LayerMode::ByObject => sanitized_table_name(&name.to_uppercase(), "0"),
LayerMode::ByMaterial => {
sanitized_table_name(&material.as_deref().unwrap_or("0").to_uppercase(), "0")
}
LayerMode::ByObjectColor => match mesh.color {
Some(c) => format!("COLOR{c:02}"),
None => "COLORNONE".to_string(),
},
LayerMode::Single => "AVLAYER".to_string(),
LayerMode::Single => options.single_layer.clone(),
};
out.parts.push(TdsPart {
layer,
material,
mesh: polyface(mesh, &faces, compact),
});
for polyface in polyfaces(mesh, &faces, compact) {
out.parts.push(TdsPart {
layer: layer.clone(),
material: material.clone(),
mesh: polyface,
});
}
}
}
TdsObject::Camera(camera) => {
let name = name.to_uppercase();
let name = sanitized_table_name(&name.to_uppercase(), "CAMERA");
out.messages.push(format!("Creating camera {name}"));
let direction = [0, 1, 2].map(|k| camera.position[k] - camera.target[k]);
let distance = direction.iter().map(|d| d * d).sum::<f64>().sqrt();