diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 19ffda91a..3dc0cba41 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -3560,7 +3560,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 389d66f91..96c7bc7a2 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index bfca85166..f4e7cae6b 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3545,7 +3545,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index e701d670c..614f3588f 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3548,7 +3548,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index cf565cd37..c6060e0f4 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -3548,7 +3548,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index a00e6dfaf..07e34760b 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3549,7 +3549,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 79d7a4119..06cc70c2a 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3548,7 +3548,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 528056696..1e4b4d405 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index c510c992c..22dfccd89 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3548,7 +3548,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index c03aab010..46a08dfbc 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3536,7 +3536,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 44a39fee8..78bb7f75e 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 656d9edae..6a8493c26 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 2c27e4501..3320f7c34 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3538,7 +3538,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index e08d60cc1..e127700da 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index f41cc6de4..3c6059bc8 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3546,7 +3546,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index dc6a7936d..a5d3cc97d 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 0f64baf7f..d0b994b58 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3548,7 +3548,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 49d53d053..d31a823d9 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3549,7 +3549,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 5983e2d36..2917d9020 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3522,7 +3522,7 @@ insert = .pdfui-vector-geometry = Vektör geometri .pdfui-pdf-data-to-import = İçe aktarılacak PDF verisi .pdfui-select-an-underlay-to-view-its-layers = Katmanlarını görmek için bir altlık seçin. - .pdfui-this-file-does-not-contain-any-layers = Bu dosya herhangi bir katman içermiyor. + .pdfui-this-file-does-not-contain-any-layers = Bu dosya herhangi bir katman içermiyor .pdfui-search-for-layer = Katman ara .pdfui-reference-name = Referans adı: .pdfui-page-size = Sayfa boyutu: diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 44d86efb5..7adf15625 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3535,7 +3535,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 8b6020f10..35e1a6934 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3547,7 +3547,7 @@ insert = .pdfui-vector-geometry = Vector geometry .pdfui-pdf-data-to-import = PDF data to import .pdfui-select-an-underlay-to-view-its-layers = Select an underlay to view its layers. - .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers. + .pdfui-this-file-does-not-contain-any-layers = This file does not contain any layers .pdfui-search-for-layer = Search for layer .pdfui-reference-name = Reference name: .pdfui-page-size = Page size: diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index 52bce9863..9c29cc07f 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -709,9 +709,14 @@ impl OpenCADStudio { Err(_) => self.command_line.push_error(&format!("{path} not found.")), } } - "PDFCLIP" => { + "PDFCLIP" | "DWFCLIP" | "DGNCLIP" => { use crate::command::CadCommand; - let command = crate::modules::insert::pdf_clip::PdfClipCommand::new(); + let kind = match cmd { + "DWFCLIP" => codec::entities::UnderlayType::Dwf, + "DGNCLIP" => codec::entities::UnderlayType::Dgn, + _ => codec::entities::UnderlayType::Pdf, + }; + let command = crate::modules::insert::pdf_clip::PdfClipCommand::for_kind(kind); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } diff --git a/src/app/commands/pdf_dialogs.rs b/src/app/commands/pdf_dialogs.rs index ea9dc7fc4..70ebf72fa 100644 --- a/src/app/commands/pdf_dialogs.rs +++ b/src/app/commands/pdf_dialogs.rs @@ -73,8 +73,7 @@ impl OpenCADStudio { crate::entities::underlay::display_path(&self.pdf_stored_path(path, state.path_type.0)); } - /// ULAYERS: every PDF underlay of the drawing, the selected one first - /// shown. + /// ULAYERS: every underlay of the drawing, the selected one first shown. pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) { let document = &self.tabs[i].scene.document; let selected: Vec = self.tabs[i] @@ -88,9 +87,6 @@ impl OpenCADStudio { let codec::EntityType::Underlay(u) = entity else { continue; }; - if u.underlay_type != codec::entities::UnderlayType::Pdf { - continue; - } let Some(def) = crate::entities::underlay::definition(u, document) else { continue; }; @@ -99,10 +95,13 @@ impl OpenCADStudio { if same > 0 { name = format!("{name} ({})", same + 1); } - let mut layers: Vec = crate::scene::model::pdf_layers::layers(&def.file_path) - .into_iter() - .map(|l| l.name) - .collect(); + let mut layers: Vec = match u.underlay_type { + codec::entities::UnderlayType::Pdf => crate::scene::model::pdf_layers::layers(&def.file_path) + .into_iter() + .map(|l| l.name) + .collect(), + kind => crate::scene::model::underlay_vector::layer_names(kind, &def.file_path), + }; layers.sort_by_key(|n| n.to_lowercase()); layers.dedup(); targets.push(LayerTarget { @@ -113,7 +112,7 @@ impl OpenCADStudio { }); } if targets.is_empty() { - self.command_line.push_error("No PDF underlays found."); + self.command_line.push_error("No underlays found."); return; } let current = targets @@ -152,7 +151,7 @@ impl OpenCADStudio { /// Writes the layers each underlay turns off, as one undo step. fn apply_underlay_layers(&mut self, i: usize, targets: Vec) { - use crate::scene::model::pdf_layers::{hidden_layers, LAYER_OVERRIDE_APP}; + use crate::scene::model::pdf_layers::{hidden_layers, DGN_OVERRIDE_PLACEHOLDER, LAYER_OVERRIDE_APP}; let changed: Vec = targets .into_iter() .filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) { @@ -182,6 +181,10 @@ impl OpenCADStudio { data.remove_record(LAYER_OVERRIDE_APP); if !target.hidden.is_empty() { let mut record = codec::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP); + // A DGN underlay's names follow a placeholder string. + if u.underlay_type == codec::entities::UnderlayType::Dgn { + record.add_value(codec::xdata::XDataValue::String(DGN_OVERRIDE_PLACEHOLDER.to_string())); + } for name in &target.hidden { record.add_value(codec::xdata::XDataValue::String(name.clone())); } diff --git a/src/app/commands/pdf_underlay.rs b/src/app/commands/pdf_underlay.rs index fce43efc6..62e2f0cac 100644 --- a/src/app/commands/pdf_underlay.rs +++ b/src/app/commands/pdf_underlay.rs @@ -1,17 +1,20 @@ -//! The right-edge PDF underlay and xref tools: their state comes from the +//! The right-edge underlay and xref tools: their state comes from the //! selection, their buttons edit it. use super::*; -use codec::entities::{Underlay, UnderlayDisplayFlags, UnderlayType}; +use codec::entities::{Underlay, UnderlayDisplayFlags}; use crate::ui::ribbon::UnderlayContext; impl OpenCADStudio { - /// The selected PDF underlays, or nothing when anything else is selected. + /// The selected underlays when they are all of one kind, or nothing + /// when anything else (or another kind) is selected. fn selected_pdf_underlays(&self, i: usize) -> Vec<(codec::Handle, Underlay)> { - let mut out = Vec::new(); + let mut out: Vec<(codec::Handle, Underlay)> = Vec::new(); for (handle, entity) in self.tabs[i].scene.selected_entities() { match entity { - codec::EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf => { + codec::EntityType::Underlay(u) + if out.first().is_none_or(|(_, first)| first.underlay_type == u.underlay_type) => + { out.push((handle, u.clone())); } _ => return Vec::new(), @@ -51,9 +54,10 @@ impl OpenCADStudio { } else { self.selected_pdf_underlays(i).first().map(|(_, u)| { UnderlayContext { + kind: u.underlay_type, monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME), shown: u.flags.contains(UnderlayDisplayFlags::ON), - snap: crate::scene::model::pdf_vector::pdf_osnap(), + snap: crate::scene::model::pdf_vector::underlay_osnap(u.underlay_type), } }) }; @@ -106,9 +110,14 @@ impl OpenCADStudio { .is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::ON)); self.edit_selected_underlays(i, "PDFUNDERLAY", |u| u.set_on(on)); } + // Enable Snap switches PDFOSNAP, DWFOSNAP or DGNOSNAP. "_PDFULSNAP" => { - let on = !crate::scene::model::pdf_vector::pdf_osnap(); - crate::scene::model::pdf_vector::set_pdf_osnap(on); + let Some((_, underlay)) = self.selected_pdf_underlays(i).into_iter().next() else { + return Some(Task::none()); + }; + let kind = underlay.underlay_type; + let on = !crate::scene::model::pdf_vector::underlay_osnap(kind); + crate::scene::model::pdf_vector::set_underlay_osnap(kind, on); self.tabs[i].scene.reseed_underlays(); self.sync_underlay_tab(); } diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index c54b53d5c..929f6fde7 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1183,16 +1183,27 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } - // ponytail: one flag for every underlay kind; split it if a drawing - // needs PDF snaps on while DWF/DGN snaps are off. if matches!(name.as_str(), "PDFOSNAP" | "DWFOSNAP" | "DGNOSNAP" | "UOSNAP") { - let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap()); + use crate::scene::model::pdf_vector as pv; + use codec::entities::UnderlayType; + // UOSNAP sets every kind and reads 2 while they differ. + let kind = match name.as_str() { + "PDFOSNAP" => Some(UnderlayType::Pdf), + "DWFOSNAP" => Some(UnderlayType::Dwf), + "DGNOSNAP" => Some(UnderlayType::Dgn), + _ => None, + }; + let current = kind.map_or_else(pv::uosnap, |kind| i16::from(pv::underlay_osnap(kind))); if let Some(value) = &value { match value.parse::().ok().filter(|value| (0..=1).contains(value)) { Some(mode) => { if current != mode { - crate::scene::model::pdf_vector::set_pdf_osnap(mode == 1); + match kind { + Some(kind) => pv::set_underlay_osnap(kind, mode == 1), + None => pv::set_uosnap(mode == 1), + } self.tabs[i].scene.reseed_underlays(); + self.sync_underlay_tab(); } } None => self.command_line.push_error("Requires 0 or 1 only"), diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index c754abbfb..1963f4c82 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1613,7 +1613,7 @@ bg={bg_ms:.1}ms n={view_count}" } else if let Some(ctx) = self.ribbon.underlay_context() { let ctx = ctx.clone(); crate::ui::side_toolbar::view_with_active( - &crate::ui::ribbon::pdf_underlay_tools(), + &crate::ui::ribbon::pdf_underlay_tools(ctx.kind), &move |id| match id { "_PDFULMONO" => ctx.monochrome, "_PDFULSHOW" => ctx.shown, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 511658edd..93ab62e94 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -2985,7 +2985,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Page size:" => Some(("insert", "pdfui-page-size")), "Reference name:" => Some(("insert", "pdfui-reference-name")), "Search for layer" => Some(("insert", "pdfui-search-for-layer")), - "This file does not contain any layers." => Some(("insert", "pdfui-this-file-does-not-contain-any-layers")), + "This file does not contain any layers" => Some(("insert", "pdfui-this-file-does-not-contain-any-layers")), "Select an underlay to view its layers." => Some(("insert", "pdfui-select-an-underlay-to-view-its-layers")), "PDF data to import" => Some(("insert", "pdfui-pdf-data-to-import")), "Vector geometry" => Some(("insert", "pdfui-vector-geometry")), diff --git a/src/modules/insert/pdf_clip.rs b/src/modules/insert/pdf_clip.rs index 981d8a3a4..0ff244078 100644 --- a/src/modules/insert/pdf_clip.rs +++ b/src/modules/insert/pdf_clip.rs @@ -1,7 +1,7 @@ -// PDFCLIP / IMAGECLIP — clip a PDF underlay or a raster image to a boundary -// (CLIP continues here for either). +// PDFCLIP / DWFCLIP / DGNCLIP / IMAGECLIP — clip an underlay or a raster +// image to a boundary (CLIP continues here for either). // -// Select PDF to clip: (Select image to clip:) +// Select PDF to clip: (DWF / DGN; Select image to clip:) // Enter PDF clipping option [ON/OFF/Delete/New boundary] : // (Enter image clipping option [ON/OFF/Delete/New boundary] :) // Delete old boundary? [Yes/No] : (when one exists; [No/Yes] for @@ -38,6 +38,8 @@ enum Step { } pub struct PdfClipCommand { + /// The underlay kind PDFCLIP / DWFCLIP / DGNCLIP clips. + kind: UnderlayType, step: Step, handle: Handle, underlay: Option, @@ -55,7 +57,13 @@ const BOUNDARY: &str = "Specify clipping boundary or select invert option:"; impl PdfClipCommand { pub fn new() -> Self { + Self::for_kind(UnderlayType::Pdf) + } + + /// DWFCLIP / DGNCLIP (or PDFCLIP): asks for an underlay of the kind. + pub fn for_kind(kind: UnderlayType) -> Self { Self { + kind, step: Step::Select, handle: Handle::NULL, underlay: None, @@ -147,7 +155,7 @@ impl PdfClipCommand { /// Create Clipping Boundary): the New boundary branch at once. Returns the /// command and the result of its first step. pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) { - let mut command = Self::new(); + let mut command = Self::for_kind(underlay.underlay_type); command.handle = handle; command.underlay = Some(underlay); command.step = Step::Option; @@ -157,7 +165,7 @@ impl PdfClipCommand { /// An underlay CLIP picked: straight to the clipping option. pub fn for_underlay(handle: Handle, underlay: Underlay) -> Self { - let mut command = Self::new(); + let mut command = Self::for_kind(underlay.underlay_type); command.handle = handle; command.underlay = Some(underlay); command.step = Step::Option; @@ -255,6 +263,14 @@ impl PdfClipCommand { } } + fn kind_name(&self) -> &'static str { + match self.kind { + UnderlayType::Pdf => "PDF", + UnderlayType::Dwf => "DWF", + UnderlayType::Dgn => "DGN", + } + } + fn rectangle(a: DVec3, b: DVec3) -> Vec { vec![ a, @@ -267,19 +283,25 @@ impl PdfClipCommand { impl CadCommand for PdfClipCommand { fn name(&self) -> &'static str { - if self.image_mode { "IMAGECLIP" } else { "PDFCLIP" } + match (self.image_mode, self.kind) { + (true, _) => "IMAGECLIP", + (false, UnderlayType::Pdf) => "PDFCLIP", + (false, UnderlayType::Dwf) => "DWFCLIP", + (false, UnderlayType::Dgn) => "DGNCLIP", + } } fn prompt(&self) -> String { match self.step { Step::Select if self.image_mode => "Select image to clip:".to_string(), - Step::Select => "Select PDF to clip:".to_string(), + Step::Select => format!("Select {} to clip:", self.kind_name()), Step::Option if self.image_mode => { "Enter image clipping option [ON/OFF/Delete/New boundary] :".to_string() } - Step::Option => { - "Enter PDF clipping option [ON/OFF/Delete/New boundary] :".to_string() - } + Step::Option => format!( + "Enter {} clipping option [ON/OFF/Delete/New boundary] :", + self.kind_name() + ), Step::DeleteOld if self.image_mode => "Delete old boundary? [No/Yes] :".to_string(), Step::DeleteOld => "Delete old boundary? [Yes/No] :".to_string(), Step::Mode => { @@ -359,13 +381,16 @@ impl CadCommand for PdfClipCommand { _ => CmdResult::NeedPoint, }, Step::Select => match picked { - Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => { + Some(EntityType::Underlay(u)) if u.underlay_type == self.kind => { self.handle = handle; self.underlay = Some(u); self.step = Step::Option; CmdResult::NeedPoint } - _ => CmdResult::ReportError("Object selected was not a PDF underlay.".to_string()), + _ => CmdResult::ReportError(format!( + "Object selected was not a {} underlay.", + self.kind_name() + )), }, Step::Polyline => match picked { Some(EntityType::LwPolyline(pl)) if pl.vertices.len() >= 3 => { @@ -440,6 +465,8 @@ impl CadCommand for PdfClipCommand { self.apply_boundary(&points, false) } Step::PolyPoints => CmdResult::NeedPoint, + // A corner is asked again, as the reference does. + Step::RectFirst | Step::RectSecond => CmdResult::NeedPoint, _ => CmdResult::Cancel, } } @@ -461,5 +488,5 @@ impl CadCommand for PdfClipCommand { } inventory::submit!(crate::command::CommandRegistration { - names: &["PDFCLIP", "IMAGECLIP"] + names: &["PDFCLIP", "DWFCLIP", "DGNCLIP", "IMAGECLIP"] }); diff --git a/src/modules/insert/xclip.rs b/src/modules/insert/xclip.rs index b6575dd4d..6fdbb8cd3 100644 --- a/src/modules/insert/xclip.rs +++ b/src/modules/insert/xclip.rs @@ -16,7 +16,6 @@ // underlay as PDFCLIP, a raster image as IMAGECLIP, a layout viewport as // VPCLIP, anything else is "*Invalid selection*". -use codec::entities::UnderlayType; use codec::types::Handle; use codec::EntityType; use glam::DVec3; @@ -488,7 +487,7 @@ impl CadCommand for ClipCommand { self.inner = Some(Box::new(XclipCommand::for_inserts(vec![handle], false))); CmdResult::NeedPoint } - Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => { + Some(EntityType::Underlay(u)) => { self.inner = Some(Box::new(PdfClipCommand::for_underlay(handle, u))); CmdResult::NeedPoint } diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index b258fc1b3..b9eed710d 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -1405,7 +1405,26 @@ impl Scene { && z[1].is_finite() }) .collect(); - if wires.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() { + // Raster images and underlays draw as GPU quads; with their frames + // hidden they carry no wire outline, so fold their corners in too. + let image_corners: Vec = self + .images + .iter() + .filter(|(h, _)| { + let handle = **h; + self.mesh_entity_visible(handle) + && self.document.get_entity(handle).is_some_and(|entity| { + self.belongs_to_visible_block(handle, entity.common().owner_handle, layout_block) + }) + }) + .flat_map(|(_, image)| { + image.corners.iter().zip(image.corners_low.iter()).map(|(c, low)| { + glam::Vec3::new(c[0] + low[0], c[1] + low[1], c[2] + low[2]) + }) + }) + .filter(|p| p.is_finite()) + .collect(); + if wires.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() && image_corners.is_empty() { return; } @@ -1449,7 +1468,7 @@ impl Scene { }); } } - if cents.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() { + if cents.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() && image_corners.is_empty() { return; } @@ -1516,6 +1535,10 @@ impl Scene { min = min.min(glam::Vec3::new(*ax, *ay, *az)); max = max.max(glam::Vec3::new(*bx, *by, *bz)); } + for p in &image_corners { + min = min.min(*p); + max = max.max(*p); + } // Drawing holds only infinite construction geometry: fit the view to // its base points rather than leaving the camera unchanged. if min.x > max.x { diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index c00b98c9a..4b1de97b6 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -222,15 +222,24 @@ impl ImageModel { }; (source, raster) } - kind => ( - def.file_path.clone(), - super::underlay_vector::display_raster( + kind => { + let hidden = super::pdf_layers::hidden_layers(u); + // The adjusted-pixel memo keys on the source: one per set of + // hidden layers. + let source = if hidden.is_empty() { + def.file_path.clone() + } else { + format!("{}#{}", def.file_path, hidden.join("|")) + }; + let raster = super::underlay_vector::display_raster( kind, &def.file_path, page, + &hidden, screen_side.unwrap_or(super::underlay_vector::RASTER_SIDE), - )?, - ), + )?; + (source, raster) + } }; // Dark means an HSL lightness under one half: pure blue counts as // light, (0, 128, 0) as dark. @@ -243,6 +252,11 @@ impl ImageModel { &raster, PageAdjust { contrast: u.contrast.min(100), + contrast_pivot: match def.underlay_type { + UnderlayType::Pdf => 500, + UnderlayType::Dwf => 243, + UnderlayType::Dgn => 650, + }, monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME), adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND), // A DGN model is drawn for a black background, so its diff --git a/src/scene/model/pdf_layers.rs b/src/scene/model/pdf_layers.rs index 5054f3ce0..b44a40d55 100644 --- a/src/scene/model/pdf_layers.rs +++ b/src/scene/model/pdf_layers.rs @@ -231,6 +231,9 @@ fn without_key(dict: &str, key: &str) -> Option { /// them. pub const LAYER_OVERRIDE_APP: &str = "AdeskUnderlayLayerOverrideData"; +/// The first string of a DGN underlay's layer overrides, before the names. +pub const DGN_OVERRIDE_PLACEHOLDER: &str = "???DGN_Placeholder???"; + pub fn hidden_layers(u: &codec::entities::Underlay) -> Vec { u.common .extended_data @@ -239,7 +242,7 @@ pub fn hidden_layers(u: &codec::entities::Underlay) -> Vec { .filter(|r| r.application_name.eq_ignore_ascii_case(LAYER_OVERRIDE_APP)) .flat_map(|r| r.values.iter()) .filter_map(|v| match v { - codec::xdata::XDataValue::String(s) => Some(s.clone()), + codec::xdata::XDataValue::String(s) if s != DGN_OVERRIDE_PLACEHOLDER => Some(s.clone()), _ => None, }) .collect() diff --git a/src/scene/model/pdf_raster.rs b/src/scene/model/pdf_raster.rs index 0fb9a85d0..eab0bdd63 100644 --- a/src/scene/model/pdf_raster.rs +++ b/src/scene/model/pdf_raster.rs @@ -214,8 +214,11 @@ pub fn page_size_inches(path: &str, page: &str) -> Option<(f64, f64)> { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct PageAdjust { /// 0..=100; 100 keeps the colours, lower values pull their lightness - /// towards one third. + /// towards `contrast_pivot` (hue and saturation kept). pub contrast: u8, + /// The lightness contrast 0 leaves every colour at, in thousandths: + /// measured 500 for PDF, 243 for DWF and 650 for DGN underlays. + pub contrast_pivot: u16, pub monochrome: bool, /// Lighten dark, unsaturated content on a dark background (black lines /// and text become white), keeping coloured content. @@ -266,11 +269,12 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0)); } if adjust.contrast < 100 { - // Lightness is pulled towards one third, hue and saturation kept: - // contrast 0 leaves every colour at a third of full lightness. + // Lightness is pulled towards the pivot, hue and saturation kept: + // contrast 0 leaves every colour at the pivot's lightness. let (h, s, l) = rgb_to_hsl(rgb); let k = adjust.contrast as f32 / 100.0; - rgb = hsl_to_rgb(h, s, 1.0 / 3.0 + (l - 1.0 / 3.0) * k); + let pivot = adjust.contrast_pivot as f32 / 1000.0; + rgb = hsl_to_rgb(h, s, pivot + (l - pivot) * k); } for (dst, c) in px[..3].iter_mut().zip(rgb) { *dst = (c.clamp(0.0, 1.0) * 255.0).round() as u8; diff --git a/src/scene/model/pdf_vector.rs b/src/scene/model/pdf_vector.rs index 1715bf18e..16d53b6b2 100644 --- a/src/scene/model/pdf_vector.rs +++ b/src/scene/model/pdf_vector.rs @@ -600,15 +600,40 @@ pub fn circle_of(sp: &SubPath) -> Option<([f64; 2], f64)> { // ── Snapping ──────────────────────────────────────────────────────────────── -static PDF_OSNAP: AtomicBool = AtomicBool::new(true); +/// PDFOSNAP, DWFOSNAP and DGNOSNAP: whether object snaps find the geometry +/// inside underlays of each kind. +static UNDERLAY_OSNAP: [AtomicBool; 3] = [AtomicBool::new(true), AtomicBool::new(true), AtomicBool::new(true)]; -/// PDFOSNAP: whether object snaps find the geometry inside PDF underlays. -pub fn pdf_osnap() -> bool { - PDF_OSNAP.load(Ordering::Relaxed) +fn osnap_slot(kind: codec::entities::UnderlayType) -> &'static AtomicBool { + match kind { + codec::entities::UnderlayType::Pdf => &UNDERLAY_OSNAP[0], + codec::entities::UnderlayType::Dwf => &UNDERLAY_OSNAP[1], + codec::entities::UnderlayType::Dgn => &UNDERLAY_OSNAP[2], + } } -pub fn set_pdf_osnap(on: bool) { - PDF_OSNAP.store(on, Ordering::Relaxed); +pub fn underlay_osnap(kind: codec::entities::UnderlayType) -> bool { + osnap_slot(kind).load(Ordering::Relaxed) +} + +pub fn set_underlay_osnap(kind: codec::entities::UnderlayType, on: bool) { + osnap_slot(kind).store(on, Ordering::Relaxed); +} + +/// UOSNAP: 1 when snaps find every kind of underlay geometry, 0 when none, +/// 2 when the kinds differ. +pub fn uosnap() -> i16 { + match UNDERLAY_OSNAP.iter().filter(|s| s.load(Ordering::Relaxed)).count() { + 0 => 0, + n if n == UNDERLAY_OSNAP.len() => 1, + _ => 2, + } +} + +pub fn set_uosnap(on: bool) { + for slot in &UNDERLAY_OSNAP { + slot.store(on, Ordering::Relaxed); + } } /// Most snap points one underlay contributes. @@ -623,7 +648,7 @@ pub fn underlay_snap_points( document: &codec::CadDocument, ) -> Vec<(glam::DVec3, crate::scene::model::wire_model::SnapHint)> { use crate::scene::model::wire_model::SnapHint; - if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) { + if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) { return Vec::new(); } let Some(def) = crate::entities::underlay::definition(u, document) else { @@ -675,7 +700,7 @@ fn underlay_vectors( codec::entities::UnderlayType::Pdf => { page_vectors(&super::pdf_layers::underlay_source(u, &def.file_path), page) } - kind => super::underlay_vector::page_vectors(kind, &def.file_path, page), + kind => super::underlay_vector::page_vectors(kind, &def.file_path, page, &super::pdf_layers::hidden_layers(u)), } } @@ -698,7 +723,7 @@ pub fn underlay_snap_geometry( u: &codec::entities::Underlay, document: &codec::CadDocument, ) -> Vec { - if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) { + if !underlay_osnap(u.underlay_type) || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) { return Vec::new(); } let Some(def) = crate::entities::underlay::definition(u, document) else { diff --git a/src/scene/model/underlay_vector/dgn7.rs b/src/scene/model/underlay_vector/dgn7.rs index e38f30b95..8664c5eca 100644 --- a/src/scene/model/underlay_vector/dgn7.rs +++ b/src/scene/model/underlay_vector/dgn7.rs @@ -262,6 +262,7 @@ fn read_run(run: &[&[u8]], colors: &[[u8; 3]]) -> Raw { width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation: (rotation as f64 / 360000.0).to_radians(), color, + font: "txt".to_string(), }); } 27 => { @@ -380,14 +381,65 @@ fn place(raw: &Raw, m: &Affine, defs: &HashMap, depth: usize, out: } } -/// The model's geometry, in master units. -pub fn model(bytes: &[u8]) -> Option { +/// A level's number (0-63) from an element header. +fn level(e: &[u8]) -> u8 { + e[0] & 0x3f +} + +/// Level names from the level name table (type 66 elements on level 6): +/// each entry is a level number followed by its name. +// ponytail: entries are found by that pattern rather than by the table's +// record layout; read the layout if a file lists a level wrongly. +fn level_names(all: &[&[u8]]) -> HashMap { + let mut names = HashMap::new(); + for e in all.iter().filter(|e| e[1] & 0x7f == 66 && level(e) == 6) { + for j in 36..e.len().saturating_sub(3) { + let n = u16::from_le_bytes([e[j], e[j + 1]]); + if !(1..=63).contains(&n) || (j > 36 && e[j - 1] != 0) || names.contains_key(&(n as u8)) { + continue; + } + let rest = &e[j + 2..]; + let len = rest.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b'_' | b'$'))).unwrap_or(rest.len()); + if len > 0 && rest.get(len) == Some(&0) { + names.insert(n as u8, String::from_utf8_lossy(&rest[..len]).to_string()); + } + } + } + names +} + +/// The used levels' names (or numbers), for the underlay layers list. +pub fn layer_names(bytes: &[u8]) -> Option> { + if !is_v7(bytes) { + return None; + } + let all = elements(bytes); + let names = level_names(&all); + let mut used: Vec = all + .iter() + .filter(|e| !matches!(e[1] & 0x7f, 9 | 8 | 10 | 5 | 66 | 34) && level(e) > 0) + .map(|e| level(e)) + .collect(); + used.sort_unstable(); + used.dedup(); + Some(used.into_iter().map(|l| names.get(&l).cloned().unwrap_or_else(|| l.to_string())).collect()) +} + +/// The model's geometry, in master units, without the `hidden` levels. +pub fn model(bytes: &[u8], hidden: &[String]) -> Option { if !is_v7(bytes) { return None; } let u = units(bytes)?; let all = elements(bytes); let colors = color_table(&all).unwrap_or_else(default_colors); + let names = level_names(&all); + let off = |e: &[u8]| { + let l = level(e); + let name = names.get(&l).cloned().unwrap_or_else(|| l.to_string()); + hidden.iter().any(|h| h.eq_ignore_ascii_case(&name)) + }; + let all: Vec<&[u8]> = all.into_iter().filter(|e| !off(e)).collect(); // Shared cell definitions (type 34) and their components are drawn only // where instances place them; everything else is the model itself. let mut defs: HashMap = HashMap::new(); diff --git a/src/scene/model/underlay_vector/dgn8.rs b/src/scene/model/underlay_vector/dgn8.rs index fba1bca65..4bc4d8605 100644 --- a/src/scene/model/underlay_vector/dgn8.rs +++ b/src/scene/model/underlay_vector/dgn8.rs @@ -210,15 +210,22 @@ struct Styles { colors: Vec<[u8; 3]>, /// Level id → colour index. levels: HashMap, + /// Levels the underlay turns off: their elements are left out. + hidden: std::collections::HashSet, } impl Styles { fn color(&self, e: &[u8]) -> [u8; 3] { - let explicit = u32_at(e, 44).unwrap_or(u32::MAX); - let index = if explicit < 256 { - explicit - } else { + // The colour word: an index into the colour table, 0xFFFFFFFF for + // the level's colour, or a true colour — n × 256 plus the index of + // its nearest table colour. + // ponytail: a true colour draws as that nearest index; the RGB it + // stands for is not read. + let explicit = u32_at(e, 52).unwrap_or(u32::MAX); + let index = if explicit == u32::MAX { self.levels.get(&u32_at(e, 12).unwrap_or(0)).copied().unwrap_or(0) + } else { + explicit & 0xff }; self.colors.get(index as usize).copied().unwrap_or([255, 255, 255]) } @@ -239,6 +246,32 @@ struct Instance { origin: [f64; 2], } +/// A 3D element (flag 0x0800 of its properties word): three coordinates a +/// point, orientations as quaternions. Its plan view is drawn. +fn is_3d(e: &[u8]) -> bool { + u32_at(e, 40).is_some_and(|v| v & 0x0800 != 0) +} + +/// The in-plane x and y axes (projected onto the plan) of a 3D element's +/// orientation quaternion w, x, y, z stored at `at` (the rows of its +/// rotation matrix). +fn quat_axes(e: &[u8], at: usize) -> Option<([f64; 2], [f64; 2])> { + let (w, x, y, z) = (f64_at(e, at)?, f64_at(e, at + 8)?, f64_at(e, at + 16)?, f64_at(e, at + 24)?); + Some(( + [1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - w * z)], + [2.0 * (x * y + w * z), 1.0 - 2.0 * (x * x + z * z)], + )) +} + +/// An elliptical arc on axes `u` and `v` (plan projections) as cubics. +fn projected_arc(c: [f64; 2], a: f64, b: f64, u: [f64; 2], v: [f64; 2], start: f64, sweep: f64) -> Vec { + let map = |p: [f64; 2]| [c[0] + a * p[0] * u[0] + b * p[1] * v[0], c[1] + a * p[0] * u[1] + b * p[1] * v[1]]; + arc_cubics([0.0, 0.0], 1.0, 1.0, 0.0, start, sweep) + .into_iter() + .map(|[p0, p1, p2, p3]| Segment::Cubic(map(p0), map(p1), map(p2), map(p3))) + .collect() +} + fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { let mut raw = Raw::default(); // A complex shape or chain collects its components into one path. @@ -272,12 +305,13 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { } } for e in run { - if e.len() < 104 { + if e.len() < 104 || styles.hidden.contains(&u32_at(e, 12).unwrap_or(0)) { continue; } let color = styles.color(e); let stroke = Some((color, -1.0)); let p2 = |at: usize| -> Option<[f64; 2]> { Some([f64_at(e, at)?, f64_at(e, at + 8)?]) }; + let d3 = is_3d(e); match kind(e) { 12 | 14 => { let n = u32_at(e, 104).unwrap_or(0) as usize; @@ -287,13 +321,14 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { } } 3 => { - if let (Some(a), Some(b)) = (p2(104), p2(120)) { + if let (Some(a), Some(b)) = (p2(104), p2(if d3 { 128 } else { 120 })) { push(&mut raw, &mut complex, Path { subpaths: vec![SubPath { segments: vec![Segment::Line(a, b)], closed: false }], stroke, fill: None }); } } 4 | 6 | 11 => { let n = u32_at(e, 104).unwrap_or(0) as usize; - let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * 16)).collect(); + let stride = if d3 { 24 } else { 16 }; + let pts: Vec<[f64; 2]> = (0..n).filter_map(|k| p2(112 + k * stride)).collect(); if pts.len() >= 2 { let mut pb = PathBuilder::default(); pb.move_to(pts[0]); @@ -306,6 +341,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { push(&mut raw, &mut complex, Path { subpaths: pb.finish(), stroke, fill: None }); } } + 15 | 16 if d3 => { + // Axes, orientation and centre: ellipse a 104, b 112, + // quaternion 120, centre 152; arc start 104, sweep 112, a 120, + // b 128, quaternion 136, centre 168. + let arc = if kind(e) == 15 { + (|| Some((f64_at(e, 104)?, f64_at(e, 112)?, quat_axes(e, 120)?, p2(152)?, 0.0, std::f64::consts::TAU)))() + } else { + (|| Some((f64_at(e, 120)?, f64_at(e, 128)?, quat_axes(e, 136)?, p2(168)?, f64_at(e, 104)?, f64_at(e, 112)?)))() + }; + let Some((a, b, (u, v), c, start, sweep)) = arc else { continue }; + let sweep = if sweep == 0.0 { std::f64::consts::TAU } else { sweep }; + let closed = sweep.abs() >= std::f64::consts::TAU - 1e-9; + push( + &mut raw, + &mut complex, + Path { subpaths: vec![SubPath { segments: projected_arc(c, a, b, u, v, start, sweep), closed }], stroke, fill: None }, + ); + } 15 | 16 => { let arc = if kind(e) == 15 { (|| Some((f64_at(e, 104)?, f64_at(e, 112)?, f64_at(e, 120)?, p2(128)?, 0.0, std::f64::consts::TAU)))() @@ -321,6 +374,24 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { Path { subpaths: vec![SubPath { segments: pieces.into_iter().map(|[a, b, c, d]| Segment::Cubic(a, b, c, d)).collect(), closed }], stroke, fill: None }, ); } + 17 if d3 => { + // Width 112, height 120, quaternion 144, origin 176; the + // characters follow the string marker. + let (Some(width), Some(height), Some((u, _)), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), quat_axes(e, 144), p2(176)) else { + continue; + }; + let Some(text) = marked_string(e, 200) else { continue }; + let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0); + raw.texts.push(Text { + text, + origin, + height, + width_factor: if height > 0.0 { width / height } else { 1.0 }, + rotation: u[1].atan2(u[0]), + color, + font: "txt".to_string(), + }); + } 17 => { let (Some(width), Some(height), Some(rotation), Some(origin)) = (f64_at(e, 112), f64_at(e, 120), f64_at(e, 144), p2(152)) else { continue; @@ -331,7 +402,7 @@ fn read_run(run: &[&[u8]], styles: &Styles) -> Raw { let Some(chars) = e.get(174..170 + count.max(4)) else { continue }; let text = String::from_utf8_lossy(chars).trim_end_matches(char::from(0)).to_string(); let (height, width) = (height * 6.0 / 1000.0, width * 6.0 / 1000.0); - raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color }); + raw.texts.push(Text { text, origin, height, width_factor: if height > 0.0 { width / height } else { 1.0 }, rotation, color, font: "txt".to_string() }); } 35 => { let (Some(m0), Some(m1), Some(m3), Some(m4), Some(origin)) = (f64_at(e, 160), f64_at(e, 168), f64_at(e, 184), f64_at(e, 192), p2(232)) else { @@ -457,23 +528,59 @@ pub fn model_names(bytes: &[u8]) -> Option> { (!names.is_empty()).then_some(names) } -pub fn model(bytes: &[u8], name: &str) -> Option { +/// The level table (the table entries after a header of table 1): id, +/// name and colour index. The default level (64) is listed as "0", as the +/// reference names it. +fn level_table(nm: &[&[u8]]) -> Vec<(u32, String, u32)> { + let mut out = Vec::new(); + let mut in_levels = false; + for e in nm { + match kind(e) { + 96 => in_levels = u32_at(e, 12) == Some(1), + 95 if in_levels && e.len() >= 80 => { + let (Some(id), Some(color)) = (u32_at(e, 32), u32_at(e, 72)) else { continue }; + let mut name = marked_string(e, 32).unwrap_or_default(); + if id == 64 && name.eq_ignore_ascii_case("Default") { + name = "0".to_string(); + } + out.push((id, name, color)); + } + _ => {} + } + } + out +} + +fn non_model(cfb: &Cfb) -> Vec { + cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default() +} + +/// Level names of the file, for the underlay layers list. +pub fn layer_names(bytes: &[u8]) -> Option> { + let cfb = Cfb::open(bytes)?; + let nm = non_model(&cfb); + Some(level_table(&elements(&nm)).into_iter().map(|l| l.1).filter(|n| !n.is_empty()).collect()) +} + +pub fn model(bytes: &[u8], name: &str, hidden: &[String]) -> Option { let cfb = Cfb::open(bytes)?; let all = models(&cfb); let (_, graphics, header) = all.iter().find(|m| m.0.eq_ignore_ascii_case(name)).or_else(|| all.first())?; - let non_model = cfb.streams().iter().find(|(p, _)| p == "Dgn^Nm/$1").map(|(_, e)| inflate(&cfb.stream(e))).unwrap_or_default(); + let non_model = non_model(&cfb); let nm = elements(&non_model); let colors = nm .iter() .find(|e| kind(e) == 5 && e.len() >= 37 + 768) .map(|e| (0..256).map(|i| [e[37 + i * 3], e[38 + i * 3], e[39 + i * 3]]).collect()) .unwrap_or_else(|| vec![[255, 255, 255]; 256]); - let levels = nm + let table = level_table(&nm); + let levels = table.iter().map(|l| (l.0, l.2)).collect(); + let hidden = table .iter() - .filter(|e| kind(e) == 95 && e.len() >= 80) - .filter_map(|e| Some((u32_at(e, 32)?, u32_at(e, 72)?))) + .filter(|l| hidden.iter().any(|h| h.eq_ignore_ascii_case(&l.1))) + .map(|l| l.0) .collect(); - let styles = Styles { colors, levels }; + let styles = Styles { colors, levels, hidden }; // Shared cell definitions: a type-34 element and its components. let mut defs = HashMap::new(); let mut i = 0; diff --git a/src/scene/model/underlay_vector/dwf.rs b/src/scene/model/underlay_vector/dwf.rs index 0daf4de8a..094f9b19a 100644 --- a/src/scene/model/underlay_vector/dwf.rs +++ b/src/scene/model/underlay_vector/dwf.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::io::{Cursor, Read}; -use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath}; +use super::model::{arc_cubics, paths_bounds, Path, PathBuilder, Segment, Sheet, SubPath, Text, CAP_PER_EM}; /// The package's files, with `\` separators turned into `/`. fn unzip(bytes: &[u8]) -> Option>> { @@ -160,37 +160,58 @@ pub fn sheet(bytes: &[u8], name: &str) -> Option { .find(|s| s.name.eq_ignore_ascii_case(name)) .or_else(|| (name.is_empty()).then(|| entries.first()).flatten())?; let data = files.get(&entry.graphics)?; - let (paths, view) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") { + let (paths, view, texts) = if entry.graphics.to_ascii_lowercase().ends_with(".w2d") { w2d::read(data)? } else { let w2x = entry.w2x.as_ref().and_then(|p| files.get(p)); xps::read(data, w2x.map(|v| v.as_slice()), entry)? }; let rect = view.or_else(|| paths_bounds(&paths))?; - Some(Sheet { rect, paths, texts: Vec::new(), sub_per_master: 1.0 }) + Some(Sheet { rect, paths, texts, sub_per_master: 1.0 }) } -/// The default colour map of a W2D stream: black, a 6×6×6 colour cube from -/// index 10 (red 180 steps, green 30, blue 5), greys after it. +/// The default colour map of a W2D stream: ten system colours, a 6×6×6 +/// colour cube from index 10 (red 36 steps, green 6, blue 1), a grey ramp +/// at 226-245 and ten more system colours (measured against plotted files: +/// the cube, the ramp, 7 and 248). fn default_color(index: u8) -> [u8; 3] { let i = index as usize; const LEVELS: [u8; 6] = [0, 51, 102, 153, 204, 255]; - // ponytail: only the cube is confirmed against plotted files; the entries - // around it are a grey ramp until a file uses them. + const LOW: [[u8; 3]; 10] = [ + [0, 0, 0], + [128, 0, 0], + [0, 128, 0], + [128, 128, 0], + [0, 0, 128], + [128, 0, 128], + [0, 128, 128], + [192, 192, 192], + [192, 220, 192], + [166, 202, 240], + ]; + const HIGH: [[u8; 3]; 10] = [ + [255, 251, 240], + [160, 160, 164], + [128, 128, 128], + [255, 0, 0], + [0, 255, 0], + [255, 255, 0], + [0, 0, 255], + [255, 0, 255], + [0, 255, 255], + [255, 255, 255], + ]; match i { + 0..=9 => LOW[i], 10..=225 => { let c = i - 10; [LEVELS[c / 36], LEVELS[(c / 6) % 6], LEVELS[c % 6]] } - 0 => [0, 0, 0], - 1..=9 => { - let g = (i * 255 / 10) as u8; - [g, g, g] - } - _ => { - let g = ((i - 226) * 255 / 29) as u8; + 226..=245 => { + let g = ((i - 226) * 255 / 19) as u8; [g, g, g] } + _ => HIGH[i - 246], } } @@ -286,9 +307,13 @@ mod w2d { (r[0] < r[2] && r[1] < r[3]).then_some(r) } - /// The stream's geometry in model units, and its plotted view. - pub fn read(b: &[u8]) -> Option<(Vec, Option<[f64; 4]>)> { + /// The stream's geometry and text in model units, and its plotted view. + pub fn read(b: &[u8]) -> Option<(Vec, Option<[f64; 4]>, Vec)> { let mut r = Reader { b, i: 0 }; + // The current font: family, em height (graphics units), rotation + // (radians) and width scale. + let mut font = (String::from("Arial"), 0.0f64, 0.0f64, 1.0f64); + let mut texts: Vec<(String, [f64; 2], (String, f64, f64, f64), [u8; 3])> = Vec::new(); let mut units = Units::identity(); let mut view: Option<[f64; 4]> = None; let mut color = [0u8, 0, 0]; @@ -322,6 +347,13 @@ mod w2d { color = [v[0] as u8, v[1] as u8, v[2] as u8]; } } + // "(FontExtension 'logfont name' 'canonical name')" + "FontExtension" => { + let body = String::from_utf8_lossy(body); + if let Some(name) = body.split('\'').nth(1).filter(|n| !n.is_empty()) { + font.0 = name.to_string(); + } + } _ => {} } } @@ -426,12 +458,47 @@ mod w2d { let fill = op == b'E'; raw.push((arc_path(c, major, minor, tilt * turn, s * turn, sweep), color, if fill { -1.0 } else { weight }, visible)); } - b'x' => { - // Text: relative position and a string (drawn as the - // outlines that follow it, so only the position counts). + 0x06 => { + // Font: a field mask, then the fields it names in order. r.i += 1; - rel(&mut at, r.i32()? as f64, r.i32()? as f64); - skip_string(&mut r)?; + let mask = r.u16()?; + if mask & !0x07fe != 0 { + // A font name field (or one unknown here) — its + // encoding is not read, so stop. + break; + } + for bit in 1..11 { + if mask & (1 << bit) == 0 { + continue; + } + match 1u16 << bit { + // charset, pitch, family, style + 0x0002 | 0x0004 | 0x0008 | 0x0010 => { + r.u8()?; + } + 0x0020 => font.1 = r.i32()? as f64, + 0x0040 => font.2 = r.u16()? as f64 * std::f64::consts::TAU / 65536.0, + 0x0080 => font.3 = r.u16()? as f64 / 1024.0, + // oblique, spacing + 0x0100 | 0x0200 => { + r.u16()?; + } + _ => { + r.i32()?; + } + } + } + } + b'x' => { + // Text at a relative position. Stroke-font text is also + // plotted as outlines with the text itself hidden, so only + // visible text is kept. + r.i += 1; + let p = rel(&mut at, r.i32()? as f64, r.i32()? as f64); + let s = read_string(&mut r)?; + if visible && font.1 > 0.0 && !s.trim().is_empty() { + texts.push((s, p, font.clone(), color)); + } } _ => break, } @@ -453,22 +520,42 @@ mod w2d { let b = units.model([v[2], v[3]]); [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])] }); - Some((paths, view)) + let texts = texts + .into_iter() + .map(|(text, p, (family, em, rotation, width), color)| Text { + text, + origin: units.model(p), + height: units.length(em) * CAP_PER_EM, + width_factor: width, + rotation, + color, + font: family, + }) + .collect(); + Some((paths, view, texts)) } /// A W2D string: quoted ASCII, or a count followed by UTF-16 units. - fn skip_string(r: &mut Reader) -> Option<()> { + fn read_string(r: &mut Reader) -> Option { if r.b.get(r.i) == Some(&b'\'') { r.i += 1; + let start = r.i; while *r.b.get(r.i)? != b'\'' { r.i += 1; } r.i += 1; + Some(String::from_utf8_lossy(&r.b[start..r.i - 1]).to_string()) } else { let n = r.count()?; + let units: Vec = r + .b + .get(r.i..r.i + n * 2)? + .chunks_exact(2) + .map(|c| u16::from_le_bytes([c[0], c[1]])) + .collect(); r.i += n * 2; + Some(String::from_utf16_lossy(&units)) } - Some(()) } } @@ -529,8 +616,8 @@ mod xps { }) } - /// The fixed page's paths in model units, and the plotted view. - pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec, Option<[f64; 4]>)> { + /// The fixed page's paths and text in model units, and the plotted view. + pub fn read(page: &[u8], w2x: Option<&[u8]>, entry: &SheetEntry) -> Option<(Vec, Option<[f64; 4]>, Vec)> { let mut units = Units::identity(); let mut view_log = None; if let Some(w2x) = w2x.and_then(|b| std::str::from_utf8(b).ok()) { @@ -565,12 +652,37 @@ mod xps { [(x - offset[0]) / scale[0], (y - offset[1]) / scale[1]] }; let mut paths = Vec::new(); + let mut texts = Vec::new(); walk(doc.root_element(), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], &mut |node, m| { - let Some(data) = attr(node, "Data") else { return }; let m = match attr(node, "RenderTransform").and_then(parse_affine) { Some(local) => mul(m, local), None => m, }; + if node.tag_name().name() == "Glyphs" { + // Text: its baseline origin and em size in page units (y + // down), drawn in the plotted family. + // ponytail: the embedded font is obfuscated and not read; + // DWFx text is drawn in Arial. + let num = |name: &str| attr(node, name).and_then(|v| v.parse::().ok()); + let (Some(em), Some(text)) = (num("FontRenderingEmSize"), attr(node, "UnicodeString")) else { return }; + let o = [num("OriginX").unwrap_or(0.0), num("OriginY").unwrap_or(0.0)]; + let map = |p: [f64; 2]| units.model(to_logical(apply(m, p))); + let (a, along, up) = (map(o), map([o[0] + em, o[1]]), map([o[0], o[1] - em])); + let len = |p: [f64; 2]| ((p[0] - a[0]).powi(2) + (p[1] - a[1]).powi(2)).sqrt(); + if !text.trim().is_empty() { + texts.push(Text { + text: text.to_string(), + origin: a, + height: len(up) * CAP_PER_EM, + width_factor: if len(up) > 0.0 { len(along) / len(up) } else { 1.0 }, + rotation: (along[1] - a[1]).atan2(along[0] - a[0]), + color: attr(node, "Fill").and_then(parse_color).unwrap_or([0, 0, 0]), + font: "Arial".to_string(), + }); + } + return; + } + let Some(data) = attr(node, "Data") else { return }; let map = |p: [f64; 2]| units.model(to_logical(apply(m, p))); let subpaths: Vec = parse_data(data).into_iter().map(|sp| map_subpath(sp, map)).collect(); if subpaths.is_empty() { @@ -591,7 +703,7 @@ mod xps { let b = units.model([v[2], v[3]]); [a[0].min(b[0]), a[1].min(b[1]), a[0].max(b[0]), a[1].max(b[1])] }); - Some((paths, view)) + Some((paths, view, texts)) } fn walk(node: roxmltree::Node, m: Affine, f: &mut impl FnMut(roxmltree::Node, Affine)) { @@ -604,7 +716,7 @@ mod xps { }; walk(child, m2, f); } - "Path" => f(child, m), + "Path" | "Glyphs" => f(child, m), _ => {} } } diff --git a/src/scene/model/underlay_vector/mod.rs b/src/scene/model/underlay_vector/mod.rs index 144d83a4a..b0073acae 100644 --- a/src/scene/model/underlay_vector/mod.rs +++ b/src/scene/model/underlay_vector/mod.rs @@ -39,6 +39,15 @@ fn sheet_cache() -> &'static Mutex>>> { CACHE.get_or_init(Default::default) } +/// The cache key of an item seen without some layers. +fn layered_item(item: &str, hidden: &[String]) -> String { + if hidden.is_empty() { + item.to_string() + } else { + format!("{item}#{}", hidden.join("|")) + } +} + fn raster_cache() -> &'static Mutex>>> { static CACHE: OnceLock>>>> = OnceLock::new(); CACHE.get_or_init(Default::default) @@ -51,10 +60,28 @@ pub fn forget(path: &str) { vector_cache().lock().unwrap_or_else(|e| e.into_inner()).retain(|k, _| k.0 != path); } +/// Layer (DWF) or level (DGN) names of a file; empty when it has none. +pub fn layer_names(kind: UnderlayType, path: &str) -> Vec { + let Some(bytes) = source_bytes(path) else { return Vec::new() }; + match kind { + // A plotted DWF carries no layers. + UnderlayType::Dwf | UnderlayType::Pdf => None, + UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::layer_names(&bytes), + UnderlayType::Dgn => dgn8::layer_names(&bytes), + } + .unwrap_or_default() +} + /// The named sheet or model (the first when the name is empty or unknown), /// memoised. Its text is drawn as strokes with the drawing's text engine. pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option> { - let key = (path.to_string(), item.to_string()); + sheet_without(kind, path, item, &[]) +} + +/// The sheet without the `hidden` layers / levels, on the full sheet's +/// rectangle (turning layers off never moves the underlay), memoised. +pub fn sheet_without(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option> { + let key = (path.to_string(), layered_item(item, hidden)); if let Some(hit) = sheet_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) { return hit.clone(); } @@ -62,11 +89,22 @@ pub fn sheet(kind: UnderlayType, path: &str, item: &str) -> Option> { let bytes = source_bytes(path)?; let mut sheet = match kind { UnderlayType::Dwf => dwf::sheet(&bytes, item)?, - UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes)?, - UnderlayType::Dgn => dgn8::model(&bytes, item)?, + UnderlayType::Dgn if dgn7::is_v7(&bytes) => dgn7::model(&bytes, hidden).unwrap_or_default(), + UnderlayType::Dgn => dgn8::model(&bytes, item, hidden).unwrap_or_default(), UnderlayType::Pdf => return None, }; outline_texts(&mut sheet); + // A DGN model's extent is its geometry, text included (a DWF sheet + // keeps its plotted view). + if kind == UnderlayType::Dgn { + if let Some(b) = model::paths_bounds(&sheet.paths) { + let r = sheet.rect; + sheet.rect = [r[0].min(b[0]), r[1].min(b[1]), r[2].max(b[2]), r[3].max(b[3])]; + } + } + if !hidden.is_empty() { + sheet.rect = sheet_without(kind, path, item, &[])?.rect; + } Some(sheet) }; let value = read().map(Arc::new); @@ -83,7 +121,7 @@ fn outline_texts(sheet: &mut Sheet) { t.rotation as f32, t.width_factor as f32, 0.0, - "txt", + &t.font, &t.text, ); let at = |p: [f32; 2]| [p[0] as f64, p[1] as f64]; @@ -114,13 +152,13 @@ pub const RASTER_SIDE: f64 = 3072.0; /// The sheet drawn on a transparent background with `side` pixels on its /// longest side (at most [`RASTER_SIDE`]), straight alpha, rows from the /// top, memoised per size. -pub fn display_raster(kind: UnderlayType, path: &str, item: &str, side: f64) -> Option> { +pub fn display_raster(kind: UnderlayType, path: &str, item: &str, hidden: &[String], side: f64) -> Option> { let side = side.clamp(16.0, RASTER_SIDE).round(); - let key = (path.to_string(), format!("{item}@{side}")); + let key = (path.to_string(), format!("{}@{side}", layered_item(item, hidden))); if let Some(hit) = raster_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) { return hit.clone(); } - let value = sheet(kind, path, item).and_then(|s| rasterize(&s, side)).map(Arc::new); + let value = sheet_without(kind, path, item, hidden).and_then(|s| rasterize(&s, side)).map(Arc::new); raster_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone()); value } @@ -198,12 +236,12 @@ fn vector_cache() -> &'static Mutex Option> { - let key = (path.to_string(), item.to_string()); +pub fn page_vectors(kind: UnderlayType, path: &str, item: &str, hidden: &[String]) -> Option> { + let key = (path.to_string(), layered_item(item, hidden)); if let Some(hit) = vector_cache().lock().unwrap_or_else(|e| e.into_inner()).get(&key) { return hit.clone(); } - let value = sheet(kind, path, item).map(|s| Arc::new(to_page_vectors(&s))); + let value = sheet_without(kind, path, item, hidden).map(|s| Arc::new(to_page_vectors(&s))); vector_cache().lock().unwrap_or_else(|e| e.into_inner()).insert(key, value.clone()); value } diff --git a/src/scene/model/underlay_vector/model.rs b/src/scene/model/underlay_vector/model.rs index 331f53cb7..90e7dd28a 100644 --- a/src/scene/model/underlay_vector/model.rs +++ b/src/scene/model/underlay_vector/model.rs @@ -169,8 +169,17 @@ pub struct Text { pub width_factor: f64, pub rotation: f64, pub color: [u8; 3], + /// The font the text is drawn in: a stroke font name or a TrueType + /// family. + pub font: String, } +/// Height of a TrueType font's capitals per em (Arial and its kin), which +/// turns a plotted em size into a text height. +// ponytail: one ratio for every family; read the font's own metrics if a +// serif family plots noticeably off. +pub const CAP_PER_EM: f64 = 0.716; + /// Points of a B-spline curve: its order, control points and knot vector /// (clamped: `order` equal knots at each end). pub fn bspline_points(order: usize, poles: &[[f64; 2]], knots: &[f64], samples: usize) -> Vec<[f64; 2]> { diff --git a/src/ui/ribbon/context_tools.rs b/src/ui/ribbon/context_tools.rs index 8428f39bf..d0b4a4c86 100644 --- a/src/ui/ribbon/context_tools.rs +++ b/src/ui/ribbon/context_tools.rs @@ -1,4 +1,4 @@ -//! Selection-driven tools: while only PDF underlays (or only xrefs) are +//! Selection-driven tools: while only underlays of one kind (or only xrefs) are //! selected their actions show on the right-edge toolbar; they go when the //! selection changes. Contrast and fade stay in the Properties panel. @@ -9,6 +9,7 @@ use crate::modules::{ModuleEvent, ToolDef}; /// as on. #[derive(Clone, Debug, PartialEq)] pub struct UnderlayContext { + pub kind: codec::entities::UnderlayType, pub monochrome: bool, pub shown: bool, pub snap: bool, @@ -27,10 +28,11 @@ const CLIP: &[u8] = include_bytes!("../../../assets/icons/xclip.svg"); /// The clip icon with a small red cross at its lower right. const UNCLIP: &[u8] = include_bytes!("../../../assets/icons/xclip_remove.svg"); -/// PDF underlay tools: display, clipping, options, layers and import. -pub fn pdf_underlay_tools() -> Vec { +/// Underlay tools: display, clipping, options, layers and (PDF only) +/// import. +pub fn pdf_underlay_tools(kind: codec::entities::UnderlayType) -> Vec { const FRAMES: &[u8] = include_bytes!("../../../assets/icons/underlay_frames.svg"); - vec![ + let mut tools = vec![ tool("_PDFULMONO", "Display in\nMonochrome", FRAMES), tool("_PDFULCLIP", "Create Clipping\nBoundary", CLIP), tool("_PDFULUNCLIP", "Remove\nClipping", UNCLIP), @@ -46,12 +48,15 @@ pub fn pdf_underlay_tools() -> Vec { "Edit\nLayers", include_bytes!("../../../assets/icons/underlay_layers.svg"), ), - tool( + ]; + if kind == codec::entities::UnderlayType::Pdf { + tools.push(tool( "_PDFULIMPORT", "Import As\nObjects", include_bytes!("../../../assets/icons/cui_import.svg"), - ), - ] + )); + } + tools } /// External reference tools: edit, open, clipping and the palette. diff --git a/src/ui/window/pdf_dialogs.rs b/src/ui/window/pdf_dialogs.rs index 4964214d8..7c72a5bb1 100644 --- a/src/ui/window/pdf_dialogs.rs +++ b/src/ui/window/pdf_dialogs.rs @@ -679,7 +679,7 @@ pub fn view_layers<'a>( .height(Fill) .into() } - _ => container(text(t!("This file does not contain any layers.")).size(11).style(muted_style)) + _ => container(text(t!("This file does not contain any layers")).size(11).style(muted_style)) .width(Fill) .height(Fill) .align_x(iced::Center)