PDF underlays: attach dialog with pages, underlay tab, layers, import settings and file import, unload on the definition

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-24 18:31:40 +03:00
1 parent 0c50013ca7
commit 3f6bdf8c59
57 files changed
+5049 -365

No files matched your search

+19 -17
View File
@@ -3190,24 +3190,8 @@ impl OpenCADStudio {
// (before `entity` is moved into commit_entity).
self.update_cont_anchor(&entity);
let label = self.history_label_from_active_cmd(i, "ENTITY");
// A PDF attach creates its definition (and the definitions
// dictionary) with the underlay, in the same undo step.
let pdf_source = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|cmd| cmd.pdf_attach_source());
let delta_safe = pdf_source.is_none() && self.delta_add_safe(i, &entity);
let delta_safe = self.delta_add_safe(i, &entity);
let pending = self.begin_undo(i, label, 1, delta_safe);
if let Some((path, page)) = pdf_source {
let definition = crate::modules::insert::pdf_attach::ensure_pdf_definition(
&mut self.tabs[i].scene.document,
&path,
&page,
);
if let acadrust::EntityType::Underlay(underlay) = &mut entity {
underlay.definition_handle = definition;
}
}
let is_associative_dimension = matches!(
entity,
acadrust::EntityType::Dimension(
@@ -3947,6 +3931,24 @@ impl OpenCADStudio {
}
self.refresh_properties();
}
CmdResult::OpenPdfImportSettings => self.open_pdf_import_settings(),
CmdResult::PdfImportFile(import) => {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.run_pdf_import(
i,
crate::app::commands::pdf_import::PdfImportSource::File(import),
);
}
CmdResult::AttachPdfPages { path, pages } => {
let label = self.history_label_from_active_cmd(i, "PDFATTACH");
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.attach_pdf_pages(i, label, &path, pages);
self.restore_pre_cmd_tangent();
}
CmdResult::PdfImport(request) => {
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
+2 -1
View File
@@ -1861,7 +1861,8 @@ impl OpenCADStudio {
}
}
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS" => {
"UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i),
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" => {
self.command_line
.push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
}
+8
View File
@@ -13,6 +13,8 @@ mod inquiry;
mod layerprops;
mod layers;
pub(crate) mod pdf_import;
mod pdf_underlay;
mod pdf_dialogs;
mod plotvars;
mod styleprops;
mod view;
@@ -250,6 +252,9 @@ impl OpenCADStudio {
if let Some(t) = self.dispatch_blocks(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_pdf_underlay(cmd, i) {
return Some(t);
}
if let Some(t) = self.dispatch_draw(cmd, i) {
return Some(t);
}
@@ -609,6 +614,9 @@ inventory::submit!(crate::command::CommandRegistration {
"PDFFRAME",
"PDFOSNAP",
"UOSNAP",
"PDFIMPORTMODE",
"PDFIMPORTFILTER",
"PDFIMPORTLAYERS",
"POINTCLOUDCLIPFRAME",
"XCLIPFRAME",
"WIPEOUTFRAME",
+507
View File
@@ -0,0 +1,507 @@
//! The PDF dialogs' state changes: Attach PDF Underlay, Underlay Layers,
//! PDF Import Settings and Import PDF.
use super::*;
use crate::io::xref_model::Pathtype;
use crate::modules::insert::pdf_import::{self, PdfFileImport};
use crate::ui::window::pdf_dialogs::{
click_page, page_thumbs, LayerTarget, PdfAttachState, PdfDialogMsg, PdfImportFileState,
RotationChoice, UnderlayLayersState,
};
/// "7.8740 × 3.9369" (inches) for a page.
fn page_size_text(path: &str, page: &str) -> String {
crate::scene::model::pdf_raster::page_size_inches(path, page)
.map(|(w, h)| format!("{w:.4} × {h:.4}"))
.unwrap_or_default()
}
fn file_stem(path: &str) -> String {
std::path::Path::new(&path.replace('\\', "/"))
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default()
}
impl OpenCADStudio {
/// The path an attached file is stored with: full, relative to the saved
/// drawing (the full path until it is saved), or the file name alone.
fn pdf_stored_path(&self, path: &str, path_type: Pathtype) -> String {
match path_type {
Pathtype::Full => path.to_string(),
Pathtype::Relative => self.tabs[self.active_tab]
.current_path
.as_deref()
.and_then(|host| {
crate::io::xref_model::to_pathtype_result(path, host, Pathtype::Relative).ok()
})
.unwrap_or_else(|| path.to_string()),
Pathtype::None => std::path::Path::new(&path.replace('\\', "/"))
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string()),
}
}
/// PDFATTACH after the file is chosen.
pub(in crate::app) fn open_pdf_attach_dialog(&mut self, path: &str) {
let i = self.active_tab;
let mut existing: Vec<(String, String)> = Vec::new();
for object in self.tabs[i].scene.document.objects.values() {
if let acadrust::objects::ObjectType::UnderlayDefinition(def) = object {
let name = file_stem(&def.file_path);
if !existing.iter().any(|(n, _)| *n == name) {
existing.push((name, def.file_path.clone()));
}
}
}
let mut state = PdfAttachState::new(existing);
self.load_pdf_attach_file(&mut state, path);
self.pdf_attach = Some(state);
self.active_modal = Some(crate::app::ModalKind::PdfAttach);
}
fn load_pdf_attach_file(&self, state: &mut PdfAttachState, path: &str) {
state.path = path.to_string();
state.name = file_stem(path);
state.pages = page_thumbs(path);
state.selected = vec![0];
state.anchor = 0;
state.found_in = crate::entities::underlay::display_path(path);
state.page_size = page_size_text(path, "1");
state.saved_path =
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.
pub(in crate::app) fn open_underlay_layers_dialog(&mut self, i: usize) {
let document = &self.tabs[i].scene.document;
let selected: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.collect();
let mut targets: Vec<LayerTarget> = Vec::new();
for entity in document.entities() {
let acadrust::EntityType::Underlay(u) = entity else {
continue;
};
if u.underlay_type != acadrust::entities::UnderlayType::Pdf {
continue;
}
let Some(def) = crate::entities::underlay::definition(u, document) else {
continue;
};
let mut name = crate::entities::underlay::definition_display_name(def);
let same = targets.iter().filter(|t| t.name.starts_with(&name)).count();
if same > 0 {
name = format!("{name} ({})", same + 1);
}
let mut layers: Vec<String> = crate::scene::model::pdf_layers::layers(&def.file_path)
.into_iter()
.map(|l| l.name)
.collect();
layers.sort_by_key(|n| n.to_lowercase());
layers.dedup();
targets.push(LayerTarget {
name,
handle: u.common.handle,
layers,
hidden: crate::scene::model::pdf_layers::hidden_layers(u),
});
}
if targets.is_empty() {
self.command_line.push_error("No PDF underlays found.");
return;
}
let current = targets
.iter()
.position(|t| selected.contains(&t.handle))
.unwrap_or(0);
self.underlay_layers = Some(UnderlayLayersState {
targets,
current,
search: String::new(),
});
self.active_modal = Some(crate::app::ModalKind::UnderlayLayers);
}
/// PDFIMPORT's Settings option: the dialog over the running command.
pub(in crate::app) fn open_pdf_import_settings(&mut self) {
self.pdf_import_settings = Some(pdf_import::import_settings());
self.active_modal = Some(crate::app::ModalKind::PdfImportSettings);
}
/// PDFIMPORT's File option after the file is chosen.
pub(in crate::app) fn open_pdf_import_file(&mut self, path: &str) {
self.pdf_import_file = Some(PdfImportFileState {
path: path.to_string(),
pages: page_thumbs(path),
selected: 0,
page_text: "1".into(),
insert_on_screen: false,
scale: "1".into(),
rotation: RotationChoice(0),
page_size: page_size_mm(path, "1"),
settings: pdf_import::import_settings(),
});
self.active_modal = Some(crate::app::ModalKind::PdfImportFile);
}
/// Writes the layers each underlay turns off, as one undo step.
fn apply_underlay_layers(&mut self, i: usize, targets: Vec<LayerTarget>) {
use crate::scene::model::pdf_layers::{hidden_layers, LAYER_OVERRIDE_APP};
let changed: Vec<LayerTarget> = targets
.into_iter()
.filter(|t| match self.tabs[i].scene.document.get_entity(t.handle) {
Some(acadrust::EntityType::Underlay(u)) => hidden_layers(u) != t.hidden,
_ => false,
})
.collect();
if changed.is_empty() {
return;
}
self.push_undo_snapshot(i, "ULAYERS");
self.tabs[i].scene.ensure_app_id(LAYER_OVERRIDE_APP);
let app_handle = self.tabs[i]
.scene
.document
.app_ids
.get(LAYER_OVERRIDE_APP)
.map(|a| a.handle.value());
for target in &changed {
if let Some(acadrust::EntityType::Underlay(u)) =
self.tabs[i].scene.document.get_entity_mut(target.handle)
{
let data = &mut u.common.extended_data;
if let Some(app) = app_handle {
data.raw_dwg_eed.retain(|(a, _)| *a != app);
}
data.remove_record(LAYER_OVERRIDE_APP);
if !target.hidden.is_empty() {
let mut record = acadrust::xdata::ExtendedDataRecord::new(LAYER_OVERRIDE_APP);
for name in &target.hidden {
record.add_value(acadrust::xdata::XDataValue::String(name.clone()));
}
data.add_record(record);
}
}
self.tabs[i].scene.reseed_derived_caches(target.handle);
}
let changes: Vec<_> = changed
.iter()
.map(|t| (t.handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
}
pub(in crate::app) fn update_pdf_dialog(&mut self, message: PdfDialogMsg) -> Task<Message> {
let i = self.active_tab;
match message {
PdfDialogMsg::Help(which) => {
let text = match which {
"attach" => crate::t!("Attaches a PDF file as an underlay."),
"layers" => crate::t!("Turns the layers of a PDF underlay on or off."),
_ => crate::t!("Imports the geometry, fills, raster images and text of a PDF file as drawing objects."),
};
self.command_line.push_info(text.as_ref());
}
PdfDialogMsg::Options => {
self.options_parent = self.active_modal;
self.options_open();
}
// ── Attach ────────────────────────────────────────────────────
PdfDialogMsg::AttachBrowse => {
if let Some(state) = &self.pdf_attach {
state.remember();
}
return Task::done(Message::PdfAttachPick);
}
PdfDialogMsg::AttachName(name) => {
let Some(mut state) = self.pdf_attach.take() else {
return Task::none();
};
if let Some((_, path)) = state.existing.iter().find(|(n, _)| *n == name).cloned() {
self.load_pdf_attach_file(&mut state, &path);
}
self.pdf_attach = Some(state);
}
PdfDialogMsg::AttachPage(page) => {
let (ctrl, shift) = (self.ctrl_down, self.shift_down);
if let Some(state) = self.pdf_attach.as_mut() {
click_page(&mut state.selected, &mut state.anchor, page, ctrl, shift);
let first = state.page_labels().into_iter().next().unwrap_or_default();
state.page_size = page_size_text(&state.path, &first);
}
}
PdfDialogMsg::AttachPathType(choice) => {
let Some(mut state) = self.pdf_attach.take() else {
return Task::none();
};
state.path_type = choice;
state.saved_path = crate::entities::underlay::display_path(
&self.pdf_stored_path(&state.path, choice.0),
);
self.pdf_attach = Some(state);
}
PdfDialogMsg::AttachInsertOnScreen(on) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.insert_on_screen = on;
}
}
PdfDialogMsg::AttachInsert(axis, value) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.insert[axis.min(2)] = value;
}
}
PdfDialogMsg::AttachScaleOnScreen(on) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.scale_on_screen = on;
}
}
PdfDialogMsg::AttachScale(value) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.scale = value;
}
}
PdfDialogMsg::AttachRotationOnScreen(on) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.rotation_on_screen = on;
}
}
PdfDialogMsg::AttachRotation(value) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.rotation = value;
}
}
PdfDialogMsg::AttachDetails(on) => {
if let Some(s) = self.pdf_attach.as_mut() {
s.details = on;
}
}
PdfDialogMsg::AttachOk => return self.pdf_attach_ok(i),
// ── Underlay Layers ───────────────────────────────────────────
PdfDialogMsg::LayersUnderlay(name) => {
if let Some(s) = self.underlay_layers.as_mut() {
if let Some(at) = s.targets.iter().position(|t| t.name == name) {
s.current = at;
}
}
}
PdfDialogMsg::LayersSearch(text) => {
if let Some(s) = self.underlay_layers.as_mut() {
s.search = text;
}
}
PdfDialogMsg::LayersToggle(layer) => {
if let Some(s) = self.underlay_layers.as_mut() {
if let Some(t) = s.targets.get_mut(s.current) {
match t.hidden.iter().position(|h| *h == layer) {
Some(at) => {
t.hidden.remove(at);
}
None => t.hidden.push(layer),
}
}
}
}
PdfDialogMsg::LayersOk => {
if let Some(state) = self.underlay_layers.take() {
self.apply_underlay_layers(i, state.targets);
}
self.close_active_modal();
}
// ── Import settings ───────────────────────────────────────────
PdfDialogMsg::Vector(_)
| PdfDialogMsg::Fills(_)
| PdfDialogMsg::Text(_)
| PdfDialogMsg::Raster(_)
| PdfDialogMsg::Layers(_)
| PdfDialogMsg::AsBlock(_)
| PdfDialogMsg::Join(_)
| PdfDialogMsg::Hatches(_)
| PdfDialogMsg::Lineweights(_)
| PdfDialogMsg::Linetypes(_) => {
let settings = if self.active_modal == Some(crate::app::ModalKind::PdfImportFile) {
self.pdf_import_file.as_mut().map(|s| &mut s.settings)
} else {
self.pdf_import_settings.as_mut()
};
if let Some(s) = settings {
match message {
PdfDialogMsg::Vector(v) => s.vector = v,
PdfDialogMsg::Fills(v) => s.fills = v,
PdfDialogMsg::Text(v) => s.text = v,
PdfDialogMsg::Raster(v) => s.raster = v,
PdfDialogMsg::Layers(v) => s.layers = v,
PdfDialogMsg::AsBlock(v) => s.as_block = v,
PdfDialogMsg::Join(v) => s.join = v,
PdfDialogMsg::Hatches(v) => s.hatches = v,
PdfDialogMsg::Lineweights(v) => s.lineweights = v,
PdfDialogMsg::Linetypes(v) => s.linetypes = v,
_ => {}
}
}
}
PdfDialogMsg::SettingsOk => {
if let Some(s) = self.pdf_import_settings.take() {
pdf_import::set_import_settings(s);
}
self.close_active_modal();
}
// ── Import PDF ────────────────────────────────────────────────
PdfDialogMsg::ImportBrowse => return Task::done(Message::PdfImportPick),
PdfDialogMsg::ImportPage(page) => {
if let Some(s) = self.pdf_import_file.as_mut() {
s.selected = page;
s.page_text = (page + 1).to_string();
s.page_size = page_size_mm(&s.path, &s.page_text);
}
}
PdfDialogMsg::ImportPageText(text) => {
if let Some(s) = self.pdf_import_file.as_mut() {
if let Some(n) = text.trim().parse::<usize>().ok().filter(|n| (1..=s.pages.len()).contains(n)) {
s.selected = n - 1;
s.page_size = page_size_mm(&s.path, &n.to_string());
}
s.page_text = text;
}
}
PdfDialogMsg::ImportInsertOnScreen(on) => {
if let Some(s) = self.pdf_import_file.as_mut() {
s.insert_on_screen = on;
}
}
PdfDialogMsg::ImportScale(value) => {
if let Some(s) = self.pdf_import_file.as_mut() {
s.scale = value;
}
}
PdfDialogMsg::ImportRotation(r) => {
if let Some(s) = self.pdf_import_file.as_mut() {
s.rotation = r;
}
}
PdfDialogMsg::ImportOk => {
let Some(state) = self.pdf_import_file.as_ref() else {
return Task::none();
};
let Some(scale) = state.scale.trim().parse::<f64>().ok().filter(|v| *v > 0.0) else {
self.command_line.push_error("Value must be positive and nonzero.");
return Task::none();
};
let state = self.pdf_import_file.take().expect("checked above");
pdf_import::set_import_settings(state.settings);
self.close_active_modal();
let import = PdfFileImport {
path: state.path.clone(),
page: (state.selected + 1).to_string(),
scale,
rotation: (state.rotation.0 as f64).to_radians(),
insertion: glam::DVec3::ZERO,
};
if state.insert_on_screen {
use crate::command::CadCommand;
let command = pdf_import::PdfImportPointCommand::new(import);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
return self.focus_cmd_input();
}
self.run_pdf_import(i, crate::app::commands::pdf_import::PdfImportSource::File(import));
}
}
Task::none()
}
fn pdf_attach_ok(&mut self, i: usize) -> Task<Message> {
use crate::command::CadCommand;
let Some(state) = self.pdf_attach.as_ref() else {
return Task::none();
};
let number = |s: &str| s.trim().parse::<f64>().ok();
let insertion = if state.insert_on_screen {
None
} else {
match (number(&state.insert[0]), number(&state.insert[1]), number(&state.insert[2])) {
(Some(x), Some(y), Some(z)) => Some(glam::DVec3::new(x, y, z)),
_ => {
self.command_line.push_error("Requires numeric value.");
return Task::none();
}
}
};
let scale = if state.scale_on_screen {
None
} else {
match number(&state.scale).filter(|v| *v > 0.0) {
Some(v) => Some(v),
None => {
self.command_line.push_error("Value must be positive and nonzero.");
return Task::none();
}
}
};
let rotation = if state.rotation_on_screen {
None
} else {
match number(&state.rotation) {
Some(v) => Some(v),
None => {
self.command_line.push_error("Requires numeric value.");
return Task::none();
}
}
};
let state = self.pdf_attach.take().expect("checked above");
state.remember();
let stored = self.pdf_stored_path(&state.path, state.path_type.0);
if stored != state.path {
if let Some(bytes) = crate::scene::model::pdf_raster::source_bytes(&state.path) {
crate::scene::model::pdf_raster::register_source(&stored, bytes);
}
}
self.close_active_modal();
let insunits = self.tabs[i].scene.document.header.insertion_units;
let mut command = crate::modules::insert::pdf_attach::PdfAttachCommand::from_dialog(
&state.path,
&stored,
&state.page_labels(),
scale,
rotation,
insunits,
);
match insertion {
Some(point) => {
let result = command.on_point(point);
self.tabs[i].active_cmd = Some(Box::new(command));
if matches!(result, crate::command::CmdResult::NeedPoint) {
self.command_line.push_info(
&self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(),
);
return self.focus_cmd_input();
}
self.apply_cmd_result(result)
}
None => {
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
self.focus_cmd_input()
}
}
}
}
/// "200 × 100 mm" for a page.
fn page_size_mm(path: &str, page: &str) -> String {
crate::scene::model::pdf_raster::page_size_inches(path, page)
.map(|(w, h)| format!("{:.0} × {:.0} mm", w * 25.4, h * 25.4))
.unwrap_or_default()
}
+162 -17
View File
@@ -9,7 +9,7 @@ use crate::modules::insert::pdf_import::{self, ImportArea, PdfImportRequest, Und
/// origin at full size.
pub(crate) enum PdfImportSource {
Underlay(PdfImportRequest),
File(String),
File(pdf_import::PdfFileImport),
}
impl OpenCADStudio {
@@ -26,11 +26,7 @@ impl OpenCADStudio {
let Some(def) = crate::entities::underlay::definition(underlay, document) else {
return;
};
if self.tabs[i]
.scene
.unloaded_underlay_definitions
.contains(&underlay.definition_handle)
{
if def.unloaded {
self.command_line
.push_error("Cannot bind a PDF underlay that is unloaded.");
return;
@@ -44,10 +40,10 @@ impl OpenCADStudio {
Some(request.underlay),
)
}
PdfImportSource::File(path) => (
acadrust::entities::Underlay::pdf(),
path.clone(),
"1".to_string(),
PdfImportSource::File(import) => (
import.placement(),
import.path.clone(),
import.page.clone(),
ImportArea::All,
None,
None,
@@ -62,14 +58,80 @@ impl OpenCADStudio {
.command_line
.push_output(&format!("Importing page {page} of PDF file: {shown}...")),
}
let Some(vectors) = crate::scene::model::pdf_vector::page_vectors(&path, &page) else {
// The underlay's own layer overrides leave those layers out.
let hidden = crate::scene::model::pdf_layers::hidden_layers(&underlay);
let Some(content) =
crate::scene::model::pdf_vector::page_content_by_layer(&path, &page, &hidden)
else {
self.command_line.push_error(&format!("{shown} not found."));
return;
};
let result = pdf_import::convert(&vectors, &underlay, &area);
let settings = pdf_import::import_settings();
let current_layer = {
let header = &self.tabs[i].scene.document.header;
if header.current_layer_name.is_empty() {
self.tabs[i].active_layer.clone()
} else {
header.current_layer_name.clone()
}
};
let naming = pdf_import::LayerNaming {
settings,
prefix: import_prefix(&path),
current: current_layer.clone(),
};
let mut result = pdf_import::convert(&content, &underlay, &area, &naming);
let images = std::mem::take(&mut result.images);
let image_dir = self.tabs[i]
.current_path
.as_deref()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.or_else(|| std::path::Path::new(&path).parent().map(|p| p.to_path_buf()))
.map(|dir| dir.join("PDF Images"));
self.push_undo_snapshot(i, "PDFIMPORT");
let scene = &mut self.tabs[i].scene;
if result.dashed && !scene.document.line_types.contains(pdf_import::DASH_LINETYPE) {
let mut lt = acadrust::tables::LineType::new(pdf_import::DASH_LINETYPE);
lt.handle = scene.document.allocate_handle();
lt.add_element(acadrust::tables::LineTypeElement::dash(0.8));
lt.add_element(acadrust::tables::LineTypeElement::space(0.2));
lt.pattern_length = 1.0;
let _ = scene.document.line_types.add(lt);
}
// Raster images: each written as a PNG under "PDF Images" next to the
// drawing (or the PDF while the drawing is unsaved), then referenced.
let stem = std::path::Path::new(&path.replace('\\', "/"))
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "PDF".to_string());
for (n, image) in images.iter().enumerate() {
let Some(dir) = &image_dir else { break };
if std::fs::create_dir_all(dir).is_err() {
break;
}
let file = dir.join(format!("{stem}{:08x}.png", image_file_id(&path, &page, n)));
if image::save_buffer(&file, &image.rgba, image.width, image.height, image::ColorType::Rgba8)
.is_err()
{
continue;
}
let mut raster = acadrust::entities::RasterImage::with_size(
file.to_string_lossy().as_ref(),
image.insertion,
image.width as f64,
image.height as f64,
1.0,
1.0,
);
raster.u_vector = image.u;
raster.v_vector = image.v;
raster.flags = acadrust::entities::ImageDisplayFlags::SHOW_IMAGE
| acadrust::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
raster.common.layer = image.layer.clone();
result.entities.push(acadrust::EntityType::RasterImage(raster));
}
for (name, color) in &result.layers {
if !scene.document.layers.contains(name) {
let mut layer = acadrust::tables::Layer::new(name.as_str());
@@ -86,16 +148,40 @@ impl OpenCADStudio {
let _ = scene.document.text_styles.add(style);
}
}
for entity in result.entities {
scene.add_entity(entity);
if settings.as_block && !result.entities.is_empty() {
// One block named after the file, inserted at the origin on the
// current layer.
let mut name = stem.clone();
let mut n = 1;
while scene.document.block_records.get(&name).is_some() {
n += 1;
name = format!("{stem}({n})");
}
if scene
.define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO)
.is_ok()
{
let mut insert = acadrust::entities::Insert::new(
name,
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
);
insert.common.layer = current_layer;
scene.add_entity(acadrust::EntityType::Insert(insert));
}
} else {
for entity in result.entities {
scene.add_entity(entity);
}
}
if let (Some(mode), Some(handle)) = (mode, handle) {
match mode {
UnderlayMode::Keep => {}
UnderlayMode::Unload => {
scene
.unloaded_underlay_definitions
.insert(underlay.definition_handle);
if let Some(ObjectType::UnderlayDefinition(definition)) =
scene.document.objects.get_mut(&underlay.definition_handle)
{
definition.unloaded = true;
}
scene.reseed_underlays();
}
UnderlayMode::Detach => {
@@ -122,3 +208,62 @@ impl OpenCADStudio {
self.refresh_properties();
}
}
impl OpenCADStudio {
/// PDFATTACH's end: every page's definition (created or reused, with the
/// definitions dictionary) and underlay in one undo step.
pub(crate) fn attach_pdf_pages(
&mut self,
i: usize,
label: String,
path: &str,
pages: Vec<(String, acadrust::EntityType)>,
) {
let pending = self.begin_undo(i, label, pages.len(), false);
for (page, mut entity) in pages {
let definition = crate::modules::insert::pdf_attach::ensure_pdf_definition(
&mut self.tabs[i].scene.document,
path,
&page,
);
if let acadrust::EntityType::Underlay(underlay) = &mut entity {
underlay.definition_handle = definition;
}
self.commit_entity_handle(entity);
}
self.tabs[i].dirty = true;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
}
/// The layer prefix of an imported file: "PDF_" for the first file of the
/// session, "PDF2_", "PDF3_" … for each other file.
// ponytail: remembered for the session only; a reopened drawing starts from
// "PDF_" again.
fn import_prefix(path: &str) -> String {
static FILES: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
let mut files = FILES.lock().unwrap_or_else(|e| e.into_inner());
let index = match files.iter().position(|f| f.eq_ignore_ascii_case(path)) {
Some(at) => at,
None => {
files.push(path.to_string());
files.len() - 1
}
};
if index == 0 {
"PDF_".to_string()
} else {
format!("PDF{}_", index + 1)
}
}
/// A file name suffix for an imported image, different per import.
fn image_file_id(path: &str, page: &str, n: usize) -> u32 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
(path, page, n, std::time::SystemTime::now()).hash(&mut h);
h.finish() as u32
}
+166
View File
@@ -0,0 +1,166 @@
//! The contextual PDF Underlay ribbon tab: its values come from the selected
//! underlays, its buttons edit them.
use super::*;
use acadrust::entities::{Underlay, UnderlayDisplayFlags, UnderlayType};
use crate::ui::ribbon::{UnderlayContext, UnderlayTabMsg};
impl OpenCADStudio {
/// The selected PDF underlays, or nothing when anything else is selected.
fn selected_pdf_underlays(&self, i: usize) -> Vec<(acadrust::Handle, Underlay)> {
let mut out = Vec::new();
for (handle, entity) in self.tabs[i].scene.selected_entities() {
match entity {
acadrust::EntityType::Underlay(u) if u.underlay_type == UnderlayType::Pdf => {
out.push((handle, u.clone()));
}
_ => return Vec::new(),
}
}
out
}
/// Show the tab while only PDF underlays are selected.
pub(in crate::app) fn sync_underlay_tab(&mut self) {
let i = self.active_tab;
let context = if self.tabs[i].is_start {
None
} else {
self.selected_pdf_underlays(i).first().map(|(_, u)| {
UnderlayContext::new(
u.contrast,
u.fade,
u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
u.flags.contains(UnderlayDisplayFlags::ON),
crate::scene::model::pdf_vector::pdf_osnap(),
)
})
};
self.ribbon.set_underlay_context(context);
}
/// One undo step that edits every selected PDF underlay.
fn edit_selected_underlays(&mut self, i: usize, label: &str, edit: impl Fn(&mut Underlay)) {
let handles: Vec<_> = self
.selected_pdf_underlays(i)
.into_iter()
.map(|(h, _)| h)
.filter(|h| !self.tabs[i].scene.is_layer_locked(*h))
.collect();
if handles.is_empty() {
return;
}
self.push_undo_snapshot(i, label);
for handle in &handles {
if let Some(acadrust::EntityType::Underlay(u)) =
self.tabs[i].scene.document.get_entity_mut(*handle)
{
edit(u);
}
self.tabs[i].scene.reseed_derived_caches(*handle);
}
let changes: Vec<_> = handles
.iter()
.map(|h| (*h, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i].dirty = true;
self.refresh_properties();
}
pub(in crate::app) fn update_underlay_tab(&mut self, message: UnderlayTabMsg) -> Task<Message> {
let i = self.active_tab;
let Some(ctx) = self.ribbon.underlay_context_mut() else {
return Task::none();
};
match message {
UnderlayTabMsg::Contrast(v) => {
ctx.contrast = v;
ctx.contrast_text = v.to_string();
}
UnderlayTabMsg::Fade(v) => {
ctx.fade = v;
ctx.fade_text = v.to_string();
}
UnderlayTabMsg::ContrastText(s) => ctx.contrast_text = s,
UnderlayTabMsg::FadeText(s) => ctx.fade_text = s,
UnderlayTabMsg::Commit => {
// A typed value outside 0–100 or not a number keeps the value.
let parse = |s: &str, old: u8| {
s.trim().parse::<u8>().ok().filter(|v| *v <= 100).unwrap_or(old)
};
let (contrast, fade) = (
parse(&ctx.contrast_text, ctx.contrast),
parse(&ctx.fade_text, ctx.fade),
);
ctx.contrast_text = contrast.to_string();
ctx.fade_text = fade.to_string();
self.edit_selected_underlays(i, "PDFADJUST", |u| {
u.set_contrast(contrast);
u.set_fade(fade);
});
}
}
Task::none()
}
/// The tab's buttons (host-only command names).
pub(super) fn dispatch_pdf_underlay(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
"_PDFULMONO" => {
let on = !self
.selected_pdf_underlays(i)
.first()
.is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::MONOCHROME));
self.edit_selected_underlays(i, "PDFADJUST", |u| u.set_monochrome(on));
}
"_PDFULSHOW" => {
let on = !self
.selected_pdf_underlays(i)
.first()
.is_some_and(|(_, u)| u.flags.contains(UnderlayDisplayFlags::ON));
self.edit_selected_underlays(i, "PDFUNDERLAY", |u| u.set_on(on));
}
"_PDFULSNAP" => {
let on = !crate::scene::model::pdf_vector::pdf_osnap();
crate::scene::model::pdf_vector::set_pdf_osnap(on);
self.tabs[i].scene.reseed_underlays();
self.sync_underlay_tab();
}
"_PDFULUNCLIP" => {
self.edit_selected_underlays(i, "PDFCLIP", |u| {
u.clip_boundary_vertices.clear();
u.clip_inverted = false;
u.flags -= UnderlayDisplayFlags::CLIPPING;
});
}
"_PDFULCLIP" => {
use crate::command::CadCommand;
let Some((handle, underlay)) = self.selected_pdf_underlays(i).into_iter().next()
else {
return Some(Task::none());
};
let (command, first) =
crate::modules::insert::pdf_clip::PdfClipCommand::new_boundary(handle, underlay);
if let crate::command::CmdResult::ReportMeasurement(text) = first {
for line in text.lines() {
self.command_line.push_output(line);
}
}
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"_PDFULIMPORT" => {
use crate::command::CadCommand;
let Some((handle, _)) = self.selected_pdf_underlays(i).into_iter().next() else {
return Some(Task::none());
};
let command = crate::modules::insert::pdf_import::PdfImportCommand::for_underlay(handle);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
_ => return None,
}
Some(self.finish_dispatch(cmd))
}
}
+32
View File
@@ -1017,6 +1017,9 @@ impl OpenCADStudio {
| "PDFFRAME"
| "PDFOSNAP"
| "UOSNAP"
| "PDFIMPORTMODE"
| "PDFIMPORTFILTER"
| "PDFIMPORTLAYERS"
| "POINTCLOUDCLIPFRAME"
| "XCLIPFRAME"
| "WIPEOUTFRAME"
@@ -1110,6 +1113,35 @@ impl OpenCADStudio {
}
// Snapping to the geometry inside underlays (one switch for
// PDF underlays and underlays in general).
// The PDF Import Settings' options as bits.
if matches!(name.as_str(), "PDFIMPORTMODE" | "PDFIMPORTFILTER" | "PDFIMPORTLAYERS") {
use crate::modules::insert::pdf_import::{import_settings, set_import_settings, ImportLayers};
let mut settings = import_settings();
let (current, max) = match name.as_str() {
"PDFIMPORTMODE" => (settings.mode(), 31),
"PDFIMPORTFILTER" => (settings.filter(), 15),
_ => (settings.layers_value(), 2),
};
if let Some(value) = &value {
match value.parse::<i16>().ok().filter(|v| (0..=max).contains(v)) {
Some(v) => {
match name.as_str() {
"PDFIMPORTMODE" => settings.set_mode(v),
"PDFIMPORTFILTER" => settings.set_filter(v),
_ => {
settings.layers = [ImportLayers::Pdf, ImportLayers::Object, ImportLayers::Current][v as usize]
}
}
set_import_settings(settings);
}
None => self.command_line.push_error(&format!("Requires an integer between 0 and {max}.")),
}
} else {
self.command_line.push_output(&format!("Enter new value for {name} <{current}>:"));
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
}
if matches!(name.as_str(), "PDFOSNAP" | "UOSNAP") {
let current = i16::from(crate::scene::model::pdf_vector::pdf_osnap());
if let Some(value) = &value {
+21
View File
@@ -493,6 +493,13 @@ pub(super) struct OpenCADStudio {
drawing_units: Option<crate::ui::window::drawing_units::State>,
/// Working copy of the Block Definition dialog; `None` while it is closed.
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
/// The dialog Options was opened from; it comes back when Options closes.
options_parent: Option<ModalKind>,
/// Working copy of the structured feature-control-frame editor.
geometric_tolerance: Option<crate::ui::window::geometric_tolerance::State>,
/// PICKDRAG (#226): false (default) = press-drag lassoes; true =
@@ -1871,6 +1878,10 @@ pub enum ModalKind {
LayerTranslator,
DrawingUnits,
BlockDefinition,
PdfAttach,
UnderlayLayers,
PdfImportSettings,
PdfImportFile,
GeometricTolerance,
DraftingSettings,
AutoConstrainSettings,
@@ -3773,6 +3784,11 @@ pub enum Message {
PdfAttachPick,
/// PDFIMPORT File: pick the PDF to import.
PdfImportPick,
/// The contextual PDF Underlay tab.
RibbonSelectUnderlayTab,
/// An edit in one of the PDF dialogs.
PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg),
UnderlayTab(crate::ui::ribbon::UnderlayTabMsg),
PdfImportPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>),
/// Result of the PDFATTACH file picker.
PdfAttachPickResult(Result<(std::path::PathBuf, std::sync::Arc<Vec<u8>>), String>),
@@ -3944,6 +3960,11 @@ impl OpenCADStudio {
layer_translator: None,
drawing_units: None,
block_definition: None,
pdf_attach: None,
underlay_layers: None,
pdf_import_settings: None,
pdf_import_file: None,
options_parent: None,
geometric_tolerance: None,
pick_drag_rect: false,
perf_hud: false,
+1
View File
@@ -2610,6 +2610,7 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}",
let m_panel = t_all.map(|t| t.elapsed().as_secs_f64() * 1000.0);
let t_ribbon = crate::perf::enabled().then(iced::time::Instant::now);
self.sync_ribbon_from_selection();
self.sync_underlay_tab();
if let Some(t) = t_all {
let ribbon_ms = t_ribbon.map_or(0.0, |r| r.elapsed().as_secs_f64() * 1000.0);
let total_ms = t.elapsed().as_secs_f64() * 1000.0;
+16
View File
@@ -990,6 +990,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
Err(msg) => self.command_line.push_error(msg.as_str()),
}
}
self.tabs[i].scene.reseed_underlays();
}
XrefPaletteOp::Reload => {
let base_dir: std::path::PathBuf = host
@@ -1016,6 +1017,21 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
"XREF: cannot reload nested reference '{}'. Reload it in its host drawing.",
name
).as_ref());
} else if *kind == crate::io::xref_model::RefKind::Pdf {
// A PDF reloads by clearing its definition's unloaded state.
for (key, row_name, row_kind, _) in &picked {
if row_kind != kind || row_name != name {
continue;
}
let handle = acadrust::types::Handle::new(*key);
if let Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) =
self.tabs[i].scene.document.objects.get_mut(&handle)
{
def.unloaded = false;
}
self.tabs[i].xref_unloaded.remove(key);
}
self.tabs[i].scene.reseed_underlays();
} else if *kind != crate::io::xref_model::RefKind::DwgXref {
self.command_line.push_error(crate::tf!(
"{}: reload applies to drawing references only.",
+24 -16
View File
@@ -128,12 +128,19 @@ impl OpenCADStudio {
/// Close the active in-canvas modal (Plan B), mirroring what closing the
/// old OS window did: a style editor discards its staged (un-applied)
/// changes, and the ribbon tool that launched the dialog is de-highlighted.
fn close_active_modal(&mut self) {
pub(in crate::app) fn close_active_modal(&mut self) {
self.mark_startup_modal_shown();
use super::ModalKind::*;
// Plot Style opened from PLOT behaves as a child modal.
// Closing it restores the parent Plot dialog instead of returning
// to the drawing.
// Options opened from a PDF dialog returns to it.
if self.active_modal == Some(Options) {
if let Some(parent) = self.options_parent.take() {
self.active_modal = Some(parent);
return;
}
}
if self.active_modal == Some(Plotstyle) {
if let Some((plot_offset, plot_resize)) = self.plotstyle_parent_plot_geometry.take() {
self.active_modal = Some(Plot);
@@ -208,6 +215,10 @@ impl OpenCADStudio {
}
Some(GeometricTolerance) => self.geometric_tolerance = None,
Some(BlockDefinition) => self.block_definition = None,
Some(PdfAttach) => self.pdf_attach = None,
Some(UnderlayLayers) => self.underlay_layers = None,
Some(PdfImportSettings) => self.pdf_import_settings = None,
Some(PdfImportFile) => self.pdf_import_file = None,
Some(Hyperlink) => {
self.hyperlink_editor_handles.clear();
self.hyperlink_editor_url.clear();
@@ -1254,20 +1265,13 @@ impl OpenCADStudio {
),
Message::PdfAttachPickResult(Ok((path, bytes))) => {
use crate::command::CadCommand;
use crate::modules::insert::pdf_attach::PdfAttachCommand;
let i = self.active_tab;
let path_str = path.to_string_lossy().into_owned();
let _ = i;
crate::scene::model::pdf_raster::register_source(&path_str, bytes);
// The definition is created with the underlay when it is
// placed; the command asks for the page first.
let insunits = self.tabs[i].scene.document.header.insertion_units;
let cmd = PdfAttachCommand::with_file(&path_str, insunits);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
// Pages, placement and path type are chosen in the Attach
// dialog; the definition is created with the underlay.
self.open_pdf_attach_dialog(&path_str);
Task::none()
}
@@ -1292,11 +1296,9 @@ impl OpenCADStudio {
Message::PdfImportPickResult(Ok((path, bytes))) => {
let i = self.active_tab;
let path_str = path.to_string_lossy().into_owned();
let _ = i;
crate::scene::model::pdf_raster::register_source(&path_str, bytes);
self.run_pdf_import(
i,
crate::app::commands::pdf_import::PdfImportSource::File(path_str),
);
self.open_pdf_import_file(&path_str);
Task::none()
}
Message::PdfImportPickResult(Err(_)) => Task::none(),
@@ -1711,6 +1713,12 @@ impl OpenCADStudio {
Task::none()
}
Message::RibbonSelectUnderlayTab => {
self.ribbon.select_underlay_tab();
Task::none()
}
Message::UnderlayTab(message) => self.update_underlay_tab(message),
Message::PdfDialog(message) => self.update_pdf_dialog(message),
Message::RibbonSelectTab(idx) => {
self.ribbon.select(idx);
Task::none()
+29
View File
@@ -24,6 +24,10 @@ impl OpenCADStudio {
Some(K::LayerTranslator) => crate::t!("Layer Translator").into_owned(),
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
Some(K::BlockDefinition) => crate::t!("Block Definition").into_owned(),
Some(K::PdfAttach) => crate::t!("Attach PDF Underlay").into_owned(),
Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(),
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
@@ -506,6 +510,31 @@ impl OpenCADStudio {
crate::ui::window::block_definition::view_window(state, flow)
})
}
super::super::ModalKind::PdfAttach => {
let state = self.pdf_attach.as_ref()?;
let height = if state.details { 520 } else { 440 };
sized_flow(ex, 820, height, |flow| {
crate::ui::window::pdf_dialogs::view_attach(state, flow)
})
}
super::super::ModalKind::UnderlayLayers => {
let state = self.underlay_layers.as_ref()?;
sized_flow(ex, 500, 500, |flow| {
crate::ui::window::pdf_dialogs::view_layers(state, flow)
})
}
super::super::ModalKind::PdfImportSettings => {
let settings = self.pdf_import_settings.as_ref()?;
sized_flow(ex, 600, 360, |flow| {
crate::ui::window::pdf_dialogs::view_import_settings(settings, flow)
})
}
super::super::ModalKind::PdfImportFile => {
let state = self.pdf_import_file.as_ref()?;
sized_flow(ex, 900, 560, |flow| {
crate::ui::window::pdf_dialogs::view_import_file(state, flow)
})
}
super::super::ModalKind::GeometricTolerance => {
let state = self.geometric_tolerance.as_ref()?;
sized_flow(ex, 670, 530, |flow| {