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:
ramox81andClaude Opus 5.5 committed 2026-10-16 15:06:16 +03:00
1 parent 3c9404510f
commit 659bb06cd6
38 files changed
+2400 -113

No files matched your search

Generated
+3 -3
View File
@@ -4382,7 +4382,7 @@ dependencies = [
[[package]]
name = "opencadcodec"
version = "0.6.0"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=cb3ce6a#cb3ce6a93c76a5069f5c2391ee44ea38b4485d15"
source = "git+https://github.com/ramox81/cadcodec.git?rev=6c33e24#6c33e24dc3c72034afec8333f0554541f01cc508"
dependencies = [
"anyhow",
"bitflags 2.13.1",
@@ -4415,7 +4415,7 @@ dependencies = [
[[package]]
name = "opencadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=40f59ca#40f59caf1e18df6c4bad3b53389fe59d0e740796"
source = "git+https://github.com/ramox81/cadkernel.git?rev=9490fb8#9490fb81fa81088d7890ce87af01353fd834101f"
dependencies = [
"cavalier_contours",
"opencadcodec",
@@ -4426,7 +4426,7 @@ dependencies = [
[[package]]
name = "opencadkernel-constraints"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=40f59ca#40f59caf1e18df6c4bad3b53389fe59d0e740796"
source = "git+https://github.com/ramox81/cadkernel.git?rev=9490fb8#9490fb81fa81088d7890ce87af01353fd834101f"
dependencies = [
"nalgebra",
]
+7
View File
@@ -222,3 +222,10 @@ harness = false
[[test]]
name = "integration"
path = "tests/integration.rs"
[patch."https://github.com/HakanSeven12/opencadcodec.git"]
opencadcodec = { git = "https://github.com/ramox81/cadcodec.git", rev = "6c33e24" }
[patch."https://github.com/HakanSeven12/opencadkernel.git"]
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "9490fb8" }
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "9490fb8" }
-4
View File
@@ -6663,10 +6663,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6648,10 +6648,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6649,10 +6649,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6650,10 +6650,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6648,10 +6648,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6639,10 +6639,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6635,10 +6635,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6650,10 +6650,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6639,10 +6639,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6650,10 +6650,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6649,10 +6649,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6650,10 +6650,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6651,10 +6651,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6652,10 +6652,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6572,10 +6572,6 @@ txt2mtxt =
dgn-import =
.supported = Destekleniyor
.by-extension = Uzantıya göre
.acis-solver = ACIS çözücü var
.export-only = Yalnız dışa aktarma
.acis-reason = ACIS gövdeleri yalnız çizim içinde okunur; SAT dosyası içe aktarılamaz.
.step-reason = STEP dosyaları yalnız yazılır (STEPOUT), okunmaz.
.no-importer = OpenCAD Studio'da bu biçim için içe aktarıcı yok.
.recent = Son:
.group-2d = 2B / Altlık
-4
View File
@@ -6634,10 +6634,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
-4
View File
@@ -6646,10 +6646,6 @@ txt2mtxt =
dgn-import =
.supported = Supported
.by-extension = By extension
.acis-solver = ACIS solver available
.export-only = Export only
.acis-reason = ACIS bodies are read only inside drawings; SAT files cannot be imported.
.step-reason = STEP files are only written (STEPOUT), not read.
.no-importer = OpenCAD Studio has no importer for this format.
.recent = Recent:
.group-2d = 2D / Underlay
+41 -3
View File
@@ -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 ───────────────────────────────────────────────
+6
View File
@@ -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",
+206
View File
@@ -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()
}
}
+205
View File
@@ -0,0 +1,205 @@
//! Assembles a kernel B-rep from the topology an exchange file spells out:
//! vertices, edges along their curves, faces bounded by loops of those
//! edges, shells of faces. The STEP and IGES readers share it.
//!
//! Spline curves and surfaces that are exactly lines, circles, planes,
//! spheres, cylinders, cones or tori are kept as those shapes: many systems write
//! every one of them as a spline.
use std::f64::consts::TAU;
use kernel::brep::{
Body, Coedge, Curve3, Edge, EdgeKey, Face, Loop, Lump, Provenance, Shell, Surface, Vertex,
VertexKey,
};
use kernel::space::Vec3;
/// A face as the file gives it: its surface, whether its outward normal
/// agrees with the surface's, and its loops — the outer one first — as
/// edges with the sense each is run in.
pub struct FaceSpec {
pub surface: Surface,
pub forward: bool,
pub loops: Vec<Vec<(EdgeKey, bool)>>,
}
pub struct Builder {
pub body: Body,
}
/// Whether a curve repeats, and over how long a parameter run.
fn period(curve: &Curve3) -> Option<f64> {
match curve {
Curve3::Circle(_) | Curve3::Ellipse(_) => Some(TAU),
Curve3::Nurbs(nurbs) if nurbs.periodicity() => {
let (from, to) = nurbs.domain();
Some(to - from)
}
_ => None,
}
}
/// The parameter of a point on (or next to) a curve.
fn parameter(curve: &Curve3, point: [f64; 3]) -> Option<f64> {
Some(match curve {
Curve3::Line(line) => {
let direction = Vec3::from(line.direction);
(Vec3::from(point) - Vec3::from(line.origin)).dot(direction)
/ direction.length_squared().max(f64::MIN_POSITIVE)
}
Curve3::Circle(circle) => {
let [x, y] = circle.plane.project(point)?;
y.atan2(x).rem_euclid(TAU)
}
Curve3::Ellipse(ellipse) => {
let [x, y] = ellipse.plane.project(point)?;
(y / ellipse.minor_radius)
.atan2(x / ellipse.major_radius)
.rem_euclid(TAU)
}
Curve3::Nurbs(nurbs) => nurbs.parameter_at(point),
Curve3::PlanarSpline { .. } => return None,
})
}
/// The same curve run the other way.
fn reversed(curve: Curve3) -> Option<Curve3> {
Some(match curve {
Curve3::Line(mut line) => {
line.direction = (-Vec3::from(line.direction)).to_array();
Curve3::Line(line)
}
Curve3::Nurbs(nurbs) => Curve3::Nurbs(nurbs.reversed()?),
_ => return None,
})
}
impl Builder {
pub fn new() -> Self {
Self { body: Body::new() }
}
pub fn vertex(&mut self, point: [f64; 3]) -> VertexKey {
self.body.vertices.insert(Vertex {
point,
provenance: Provenance::Synthesized,
})
}
fn point(&self, vertex: VertexKey) -> Option<[f64; 3]> {
self.body.vertices.get(vertex).map(|v| v.point)
}
/// An edge along `curve` from `start` to `end`, which the curve runs
/// through in that order. A curve that turns out to run the other way
/// (or an open one handed its ends swapped) is reversed to match.
pub fn edge(&mut self, curve: Curve3, start: VertexKey, end: VertexKey) -> Option<EdgeKey> {
let (a, b) = (self.point(start)?, self.point(end)?);
let mut curve = match curve {
Curve3::Nurbs(nurbs) => kernel::brep::analytic_curve(&nurbs).unwrap_or(Curve3::Nurbs(nurbs)),
other => other,
};
let mut t0 = parameter(&curve, a)?;
let mut t1 = parameter(&curve, b)?;
match period(&curve) {
Some(period) => {
if let Curve3::Nurbs(nurbs) = &curve {
let (from, _) = nurbs.domain();
if start == end {
t0 = from;
}
}
if start == end {
t1 = t0 + period;
} else {
while t1 <= t0 + 1e-12 {
t1 += period;
}
}
}
None => {
if start == end {
// A clamped curve closing on itself: its whole domain.
if let Curve3::Nurbs(nurbs) = &curve {
(t0, t1) = nurbs.domain();
}
} else if t1 < t0 {
curve = reversed(curve)?;
t0 = parameter(&curve, a)?;
t1 = parameter(&curve, b)?;
}
}
}
if !(t1 > t0) {
return None;
}
let curve = self.body.curves.insert(curve);
Some(self.body.edges.insert(Edge {
curve,
start_parameter: t0,
end_parameter: t1,
start,
end,
coedges: Vec::new(),
provenance: Provenance::Synthesized,
}))
}
/// A lump of shells — the outer one first, then voids — each a list of
/// faces. `None` when a face has no surface or an edge has gone.
pub fn lump(&mut self, shells: Vec<Vec<FaceSpec>>) -> Option<()> {
let lump = self.body.lumps.insert(Lump {
shells: Vec::new(),
provenance: Provenance::Synthesized,
});
for faces in shells {
let shell = self.body.shells.insert(Shell {
faces: Vec::new(),
owner: lump,
provenance: Provenance::Synthesized,
});
for spec in faces {
let (surface, forward) = match spec.surface {
Surface::Nurbs(nurbs) => match kernel::brep::analytic_surface(&nurbs) {
Some((analytic, agrees)) => (analytic, spec.forward == agrees),
None => (Surface::Nurbs(nurbs), spec.forward),
},
other => (other, spec.forward),
};
let surface = self.body.surfaces.insert(surface);
let face = self.body.faces.insert(Face {
surface,
forward,
loops: Vec::new(),
owner: shell,
provenance: Provenance::Synthesized,
});
for ring in spec.loops {
let key = self.body.loops.insert(Loop {
coedges: Vec::new(),
owner: face,
provenance: Provenance::Synthesized,
});
let mut coedges = Vec::with_capacity(ring.len());
for (edge, forward) in ring {
let coedge = self.body.coedges.insert(Coedge {
edge,
forward,
pcurve: None,
owner: key,
provenance: Provenance::Synthesized,
});
self.body.edges.get_mut(edge)?.coedges.push(coedge);
coedges.push(coedge);
}
self.body.loops.get_mut(key)?.coedges = coedges;
self.body.faces.get_mut(face)?.loops.push(key);
}
self.body.shells.get_mut(shell)?.faces.push(face);
}
self.body.lumps.get_mut(lump)?.shells.push(shell);
}
self.body.roots.push(lump);
Some(())
}
}
+454
View File
@@ -0,0 +1,454 @@
//! IGES reading: the manifold solids (type 186) of a file, their shells,
//! faces, loops and edges (514/510/508/504/502) over spline surfaces and
//! curves (128/126), lines and arcs (110/100), as kernel bodies.
use std::collections::HashMap;
use kernel::brep::{Circle3, Curve3, EdgeKey, Line3, Surface, VertexKey};
use kernel::space::{NurbsCurve3, NurbsSurface3, Plane};
use super::brep_build::{Builder, FaceSpec};
/// One parameter: a number, or a Hollerith string.
#[derive(Debug, Clone)]
enum P {
Num(f64),
Str(String),
}
impl P {
fn num(&self) -> f64 {
match self {
P::Num(v) => *v,
P::Str(_) => 0.0,
}
}
}
/// Splits a section's text into parameters at `delimiter`, up to the first
/// `terminator`, reading `nH…` strings whole.
fn parameters(text: &str, delimiter: char, terminator: char) -> Vec<P> {
let chars: Vec<char> = text.chars().collect();
let mut out = Vec::new();
let mut i = 0;
loop {
while chars.get(i).is_some_and(|c| *c == ' ') {
i += 1;
}
let start = i;
while chars.get(i).is_some_and(|c| c.is_ascii_digit()) {
i += 1;
}
if i > start && matches!(chars.get(i), Some('H' | 'h')) {
let count: usize = chars[start..i].iter().collect::<String>().parse().unwrap_or(0);
let body: String = chars.iter().skip(i + 1).take(count).collect();
i += 1 + count;
out.push(P::Str(body));
} else {
i = start;
while chars
.get(i)
.is_some_and(|c| *c != delimiter && *c != terminator)
{
i += 1;
}
let raw: String = chars[start..i].iter().collect::<String>().replace(['D', 'd'], "E");
out.push(P::Num(raw.trim().parse().unwrap_or(0.0)));
}
while chars.get(i).is_some_and(|c| *c == ' ') {
i += 1;
}
match chars.get(i) {
Some(c) if *c == delimiter => i += 1,
_ => return out,
}
}
}
/// A directory entry and its parameters.
struct Entity {
kind: i32,
/// Pointer to a transformation matrix entity, or zero.
transform: usize,
/// Colour number, or a negated pointer to a colour definition.
color: i64,
params: Vec<P>,
}
struct Doc {
/// Keyed by the entry's first directory line number.
entities: HashMap<usize, Entity>,
/// File units to drawing units.
scale: f64,
}
/// A 3×4 matrix applied as `R·p + T`.
type Matrix = [[f64; 4]; 3];
impl Doc {
fn get(&self, pointer: f64) -> Option<&Entity> {
self.entities.get(&(pointer.abs() as usize))
}
/// An entry's colour: a numbered one, or a definition's RGB percentages
/// — as the nearest standard index colour when it is one exactly.
fn color(&self, number: i64) -> codec::types::Color {
use codec::types::Color;
let rgb = match number {
1 => [0, 0, 0],
2 => [255, 0, 0],
3 => [0, 255, 0],
4 => [0, 0, 255],
5 => [255, 255, 0],
6 => [255, 0, 255],
7 => [0, 255, 255],
8 => [255, 255, 255],
n if n < 0 => match self.entities.get(&(-n as usize)).filter(|e| e.kind == 314) {
Some(e) => [1, 2, 3].map(|k| {
(e.params.get(k).map_or(0.0, P::num) * 2.55).round().clamp(0.0, 255.0) as u8
}),
None => return Color::ByLayer,
},
_ => return Color::ByLayer,
};
match rgb {
[255, 0, 0] => Color::Index(1),
[255, 255, 0] => Color::Index(2),
[0, 255, 0] => Color::Index(3),
[0, 255, 255] => Color::Index(4),
[0, 0, 255] => Color::Index(5),
[255, 0, 255] => Color::Index(6),
[255, 255, 255] => Color::Index(7),
[r, g, b] => Color::Rgb { r, g, b },
}
}
fn matrix(&self, pointer: usize) -> Option<Matrix> {
let entity = self.entities.get(&pointer).filter(|e| e.kind == 124)?;
let v = |k: usize| entity.params.get(k).map_or(0.0, P::num);
let mut m = [[v(1), v(2), v(3), v(4)], [v(5), v(6), v(7), v(8)], [v(9), v(10), v(11), v(12)]];
// Matrices chain through their own transformation pointer.
if let Some(outer) = (entity.transform != 0).then(|| self.matrix(entity.transform)).flatten() {
let mut product = [[0.0; 4]; 3];
for r in 0..3 {
for c in 0..4 {
product[r][c] = (0..3).map(|k| outer[r][k] * m[k][c]).sum::<f64>()
+ if c == 3 { outer[r][3] } else { 0.0 };
}
}
m = product;
}
Some(m)
}
/// `entity`'s placement applied to a point it defines.
fn place(&self, entity: &Entity) -> impl Fn([f64; 3]) -> [f64; 3] {
let m = (entity.transform != 0).then(|| self.matrix(entity.transform)).flatten();
let scale = self.scale;
move |p| {
let p = match m {
Some(m) => [0, 1, 2].map(|r| m[r][0] * p[0] + m[r][1] * p[1] + m[r][2] * p[2] + m[r][3]),
None => p,
};
p.map(|c| c * scale)
}
}
fn curve(&self, pointer: f64) -> Option<Curve3> {
let entity = self.get(pointer)?;
let p = &entity.params;
let v = |k: usize| p.get(k).map_or(0.0, P::num);
let place = self.place(entity);
Some(match entity.kind {
110 => {
let a = place([v(1), v(2), v(3)]);
let b = place([v(4), v(5), v(6)]);
Curve3::Line(Line3 {
origin: a,
direction: [b[0] - a[0], b[1] - a[1], b[2] - a[2]],
})
}
100 => {
let centre = place([v(2), v(3), v(1)]);
let x = place([v(2) + 1.0, v(3), v(1)]);
let y = place([v(2), v(3) + 1.0, v(1)]);
let start = place([v(4), v(5), v(1)]);
let radius = (0..3).map(|k| (start[k] - centre[k]).powi(2)).sum::<f64>().sqrt();
Curve3::Circle(Circle3 {
plane: Plane::orthonormal(
centre,
[x[0] - centre[0], x[1] - centre[1], x[2] - centre[2]],
{
let (a, b) = (
[x[0] - centre[0], x[1] - centre[1], x[2] - centre[2]],
[y[0] - centre[0], y[1] - centre[1], y[2] - centre[2]],
);
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
},
)?,
radius,
})
}
126 => {
let k = v(1) as usize;
let degree = v(2) as usize;
let knots = (0..k + degree + 2).map(|n| v(7 + n)).collect();
let w0 = 7 + k + degree + 2;
let weights = (0..=k).map(|n| v(w0 + n)).collect();
let c0 = w0 + k + 1;
let points = (0..=k)
.map(|n| place([v(c0 + 3 * n), v(c0 + 3 * n + 1), v(c0 + 3 * n + 2)]))
.collect();
Curve3::Nurbs(NurbsCurve3::new(degree, points, knots, Some(weights))?)
}
_ => return None,
})
}
fn surface(&self, pointer: f64) -> Option<Surface> {
let entity = self.get(pointer)?;
if entity.kind != 128 {
return None;
}
let v = |k: usize| entity.params.get(k).map_or(0.0, P::num);
let place = self.place(entity);
let (k1, k2, m1, m2) = (v(1) as usize, v(2) as usize, v(3) as usize, v(4) as usize);
let s0 = 10;
let t0 = s0 + k1 + m1 + 2;
let w0 = t0 + k2 + m2 + 2;
let c0 = w0 + (k1 + 1) * (k2 + 1);
let u_knots = (s0..t0).map(v).collect();
let v_knots = (t0..w0).map(v).collect();
// Points run with the first index fastest.
let at = |i: usize, j: usize| j * (k1 + 1) + i;
let weights = (0..=k1)
.map(|i| (0..=k2).map(|j| v(w0 + at(i, j))).collect())
.collect();
let points = (0..=k1)
.map(|i| {
(0..=k2)
.map(|j| {
let n = c0 + 3 * at(i, j);
place([v(n), v(n + 1), v(n + 2)])
})
.collect()
})
.collect();
NurbsSurface3::new(m1, m2, points, u_knots, v_knots, Some(weights)).map(Surface::Nurbs)
}
}
struct Assembly<'a> {
doc: &'a Doc,
build: Builder,
vertices: HashMap<(usize, usize), VertexKey>,
edges: HashMap<(usize, usize), EdgeKey>,
}
impl Assembly<'_> {
fn vertex(&mut self, list: f64, index: f64) -> Option<VertexKey> {
let key = (list.abs() as usize, index as usize);
if let Some(&v) = self.vertices.get(&key) {
return Some(v);
}
let entity = self.doc.get(list).filter(|e| e.kind == 502)?;
let n = 2 + 3 * (key.1.checked_sub(1)?);
let p = |k: usize| entity.params.get(k).map(P::num);
let point = self.doc.place(entity)([p(n)?, p(n + 1)?, p(n + 2)?]);
let v = self.build.vertex(point);
self.vertices.insert(key, v);
Some(v)
}
fn edge(&mut self, list: f64, index: f64) -> Option<EdgeKey> {
let key = (list.abs() as usize, index as usize);
if let Some(&e) = self.edges.get(&key) {
return Some(e);
}
let entity = self.doc.get(list).filter(|e| e.kind == 504)?;
let n = 2 + 5 * key.1.checked_sub(1)?;
let p = |k: usize| entity.params.get(k).map(P::num);
let curve = self.doc.curve(p(n)?)?;
let start = self.vertex(p(n + 1)?, p(n + 2)?)?;
let end = self.vertex(p(n + 3)?, p(n + 4)?)?;
let e = self.build.edge(curve, start, end)?;
self.edges.insert(key, e);
Some(e)
}
fn ring(&mut self, pointer: f64) -> Option<Vec<(EdgeKey, bool)>> {
let entity = self.doc.get(pointer).filter(|e| e.kind == 508)?;
let p = |k: usize| entity.params.get(k).map(P::num);
let mut ring = Vec::new();
let mut at = 2;
for _ in 0..p(1)? as usize {
let (kind, list, index, agrees, pcurves) = (p(at)?, p(at + 1)?, p(at + 2)?, p(at + 3)?, p(at + 4)?);
if kind == 0.0 {
ring.push((self.edge(list, index)?, agrees != 0.0));
}
at += 5 + 2 * pcurves as usize;
}
Some(ring)
}
fn face(&mut self, pointer: f64, agrees: bool) -> Option<FaceSpec> {
let entity = self.doc.get(pointer).filter(|e| e.kind == 510)?;
let p = |k: usize| entity.params.get(k).map(P::num);
let surface = self.doc.surface(p(1)?)?;
let loops = (0..p(2)? as usize)
.map(|n| self.ring(p(4 + n)?))
.collect::<Option<Vec<_>>>()?;
Some(FaceSpec {
surface,
forward: agrees,
loops,
})
}
fn shell(&mut self, pointer: f64, agrees: bool) -> Option<Vec<FaceSpec>> {
let entity = self.doc.get(pointer).filter(|e| e.kind == 514)?;
let p = |k: usize| entity.params.get(k).map(P::num);
(0..p(1)? as usize)
.map(|n| {
let face_agrees = p(3 + 2 * n)? != 0.0;
let mut face = self.face(p(2 + 2 * n)?, face_agrees == agrees)?;
if !agrees {
for ring in &mut face.loops {
ring.reverse();
for (_, f) in ring.iter_mut() {
*f = !*f;
}
}
}
Some(face)
})
.collect()
}
}
/// What an IGES file holds.
pub struct IgesModel {
/// Each solid, scaled to the drawing, with its colour.
pub bodies: Vec<(kernel::brep::Body, codec::types::Color)>,
}
/// `drawing_mm`: millimetres per drawing unit, `None` for a unitless drawing.
pub fn read(bytes: &[u8], drawing_mm: Option<f64>) -> Option<IgesModel> {
let text = String::from_utf8_lossy(bytes);
let mut sections: HashMap<char, Vec<&str>> = HashMap::new();
for line in text.lines() {
let line = line.trim_end_matches('\r');
if line.len() < 73 {
continue;
}
let Some(section) = line.get(72..73).and_then(|s| s.chars().next()) else { continue };
sections.entry(section).or_default().push(line);
}
let directory = sections.get(&'D')?;
let global: String = sections
.get(&'G')
.map(|lines| lines.iter().filter_map(|l| l.get(..72)).collect())
.unwrap_or_default();
// The delimiters are themselves the first two parameters, when given.
let mut delimiter = ',';
let mut terminator = ';';
if global.starts_with("1H") {
delimiter = global.chars().nth(2).unwrap_or(',');
let rest = global.get(4..).unwrap_or_default();
if rest.starts_with("1H") {
terminator = rest.chars().nth(2).unwrap_or(';');
}
}
let globals = parameters(&global, delimiter, terminator);
let unit_mm = match globals.get(13).map(P::num).unwrap_or(1.0) as i32 {
1 => 25.4,
4 => 304.8,
5 => 1_609_344.0,
6 => 1000.0,
7 => 1_000_000.0,
8 => 0.0254,
9 => 0.001,
10 => 10.0,
11 => 0.000_025_4,
3 => match globals.get(14) {
Some(P::Str(name)) if name.eq_ignore_ascii_case("IN") || name.eq_ignore_ascii_case("INCH") => 25.4,
Some(P::Str(name)) if name.eq_ignore_ascii_case("FT") => 304.8,
Some(P::Str(name)) if name.eq_ignore_ascii_case("M") => 1000.0,
Some(P::Str(name)) if name.eq_ignore_ascii_case("CM") => 10.0,
_ => 1.0,
},
_ => 1.0,
} * globals.get(12).map(P::num).filter(|s| *s > 0.0).map_or(1.0, |s| 1.0 / s);
let parameter_lines: HashMap<usize, &str> = sections
.get(&'P')
.map(|lines| {
lines
.iter()
.filter_map(|l| Some((l.get(73..)?.trim().parse().ok()?, *l)))
.collect()
})
.unwrap_or_default();
let field = |line: &str, n: usize| -> i64 {
line.get(n * 8..n * 8 + 8)
.and_then(|f| f.trim().parse().ok())
.unwrap_or(0)
};
let mut entities = HashMap::new();
for pair in directory.chunks(2) {
let [first, second] = pair else { continue };
let Some(number) = first.get(73..).and_then(|n| n.trim().parse::<usize>().ok()) else {
continue;
};
let start = field(first, 1) as usize;
let count = field(second, 3).max(1) as usize;
let data: String = (start..start + count)
.filter_map(|n| parameter_lines.get(&n))
.filter_map(|l| l.get(..64))
.collect();
entities.insert(
number,
Entity {
kind: field(first, 0) as i32,
transform: field(first, 6).max(0) as usize,
color: field(second, 2),
params: parameters(&data, delimiter, terminator),
},
);
}
let scale = drawing_mm.filter(|mm| *mm > 0.0).map_or(1.0, |mm| unit_mm / mm);
let doc = Doc { entities, scale };
let mut solids: Vec<usize> = doc
.entities
.iter()
.filter(|(_, e)| e.kind == 186)
.map(|(n, _)| *n)
.collect();
solids.sort_unstable();
let mut bodies = Vec::new();
for number in solids {
let entity = &doc.entities[&number];
let v = |k: usize| entity.params.get(k).map(P::num);
let Some((shell, agrees)) = v(1).zip(v(2)) else { continue };
let mut assembly = Assembly {
doc: &doc,
build: Builder::new(),
vertices: HashMap::new(),
edges: HashMap::new(),
};
let mut shells = Vec::new();
let outer = assembly.shell(shell, agrees != 0.0);
let voids = (0..v(3).unwrap_or(0.0) as usize)
.map(|n| assembly.shell(v(4 + 2 * n)?, v(5 + 2 * n)? != 0.0))
.collect::<Option<Vec<_>>>();
if let (Some(outer), Some(voids)) = (outer, voids) {
shells.push(outer);
shells.extend(voids);
if assembly.build.lump(shells).is_some() {
bodies.push((assembly.build.body, doc.color(entity.color)));
}
}
}
Some(IgesModel { bodies })
}
+5
View File
@@ -8,12 +8,15 @@ pub mod edit_lock;
// Same split as web_session/web_autosave: the save-routing decision (and
// its tests) is pure and compiles everywhere; the showSaveFilePicker half
// inside is wasm-only.
pub mod brep_build;
pub mod fbx;
pub mod file_association;
pub mod font_repo;
pub(crate) mod fs_access;
pub mod iges_import;
pub mod line_read;
pub mod linetypes;
pub mod model_import;
pub mod obj;
pub mod ole_embed;
pub mod paper_catalog;
@@ -27,11 +30,13 @@ pub mod recovery;
#[cfg(not(target_arch = "wasm32"))]
pub mod single_instance;
pub mod step;
pub mod step_import;
pub mod stl;
pub mod svg_export;
pub mod thumbnail;
pub mod update_check;
pub mod visual_styles;
pub mod wmf_import;
#[cfg(target_arch = "wasm32")]
pub(crate) mod web_recent;
// Registered on every target so the pure helpers (and their tests) compile on
+154
View File
@@ -0,0 +1,154 @@
//! Model files IMPORT reads: ACIS text (SAT), STEP and IGES, as the drawing
//! objects they become.
use codec::entities::{Entity as _, Insert};
use codec::types::{Color, Matrix4, Transform, Vector3};
use codec::EntityType;
use crate::scene::convert::acis_import::{body_entity, sat_entities};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelFormat {
Sat,
Step,
Iges,
}
/// The objects a file brings in.
pub struct Imported {
/// Block definitions, each before any block that inserts it.
pub blocks: Vec<(String, Vec<EntityType>)>,
/// The objects for the current space.
pub entities: Vec<EntityType>,
}
/// File units over drawing units; one where the drawing is unitless.
fn ratio(file_mm: f64, drawing_mm: Option<f64>) -> f64 {
drawing_mm.filter(|mm| *mm > 0.0).map_or(1.0, |mm| file_mm / mm)
}
/// 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 }
}
/// An insert of `block` placed by `placement` (columns: axes, origin).
fn placed_insert(block: &str, placement: &[[f64; 4]; 3]) -> EntityType {
let mut insert = Insert::new(block, Vector3::ZERO);
let mut m = Matrix4::identity();
for r in 0..3 {
m.m[r] = placement[r];
}
insert.apply_transform(&Transform::from_matrix(m));
EntityType::Insert(insert)
}
/// STEP: a block per product — its solids, and inserts of its components —
/// with each top product inserted at the origin, scaled from the file's
/// units to the drawing's. The solids take their colour from the block.
fn step_blocks(model: crate::io::step_import::StepModel, scale: f64) -> Imported {
// Unique names, in product order.
let mut names: Vec<String> = Vec::new();
for product in &model.products {
let base = block_name(&product.name);
let mut name = base.clone();
let mut n = 1;
while names.iter().any(|taken| taken.eq_ignore_ascii_case(&name)) {
name = format!("{base}_{n}");
n += 1;
}
names.push(name);
}
// Children before parents.
let mut order = Vec::new();
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);
}
let blocks = order
.iter()
.map(|&k| {
let product = &model.products[k];
let mut entities: Vec<EntityType> = product
.bodies
.iter()
.filter_map(body_entity)
.map(|mut entity| {
entity.common_mut().color = Color::ByBlock;
entity
})
.collect();
entities.extend(
product
.children
.iter()
.map(|(child, placement)| {
let mut insert = placed_insert(&names[*child], placement);
insert.common_mut().color = Color::ByBlock;
insert
}),
);
(names[k].clone(), entities)
})
.collect();
let identity = [[scale, 0.0, 0.0, 0.0], [0.0, scale, 0.0, 0.0], [0.0, 0.0, scale, 0.0]];
Imported {
blocks,
entities: model
.roots
.iter()
.map(|&k| placed_insert(&names[k], &identity))
.collect(),
}
}
/// `None` when the file is not of `format` or holds nothing it can read.
pub fn read(format: ModelFormat, bytes: &[u8], drawing_mm: Option<f64>) -> Option<Imported> {
let imported = match format {
ModelFormat::Sat => Imported {
blocks: Vec::new(),
entities: sat_entities(&String::from_utf8_lossy(bytes))?,
},
ModelFormat::Step => {
let model = crate::io::step_import::read(bytes)?;
let scale = ratio(model.unit_mm, drawing_mm);
step_blocks(model, scale)
}
ModelFormat::Iges => Imported {
blocks: Vec::new(),
entities: crate::io::iges_import::read(bytes, drawing_mm)?
.bodies
.iter()
.filter_map(|(body, color)| {
let mut entity = body_entity(body)?;
entity.common_mut().color = *color;
Some(entity)
})
.collect(),
},
};
let solids = imported
.blocks
.iter()
.flat_map(|(_, entities)| entities)
.chain(&imported.entities)
.any(|entity| !matches!(entity, EntityType::Insert(_)));
solids.then_some(imported)
}
+829
View File
@@ -0,0 +1,829 @@
//! STEP (ISO 10303-21) reading: the B-rep solids and shell models of an
//! AP203/AP214/AP242 file, as kernel bodies, with the product's name and the
//! file's length unit.
use std::collections::HashMap;
use kernel::brep::{
Circle3, Cone, Curve3, Cylinder, EdgeKey, Ellipse3, Line3, Sphere, Surface, Torus, VertexKey,
};
use kernel::space::{NurbsCurve3, NurbsSurface3, Plane, Vec3};
use super::brep_build::{Builder, FaceSpec};
/// One parameter of an entity instance.
#[derive(Debug, Clone)]
enum P {
Num(f64),
Str(String),
Enum(String),
Ref(u64),
List(Vec<P>),
Null,
/// A typed value, such as `LENGTH_MEASURE(1.)`.
Typed(Vec<P>),
}
impl P {
fn num(&self) -> Option<f64> {
match self {
P::Num(v) => Some(*v),
P::Typed(inner) => inner.first()?.num(),
_ => None,
}
}
fn reference(&self) -> Option<u64> {
match self {
P::Ref(id) => Some(*id),
_ => None,
}
}
fn list(&self) -> &[P] {
match self {
P::List(items) => items,
_ => &[],
}
}
fn truth(&self) -> bool {
matches!(self, P::Enum(e) if e == "T")
}
fn text(&self) -> Option<&str> {
match self {
P::Str(s) => Some(s),
_ => None,
}
}
}
/// An instance: one `(type, parameters)` part, or several for a complex one.
type Instance = Vec<(String, Vec<P>)>;
struct Cursor<'a> {
s: &'a [u8],
i: usize,
}
impl Cursor<'_> {
fn skip(&mut self) {
loop {
while self.s.get(self.i).is_some_and(|c| c.is_ascii_whitespace()) {
self.i += 1;
}
if self.s[self.i..].starts_with(b"/*") {
match self.s[self.i + 2..].windows(2).position(|w| w == b"*/") {
Some(end) => self.i += end + 4,
None => self.i = self.s.len(),
}
} else {
break;
}
}
}
fn peek(&mut self) -> Option<u8> {
self.skip();
self.s.get(self.i).copied()
}
fn eat(&mut self, c: u8) -> Option<()> {
(self.peek()? == c).then(|| self.i += 1)
}
fn word(&mut self) -> String {
self.skip();
let start = self.i;
while self
.s
.get(self.i)
.is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_' || *c == b'-')
{
self.i += 1;
}
String::from_utf8_lossy(&self.s[start..self.i]).to_ascii_uppercase()
}
fn param(&mut self) -> Option<P> {
Some(match self.peek()? {
b'#' => {
self.i += 1;
P::Ref(self.word().parse().ok()?)
}
b'\'' => {
self.i += 1;
let mut text = Vec::new();
loop {
let c = *self.s.get(self.i)?;
self.i += 1;
if c == b'\'' {
if self.s.get(self.i) == Some(&b'\'') {
self.i += 1;
} else {
break;
}
}
text.push(c);
}
P::Str(String::from_utf8_lossy(&text).into_owned())
}
b'.' => {
self.i += 1;
let value = self.word();
self.eat(b'.')?;
P::Enum(value)
}
b'$' | b'*' => {
self.i += 1;
P::Null
}
b'(' => P::List(self.params()?),
c if c.is_ascii_digit() || c == b'-' || c == b'+' || c == b'.' => {
let start = self.i;
while self
.s
.get(self.i)
.is_some_and(|c| c.is_ascii_digit() || b"+-.eE".contains(c))
{
self.i += 1;
}
let raw = String::from_utf8_lossy(&self.s[start..self.i]).replace(".E", ".0E");
P::Num(raw.replace(".e", ".0e").parse().ok()?)
}
_ => {
if self.word().is_empty() {
return None;
}
P::Typed(self.params()?)
}
})
}
fn params(&mut self) -> Option<Vec<P>> {
self.eat(b'(')?;
let mut items = Vec::new();
if self.eat(b')').is_some() {
return Some(items);
}
loop {
items.push(self.param()?);
match self.peek()? {
b',' => self.i += 1,
b')' => {
self.i += 1;
return Some(items);
}
_ => return None,
}
}
}
/// `#id = TYPE(...)` or `#id = (A(...) B(...))`, up to its `;`.
fn instance(&mut self) -> Option<(u64, Instance)> {
self.eat(b'#')?;
let id = self.word().parse().ok()?;
self.eat(b'=')?;
let mut parts = Vec::new();
if self.eat(b'(').is_some() {
while self.eat(b')').is_none() {
let name = self.word();
if name.is_empty() {
return None;
}
parts.push((name, self.params()?));
}
} else {
let name = self.word();
parts.push((name, self.params()?));
}
self.eat(b';')?;
Some((id, parts))
}
}
struct Doc {
instances: HashMap<u64, Instance>,
/// Plane-angle unit in radians.
angle: f64,
}
impl Doc {
fn parts(&self, id: u64) -> Option<&Instance> {
self.instances.get(&id)
}
fn kind(&self, id: u64) -> &str {
self.parts(id)
.and_then(|parts| parts.first())
.map_or("", |(name, _)| name.as_str())
}
/// The parameters of the part named `name`.
fn part(&self, id: u64, name: &str) -> Option<&[P]> {
self.parts(id)?
.iter()
.find(|(n, _)| n == name)
.map(|(_, p)| p.as_slice())
}
/// A simple instance's type and parameters.
fn simple(&self, id: u64) -> Option<(&str, &[P])> {
let parts = self.parts(id)?;
let (name, params) = parts.first()?;
Some((name.as_str(), params.as_slice()))
}
fn point(&self, p: &P) -> Option<[f64; 3]> {
let (_, params) = self.simple(p.reference()?)?;
let c = params.get(1)?.list();
Some([c.first()?.num()?, c.get(1)?.num()?, c.get(2).and_then(P::num).unwrap_or(0.0)])
}
fn direction(&self, p: &P) -> Option<[f64; 3]> {
self.point(p)
}
fn placement(&self, p: &P) -> Option<Plane> {
let (_, params) = self.simple(p.reference()?)?;
let origin = self.point(params.get(1)?)?;
let z = params.get(2).and_then(|d| self.direction(d)).unwrap_or([0.0, 0.0, 1.0]);
let x = params
.get(3)
.and_then(|d| self.direction(d))
.filter(|x| Vec3::from(*x).cross(Vec3::from(z)).length() > 1e-9)
// Any axis off the normal; the plane squares it up.
.unwrap_or(if z[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] });
Plane::orthonormal(origin, x, z)
}
/// Knots expanded by their multiplicities.
fn knots(mults: &P, knots: &P) -> Option<Vec<f64>> {
let mut out = Vec::new();
for (m, k) in mults.list().iter().zip(knots.list()) {
for _ in 0..m.num()? as usize {
out.push(k.num()?);
}
}
Some(out)
}
fn curve(&self, p: &P) -> Option<Curve3> {
let id = p.reference()?;
let kind = self.kind(id);
if kind == "B_SPLINE_CURVE_WITH_KNOTS" || self.part(id, "B_SPLINE_CURVE").is_some() {
return self.spline_curve(id).map(Curve3::Nurbs);
}
let (_, params) = self.simple(id)?;
Some(match kind {
"LINE" => {
let origin = self.point(params.get(1)?)?;
let (_, vector) = self.simple(params.get(2)?.reference()?)?;
let direction = Vec3::from(self.direction(vector.get(1)?)?).normalize()?
* vector.get(2)?.num()?;
Curve3::Line(Line3 {
origin,
direction: direction.to_array(),
})
}
"CIRCLE" => Curve3::Circle(Circle3 {
plane: self.placement(params.get(1)?)?,
radius: params.get(2)?.num()?,
}),
"ELLIPSE" => Curve3::Ellipse(Ellipse3 {
plane: self.placement(params.get(1)?)?,
major_radius: params.get(2)?.num()?,
minor_radius: params.get(3)?.num()?,
}),
"SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "TRIMMED_CURVE" => {
self.curve(params.get(1)?)?
}
"POLYLINE" => {
let points: Option<Vec<_>> =
params.get(1)?.list().iter().map(|q| self.point(q)).collect();
Curve3::Nurbs(NurbsCurve3::new(1, points?, Vec::new(), None)?)
}
_ => return None,
})
}
fn spline_curve(&self, id: u64) -> Option<NurbsCurve3> {
// The simple form carries everything after its name; the complex
// form splits it over parts that have no name.
let (degree, points, mults, knots, weights) =
if let Some(p) = self.part(id, "B_SPLINE_CURVE_WITH_KNOTS").filter(|p| p.len() >= 9) {
(&p[1], &p[2], &p[6], &p[7], None)
} else {
let base = self.part(id, "B_SPLINE_CURVE")?;
let with = self.part(id, "B_SPLINE_CURVE_WITH_KNOTS")?;
let weights = self.part(id, "RATIONAL_B_SPLINE_CURVE").and_then(|w| w.first());
(&base[0], &base[1], &with[0], &with[1], weights)
};
let points: Option<Vec<_>> = points.list().iter().map(|q| self.point(q)).collect();
let weights: Option<Vec<f64>> = weights.map(|w| w.list().iter().filter_map(P::num).collect());
let degree = degree.num()? as usize;
let points = points?;
let knots = Self::knots(mults, knots)?;
match &weights {
Some(w) => NurbsCurve3::new_strict(degree, points.clone(), knots.clone(), w.clone())
.or_else(|| NurbsCurve3::new(degree, points, knots, weights.clone())),
None => NurbsCurve3::new(degree, points, knots, None),
}
}
fn surface(&self, p: &P) -> Option<Surface> {
let id = p.reference()?;
let kind = self.kind(id);
if kind == "B_SPLINE_SURFACE_WITH_KNOTS" || self.part(id, "B_SPLINE_SURFACE").is_some() {
return self.spline_surface(id).map(Surface::Nurbs);
}
let (_, params) = self.simple(id)?;
let frame = self.placement(params.get(1)?)?;
let value = |k: usize| params.get(k).and_then(P::num);
Some(match kind {
"PLANE" => Surface::Plane(frame),
"CYLINDRICAL_SURFACE" => Surface::Cylinder(Cylinder {
base: frame,
radius: value(2)?,
}),
// A STEP cone widens along its axis by its semi-angle.
"CONICAL_SURFACE" => Surface::Cone(Cone {
base: frame,
radius: value(2)?,
half_angle: -value(3)? * self.angle,
}),
"SPHERICAL_SURFACE" => Surface::Sphere(Sphere {
frame,
radius: value(2)?,
}),
"TOROIDAL_SURFACE" => Surface::Torus(Torus {
frame,
major_radius: value(2)?,
minor_radius: value(3)?,
}),
_ => return None,
})
}
fn spline_surface(&self, id: u64) -> Option<NurbsSurface3> {
let (ud, vd, points, um, vm, uk, vk, weights) = if let Some(p) = self
.part(id, "B_SPLINE_SURFACE_WITH_KNOTS")
.filter(|p| p.len() >= 13)
{
(&p[1], &p[2], &p[3], &p[8], &p[9], &p[10], &p[11], None)
} else {
let base = self.part(id, "B_SPLINE_SURFACE")?;
let with = self.part(id, "B_SPLINE_SURFACE_WITH_KNOTS")?;
let weights = self.part(id, "RATIONAL_B_SPLINE_SURFACE").and_then(|w| w.first());
(&base[0], &base[1], &base[2], &with[0], &with[1], &with[2], &with[3], weights)
};
let rows: Option<Vec<Vec<[f64; 3]>>> = points
.list()
.iter()
.map(|row| row.list().iter().map(|q| self.point(q)).collect())
.collect();
let weights: Option<Vec<Vec<f64>>> = weights.map(|w| {
w.list()
.iter()
.map(|row| row.list().iter().filter_map(P::num).collect())
.collect()
});
NurbsSurface3::new(
ud.num()? as usize,
vd.num()? as usize,
rows?,
Self::knots(um, uk)?,
Self::knots(vm, vk)?,
weights,
)
}
}
/// Topology of one body, keyed by the file's instance ids.
struct Assembly<'a> {
doc: &'a Doc,
build: Builder,
vertices: HashMap<u64, VertexKey>,
/// Each edge, and whether it runs from the file's edge end to its start.
edges: HashMap<u64, (EdgeKey, bool)>,
}
impl Assembly<'_> {
fn vertex(&mut self, p: &P) -> Option<VertexKey> {
let id = p.reference()?;
if let Some(&v) = self.vertices.get(&id) {
return Some(v);
}
let (_, params) = self.doc.simple(id)?;
let v = self.build.vertex(self.doc.point(params.get(1)?)?);
self.vertices.insert(id, v);
Some(v)
}
fn edge(&mut self, p: &P) -> Option<(EdgeKey, bool)> {
let id = p.reference()?;
if let Some(&e) = self.edges.get(&id) {
return Some(e);
}
let (_, params) = self.doc.simple(id)?;
let (a, b) = (self.vertex(params.get(1)?)?, self.vertex(params.get(2)?)?);
let curve = self.doc.curve(params.get(3)?)?;
let same_sense = params.get(4).is_none_or(P::truth);
let key = if same_sense {
self.build.edge(curve, a, b)?
} else {
self.build.edge(curve, b, a)?
};
self.edges.insert(id, (key, !same_sense));
Some((key, !same_sense))
}
/// A bound's coedges, in the order the face runs them.
fn bound(&mut self, p: &P) -> Option<Vec<(EdgeKey, bool)>> {
let (_, params) = self.doc.simple(p.reference()?)?;
let ring_id = params.get(1)?.reference()?;
let agrees = params.get(2).is_none_or(P::truth);
let mut ring = Vec::new();
if self.doc.kind(ring_id) == "EDGE_LOOP" {
let (_, loop_params) = self.doc.simple(ring_id)?;
for oriented in loop_params.get(1)?.list() {
let (_, o) = self.doc.simple(oriented.reference()?)?;
let (edge, flipped) = self.edge(o.get(3)?)?;
let along = o.get(4).is_none_or(P::truth);
ring.push((edge, along != flipped));
}
}
if !agrees {
ring.reverse();
for (_, forward) in &mut ring {
*forward = !*forward;
}
}
Some(ring)
}
fn face(&mut self, p: &P, flip: bool) -> Option<FaceSpec> {
let id = p.reference()?;
let (_, params) = self.doc.simple(id)?;
let surface = self.doc.surface(params.get(2)?)?;
let mut outer = Vec::new();
let mut inner = Vec::new();
for bound in params.get(1)?.list() {
let ring = self.bound(bound)?;
if self.doc.kind(bound.reference()?) == "FACE_OUTER_BOUND" {
outer.push(ring);
} else {
inner.push(ring);
}
}
outer.extend(inner);
let mut loops = outer;
let mut forward = params.get(3).is_none_or(P::truth);
if flip {
forward = !forward;
for ring in &mut loops {
ring.reverse();
for (_, f) in ring.iter_mut() {
*f = !*f;
}
}
}
Some(FaceSpec {
surface,
forward,
loops,
})
}
/// A shell's faces; an oriented shell set against its faces flips them.
fn shell(&mut self, p: &P) -> Option<Vec<FaceSpec>> {
let mut id = p.reference()?;
let mut flip = false;
if self.doc.kind(id) == "ORIENTED_CLOSED_SHELL" || self.doc.kind(id) == "ORIENTED_OPEN_SHELL" {
let (_, params) = self.doc.simple(id)?;
flip = !params.get(3).is_none_or(P::truth);
id = params.get(2)?.reference()?;
}
let (_, params) = self.doc.simple(id)?;
params
.get(1)?
.list()
.iter()
.map(|face| self.face(face, flip))
.collect()
}
}
/// A 3×4 placement: the columns are the x, y and z axes and the origin.
pub type Placement = [[f64; 4]; 3];
const IDENTITY: Placement = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]];
/// One product: its own bodies and where each of its components sits.
pub struct StepProduct {
pub name: String,
pub bodies: Vec<kernel::brep::Body>,
/// Components: the product (an index into [`StepModel::products`]) and
/// its placement in this one.
pub children: Vec<(usize, Placement)>,
}
/// What a STEP file holds.
pub struct StepModel {
/// Millimetres per file length unit.
pub unit_mm: f64,
pub products: Vec<StepProduct>,
/// The products no other one is a component of.
pub roots: Vec<usize>,
}
/// A named unit's size: millimetres for a length, radians for an angle.
fn unit_size(doc: &Doc, id: u64) -> Option<f64> {
if let Some(si) = doc.part(id, "SI_UNIT") {
let prefix = match si.first() {
Some(P::Enum(prefix)) => match prefix.as_str() {
"MILLI" => 1e-3,
"CENTI" => 1e-2,
"DECI" => 1e-1,
"KILO" => 1e3,
"MICRO" => 1e-6,
_ => 1.0,
},
_ => 1.0,
};
return Some(match si.get(1) {
Some(P::Enum(name)) if name == "METRE" => prefix * 1000.0,
_ => prefix,
});
}
let conversion = doc.part(id, "CONVERSION_BASED_UNIT")?;
let (_, measure) = doc.simple(conversion.get(1)?.reference()?)?;
Some(measure.first()?.num()? * unit_size(doc, measure.get(1)?.reference()?)?)
}
/// The bodies of one B-rep item: a solid, a solid with voids, or each shell
/// of a shell model.
fn item_bodies(doc: &Doc, id: u64) -> Vec<kernel::brep::Body> {
let Some((kind, params)) = doc.simple(id) else {
return Vec::new();
};
let lumps: Vec<Vec<&P>> = match kind {
"MANIFOLD_SOLID_BREP" => vec![params.get(1).into_iter().collect()],
"BREP_WITH_VOIDS" => vec![params
.get(1)
.into_iter()
.chain(params.get(2).map(P::list).unwrap_or_default())
.collect()],
"SHELL_BASED_SURFACE_MODEL" => params
.get(1)
.map(P::list)
.unwrap_or_default()
.iter()
.map(|shell| vec![shell])
.collect(),
_ => return Vec::new(),
};
let mut bodies = Vec::new();
for lump in lumps {
let mut assembly = Assembly {
doc,
build: Builder::new(),
vertices: HashMap::new(),
edges: HashMap::new(),
};
let faces: Option<Vec<_>> = lump.iter().map(|shell| assembly.shell(shell)).collect();
if let Some(faces) = faces {
if assembly.build.lump(faces).is_some() {
bodies.push(assembly.build.body);
}
}
}
bodies
}
impl Doc {
/// An axis placement as a placement matrix.
fn frame(&self, id: u64) -> Option<Placement> {
let plane = self.placement(&P::Ref(id))?;
let z = plane.normal()?;
let [x, y, o] = [plane.x_axis, plane.y_axis, plane.origin];
Some([0, 1, 2].map(|r| [x[r], y[r], z[r], o[r]]))
}
}
/// `a·b`.
fn compose(a: &Placement, b: &Placement) -> Placement {
let mut out = [[0.0; 4]; 3];
for r in 0..3 {
for c in 0..4 {
out[r][c] = (0..3).map(|k| a[r][k] * b[k][c]).sum::<f64>() + if c == 3 { a[r][3] } else { 0.0 };
}
}
out
}
/// The inverse of a rigid placement.
fn invert(a: &Placement) -> Placement {
let mut out = [[0.0; 4]; 3];
for r in 0..3 {
for c in 0..3 {
out[r][c] = a[c][r];
}
out[r][3] = -(0..3).map(|k| a[k][r] * a[k][3]).sum::<f64>();
}
out
}
pub fn read(bytes: &[u8]) -> Option<StepModel> {
let text = String::from_utf8_lossy(bytes);
if !text.trim_start().starts_with("ISO-10303-21") {
return None;
}
let data = text.find("DATA;")? + 5;
let mut cursor = Cursor {
s: &text.as_bytes()[data..],
i: 0,
};
let mut instances = HashMap::new();
while cursor.peek() == Some(b'#') {
match cursor.instance() {
Some((id, parts)) => {
instances.insert(id, parts);
}
None => {
// Skip a malformed instance up to the next one.
while cursor.s.get(cursor.i).is_some_and(|c| *c != b';') {
cursor.i += 1;
}
cursor.i += 1;
}
}
}
let mut doc = Doc {
instances,
angle: 1.0,
};
let mut unit_mm = 1.0;
let mut angle = 1.0;
let mut ids: Vec<u64> = doc.instances.keys().copied().collect();
ids.sort_unstable();
for &id in &ids {
let Some(units) = doc.part(id, "GLOBAL_UNIT_ASSIGNED_CONTEXT") else {
continue;
};
for unit in units.first().map(P::list).unwrap_or_default() {
let Some(unit) = unit.reference() else { continue };
if doc.part(unit, "LENGTH_UNIT").is_some() {
unit_mm = unit_size(&doc, unit).unwrap_or(1.0);
} else if doc.part(unit, "PLANE_ANGLE_UNIT").is_some() {
angle = unit_size(&doc, unit).unwrap_or(1.0);
}
}
break;
}
doc.angle = angle;
let doc = doc;
// What each instance id is, by kind.
let of_kind = |kind: &str| -> Vec<u64> {
ids.iter()
.copied()
.filter(|&id| doc.kind(id) == kind)
.collect()
};
let param = |id: u64, k: usize| doc.simple(id).and_then(|(_, p)| p.get(k)).and_then(P::reference);
// Product definitions and the name of the product each defines.
let definitions: Vec<u64> = ids
.iter()
.copied()
.filter(|&id| {
matches!(doc.kind(id), "PRODUCT_DEFINITION" | "PRODUCT_DEFINITION_WITH_ASSOCIATED_DOCUMENTS")
})
.collect();
let product_name = |definition: u64| -> String {
let product = param(definition, 2).and_then(|formation| param(formation, 2));
product
.and_then(|p| doc.simple(p))
.and_then(|(_, params)| {
params
.get(1)
.and_then(P::text)
.filter(|s| !s.trim().is_empty())
.or_else(|| params.first().and_then(P::text))
})
.unwrap_or_default()
.to_string()
};
// The shape of a definition (or an occurrence), and the representations
// attached to a shape.
let shape_of: HashMap<u64, u64> = of_kind("PRODUCT_DEFINITION_SHAPE")
.into_iter()
.filter_map(|pds| Some((param(pds, 2)?, pds)))
.collect();
let mut reps_of_shape: HashMap<u64, Vec<u64>> = HashMap::new();
for sdr in of_kind("SHAPE_DEFINITION_REPRESENTATION") {
if let (Some(pds), Some(rep)) = (param(sdr, 0), param(sdr, 1)) {
reps_of_shape.entry(pds).or_default().push(rep);
}
}
// Representations tied without a transformation share their items.
let mut linked: HashMap<u64, Vec<u64>> = HashMap::new();
for id in &ids {
let Some(("SHAPE_REPRESENTATION_RELATIONSHIP", params)) = doc.simple(*id) else {
continue;
};
if let (Some(a), Some(b)) = (params.get(2).and_then(P::reference), params.get(3).and_then(P::reference)) {
linked.entry(a).or_default().push(b);
linked.entry(b).or_default().push(a);
}
}
let items_of = |rep: u64| -> Vec<u64> {
let mut seen = vec![rep];
let mut at = 0;
while at < seen.len() {
for next in linked.get(&seen[at]).into_iter().flatten() {
if !seen.contains(next) {
seen.push(*next);
}
}
at += 1;
}
seen.iter()
.filter_map(|r| doc.simple(*r))
.flat_map(|(_, params)| params.get(1).map(P::list).unwrap_or_default())
.filter_map(P::reference)
.collect()
};
let mut index: HashMap<u64, usize> = HashMap::new();
let mut products: Vec<StepProduct> = Vec::new();
let mut used_items: Vec<u64> = Vec::new();
for &definition in &definitions {
let mut bodies = Vec::new();
for rep in shape_of.get(&definition).and_then(|pds| reps_of_shape.get(pds)).into_iter().flatten() {
for item in items_of(*rep) {
if !used_items.contains(&item) {
let found = item_bodies(&doc, item);
if !found.is_empty() {
used_items.push(item);
}
bodies.extend(found);
}
}
}
index.insert(definition, products.len());
products.push(StepProduct {
name: product_name(definition),
bodies,
children: Vec::new(),
});
}
// Components, placed by the transformation of their occurrence.
let mut component = vec![false; products.len()];
for occurrence in of_kind("NEXT_ASSEMBLY_USAGE_OCCURRENCE") {
let (Some(&parent), Some(&child)) = (
param(occurrence, 3).and_then(|d| index.get(&d)),
param(occurrence, 4).and_then(|d| index.get(&d)),
) else {
continue;
};
let placement = shape_of
.get(&occurrence)
.and_then(|pds| {
ids.iter().copied().find(|&id| {
doc.kind(id) == "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION" && param(id, 1) == Some(*pds)
})
})
.and_then(|cdsr| param(cdsr, 0))
.and_then(|relation| doc.part(relation, "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION"))
.and_then(|p| p.first()?.reference())
.and_then(|transformation| {
let (_, t) = doc.simple(transformation)?;
let from = doc.frame(t.get(2)?.reference()?)?;
let to = doc.frame(t.get(3)?.reference()?)?;
Some(compose(&to, &invert(&from)))
})
.unwrap_or(IDENTITY);
products[parent].children.push((child, placement));
component[child] = true;
}
// A file whose solids no product claims: one product holding them all,
// named after the file's first product.
let loose: Vec<u64> = ids
.iter()
.copied()
.filter(|id| !used_items.contains(id))
.collect();
let mut loose_bodies = Vec::new();
for id in loose {
loose_bodies.extend(item_bodies(&doc, id));
}
if !loose_bodies.is_empty() {
let name = definitions.first().map(|d| product_name(*d)).unwrap_or_default();
component.push(false);
products.push(StepProduct {
name,
bodies: loose_bodies,
children: Vec::new(),
});
}
let roots = (0..products.len())
.filter(|&k| !component[k] && (!products[k].bodies.is_empty() || !products[k].children.is_empty()))
.collect();
Some(StepModel {
unit_mm,
products,
roots,
})
}
+399
View File
@@ -0,0 +1,399 @@
//! Windows metafile (WMF) reading: the records a metafile plays, as the
//! drawing objects of a block — outlines as polylines in their pen's colour
//! and width, filled shapes as closed polylines in their brush's colour,
//! ellipses as 99-segment polylines, text as single-line text.
//!
//! The picture is scaled to fit half the view it is imported into, with its
//! top-left corner at the block's origin and y running down the screen as
//! a metafile's does.
use codec::entities::{Polyline2D, Text, Vertex2D};
use codec::types::{Color, Vector3};
use codec::EntityType;
/// A picture: its objects, in block coordinates, and the font of each
/// text (an index into `fonts`).
pub struct Picture {
pub entities: Vec<EntityType>,
/// The face name of each font the text uses; text styles are
/// `WMF-<n>` for each, in this order.
pub fonts: Vec<String>,
/// For each text in `entities` (by position), its font index.
pub text_fonts: Vec<(usize, usize)>,
}
#[derive(Clone, Copy)]
struct Pen {
null: bool,
width: f64,
rgb: [u8; 3],
}
#[derive(Clone, Copy)]
struct Brush {
null: bool,
rgb: [u8; 3],
}
#[derive(Clone)]
struct Font {
height: f64,
escapement: f64,
face: String,
}
#[derive(Clone)]
enum Object {
Pen(Pen),
Brush(Brush),
Font(Font),
Other,
}
/// Segments of a whole ellipse.
const ELLIPSE_SEGMENTS: usize = 99;
/// A colour reference as the nearest index colour; black and white both
/// draw as colour 7.
fn color(rgb: [u8; 3]) -> Color {
if rgb == [0, 0, 0] || rgb == [255, 255, 255] {
return Color::Index(7);
}
Color::Index(codec::types::nearest_aci(rgb[0], rgb[1], rgb[2]))
}
fn colorref(lo: u16, hi: u16) -> [u8; 3] {
[(lo & 0xff) as u8, (lo >> 8) as u8, (hi & 0xff) as u8]
}
struct Player {
origin: [f64; 2],
scale: [f64; 2],
entities: Vec<EntityType>,
fonts: Vec<String>,
text_fonts: Vec<(usize, usize)>,
}
impl Player {
fn at(&self, x: f64, y: f64) -> Vector3 {
Vector3::new((x - self.origin[0]) * self.scale[0], (y - self.origin[1]) * self.scale[1], 0.0)
}
fn polyline(&mut self, points: &[[f64; 2]], closed: bool, width: f64, rgb: [u8; 3]) {
if points.len() < 2 {
return;
}
let width = width * self.scale[0].abs();
let mut polyline = Polyline2D::new();
polyline.start_width = width;
polyline.end_width = width;
for p in points {
let mut vertex = Vertex2D::new(self.at(p[0], p[1]));
vertex.start_width = width;
vertex.end_width = width;
polyline.add_vertex(vertex);
}
if closed {
polyline.close();
}
polyline.common.color = color(rgb);
self.entities.push(EntityType::Polyline2D(polyline));
}
/// An outline in the pen, or a filled area as its brush's outline.
fn shape(&mut self, points: &[[f64; 2]], pen: &Pen, brush: &Brush) {
let width = if pen.null { 0.0 } else { pen.width };
if !brush.null {
self.polyline(points, true, width, brush.rgb);
} else if !pen.null {
self.polyline(points, true, width, pen.rgb);
}
}
}
/// Points round an ellipse inside `(left, top)–(right, bottom)`, from
/// `start` to `end` (radians, counter-clockwise in drawing coordinates).
fn ellipse_points(bounds: [f64; 4], start: f64, sweep: f64) -> Vec<[f64; 2]> {
let [left, top, right, bottom] = bounds;
let (cx, cy) = (0.5 * (left + right), 0.5 * (top + bottom));
let (rx, ry) = (0.5 * (right - left).abs(), 0.5 * (bottom - top).abs());
let step = std::f64::consts::TAU / ELLIPSE_SEGMENTS as f64;
let count = ((sweep.abs() / step).round() as usize).max(1);
(0..=count)
.map(|k| {
let a = start + sweep * k as f64 / count as f64;
// Metafile y runs down: counter-clockwise on the drawing is
// up the page.
[cx + rx * a.cos(), cy - ry * a.sin()]
})
.collect()
}
/// The angle of a radial point round an ellipse's centre, in drawing
/// orientation.
fn angle(bounds: [f64; 4], x: f64, y: f64) -> f64 {
let [left, top, right, bottom] = bounds;
let (cx, cy) = (0.5 * (left + right), 0.5 * (top + bottom));
(-(y - cy)).atan2(x - cx)
}
/// Reads a metafile, scaled so the picture fits within `fit` (width and
/// height, drawing units). `None` when it is not a metafile.
pub fn read(bytes: &[u8], fit: [f64; 2]) -> Option<Picture> {
let word = |at: usize| -> Option<u16> {
Some(u16::from_le_bytes([*bytes.get(at)?, *bytes.get(at + 1)?]))
};
let mut at = 0;
let mut bounds = None;
if word(0)? == 0xCDD7 && word(2)? == 0x9AC6 {
let s = |k: usize| word(6 + 2 * k).map(|w| w as i16 as f64);
bounds = Some([s(0)?, s(1)?, s(2)?, s(3)?]);
at = 22;
}
// The metafile header: type 1 or 2, nine words long.
if !matches!(word(at)?, 1 | 2) || word(at + 2)? != 9 {
return None;
}
at += 18;
let mut records = Vec::new();
while at + 6 <= bytes.len() {
let size = (u32::from(word(at)?) | (u32::from(word(at + 2)?) << 16)) as usize;
let function = word(at + 4)?;
if function == 0 || size < 3 {
break;
}
let params: Vec<u16> = (0..size - 3).filter_map(|k| word(at + 6 + 2 * k)).collect();
records.push((function, params));
at += 2 * size;
}
// The window first: where the picture is and how big.
let mut origin = [0.0, 0.0];
let mut extent = None;
for (function, p) in &records {
let s = |k: usize| p.get(k).map(|w| *w as i16 as f64);
match function {
0x020B => origin = [s(1)?, s(0)?],
0x020C => extent = Some([s(1)?, s(0)?]),
_ => {}
}
}
let extent = extent
.or_else(|| bounds.map(|[l, t, r, b]| [r - l, b - t]))
.filter(|[w, h]| *w != 0.0 && *h != 0.0)?;
if let (None, Some([l, t, _, _])) = (records.iter().find(|(f, _)| *f == 0x020B), bounds) {
origin = [l, t];
}
let scale = (fit[0] / extent[0].abs()).min(fit[1] / extent[1].abs());
let mut player = Player {
origin,
scale: [scale * extent[0].signum(), -scale * extent[1].signum()],
entities: Vec::new(),
fonts: Vec::new(),
text_fonts: Vec::new(),
};
// A fresh device context: a one-unit black pen, a white brush.
let mut pen = Pen {
null: false,
width: 1.0,
rgb: [0, 0, 0],
};
let mut brush = Brush {
null: false,
rgb: [255, 255, 255],
};
let mut font = Font {
height: 12.0,
escapement: 0.0,
face: String::new(),
};
let mut text_rgb = [0, 0, 0];
let mut table: Vec<Option<Object>> = Vec::new();
let mut current = [0.0, 0.0];
for (function, p) in &records {
let s = |k: usize| p.get(k).map(|w| *w as i16 as f64).unwrap_or(0.0);
let points = |from: usize, count: usize| -> Vec<[f64; 2]> {
(0..count).map(|k| [s(from + 2 * k), s(from + 2 * k + 1)]).collect()
};
let create = |table: &mut Vec<Option<Object>>, object: Object| {
match table.iter().position(Option::is_none) {
Some(slot) => table[slot] = Some(object),
None => table.push(Some(object)),
}
};
match function {
// CREATEPENINDIRECT
0x02FA => create(
&mut table,
Object::Pen(Pen {
null: p.first().is_some_and(|style| style & 0x0f == 5),
width: s(1),
rgb: colorref(p.get(3).copied().unwrap_or(0), p.get(4).copied().unwrap_or(0)),
}),
),
// CREATEBRUSHINDIRECT
0x02FC => create(
&mut table,
Object::Brush(Brush {
null: p.first() == Some(&1),
rgb: colorref(p.get(1).copied().unwrap_or(0), p.get(2).copied().unwrap_or(0)),
}),
),
// CREATEFONTINDIRECT: height, width, escapement, ... face at
// byte 18.
0x02FB => {
let mut face = Vec::new();
for w in p.iter().skip(9) {
face.extend(w.to_le_bytes());
}
let end = face.iter().position(|b| *b == 0).unwrap_or(face.len());
create(
&mut table,
Object::Font(Font {
height: s(0).abs(),
escapement: s(2) / 10.0,
face: String::from_utf8_lossy(&face[..end]).into_owned(),
}),
)
}
// Palettes and regions take a slot too.
0x00F7 | 0x06FF | 0x01F9 | 0x0142 => create(&mut table, Object::Other),
// SELECTOBJECT
0x012D => match table.get(p.first().copied().unwrap_or(0) as usize) {
Some(Some(Object::Pen(x))) => pen = *x,
Some(Some(Object::Brush(x))) => brush = *x,
Some(Some(Object::Font(x))) => font = x.clone(),
_ => {}
},
// DELETEOBJECT
0x01F0 => {
if let Some(slot) = table.get_mut(p.first().copied().unwrap_or(0) as usize) {
*slot = None;
}
}
// SETTEXTCOLOR
0x0209 => text_rgb = colorref(p.first().copied().unwrap_or(0), p.get(1).copied().unwrap_or(0)),
// MOVETO, LINETO (y, x)
0x0214 => current = [s(1), s(0)],
0x0213 => {
let to = [s(1), s(0)];
if !pen.null {
player.polyline(&[current, to], false, pen.width, pen.rgb);
}
current = to;
}
// POLYLINE
0x0325 => {
if !pen.null {
let line = points(1, s(0) as usize);
player.polyline(&line, false, pen.width, pen.rgb);
}
}
// POLYGON
0x0324 => {
let ring = points(1, s(0) as usize);
player.shape(&ring, &pen, &brush);
}
// POLYPOLYGON: counts, then every ring's points
0x0538 => {
let rings = s(0) as usize;
let mut from = 1 + rings;
for k in 0..rings {
let count = s(1 + k) as usize;
let ring = points(from, count);
player.shape(&ring, &pen, &brush);
from += 2 * count;
}
}
// RECTANGLE, ROUNDRECT (bottom, right, top, left; corner
// ellipse first for the latter)
0x041B | 0x061C => {
let o = if *function == 0x061C { 2 } else { 0 };
let (b, r, t, l) = (s(o), s(o + 1), s(o + 2), s(o + 3));
player.shape(&[[l, t], [r, t], [r, b], [l, b]], &pen, &brush);
}
// ELLIPSE: its outline in the pen
0x0418 => {
if !pen.null {
let bounds = [s(3), s(2), s(1), s(0)];
let ring = ellipse_points(bounds, 0.0, std::f64::consts::TAU);
player.polyline(&ring, false, pen.width, pen.rgb);
}
}
// ARC, CHORD, PIE: (yEnd, xEnd, yStart, xStart, bottom,
// right, top, left), drawn counter-clockwise
0x0817 | 0x0830 | 0x081A => {
let bounds = [s(7), s(6), s(5), s(4)];
let start = angle(bounds, s(3), s(2));
let end = angle(bounds, s(1), s(0));
let mut sweep = end - start;
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
}
let mut ring = ellipse_points(bounds, start, sweep);
match function {
0x0817 => {
if !pen.null {
player.polyline(&ring, false, pen.width, pen.rgb);
}
}
_ => {
if *function == 0x081A {
ring.push([0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3])]);
}
player.shape(&ring, &pen, &brush);
}
}
}
// TEXTOUT (count, string, y, x) and EXTTEXTOUT (y, x, count,
// options, [rectangle], string)
0x0521 | 0x0A32 => {
let (count, text_at, (x, y)) = if *function == 0x0521 {
let count = s(0) as usize;
let words = count.div_ceil(2);
(count, 1, (s(2 + words), s(1 + words)))
} else {
let count = s(2) as usize;
let clipped = p.get(3).is_some_and(|o| o & 0x0006 != 0);
(count, if clipped { 8 } else { 4 }, (s(1), s(0)))
};
let mut raw = Vec::new();
for w in p.iter().skip(text_at) {
raw.extend(w.to_le_bytes());
}
raw.truncate(count);
let value: String = raw.iter().map(|b| char::from(*b)).collect();
if value.trim().is_empty() {
continue;
}
// The reference point is the cell's top; the baseline sits
// nine tenths of the font height below it.
let rotation = font.escapement.to_radians();
let drop = 0.9 * font.height;
let base = player.at(x + drop * rotation.sin(), y + drop * rotation.cos());
let index = match player.fonts.iter().position(|f| *f == font.face) {
Some(k) => k,
None => {
player.fonts.push(font.face.clone());
player.fonts.len() - 1
}
};
let mut text = Text::new();
text.value = value;
text.insertion_point = base;
text.height = 0.72 * font.height * scale;
text.rotation = rotation;
text.common.color = color(text_rgb);
player.text_fonts.push((player.entities.len(), index));
player.entities.push(EntityType::Text(text));
}
_ => {}
}
}
Some(Picture {
entities: player.entities,
fonts: player.fonts,
text_fonts: player.text_fonts,
})
}
-4
View File
@@ -3477,10 +3477,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Unavailable" => Some(("plugins", "unavailable")),
"Supported" => Some(("dgn-import", "supported")),
"By extension" => Some(("dgn-import", "by-extension")),
"ACIS solver available" => Some(("dgn-import", "acis-solver")),
"Export only" => Some(("dgn-import", "export-only")),
"ACIS bodies are read only inside drawings; SAT files cannot be imported." => Some(("dgn-import", "acis-reason")),
"STEP files are only written (STEPOUT), not read." => Some(("dgn-import", "step-reason")),
"OpenCAD Studio has no importer for this format." => Some(("dgn-import", "no-importer")),
"Recent:" => Some(("dgn-import", "recent")),
"2D / Underlay" => Some(("dgn-import", "group-2d")),
+11
View File
@@ -615,6 +615,17 @@ impl Scene {
true
}
/// The plan view's window: its centre and its width and height in world
/// units, and the world size of one screen pixel (zero before the first
/// frame).
pub fn view_window(&self) -> ([f64; 2], [f64; 2], f64) {
let aspect = f64::from(self.active_camera_aspect());
let camera = self.camera.borrow();
let height = 2.0 * f64::from(camera.ortho_size());
let pixel = self.world_per_pixel().map_or(0.0, f64::from);
([camera.target.x, camera.target.y], [height * aspect, height], pixel)
}
/// Aspect ratio of the active camera's actual render rectangle. Model
/// panes are separate shader widgets, so the full-canvas render aspect is
/// wrong whenever a tiled viewport is active.
+75
View File
@@ -0,0 +1,75 @@
//! ACIS text (SAT) files and kernel bodies as drawing objects.
//!
//! Each body of a file becomes one object: a closed solid a 3DSOLID, a sheet
//! whose faces all lie on planes a REGION, any other sheet a SURFACE — the
//! classes the ACIS import of the reference application produces.
use codec::entities::acis::{SatDocument, SatFace, Sidedness};
use codec::entities::{AcisData, Region, Solid3D, Surface, SurfaceKind};
use codec::EntityType;
/// What a body restores as.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Class {
Solid,
Region,
Surface,
}
fn class(document: &SatDocument) -> Option<Class> {
let faces: Vec<SatFace<'_>> = document
.records
.iter()
.filter_map(SatFace::from_record)
.collect();
if faces.is_empty() {
return None;
}
if faces
.iter()
.all(|face| face.sidedness() != Sidedness::Double)
{
return Some(Class::Solid);
}
let planar = faces.iter().all(|face| {
face.surface()
.index()
.and_then(|at| document.record(at))
.is_some_and(|surface| surface.entity_type == "plane-surface")
});
Some(if planar { Class::Region } else { Class::Surface })
}
/// One body's document as its drawing object.
fn entity(document: &SatDocument) -> Option<EntityType> {
Some(match class(document)? {
Class::Solid => {
let mut solid = Solid3D::new();
solid.set_sat_document(document);
EntityType::Solid3D(solid)
}
Class::Region => {
let mut region = Region::new();
region.set_sat_document(document);
EntityType::Region(region)
}
Class::Surface => {
let mut surface = Surface::new(SurfaceKind::Generic);
surface.acis_data = AcisData::from_sat(&document.to_sat_string());
EntityType::Surface(Box::new(surface))
}
})
}
/// The objects of an ACIS text file, one per body with faces; `None` when
/// the text is no ACIS data.
pub fn sat_entities(text: &str) -> Option<Vec<EntityType>> {
let document = SatDocument::parse(text).ok()?;
Some(document.split_bodies().iter().filter_map(entity).collect())
}
/// A kernel body as its drawing object, written as exact ACIS.
pub fn body_entity(body: &kernel::brep::Body) -> Option<EntityType> {
let document = super::acis_export::solid_to_sat(body)?;
entity(&document)
}
+1
View File
@@ -1,5 +1,6 @@
pub mod acad_to_render;
pub mod acis_export;
pub mod acis_import;
pub mod acis_kernel;
pub mod dgn_linestyle;
pub mod proxy_graphics;
+4 -15
View File
@@ -50,10 +50,6 @@ pub enum Support {
Yes,
/// Any file: the importer follows the file's extension.
ByExtension,
/// SAT: ACIS bodies are read only inside drawings.
AcisSolverOnly,
/// STEP: written by STEPOUT, not read.
ExportOnly,
No,
}
@@ -87,9 +83,9 @@ pub const FORMATS: [ImportFormat; 21] = [
format("PDF Files", "*.pdf", &["pdf"], 0, Support::Yes),
format("MicroStation DGN", "*.dgn", &["dgn"], 0, Support::Yes),
format("All DGN Files", "*.*", &["*"], 0, Support::Yes),
format("Metafile", "*.wmf", &["wmf"], 0, Support::No),
format("Metafile", "*.wmf", &["wmf"], 0, Support::Yes),
format("3D Studio", "*.3ds", &["3ds"], 1, Support::No),
format("ACIS", "*.sat", &["sat"], 1, Support::AcisSolverOnly),
format("ACIS", "*.sat", &["sat"], 1, Support::Yes),
format(
"CATIA V4",
"*.model; *.session; *.exp; *.dlv3",
@@ -104,7 +100,7 @@ pub const FORMATS: [ImportFormat; 21] = [
1,
Support::No,
),
format("IGES", "*.igs; *.iges", &["igs", "iges"], 1, Support::No),
format("IGES", "*.igs; *.iges", &["igs", "iges"], 1, Support::Yes),
format("Inventor", "*.ipt; *.iam", &["ipt", "iam"], 1, Support::No),
format("JT", "*.jt", &["jt"], 1, Support::No),
format("NX", "*.prt", &["prt"], 1, Support::No),
@@ -121,7 +117,7 @@ pub const FORMATS: [ImportFormat; 21] = [
1,
Support::No,
),
format("STEP", "*.ste; *.stp; *.step", &["ste", "stp", "step"], 1, Support::ExportOnly),
format("STEP", "*.ste; *.stp; *.step", &["ste", "stp", "step"], 1, Support::Yes),
format("All Files", "*.*", &["*"], 2, Support::ByExtension),
];
@@ -178,13 +174,10 @@ fn badge<'a>(support: Support) -> Element<'a, Message> {
let (label, tone) = match support {
Support::Yes => (t!("Supported"), 0),
Support::ByExtension => (t!("By extension"), 2),
Support::AcisSolverOnly => (t!("ACIS solver available"), 1),
Support::ExportOnly => (t!("Export only"), 1),
Support::No => (t!("None"), 2),
};
let color = move |theme: &Theme| match tone {
0 => theme.palette().success.base.color,
1 => AMBER,
_ => theme.palette().background.base.text.scale_alpha(0.55),
};
container(
@@ -210,10 +203,6 @@ fn badge<'a>(support: Support) -> Element<'a, Message> {
pub fn unsupported_reason(support: Support) -> Option<std::borrow::Cow<'static, str>> {
match support {
Support::Yes | Support::ByExtension => None,
Support::AcisSolverOnly => Some(t!(
"ACIS bodies are read only inside drawings; SAT files cannot be imported."
)),
Support::ExportOnly => Some(t!("STEP files are only written (STEPOUT), not read.")),
Support::No => Some(t!("OpenCAD Studio has no importer for this format.")),
}
}