Files
OneCAD-Draft/src/app/commands/dgn_import.rs
T
Hakan SevenandClaude Opus 5.5 ed4dc91670 Keep DGN reference attachments and cell instances well-formed
Plain -DGNIMPORT no longer writes drawings next to referenced files on
its own: references are ignored unless the window (or the kept import
settings) asks to translate them or attach them as underlays.

A referenced file named relative to the host is looked up in the host's
folder rather than the working directory. Its translated drawing takes
a file name made from the model name with path separators and other
reserved characters replaced, so it is always written in the
reference's own folder. A reference placed in sub units is scaled by the
same factor as its origin.

Layer, block, text style and reference names read from the file are
made valid symbol names (reserved characters become underscores).

An instance of a cell that has no block (the point display cell, an
empty or missing definition) is dropped instead of inserting a block
that does not exist.

"Prompt to overwrite" still keeps an existing translated drawing; the
code now says so instead of implying a prompt.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 23:11:18 +03:00

734 lines
30 KiB
Rust

//! IMPORT (the Import File window, or a file name on the command line,
//! routed by format) and DGNIMPORT / -DGNIMPORT (the DGN Import window or
//! the command line), and the import itself: a DGN model's layers, text
//! styles, cells and objects added as one undo step, into a new drawing
//! unless the window chose the current one.
use std::collections::HashMap;
use std::sync::Arc;
use super::*;
use crate::modules::insert::dgn_import::{
self, DgnImportCommand, DgnImportRequest, ExistingReference, ReferenceMode,
};
use crate::ui::window::import_dialogs::{
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
};
/// How many files the Import File window offers as recent.
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> {
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();
dialog = dialog.add_filter(name.as_str(), &extensions);
}
let handle = dialog.pick_file().await?;
let path = crate::sys::handle_path(&handle)
.to_string_lossy()
.into_owned();
let bytes = handle.read().await;
crate::scene::model::pdf_raster::register_source(&path, Arc::new(bytes));
Some(path)
}
fn dgn_filters() -> Vec<(String, Vec<String>)> {
vec![
("MicroStation DGN (*.dgn)".to_string(), vec!["dgn".to_string()]),
("All DGN Files (*.*)".to_string(), vec!["*".to_string()]),
]
}
fn extension(path: &str) -> String {
std::path::Path::new(path)
.extension()
.map(|e| e.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default()
}
fn exists(path: &str) -> bool {
crate::scene::model::pdf_raster::source_bytes(path).is_some()
}
/// Whether the file is a DGN model DGNIMPORT reads.
fn is_dgn(path: &str) -> bool {
crate::scene::model::underlay_vector::dgn_import_models(path)
.and_then(|models| models.into_iter().next())
.is_some_and(|model| crate::scene::model::underlay_vector::dgn_import(path, &model).is_some())
}
/// The importer a file goes to.
enum Route {
Pdf,
Dgn,
}
/// The importer for `path` chosen as `format` (an index of [`FORMATS`]; `None`
/// follows the extension), or why there is none.
fn route(path: &str, format: Option<usize>) -> Result<Route, String> {
let f = format.map(|k| &FORMATS[k]);
if let Some(f) = f {
if let Some(reason) = unsupported_reason(f.support) {
return Err(format!("{}: {reason}", f.name));
}
}
let ext = extension(path);
let by_extension = f.is_none_or(|f| f.support == Support::ByExtension);
let pdf = if by_extension { ext == "pdf" } else { f.is_some_and(|f| f.name == "PDF Files") };
if pdf {
return Ok(Route::Pdf);
}
let dgn_format = f.is_some_and(|f| f.name.contains("DGN"));
if (dgn_format || by_extension) && is_dgn(path) {
return Ok(Route::Dgn);
}
if dgn_format || ext == "dgn" {
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
}
// Any other extension: the format whose patterns name it.
let named = FORMATS
.iter()
.find(|f| f.extensions.iter().any(|e| e.eq_ignore_ascii_case(&ext)));
Err(match named.and_then(|f| unsupported_reason(f.support).map(|r| (f, r))) {
Some((f, reason)) => format!("{}: {reason}", f.name),
None => crate::t!("OpenCAD Studio has no importer for this format.").into_owned(),
})
}
impl OpenCADStudio {
/// IMPORT, DGNIMPORT and -DGNIMPORT; `None` defers to the other
/// families.
pub(super) fn dispatch_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(), ""),
};
let dialog = crate::sys::file_dia();
match (verb.as_str(), rest.is_empty()) {
("IMPORT", true) if dialog => {
self.open_import_file_dialog();
Some(Task::none())
}
("IMPORT", true) => {
use crate::command::{CadCommand, ValuePromptCommand};
let command = ValuePromptCommand::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())
}
// A file named after IMPORT (or typed at its prompt).
("IMPORT" | "_IMPORTFILE", false) => Some(self.import_routed(rest, None)),
("_IMPORTFILE", true) => Some(Task::none()),
("DGNIMPORT", true) if dialog => Some(self.pick_dgn_file()),
("DGNIMPORT" | "-DGNIMPORT", _) => {
use crate::command::CadCommand;
let command = DgnImportCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
if !rest.is_empty() {
return Some(
self.feed_command(crate::command::StepInput::Text(rest.to_string())),
);
}
Some(Task::none())
}
_ => None,
}
}
fn pick_dgn_file(&mut self) -> Task<Message> {
let title = crate::t!("Import DGN File").into_owned();
Task::perform(pick(title, dgn_filters()), |path| {
Message::ImportDialog(ImportDialogMsg::DgnPicked(path))
})
}
pub(in crate::app) fn open_import_file_dialog(&mut self) {
let path = self.import_dialogs.recent.first().cloned().unwrap_or_default();
self.import_dialogs.file = Some(ImportFileState {
path,
format: 0,
search: String::new(),
only_supported: false,
error: None,
});
self.active_modal = Some(crate::app::ModalKind::ImportFile);
}
/// The DGN Import window for a file; an error when it is no DGN model.
pub(in crate::app) fn open_dgn_import_dialog(&mut self, path: &str) -> Result<(), String> {
use crate::scene::model::underlay_vector::{dgn_import, dgn_import_models};
let models = dgn_import_models(path).unwrap_or_default();
let Some(import) = models.first().and_then(|m| dgn_import(path, m)) else {
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
};
// The options chosen last (DGNIMPORTMODE and the kept import
// settings when the window opens) stay when the file is changed.
let previous = self.import_dialogs.dgn.take();
let saved = dgn_import::settings();
let keep = |f: fn(&DgnImportState) -> bool, default: bool| {
previous.as_ref().map_or(default, f)
};
self.import_dialogs.dgn = Some(DgnImportState {
path: path.to_string(),
version: if import.v7 { "V7" } else { "V8" },
// Sheet models are not imported.
models: crate::ui::window::pdf_dialogs::item_thumbs(
codec::entities::UnderlayType::Dgn,
path,
)
.into_iter()
.filter(|m| models.contains(&m.label))
.collect(),
model: 0,
current_drawing: keep(|s| s.current_drawing, saved.mode != 0),
prefix_duplicates: keep(|s| s.prefix_duplicates, saved.mode == 2),
sub_units: keep(|s| s.sub_units, false),
explode_text_nodes: keep(|s| s.explode_text_nodes, saved.explode_text_nodes),
references: previous.as_ref().map_or(saved.references, |s| s.references),
existing_reference: previous
.as_ref()
.map_or(saved.existing_reference, |s| s.existing_reference),
units: import.unit_names.clone(),
mapping_tab: previous.as_ref().map_or(0, |s| s.mapping_tab),
mapping: [0, 1, 2, 3].map(|tab| dgn_import::mapping_rows(&import, tab)),
});
self.active_modal = Some(crate::app::ModalKind::DgnImport);
Ok(())
}
fn remember_import(&mut self, path: &str) {
let recent = &mut self.import_dialogs.recent;
recent.retain(|p| p != path);
recent.insert(0, path.to_string());
recent.truncate(RECENT);
}
/// IMPORT on the command line: the file to its importer.
fn import_routed(&mut self, path: &str, format: Option<usize>) -> Task<Message> {
if !exists(path) {
self.command_line
.push_error(&format!("File {path} does not exist."));
return Task::none();
}
match route(path, format) {
Ok(Route::Pdf) => {
self.remember_import(path);
self.dispatch_command(&format!("_PDFIMPORTFILE {path}"))
}
Ok(Route::Dgn) => {
self.remember_import(path);
if crate::sys::file_dia() {
if let Err(error) = self.open_dgn_import_dialog(path) {
self.command_line.push_error(&error);
}
return Task::none();
}
self.dispatch_command(&format!("-DGNIMPORT {path}"))
}
Err(error) => {
self.command_line.push_error(&error);
Task::none()
}
}
}
pub(in crate::app) fn update_import_dialog(&mut self, message: ImportDialogMsg) -> Task<Message> {
let i = self.active_tab;
match message {
ImportDialogMsg::Help(which) => {
let help = match which {
"dgn" => crate::t!(
"Imports a DGN model as drawing objects: levels as layers, cells as blocks."
),
_ => crate::t!(
"Imports a file of the chosen format with the format's importer."
),
};
self.command_line.push_info(help.as_ref());
}
// ── Import File ──────────────────────────────────────────────
ImportDialogMsg::Path(path) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.path = path;
s.error = None;
}
}
ImportDialogMsg::Recent(n) => {
let path = self.import_dialogs.recent.get(n).cloned();
if let (Some(s), Some(path)) = (self.import_dialogs.file.as_mut(), path) {
s.path = path;
s.error = None;
}
}
ImportDialogMsg::Format(k) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.format = k.min(FORMATS.len() - 1);
s.error = None;
}
}
ImportDialogMsg::Search(text) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.search = text;
}
}
ImportDialogMsg::OnlySupported(on) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.only_supported = on;
}
}
ImportDialogMsg::Browse => {
let Some(s) = self.import_dialogs.file.as_ref() else {
return Task::none();
};
let f = &FORMATS[s.format];
let filters = vec![(
format!("{} ({})", f.name, f.patterns),
f.extensions.iter().map(|e| e.to_string()).collect(),
)];
let title = crate::t!("Import File").into_owned();
return Task::perform(pick(title, filters), |path| {
Message::ImportDialog(ImportDialogMsg::Picked(path))
});
}
ImportDialogMsg::Picked(Some(path)) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.path = path;
s.error = None;
}
}
ImportDialogMsg::Picked(None) => {}
ImportDialogMsg::ImportOk => {
let Some(s) = self.import_dialogs.file.as_ref() else {
return Task::none();
};
let path = s.path.trim().trim_matches('"').to_string();
let format = s.format;
let error = if path.is_empty() {
Some(crate::t!("Filename cannot be blank").into_owned())
} else if !exists(&path) {
Some(format!("File {path} does not exist."))
} else {
None
};
let routed = match error {
Some(error) => Err(error),
None => route(&path, Some(format)),
};
match routed {
Err(error) => {
if let Some(s) = self.import_dialogs.file.as_mut() {
s.error = Some(error);
}
}
Ok(Route::Pdf) => {
self.remember_import(&path);
self.close_active_modal();
return self.dispatch_command(&format!("_PDFIMPORTFILE {path}"));
}
Ok(Route::Dgn) => {
self.remember_import(&path);
self.close_active_modal();
if let Err(error) = self.open_dgn_import_dialog(&path) {
self.command_line.push_error(&error);
}
}
}
}
// ── DGN Import ───────────────────────────────────────────────
ImportDialogMsg::DgnChange => return self.pick_dgn_file(),
ImportDialogMsg::DgnPicked(Some(path)) => {
if let Err(error) = self.open_dgn_import_dialog(&path) {
self.command_line.push_error(&error);
}
}
ImportDialogMsg::DgnPicked(None) => {}
ImportDialogMsg::DgnModel(k) => {
use crate::scene::model::underlay_vector::dgn_import;
if let Some(s) = self.import_dialogs.dgn.as_mut() {
if let Some(model) = s.models.get(k) {
if let Some(import) = dgn_import(&s.path, &model.label) {
s.mapping = [0, 1, 2, 3].map(|tab| dgn_import::mapping_rows(&import, tab));
s.units = import.unit_names.clone();
}
s.model = k;
}
}
}
ImportDialogMsg::DgnCurrent(on) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.current_drawing = on;
}
}
ImportDialogMsg::DgnPrefix(on) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.prefix_duplicates = on;
}
}
ImportDialogMsg::DgnSub(on) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.sub_units = on;
}
}
ImportDialogMsg::DgnExplode(on) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.explode_text_nodes = on;
}
}
ImportDialogMsg::DgnReferences(mode) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.references = mode;
}
}
ImportDialogMsg::DgnExisting(choice) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.existing_reference = choice;
}
}
ImportDialogMsg::DgnMappingTab(tab) => {
if let Some(s) = self.import_dialogs.dgn.as_mut() {
s.mapping_tab = tab.min(3);
}
}
ImportDialogMsg::DgnOk => {
let Some(s) = self.import_dialogs.dgn.as_ref() else {
return Task::none();
};
let request = DgnImportRequest {
path: s.path.clone(),
model: s
.models
.get(s.model)
.map(|m| m.label.clone())
.unwrap_or_default(),
sub_units: s.sub_units,
explode_text_nodes: s.explode_text_nodes,
current_drawing: s.current_drawing,
prefix_duplicates: s.prefix_duplicates,
references: s.references,
existing_reference: s.existing_reference,
};
// The window's choices become DGNIMPORTMODE and the kept
// settings the command line imports with.
dgn_import::set_settings(dgn_import::DgnImportSettings {
mode: match (s.current_drawing, s.prefix_duplicates) {
(false, _) => 0,
(true, false) => 1,
(true, true) => 2,
},
explode_text_nodes: s.explode_text_nodes,
references: s.references,
existing_reference: s.existing_reference,
});
self.save_config();
self.close_active_modal();
return self.run_dgn_import(i, request);
}
}
Task::none()
}
/// The import: into a new drawing (or the current one), one undo step;
/// then the model's reference attachments as the request says.
pub(in crate::app) fn run_dgn_import(&mut self, i: usize, request: DgnImportRequest) -> Task<Message> {
let Some(import) =
crate::scene::model::underlay_vector::dgn_import(&request.path, &request.model)
else {
self.command_line
.push_error(crate::t!("Invalid or unsupported DGN file.").as_ref());
return Task::none();
};
let converted =
dgn_import::convert(&import, request.sub_units, request.explode_text_nodes);
let mut task = Task::none();
let i = if request.current_drawing {
i
} else {
task = self.update(Message::TabNew);
self.active_tab
};
self.push_undo_snapshot(i, "DGNIMPORT");
let naming = if !request.current_drawing {
Naming::Number
} else if request.prefix_duplicates {
let stem = std::path::Path::new(&request.path.replace('\\', "/"))
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
Naming::Prefix(dgn_import::symbol_name(&format!("{stem} - {}$0$", request.model)))
} else {
Naming::Keep
};
let layers = place_converted(&mut self.tabs[i].scene, converted, &naming);
if request.references != ReferenceMode::Ignore {
let k = if request.sub_units { import.sub_per_master } else { 1.0 };
// A reference is placed on the model's default level.
let layer = import
.levels
.get(&64)
.map(|l| dgn_import::symbol_name(&l.0))
.unwrap_or_else(|| "0".to_string());
let layer = layers.get(&layer).cloned().unwrap_or(layer);
for reference in &import.references {
self.attach_dgn_reference(i, &request, reference, k, &layer);
}
}
if !request.current_drawing {
self.tabs[i].scene.fit_all();
}
self.tabs[i].dirty = true;
self.command_line.push_output("Regenerating model.");
self.refresh_layer_panel();
self.refresh_properties();
task
}
/// One reference attachment: its file translated to "<file>-<model>.dwg"
/// beside it and attached as the external reference "<file> - <model>",
/// or the file attached as a DGN underlay, at the attachment's place.
fn attach_dgn_reference(
&mut self,
i: usize,
request: &DgnImportRequest,
reference: &crate::scene::model::underlay_vector::DgnReference,
units: f64,
layer: &str,
) {
use crate::scene::model::underlay_vector::{dgn_import, dgn_import_models};
let host_dir = std::path::Path::new(&request.path)
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_default();
// A relative name is beside the host file; failing that, the file
// of that name in the host's folder.
let named = std::path::Path::new(&reference.file.replace('\\', "/")).to_path_buf();
let joined = host_dir.join(&named);
let file = if joined.is_file() {
joined
} else {
host_dir.join(named.file_name().unwrap_or_default())
};
let path = file.to_string_lossy().into_owned();
if crate::scene::model::pdf_raster::source_bytes(&path).is_none() {
return;
}
let Some(model) = (if reference.model.is_empty() {
dgn_import_models(&path).and_then(|m| m.into_iter().next())
} else {
Some(reference.model.clone())
}) else {
return;
};
// Sub units scale the placement and the size alike.
let origin = glam::DVec3::from_array(reference.origin) * units;
let scale = reference.scale * units;
if request.references == ReferenceMode::Underlay {
let mut placed = crate::modules::insert::pdf_attach::underlays_for_pages(
codec::entities::UnderlayType::Dgn,
std::slice::from_ref(&model),
origin,
scale,
reference.rotation,
);
let scene = &mut self.tabs[i].scene;
for (page, mut entity) in placed.drain(..) {
let definition = crate::modules::insert::pdf_attach::ensure_underlay_definition(
&mut scene.document,
codec::entities::UnderlayType::Dgn,
&path,
&page,
);
if let codec::EntityType::Underlay(underlay) = &mut entity {
underlay.definition_handle = definition;
}
entity.common_mut().layer = layer.to_string();
scene.add_entity(entity);
}
return;
}
let stem = file
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
// The model name comes from the file: made a plain file name so the
// drawing is written in the reference's folder.
let model_name = dgn_import::symbol_name(&model);
let target = file.with_file_name(format!("{stem}-{model_name}.dwg"));
// ponytail: "Prompt to overwrite" asks nothing and keeps the existing
// drawing, like "Do not overwrite"; add a confirmation if wanted.
let write = !target.is_file() || request.existing_reference == ExistingReference::Overwrite;
if write {
let Some(source) = dgn_import(&path, &model) else {
return;
};
let mut scene = crate::scene::Scene::new();
let converted = dgn_import::convert(&source, false, request.explode_text_nodes);
place_converted(&mut scene, converted, &Naming::Number);
if let Err(error) = crate::io::save(&scene.document, &target) {
self.command_line.push_error(&error);
return;
}
}
let xref = crate::modules::insert::xattach::XrefAttachRequest {
path: target.to_string_lossy().into_owned(),
overlay: false,
path_type: crate::io::xref_model::Pathtype::Relative,
};
let name = dgn_import::symbol_name(&format!("{stem} - {model}"));
let full = crate::modules::insert::xattach::resolve_xref_store_path(&xref.path, None);
self.command_line
.push_output(&format!("Attach Xref \"{name}\": {full}"));
let host = self.tabs[i].current_path.clone();
let block = crate::modules::insert::xattach::prepare_named_xref_definition(
&mut self.tabs[i].scene,
&xref,
host.as_deref(),
&name,
);
if host.is_none() {
self.tabs[i].xref_relative_on_save.insert(block.clone());
}
self.command_line
.push_output(&format!("\"{name}\" loaded: {full}"));
let mut insert = codec::entities::Insert::new(
block,
codec::types::Vector3::new(origin.x, origin.y, origin.z),
)
.with_scale(scale, scale, scale)
.with_rotation(reference.rotation);
insert.common.layer = layer.to_string();
insert.common.linetype = "Continuous".to_string();
self.tabs[i]
.scene
.add_entity(codec::EntityType::Insert(insert));
self.refresh_xref_manager();
}
}
/// How imported names meet the names a drawing already has: a new drawing
/// numbers a clashing layer "<name>_1" and a clashing block or text style
/// "<name>1" (layer 0 and the Standard style are shared); the current
/// drawing either keeps its own definitions or prefixes every imported name
/// (layers 0 and Defpoints excepted).
enum Naming {
Number,
Keep,
Prefix(String),
}
impl Naming {
fn name(&self, kind: char, name: &str, taken: &dyn Fn(&str) -> bool) -> String {
match self {
Naming::Keep => name.to_string(),
Naming::Prefix(p) => {
let shared = kind == 'l'
&& (name == "0" || name.eq_ignore_ascii_case("Defpoints"));
if shared {
name.to_string()
} else {
format!("{p}{name}")
}
}
Naming::Number => {
let shared = (kind == 'l' && name == "0")
|| (kind == 's' && name.eq_ignore_ascii_case("Standard"));
if shared || !taken(name) {
return name.to_string();
}
let sep = if kind == 'l' { "_" } else { "" };
(1..)
.map(|n| format!("{name}{sep}{n}"))
.find(|n| !taken(n))
.unwrap_or_default()
}
}
}
}
/// Adds converted objects to a drawing under the naming rule; returns the
/// layer names as placed.
fn place_converted(
scene: &mut crate::scene::Scene,
converted: dgn_import::Converted,
naming: &Naming,
) -> HashMap<String, String> {
let mut layers: HashMap<String, String> = HashMap::new();
for (name, color) in &converted.layers {
let doc = &scene.document;
let taken = |n: &str| doc.layers.iter().any(|l| l.name.eq_ignore_ascii_case(n));
let target = naming.name('l', name, &taken);
if !scene.document.layers.iter().any(|l| l.name.eq_ignore_ascii_case(&target)) {
let mut layer = codec::tables::Layer::new(target.as_str());
layer.handle = scene.document.allocate_handle();
layer.color = *color;
let _ = scene.document.layers.add(layer);
}
layers.insert(name.clone(), target);
}
let mut styles: HashMap<String, String> = HashMap::new();
for (name, font) in &converted.styles {
let doc = &scene.document;
let taken = |n: &str| doc.text_styles.iter().any(|s| s.name.eq_ignore_ascii_case(n));
let target = naming.name('s', name, &taken);
if !scene.document.text_styles.iter().any(|s| s.name.eq_ignore_ascii_case(&target)) {
let mut style = codec::tables::TextStyle::new(target.as_str());
style.handle = scene.document.allocate_handle();
style.font_file = font.clone();
let _ = scene.document.text_styles.add(style);
}
styles.insert(name.clone(), target);
}
let mut blocks: HashMap<String, String> = HashMap::new();
let mut claimed: Vec<String> = Vec::new();
for (name, _) in &converted.blocks {
let doc = &scene.document;
let taken = |n: &str| {
doc.block_records.iter().any(|b| b.name.eq_ignore_ascii_case(n))
|| claimed.iter().any(|c| c.eq_ignore_ascii_case(n))
};
let target = naming.name('b', name, &taken);
claimed.push(target.clone());
blocks.insert(name.clone(), target);
}
let fix = |mut entity: codec::EntityType| {
let common = entity.common_mut();
if let Some(layer) = layers.get(&common.layer) {
common.layer = layer.clone();
}
match &mut entity {
codec::EntityType::Text(t) => {
if let Some(s) = styles.get(&t.style) {
t.style = s.clone();
}
}
codec::EntityType::MText(t) => {
if let Some(s) = styles.get(&t.style) {
t.style = s.clone();
}
}
codec::EntityType::Insert(insert) => {
if let Some(b) = blocks.get(&insert.block_name) {
insert.block_name = b.clone();
}
}
_ => {}
}
entity
};
for (name, entities) in converted.blocks {
let target = &blocks[&name];
if scene.document.block_records.get(target).is_some() {
continue;
}
let entities = entities.into_iter().map(&fix).collect();
let _ = scene.define_block_from_owned_entities(entities, target, glam::DVec3::ZERO);
}
for entity in converted.entities {
scene.add_entity(fix(entity));
}
layers
}