forked from 1b-agent/opencadstudio-upstream
Merge pull request #1845 from ramox81/codex/dgnimport-followups
DGNIMPORT: names, cells, text placement, curves, 3D, models and references # Conflicts: # Cargo.lock
This commit is contained in:
commit
2b807a7654
36 files changed
+2041
-461
No files matched your search
@@ -6680,7 +6680,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6665,7 +6665,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6666,7 +6666,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6667,7 +6667,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6665,7 +6665,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6656,7 +6656,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6652,7 +6652,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6667,7 +6667,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6656,7 +6656,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6667,7 +6667,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6666,7 +6666,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6667,7 +6667,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6668,7 +6668,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6669,7 +6669,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6589,7 +6589,10 @@ dgn-import =
|
|||||||
.references = Dış DGN referansları
|
.references = Dış DGN referansları
|
||||||
.bind = Blok / xref yap
|
.bind = Blok / xref yap
|
||||||
.attach = DGN altlığı bağla
|
.attach = DGN altlığı bağla
|
||||||
.references-note = DGN dosyalarının referans ekleri okunmaz; dışarıda bırakılır.
|
.existing = Çevrilmiş çizim zaten varsa
|
||||||
|
.prompt-overwrite = Sor
|
||||||
|
.overwrite = Sormadan üzerine yaz
|
||||||
|
.keep-existing = Üzerine yazma
|
||||||
.mapping = Özellik eşleme
|
.mapping = Özellik eşleme
|
||||||
.mapping-standard = Standart eşleme kurulumu.
|
.mapping-standard = Standart eşleme kurulumu.
|
||||||
.mapping-note = Önizleme seçili kurulumun bu dosyaya etkisini gösterir.
|
.mapping-note = Önizleme seçili kurulumun bu dosyaya etkisini gösterir.
|
||||||
|
|||||||
@@ -6651,7 +6651,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
@@ -6663,7 +6663,10 @@ dgn-import =
|
|||||||
.references = External DGN references
|
.references = External DGN references
|
||||||
.bind = Bind as block / xref
|
.bind = Bind as block / xref
|
||||||
.attach = Attach as DGN underlay
|
.attach = Attach as DGN underlay
|
||||||
.references-note = Reference attachments of DGN files are not read; they are left out.
|
.existing = When the translated drawing exists
|
||||||
|
.prompt-overwrite = Prompt to overwrite
|
||||||
|
.overwrite = Overwrite without prompting
|
||||||
|
.keep-existing = Do not overwrite
|
||||||
.mapping = Property mapping
|
.mapping = Property mapping
|
||||||
.mapping-standard = Standard mapping setup.
|
.mapping-standard = Standard mapping setup.
|
||||||
.mapping-note = The preview shows the selected setup's effect on this file.
|
.mapping-note = The preview shows the selected setup's effect on this file.
|
||||||
|
|||||||
+308
-84
@@ -8,7 +8,9 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::modules::insert::dgn_import::{self, DgnImportCommand, DgnImportRequest};
|
use crate::modules::insert::dgn_import::{
|
||||||
|
self, DgnImportCommand, DgnImportRequest, ExistingReference, ReferenceMode,
|
||||||
|
};
|
||||||
use crate::ui::window::import_dialogs::{
|
use crate::ui::window::import_dialogs::{
|
||||||
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
|
unsupported_reason, DgnImportState, ImportDialogMsg, ImportFileState, Support, FORMATS,
|
||||||
};
|
};
|
||||||
@@ -53,7 +55,7 @@ fn exists(path: &str) -> bool {
|
|||||||
|
|
||||||
/// Whether the file is a DGN model DGNIMPORT reads.
|
/// Whether the file is a DGN model DGNIMPORT reads.
|
||||||
fn is_dgn(path: &str) -> bool {
|
fn is_dgn(path: &str) -> bool {
|
||||||
crate::scene::model::underlay_vector::item_names(codec::entities::UnderlayType::Dgn, path)
|
crate::scene::model::underlay_vector::dgn_import_models(path)
|
||||||
.and_then(|models| models.into_iter().next())
|
.and_then(|models| models.into_iter().next())
|
||||||
.is_some_and(|model| crate::scene::model::underlay_vector::dgn_import(path, &model).is_some())
|
.is_some_and(|model| crate::scene::model::underlay_vector::dgn_import(path, &model).is_some())
|
||||||
}
|
}
|
||||||
@@ -158,28 +160,38 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
/// The DGN Import window for a file; an error when it is no DGN model.
|
/// 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> {
|
pub(in crate::app) fn open_dgn_import_dialog(&mut self, path: &str) -> Result<(), String> {
|
||||||
use crate::scene::model::underlay_vector::{dgn_import, item_names};
|
use crate::scene::model::underlay_vector::{dgn_import, dgn_import_models};
|
||||||
let models = item_names(codec::entities::UnderlayType::Dgn, path).unwrap_or_default();
|
let models = dgn_import_models(path).unwrap_or_default();
|
||||||
let Some(import) = models.first().and_then(|m| dgn_import(path, m)) else {
|
let Some(import) = models.first().and_then(|m| dgn_import(path, m)) else {
|
||||||
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
|
return Err(crate::t!("Invalid or unsupported DGN file.").into_owned());
|
||||||
};
|
};
|
||||||
// The options chosen last stay when the file is changed.
|
// 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 previous = self.import_dialogs.dgn.take();
|
||||||
|
let saved = dgn_import::settings();
|
||||||
let keep = |f: fn(&DgnImportState) -> bool, default: bool| {
|
let keep = |f: fn(&DgnImportState) -> bool, default: bool| {
|
||||||
previous.as_ref().map_or(default, f)
|
previous.as_ref().map_or(default, f)
|
||||||
};
|
};
|
||||||
self.import_dialogs.dgn = Some(DgnImportState {
|
self.import_dialogs.dgn = Some(DgnImportState {
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
version: if import.v7 { "V7" } else { "V8" },
|
version: if import.v7 { "V7" } else { "V8" },
|
||||||
|
// Sheet models are not imported.
|
||||||
models: crate::ui::window::pdf_dialogs::item_thumbs(
|
models: crate::ui::window::pdf_dialogs::item_thumbs(
|
||||||
codec::entities::UnderlayType::Dgn,
|
codec::entities::UnderlayType::Dgn,
|
||||||
path,
|
path,
|
||||||
),
|
)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|m| models.contains(&m.label))
|
||||||
|
.collect(),
|
||||||
model: 0,
|
model: 0,
|
||||||
current_drawing: keep(|s| s.current_drawing, false),
|
current_drawing: keep(|s| s.current_drawing, saved.mode != 0),
|
||||||
prefix_duplicates: keep(|s| s.prefix_duplicates, false),
|
prefix_duplicates: keep(|s| s.prefix_duplicates, saved.mode == 2),
|
||||||
sub_units: keep(|s| s.sub_units, false),
|
sub_units: keep(|s| s.sub_units, false),
|
||||||
explode_text_nodes: keep(|s| s.explode_text_nodes, 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(),
|
units: import.unit_names.clone(),
|
||||||
mapping_tab: previous.as_ref().map_or(0, |s| s.mapping_tab),
|
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)),
|
mapping: [0, 1, 2, 3].map(|tab| dgn_import::mapping_rows(&import, tab)),
|
||||||
@@ -366,6 +378,16 @@ impl OpenCADStudio {
|
|||||||
s.explode_text_nodes = on;
|
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) => {
|
ImportDialogMsg::DgnMappingTab(tab) => {
|
||||||
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
if let Some(s) = self.import_dialogs.dgn.as_mut() {
|
||||||
s.mapping_tab = tab.min(3);
|
s.mapping_tab = tab.min(3);
|
||||||
@@ -386,7 +408,22 @@ impl OpenCADStudio {
|
|||||||
explode_text_nodes: s.explode_text_nodes,
|
explode_text_nodes: s.explode_text_nodes,
|
||||||
current_drawing: s.current_drawing,
|
current_drawing: s.current_drawing,
|
||||||
prefix_duplicates: s.prefix_duplicates,
|
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();
|
self.close_active_modal();
|
||||||
return self.run_dgn_import(i, request);
|
return self.run_dgn_import(i, request);
|
||||||
}
|
}
|
||||||
@@ -394,7 +431,8 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The import: into a new drawing (or the current one), one undo step.
|
/// 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> {
|
pub(in crate::app) fn run_dgn_import(&mut self, i: usize, request: DgnImportRequest) -> Task<Message> {
|
||||||
let Some(import) =
|
let Some(import) =
|
||||||
crate::scene::model::underlay_vector::dgn_import(&request.path, &request.model)
|
crate::scene::model::underlay_vector::dgn_import(&request.path, &request.model)
|
||||||
@@ -413,84 +451,33 @@ impl OpenCADStudio {
|
|||||||
self.active_tab
|
self.active_tab
|
||||||
};
|
};
|
||||||
self.push_undo_snapshot(i, "DGNIMPORT");
|
self.push_undo_snapshot(i, "DGNIMPORT");
|
||||||
let stem = std::path::Path::new(&request.path.replace('\\', "/"))
|
let naming = if !request.current_drawing {
|
||||||
.file_stem()
|
Naming::Number
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
} else if request.prefix_duplicates {
|
||||||
.unwrap_or_default();
|
let stem = std::path::Path::new(&request.path.replace('\\', "/"))
|
||||||
let scene = &mut self.tabs[i].scene;
|
.file_stem()
|
||||||
// In the current drawing, names it already has keep the existing
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
// definitions, or are imported under the file's name as prefix.
|
.unwrap_or_default();
|
||||||
let prefix = (request.current_drawing && request.prefix_duplicates)
|
Naming::Prefix(format!("{stem} - {}$0$", request.model))
|
||||||
.then(|| format!("{stem}$0$"));
|
} else {
|
||||||
let rename = |taken: bool, name: &str| match &prefix {
|
Naming::Keep
|
||||||
Some(p) if taken && name != "0" => format!("{p}{name}"),
|
|
||||||
_ => name.to_string(),
|
|
||||||
};
|
};
|
||||||
let mut layers: HashMap<String, String> = HashMap::new();
|
let layers = place_converted(&mut self.tabs[i].scene, converted, &naming);
|
||||||
for (name, color) in &converted.layers {
|
if request.references != ReferenceMode::Ignore {
|
||||||
let target = rename(scene.document.layers.contains(name), name);
|
let k = if request.sub_units { import.sub_per_master } else { 1.0 };
|
||||||
if !scene.document.layers.contains(&target) {
|
// A reference is placed on the model's default level.
|
||||||
let mut layer = codec::tables::Layer::new(target.as_str());
|
let layer = import
|
||||||
layer.handle = scene.document.allocate_handle();
|
.levels
|
||||||
layer.color = *color;
|
.get(&64)
|
||||||
let _ = scene.document.layers.add(layer);
|
.map(|l| l.0.clone())
|
||||||
|
.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);
|
||||||
}
|
}
|
||||||
layers.insert(name.clone(), target);
|
|
||||||
}
|
|
||||||
let mut styles: HashMap<String, String> = HashMap::new();
|
|
||||||
for (name, font) in &converted.styles {
|
|
||||||
let target = rename(scene.document.text_styles.contains(name), name);
|
|
||||||
if !scene.document.text_styles.contains(&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();
|
|
||||||
for (name, _) in &converted.blocks {
|
|
||||||
let taken = scene.document.block_records.get(name).is_some();
|
|
||||||
blocks.insert(name.clone(), rename(taken, name));
|
|
||||||
}
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
if !request.current_drawing {
|
if !request.current_drawing {
|
||||||
scene.fit_all();
|
self.tabs[i].scene.fit_all();
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output("Regenerating model.");
|
self.command_line.push_output("Regenerating model.");
|
||||||
@@ -498,4 +485,241 @@ impl OpenCADStudio {
|
|||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
task
|
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();
|
||||||
|
let named = std::path::Path::new(&reference.file.replace('\\', "/")).to_path_buf();
|
||||||
|
let file = if named.is_file() {
|
||||||
|
named
|
||||||
|
} 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;
|
||||||
|
};
|
||||||
|
let origin = glam::DVec3::from_array(reference.origin) * 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,
|
||||||
|
reference.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();
|
||||||
|
let target = file.with_file_name(format!("{stem}-{model}.dwg"));
|
||||||
|
// ponytail: "Prompt to overwrite" keeps the existing drawing, as the
|
||||||
|
// reference does when it cannot ask; ask here if a window is 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 = 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(reference.scale, reference.scale, reference.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
|
||||||
}
|
}
|
||||||
@@ -856,6 +856,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"SELECTIONCYCLING",
|
"SELECTIONCYCLING",
|
||||||
"UCSDETECT",
|
"UCSDETECT",
|
||||||
"SURFACEASSOCIATIVITY",
|
"SURFACEASSOCIATIVITY",
|
||||||
|
"DGNIMPORTMODE",
|
||||||
"LAYOUTCREATEVIEWPORT",
|
"LAYOUTCREATEVIEWPORT",
|
||||||
"TXT2MTXTCOMBINEMTEXT",
|
"TXT2MTXTCOMBINEMTEXT",
|
||||||
"TXT2MTXTTEXTORDER",
|
"TXT2MTXTTEXTORDER",
|
||||||
|
|||||||
@@ -1223,6 +1223,7 @@ impl OpenCADStudio {
|
|||||||
| "SELECTIONCYCLING"
|
| "SELECTIONCYCLING"
|
||||||
| "UCSDETECT"
|
| "UCSDETECT"
|
||||||
| "SURFACEASSOCIATIVITY"
|
| "SURFACEASSOCIATIVITY"
|
||||||
|
| "DGNIMPORTMODE"
|
||||||
| "UCSORTHO"
|
| "UCSORTHO"
|
||||||
| "UCSVIEW"
|
| "UCSVIEW"
|
||||||
| "LAYOUTCREATEVIEWPORT"
|
| "LAYOUTCREATEVIEWPORT"
|
||||||
@@ -1622,6 +1623,41 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Some(self.finish_dispatch(cmd));
|
return Some(self.finish_dispatch(cmd));
|
||||||
}
|
}
|
||||||
|
if name == "DGNIMPORTMODE" {
|
||||||
|
// Where DGNIMPORT puts the model: 0 a new drawing, 1 the
|
||||||
|
// current one keeping its definitions, 2 the current one
|
||||||
|
// with prefixed names.
|
||||||
|
use crate::modules::insert::dgn_import::{set_settings, settings};
|
||||||
|
let mut s = settings();
|
||||||
|
let current = s.mode;
|
||||||
|
match value
|
||||||
|
.as_deref()
|
||||||
|
.map(|v| v.trim().parse::<u8>().ok().filter(|v| *v <= 2))
|
||||||
|
{
|
||||||
|
Some(Some(mode)) => {
|
||||||
|
if current != mode {
|
||||||
|
s.mode = mode;
|
||||||
|
set_settings(s);
|
||||||
|
self.save_config();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(None) => {
|
||||||
|
self.command_line
|
||||||
|
.push_error("Requires an integer between 0 and 2.");
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for DGNIMPORTMODE <{current}>:"
|
||||||
|
));
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"Enter new value for DGNIMPORTMODE <{current}>:"
|
||||||
|
));
|
||||||
|
self.pending_setvar = Some(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Some(self.finish_dispatch(cmd));
|
||||||
|
}
|
||||||
if name == "SURFACEASSOCIATIVITY" {
|
if name == "SURFACEASSOCIATIVITY" {
|
||||||
use crate::scene::model::sweep_model;
|
use crate::scene::model::sweep_model;
|
||||||
let current = i16::from(sweep_model::surface_associativity());
|
let current = i16::from(sweep_model::surface_associativity());
|
||||||
|
|||||||
@@ -2237,6 +2237,7 @@ impl OpenCADStudio {
|
|||||||
// The DGN Import window, one field per call: "path=<file>" (as
|
// The DGN Import window, one field per call: "path=<file>" (as
|
||||||
// Change… picks it), "model=<n>", "target=new|current",
|
// Change… picks it), "model=<n>", "target=new|current",
|
||||||
// "conflict=prefix|ignore", "units=master|sub", "explode=on|off",
|
// "conflict=prefix|ignore", "units=master|sub", "explode=on|off",
|
||||||
|
// "references=translate|ignore|underlay", "existing=prompt|overwrite|keep",
|
||||||
// "mapping=layer|linetype|lineweight|color", "ok" or "cancel".
|
// "mapping=layer|linetype|lineweight|color", "ok" or "cancel".
|
||||||
"dgn_import_dialog" => {
|
"dgn_import_dialog" => {
|
||||||
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
|
use crate::ui::window::import_dialogs::ImportDialogMsg as I;
|
||||||
@@ -2274,6 +2275,18 @@ impl OpenCADStudio {
|
|||||||
_ => return Err(bad()),
|
_ => return Err(bad()),
|
||||||
}),
|
}),
|
||||||
"explode" => I::DgnExplode(on(value)?),
|
"explode" => I::DgnExplode(on(value)?),
|
||||||
|
"references" => I::DgnReferences(match value {
|
||||||
|
"translate" => crate::modules::insert::dgn_import::ReferenceMode::Translate,
|
||||||
|
"ignore" => crate::modules::insert::dgn_import::ReferenceMode::Ignore,
|
||||||
|
"underlay" => crate::modules::insert::dgn_import::ReferenceMode::Underlay,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
|
"existing" => I::DgnExisting(match value {
|
||||||
|
"prompt" => crate::modules::insert::dgn_import::ExistingReference::Prompt,
|
||||||
|
"overwrite" => crate::modules::insert::dgn_import::ExistingReference::Overwrite,
|
||||||
|
"keep" => crate::modules::insert::dgn_import::ExistingReference::Keep,
|
||||||
|
_ => return Err(bad()),
|
||||||
|
}),
|
||||||
"mapping" => I::DgnMappingTab(match value {
|
"mapping" => I::DgnMappingTab(match value {
|
||||||
"layer" => 0,
|
"layer" => 0,
|
||||||
"linetype" => 1,
|
"linetype" => 1,
|
||||||
|
|||||||
@@ -519,6 +519,8 @@ pub struct UserSettings {
|
|||||||
/// PDFSHXTEXT's recognition settings (fonts, threshold, PDFSHXLAYER,
|
/// PDFSHXTEXT's recognition settings (fonts, threshold, PDFSHXLAYER,
|
||||||
/// PDFSHXBESTFONT, PDFSHXTEXTSETTINGS).
|
/// PDFSHXBESTFONT, PDFSHXTEXTSETTINGS).
|
||||||
pub pdf_shx_text: crate::modules::insert::pdf_shx_text::ShxTextSettings,
|
pub pdf_shx_text: crate::modules::insert::pdf_shx_text::ShxTextSettings,
|
||||||
|
/// DGNIMPORTMODE and the DGN Import window's last choices.
|
||||||
|
pub dgn_import: crate::modules::insert::dgn_import::DgnImportSettings,
|
||||||
/// Nested-copy symbol handling: false inserts, true binds.
|
/// Nested-copy symbol handling: false inserts, true binds.
|
||||||
pub ncopy_bind: bool,
|
pub ncopy_bind: bool,
|
||||||
/// Last Options page; unknown saved names fall back without rejecting the config.
|
/// Last Options page; unknown saved names fall back without rejecting the config.
|
||||||
@@ -999,6 +1001,7 @@ impl Default for UserSettings {
|
|||||||
ucs_view: true,
|
ucs_view: true,
|
||||||
layout_create_viewport: true,
|
layout_create_viewport: true,
|
||||||
pdf_shx_text: Default::default(),
|
pdf_shx_text: Default::default(),
|
||||||
|
dgn_import: Default::default(),
|
||||||
ncopy_bind: false,
|
ncopy_bind: false,
|
||||||
cursor_type: CursorType::Crosshair,
|
cursor_type: CursorType::Crosshair,
|
||||||
crosshair_color: None,
|
crosshair_color: None,
|
||||||
|
|||||||
@@ -864,6 +864,7 @@ impl OpenCADStudio {
|
|||||||
ucs_view: crate::modules::view::view_cmd::ucs_view(),
|
ucs_view: crate::modules::view::view_cmd::ucs_view(),
|
||||||
layout_create_viewport: crate::scene::layout_create_viewport(),
|
layout_create_viewport: crate::scene::layout_create_viewport(),
|
||||||
pdf_shx_text: crate::modules::insert::pdf_shx_text::settings(),
|
pdf_shx_text: crate::modules::insert::pdf_shx_text::settings(),
|
||||||
|
dgn_import: crate::modules::insert::dgn_import::settings(),
|
||||||
ncopy_bind: self.ncopy_bind,
|
ncopy_bind: self.ncopy_bind,
|
||||||
cursor_type: self.cursor_type,
|
cursor_type: self.cursor_type,
|
||||||
crosshair_color: self.crosshair_color,
|
crosshair_color: self.crosshair_color,
|
||||||
@@ -1013,6 +1014,7 @@ impl OpenCADStudio {
|
|||||||
crate::modules::view::view_cmd::set_ucs_view(s.ucs_view);
|
crate::modules::view::view_cmd::set_ucs_view(s.ucs_view);
|
||||||
crate::scene::set_layout_create_viewport(s.layout_create_viewport);
|
crate::scene::set_layout_create_viewport(s.layout_create_viewport);
|
||||||
crate::modules::insert::pdf_shx_text::set_settings(s.pdf_shx_text.clone());
|
crate::modules::insert::pdf_shx_text::set_settings(s.pdf_shx_text.clone());
|
||||||
|
crate::modules::insert::dgn_import::set_settings(s.dgn_import);
|
||||||
self.ncopy_bind = s.ncopy_bind;
|
self.ncopy_bind = s.ncopy_bind;
|
||||||
self.cursor_type = s.cursor_type;
|
self.cursor_type = s.cursor_type;
|
||||||
self.crosshair_color = s.crosshair_color;
|
self.crosshair_color = s.crosshair_color;
|
||||||
|
|||||||
@@ -3498,7 +3498,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"External DGN references" => Some(("dgn-import", "references")),
|
"External DGN references" => Some(("dgn-import", "references")),
|
||||||
"Bind as block / xref" => Some(("dgn-import", "bind")),
|
"Bind as block / xref" => Some(("dgn-import", "bind")),
|
||||||
"Attach as DGN underlay" => Some(("dgn-import", "attach")),
|
"Attach as DGN underlay" => Some(("dgn-import", "attach")),
|
||||||
"Reference attachments of DGN files are not read; they are left out." => Some(("dgn-import", "references-note")),
|
"When the translated drawing exists" => Some(("dgn-import", "existing")),
|
||||||
|
"Prompt to overwrite" => Some(("dgn-import", "prompt-overwrite")),
|
||||||
|
"Overwrite without prompting" => Some(("dgn-import", "overwrite")),
|
||||||
|
"Do not overwrite" => Some(("dgn-import", "keep-existing")),
|
||||||
"Property mapping" => Some(("dgn-import", "mapping")),
|
"Property mapping" => Some(("dgn-import", "mapping")),
|
||||||
"Standard mapping setup." => Some(("dgn-import", "mapping-standard")),
|
"Standard mapping setup." => Some(("dgn-import", "mapping-standard")),
|
||||||
"The preview shows the selected setup's effect on this file." => Some(("dgn-import", "mapping-note")),
|
"The preview shows the selected setup's effect on this file." => Some(("dgn-import", "mapping-note")),
|
||||||
|
|||||||
+778
-245
File diff suppressed because it is too large.
Load diff
@@ -651,7 +651,17 @@ pub fn prepare_xref_definition(
|
|||||||
request: &XrefAttachRequest,
|
request: &XrefAttachRequest,
|
||||||
host_file: Option<&std::path::Path>,
|
host_file: Option<&std::path::Path>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let stem = path_to_block_name(&request.path);
|
prepare_named_xref_definition(scene, request, host_file, &path_to_block_name(&request.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`prepare_xref_definition`] under a chosen name instead of the file's.
|
||||||
|
pub fn prepare_named_xref_definition(
|
||||||
|
scene: &mut Scene,
|
||||||
|
request: &XrefAttachRequest,
|
||||||
|
host_file: Option<&std::path::Path>,
|
||||||
|
stem: &str,
|
||||||
|
) -> String {
|
||||||
|
let stem = stem.to_string();
|
||||||
if let Some(existing) = existing_reference(scene, &stem) {
|
if let Some(existing) = existing_reference(scene, &stem) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::model::{
|
use super::model::{
|
||||||
arc_cubics, bspline_points, dgn_color_table, paths_bounds, scaled_elements, DgnCollector,
|
arc_cubics, bspline_points, dgn_color_table, paths_bounds, scaled_elements, By, DgnCollector,
|
||||||
DgnElement, DgnGeom, DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text,
|
DgnColor, DgnElement, DgnGeom, DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text,
|
||||||
DGN_DEFAULT_COLORS,
|
DGN_DEFAULT_COLORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +49,42 @@ pub fn is_v7(bytes: &[u8]) -> bool {
|
|||||||
bytes.len() > 1256 && (bytes[1] & 0x7f) == 9 && (bytes[0] & 0x3f) == 8
|
bytes.len() > 1256 && (bytes[1] & 0x7f) == 9 && (bytes[0] & 0x3f) == 8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 3D design file: the header element's 3D flag. Its points have a z,
|
||||||
|
/// and planar elements carry their plane as a quaternion.
|
||||||
|
fn is_3d(bytes: &[u8]) -> bool {
|
||||||
|
bytes[0] & 0x40 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rows of the rotation a 3D element's quaternion (w, x, y, z as
|
||||||
|
/// signed 1.31 fractions) stands for: the element's own x, y and z axes.
|
||||||
|
fn quaternion_axes(e: &[u8], at: usize) -> Option<[[f64; 3]; 3]> {
|
||||||
|
let q = |k: usize| int32(e, at + 4 * k).map(|v| v as f64 / 2_147_483_648.0);
|
||||||
|
let (w, x, y, z) = (q(0)?, q(1)?, q(2)?, q(3)?);
|
||||||
|
// The rotation matrix's columns are the axes.
|
||||||
|
let m = [
|
||||||
|
[x * x - y * y - z * z + w * w, 2.0 * (z * w + x * y), 2.0 * (x * z - y * w)],
|
||||||
|
[2.0 * (x * y - z * w), -x * x + y * y - z * z + w * w, 2.0 * (x * w + y * z)],
|
||||||
|
[2.0 * (x * z + y * w), 2.0 * (y * z - x * w), -x * x - y * y + z * z + w * w],
|
||||||
|
];
|
||||||
|
Some([0, 1, 2].map(|c| [m[0][c], m[1][c], m[2][c]]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A cell name in Radix-50: two words of three characters each.
|
||||||
|
fn radix50(e: &[u8], at: usize) -> String {
|
||||||
|
const SET: &[u8] = b" ABCDEFGHIJKLMNOPQRSTUVWXYZ$.?0123456789";
|
||||||
|
let mut name = String::new();
|
||||||
|
for k in 0..2 {
|
||||||
|
let Some(mut word) = uint16(e, at + 2 * k).map(usize::from) else { break };
|
||||||
|
let mut chars = [0u8; 3];
|
||||||
|
for c in chars.iter_mut().rev() {
|
||||||
|
*c = SET.get(word % 40).copied().unwrap_or(b' ');
|
||||||
|
word /= 40;
|
||||||
|
}
|
||||||
|
name.extend(chars.iter().map(|c| char::from(*c)));
|
||||||
|
}
|
||||||
|
name.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
struct Units {
|
struct Units {
|
||||||
uor_per_sub: f64,
|
uor_per_sub: f64,
|
||||||
sub_per_master: f64,
|
sub_per_master: f64,
|
||||||
@@ -137,10 +173,14 @@ struct Instance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a run of elements into raw geometry: plain elements, complex
|
/// Reads a run of elements into raw geometry: plain elements, complex
|
||||||
/// chains and shapes (joined), B-splines, text and shared cell instances.
|
/// chains and shapes (joined), B-splines, text, cells and shared cell
|
||||||
fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
/// instances. In a 3D file (`d3`) points have a z and planar elements their
|
||||||
|
/// plane; the drawn geometry is their plan.
|
||||||
|
fn read_run(run: &[&[u8]], colors: &[[u8; 3]], d3: bool) -> Raw {
|
||||||
let mut raw = Raw::default();
|
let mut raw = Raw::default();
|
||||||
let mut spline: Option<(usize, usize, [u8; 3], f64)> = None;
|
// An open B-spline curve: order, pole count, colour, closed, and the
|
||||||
|
// header as the element it imports as.
|
||||||
|
let mut spline: Option<(usize, usize, [u8; 3], bool, DgnElement)> = None;
|
||||||
let mut knots: Vec<f64> = Vec::new();
|
let mut knots: Vec<f64> = Vec::new();
|
||||||
// A complex shape or chain collects its components into one path.
|
// A complex shape or chain collects its components into one path.
|
||||||
let mut complex: Option<(
|
let mut complex: Option<(
|
||||||
@@ -203,7 +243,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut collect = DgnCollector::default();
|
let mut collect = DgnCollector::default();
|
||||||
for e in run {
|
let mut index = 0;
|
||||||
|
while index < run.len() {
|
||||||
|
let e = run[index];
|
||||||
|
index += 1;
|
||||||
let kind = e[1] & 0x7f;
|
let kind = e[1] & 0x7f;
|
||||||
if e[1] & 0x80 != 0 || e.len() < 36 {
|
if e[1] & 0x80 != 0 || e.len() < 36 {
|
||||||
continue;
|
continue;
|
||||||
@@ -212,22 +255,57 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
let color = colors[(symbology >> 8) as usize];
|
let color = colors[(symbology >> 8) as usize];
|
||||||
let element = |geom: DgnGeom| DgnElement {
|
let element = |geom: DgnGeom| DgnElement {
|
||||||
level: level(e) as u32,
|
level: level(e) as u32,
|
||||||
color: Some(color),
|
color: By::Own(DgnColor {
|
||||||
weight: Some(((symbology >> 3) & 31) as u32),
|
index: (symbology >> 8) as u32,
|
||||||
style: Some((symbology & 7) as u32),
|
table: color,
|
||||||
|
rgb: color,
|
||||||
|
}),
|
||||||
|
weight: By::Own(((symbology >> 3) & 31) as u32),
|
||||||
|
style: By::Own((symbology & 7) as u32),
|
||||||
geom,
|
geom,
|
||||||
|
filled: fill_index(e).is_some(),
|
||||||
};
|
};
|
||||||
// One pixel wide, as V8 models draw: the element weight is a
|
// One pixel wide, as V8 models draw: the element weight is a
|
||||||
// lineweight, not shown while lineweights are off.
|
// lineweight, not shown while lineweights are off.
|
||||||
let stroke = Some((color, -1.0));
|
let stroke = Some((color, -1.0));
|
||||||
let fill = fill_index(e).map(|i| colors[i as usize]);
|
let fill = fill_index(e).map(|i| colors[i as usize]);
|
||||||
let int_pt =
|
// A point as the file holds it: two whole UORs, or three in 3D.
|
||||||
|i: usize| -> Option<[f64; 2]> { Some([int32(e, i)? as f64, int32(e, i + 4)? as f64]) };
|
let pt3 = |i: usize| -> Option<[f64; 3]> {
|
||||||
|
Some([
|
||||||
|
int32(e, i)? as f64,
|
||||||
|
int32(e, i + 4)? as f64,
|
||||||
|
if d3 { int32(e, i + 8)? as f64 } else { 0.0 },
|
||||||
|
])
|
||||||
|
};
|
||||||
|
let plan = |p: [f64; 3]| [p[0], p[1]];
|
||||||
|
let step = if d3 { 12 } else { 8 };
|
||||||
match kind {
|
match kind {
|
||||||
// A text node: its count of text components.
|
// A cell: its components follow as complex elements; they are
|
||||||
|
// already placed, so the cell keeps them as they are.
|
||||||
|
2 => {
|
||||||
|
let start = index;
|
||||||
|
while index < run.len() && run[index][0] & 0x80 != 0 {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
let inner = read_run(&run[start..index], colors, d3);
|
||||||
|
raw.paths.extend(inner.paths);
|
||||||
|
raw.texts.extend(inner.texts);
|
||||||
|
raw.instances.extend(inner.instances);
|
||||||
|
collect.push(element(DgnGeom::CellGroup {
|
||||||
|
name: radix50(e, 38),
|
||||||
|
parts: inner.elements,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// A text node: its count of text components, justification
|
||||||
|
// (45), line spacing (46) and origin (62, or 74 in 3D).
|
||||||
7 => collect.open(
|
7 => collect.open(
|
||||||
uint16(e, 38).unwrap_or(0) as usize,
|
uint16(e, 38).unwrap_or(0) as usize,
|
||||||
element(DgnGeom::TextNode(Vec::new())),
|
element(DgnGeom::TextNode {
|
||||||
|
lines: Vec::new(),
|
||||||
|
origin: pt3(if d3 { 74 } else { 62 }),
|
||||||
|
just: e.get(45).copied().unwrap_or(0),
|
||||||
|
line_spacing: int32(e, 46).map(|v| v as f64),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
12 | 14 => {
|
12 | 14 => {
|
||||||
let n = uint16(e, 38).unwrap_or(0) as usize;
|
let n = uint16(e, 38).unwrap_or(0) as usize;
|
||||||
@@ -243,8 +321,9 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
if let (Some(a), Some(b)) = (int_pt(36), int_pt(44)) {
|
if let (Some(a3), Some(b3)) = (pt3(36), pt3(36 + step)) {
|
||||||
collect.push(element(DgnGeom::Line(a, b)));
|
let (a, b) = (plan(a3), plan(b3));
|
||||||
|
collect.push(element(DgnGeom::Line(a3, b3)));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -261,9 +340,10 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
4 | 6 | 11 => {
|
4 | 6 | 11 => {
|
||||||
let n = uint16(e, 36).unwrap_or(0) as usize;
|
let n = uint16(e, 36).unwrap_or(0) as usize;
|
||||||
let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
let pts3: Vec<[f64; 3]> = (0..n).filter_map(|k| pt3(38 + k * step)).collect();
|
||||||
|
let pts: Vec<[f64; 2]> = pts3.iter().map(|p| plan(*p)).collect();
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
collect.push(element(DgnGeom::Points(pts.clone())));
|
collect.push(element(DgnGeom::Points(pts3)));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -271,6 +351,69 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 3D ellipses and arcs: the axes from the quaternion (52 or
|
||||||
|
// 60), the centre as three doubles after it.
|
||||||
|
15 | 16 if d3 => {
|
||||||
|
let at = if kind == 15 { 36 } else { 44 };
|
||||||
|
let q = at + 16;
|
||||||
|
let (Some(a), Some(b), Some(axes), Some(cx), Some(cy), Some(cz)) = (
|
||||||
|
vax_double(e, at),
|
||||||
|
vax_double(e, at + 8),
|
||||||
|
quaternion_axes(e, q),
|
||||||
|
vax_double(e, q + 16),
|
||||||
|
vax_double(e, q + 24),
|
||||||
|
vax_double(e, q + 32),
|
||||||
|
) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let (start, sweep) = if kind == 15 {
|
||||||
|
(0.0, 360.0)
|
||||||
|
} else {
|
||||||
|
let (Some(st), Some(sw)) = (int32(e, 36), int32(e, 40)) else { continue };
|
||||||
|
let sw = sw as u32;
|
||||||
|
let mut sweep = if sw & 0x8000_0000 != 0 {
|
||||||
|
-((sw & 0x7fff_ffff) as f64)
|
||||||
|
} else {
|
||||||
|
sw as f64
|
||||||
|
} / 360000.0;
|
||||||
|
if sweep == 0.0 {
|
||||||
|
sweep = 360.0;
|
||||||
|
}
|
||||||
|
(st as f64 / 360000.0, sweep)
|
||||||
|
};
|
||||||
|
let [x, y, _] = axes;
|
||||||
|
collect.push(element(DgnGeom::Arc {
|
||||||
|
center: [cx, cy, cz],
|
||||||
|
u: x.map(|c| c * a),
|
||||||
|
v: y.map(|c| c * b),
|
||||||
|
start: start.to_radians(),
|
||||||
|
sweep: sweep.to_radians(),
|
||||||
|
}));
|
||||||
|
let pieces = arc_cubics(
|
||||||
|
[cx, cy],
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
x[1].atan2(x[0]),
|
||||||
|
start.to_radians(),
|
||||||
|
sweep.to_radians(),
|
||||||
|
);
|
||||||
|
let closed = sweep.abs() >= 360.0;
|
||||||
|
push(
|
||||||
|
&mut raw,
|
||||||
|
&mut complex,
|
||||||
|
Path {
|
||||||
|
subpaths: vec![SubPath {
|
||||||
|
segments: pieces
|
||||||
|
.into_iter()
|
||||||
|
.map(|[a, b, c, d]| Segment::Cubic(a, b, c, d))
|
||||||
|
.collect(),
|
||||||
|
closed,
|
||||||
|
}],
|
||||||
|
stroke,
|
||||||
|
fill: if closed { fill } else { None },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
15 | 16 => {
|
15 | 16 => {
|
||||||
let arc = if kind == 15 {
|
let arc = if kind == 15 {
|
||||||
(|| {
|
(|| {
|
||||||
@@ -312,9 +455,9 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
let turn = (r as f64 / 360000.0).to_radians();
|
let turn = (r as f64 / 360000.0).to_radians();
|
||||||
let (c, s) = (turn.cos(), turn.sin());
|
let (c, s) = (turn.cos(), turn.sin());
|
||||||
collect.push(element(DgnGeom::Arc {
|
collect.push(element(DgnGeom::Arc {
|
||||||
center: [ox, oy],
|
center: [ox, oy, 0.0],
|
||||||
u: [a * c, a * s],
|
u: [a * c, a * s, 0.0],
|
||||||
v: [-b * s, b * c],
|
v: [-b * s, b * c, 0.0],
|
||||||
start: start.to_radians(),
|
start: start.to_radians(),
|
||||||
sweep: sweep.to_radians(),
|
sweep: sweep.to_radians(),
|
||||||
}));
|
}));
|
||||||
@@ -344,16 +487,30 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
17 => {
|
17 => {
|
||||||
let (Some(width), Some(height), Some(rotation), Some(origin)) =
|
// 2D: rotation (46), origin (50), count (58), characters
|
||||||
(int32(e, 38), int32(e, 42), int32(e, 46), int_pt(50))
|
// (60); 3D: quaternion (46), origin (62), count (74),
|
||||||
|
// characters (76).
|
||||||
|
let rotation = if d3 {
|
||||||
|
quaternion_axes(e, 46).map(|[x, _, _]| (x[1].atan2(x[0]).to_degrees() * 360000.0).round() as i32)
|
||||||
|
} else {
|
||||||
|
int32(e, 46)
|
||||||
|
};
|
||||||
|
let (origin_at, count_at) = if d3 { (62, 74) } else { (50, 58) };
|
||||||
|
let (Some(width), Some(height), Some(rotation), Some(origin3)) =
|
||||||
|
(int32(e, 38), int32(e, 42), rotation, pt3(origin_at))
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let n = e.get(58).copied().unwrap_or(0) as usize;
|
let origin = plan(origin3);
|
||||||
let Some(chars) = e.get(60..60 + n) else {
|
let n = e.get(count_at).copied().unwrap_or(0) as usize;
|
||||||
|
let Some(chars) = e.get(count_at + 2..count_at + 2 + n) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let (height, width) = (height as f64 * 6.0 / 1000.0, width as f64 * 6.0 / 1000.0);
|
// Sizes in whole UORs (the multipliers are 1000/6 of one).
|
||||||
|
let (height, width) = (
|
||||||
|
(height as f64 * 6.0 / 1000.0).round(),
|
||||||
|
(width as f64 * 6.0 / 1000.0).round(),
|
||||||
|
);
|
||||||
let text = Text {
|
let text = Text {
|
||||||
text: String::from_utf8_lossy(chars)
|
text: String::from_utf8_lossy(chars)
|
||||||
.trim_end_matches('\0')
|
.trim_end_matches('\0')
|
||||||
@@ -365,26 +522,36 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
color,
|
color,
|
||||||
font: "txt".to_string(),
|
font: "txt".to_string(),
|
||||||
};
|
};
|
||||||
|
// A V7 font is drawn monospaced: the text is as long as its
|
||||||
|
// characters times their width.
|
||||||
collect.push(element(DgnGeom::Text {
|
collect.push(element(DgnGeom::Text {
|
||||||
text: text.text.clone(),
|
text: text.text.clone(),
|
||||||
origin,
|
origin: origin3,
|
||||||
height,
|
height,
|
||||||
width_factor: text.width_factor,
|
width_factor: text.width_factor,
|
||||||
|
length: text.text.chars().count() as f64 * width,
|
||||||
rotation: text.rotation,
|
rotation: text.rotation,
|
||||||
|
just: e[37],
|
||||||
font: Some(e[36] as u32),
|
font: Some(e[36] as u32),
|
||||||
|
style: None,
|
||||||
}));
|
}));
|
||||||
raw.texts.push(text);
|
raw.texts.push(text);
|
||||||
}
|
}
|
||||||
27 => {
|
27 => {
|
||||||
// B-spline curve header: order, pole count.
|
// B-spline curve header: order and closure (40), pole count
|
||||||
let order = ((e.get(40).copied().unwrap_or(0) & 0x0f) + 2) as usize;
|
// (42), knot count (44); the curve imports with the header's
|
||||||
|
// level and symbology.
|
||||||
|
let flags = e.get(40).copied().unwrap_or(0);
|
||||||
|
let order = ((flags & 0x0f) + 2) as usize;
|
||||||
let poles = uint16(e, 42).unwrap_or(0) as usize;
|
let poles = uint16(e, 42).unwrap_or(0) as usize;
|
||||||
spline = Some((order, poles, color, -1.0));
|
let header = element(DgnGeom::Chain(Vec::new()));
|
||||||
knots.clear();
|
spline = Some((order, poles, color, flags & 0x80 != 0, header));
|
||||||
|
knots = vec![f64::NAN; uint16(e, 44).unwrap_or(0) as usize];
|
||||||
}
|
}
|
||||||
26 => {
|
26 => {
|
||||||
// Interior knots as fractions of the parameter range.
|
// Knots as fractions of the parameter range; a closed
|
||||||
let n = spline.map(|s| s.1.saturating_sub(s.0)).unwrap_or(0);
|
// curve's list also holds its ends.
|
||||||
|
let n = knots.len();
|
||||||
knots = (0..n)
|
knots = (0..n)
|
||||||
.filter_map(|k| int32(e, 36 + k * 4))
|
.filter_map(|k| int32(e, 36 + k * 4))
|
||||||
.map(|v| v as u32 as f64 / 2_147_483_648.0)
|
.map(|v| v as u32 as f64 / 2_147_483_648.0)
|
||||||
@@ -392,11 +559,17 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
}
|
}
|
||||||
21 => {
|
21 => {
|
||||||
let n = uint16(e, 36).unwrap_or(0) as usize;
|
let n = uint16(e, 36).unwrap_or(0) as usize;
|
||||||
let poles: Vec<[f64; 2]> = (0..n).filter_map(|k| int_pt(38 + k * 8)).collect();
|
let poles3: Vec<[f64; 3]> = (0..n).filter_map(|k| pt3(38 + k * step)).collect();
|
||||||
if let Some((order, count, color, width)) = spline.take() {
|
let poles: Vec<[f64; 2]> = poles3.iter().map(|p| plan(*p)).collect();
|
||||||
|
if let Some((order, count, color, closed, header)) = spline.take() {
|
||||||
if poles.len() == count && count >= order {
|
if poles.len() == count && count >= order {
|
||||||
let interior = if knots.len() == count - order {
|
let stored: Vec<f64> = knots
|
||||||
knots.clone()
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|k| *k > 1e-9 && *k < 1.0 - 1e-6)
|
||||||
|
.collect();
|
||||||
|
let interior = if stored.len() == count - order {
|
||||||
|
stored
|
||||||
} else {
|
} else {
|
||||||
(1..=count - order)
|
(1..=count - order)
|
||||||
.map(|k| k as f64 / (count - order + 1) as f64)
|
.map(|k| k as f64 / (count - order + 1) as f64)
|
||||||
@@ -407,13 +580,18 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
full.extend(vec![1.0; order]);
|
full.extend(vec![1.0; order]);
|
||||||
let pts = bspline_points(order, &poles, &full, 24 * count);
|
let pts = bspline_points(order, &poles, &full, 24 * count);
|
||||||
collect.push(DgnElement {
|
collect.push(DgnElement {
|
||||||
color: Some(color),
|
geom: DgnGeom::Spline {
|
||||||
..element(DgnGeom::Points(pts.clone()))
|
degree: order - 1,
|
||||||
|
knots: full,
|
||||||
|
poles: poles3.clone(),
|
||||||
|
closed,
|
||||||
|
},
|
||||||
|
..header
|
||||||
});
|
});
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
polyline(&pts, false, Some((color, width)), None),
|
polyline(&pts, false, Some((color, -1.0)), None),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -430,21 +608,32 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw {
|
|||||||
.to_string()
|
.to_string()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
// A 3D instance holds a 3×3 matrix (its plan part and the z
|
||||||
|
// scale are used) and a z.
|
||||||
|
let (r1, zz) = if d3 { (100, 140) } else { (92, 0) };
|
||||||
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) = (
|
let (Some(m0), Some(m1), Some(m2), Some(m3), Some(ox), Some(oy)) = (
|
||||||
vax_double(e, 76),
|
vax_double(e, 76),
|
||||||
vax_double(e, 84),
|
vax_double(e, 84),
|
||||||
vax_double(e, 92),
|
vax_double(e, r1),
|
||||||
vax_double(e, 100),
|
vax_double(e, r1 + 8),
|
||||||
le_i32(e, 148),
|
le_i32(e, 148),
|
||||||
le_i32(e, 152),
|
le_i32(e, 152),
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let (oz, z_scale) = if d3 {
|
||||||
|
(
|
||||||
|
le_i32(e, 156).unwrap_or(0) as f64,
|
||||||
|
vax_double(e, zz).filter(|z| *z != 0.0).unwrap_or(1.0),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(0.0, 1.0)
|
||||||
|
};
|
||||||
collect.push(element(DgnGeom::Cell {
|
collect.push(element(DgnGeom::Cell {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
m: [m0, m1, m2, m3],
|
m: [m0, m1, m2, m3],
|
||||||
origin: [ox as f64, oy as f64],
|
origin: [ox as f64, oy as f64, oz],
|
||||||
z_scale: 1.0,
|
z_scale,
|
||||||
}));
|
}));
|
||||||
raw.instances.push(Instance {
|
raw.instances.push(Instance {
|
||||||
name,
|
name,
|
||||||
@@ -647,14 +836,14 @@ pub fn model(bytes: &[u8], hidden: &[String]) -> Option<Sheet> {
|
|||||||
while j < all.len() && all[j][0] & 0x80 != 0 {
|
while j < all.len() && all[j][0] & 0x80 != 0 {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
defs.insert(name, read_run(&all[i + 1..j], &colors));
|
defs.insert(name, read_run(&all[i + 1..j], &colors, is_3d(bytes)));
|
||||||
i = j;
|
i = j;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
top.push(e);
|
top.push(e);
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
let raw = read_run(&top, &colors);
|
let raw = read_run(&top, &colors, is_3d(bytes));
|
||||||
let per_unit = u.uor_per_sub * u.sub_per_master;
|
let per_unit = u.uor_per_sub * u.sub_per_master;
|
||||||
let m: Affine = [
|
let m: Affine = [
|
||||||
1.0 / per_unit,
|
1.0 / per_unit,
|
||||||
@@ -721,7 +910,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
|
|||||||
while j < all.len() && all[j][0] & 0x80 != 0 {
|
while j < all.len() && all[j][0] & 0x80 != 0 {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
let def = read_run(&all[i + 1..j], &colors).elements;
|
let def = read_run(&all[i + 1..j], &colors, is_3d(bytes)).elements;
|
||||||
cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
|
cells.push((name, scaled_elements(&def, s, [0.0, 0.0])));
|
||||||
i = j;
|
i = j;
|
||||||
continue;
|
continue;
|
||||||
@@ -729,7 +918,7 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
|
|||||||
top.push(e);
|
top.push(e);
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
let raw = read_run(&top, &colors);
|
let raw = read_run(&top, &colors, is_3d(bytes));
|
||||||
let levels = (0..64u32)
|
let levels = (0..64u32)
|
||||||
.map(|l| {
|
.map(|l| {
|
||||||
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
|
let name = names.get(&(l as u8)).cloned().unwrap_or_else(|| l.to_string());
|
||||||
@@ -743,5 +932,6 @@ pub fn import(bytes: &[u8]) -> Option<DgnImport> {
|
|||||||
sub_per_master: u.sub_per_master,
|
sub_per_master: u.sub_per_master,
|
||||||
unit_names: (unit_name(bytes, 1120), unit_name(bytes, 1122)),
|
unit_names: (unit_name(bytes, 1120), unit_name(bytes, 1122)),
|
||||||
v7: true,
|
v7: true,
|
||||||
|
..Default::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,9 @@ use std::collections::HashMap;
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
use super::model::{
|
use super::model::{
|
||||||
arc_cubics, dgn_color_table, paths_bounds, scaled_elements, DgnCollector, DgnElement, DgnGeom,
|
arc_cubics, bspline_points, dgn_color_table, paths_bounds, scaled_elements, By, DgnCollector,
|
||||||
DgnImport, Path, PathBuilder, Segment, Sheet, SubPath, Text, DGN_DEFAULT_COLORS,
|
DgnColor, DgnElement, DgnGeom, DgnImport, DgnReference, Path, PathBuilder, Segment, Sheet,
|
||||||
|
SubPath, Text, DGN_DEFAULT_COLORS,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Compound file ───────────────────────────────────────────────────────────
|
// ── Compound file ───────────────────────────────────────────────────────────
|
||||||
@@ -278,6 +279,34 @@ fn f64_at(e: &[u8], at: usize) -> Option<f64> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A string stored after the "ff fe 01 00" marker, when the element has one.
|
/// A string stored after the "ff fe 01 00" marker, when the element has one.
|
||||||
|
/// The text style a text element uses: the id in its text style linkage
|
||||||
|
/// (the four bytes after `04 20 e6 04 00 00`).
|
||||||
|
fn text_style_id(e: &[u8]) -> Option<u32> {
|
||||||
|
let at = e.windows(6).position(|w| w == [0x04, 0x20, 0xe6, 0x04, 0x00, 0x00])? + 6;
|
||||||
|
u32_at(e, at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text style table (the table entries after a header of table 3):
|
||||||
|
/// each style's id and name.
|
||||||
|
fn text_style_table(nm: &[&[u8]]) -> Vec<(u32, String)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut table = 0;
|
||||||
|
for e in nm {
|
||||||
|
match kind(e) {
|
||||||
|
96 => table = u32_at(e, 12).unwrap_or(0),
|
||||||
|
95 if table == 3 => {
|
||||||
|
if let (Some(id), Some(name)) = (u32_at(e, 32), marked_string(e, 32)) {
|
||||||
|
if !out.iter().any(|(i, _)| *i == id) {
|
||||||
|
out.push((id, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn marked_string(e: &[u8], from: usize) -> Option<String> {
|
fn marked_string(e: &[u8], from: usize) -> Option<String> {
|
||||||
let i = e
|
let i = e
|
||||||
.get(from..)?
|
.get(from..)?
|
||||||
@@ -384,6 +413,100 @@ impl Styles {
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or([255, 255, 255])
|
.unwrap_or([255, 255, 255])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The colour word as an element's colour: the level's (0xFFFFFFFF),
|
||||||
|
/// the enclosing cell's (0xFFFFFFFE), or a table index (low byte) with
|
||||||
|
/// the table entry and the colour drawn.
|
||||||
|
fn color_word(&self, e: &[u8]) -> By<DgnColor> {
|
||||||
|
match u32_at(e, 52) {
|
||||||
|
None | Some(u32::MAX) => By::Level,
|
||||||
|
Some(0xFFFF_FFFE) => By::Cell,
|
||||||
|
Some(word) => {
|
||||||
|
let index = word & 0xff;
|
||||||
|
By::Own(DgnColor {
|
||||||
|
index,
|
||||||
|
table: self
|
||||||
|
.colors
|
||||||
|
.get(index as usize)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or([255, 255, 255]),
|
||||||
|
rgb: self.color(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A B-spline curve from its order and poles, with uniform knots. A closed
|
||||||
|
/// curve (its last pole repeating the first) is periodic: its distinct
|
||||||
|
/// poles wrap around with knots (i + 1) / (count + 1), and the curve is
|
||||||
|
/// clamped at both ends of its domain by knot insertion, as the reference
|
||||||
|
/// converts it.
|
||||||
|
fn bspline_geom(order: usize, poles: Vec<[f64; 3]>, closed: bool) -> DgnGeom {
|
||||||
|
let degree = order - 1;
|
||||||
|
let distinct = poles.len() - usize::from(closed && poles.first() == poles.last());
|
||||||
|
if !closed || distinct < order {
|
||||||
|
let interior = poles.len() - order;
|
||||||
|
let mut knots = vec![0.0; order];
|
||||||
|
knots.extend((1..=interior).map(|k| k as f64 / (interior + 1) as f64));
|
||||||
|
knots.extend(vec![1.0; order]);
|
||||||
|
return DgnGeom::Spline {
|
||||||
|
degree,
|
||||||
|
knots,
|
||||||
|
poles,
|
||||||
|
closed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let mut ctrl: Vec<[f64; 3]> = (0..distinct + degree).map(|i| poles[i % distinct]).collect();
|
||||||
|
let count = ctrl.len() + order;
|
||||||
|
let mut knots: Vec<f64> = (0..count).map(|i| (i + 1) as f64 / (count + 1) as f64).collect();
|
||||||
|
let (lo, hi) = (knots[degree], knots[ctrl.len()]);
|
||||||
|
let same = |a: f64, b: f64| (a - b).abs() < 1e-12;
|
||||||
|
// Knot insertion (Boehm) of t until it is a degree-fold knot.
|
||||||
|
let mut insert = |t: f64| loop {
|
||||||
|
let mult = knots.iter().filter(|k| same(**k, t)).count();
|
||||||
|
if mult >= degree {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(k) = knots.iter().rposition(|x| *x <= t + 1e-12) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut next = Vec::with_capacity(ctrl.len() + 1);
|
||||||
|
for i in 0..=ctrl.len() {
|
||||||
|
if i + degree <= k {
|
||||||
|
next.push(ctrl[i]);
|
||||||
|
} else if i + mult > k {
|
||||||
|
next.push(ctrl[i - 1]);
|
||||||
|
} else {
|
||||||
|
let a = (t - knots[i]) / (knots[i + degree] - knots[i]);
|
||||||
|
next.push(std::array::from_fn(|c| (1.0 - a) * ctrl[i - 1][c] + a * ctrl[i][c]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctrl = next;
|
||||||
|
knots.insert(k + 1, t);
|
||||||
|
};
|
||||||
|
insert(lo);
|
||||||
|
insert(hi);
|
||||||
|
// The domain clamped: an order-fold knot at each end, the poles that
|
||||||
|
// span it.
|
||||||
|
let first = knots.iter().position(|k| same(*k, lo)).unwrap_or(1).max(1);
|
||||||
|
let last = knots.iter().rposition(|k| same(*k, hi)).unwrap_or(knots.len() - 1);
|
||||||
|
let mut clamped = vec![lo];
|
||||||
|
clamped.extend_from_slice(&knots[first..=last]);
|
||||||
|
clamped.push(hi);
|
||||||
|
let poles: Vec<[f64; 3]> = ctrl
|
||||||
|
.iter()
|
||||||
|
.skip(first - 1)
|
||||||
|
.take(clamped.len() - order)
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let knots = clamped;
|
||||||
|
DgnGeom::Spline {
|
||||||
|
degree,
|
||||||
|
knots,
|
||||||
|
poles,
|
||||||
|
closed: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Plan geometry in UORs, and the shared cells it places.
|
/// Plan geometry in UORs, and the shared cells it places.
|
||||||
@@ -554,19 +677,31 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
[q[0], q[1]]
|
[q[0], q[1]]
|
||||||
};
|
};
|
||||||
let mut collect = DgnCollector::default();
|
let mut collect = DgnCollector::default();
|
||||||
|
// An open B-spline curve: order, pole count, closure and its header.
|
||||||
|
let mut spline: Option<(usize, usize, bool, DgnElement)> = None;
|
||||||
for (index, e) in run.iter().enumerate() {
|
for (index, e) in run.iter().enumerate() {
|
||||||
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
|
if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let color = styles.color(e);
|
let color = styles.color(e);
|
||||||
// Colour and weight words: 0xFFFFFFFF takes the level's; line style
|
// Colour and weight words: 0xFFFFFFFF takes the level's, 0xFFFFFFFE
|
||||||
// 0x7FFFFFFF (or 0xFFFFFFFF).
|
// the enclosing cell's; line style 0x7FFFFFFF (or 0xFFFFFFFF) the
|
||||||
|
// level's, 0x7FFFFFFE the cell's.
|
||||||
let element = |geom: DgnGeom| DgnElement {
|
let element = |geom: DgnGeom| DgnElement {
|
||||||
level: u32_at(e, 12).unwrap_or(0),
|
level: u32_at(e, 12).unwrap_or(0),
|
||||||
color: (u32_at(e, 52) != Some(u32::MAX)).then_some(color),
|
color: styles.color_word(e),
|
||||||
weight: u32_at(e, 48).filter(|w| *w != u32::MAX).map(|w| w & 31),
|
weight: match u32_at(e, 48) {
|
||||||
style: u32_at(e, 44).filter(|s| *s != u32::MAX && *s != 0x7FFF_FFFF),
|
None | Some(u32::MAX) => By::Level,
|
||||||
|
Some(0xFFFF_FFFE) => By::Cell,
|
||||||
|
Some(w) => By::Own(w & 31),
|
||||||
|
},
|
||||||
|
style: match u32_at(e, 44) {
|
||||||
|
None | Some(u32::MAX) | Some(0x7FFF_FFFF) => By::Level,
|
||||||
|
Some(0xFFFF_FFFE) | Some(0x7FFF_FFFE) => By::Cell,
|
||||||
|
Some(s) => By::Own(s),
|
||||||
|
},
|
||||||
geom,
|
geom,
|
||||||
|
filled: matches!(kind(e), 6 | 14) && e.windows(4).any(|w| w == [0x07, 0x10, 0x41, 0x00]),
|
||||||
};
|
};
|
||||||
let stroke = Some((color, -1.0));
|
let stroke = Some((color, -1.0));
|
||||||
let d3 = is_3d(e);
|
let d3 = is_3d(e);
|
||||||
@@ -578,6 +713,9 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
if d3 { f64_at(e, at + 16)? } else { 0.0 },
|
if d3 { f64_at(e, at + 16)? } else { 0.0 },
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
// A point or vector placed in the model (3D kept for the import).
|
||||||
|
let at3 = |p: [f64; 3]| xf_point(xf, p);
|
||||||
|
let dir3 = |v: [f64; 3]| xf_vector(xf, v);
|
||||||
match kind(e) {
|
match kind(e) {
|
||||||
// A text node: grouped when its count is followed by that many
|
// A text node: grouped when its count is followed by that many
|
||||||
// text components.
|
// text components.
|
||||||
@@ -587,7 +725,34 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
parts.iter().all(|p| p.len() >= 104 && kind(p) == 17 && is_component(p))
|
parts.iter().all(|p| p.len() >= 104 && kind(p) == 17 && is_component(p))
|
||||||
});
|
});
|
||||||
if texts {
|
if texts {
|
||||||
collect.open(n, element(DgnGeom::TextNode(Vec::new())));
|
// 2D: justification 118, origin 152 (the node's
|
||||||
|
// justification point).
|
||||||
|
let origin = (!d3).then(|| p3(152)).flatten().map(at3);
|
||||||
|
collect.open(
|
||||||
|
n,
|
||||||
|
element(DgnGeom::TextNode {
|
||||||
|
lines: Vec::new(),
|
||||||
|
origin,
|
||||||
|
just: if d3 { 0 } else { e.get(118).copied().unwrap_or(0) },
|
||||||
|
line_spacing: None,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A cell (not shared): its components follow, flagged as such.
|
||||||
|
2 => {
|
||||||
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
|
let parts = run
|
||||||
|
.get(index + 1..index + 1 + n)
|
||||||
|
.is_some_and(|parts| parts.iter().all(|p| is_component(p)));
|
||||||
|
if parts && n > 0 {
|
||||||
|
collect.open(
|
||||||
|
n,
|
||||||
|
element(DgnGeom::CellGroup {
|
||||||
|
name: marked_string(e, 104).unwrap_or_default(),
|
||||||
|
parts: Vec::new(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12 | 14 => {
|
12 | 14 => {
|
||||||
@@ -602,7 +767,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
}
|
}
|
||||||
3 => {
|
3 => {
|
||||||
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
|
if let (Some(a), Some(b)) = (p3(104), p3(if d3 { 128 } else { 120 })) {
|
||||||
collect.push(element(DgnGeom::Line(plan(a), plan(b))));
|
collect.push(element(DgnGeom::Line(at3(a), at3(b))));
|
||||||
push(
|
push(
|
||||||
&mut raw,
|
&mut raw,
|
||||||
&mut complex,
|
&mut complex,
|
||||||
@@ -620,12 +785,10 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
4 | 6 | 11 => {
|
4 | 6 | 11 => {
|
||||||
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
let stride = if d3 { 24 } else { 16 };
|
let stride = if d3 { 24 } else { 16 };
|
||||||
let pts: Vec<[f64; 2]> = (0..n)
|
let pts3: Vec<[f64; 3]> = (0..n).filter_map(|k| p3(112 + k * stride)).collect();
|
||||||
.filter_map(|k| p3(112 + k * stride))
|
let pts: Vec<[f64; 2]> = pts3.iter().map(|p| plan(*p)).collect();
|
||||||
.map(plan)
|
|
||||||
.collect();
|
|
||||||
if pts.len() >= 2 {
|
if pts.len() >= 2 {
|
||||||
collect.push(element(DgnGeom::Points(pts.clone())));
|
collect.push(element(DgnGeom::Points(pts3.iter().map(|p| at3(*p)).collect())));
|
||||||
let mut pb = PathBuilder::default();
|
let mut pb = PathBuilder::default();
|
||||||
pb.move_to(pts[0]);
|
pb.move_to(pts[0]);
|
||||||
for p in &pts[1..] {
|
for p in &pts[1..] {
|
||||||
@@ -702,11 +865,11 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
};
|
};
|
||||||
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9;
|
||||||
{
|
{
|
||||||
let (pu, pv) = (axis(u), axis(v));
|
let (du, dv) = (dir3(u), dir3(v));
|
||||||
collect.push(element(DgnGeom::Arc {
|
collect.push(element(DgnGeom::Arc {
|
||||||
center: plan(c),
|
center: at3(c),
|
||||||
u: [a * pu[0], a * pu[1]],
|
u: du.map(|x| a * x),
|
||||||
v: [b * pv[0], b * pv[1]],
|
v: dv.map(|x| b * x),
|
||||||
start,
|
start,
|
||||||
sweep,
|
sweep,
|
||||||
}));
|
}));
|
||||||
@@ -726,7 +889,8 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
}
|
}
|
||||||
17 => {
|
17 => {
|
||||||
// 3D: width 112, height 120, quaternion 144, origin 176, the
|
// 3D: width 112, height 120, quaternion 144, origin 176, the
|
||||||
// characters after the string marker. 2D: angle 144, origin
|
// characters after the string marker, as many as the byte
|
||||||
|
// count at 110 holds. 2D: angle 144, origin
|
||||||
// 152, a byte count (with its 4-byte marker) at 110 and the
|
// 152, a byte count (with its 4-byte marker) at 110 and the
|
||||||
// characters from 174.
|
// characters from 174.
|
||||||
let placed = if d3 {
|
let placed = if d3 {
|
||||||
@@ -736,7 +900,12 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
f64_at(e, 120)?,
|
f64_at(e, 120)?,
|
||||||
quat_axes(e, 144)?,
|
quat_axes(e, 144)?,
|
||||||
p3(176)?,
|
p3(176)?,
|
||||||
marked_string(e, 200)?,
|
// The byte count at 110 (with the 4-byte marker)
|
||||||
|
// ends the characters.
|
||||||
|
{
|
||||||
|
let count = u16::from_le_bytes(e.get(110..112)?.try_into().ok()?) as usize;
|
||||||
|
marked_string(e, 200)?.chars().take(count.saturating_sub(4)).collect()
|
||||||
|
},
|
||||||
))
|
))
|
||||||
})()
|
})()
|
||||||
} else {
|
} else {
|
||||||
@@ -758,6 +927,16 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
let Some((width, height, (u, v), origin, text)) = placed else {
|
let Some((width, height, (u, v), origin, text)) = placed else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// 2D: the justification (108) and the length of the text box
|
||||||
|
// along the baseline (128).
|
||||||
|
let (just, length) = if d3 {
|
||||||
|
(2, 0.0)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
e.get(108).copied().unwrap_or(2),
|
||||||
|
f64_at(e, 128).filter(|l| l.is_finite()).unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
};
|
||||||
// The text's axes as the plan shows them: its baseline sets the
|
// The text's axes as the plan shows them: its baseline sets the
|
||||||
// angle, and each axis's plan length scales its size.
|
// angle, and each axis's plan length scales its size.
|
||||||
let (pu, pv) = (axis(u), axis(v));
|
let (pu, pv) = (axis(u), axis(v));
|
||||||
@@ -769,11 +948,14 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
let width_factor = if height > 0.0 { width / height } else { 1.0 };
|
let width_factor = if height > 0.0 { width / height } else { 1.0 };
|
||||||
collect.push(element(DgnGeom::Text {
|
collect.push(element(DgnGeom::Text {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
origin: plan(origin),
|
origin: at3(origin),
|
||||||
height,
|
height,
|
||||||
width_factor,
|
width_factor,
|
||||||
|
length: length * su,
|
||||||
rotation: pu[1].atan2(pu[0]),
|
rotation: pu[1].atan2(pu[0]),
|
||||||
|
just,
|
||||||
font: None,
|
font: None,
|
||||||
|
style: text_style_id(e),
|
||||||
}));
|
}));
|
||||||
raw.texts.push(Text {
|
raw.texts.push(Text {
|
||||||
text,
|
text,
|
||||||
@@ -804,7 +986,7 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
collect.push(element(DgnGeom::Cell {
|
collect.push(element(DgnGeom::Cell {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
m: [m[0], m[1], m[3], m[4]],
|
m: [m[0], m[1], m[3], m[4]],
|
||||||
origin: [o[0], o[1]],
|
origin: o,
|
||||||
z_scale: m[8],
|
z_scale: m[8],
|
||||||
}));
|
}));
|
||||||
raw.instances.push(Instance {
|
raw.instances.push(Instance {
|
||||||
@@ -812,6 +994,48 @@ fn read_run(run: &[&[u8]], styles: &Styles, xf: &Xf) -> Raw {
|
|||||||
xf: xf_compose(xf, &local),
|
xf: xf_compose(xf, &local),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
27 => {
|
||||||
|
// B-spline curve header: order and closure in the word at
|
||||||
|
// 108, pole count 112; its poles follow (type 21).
|
||||||
|
let flags = u16::from_le_bytes([e[108], e[109]]);
|
||||||
|
spline = Some((
|
||||||
|
((flags & 0x0f) + 2) as usize,
|
||||||
|
u32_at(e, 112).unwrap_or(0) as usize,
|
||||||
|
flags & 0x0180 != 0,
|
||||||
|
element(DgnGeom::Chain(Vec::new())),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
21 => {
|
||||||
|
let n = u32_at(e, 104).unwrap_or(0) as usize;
|
||||||
|
let stride = if d3 { 24 } else { 16 };
|
||||||
|
let poles: Vec<[f64; 3]> = (0..n).filter_map(|k| p3(112 + k * stride)).collect();
|
||||||
|
if let Some((order, count, closed, header)) = spline.take() {
|
||||||
|
if poles.len() == count && count >= order && order >= 2 {
|
||||||
|
let poles: Vec<[f64; 3]> = poles.iter().map(|p| at3(*p)).collect();
|
||||||
|
let geom = bspline_geom(order, poles, closed);
|
||||||
|
if let DgnGeom::Spline { degree, knots, poles, .. } = &geom {
|
||||||
|
let plan_poles: Vec<[f64; 2]> =
|
||||||
|
poles.iter().map(|p| [p[0], p[1]]).collect();
|
||||||
|
let pts = bspline_points(degree + 1, &plan_poles, knots, 24 * count);
|
||||||
|
let mut pb = PathBuilder::default();
|
||||||
|
pb.move_to(pts[0]);
|
||||||
|
for p in &pts[1..] {
|
||||||
|
pb.line_to(*p);
|
||||||
|
}
|
||||||
|
push(
|
||||||
|
&mut raw,
|
||||||
|
&mut complex,
|
||||||
|
Path {
|
||||||
|
subpaths: pb.finish(),
|
||||||
|
stroke,
|
||||||
|
fill: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
collect.push(DgnElement { geom, ..header });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -983,12 +1207,14 @@ fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The level table with the file's own names.
|
/// The level table with the file's own names: the first one (the file's;
|
||||||
|
/// a later level table holds a reference attachment's levels).
|
||||||
fn level_table_raw(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
fn level_table_raw(nm: &[&[u8]]) -> Vec<(u32, String, u32)> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut in_levels = false;
|
let mut in_levels = false;
|
||||||
for e in nm {
|
for e in nm {
|
||||||
match kind(e) {
|
match kind(e) {
|
||||||
|
96 if u32_at(e, 12) == Some(1) && !out.is_empty() => break,
|
||||||
96 => in_levels = u32_at(e, 12) == Some(1),
|
96 => in_levels = u32_at(e, 12) == Some(1),
|
||||||
95 if in_levels && e.len() >= 80 => {
|
95 if in_levels && e.len() >= 80 => {
|
||||||
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
|
let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else {
|
||||||
@@ -1156,13 +1382,108 @@ fn unit_name(metres: f64) -> String {
|
|||||||
/// The named model element by element (DGNIMPORT): in master units with
|
/// The named model element by element (DGNIMPORT): in master units with
|
||||||
/// the global origin at 0,0, shared cell definitions in master units, and
|
/// the global origin at 0,0, shared cell definitions in master units, and
|
||||||
/// every level by id with its own name and colour.
|
/// every level by id with its own name and colour.
|
||||||
|
/// A design model (not a sheet): the model type byte of its header (4105)
|
||||||
|
/// is 2 for a sheet.
|
||||||
|
fn is_design(header: &[u8]) -> bool {
|
||||||
|
header.get(4105) != Some(&2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The models DGNIMPORT offers: the design models (sheets are not
|
||||||
|
/// imported).
|
||||||
|
pub fn design_model_names(bytes: &[u8]) -> Option<Vec<String>> {
|
||||||
|
let cfb = Cfb::open(bytes)?;
|
||||||
|
let names: Vec<String> = models(&cfb)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|m| is_design(&m.2))
|
||||||
|
.map(|m| m.0)
|
||||||
|
.collect();
|
||||||
|
(!names.is_empty()).then_some(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control stream (Dgn^C) of each model, in the order `models` lists
|
||||||
|
/// them.
|
||||||
|
fn control_streams(cfb: &Cfb) -> Vec<Vec<u8>> {
|
||||||
|
let streams = cfb.streams();
|
||||||
|
let mut dirs: Vec<String> = streams
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(p, _)| p.strip_suffix("/Dgn^G/$1").map(|d| d.to_string()))
|
||||||
|
.collect();
|
||||||
|
dirs.sort();
|
||||||
|
dirs.iter()
|
||||||
|
.map(|dir| {
|
||||||
|
streams
|
||||||
|
.iter()
|
||||||
|
.find(|(p, _)| *p == format!("{dir}/Dgn^C/$1"))
|
||||||
|
.map(|(_, e)| inflate(&cfb.stream(e)))
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every string of an element stored after the "ff fe 01 00" marker, with
|
||||||
|
/// the subtype of the string linkage before it (2 file name, 3 full path,
|
||||||
|
/// 4 logical name).
|
||||||
|
fn linked_strings(e: &[u8]) -> Vec<(u32, String)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut from = 0;
|
||||||
|
while let Some(i) = e
|
||||||
|
.get(from..)
|
||||||
|
.and_then(|rest| rest.windows(4).position(|w| w == [0xff, 0xfe, 0x01, 0x00]))
|
||||||
|
{
|
||||||
|
let at = from + i;
|
||||||
|
let end = e[at + 4..]
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == 0 || c < 0x20)
|
||||||
|
.map(|n| at + 4 + n)
|
||||||
|
.unwrap_or(e.len());
|
||||||
|
let subtype = at.checked_sub(8).and_then(|p| u32_at(e, p)).unwrap_or(0);
|
||||||
|
out.push((subtype, String::from_utf8_lossy(&e[at + 4..end]).to_string()));
|
||||||
|
from = end.max(at + 4);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reference attachments (type 100) of a model's control stream: the
|
||||||
|
/// file (full path, else file name), origin 192, rotation rows from 216,
|
||||||
|
/// scale 288, in UORs of the model.
|
||||||
|
fn references(control: &[u8]) -> Vec<DgnReference> {
|
||||||
|
elements(control)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|e| kind(e) == 100 && e.len() >= 296)
|
||||||
|
.filter_map(|e| {
|
||||||
|
let strings = linked_strings(e);
|
||||||
|
let file = strings
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.0 == 3)
|
||||||
|
.or_else(|| strings.iter().find(|s| s.0 == 2))?
|
||||||
|
.1
|
||||||
|
.clone();
|
||||||
|
Some(DgnReference {
|
||||||
|
file,
|
||||||
|
model: strings
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.0 == 1)
|
||||||
|
.map(|s| s.1.clone())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
origin: [f64_at(e, 192)?, f64_at(e, 200)?, f64_at(e, 208)?],
|
||||||
|
scale: f64_at(e, 288).filter(|s| *s > 0.0).unwrap_or(1.0),
|
||||||
|
rotation: f64_at(e, 240)?.atan2(f64_at(e, 216)?),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
||||||
let cfb = Cfb::open(bytes)?;
|
let cfb = Cfb::open(bytes)?;
|
||||||
let all = models(&cfb);
|
let all = models(&cfb);
|
||||||
let (_, graphics, header) = all
|
let controls = control_streams(&cfb);
|
||||||
|
let designs: Vec<usize> = (0..all.len()).filter(|k| is_design(&all[*k].2)).collect();
|
||||||
|
let k = designs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| m.0.eq_ignore_ascii_case(name))
|
.copied()
|
||||||
.or_else(|| all.first())?;
|
.find(|k| all[*k].0.eq_ignore_ascii_case(name))
|
||||||
|
.or_else(|| designs.first().copied())?;
|
||||||
|
let (_, graphics, header) = &all[k];
|
||||||
let non_model = non_model(&cfb);
|
let non_model = non_model(&cfb);
|
||||||
let nm = elements(&non_model);
|
let nm = elements(&non_model);
|
||||||
let colors = nm
|
let colors = nm
|
||||||
@@ -1184,6 +1505,7 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
|||||||
(*id, (name.clone(), rgb))
|
(*id, (name.clone(), rgb))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let level_order = table.iter().map(|l| l.0).collect();
|
||||||
let (per_unit, sub_uor) = units(header);
|
let (per_unit, sub_uor) = units(header);
|
||||||
let s = 1.0 / per_unit;
|
let s = 1.0 / per_unit;
|
||||||
let mut cells = Vec::new();
|
let mut cells = Vec::new();
|
||||||
@@ -1195,8 +1517,12 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
|||||||
while j < nm.len() && is_component(nm[j]) {
|
while j < nm.len() && is_component(nm[j]) {
|
||||||
j += 1;
|
j += 1;
|
||||||
}
|
}
|
||||||
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
|
// The cell that draws points is the writer's own; the reference
|
||||||
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
|
// leaves it out of the drawing.
|
||||||
|
if !cell.eq_ignore_ascii_case("POINT_DISPLAY") {
|
||||||
|
let def = read_run(&nm[i + 1..j], &styles, &IDENTITY).elements;
|
||||||
|
cells.push((cell, scaled_elements(&def, s, [0.0, 0.0])));
|
||||||
|
}
|
||||||
i = j;
|
i = j;
|
||||||
} else {
|
} else {
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -1210,13 +1536,30 @@ pub fn import(bytes: &[u8], name: &str) -> Option<DgnImport> {
|
|||||||
(Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n),
|
(Some(n), Some(d)) if n > 0.0 && d > 0.0 => unit_name(d / n),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
|
let references = controls
|
||||||
|
.get(k)
|
||||||
|
.map(|c| references(c))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| DgnReference {
|
||||||
|
origin: [
|
||||||
|
(r.origin[0] - origin[0]) * s,
|
||||||
|
(r.origin[1] - origin[1]) * s,
|
||||||
|
r.origin[2] * s,
|
||||||
|
],
|
||||||
|
..r
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
Some(DgnImport {
|
Some(DgnImport {
|
||||||
elements: scaled_elements(&raw.elements, s, [-origin[0] * s, -origin[1] * s]),
|
elements: scaled_elements(&raw.elements, s, [-origin[0] * s, -origin[1] * s]),
|
||||||
cells,
|
cells,
|
||||||
levels,
|
levels,
|
||||||
|
level_order,
|
||||||
sub_per_master: per_unit / sub_uor,
|
sub_per_master: per_unit / sub_uor,
|
||||||
unit_names: (metres(4180), metres(4196)),
|
unit_names: (metres(4180), metres(4196)),
|
||||||
v7: false,
|
v7: false,
|
||||||
|
references,
|
||||||
|
text_styles: text_style_table(&nm),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ use std::sync::{Arc, Mutex, OnceLock};
|
|||||||
|
|
||||||
use codec::entities::UnderlayType;
|
use codec::entities::UnderlayType;
|
||||||
|
|
||||||
pub use model::{scaled_elements, DgnElement, DgnGeom, DgnImport, Path, Segment, Sheet, SubPath};
|
pub use model::{
|
||||||
|
scaled_elements, By, DgnColor, DgnElement, DgnGeom, DgnImport, DgnReference, Path, Segment,
|
||||||
|
Sheet, SubPath, P3,
|
||||||
|
};
|
||||||
|
|
||||||
use super::pdf_raster::{source_bytes, PdfPage};
|
use super::pdf_raster::{source_bytes, PdfPage};
|
||||||
|
|
||||||
@@ -35,6 +38,17 @@ pub fn dgn_import(path: &str, item: &str) -> Option<DgnImport> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The models DGNIMPORT offers: a V7 file's one model, a V8 file's design
|
||||||
|
/// models (sheet models are not imported).
|
||||||
|
pub fn dgn_import_models(path: &str) -> Option<Vec<String>> {
|
||||||
|
let bytes = source_bytes(path)?;
|
||||||
|
if dgn7::is_v7(&bytes) {
|
||||||
|
dgn7::model_names(&bytes)
|
||||||
|
} else {
|
||||||
|
dgn8::design_model_names(&bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Sheet names (DWF) or model names (DGN) of a file.
|
/// Sheet names (DWF) or model names (DGN) of a file.
|
||||||
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
|
pub fn item_names(kind: UnderlayType, path: &str) -> Option<Vec<String>> {
|
||||||
let bytes = source_bytes(path)?;
|
let bytes = source_bytes(path)?;
|
||||||
|
|||||||
@@ -532,55 +532,112 @@ pub fn dgn_color_table(e: &[u8], first: usize) -> Option<Vec<[u8; 3]>> {
|
|||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A symbology attribute of a DGN element: the level's, the enclosing
|
||||||
|
/// cell's (V8 "by cell"), or its own.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum By<T> {
|
||||||
|
Level,
|
||||||
|
Cell,
|
||||||
|
Own(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> By<T> {
|
||||||
|
pub fn own(self) -> Option<T> {
|
||||||
|
match self {
|
||||||
|
By::Own(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An element's own colour: its index in the file's colour table, that
|
||||||
|
/// table entry's RGB and the RGB it is drawn in (a true colour can differ
|
||||||
|
/// from the table entry it names).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct DgnColor {
|
||||||
|
pub index: u32,
|
||||||
|
pub table: [u8; 3],
|
||||||
|
pub rgb: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
/// A DGN element as drawing geometry (what DGNIMPORT converts): its level,
|
/// A DGN element as drawing geometry (what DGNIMPORT converts): its level,
|
||||||
/// symbology and shape, in the units of the run that read it.
|
/// symbology and shape, in the units of the run that read it.
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct DgnElement {
|
pub struct DgnElement {
|
||||||
/// Level number (V7) or id (V8).
|
/// Level number (V7) or id (V8).
|
||||||
pub level: u32,
|
pub level: u32,
|
||||||
/// Colour; `None` takes the level's.
|
pub color: By<DgnColor>,
|
||||||
pub color: Option<[u8; 3]>,
|
/// Weight 0-31.
|
||||||
/// Weight 0-31; `None` takes the level's.
|
pub weight: By<u32>,
|
||||||
pub weight: Option<u32>,
|
/// Line style.
|
||||||
/// Line style; `None` takes the level's.
|
pub style: By<u32>,
|
||||||
pub style: Option<u32>,
|
|
||||||
pub geom: DgnGeom,
|
pub geom: DgnGeom,
|
||||||
|
/// The element is a filled shape (it has a fill linkage).
|
||||||
|
pub filled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A point of a DGN element (z 0 in a 2D file).
|
||||||
|
pub type P3 = [f64; 3];
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum DgnGeom {
|
pub enum DgnGeom {
|
||||||
Line([f64; 2], [f64; 2]),
|
Line(P3, P3),
|
||||||
/// A line string, curve or B-spline (sampled), or a shape (its points
|
/// A line string, curve or shape (its points end where they start).
|
||||||
/// end where they start).
|
Points(Vec<P3>),
|
||||||
Points(Vec<[f64; 2]>),
|
|
||||||
/// An elliptical arc: the point at parameter t is
|
/// An elliptical arc: the point at parameter t is
|
||||||
/// `center + cos t · u + sin t · v`, from `start` over `sweep` (radians).
|
/// `center + cos t · u + sin t · v`, from `start` over `sweep` (radians).
|
||||||
Arc {
|
Arc {
|
||||||
center: [f64; 2],
|
center: P3,
|
||||||
u: [f64; 2],
|
u: P3,
|
||||||
v: [f64; 2],
|
v: P3,
|
||||||
start: f64,
|
start: f64,
|
||||||
sweep: f64,
|
sweep: f64,
|
||||||
},
|
},
|
||||||
|
/// A B-spline curve: degree, full knot vector and poles.
|
||||||
|
Spline {
|
||||||
|
degree: usize,
|
||||||
|
knots: Vec<f64>,
|
||||||
|
poles: Vec<P3>,
|
||||||
|
closed: bool,
|
||||||
|
},
|
||||||
/// A complex chain or shape: its components in order.
|
/// A complex chain or shape: its components in order.
|
||||||
Chain(Vec<DgnGeom>),
|
Chain(Vec<DgnGeom>),
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
origin: [f64; 2],
|
/// The text's lower left (baseline start).
|
||||||
|
origin: P3,
|
||||||
height: f64,
|
height: f64,
|
||||||
width_factor: f64,
|
width_factor: f64,
|
||||||
|
/// Length of the text along its baseline (its justification box).
|
||||||
|
length: f64,
|
||||||
rotation: f64,
|
rotation: f64,
|
||||||
|
/// DGN justification code (2 left bottom).
|
||||||
|
just: u8,
|
||||||
/// Font number (V7); `None` when the file's font is not read.
|
/// Font number (V7); `None` when the file's font is not read.
|
||||||
font: Option<u32>,
|
font: Option<u32>,
|
||||||
|
/// Text style id (V8), an entry of [`DgnImport::text_styles`].
|
||||||
|
style: Option<u32>,
|
||||||
|
},
|
||||||
|
/// A text node: its lines, its origin (the justification point, when
|
||||||
|
/// read), its justification and (V7) the space between its lines.
|
||||||
|
TextNode {
|
||||||
|
lines: Vec<DgnGeom>,
|
||||||
|
origin: Option<P3>,
|
||||||
|
just: u8,
|
||||||
|
line_spacing: Option<f64>,
|
||||||
|
},
|
||||||
|
/// A cell that is not shared: its name (empty when anonymous) and its
|
||||||
|
/// components, each with its own level and symbology.
|
||||||
|
CellGroup {
|
||||||
|
name: String,
|
||||||
|
parts: Vec<DgnElement>,
|
||||||
},
|
},
|
||||||
/// A text node: its lines.
|
|
||||||
TextNode(Vec<DgnGeom>),
|
|
||||||
/// A shared cell instance: x' = m[0]·x + m[1]·y + origin.x,
|
/// A shared cell instance: x' = m[0]·x + m[1]·y + origin.x,
|
||||||
/// y' = m[2]·x + m[3]·y + origin.y.
|
/// y' = m[2]·x + m[3]·y + origin.y.
|
||||||
Cell {
|
Cell {
|
||||||
name: String,
|
name: String,
|
||||||
m: [f64; 4],
|
m: [f64; 4],
|
||||||
origin: [f64; 2],
|
origin: P3,
|
||||||
/// Scale along z (1 in a 2D file).
|
/// Scale along z (1 in a 2D file).
|
||||||
z_scale: f64,
|
z_scale: f64,
|
||||||
},
|
},
|
||||||
@@ -589,8 +646,8 @@ pub enum DgnGeom {
|
|||||||
impl DgnGeom {
|
impl DgnGeom {
|
||||||
/// The geometry through x' = s·x + t (lengths scaled by `s`).
|
/// The geometry through x' = s·x + t (lengths scaled by `s`).
|
||||||
pub fn scaled(&self, s: f64, t: [f64; 2]) -> DgnGeom {
|
pub fn scaled(&self, s: f64, t: [f64; 2]) -> DgnGeom {
|
||||||
let p = |q: [f64; 2]| [q[0] * s + t[0], q[1] * s + t[1]];
|
let p = |q: P3| [q[0] * s + t[0], q[1] * s + t[1], q[2] * s];
|
||||||
let v = |q: [f64; 2]| [q[0] * s, q[1] * s];
|
let v = |q: P3| [q[0] * s, q[1] * s, q[2] * s];
|
||||||
match self {
|
match self {
|
||||||
DgnGeom::Line(a, b) => DgnGeom::Line(p(*a), p(*b)),
|
DgnGeom::Line(a, b) => DgnGeom::Line(p(*a), p(*b)),
|
||||||
DgnGeom::Points(pts) => DgnGeom::Points(pts.iter().map(|q| p(*q)).collect()),
|
DgnGeom::Points(pts) => DgnGeom::Points(pts.iter().map(|q| p(*q)).collect()),
|
||||||
@@ -607,24 +664,53 @@ impl DgnGeom {
|
|||||||
start: *start,
|
start: *start,
|
||||||
sweep: *sweep,
|
sweep: *sweep,
|
||||||
},
|
},
|
||||||
|
DgnGeom::Spline {
|
||||||
|
degree,
|
||||||
|
knots,
|
||||||
|
poles,
|
||||||
|
closed,
|
||||||
|
} => DgnGeom::Spline {
|
||||||
|
degree: *degree,
|
||||||
|
knots: knots.clone(),
|
||||||
|
poles: poles.iter().map(|q| p(*q)).collect(),
|
||||||
|
closed: *closed,
|
||||||
|
},
|
||||||
DgnGeom::Chain(parts) => DgnGeom::Chain(parts.iter().map(|g| g.scaled(s, t)).collect()),
|
DgnGeom::Chain(parts) => DgnGeom::Chain(parts.iter().map(|g| g.scaled(s, t)).collect()),
|
||||||
DgnGeom::TextNode(parts) => {
|
DgnGeom::CellGroup { name, parts } => DgnGeom::CellGroup {
|
||||||
DgnGeom::TextNode(parts.iter().map(|g| g.scaled(s, t)).collect())
|
name: name.clone(),
|
||||||
}
|
parts: scaled_elements(parts, s, t),
|
||||||
|
},
|
||||||
|
DgnGeom::TextNode {
|
||||||
|
lines,
|
||||||
|
origin,
|
||||||
|
just,
|
||||||
|
line_spacing,
|
||||||
|
} => DgnGeom::TextNode {
|
||||||
|
lines: lines.iter().map(|g| g.scaled(s, t)).collect(),
|
||||||
|
origin: origin.map(p),
|
||||||
|
just: *just,
|
||||||
|
line_spacing: line_spacing.map(|l| l * s),
|
||||||
|
},
|
||||||
DgnGeom::Text {
|
DgnGeom::Text {
|
||||||
text,
|
text,
|
||||||
origin,
|
origin,
|
||||||
height,
|
height,
|
||||||
width_factor,
|
width_factor,
|
||||||
|
length,
|
||||||
rotation,
|
rotation,
|
||||||
|
just,
|
||||||
font,
|
font,
|
||||||
|
style,
|
||||||
} => DgnGeom::Text {
|
} => DgnGeom::Text {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
origin: p(*origin),
|
origin: p(*origin),
|
||||||
height: height * s,
|
height: height * s,
|
||||||
width_factor: *width_factor,
|
width_factor: *width_factor,
|
||||||
|
length: length * s,
|
||||||
rotation: *rotation,
|
rotation: *rotation,
|
||||||
|
just: *just,
|
||||||
font: *font,
|
font: *font,
|
||||||
|
style: *style,
|
||||||
},
|
},
|
||||||
DgnGeom::Cell {
|
DgnGeom::Cell {
|
||||||
name,
|
name,
|
||||||
@@ -662,8 +748,12 @@ impl DgnCollector {
|
|||||||
|
|
||||||
pub fn push(&mut self, element: DgnElement) {
|
pub fn push(&mut self, element: DgnElement) {
|
||||||
if let Some((left, header)) = self.open.as_mut() {
|
if let Some((left, header)) = self.open.as_mut() {
|
||||||
if let DgnGeom::Chain(parts) | DgnGeom::TextNode(parts) = &mut header.geom {
|
match &mut header.geom {
|
||||||
parts.push(element.geom);
|
DgnGeom::Chain(parts) | DgnGeom::TextNode { lines: parts, .. } => {
|
||||||
|
parts.push(element.geom)
|
||||||
|
}
|
||||||
|
DgnGeom::CellGroup { parts, .. } => parts.push(element),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
*left -= 1;
|
*left -= 1;
|
||||||
if *left == 0 {
|
if *left == 0 {
|
||||||
@@ -690,11 +780,31 @@ pub struct DgnImport {
|
|||||||
pub elements: Vec<DgnElement>,
|
pub elements: Vec<DgnElement>,
|
||||||
pub cells: Vec<(String, Vec<DgnElement>)>,
|
pub cells: Vec<(String, Vec<DgnElement>)>,
|
||||||
pub levels: std::collections::HashMap<u32, (String, Option<[u8; 3]>)>,
|
pub levels: std::collections::HashMap<u32, (String, Option<[u8; 3]>)>,
|
||||||
|
/// Level ids in the file's level table order; a V8 file's levels all
|
||||||
|
/// become layers, used or not.
|
||||||
|
pub level_order: Vec<u32>,
|
||||||
pub sub_per_master: f64,
|
pub sub_per_master: f64,
|
||||||
/// Master and sub unit names, when the file has them.
|
/// Master and sub unit names, when the file has them.
|
||||||
pub unit_names: (String, String),
|
pub unit_names: (String, String),
|
||||||
/// A V7 file (its elements carry their own line style and weight).
|
/// A V7 file (its elements carry their own line style and weight).
|
||||||
pub v7: bool,
|
pub v7: bool,
|
||||||
|
/// The model's reference attachments.
|
||||||
|
pub references: Vec<DgnReference>,
|
||||||
|
/// The text style table (V8): each style's id and name.
|
||||||
|
pub text_styles: Vec<(u32, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference attachment of a DGN model: the file and model it shows and
|
||||||
|
/// how it is placed (in master units of the attaching model).
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
pub struct DgnReference {
|
||||||
|
/// The file as the attachment names it.
|
||||||
|
pub file: String,
|
||||||
|
pub model: String,
|
||||||
|
pub origin: P3,
|
||||||
|
/// Scale of the reference's master units into the model's.
|
||||||
|
pub scale: f64,
|
||||||
|
pub rotation: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The elements through x' = s·x + t.
|
/// The elements through x' = s·x + t.
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ pub enum ImportDialogMsg {
|
|||||||
DgnPrefix(bool),
|
DgnPrefix(bool),
|
||||||
DgnSub(bool),
|
DgnSub(bool),
|
||||||
DgnExplode(bool),
|
DgnExplode(bool),
|
||||||
|
DgnReferences(crate::modules::insert::dgn_import::ReferenceMode),
|
||||||
|
DgnExisting(crate::modules::insert::dgn_import::ExistingReference),
|
||||||
DgnMappingTab(usize),
|
DgnMappingTab(usize),
|
||||||
DgnOk,
|
DgnOk,
|
||||||
Help(&'static str),
|
Help(&'static str),
|
||||||
@@ -142,6 +144,8 @@ pub struct DgnImportState {
|
|||||||
pub prefix_duplicates: bool,
|
pub prefix_duplicates: bool,
|
||||||
pub sub_units: bool,
|
pub sub_units: bool,
|
||||||
pub explode_text_nodes: bool,
|
pub explode_text_nodes: bool,
|
||||||
|
pub references: crate::modules::insert::dgn_import::ReferenceMode,
|
||||||
|
pub existing_reference: crate::modules::insert::dgn_import::ExistingReference,
|
||||||
/// Master and sub unit names.
|
/// Master and sub unit names.
|
||||||
pub units: (String, String),
|
pub units: (String, String),
|
||||||
pub mapping_tab: usize,
|
pub mapping_tab: usize,
|
||||||
@@ -564,21 +568,49 @@ pub fn view_dgn_import<'a>(
|
|||||||
]
|
]
|
||||||
.spacing(8),
|
.spacing(8),
|
||||||
);
|
);
|
||||||
// The DGN readers do not read reference attachments, so their handling
|
use crate::modules::insert::dgn_import::{ExistingReference as E, ReferenceMode as R};
|
||||||
// is shown but cannot be chosen.
|
let translate = state.references == R::Translate;
|
||||||
let references = card(
|
let references = card(
|
||||||
t!("External DGN references").into_owned(),
|
t!("External DGN references").into_owned(),
|
||||||
column![
|
column![
|
||||||
segmented(vec![
|
segmented(vec![
|
||||||
(t!("Bind as block / xref").into_owned(), false, None),
|
(
|
||||||
(t!("Ignore").into_owned(), true, None),
|
t!("Bind as block / xref").into_owned(),
|
||||||
(t!("Attach as DGN underlay").into_owned(), false, None),
|
translate,
|
||||||
|
Some(msg(ImportDialogMsg::DgnReferences(R::Translate)))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
t!("Ignore").into_owned(),
|
||||||
|
state.references == R::Ignore,
|
||||||
|
Some(msg(ImportDialogMsg::DgnReferences(R::Ignore)))
|
||||||
|
),
|
||||||
|
(
|
||||||
|
t!("Attach as DGN underlay").into_owned(),
|
||||||
|
state.references == R::Underlay,
|
||||||
|
Some(msg(ImportDialogMsg::DgnReferences(R::Underlay)))
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
text(t!("Reference attachments of DGN files are not read; they are left out."))
|
text(t!("When the translated drawing exists"))
|
||||||
.size(11)
|
.size(10)
|
||||||
.style(muted_style),
|
.style(muted_style),
|
||||||
|
segmented(
|
||||||
|
[
|
||||||
|
(t!("Prompt to overwrite"), E::Prompt),
|
||||||
|
(t!("Overwrite without prompting"), E::Overwrite),
|
||||||
|
(t!("Do not overwrite"), E::Keep),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, value)| {
|
||||||
|
(
|
||||||
|
label.into_owned(),
|
||||||
|
translate && state.existing_reference == value,
|
||||||
|
translate.then(|| msg(ImportDialogMsg::DgnExisting(value))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.spacing(8),
|
.spacing(6),
|
||||||
);
|
);
|
||||||
let tabs = [t!("Layer"), t!("Linetype"), t!("Lineweight"), t!("Color")];
|
let tabs = [t!("Layer"), t!("Linetype"), t!("Lineweight"), t!("Color")];
|
||||||
let mut table = column![row![
|
let mut table = column![row![
|
||||||
|
|||||||
Reference in new issue
Block a user