forked from 1b-agent/opencadstudio-upstream
Merge pull request #1844 from ramox81/codex/import-formats
IMPORT: read SAT, STEP, IGES and WMF files; ACISIN and WMFIN # Conflicts: # Cargo.lock
This commit is contained in:
commit
c3baff7ea2
48 files changed
+4934
-119
No files matched your search
@@ -664,6 +664,11 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
return self.run_dgn_import(i, request);
|
||||
}
|
||||
CmdResult::TdsImport(request) => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.run_tds_import(i, request);
|
||||
}
|
||||
CmdResult::PdfImportFile(import) => {
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::io::model_import::ModelFormat;
|
||||
use crate::modules::insert::dgn_import::{self, DgnImportCommand, DgnImportRequest};
|
||||
use crate::ui::window::import_dialogs::{
|
||||
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
|
||||
@@ -18,7 +19,7 @@ const RECENT: usize = 5;
|
||||
|
||||
/// Picks a file and registers its bytes (the web build reads them only
|
||||
/// here); `None` when the picker is cancelled.
|
||||
async fn pick(title: String, filters: Vec<(String, Vec<String>)>) -> Option<String> {
|
||||
pub(super) async fn pick(title: String, filters: Vec<(String, Vec<String>)>) -> Option<String> {
|
||||
let mut dialog = crate::sys::file_dialog().set_title(title.as_str());
|
||||
for (name, extensions) in &filters {
|
||||
let extensions: Vec<&str> = extensions.iter().map(String::as_str).collect();
|
||||
@@ -62,6 +63,9 @@ fn is_dgn(path: &str) -> bool {
|
||||
enum Route {
|
||||
Pdf,
|
||||
Dgn,
|
||||
Model(ModelFormat),
|
||||
Wmf,
|
||||
Tds,
|
||||
}
|
||||
|
||||
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
|
||||
@@ -75,6 +79,22 @@ fn route(path: &str, format: Option<usize>) -> Result<Route, String> {
|
||||
}
|
||||
let ext = extension(path);
|
||||
let by_extension = f.is_none_or(|f| f.support == Support::ByExtension);
|
||||
// The chosen format, or the one whose patterns name the extension.
|
||||
let named = if by_extension {
|
||||
FORMATS
|
||||
.iter()
|
||||
.find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)))
|
||||
} else {
|
||||
f
|
||||
};
|
||||
match named.map(|f| f.name) {
|
||||
Some("ACIS") => return Ok(Route::Model(ModelFormat::Sat)),
|
||||
Some("STEP") => return Ok(Route::Model(ModelFormat::Step)),
|
||||
Some("IGES") => return Ok(Route::Model(ModelFormat::Iges)),
|
||||
Some("Metafile") => return Ok(Route::Wmf),
|
||||
Some("3D Studio") => return Ok(Route::Tds),
|
||||
_ => {}
|
||||
}
|
||||
let pdf = if by_extension { ext == "pdf" } else { f.is_some_and(|f| f.name == "PDF Files") };
|
||||
if pdf {
|
||||
return Ok(Route::Pdf);
|
||||
@@ -111,8 +131,8 @@ impl OpenCADStudio {
|
||||
Some(Task::none())
|
||||
}
|
||||
("IMPORT", true) => {
|
||||
use crate::command::{CadCommand, ValuePromptCommand};
|
||||
let command = ValuePromptCommand::new("_IMPORTFILE", "Enter import file name:");
|
||||
use crate::command::{CadCommand, FreeTextValuePromptCommand};
|
||||
let command = FreeTextValuePromptCommand::new("_IMPORTFILE", "Enter import file name:");
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
@@ -217,6 +237,24 @@ impl OpenCADStudio {
|
||||
}
|
||||
self.dispatch_command(&format!("-DGNIMPORT {path}"))
|
||||
}
|
||||
Ok(Route::Model(format)) => {
|
||||
self.remember_import(path);
|
||||
let i = self.active_tab;
|
||||
self.import_model(i, path, format)
|
||||
}
|
||||
Ok(Route::Wmf) => {
|
||||
self.remember_import(path);
|
||||
let i = self.active_tab;
|
||||
self.import_wmf(i, path, false)
|
||||
}
|
||||
// The options window opens whatever FILEDIA is.
|
||||
Ok(Route::Tds) => {
|
||||
self.remember_import(path);
|
||||
if let Err(error) = self.open_tds_import_dialog(path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
Task::none()
|
||||
@@ -232,6 +270,7 @@ impl OpenCADStudio {
|
||||
"dgn" => crate::t!(
|
||||
"Imports a DGN model as drawing objects: levels as layers, cells as blocks."
|
||||
),
|
||||
"3ds" => std::borrow::Cow::Owned(crate::tr!("import-3ds", "help")),
|
||||
_ => crate::t!(
|
||||
"Imports a file of the chosen format with the format's importer."
|
||||
),
|
||||
@@ -324,8 +363,81 @@ impl OpenCADStudio {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
Ok(Route::Model(kind)) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
return self.import_model(i, &path, kind);
|
||||
}
|
||||
Ok(Route::Wmf) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
return self.import_wmf(i, &path, false);
|
||||
}
|
||||
Ok(Route::Tds) => {
|
||||
self.remember_import(&path);
|
||||
self.close_active_modal();
|
||||
if let Err(error) = self.open_tds_import_dialog(&path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── 3D Studio Import ─────────────────────────────────────────
|
||||
ImportDialogMsg::TdsChange => {
|
||||
let title = crate::t!("Import File").into_owned();
|
||||
let filters = vec![("3D Studio (*.3ds)".to_string(), vec!["3ds".to_string()])];
|
||||
return Task::perform(pick(title, filters), |path| {
|
||||
Message::ImportDialog(ImportDialogMsg::TdsPicked(path))
|
||||
});
|
||||
}
|
||||
ImportDialogMsg::TdsPicked(Some(path)) => {
|
||||
if let Err(error) = self.open_tds_import_dialog(&path) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsPicked(None) => {}
|
||||
ImportDialogMsg::TdsCheck(k, on) => {
|
||||
if let Some(c) = self.import_dialogs.tds.as_mut().and_then(|s| s.checked.get_mut(k)) {
|
||||
*c = on;
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsAll | ImportDialogMsg::TdsClear => {
|
||||
let on = matches!(message, ImportDialogMsg::TdsAll);
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.checked.iter_mut().for_each(|c| *c = on);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsFilter(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.filter = k.min(4);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsSearch(text) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.search = text;
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsLayers(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.layers = k.min(3);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsMulti(k) => {
|
||||
if let Some(s) = self.import_dialogs.tds.as_mut() {
|
||||
s.multi = k.min(3);
|
||||
}
|
||||
}
|
||||
ImportDialogMsg::TdsOk => {
|
||||
let Some(state) = self.import_dialogs.tds.take() else {
|
||||
return Task::none();
|
||||
};
|
||||
if !state.checked.iter().any(|c| *c) {
|
||||
self.import_dialogs.tds = Some(state);
|
||||
return Task::none();
|
||||
}
|
||||
self.close_active_modal();
|
||||
return self.start_tds_import(i, state);
|
||||
}
|
||||
// ── DGN Import ───────────────────────────────────────────────
|
||||
ImportDialogMsg::DgnChange => return self.pick_dgn_file(),
|
||||
ImportDialogMsg::DgnPicked(Some(path)) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ mod fileops;
|
||||
mod inquiry;
|
||||
mod layerprops;
|
||||
mod layers;
|
||||
mod model_import;
|
||||
mod pc_colormap;
|
||||
mod pc_extract;
|
||||
mod pdf_dialogs;
|
||||
@@ -415,6 +416,9 @@ impl OpenCADStudio {
|
||||
if let Some(t) = self.dispatch_import(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_model_import(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
if let Some(t) = self.dispatch_bvisibility(cmd, i) {
|
||||
return Some(t);
|
||||
}
|
||||
@@ -1016,6 +1020,8 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"QNEW",
|
||||
"OBJIMPORT",
|
||||
"IMPORT",
|
||||
"ACISIN",
|
||||
"WMFIN",
|
||||
"DGNIMPORT",
|
||||
"-DGNIMPORT",
|
||||
"OPEN",
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
//! 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 face, `WMF-<face><n>` with `n` counting the
|
||||
// face's bold and italic variants; the style's font is the
|
||||
// installed file of that face, none when it is not installed. A
|
||||
// style the drawing already has is used as it is.
|
||||
let mut styles: Vec<String> = Vec::new();
|
||||
for (k, face) in picture.fonts.iter().enumerate() {
|
||||
let n = picture.fonts[..k].iter().filter(|f| f.name == face.name).count();
|
||||
let name = format!("WMF-{}{n}", face.name);
|
||||
if !scene.document.text_styles.contains(&name) {
|
||||
let mut style = codec::tables::TextStyle::new(name.as_str());
|
||||
style.handle = scene.document.allocate_handle();
|
||||
style.font_file =
|
||||
crate::scene::text::sysfont::face_file(&face.name, face.bold, face.italic).unwrap_or_default();
|
||||
let _ = scene.document.text_styles.add(style);
|
||||
}
|
||||
styles.push(name);
|
||||
}
|
||||
for (at, font) in &picture.text_fonts {
|
||||
if let Some(codec::EntityType::Text(text)) = picture.entities.get_mut(*at) {
|
||||
text.style = styles[*font].clone();
|
||||
}
|
||||
}
|
||||
let mut n = 0;
|
||||
let name = loop {
|
||||
let name = format!("WMF{n}");
|
||||
if scene.document.block_records.get(&name).is_none() {
|
||||
break name;
|
||||
}
|
||||
n += 1;
|
||||
};
|
||||
if let Err(error) =
|
||||
scene.define_block_from_owned_entities(picture.entities, &name, glam::DVec3::ZERO)
|
||||
{
|
||||
self.command_line.push_error(&error);
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
if prompt {
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::insert::insert_block::InsertBlockCommand;
|
||||
let preview = self.tabs[i].scene.block_preview_wires(&name);
|
||||
let command = InsertBlockCommand::classic_for_block(name, preview, String::new());
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
} else {
|
||||
// The view's top-left corner, a pixel or two in.
|
||||
let corner = codec::types::Vector3::new(
|
||||
centre[0] - 0.5 * size[0] + pixel,
|
||||
centre[1] + 0.5 * size[1] - 2.0 * pixel,
|
||||
0.0,
|
||||
);
|
||||
let insert = codec::entities::Insert::new(&name, corner);
|
||||
self.tabs[i].scene.add_entity(codec::EntityType::Insert(insert));
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// The 3D Studio Import window for a file; an error when it is no 3D
|
||||
/// Studio file.
|
||||
pub(in crate::app) fn open_tds_import_dialog(&mut self, path: &str) -> Result<(), String> {
|
||||
let scene = crate::scene::model::pdf_raster::source_bytes(path)
|
||||
.and_then(|bytes| crate::io::tds_import::read(&bytes))
|
||||
.ok_or_else(|| format!("{} is not a valid 3D Studio file.", path.replace('/', "\\")))?;
|
||||
let mut objects: Vec<(String, &'static str, usize)> = scene
|
||||
.objects
|
||||
.iter()
|
||||
.map(|(name, object)| {
|
||||
let materials = match object {
|
||||
crate::io::tds_import::TdsObject::Mesh(mesh) => scene.mesh_materials(mesh).len(),
|
||||
_ => 0,
|
||||
};
|
||||
(name.clone(), object.kind(), materials)
|
||||
})
|
||||
.collect();
|
||||
objects.sort_by_key(|o| o.0.to_lowercase());
|
||||
objects.insert(0, ("Global Settings".to_string(), "Global", 0));
|
||||
// The layer and material choices made last stay; nothing starts
|
||||
// checked.
|
||||
let previous = self.import_dialogs.tds.take();
|
||||
self.import_dialogs.tds = Some(crate::ui::window::import_dialogs::TdsImportState {
|
||||
path: path.to_string(),
|
||||
checked: vec![false; objects.len()],
|
||||
objects,
|
||||
materials: scene.materials.len(),
|
||||
filter: 0,
|
||||
search: String::new(),
|
||||
layers: previous.as_ref().map_or(0, |s| s.layers),
|
||||
multi: previous.as_ref().map_or(0, |s| s.multi),
|
||||
});
|
||||
self.active_modal = Some(crate::app::ModalKind::TdsImport);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The window's choices as an import: asked mesh by mesh first when set
|
||||
/// to ask and some checked mesh has several materials.
|
||||
pub(in crate::app) fn start_tds_import(
|
||||
&mut self,
|
||||
i: usize,
|
||||
state: crate::ui::window::import_dialogs::TdsImportState,
|
||||
) -> Task<Message> {
|
||||
use crate::io::tds_import::{LayerMode, MultiMaterial, TdsObject};
|
||||
use crate::modules::insert::tds_import::{TdsImportRequest, TdsMaterialCommand};
|
||||
let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&state.path)
|
||||
.and_then(|bytes| crate::io::tds_import::read(&bytes))
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
let selected: Vec<String> = state
|
||||
.objects
|
||||
.iter()
|
||||
.zip(&state.checked)
|
||||
.filter(|(_, on)| **on)
|
||||
.map(|((name, _, _), _)| name.clone())
|
||||
.collect();
|
||||
let several: Vec<(String, Vec<String>)> = scene
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(name, _)| selected.contains(name))
|
||||
.filter_map(|(name, object)| match object {
|
||||
TdsObject::Mesh(mesh) => {
|
||||
let materials = scene.mesh_materials(mesh);
|
||||
(materials.len() > 1).then(|| (name.clone(), materials))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let choice = match state.multi {
|
||||
1 => Some(MultiMaterial::Split),
|
||||
2 => Some(MultiMaterial::First),
|
||||
3 => Some(MultiMaterial::Material(None)),
|
||||
_ => None,
|
||||
};
|
||||
let mut request = TdsImportRequest {
|
||||
path: state.path.clone(),
|
||||
selected,
|
||||
layers: match state.layers {
|
||||
1 => LayerMode::ByMaterial,
|
||||
2 => LayerMode::ByObjectColor,
|
||||
3 => LayerMode::Single,
|
||||
_ => LayerMode::ByObject,
|
||||
},
|
||||
multi: Default::default(),
|
||||
compact: matches!(state.multi, 0 | 1),
|
||||
};
|
||||
match choice {
|
||||
Some(choice) => {
|
||||
for (name, _) in several {
|
||||
request.multi.insert(name, choice.clone());
|
||||
}
|
||||
}
|
||||
None if !several.is_empty() => {
|
||||
use crate::command::CadCommand;
|
||||
let command = TdsMaterialCommand::new(request, several);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Task::none();
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
self.run_tds_import(i, request);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// The import itself, one undo step: the meshes on their layers with
|
||||
/// their materials, the cameras as named views.
|
||||
pub(in crate::app) fn run_tds_import(
|
||||
&mut self,
|
||||
i: usize,
|
||||
request: crate::modules::insert::tds_import::TdsImportRequest,
|
||||
) {
|
||||
use codec::objects::ObjectType;
|
||||
let Some(scene) = crate::scene::model::pdf_raster::source_bytes(&request.path)
|
||||
.and_then(|bytes| crate::io::tds_import::read(&bytes))
|
||||
else {
|
||||
self.command_line.push_error(&format!(
|
||||
"{} is not a valid 3D Studio file.",
|
||||
request.path.replace('/', "\\")
|
||||
));
|
||||
return;
|
||||
};
|
||||
let import = crate::io::tds_import::convert(
|
||||
&scene,
|
||||
&crate::io::tds_import::TdsOptions {
|
||||
selected: request.selected.clone(),
|
||||
layers: request.layers,
|
||||
multi: request.multi.clone(),
|
||||
compact: request.compact,
|
||||
},
|
||||
);
|
||||
self.push_undo_snapshot(i, "IMPORT");
|
||||
let document = &mut self.tabs[i].scene.document;
|
||||
// Materials by name: the drawing's own when it has one, else one
|
||||
// made from the file's colours.
|
||||
let mut materials: std::collections::HashMap<String, codec::Handle> = Default::default();
|
||||
for name in &import.materials {
|
||||
let existing = document.objects.iter().find_map(|(h, o)| match o {
|
||||
ObjectType::Material(m) if m.name.eq_ignore_ascii_case(name) => Some(*h),
|
||||
_ => None,
|
||||
});
|
||||
let handle = match existing {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
let source = scene.materials.iter().find(|m| m.name == *name);
|
||||
let mut material = codec::objects::Material::new();
|
||||
material.handle = document.allocate_handle();
|
||||
material.owner = document.header.acad_material_dict_handle;
|
||||
material.name = name.clone();
|
||||
let color = |rgb: Option<[u8; 3]>| match rgb {
|
||||
Some([r, g, b]) => codec::objects::MaterialColor {
|
||||
flag: 1,
|
||||
factor: 1.0,
|
||||
rgb: Some(
|
||||
((0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b))
|
||||
as i32,
|
||||
),
|
||||
},
|
||||
None => codec::objects::MaterialColor::default(),
|
||||
};
|
||||
material.ambient_color = color(source.and_then(|m| m.ambient));
|
||||
material.diffuse_color = color(source.and_then(|m| m.diffuse));
|
||||
material.specular_color = color(source.and_then(|m| m.specular));
|
||||
let handle = material.handle;
|
||||
let dictionary = document.header.acad_material_dict_handle;
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
|
||||
dictionary.add_entry(name.clone(), handle);
|
||||
}
|
||||
document.objects.insert(handle, ObjectType::Material(material));
|
||||
handle
|
||||
}
|
||||
};
|
||||
materials.insert(name.clone(), handle);
|
||||
}
|
||||
for part in &import.parts {
|
||||
if !document.layers.contains(&part.layer) {
|
||||
let mut layer = codec::tables::Layer::new(part.layer.as_str());
|
||||
layer.handle = document.allocate_handle();
|
||||
let _ = document.layers.add(layer);
|
||||
}
|
||||
}
|
||||
for view in &import.views {
|
||||
let mut named = codec::tables::View::new(view.name.as_str());
|
||||
named.target = codec::types::Vector3::new(view.target[0], view.target[1], view.target[2]);
|
||||
named.direction =
|
||||
codec::types::Vector3::new(view.direction[0], view.direction[1], view.direction[2]);
|
||||
named.height = view.height;
|
||||
named.width = view.height;
|
||||
named.lens_length = view.lens;
|
||||
named.handle = match document.views.get(&view.name) {
|
||||
Some(old) => old.handle,
|
||||
None => document.allocate_handle(),
|
||||
};
|
||||
document.views.add_or_replace(named);
|
||||
}
|
||||
for part in import.parts {
|
||||
let mut mesh = part.mesh;
|
||||
mesh.common.layer = part.layer.clone();
|
||||
mesh.common.material_handle = part.material.as_ref().and_then(|m| materials.get(m).copied());
|
||||
if mesh.common.material_handle.is_some() {
|
||||
mesh.common.material_flags = 0b11;
|
||||
}
|
||||
self.tabs[i].scene.add_entity(codec::EntityType::PolyfaceMesh(mesh));
|
||||
}
|
||||
for line in &import.messages {
|
||||
self.command_line.push_output(line);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,7 @@ pub(super) const NAMES: &[&str] = &[
|
||||
"pdf_import_pick",
|
||||
"pdf_import_dialog",
|
||||
"dgn_import_dialog",
|
||||
"tds_import_dialog",
|
||||
"refedit_dialog",
|
||||
"pdf_shx_settings",
|
||||
"pdf_layer_toggle",
|
||||
@@ -2234,6 +2235,45 @@ impl OpenCADStudio {
|
||||
_ => return Err(bad()),
|
||||
})
|
||||
}
|
||||
// The 3D Studio Import window, one field per call: "check=<name>"
|
||||
// and "uncheck=<name>" (the object's name, or "Global Settings"),
|
||||
// "all", "clear", "filter=all|mesh|light|camera|general",
|
||||
// "search=<text>", "layers=object|material|color|single",
|
||||
// "multi=ask|split|first|none", "path=<file>" (as Change… picks
|
||||
// it), "ok" or "cancel".
|
||||
"tds_import_dialog" => {
|
||||
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
|
||||
if self.active_modal != Some(crate::app::ModalKind::TdsImport) {
|
||||
return Err(failure("no_dialog", "The 3D Studio Import window is not open"));
|
||||
}
|
||||
let spec = string(req, "value")?;
|
||||
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
|
||||
let bad = || failure("invalid_value", spec);
|
||||
if key == "cancel" {
|
||||
return Ok(Task::done(Message::CloseModal));
|
||||
}
|
||||
let pick = |options: &[&str]| options.iter().position(|o| *o == value).ok_or_else(bad);
|
||||
Message::ImportDialog(match key {
|
||||
"ok" => I::TdsOk,
|
||||
"all" => I::TdsAll,
|
||||
"clear" => I::TdsClear,
|
||||
"check" | "uncheck" => {
|
||||
let k = self
|
||||
.import_dialogs
|
||||
.tds
|
||||
.as_ref()
|
||||
.and_then(|s| s.objects.iter().position(|(name, _, _)| name.eq_ignore_ascii_case(value)))
|
||||
.ok_or_else(bad)?;
|
||||
I::TdsCheck(k, key == "check")
|
||||
}
|
||||
"filter" => I::TdsFilter(pick(&["all", "mesh", "light", "camera", "general"])?),
|
||||
"search" => I::TdsSearch(value.to_string()),
|
||||
"layers" => I::TdsLayers(pick(&["object", "material", "color", "single"])?),
|
||||
"multi" => I::TdsMulti(pick(&["ask", "split", "first", "none"])?),
|
||||
"path" => I::TdsPicked(Some(value.to_string())),
|
||||
_ => return Err(bad()),
|
||||
})
|
||||
}
|
||||
// The DGN Import window, one field per call: "path=<file>" (as
|
||||
// Change… picks it), "model=<n>", "target=new|current",
|
||||
// "conflict=prefix|ignore", "units=master|sub", "explode=on|off",
|
||||
|
||||
@@ -2340,6 +2340,8 @@ pub enum ModalKind {
|
||||
ImportFile,
|
||||
/// DGNIMPORT's DGN Import window.
|
||||
DgnImport,
|
||||
/// IMPORT's 3D Studio Import window.
|
||||
TdsImport,
|
||||
/// Web: a crash-recovery autosave snapshot newer than the drawing's last
|
||||
/// save awaits a restore/discard decision. Distinct from the corrupt-file
|
||||
/// `RecoveryPrompt` — nothing is wrong with the drawing itself.
|
||||
|
||||
@@ -255,6 +255,7 @@ impl OpenCADStudio {
|
||||
Some(PdfImportFile) => self.pdf_import_file = None,
|
||||
Some(ImportFile) => self.import_dialogs.file = None,
|
||||
Some(DgnImport) => self.import_dialogs.dgn = None,
|
||||
Some(TdsImport) => self.import_dialogs.tds = None,
|
||||
Some(XrefAttach) => self.xref_attach = None,
|
||||
Some(WriteBlock) => self.wblock = None,
|
||||
Some(Hyperlink) => {
|
||||
|
||||
@@ -51,6 +51,7 @@ impl OpenCADStudio {
|
||||
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
|
||||
Some(K::ImportFile) => crate::t!("Import File").into_owned(),
|
||||
Some(K::DgnImport) => crate::t!("DGN Import").into_owned(),
|
||||
Some(K::TdsImport) => crate::tr!("import-3ds", "title"),
|
||||
Some(K::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
|
||||
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
|
||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||
@@ -620,6 +621,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::import_dialogs::view_import_file(state, recent, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::TdsImport => {
|
||||
let state = self.import_dialogs.tds.as_ref()?;
|
||||
sized_flow(ex, 860, 560, |flow| {
|
||||
crate::ui::window::import_dialogs::view_tds_import(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::DgnImport => {
|
||||
let state = self.import_dialogs.dgn.as_ref()?;
|
||||
sized_flow(ex, 900, 680, |flow| {
|
||||
|
||||
@@ -2333,6 +2333,8 @@ pub enum CmdResult {
|
||||
ReturnPoint(DVec3),
|
||||
/// Import a DGN model (DGNIMPORT); ends the command.
|
||||
DgnImport(crate::modules::insert::dgn_import::DgnImportRequest),
|
||||
/// Import a 3D Studio file with every question answered; ends the command.
|
||||
TdsImport(crate::modules::insert::tds_import::TdsImportRequest),
|
||||
}
|
||||
|
||||
/// What kind of value the active command is currently asking for. Drives
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,662 @@
|
||||
//! IGES reading: the manifold solids (type 186) of a file, their shells,
|
||||
//! faces, loops and edges (514/510/508/504/502), and its trimmed and bounded
|
||||
//! surfaces (144/142 and 143/141), over spline surfaces and curves
|
||||
//! (128/126), lines and arcs (110/100) and composite curves (102), 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,
|
||||
/// Points closer than this are one point, in drawing units.
|
||||
resolution: 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// A boundary curve as its pieces — each a curve and the points it runs
|
||||
/// from and to — composite curves taken apart.
|
||||
fn pieces(&self, pointer: f64) -> Option<Vec<(Curve3, [f64; 3], [f64; 3])>> {
|
||||
let entity = self.get(pointer)?;
|
||||
if entity.kind == 102 {
|
||||
let count = entity.params.get(1).map_or(0.0, P::num) as usize;
|
||||
let mut out = Vec::new();
|
||||
for k in 0..count {
|
||||
out.extend(self.pieces(entity.params.get(2 + k)?.num())?);
|
||||
}
|
||||
return Some(out);
|
||||
}
|
||||
let curve = self.curve(pointer)?;
|
||||
let v = |k: usize| entity.params.get(k).map_or(0.0, P::num);
|
||||
let place = self.place(entity);
|
||||
let (from, to) = match (&curve, entity.kind) {
|
||||
(_, 110) => (place([v(1), v(2), v(3)]), place([v(4), v(5), v(6)])),
|
||||
(_, 100) => (place([v(4), v(5), v(1)]), place([v(6), v(7), v(1)])),
|
||||
(Curve3::Nurbs(nurbs), _) => {
|
||||
let (t0, t1) = nurbs.domain();
|
||||
(nurbs.point_at_knot(t0), nurbs.point_at_knot(t1))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(vec![(curve, from, to)])
|
||||
}
|
||||
|
||||
/// The model-space pieces of a curve on a surface (142) or a boundary
|
||||
/// (141), each of the latter's curves in the sense it is run.
|
||||
fn boundary(&self, pointer: f64) -> Option<Vec<(Curve3, [f64; 3], [f64; 3])>> {
|
||||
let entity = self.get(pointer)?;
|
||||
let v = |k: usize| entity.params.get(k).map_or(0.0, P::num);
|
||||
match entity.kind {
|
||||
142 => self.pieces(v(4)),
|
||||
141 => {
|
||||
let mut out = Vec::new();
|
||||
let mut at = 5;
|
||||
for _ in 0..v(4) as usize {
|
||||
let mut pieces = self.pieces(v(at))?;
|
||||
if v(at + 1) == 2.0 {
|
||||
pieces.reverse();
|
||||
for piece in &mut pieces {
|
||||
std::mem::swap(&mut piece.1, &mut piece.2);
|
||||
}
|
||||
}
|
||||
out.extend(pieces);
|
||||
at += 3 + v(at + 2) as usize;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
_ => self.pieces(pointer),
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
/// Vertices made where boundary curves meet, by position.
|
||||
corners: Vec<([f64; 3], VertexKey)>,
|
||||
}
|
||||
|
||||
fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
(0..3).map(|k| (a[k] - b[k]).powi(2)).sum::<f64>().sqrt()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// The vertex at a point, shared with one already there.
|
||||
fn corner(&mut self, point: [f64; 3]) -> VertexKey {
|
||||
let tolerance = self.doc.resolution;
|
||||
if let Some((_, v)) = self.corners.iter().find(|(p, _)| distance(*p, point) <= tolerance) {
|
||||
return *v;
|
||||
}
|
||||
let v = self.build.vertex(point);
|
||||
self.corners.push((point, v));
|
||||
v
|
||||
}
|
||||
|
||||
/// A loop along curve pieces, in their order.
|
||||
fn curve_ring(&mut self, pieces: Vec<(Curve3, [f64; 3], [f64; 3])>) -> Option<Vec<(EdgeKey, bool)>> {
|
||||
let mut ring = Vec::new();
|
||||
for (curve, from, to) in pieces {
|
||||
if distance(from, to) <= self.doc.resolution && matches!(curve, Curve3::Line(_)) {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (self.corner(from), self.corner(to));
|
||||
ring.push((self.build.edge(curve, a, b)?, true));
|
||||
}
|
||||
(!ring.is_empty()).then_some(ring)
|
||||
}
|
||||
|
||||
/// A loop round the edge of a spline surface's parameter domain; sides
|
||||
/// that close up to a point (a pole) are left out.
|
||||
fn domain_ring(&mut self, surface: &NurbsSurface3) -> Option<Vec<(EdgeKey, bool)>> {
|
||||
let ((u0, u1), (v0, v1)) = surface.domain();
|
||||
let corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
|
||||
let sides = [(1, v0), (0, u1), (1, v1), (0, u0)];
|
||||
let mut pieces = Vec::new();
|
||||
for (k, (axis, at)) in sides.into_iter().enumerate() {
|
||||
let (a, b) = (corners[k], corners[(k + 1) % 4]);
|
||||
let (from, to) = (surface.point_at_knot(a.0, a.1), surface.point_at_knot(b.0, b.1));
|
||||
let middle = surface.point_at_knot(0.5 * (a.0 + b.0), 0.5 * (a.1 + b.1));
|
||||
if distance(from, middle) <= self.doc.resolution && distance(to, middle) <= self.doc.resolution {
|
||||
continue;
|
||||
}
|
||||
pieces.push((Curve3::Nurbs(surface.isocurve(axis, at)?), from, to));
|
||||
}
|
||||
self.curve_ring(pieces)
|
||||
}
|
||||
|
||||
/// Turns a loop to run counter-clockwise round the surface's normal (an
|
||||
/// outer one) or clockwise (a hole), judged by the area it encloses in
|
||||
/// the surface's parameters.
|
||||
fn orient(&self, surface: &Surface, ring: &mut [(EdgeKey, bool)], outer: bool) {
|
||||
let mut uv = Vec::new();
|
||||
for (edge, forward) in ring.iter() {
|
||||
let Some(edge) = self.build.body.edges.get(*edge) else { return };
|
||||
let Some(curve) = self.build.body.curves.get(edge.curve) else { return };
|
||||
let (t0, t1) = (edge.start_parameter, edge.end_parameter);
|
||||
for k in 0..8 {
|
||||
let s = k as f64 / 8.0;
|
||||
let s = if *forward { s } else { 1.0 - s };
|
||||
if let Some(p) = surface.parameters_at(curve.point_at(t0 + (t1 - t0) * s)) {
|
||||
uv.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
let area: f64 = (0..uv.len())
|
||||
.map(|k| {
|
||||
let (a, b) = (uv[k], uv[(k + 1) % uv.len()]);
|
||||
a.0 * b.1 - b.0 * a.1
|
||||
})
|
||||
.sum();
|
||||
if (area > 0.0) != outer {
|
||||
ring.reverse();
|
||||
for (_, forward) in ring.iter_mut() {
|
||||
*forward = !*forward;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trimmed (144) or bounded (143) surface as a one-face sheet.
|
||||
fn sheet(&mut self, entity: &Entity) -> Option<()> {
|
||||
let v = |k: usize| entity.params.get(k).map_or(0.0, P::num);
|
||||
let (surface_at, outer, holes): (f64, Option<f64>, Vec<f64>) = match entity.kind {
|
||||
144 => (
|
||||
v(1),
|
||||
(v(2) != 0.0).then(|| v(4)),
|
||||
(0..v(3) as usize).map(|k| v(5 + k)).collect(),
|
||||
),
|
||||
143 => {
|
||||
let count = v(3) as usize;
|
||||
(v(2), (count > 0).then(|| v(4)), (1..count).map(|k| v(4 + k)).collect())
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let Surface::Nurbs(nurbs) = self.doc.surface(surface_at)? else { return None };
|
||||
let surface = Surface::Nurbs(nurbs.clone());
|
||||
let mut first = match outer {
|
||||
Some(boundary) => {
|
||||
let pieces = self.doc.boundary(boundary)?;
|
||||
self.curve_ring(pieces)?
|
||||
}
|
||||
None => self.domain_ring(&nurbs)?,
|
||||
};
|
||||
self.orient(&surface, &mut first, true);
|
||||
let mut loops = vec![first];
|
||||
for hole in holes {
|
||||
let pieces = self.doc.boundary(hole)?;
|
||||
let mut ring = self.curve_ring(pieces)?;
|
||||
self.orient(&surface, &mut ring, false);
|
||||
loops.push(ring);
|
||||
}
|
||||
self.build.lump(vec![vec![FaceSpec {
|
||||
surface,
|
||||
forward: true,
|
||||
loops,
|
||||
}]])
|
||||
}
|
||||
|
||||
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)>,
|
||||
/// Each trimmed or bounded surface, a one-face sheet, with its colour.
|
||||
pub sheets: 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 resolution = globals.get(18).map(P::num).filter(|r| *r > 0.0).unwrap_or(1e-6) * scale;
|
||||
let doc = Doc {
|
||||
entities,
|
||||
scale,
|
||||
resolution,
|
||||
};
|
||||
|
||||
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(),
|
||||
corners: Vec::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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
// The solids come in last first, as the reference reader places them.
|
||||
bodies.reverse();
|
||||
let mut numbers: Vec<usize> = doc
|
||||
.entities
|
||||
.iter()
|
||||
.filter(|(_, e)| matches!(e.kind, 143 | 144))
|
||||
.map(|(n, _)| *n)
|
||||
.collect();
|
||||
numbers.sort_unstable();
|
||||
let mut sheets = Vec::new();
|
||||
for number in numbers {
|
||||
let entity = &doc.entities[&number];
|
||||
let mut assembly = Assembly {
|
||||
doc: &doc,
|
||||
build: Builder::new(),
|
||||
vertices: HashMap::new(),
|
||||
edges: HashMap::new(),
|
||||
corners: Vec::new(),
|
||||
};
|
||||
if assembly.sheet(entity).is_some() {
|
||||
sheets.push((assembly.build.body, doc.color(entity.color)));
|
||||
}
|
||||
}
|
||||
Some(IgesModel { bodies, sheets })
|
||||
}
|
||||
@@ -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,14 @@ 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 tds_import;
|
||||
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
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
//! 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::{painted_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 }
|
||||
}
|
||||
|
||||
/// 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
|
||||
// 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.
|
||||
pub fn step_colour(rgb: crate::io::step_import::Rgb) -> Color {
|
||||
let [r, g, b] = rgb.map(|c| c.clamp(0.0, 1.0));
|
||||
let true_colour = Color::Rgb {
|
||||
r: (r * 255.0).round() as u8,
|
||||
g: (g * 255.0).round() as u8,
|
||||
b: (b * 255.0).round() as u8,
|
||||
};
|
||||
let max = r.max(g).max(b);
|
||||
let min = r.min(g).min(b);
|
||||
if max == 0.0 {
|
||||
return Color::Index(250);
|
||||
}
|
||||
let saturation = (max - min) / max;
|
||||
if saturation <= 0.1 {
|
||||
return if max >= 0.935 {
|
||||
Color::Index(7)
|
||||
} else if (0.16..0.395).contains(&max) {
|
||||
Color::Index(250)
|
||||
} else {
|
||||
true_colour
|
||||
};
|
||||
}
|
||||
if saturation < 0.75 || max < 0.825 {
|
||||
return true_colour;
|
||||
}
|
||||
let delta = max - min;
|
||||
let hue = if max == r {
|
||||
60.0 * ((g - b) / delta).rem_euclid(6.0)
|
||||
} else if max == g {
|
||||
60.0 * ((b - r) / delta + 2.0)
|
||||
} else {
|
||||
60.0 * ((r - g) / delta + 4.0)
|
||||
};
|
||||
let sector = (hue / 60.0).round();
|
||||
if (hue - 60.0 * sector).abs() > 7.5 {
|
||||
return true_colour;
|
||||
}
|
||||
// Red, yellow, green, cyan, blue, magenta.
|
||||
Color::Index([1, 2, 3, 4, 5, 6][sector as usize % 6])
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
let faces: Vec<Option<Color>> = body.faces.iter().map(|c| c.map(step_colour)).collect();
|
||||
painted_body_entity(&body.body, body.colour.map(step_colour), &faces, false)
|
||||
})
|
||||
.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 => {
|
||||
// Solids, then the surfaces as surfaces however flat; faces and
|
||||
// edges carry the object's colour.
|
||||
let model = crate::io::iges_import::read(bytes, drawing_mm)?;
|
||||
let solids = model.bodies.iter().map(|body| (body, false));
|
||||
let sheets = model.sheets.iter().map(|body| (body, true));
|
||||
Imported {
|
||||
blocks: Vec::new(),
|
||||
entities: solids
|
||||
.chain(sheets)
|
||||
.filter_map(|((body, color), sheet)| painted_body_entity(body, Some(*color), &[], sheet))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let solids = imported
|
||||
.blocks
|
||||
.iter()
|
||||
.flat_map(|(_, entities)| entities)
|
||||
.chain(&imported.entities)
|
||||
.any(|entity| !matches!(entity, EntityType::Insert(_)));
|
||||
solids.then_some(imported)
|
||||
}
|
||||
@@ -0,0 +1,929 @@
|
||||
//! 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,
|
||||
/// The colour each styled item (a solid, a shell or a face) is shown
|
||||
/// in, as 0–1 RGB.
|
||||
styles: HashMap<u64, [f64; 3]>,
|
||||
}
|
||||
|
||||
/// An RGB colour as 0–1 values.
|
||||
pub type Rgb = [f64; 3];
|
||||
|
||||
impl Doc {
|
||||
/// A presentation colour: an RGB one or a predefined one.
|
||||
fn colour(&self, p: &P) -> Option<Rgb> {
|
||||
let (kind, params) = self.simple(p.reference()?)?;
|
||||
match kind {
|
||||
"COLOUR_RGB" => Some([params.get(1)?.num()?, params.get(2)?.num()?, params.get(3)?.num()?]),
|
||||
"DRAUGHTING_PRE_DEFINED_COLOUR" => Some(match params.first()?.text()? {
|
||||
"red" => [1.0, 0.0, 0.0],
|
||||
"green" => [0.0, 1.0, 0.0],
|
||||
"blue" => [0.0, 0.0, 1.0],
|
||||
"yellow" => [1.0, 1.0, 0.0],
|
||||
"magenta" => [1.0, 0.0, 1.0],
|
||||
"cyan" => [0.0, 1.0, 1.0],
|
||||
"black" => [0.0, 0.0, 0.0],
|
||||
"white" => [1.0, 1.0, 1.0],
|
||||
_ => return None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour a styled item's presentation styles give it: its surface
|
||||
/// fill (or rendering) colour, or failing that its curve colour.
|
||||
fn style_colour(&self, styles: &P) -> Option<Rgb> {
|
||||
let mut stack: Vec<&P> = vec![styles];
|
||||
let mut seen = Vec::new();
|
||||
let mut curve = None;
|
||||
while let Some(p) = stack.pop() {
|
||||
match p {
|
||||
P::List(items) | P::Typed(items) => stack.extend(items.iter().rev()),
|
||||
P::Ref(id) if !seen.contains(id) => {
|
||||
seen.push(*id);
|
||||
let Some((kind, params)) = self.simple(*id) else { continue };
|
||||
match kind {
|
||||
"FILL_AREA_STYLE_COLOUR" => {
|
||||
if let Some(c) = params.get(1).and_then(|c| self.colour(c)) {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
"SURFACE_STYLE_RENDERING" | "SURFACE_STYLE_RENDERING_WITH_PROPERTIES" => {
|
||||
if let Some(c) = params.get(1).and_then(|c| self.colour(c)) {
|
||||
return Some(c);
|
||||
}
|
||||
}
|
||||
"CURVE_STYLE" => curve = curve.or_else(|| params.get(3).and_then(|c| self.colour(c))),
|
||||
_ => stack.extend(params.iter().rev()),
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
curve
|
||||
}
|
||||
|
||||
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)>,
|
||||
/// The file's face of each face built, in build order.
|
||||
faces: Vec<u64>,
|
||||
}
|
||||
|
||||
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()?;
|
||||
self.faces.push(id);
|
||||
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]];
|
||||
|
||||
/// A body with the colours the file shows it in.
|
||||
pub struct StepBody {
|
||||
pub body: kernel::brep::Body,
|
||||
/// The colour of the solid (or shell model) as a whole.
|
||||
pub colour: Option<Rgb>,
|
||||
/// Each face's own colour, in the body's face order.
|
||||
pub faces: Vec<Option<Rgb>>,
|
||||
}
|
||||
|
||||
/// One product: its own bodies and where each of its components sits.
|
||||
pub struct StepProduct {
|
||||
pub name: String,
|
||||
pub bodies: Vec<StepBody>,
|
||||
/// 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<StepBody> {
|
||||
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(),
|
||||
faces: Vec::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() {
|
||||
// The item's own style, or its (first) shell's.
|
||||
let colour = doc.styles.get(&id).copied().or_else(|| {
|
||||
lump.iter()
|
||||
.filter_map(|shell| shell.reference())
|
||||
.find_map(|shell| doc.styles.get(&shell).copied())
|
||||
});
|
||||
bodies.push(StepBody {
|
||||
body: assembly.build.body,
|
||||
colour,
|
||||
faces: assembly.faces.iter().map(|face| doc.styles.get(face).copied()).collect(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
styles: HashMap::new(),
|
||||
};
|
||||
|
||||
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;
|
||||
// Styled items: plain ones first, so an overriding style wins.
|
||||
let mut styled = Vec::new();
|
||||
for kind in ["STYLED_ITEM", "OVER_RIDING_STYLED_ITEM"] {
|
||||
for &id in &ids {
|
||||
if doc.kind(id) != kind {
|
||||
continue;
|
||||
}
|
||||
let Some((_, params)) = doc.simple(id) else { continue };
|
||||
let (Some(styles), Some(item)) = (params.get(1), params.get(2).and_then(P::reference)) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(colour) = doc.style_colour(styles) {
|
||||
styled.push((item, colour));
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.styles.extend(styled);
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
//! 3D Studio (.3ds) reading: the materials, meshes, lights and cameras of a
|
||||
//! file's editor section, and their conversion to drawing objects — each
|
||||
//! mesh a polyface mesh on a layer chosen by object, material, object colour
|
||||
//! or a single layer, with its material; each camera a named view.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codec::entities::{PolyfaceFace, PolyfaceMesh, PolyfaceVertex};
|
||||
use codec::types::Vector3;
|
||||
|
||||
pub struct TdsMaterial {
|
||||
pub name: String,
|
||||
pub ambient: Option<[u8; 3]>,
|
||||
pub diffuse: Option<[u8; 3]>,
|
||||
pub specular: Option<[u8; 3]>,
|
||||
}
|
||||
|
||||
pub struct TdsMesh {
|
||||
pub vertices: Vec<[f64; 3]>,
|
||||
pub faces: Vec<[usize; 3]>,
|
||||
/// Each material the mesh uses and the faces it covers, in file order.
|
||||
pub groups: Vec<(String, Vec<usize>)>,
|
||||
/// The object colour, a palette index, when the file gives one.
|
||||
pub color: Option<u8>,
|
||||
}
|
||||
|
||||
pub struct TdsCamera {
|
||||
pub position: [f64; 3],
|
||||
pub target: [f64; 3],
|
||||
pub lens: f64,
|
||||
}
|
||||
|
||||
pub enum TdsObject {
|
||||
Mesh(TdsMesh),
|
||||
Light,
|
||||
Camera(TdsCamera),
|
||||
}
|
||||
|
||||
impl TdsObject {
|
||||
/// The kind as the import window lists it.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
TdsObject::Mesh(_) => "Mesh",
|
||||
TdsObject::Light => "Light",
|
||||
TdsObject::Camera(_) => "Camera",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TdsScene {
|
||||
pub materials: Vec<TdsMaterial>,
|
||||
pub objects: Vec<(String, TdsObject)>,
|
||||
}
|
||||
|
||||
impl TdsScene {
|
||||
/// The materials a mesh uses, in the order the file defines them.
|
||||
pub fn mesh_materials(&self, mesh: &TdsMesh) -> Vec<String> {
|
||||
let mut used: Vec<String> = Vec::new();
|
||||
for m in &self.materials {
|
||||
if mesh.groups.iter().any(|(name, faces)| !faces.is_empty() && *name == m.name) {
|
||||
used.push(m.name.clone());
|
||||
}
|
||||
}
|
||||
for (name, faces) in &mesh.groups {
|
||||
if !faces.is_empty() && !used.contains(name) {
|
||||
used.push(name.clone());
|
||||
}
|
||||
}
|
||||
used
|
||||
}
|
||||
}
|
||||
|
||||
struct Reader<'a> {
|
||||
b: &'a [u8],
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
fn u16(&self, at: usize) -> Option<u16> {
|
||||
Some(u16::from_le_bytes([*self.b.get(at)?, *self.b.get(at + 1)?]))
|
||||
}
|
||||
fn u32(&self, at: usize) -> Option<u32> {
|
||||
Some(u32::from_le_bytes(self.b.get(at..at + 4)?.try_into().ok()?))
|
||||
}
|
||||
fn f32(&self, at: usize) -> Option<f64> {
|
||||
Some(f64::from(f32::from_le_bytes(self.b.get(at..at + 4)?.try_into().ok()?)))
|
||||
}
|
||||
fn point(&self, at: usize) -> Option<[f64; 3]> {
|
||||
Some([self.f32(at)?, self.f32(at + 4)?, self.f32(at + 8)?])
|
||||
}
|
||||
/// A zero-terminated string and the offset after it.
|
||||
fn cstr(&self, at: usize, end: usize) -> (String, usize) {
|
||||
let stop = self.b[at.min(end)..end]
|
||||
.iter()
|
||||
.position(|c| *c == 0)
|
||||
.map_or(end, |k| at + k);
|
||||
(
|
||||
String::from_utf8_lossy(&self.b[at.min(stop)..stop]).into_owned(),
|
||||
(stop + 1).min(end),
|
||||
)
|
||||
}
|
||||
/// The chunks between `from` and `to`: id, body start, body end.
|
||||
fn chunks(&self, from: usize, to: usize) -> Vec<(u16, usize, usize)> {
|
||||
let mut out = Vec::new();
|
||||
let mut at = from;
|
||||
while at + 6 <= to {
|
||||
let (Some(id), Some(len)) = (self.u16(at), self.u32(at + 2)) else { break };
|
||||
let len = len as usize;
|
||||
if len < 6 || at + len > to {
|
||||
break;
|
||||
}
|
||||
out.push((id, at + 6, at + len));
|
||||
at += len;
|
||||
}
|
||||
out
|
||||
}
|
||||
/// A colour chunk's 8-bit RGB: 24-bit or float, plain before linear.
|
||||
fn color(&self, from: usize, to: usize) -> Option<[u8; 3]> {
|
||||
let chunks = self.chunks(from, to);
|
||||
for want in [0x0011, 0x0010, 0x0012, 0x0013] {
|
||||
for &(id, start, _) in &chunks {
|
||||
if id != want {
|
||||
continue;
|
||||
}
|
||||
return Some(match id {
|
||||
0x0011 | 0x0012 => [*self.b.get(start)?, *self.b.get(start + 1)?, *self.b.get(start + 2)?],
|
||||
_ => self.point(start)?.map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8),
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `None` when the bytes are no 3D Studio file.
|
||||
pub fn read(bytes: &[u8]) -> Option<TdsScene> {
|
||||
let r = Reader { b: bytes };
|
||||
if r.u16(0)? != 0x4D4D {
|
||||
return None;
|
||||
}
|
||||
let end = (r.u32(2)? as usize).min(bytes.len());
|
||||
let mut scene = TdsScene {
|
||||
materials: Vec::new(),
|
||||
objects: Vec::new(),
|
||||
};
|
||||
for (id, start, stop) in r.chunks(6, end) {
|
||||
if id != 0x3D3D {
|
||||
continue;
|
||||
}
|
||||
for (id, start, stop) in r.chunks(start, stop) {
|
||||
match id {
|
||||
0xAFFF => {
|
||||
let mut material = TdsMaterial {
|
||||
name: String::new(),
|
||||
ambient: None,
|
||||
diffuse: None,
|
||||
specular: None,
|
||||
};
|
||||
for (id, s, e) in r.chunks(start, stop) {
|
||||
match id {
|
||||
0xA000 => material.name = r.cstr(s, e).0,
|
||||
0xA010 => material.ambient = r.color(s, e),
|
||||
0xA020 => material.diffuse = r.color(s, e),
|
||||
0xA030 => material.specular = r.color(s, e),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
scene.materials.push(material);
|
||||
}
|
||||
0x4000 => {
|
||||
let (name, body) = r.cstr(start, stop);
|
||||
for (id, s, e) in r.chunks(body, stop) {
|
||||
let object = match id {
|
||||
0x4100 => TdsObject::Mesh(read_mesh(&r, s, e)),
|
||||
0x4600 => TdsObject::Light,
|
||||
0x4700 => TdsObject::Camera(TdsCamera {
|
||||
position: r.point(s)?,
|
||||
target: r.point(s + 12)?,
|
||||
lens: r.f32(s + 28).filter(|l| *l > 0.0).unwrap_or(50.0),
|
||||
}),
|
||||
_ => continue,
|
||||
};
|
||||
scene.objects.push((name.clone(), object));
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(scene)
|
||||
}
|
||||
|
||||
fn read_mesh(r: &Reader<'_>, from: usize, to: usize) -> TdsMesh {
|
||||
let mut mesh = TdsMesh {
|
||||
vertices: Vec::new(),
|
||||
faces: Vec::new(),
|
||||
groups: Vec::new(),
|
||||
color: None,
|
||||
};
|
||||
for (id, s, e) in r.chunks(from, to) {
|
||||
match id {
|
||||
0x4110 => {
|
||||
let n = r.u16(s).unwrap_or(0) as usize;
|
||||
mesh.vertices = (0..n).filter_map(|k| r.point(s + 2 + 12 * k)).collect();
|
||||
}
|
||||
0x4120 => {
|
||||
let n = r.u16(s).unwrap_or(0) as usize;
|
||||
mesh.faces = (0..n)
|
||||
.filter_map(|k| {
|
||||
let at = s + 2 + 8 * k;
|
||||
Some([r.u16(at)? as usize, r.u16(at + 2)? as usize, r.u16(at + 4)? as usize])
|
||||
})
|
||||
.collect();
|
||||
for (id, gs, ge) in r.chunks(s + 2 + 8 * n, e) {
|
||||
if id == 0x4130 {
|
||||
let (name, at) = r.cstr(gs, ge);
|
||||
let count = r.u16(at).unwrap_or(0) as usize;
|
||||
let faces = (0..count).filter_map(|k| r.u16(at + 2 + 2 * k).map(usize::from)).collect();
|
||||
mesh.groups.push((name, faces));
|
||||
}
|
||||
}
|
||||
}
|
||||
0x4165 => mesh.color = r.b.get(s).copied(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
mesh
|
||||
}
|
||||
|
||||
/// Where imported objects go.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum LayerMode {
|
||||
#[default]
|
||||
ByObject,
|
||||
ByMaterial,
|
||||
ByObjectColor,
|
||||
Single,
|
||||
}
|
||||
|
||||
/// What a mesh with several materials becomes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MultiMaterial {
|
||||
/// One mesh per material.
|
||||
Split,
|
||||
/// One mesh with the first material the file defines.
|
||||
First,
|
||||
/// One mesh with this material, or none.
|
||||
Material(Option<String>),
|
||||
}
|
||||
|
||||
pub struct TdsOptions {
|
||||
/// The objects to import, by name ("Global Settings" included).
|
||||
pub selected: Vec<String>,
|
||||
pub layers: LayerMode,
|
||||
/// The choice for each mesh with several materials, by name.
|
||||
pub multi: HashMap<String, MultiMaterial>,
|
||||
/// Whether meshes keep only the vertices their faces use, in the order
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// One imported mesh.
|
||||
pub struct TdsPart {
|
||||
pub layer: String,
|
||||
pub material: Option<String>,
|
||||
pub mesh: PolyfaceMesh,
|
||||
}
|
||||
|
||||
pub struct TdsCameraView {
|
||||
pub name: String,
|
||||
pub target: [f64; 3],
|
||||
pub direction: [f64; 3],
|
||||
/// The view height at the target, from the lens.
|
||||
pub height: f64,
|
||||
pub lens: f64,
|
||||
}
|
||||
|
||||
pub struct TdsImport {
|
||||
pub parts: Vec<TdsPart>,
|
||||
/// The materials the parts use, in the order they were first used.
|
||||
pub materials: Vec<String>,
|
||||
pub views: Vec<TdsCameraView>,
|
||||
pub messages: Vec<String>,
|
||||
}
|
||||
|
||||
/// A polyface mesh 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();
|
||||
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 &f in faces {
|
||||
let Some(face) = mesh.faces.get(f) else { continue };
|
||||
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]));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The view height a camera's lens sees at the target distance.
|
||||
// ponytail: one measured camera (50 mm at 86.46 units gives 30.457); the
|
||||
// height is taken as proportional to distance over lens.
|
||||
fn view_height(distance: f64, lens: f64) -> f64 {
|
||||
distance * 17.6141 / lens
|
||||
}
|
||||
|
||||
pub fn convert(scene: &TdsScene, options: &TdsOptions) -> TdsImport {
|
||||
let mut out = TdsImport {
|
||||
parts: Vec::new(),
|
||||
materials: Vec::new(),
|
||||
views: Vec::new(),
|
||||
messages: Vec::new(),
|
||||
};
|
||||
let chosen = |name: &str| options.selected.iter().any(|s| s == name);
|
||||
// The objects are taken in the order the window lists them, by name.
|
||||
let mut objects: Vec<&(String, TdsObject)> = scene.objects.iter().collect();
|
||||
objects.sort_by_key(|(name, _)| name.to_lowercase());
|
||||
for (name, object) in objects {
|
||||
if !chosen(name) {
|
||||
continue;
|
||||
}
|
||||
match object {
|
||||
TdsObject::Mesh(mesh) => {
|
||||
out.messages.push(format!("Processing object {name}"));
|
||||
let materials = scene.mesh_materials(mesh);
|
||||
let all: Vec<usize> = (0..mesh.faces.len()).collect();
|
||||
let pieces: Vec<(Option<String>, Vec<usize>)> = if materials.len() > 1 {
|
||||
match options.multi.get(name).cloned().unwrap_or(MultiMaterial::Split) {
|
||||
MultiMaterial::Split => materials
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let faces = mesh
|
||||
.groups
|
||||
.iter()
|
||||
.filter(|(g, _)| g == m)
|
||||
.flat_map(|(_, f)| f.iter().copied())
|
||||
.collect();
|
||||
(Some(m.clone()), faces)
|
||||
})
|
||||
.collect(),
|
||||
MultiMaterial::First => vec![(materials.first().cloned(), all)],
|
||||
MultiMaterial::Material(m) => vec![(m, all)],
|
||||
}
|
||||
} else {
|
||||
vec![(materials.first().cloned(), all)]
|
||||
};
|
||||
let compact = options.compact || pieces.len() > 1;
|
||||
for (material, faces) in pieces {
|
||||
if let Some(m) = &material {
|
||||
if !out.materials.contains(m) {
|
||||
out.messages.push(format!("Converting material {m}"));
|
||||
out.materials.push(m.clone());
|
||||
}
|
||||
}
|
||||
let layer = match options.layers {
|
||||
LayerMode::ByObject => name.to_uppercase(),
|
||||
LayerMode::ByMaterial => material.as_deref().unwrap_or("0").to_uppercase(),
|
||||
LayerMode::ByObjectColor => match mesh.color {
|
||||
Some(c) => format!("COLOR{c:02}"),
|
||||
None => "COLORNONE".to_string(),
|
||||
},
|
||||
LayerMode::Single => "AVLAYER".to_string(),
|
||||
};
|
||||
out.parts.push(TdsPart {
|
||||
layer,
|
||||
material,
|
||||
mesh: polyface(mesh, &faces, compact),
|
||||
});
|
||||
}
|
||||
}
|
||||
TdsObject::Camera(camera) => {
|
||||
let name = name.to_uppercase();
|
||||
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();
|
||||
out.views.push(TdsCameraView {
|
||||
name,
|
||||
target: camera.target,
|
||||
direction,
|
||||
height: view_height(distance, camera.lens),
|
||||
lens: camera.lens,
|
||||
});
|
||||
}
|
||||
TdsObject::Light => {}
|
||||
}
|
||||
}
|
||||
out.messages.push("3D Studio file import completed".to_string());
|
||||
out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
//! 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, arcs and pies on whole units, text as
|
||||
//! single-line text placed and sized by its font's own ascent. Chords are
|
||||
//! not read.
|
||||
//!
|
||||
//! 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 font face as the text uses it: its name, and whether bold and italic.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Face {
|
||||
pub name: String,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
}
|
||||
|
||||
/// A picture: its objects, in block coordinates, and the font of each
|
||||
/// text (an index into `fonts`).
|
||||
pub struct Picture {
|
||||
pub entities: Vec<EntityType>,
|
||||
/// Each face the text uses, in the order first used; each becomes a
|
||||
/// text style.
|
||||
pub fonts: Vec<Face>,
|
||||
/// 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 {
|
||||
/// The logical height: negative for the character height, positive for
|
||||
/// the cell height.
|
||||
height: f64,
|
||||
escapement: f64,
|
||||
weight: i32,
|
||||
italic: bool,
|
||||
face: String,
|
||||
}
|
||||
|
||||
/// A font's ascent, in metafile units, as the system lays the font out at
|
||||
/// its logical height: the baseline sits this far below a text's reference
|
||||
/// point. Without the system's font engine it is nine tenths of the height.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn ascent(font: &Font) -> f64 {
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
CreateCompatibleDC, CreateFontIndirectW, DeleteDC, DeleteObject, GetTextMetricsW,
|
||||
SelectObject, LOGFONTW, TEXTMETRICW,
|
||||
};
|
||||
// SAFETY: a private memory device context and font, released before
|
||||
// returning; the structures are plain data the calls fill.
|
||||
unsafe {
|
||||
let mut logfont: LOGFONTW = std::mem::zeroed();
|
||||
logfont.lfHeight = font.height as i32;
|
||||
logfont.lfWeight = font.weight;
|
||||
logfont.lfItalic = u8::from(font.italic);
|
||||
for (slot, unit) in logfont
|
||||
.lfFaceName
|
||||
.iter_mut()
|
||||
.zip(font.face.encode_utf16().take(31))
|
||||
{
|
||||
*slot = unit;
|
||||
}
|
||||
let dc = CreateCompatibleDC(std::ptr::null_mut());
|
||||
if dc.is_null() {
|
||||
return 0.9 * font.height.abs();
|
||||
}
|
||||
let handle = CreateFontIndirectW(&logfont);
|
||||
let previous = SelectObject(dc, handle);
|
||||
let mut metrics: TEXTMETRICW = std::mem::zeroed();
|
||||
let ok = GetTextMetricsW(dc, &mut metrics) != 0;
|
||||
SelectObject(dc, previous);
|
||||
DeleteObject(handle);
|
||||
DeleteDC(dc);
|
||||
if ok {
|
||||
f64::from(metrics.tmAscent)
|
||||
} else {
|
||||
0.9 * font.height.abs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn ascent(font: &Font) -> f64 {
|
||||
0.9 * font.height.abs()
|
||||
}
|
||||
|
||||
#[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<Face>,
|
||||
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()
|
||||
}
|
||||
|
||||
/// Turns an arc is cut into: its points lie on whole multiples of this
|
||||
/// share of a turn.
|
||||
const ARC_STEPS: i64 = 99;
|
||||
|
||||
/// The angle of a radial point round an ellipse, in degrees in `(0, 360]`
|
||||
/// counter-clockwise in drawing orientation — or `None` for a point inside
|
||||
/// the ellipse, which marks no angle.
|
||||
fn radial_angle(bounds: [f64; 4], x: f64, y: f64) -> Option<f64> {
|
||||
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 (u, v) = ((x - cx) / rx, -(y - cy) / ry);
|
||||
if u.hypot(v) < 1.0 {
|
||||
return None;
|
||||
}
|
||||
let degrees = v.atan2(u).to_degrees().rem_euclid(360.0);
|
||||
Some(if degrees == 0.0 { 360.0 } else { degrees })
|
||||
}
|
||||
|
||||
/// An arc as the reference reader plays it: points round the ellipse's
|
||||
/// whole-unit centre and radii at whole multiples of 1/99 turn, rounded
|
||||
/// down to whole units, from the start angle up to the end angle — past
|
||||
/// 360 degrees back from zero when the end comes first. A start inside the
|
||||
/// ellipse starts at zero; an end inside it runs on to a whole turn; with
|
||||
/// both inside there is no arc.
|
||||
fn arc_points(bounds: [f64; 4], start: [f64; 2], end: [f64; 2]) -> Vec<[f64; 2]> {
|
||||
let [left, top, right, bottom] = bounds.map(|v| v as i64);
|
||||
let (cx, cy) = ((left + right).div_euclid(2), (top + bottom).div_euclid(2));
|
||||
let (rx, ry) = ((right - left).abs() / 2, (bottom - top).abs() / 2);
|
||||
let (from, to) = (radial_angle(bounds, start[0], start[1]), radial_angle(bounds, end[0], end[1]));
|
||||
let (start_angle, end_angle) = match (from, to) {
|
||||
(None, None) => return Vec::new(),
|
||||
(None, to) => (0.0, to),
|
||||
(Some(from), to) => (from, to.filter(|to| *to != from)),
|
||||
};
|
||||
// The angles are stepped out from zero one step at a time in degrees,
|
||||
// rounding and all, as the points come out.
|
||||
let mut degrees = vec![0.0f64; ARC_STEPS as usize + 1];
|
||||
for k in 1..degrees.len() {
|
||||
degrees[k] = degrees[k - 1] + 360.0 / ARC_STEPS as f64;
|
||||
}
|
||||
let point = |k: i64| {
|
||||
let a = degrees[k as usize] * (std::f64::consts::PI / 180.0);
|
||||
[
|
||||
(cx as f64 + rx as f64 * a.cos()).floor(),
|
||||
(cy as f64 + ry as f64 * -a.sin()).floor(),
|
||||
]
|
||||
};
|
||||
// `k` steps lie below an angle when k·360 < angle·99, kept exact.
|
||||
let under = |k: &i64, angle: f64| ((k * 360) as f64) < angle * ARC_STEPS as f64;
|
||||
let first = (start_angle * ARC_STEPS as f64 / 360.0).ceil() as i64;
|
||||
let mut steps: Vec<i64> = match end_angle {
|
||||
// Up to the end.
|
||||
Some(to) if to > start_angle => (first..).take_while(|k| under(k, to)).collect(),
|
||||
// Round to a whole turn, then from zero up to the end.
|
||||
Some(to) => (first..=ARC_STEPS).chain((0..).take_while(|k| under(k, to))).collect(),
|
||||
// Round to a whole turn.
|
||||
None => (first..=ARC_STEPS).collect(),
|
||||
};
|
||||
// The whole-unit points next to the start and end rays are judged by
|
||||
// which side of the ray they fall: a point on the start ray is left
|
||||
// out, one on the end ray kept.
|
||||
let (fx, fy) = (0.5 * (bounds[0] + bounds[2]), 0.5 * (bounds[1] + bounds[3]));
|
||||
let side = |ray: [f64; 2], k: i64| {
|
||||
let p = point(k);
|
||||
let (ux, uy) = (ray[0] - fx, -(ray[1] - fy));
|
||||
let (vx, vy) = (p[0] - fx, -(p[1] - fy));
|
||||
ux * vy - uy * vx
|
||||
};
|
||||
if from.is_some() {
|
||||
if let Some(&k) = steps.first() {
|
||||
if k < ARC_STEPS && side(start, k) <= 0.0 {
|
||||
steps.remove(0);
|
||||
} else if k > 0 && k < ARC_STEPS && side(start, k - 1) > 0.0 {
|
||||
steps.insert(0, k - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if end_angle.is_some() {
|
||||
if let Some(&k) = steps.last() {
|
||||
if k > 0 && side(end, k) > 0.0 {
|
||||
steps.pop();
|
||||
} else if k + 1 < ARC_STEPS && side(end, k + 1) <= 0.0 {
|
||||
steps.push(k + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
steps.into_iter().map(point).collect()
|
||||
}
|
||||
|
||||
/// 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,
|
||||
weight: 400,
|
||||
italic: false,
|
||||
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),
|
||||
escapement: s(2) / 10.0,
|
||||
weight: s(4) as i32,
|
||||
italic: p.get(5).is_some_and(|w| w & 0xff != 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, PIE: (yEnd, xEnd, yStart, xStart, bottom, right, top,
|
||||
// left), drawn counter-clockwise; a pie's outline starts at the
|
||||
// centre. (CHORD records are not read.)
|
||||
0x0817 | 0x081A => {
|
||||
let bounds = [s(7), s(6), s(5), s(4)];
|
||||
let arc = arc_points(bounds, [s(3), s(2)], [s(1), s(0)]);
|
||||
if arc.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if *function == 0x0817 {
|
||||
if !pen.null {
|
||||
player.polyline(&arc, false, pen.width, pen.rgb);
|
||||
}
|
||||
} else {
|
||||
let centre = [
|
||||
((bounds[0] + bounds[2]) as i64).div_euclid(2) as f64,
|
||||
((bounds[1] + bounds[3]) as i64).div_euclid(2) as f64,
|
||||
];
|
||||
let ring: Vec<[f64; 2]> = std::iter::once(centre).chain(arc).collect();
|
||||
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
|
||||
// the font's ascent below it, and the text is four fifths
|
||||
// of that ascent tall.
|
||||
let rotation = font.escapement.to_radians();
|
||||
let drop = ascent(&font);
|
||||
let base = player.at(x + drop * rotation.sin(), y + drop * rotation.cos());
|
||||
let face = Face {
|
||||
name: font.face.clone(),
|
||||
bold: font.weight >= 600,
|
||||
italic: font.italic,
|
||||
};
|
||||
let index = match player.fonts.iter().position(|f| *f == face) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
player.fonts.push(face);
|
||||
player.fonts.len() - 1
|
||||
}
|
||||
};
|
||||
let mut text = Text::new();
|
||||
text.value = value;
|
||||
text.insertion_point = base;
|
||||
text.height = 0.8 * drop * 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,
|
||||
})
|
||||
}
|
||||
@@ -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")),
|
||||
|
||||
@@ -9,6 +9,7 @@ mod content_browser;
|
||||
pub(crate) mod create_block;
|
||||
mod design_center;
|
||||
pub(crate) mod dgn_import;
|
||||
pub(crate) mod tds_import;
|
||||
pub(crate) mod edit_block;
|
||||
pub(crate) mod image_transparency;
|
||||
pub(crate) mod insert_block;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// 3D Studio import: when the import is set to ask what a mesh with several
|
||||
// materials becomes, this asks for each such mesh in turn.
|
||||
//
|
||||
// <name> has multiple materials assigned. [Split/First/Material] <Split>:
|
||||
// Select a material or [None] <None>:
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||
use crate::io::tds_import::{LayerMode, MultiMaterial};
|
||||
|
||||
/// What one 3D Studio import reads and how.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TdsImportRequest {
|
||||
pub path: String,
|
||||
/// The objects to import, by name ("Global Settings" included).
|
||||
pub selected: Vec<String>,
|
||||
pub layers: LayerMode,
|
||||
/// The choice for each mesh with several materials, by name.
|
||||
pub multi: HashMap<String, MultiMaterial>,
|
||||
/// Meshes keep only the vertices their faces use (the import splits or
|
||||
/// asks), else the file's vertex list.
|
||||
pub compact: bool,
|
||||
}
|
||||
|
||||
pub struct TdsMaterialCommand {
|
||||
request: TdsImportRequest,
|
||||
/// The meshes still to ask about, with their materials.
|
||||
queue: Vec<(String, Vec<String>)>,
|
||||
choosing: bool,
|
||||
}
|
||||
|
||||
impl TdsMaterialCommand {
|
||||
pub fn new(request: TdsImportRequest, queue: Vec<(String, Vec<String>)>) -> Self {
|
||||
Self {
|
||||
request,
|
||||
queue,
|
||||
choosing: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn answer(&mut self, choice: MultiMaterial) -> CmdResult {
|
||||
if self.queue.is_empty() {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
let (name, _) = self.queue.remove(0);
|
||||
self.request.multi.insert(name, choice);
|
||||
self.choosing = false;
|
||||
if self.queue.is_empty() {
|
||||
CmdResult::TdsImport(self.request.clone())
|
||||
} else {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TdsMaterialCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"IMPORT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match (self.choosing, self.queue.first()) {
|
||||
(true, _) => "Select a material or [None] <None>:".to_string(),
|
||||
(false, Some((name, _))) => {
|
||||
format!("{name} has multiple materials assigned. [Split/First/Material] <Split>:")
|
||||
}
|
||||
(false, None) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.choosing {
|
||||
let mut options = vec![CmdOption::new("None", "N")];
|
||||
if let Some((_, materials)) = self.queue.first() {
|
||||
options.extend(materials.iter().map(|m| CmdOption::new(m, m)));
|
||||
}
|
||||
options
|
||||
} else {
|
||||
vec![
|
||||
CmdOption::new("Split", "S"),
|
||||
CmdOption::new("First", "F"),
|
||||
CmdOption::new("Material", "M"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
if self.choosing {
|
||||
InputKind::FreeText
|
||||
} else {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: glam::DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let text = text.trim();
|
||||
if self.choosing {
|
||||
if text.is_empty() || text.eq_ignore_ascii_case("None") || text.eq_ignore_ascii_case("N") {
|
||||
return Some(self.answer(MultiMaterial::Material(None)));
|
||||
}
|
||||
let found = self
|
||||
.queue
|
||||
.first()
|
||||
.and_then(|(_, materials)| materials.iter().find(|m| m.eq_ignore_ascii_case(text)))
|
||||
.cloned();
|
||||
return Some(match found {
|
||||
Some(material) => self.answer(MultiMaterial::Material(Some(material))),
|
||||
None => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
});
|
||||
}
|
||||
let t = text.to_ascii_uppercase();
|
||||
Some(match t.as_str() {
|
||||
"" => self.answer(MultiMaterial::Split),
|
||||
_ if "SPLIT".starts_with(&t) => self.answer(MultiMaterial::Split),
|
||||
_ if "FIRST".starts_with(&t) => self.answer(MultiMaterial::First),
|
||||
_ if "MATERIAL".starts_with(&t) => {
|
||||
self.choosing = true;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.on_text_input("").unwrap_or(CmdResult::NeedPoint)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1784,6 +1795,21 @@ impl Scene {
|
||||
fold!(Some((lo.min(hi), lo.max(hi))));
|
||||
}
|
||||
}
|
||||
// Solids inside blocks draw as one mesh instance per insert, placed
|
||||
// where the insert puts them: that is the content the skipped insert
|
||||
// marker stands for.
|
||||
for set in self.instanced_block_meshes(
|
||||
block,
|
||||
None,
|
||||
scale,
|
||||
self.annotation_all_visible(),
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
if let Some([x0, y0, z0, x1, y1, z1]) = set.instance_aabb {
|
||||
fold!(Some((glam::DVec3::new(x0, y0, z0), glam::DVec3::new(x1, y1, z1))));
|
||||
}
|
||||
}
|
||||
|
||||
// Raster images, OLE frames and underlays draw as GPU quads; with
|
||||
// their frames hidden they carry no wire outline at all.
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
//! 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> {
|
||||
entity_as(document, class(document)?)
|
||||
}
|
||||
|
||||
fn entity_as(document: &SatDocument, class: Class) -> Option<EntityType> {
|
||||
Some(match class {
|
||||
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())
|
||||
}
|
||||
|
||||
/// The value of an ACIS colour attribute: the index (0 by block), or the
|
||||
/// index nearest a true colour.
|
||||
fn attribute_index(color: codec::types::Color) -> i64 {
|
||||
match color {
|
||||
codec::types::Color::Index(index) => i64::from(index),
|
||||
codec::types::Color::Rgb { r, g, b } => i64::from(codec::types::nearest_aci(r, g, b)),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Hangs colour attributes on a record: a true colour first, then its
|
||||
/// index, as imported models carry them.
|
||||
fn paint_record(document: &mut codec::entities::acis::SatDocument, owner: usize, color: codec::types::Color) {
|
||||
use codec::entities::acis::{SatPointer, SatRecord, SatToken};
|
||||
let mut chain: Vec<(&str, i64)> = Vec::new();
|
||||
if let codec::types::Color::Rgb { r, g, b } = color {
|
||||
let packed = (0xC2u32 << 24) | (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b);
|
||||
chain.push(("truecolor-adesk-attrib", i64::from(packed as i32)));
|
||||
}
|
||||
chain.push(("color-adesk-attrib", attribute_index(color)));
|
||||
let Some(existing) = document.record(owner).map(|record| record.attribute) else {
|
||||
return;
|
||||
};
|
||||
let first = document.record_count() as i32;
|
||||
let count = chain.len() as i32;
|
||||
for (k, (kind, value)) in chain.into_iter().enumerate() {
|
||||
let at = first + k as i32;
|
||||
let next = if k as i32 + 1 < count { SatPointer::new(at + 1) } else { existing };
|
||||
let previous = if k == 0 { SatPointer::NULL } else { SatPointer::new(at - 1) };
|
||||
let mut record = SatRecord::new(-1, kind);
|
||||
record.tokens = vec![
|
||||
SatToken::Pointer(next),
|
||||
SatToken::Pointer(previous),
|
||||
SatToken::Pointer(SatPointer::new(owner as i32)),
|
||||
SatToken::Integer(value),
|
||||
];
|
||||
document.add_record(record);
|
||||
}
|
||||
if let Some(record) = document.record_mut(owner) {
|
||||
record.attribute = SatPointer::new(first);
|
||||
}
|
||||
}
|
||||
|
||||
/// A kernel body as its drawing object in `colour` (by block when `None`),
|
||||
/// with the faces given a colour of their own carrying it — and the edges
|
||||
/// bounding them too, as the reference modeler stores them. A `sheet` is a
|
||||
/// SURFACE however flat it is.
|
||||
pub fn painted_body_entity(
|
||||
body: &kernel::brep::Body,
|
||||
colour: Option<codec::types::Color>,
|
||||
faces: &[Option<codec::types::Color>],
|
||||
sheet: bool,
|
||||
) -> Option<EntityType> {
|
||||
let mut document = super::acis_export::solid_to_sat(body)?;
|
||||
let base = colour.unwrap_or(codec::types::Color::ByBlock);
|
||||
let own: std::collections::HashMap<kernel::brep::FaceKey, codec::types::Color> = body
|
||||
.faces
|
||||
.keys()
|
||||
.zip(faces)
|
||||
.filter_map(|(key, color)| Some((key, (*color)?)))
|
||||
.collect();
|
||||
// The writer puts faces and edges in the body's own order.
|
||||
let face_records: Vec<usize> = (0..document.record_count())
|
||||
.filter(|&at| document.record(at).is_some_and(|r| r.entity_type == "face"))
|
||||
.collect();
|
||||
let edge_records: Vec<usize> = (0..document.record_count())
|
||||
.filter(|&at| document.record(at).is_some_and(|r| r.entity_type == "edge"))
|
||||
.collect();
|
||||
for (key, at) in body.faces.keys().zip(face_records) {
|
||||
if let Some(color) = own.get(&key) {
|
||||
paint_record(&mut document, at, *color);
|
||||
}
|
||||
}
|
||||
for ((_, edge), at) in body.edges.iter().zip(edge_records) {
|
||||
let bounding = edge.coedges.iter().find_map(|coedge| {
|
||||
let ring = body.coedges.get(*coedge)?.owner;
|
||||
own.get(&body.loops.get(ring)?.owner).copied()
|
||||
});
|
||||
if let Some(color) = bounding {
|
||||
paint_record(&mut document, at, color);
|
||||
}
|
||||
}
|
||||
let mut entity = if sheet {
|
||||
entity_as(&document, Class::Surface)?
|
||||
} else {
|
||||
entity(&document)?
|
||||
};
|
||||
entity.common_mut().color = base;
|
||||
Some(entity)
|
||||
}
|
||||
@@ -70,13 +70,24 @@ pub fn tessellate_sat(
|
||||
(owner >= 0 && handle > 0).then(|| (owner, codec::Handle::new(handle as u64)))
|
||||
})
|
||||
.collect();
|
||||
// A true colour attribute outranks the index one beside it, which only
|
||||
// holds its nearest index.
|
||||
let face_colors: std::collections::HashMap<i32, [f32; 4]> = document
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| record.entity_type == "color-adesk-attrib")
|
||||
.chain(
|
||||
document
|
||||
.records
|
||||
.iter()
|
||||
.filter(|record| record.entity_type == "truecolor-adesk-attrib"),
|
||||
)
|
||||
.filter_map(|record| {
|
||||
let owner = record.token_pointer(2)?.0;
|
||||
let value = record.token(3)?.as_integer()?;
|
||||
let mut value = record.token(3)?.as_integer()?;
|
||||
if record.entity_type == "truecolor-adesk-attrib" {
|
||||
value = (value & 0x00FF_FFFF) | 0x0100_0000;
|
||||
}
|
||||
let source = if (1..=255).contains(&value) {
|
||||
codec::Color::from_index(value as i16)
|
||||
} else if value > 257 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+12
-4
@@ -1349,6 +1349,9 @@ impl Scene {
|
||||
.map_err(|e| e.to_string())?;
|
||||
entity_handles.push(handle);
|
||||
}
|
||||
// Solids, surfaces and meshes of a definition draw through their
|
||||
// instanced block meshes, which only a tessellation pass creates.
|
||||
self.refresh_meshes_for_handles(&entity_handles);
|
||||
// Block defns don't render on their own, but the geometry cache must
|
||||
// pick up the new definition so the interactive insert can preview it.
|
||||
self.bump_geometry();
|
||||
@@ -1391,12 +1394,14 @@ impl Scene {
|
||||
block_end.common.owner_handle = br_handle;
|
||||
let _ = self.document.add_entity(EntityType::BlockEnd(block_end));
|
||||
|
||||
let mut added = Vec::new();
|
||||
for mut entity in entities {
|
||||
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = br_handle;
|
||||
let _ = self.document.add_entity(entity);
|
||||
added.extend(self.document.add_entity(entity).ok());
|
||||
}
|
||||
self.refresh_meshes_for_handles(&added);
|
||||
self.bump_geometry();
|
||||
}
|
||||
|
||||
@@ -1446,18 +1451,21 @@ impl Scene {
|
||||
})
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
for h in owned {
|
||||
self.document.remove_entity(h);
|
||||
for h in &owned {
|
||||
self.document.remove_entity(*h);
|
||||
}
|
||||
if let Some(EntityType::Block(block)) = self.document.get_entity_mut(block_handle) {
|
||||
block.base_point = base_point;
|
||||
}
|
||||
// The old contents' meshes go with them; the new ones are built.
|
||||
let mut changed = owned;
|
||||
for mut entity in entities {
|
||||
Self::reset_clone_subhandles(&mut self.document, &mut entity);
|
||||
entity.common_mut().handle = Handle::NULL;
|
||||
entity.common_mut().owner_handle = br_handle;
|
||||
let _ = self.document.add_entity(entity);
|
||||
changed.extend(self.document.add_entity(entity).ok());
|
||||
}
|
||||
self.refresh_meshes_for_handles(&changed);
|
||||
self.bump_geometry();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -292,6 +292,22 @@ fn face_id_weighted(family: &str, weight: fontdb::Weight) -> Option<fontdb::ID>
|
||||
fonts().db.lock().unwrap().query(&query)
|
||||
}
|
||||
|
||||
/// The file name (lower case) of the installed face of exactly `family` —
|
||||
/// no stand-in family — in the bold and italic asked for, or `None`.
|
||||
pub fn face_file(family: &str, bold: bool, italic: bool) -> Option<String> {
|
||||
let db = fonts().db.lock().unwrap();
|
||||
let id = db.query(&fontdb::Query {
|
||||
families: &[fontdb::Family::Name(family)],
|
||||
weight: if bold { fontdb::Weight::BOLD } else { fontdb::Weight::NORMAL },
|
||||
style: if italic { fontdb::Style::Italic } else { fontdb::Style::Normal },
|
||||
..Default::default()
|
||||
})?;
|
||||
match &db.face(id)?.source {
|
||||
fontdb::Source::File(path) => Some(path.file_name()?.to_string_lossy().to_ascii_lowercase()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrow the raw face bytes for `family` and run `f` over them. The byte slice
|
||||
/// is only valid inside the closure, so callers extract everything they need
|
||||
/// (e.g. flattened glyph outlines) before returning. `index` is the face index
|
||||
|
||||
+245
-19
@@ -1,6 +1,8 @@
|
||||
//! Import File (IMPORT: the format first, then the file, routed to the
|
||||
//! format's importer) and DGN Import (DGNIMPORT: model, target drawing,
|
||||
//! conversion units and the mapping setup's effect on the file).
|
||||
//! format's importer), DGN Import (DGNIMPORT: model, target drawing,
|
||||
//! conversion units and the mapping setup's effect on the file) and 3D Studio
|
||||
//! Import (the objects of a .3ds file to bring in, their layers and what a
|
||||
//! mesh with several materials becomes).
|
||||
|
||||
use iced::widget::{button, column, container, image, row, scrollable, text, text_input, Space};
|
||||
use iced::{Background, Border, Element, Fill, Length, Theme};
|
||||
@@ -34,6 +36,17 @@ pub enum ImportDialogMsg {
|
||||
DgnExplode(bool),
|
||||
DgnMappingTab(usize),
|
||||
DgnOk,
|
||||
// 3D Studio Import
|
||||
TdsChange,
|
||||
TdsPicked(Option<String>),
|
||||
TdsCheck(usize, bool),
|
||||
TdsAll,
|
||||
TdsClear,
|
||||
TdsFilter(usize),
|
||||
TdsSearch(String),
|
||||
TdsLayers(usize),
|
||||
TdsMulti(usize),
|
||||
TdsOk,
|
||||
Help(&'static str),
|
||||
}
|
||||
|
||||
@@ -50,10 +63,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 +96,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("3D Studio", "*.3ds", &["3ds"], 1, Support::No),
|
||||
format("ACIS", "*.sat", &["sat"], 1, Support::AcisSolverOnly),
|
||||
format("Metafile", "*.wmf", &["wmf"], 0, Support::Yes),
|
||||
format("3D Studio", "*.3ds", &["3ds"], 1, Support::Yes),
|
||||
format("ACIS", "*.sat", &["sat"], 1, Support::Yes),
|
||||
format(
|
||||
"CATIA V4",
|
||||
"*.model; *.session; *.exp; *.dlv3",
|
||||
@@ -104,7 +113,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 +130,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),
|
||||
];
|
||||
|
||||
@@ -150,11 +159,29 @@ pub struct DgnImportState {
|
||||
pub mapping: [Vec<(String, String)>; 4],
|
||||
}
|
||||
|
||||
/// Both windows' working copies and the files imported this session.
|
||||
/// The objects of a 3D Studio file and the import's choices.
|
||||
pub struct TdsImportState {
|
||||
pub path: String,
|
||||
/// "Global Settings" first, then the file's objects by name: each name,
|
||||
/// its kind (Mesh, Light, Camera or Global) and the materials it uses.
|
||||
pub objects: Vec<(String, &'static str, usize)>,
|
||||
pub materials: usize,
|
||||
pub checked: Vec<bool>,
|
||||
/// 0 all, 1 meshes, 2 lights, 3 cameras, 4 global.
|
||||
pub filter: usize,
|
||||
pub search: String,
|
||||
/// 0 by object, 1 by material, 2 by object colour, 3 single layer.
|
||||
pub layers: usize,
|
||||
/// 0 always ask, 1 split by material, 2 first material, 3 no material.
|
||||
pub multi: usize,
|
||||
}
|
||||
|
||||
/// The windows' working copies and the files imported this session.
|
||||
#[derive(Default)]
|
||||
pub struct ImportDialogs {
|
||||
pub file: Option<ImportFileState>,
|
||||
pub dgn: Option<DgnImportState>,
|
||||
pub tds: Option<TdsImportState>,
|
||||
pub recent: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -178,13 +205,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 +234,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.")),
|
||||
}
|
||||
}
|
||||
@@ -425,6 +445,212 @@ pub fn view_import_file<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
/// One choice out of a few, as buttons stacked one above the other.
|
||||
fn stacked<'a>(options: Vec<(String, bool, Message)>) -> Element<'a, Message> {
|
||||
let buttons = options.into_iter().map(|(label, on, message)| {
|
||||
button(text(label).size(11).width(Fill))
|
||||
.on_press(message)
|
||||
.style(button_style(on))
|
||||
.padding([5, 10])
|
||||
.width(Fill)
|
||||
.into()
|
||||
});
|
||||
container(iced::widget::Column::with_children(buttons.collect::<Vec<_>>()).spacing(2))
|
||||
.padding(2)
|
||||
.width(Fill)
|
||||
.style(well_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The kind of a 3D Studio object in the window's words.
|
||||
fn tds_kind(kind: &str) -> String {
|
||||
match kind {
|
||||
"Mesh" => crate::tr!("import-3ds", "mesh"),
|
||||
"Light" => crate::tr!("import-3ds", "light"),
|
||||
"Camera" => crate::tr!("import-3ds", "camera"),
|
||||
_ => crate::tr!("import-3ds", "general"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_tds_import<'a>(
|
||||
state: &'a TdsImportState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let top = container(
|
||||
row![
|
||||
container(text(file_name(&state.path)).size(12))
|
||||
.padding([4, 10])
|
||||
.style(|theme: &Theme| container::Style {
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
radius: 12.0.into(),
|
||||
color: theme.palette().primary.base.color,
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
text(crate::tr!("import-3ds", "summary", objects = (state.objects.len().saturating_sub(1) as i64), materials = (state.materials as i64)))
|
||||
.size(11)
|
||||
.style(muted_style)
|
||||
.width(Fill),
|
||||
button(text(crate::tr!("import-3ds", "change")).size(11))
|
||||
.on_press(msg(ImportDialogMsg::TdsChange))
|
||||
.style(button::text)
|
||||
.padding([4, 6]),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.padding([8, 12])
|
||||
.width(Fill)
|
||||
.style(crate::ui::window::pdf_dialogs::card_style);
|
||||
|
||||
let kinds = ["", "Mesh", "Light", "Camera", "Global"];
|
||||
let mut filters = row![].spacing(4).align_y(iced::Center);
|
||||
for (k, label) in [crate::tr!("import-3ds", "all"), crate::tr!("import-3ds", "mesh"), crate::tr!("import-3ds", "light"), crate::tr!("import-3ds", "camera"), crate::tr!("import-3ds", "general")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
filters = filters.push(chip(label, state.filter == k, msg(ImportDialogMsg::TdsFilter(k))));
|
||||
}
|
||||
let search = state.search.trim().to_lowercase();
|
||||
let mut rows = column![row![
|
||||
Space::new().width(Length::Fixed(22.0)),
|
||||
text(crate::tr!("import-3ds", "name")).size(11).style(muted_style).width(Fill),
|
||||
text(crate::tr!("import-3ds", "type")).size(11).style(muted_style).width(Length::Fixed(150.0)),
|
||||
]]
|
||||
.spacing(2);
|
||||
for (k, (name, kind, materials)) in state.objects.iter().enumerate() {
|
||||
let global = *kind == "Global";
|
||||
if state.filter != 0 && kinds[state.filter] != *kind {
|
||||
continue;
|
||||
}
|
||||
if !search.is_empty() && !name.to_lowercase().contains(&search) {
|
||||
continue;
|
||||
}
|
||||
let shown_name = if global { crate::tr!("import-3ds", "global") } else { name.clone() };
|
||||
let shown_kind = if global {
|
||||
String::new()
|
||||
} else if *kind == "Mesh" {
|
||||
format!("{} · {}", tds_kind(kind), crate::tr!("import-3ds", "materials", n = (*materials as i64)))
|
||||
} else {
|
||||
tds_kind(kind)
|
||||
};
|
||||
let on = state.checked.get(k).copied().unwrap_or(false);
|
||||
rows = rows.push(
|
||||
row![
|
||||
iced::widget::checkbox(on)
|
||||
.on_toggle(move |v| msg(ImportDialogMsg::TdsCheck(k, v)))
|
||||
.size(14),
|
||||
text(shown_name).size(12).width(Fill),
|
||||
text(shown_kind).size(11).style(muted_style).width(Length::Fixed(150.0)),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
);
|
||||
}
|
||||
let objects = card(
|
||||
crate::tr!("import-3ds", "objects"),
|
||||
column![
|
||||
row![
|
||||
filters,
|
||||
Space::new().width(Fill),
|
||||
text_input(crate::tr!("import-3ds", "search").as_ref(), &state.search)
|
||||
.on_input(|s| msg(ImportDialogMsg::TdsSearch(s)))
|
||||
.size(11)
|
||||
.padding([4, 8])
|
||||
.width(Length::Fixed(170.0))
|
||||
.style(field_style),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
container(scrollable(rows).height(Length::Fixed(300.0)))
|
||||
.padding(6)
|
||||
.width(Fill)
|
||||
.style(well_style),
|
||||
row![
|
||||
button(text(crate::tr!("import-3ds", "select-all")).size(11))
|
||||
.on_press(msg(ImportDialogMsg::TdsAll))
|
||||
.style(button_style(false))
|
||||
.padding([4, 10]),
|
||||
button(text(crate::tr!("import-3ds", "clear")).size(11))
|
||||
.on_press(msg(ImportDialogMsg::TdsClear))
|
||||
.style(button_style(false))
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.spacing(6),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
let layer_notes = [
|
||||
crate::tr!("import-3ds", "note-object"),
|
||||
crate::tr!("import-3ds", "note-material"),
|
||||
crate::tr!("import-3ds", "note-color"),
|
||||
crate::tr!("import-3ds", "note-single"),
|
||||
];
|
||||
let layers = card(
|
||||
crate::tr!("import-3ds", "layers"),
|
||||
column![
|
||||
stacked(
|
||||
[crate::tr!("import-3ds", "by-object"), crate::tr!("import-3ds", "by-material"), crate::tr!("import-3ds", "by-color"), crate::tr!("import-3ds", "single")]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(k, label)| (label, state.layers == k, msg(ImportDialogMsg::TdsLayers(k))))
|
||||
.collect()
|
||||
),
|
||||
text(layer_notes[state.layers.min(3)].clone()).size(11).style(muted_style),
|
||||
]
|
||||
.spacing(8),
|
||||
);
|
||||
let multi = card(
|
||||
crate::tr!("import-3ds", "multi"),
|
||||
stacked(
|
||||
[
|
||||
crate::tr!("import-3ds", "ask"),
|
||||
crate::tr!("import-3ds", "split"),
|
||||
crate::tr!("import-3ds", "first"),
|
||||
crate::tr!("import-3ds", "none"),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(k, label)| (label, state.multi == k, msg(ImportDialogMsg::TdsMulti(k))))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
let any = state.checked.iter().any(|c| *c);
|
||||
let mut actions = row![
|
||||
button(text("?").size(12))
|
||||
.on_press(msg(ImportDialogMsg::Help("3ds")))
|
||||
.style(button_style(false))
|
||||
.padding([5, 11]),
|
||||
Space::new().width(Fill),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center);
|
||||
if !any {
|
||||
actions = actions.push(text(crate::tr!("import-3ds", "reason")).size(11).style(muted_style));
|
||||
}
|
||||
actions = actions
|
||||
.push(dialog_button(crate::tr!("import-3ds", "cancel"), Message::CloseModal, false))
|
||||
.push(dialog_button_styled_opt(
|
||||
crate::tr!("import-3ds", "import"),
|
||||
any.then(|| msg(ImportDialogMsg::TdsOk)),
|
||||
button::primary,
|
||||
));
|
||||
column![
|
||||
top,
|
||||
row![
|
||||
container(objects).width(Length::FillPortion(3)),
|
||||
column![layers, multi].spacing(10).width(Length::FillPortion(2)),
|
||||
]
|
||||
.spacing(10),
|
||||
actions,
|
||||
]
|
||||
.spacing(10)
|
||||
.padding([10, 12])
|
||||
.width(sizing.width)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn view_dgn_import<'a>(
|
||||
state: &'a DgnImportState,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
|
||||
Reference in new issue
Block a user