forked from 1b-agent/opencadstudio-upstream
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:
1 parent
0c50013ca7
commit
3f6bdf8c59
57 files changed
+5049
-365
No files matched your search
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user