Merge pull request #1472 from ramox81:codex/pdf-attach-parity

This commit is contained in:
Hakan Seven committed 2026-09-24 23:24:43 +03:00
commit 2e60c4da1b
63 files changed
+7470 -405

No files matched your search

+1 -1
View File
@@ -1066,7 +1066,7 @@ impl OpenCADStudio {
}
}
if let Some(bounds) = bounds {
let (min, max) = crate::scene::convert::tess::entity_bounds(e);
let (min, max) = crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, e);
if max[0] < bounds[0]
|| max[1] < bounds[1]
|| min[0] > bounds[2]
+27
View File
@@ -3892,6 +3892,33 @@ 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;
self.tabs[i].snap_result = None;
self.run_pdf_import(
i,
crate::app::commands::pdf_import::PdfImportSource::Underlay(request),
);
}
CmdResult::UpdateEntityAndFinish { handle, entity } => {
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
+53 -2
View File
@@ -695,8 +695,59 @@ impl OpenCADStudio {
self.open_attedit_dialog();
}
"PDFATTACH" => {
return Some(Task::done(Message::PdfAttachPick));
"PDFIMPORT" => {
use crate::command::CadCommand;
let command = crate::modules::insert::pdf_import::PdfImportCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"_PDFIMPORTFILE" => {
return Some(Task::done(Message::PdfImportPick));
}
// A file named after it skips the picker.
cmd if cmd.starts_with("_PDFIMPORTFILE ") => {
let path = cmd["_PDFIMPORTFILE ".len()..].trim().trim_matches('"').to_string();
match std::fs::read(&path) {
Ok(bytes) => {
crate::scene::model::pdf_raster::register_source(&path, std::sync::Arc::new(bytes));
self.open_pdf_import_file(&path);
}
Err(_) => self.command_line.push_error(&format!("{path} not found.")),
}
}
"PDFCLIP" | "CLIP" => {
use crate::command::CadCommand;
let command = crate::modules::insert::pdf_clip::PdfClipCommand::new(cmd == "CLIP");
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
// PDFATTACH picks the file; -PDFATTACH asks for it on the command
// line. A path after either names the file at once.
cmd if matches!(
cmd.split_whitespace().next().map(|v| v.to_ascii_uppercase()).as_deref(),
Some("PDFATTACH") | Some("-PDFATTACH")
) =>
{
use crate::command::CadCommand;
let (verb, rest) = cmd
.split_once(char::is_whitespace)
.map(|(verb, rest)| (verb.to_ascii_uppercase(), rest.trim()))
.unwrap_or_else(|| (cmd.to_ascii_uppercase(), ""));
if verb == "PDFATTACH" && rest.is_empty() {
return Some(Task::done(Message::PdfAttachPick));
}
let insunits = self.tabs[i].scene.document.header.insertion_units;
let mut command =
crate::modules::insert::pdf_attach::PdfAttachCommand::new(insunits);
if !rest.is_empty() {
if let Some(crate::command::CmdResult::ReportError(message)) =
command.on_text_input(rest)
{
self.command_line.push_error(&message);
}
}
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"XATTACH" => {
// Launch the file picker; XAttachPickResult will start the command.
+2 -2
View File
@@ -1861,8 +1861,8 @@ impl OpenCADStudio {
}
}
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS"
| "UOSNAP" => {
"UNDERLAYLAYERS" | "ULAYERS" => self.open_underlay_layers_dialog(i),
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" => {
self.command_line
.push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
}
+11
View File
@@ -13,6 +13,9 @@ mod fileops;
mod inquiry;
mod layerprops;
mod layers;
pub(crate) mod pdf_import;
mod pdf_underlay;
mod pdf_dialogs;
mod plotvars;
mod styleprops;
mod view;
@@ -253,6 +256,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);
}
@@ -610,6 +616,11 @@ inventory::submit!(crate::command::CommandRegistration {
"FRAME",
"IMAGEFRAME",
"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()
}
+269
View File
@@ -0,0 +1,269 @@
//! PDFIMPORT: adding the converted PDF content to the drawing as one undo
//! step, with the layers and text styles it uses, and what then happens to
//! the underlay (Keep / Detach / Unload).
use super::*;
use crate::modules::insert::pdf_import::{self, ImportArea, PdfImportRequest, UnderlayMode};
/// What an import reads: a placed underlay, or a file imported at the
/// origin at full size.
pub(crate) enum PdfImportSource {
Underlay(PdfImportRequest),
File(pdf_import::PdfFileImport),
}
impl OpenCADStudio {
pub(crate) fn run_pdf_import(&mut self, i: usize, source: PdfImportSource) {
use acadrust::objects::ObjectType;
let document = &self.tabs[i].scene.document;
let (underlay, path, page, area, mode, handle) = match &source {
PdfImportSource::Underlay(request) => {
let Some(acadrust::EntityType::Underlay(underlay)) =
document.get_entity(request.underlay)
else {
return;
};
let Some(def) = crate::entities::underlay::definition(underlay, document) else {
return;
};
if def.unloaded {
self.command_line
.push_error("Cannot bind a PDF underlay that is unloaded.");
return;
}
(
underlay.clone(),
def.file_path.clone(),
crate::entities::underlay::page_of(def).to_string(),
request.area.clone(),
Some(request.mode),
Some(request.underlay),
)
}
PdfImportSource::File(import) => (
import.placement(),
import.path.clone(),
import.page.clone(),
ImportArea::All,
None,
None,
),
};
let shown = crate::entities::underlay::display_path(&path);
match &source {
PdfImportSource::Underlay(_) => self
.command_line
.push_output(&format!("Binding PDF file {shown}, page {page} ...")),
PdfImportSource::File(_) => self
.command_line
.push_output(&format!("Importing page {page} of PDF file: {shown}...")),
}
// 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 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());
layer.handle = scene.document.allocate_handle();
layer.color = *color;
let _ = scene.document.layers.add(layer);
}
}
for (name, font) in &result.text_styles {
if !scene.document.text_styles.contains(name) {
let mut style = acadrust::tables::TextStyle::new(name.as_str());
style.handle = scene.document.allocate_handle();
style.font_file = font.clone();
let _ = scene.document.text_styles.add(style);
}
}
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 => {
if let Some(ObjectType::UnderlayDefinition(definition)) =
scene.document.objects.get_mut(&underlay.definition_handle)
{
definition.unloaded = true;
}
scene.reseed_underlays();
}
UnderlayMode::Detach => {
scene.erase_entities(&[handle]);
// Drop the definition once nothing references it.
let definition = underlay.definition_handle;
let referenced = scene.document.entities().any(|entity| {
matches!(entity, acadrust::EntityType::Underlay(other)
if other.definition_handle == definition)
});
if !referenced {
scene.document.objects.remove(&definition);
for object in scene.document.objects.values_mut() {
if let ObjectType::Dictionary(dictionary) = object {
dictionary.entries.retain(|(_, target)| *target != definition);
}
}
}
}
}
}
self.tabs[i].dirty = true;
self.refresh_layer_panel();
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))
}
}
+54
View File
@@ -1015,6 +1015,11 @@ impl OpenCADStudio {
| "FRAME"
| "IMAGEFRAME"
| "PDFFRAME"
| "PDFOSNAP"
| "UOSNAP"
| "PDFIMPORTMODE"
| "PDFIMPORTFILTER"
| "PDFIMPORTLAYERS"
| "POINTCLOUDCLIPFRAME"
| "XCLIPFRAME"
| "WIPEOUTFRAME"
@@ -1106,6 +1111,55 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
// 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 {
match value.parse::<i16>().ok().filter(|value| (0..=1).contains(value)) {
Some(mode) => {
if current != mode {
crate::scene::model::pdf_vector::set_pdf_osnap(mode == 1);
self.tabs[i].scene.reseed_underlays();
}
}
None => self.command_line.push_error("Requires 0 or 1 only"),
}
} 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(), "SHOWHIST" | "SOLIDHIST") {
let current = if name == "SHOWHIST" {
self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2)
+42
View File
@@ -47,6 +47,11 @@ fn color_value(color: acadrust::types::Color) -> Value {
}
pub(super) const NAMES: &[&str] = &[
"close_modal",
"pdf_dialog_ok",
"pdf_layer_toggle",
"pdf_page_select",
"ribbon_tab",
"ribbon_dropdown",
"dialog_ok",
"close_document",
"toggle_properties",
@@ -664,6 +669,43 @@ impl OpenCADStudio {
let name = string(req, "name")?;
let msg = match name {
"close_modal" => Message::CloseModal,
// The open PDF dialog's OK button.
// Underlay Layers: switch a layer of the shown underlay.
"pdf_layer_toggle" => Message::PdfDialog(
crate::ui::window::pdf_dialogs::PdfDialogMsg::LayersToggle(string(req, "value")?.into()),
),
// Attach dialog: choose pages by index ("0,2").
"pdf_page_select" => {
let pages: Vec<usize> = string(req, "value")?
.split(',')
.filter_map(|p| p.trim().parse().ok())
.collect();
let Some(state) = self.pdf_attach.as_mut() else {
return Err(failure("no_dialog", "The Attach dialog is not open"));
};
state.selected = pages;
return Ok(Task::none());
}
// Bring a ribbon tab forward by module id ("pdf_underlay" for the
// contextual underlay tab).
"ribbon_tab" => {
let id = string(req, "value")?;
if !self.ribbon.select_by_id(id) {
return Err(failure("no_tab", "No such ribbon tab"));
}
return Ok(Task::none());
}
"ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()),
"pdf_dialog_ok" => {
use crate::ui::window::pdf_dialogs::PdfDialogMsg;
Message::PdfDialog(match self.active_modal {
Some(crate::app::ModalKind::PdfAttach) => PdfDialogMsg::AttachOk,
Some(crate::app::ModalKind::UnderlayLayers) => PdfDialogMsg::LayersOk,
Some(crate::app::ModalKind::PdfImportSettings) => PdfDialogMsg::SettingsOk,
Some(crate::app::ModalKind::PdfImportFile) => PdfDialogMsg::ImportOk,
_ => return Err(failure("no_dialog", "No PDF dialog is open")),
})
}
// The open dialog's OK button.
"dialog_ok" => match self.active_modal {
Some(crate::app::ModalKind::XrefAttach) => Message::XrefAttach(
+1 -1
View File
@@ -1199,7 +1199,7 @@ impl OpenCADStudio {
let Some(entity) = tab.scene.document.get_entity(handle) else {
continue;
};
let (min, max) = crate::scene::convert::tess::entity_bounds(entity);
let (min, max) = crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, entity);
let metrics = tab
.scene
.meshes
+24
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 Attach External Reference dialog; None while closed.
xref_attach: Option<crate::ui::window::xref_attach::XrefAttachState>,
/// Working copy of the Write Block (WBLOCK) dialog; `None` while it is closed.
@@ -1884,6 +1891,10 @@ pub enum ModalKind {
LayerTranslator,
DrawingUnits,
BlockDefinition,
PdfAttach,
UnderlayLayers,
PdfImportSettings,
PdfImportFile,
XrefAttach,
WriteBlock,
GeometricTolerance,
@@ -3804,6 +3815,14 @@ pub enum Message {
// ── PDF Underlay ──────────────────────────────────────────────────────
/// Open file-picker dialog for PDFATTACH command (async).
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>),
@@ -3982,6 +4001,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,
xref_attach: None,
wblock: None,
geometric_tolerance: None,
+29 -18
View File
@@ -1498,25 +1498,29 @@ impl OpenCADStudio {
xref_rows(&mut sections, ins, &br.xref_path, factor, overrides);
}
}
// Underlay: name + path from the referenced definition.
// Underlay: name, page, path and size from the definition.
acadrust::EntityType::Underlay(ul) => {
if let Some((name, path)) =
doc.objects.iter().find_map(|(h, o)| match o {
acadrust::objects::ObjectType::UnderlayDefinition(def)
if *h == ul.definition_handle =>
{
let nm = if !def.name.is_empty() {
def.name.clone()
} else {
def.page_name.clone()
};
Some((nm, def.file_path.clone()))
}
_ => None,
})
{
set_row(&mut sections, "ul_name", name);
set_row(&mut sections, "ul_path", path);
use crate::entities::underlay as und;
if let Some(def) = und::definition(ul, doc) {
set_row(&mut sections, "ul_name", und::definition_display_name(def));
set_row(&mut sections, "ul_page", und::page_of(def).to_string());
set_row(
&mut sections,
"ul_path",
und::display_path(&def.file_path),
);
}
if let Some((w, h)) = und::shown_size(ul, doc) {
set_row_value(
&mut sections,
"ul_width",
crate::scene::model::object::PropValue::EditText(und::plain_number(w)),
);
set_row_value(
&mut sections,
"ul_height",
crate::scene::model::object::PropValue::EditText(und::plain_number(h)),
);
}
}
// Leader: text style / vertical text placement / overall
@@ -2355,6 +2359,12 @@ impl OpenCADStudio {
acadrust::EntityType::Dimension(
acadrust::entities::Dimension::Angular3Pt(_),
) => t!("3 Point Angular Dimension").into_owned(),
acadrust::EntityType::Underlay(underlay) => match underlay.underlay_type {
acadrust::entities::UnderlayType::Pdf => t!("PDF Underlay"),
acadrust::entities::UnderlayType::Dwf => t!("DWF Underlay"),
acadrust::entities::UnderlayType::Dgn => t!("DGN Underlay"),
}
.into_owned(),
_ => entity_type_label(entity),
};
// A dynamic dimension shows only its constraint and text
@@ -2619,6 +2629,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;
+32
View File
@@ -3455,6 +3455,38 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
} else {
raw_val
};
// Underlay rows: reject what the reference rejects, and
// write a Width / Height as the scale that produces it.
if field.starts_with("ul_") {
if let Err(message) =
crate::entities::underlay::validate_property(field, &val)
{
self.command_line.push_error(message);
return Task::none();
}
}
let (field, val) = match field {
"ul_width" | "ul_height" => {
let scale = handles.iter().find_map(|handle| {
match self.tabs[i].scene.document.get_entity(*handle) {
Some(acadrust::EntityType::Underlay(underlay)) => {
crate::entities::underlay::size_to_scale(
underlay,
&self.tabs[i].scene.document,
field,
&val,
)
}
_ => None,
}
});
match scale {
Some(scale) => ("ul_scale", scale),
None => return Task::none(),
}
}
_ => (field, val),
};
if matches!(
field,
"current_fit_point" | "current_control_point" | "pm_current_vertex"
+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.",
+49 -21
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(XrefAttach) => self.xref_attach = None,
Some(WriteBlock) => self.wblock = None,
Some(Hyperlink) => {
@@ -1315,32 +1326,43 @@ impl OpenCADStudio {
),
Message::PdfAttachPickResult(Ok((path, bytes))) => {
use crate::command::CadCommand;
use crate::modules::insert::pdf_attach::PdfAttachCommand;
use acadrust::objects::{ObjectType, UnderlayDefinition};
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);
let definition_handle = self.tabs[i].scene.document.allocate_handle();
let mut definition = UnderlayDefinition::pdf(&path_str, "1");
definition.handle = definition_handle;
self.tabs[i].scene.document.objects.insert(
definition_handle,
ObjectType::UnderlayDefinition(definition),
);
let cmd = PdfAttachCommand::new(definition_handle);
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()
}
Message::PdfImportPick => Task::perform(
async {
let handle = crate::sys::file_dialog()
.set_title(crate::t!("Select PDF File").as_ref())
.add_filter(crate::t!("PDF Files").as_ref(), &["pdf", "PDF"])
.pick_file()
.await;
match handle {
Some(h) => {
let path = crate::sys::handle_path(&h);
let bytes = std::sync::Arc::new(h.read().await);
Ok((path, bytes))
}
None => Err("Cancelled".to_string()),
}
},
Message::PdfImportPickResult,
),
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.open_pdf_import_file(&path_str);
Task::none()
}
Message::PdfImportPickResult(Err(_)) => Task::none(),
Message::PdfAttachPickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line
@@ -1750,6 +1772,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::XrefAttach) => crate::t!("Attach External Reference").into_owned(),
Some(K::WriteBlock) => crate::t!("Write Block").into_owned(),
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
@@ -508,6 +512,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::XrefAttach => {
let state = self.xref_attach.as_ref()?;
let height = if state.details { 520 } else { 450 };