PDF underlays: attach prompts and definitions, display adjustments and clipping, Properties, snapping, PDFCLIP and PDFIMPORT

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-24 16:00:12 +03:00
1 parent cc9d5edbff
commit 0c50013ca7
50 files changed
+2770 -325

No files matched your search

+1 -1
View File
@@ -818,7 +818,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]
+26 -1
View File
@@ -3190,8 +3190,24 @@ impl OpenCADStudio {
// (before `entity` is moved into commit_entity).
self.update_cont_anchor(&entity);
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = self.delta_add_safe(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 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(
@@ -3931,6 +3947,15 @@ impl OpenCADStudio {
}
self.refresh_properties();
}
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;
+42 -2
View File
@@ -660,8 +660,48 @@ 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));
}
"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.
+1 -2
View File
@@ -1861,8 +1861,7 @@ impl OpenCADStudio {
}
}
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS"
| "UOSNAP" => {
"POINTCLOUDATTACH" | "RECAP" | "SYNCPVIEWPORTS" | "UNDERLAYLAYERS" => {
self.command_line
.push_info(crate::tf!("{cmd}: not yet implemented.").as_ref());
}
+3
View File
@@ -12,6 +12,7 @@ mod fileops;
mod inquiry;
mod layerprops;
mod layers;
pub(crate) mod pdf_import;
mod plotvars;
mod styleprops;
mod view;
@@ -606,6 +607,8 @@ inventory::submit!(crate::command::CommandRegistration {
"FRAME",
"IMAGEFRAME",
"PDFFRAME",
"PDFOSNAP",
"UOSNAP",
"POINTCLOUDCLIPFRAME",
"XCLIPFRAME",
"WIPEOUTFRAME",
+124
View File
@@ -0,0 +1,124 @@
//! 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(String),
}
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 self.tabs[i]
.scene
.unloaded_underlay_definitions
.contains(&underlay.definition_handle)
{
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(path) => (
acadrust::entities::Underlay::pdf(),
path.clone(),
"1".to_string(),
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}...")),
}
let Some(vectors) = crate::scene::model::pdf_vector::page_vectors(&path, &page) else {
self.command_line.push_error(&format!("{shown} not found."));
return;
};
let result = pdf_import::convert(&vectors, &underlay, &area);
self.push_undo_snapshot(i, "PDFIMPORT");
let scene = &mut self.tabs[i].scene;
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);
}
}
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);
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();
}
}
+22
View File
@@ -1015,6 +1015,8 @@ impl OpenCADStudio {
| "FRAME"
| "IMAGEFRAME"
| "PDFFRAME"
| "PDFOSNAP"
| "UOSNAP"
| "POINTCLOUDCLIPFRAME"
| "XCLIPFRAME"
| "WIPEOUTFRAME"
@@ -1106,6 +1108,26 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
// Snapping to the geometry inside underlays (one switch for
// PDF underlays and underlays in general).
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)
+1 -1
View File
@@ -1162,7 +1162,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
+3
View File
@@ -3771,6 +3771,9 @@ pub enum Message {
// ── PDF Underlay ──────────────────────────────────────────────────────
/// Open file-picker dialog for PDFATTACH command (async).
PdfAttachPick,
/// PDFIMPORT File: pick the PDF to import.
PdfImportPick,
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>),
+28 -18
View File
@@ -1479,25 +1479,29 @@ impl OpenCADStudio {
);
}
}
// 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
@@ -2336,6 +2340,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
+32
View File
@@ -3453,6 +3453,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"
+33 -13
View File
@@ -1256,23 +1256,14 @@ 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();
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);
// 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));
@@ -1280,6 +1271,35 @@ impl OpenCADStudio {
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();
crate::scene::model::pdf_raster::register_source(&path_str, bytes);
self.run_pdf_import(
i,
crate::app::commands::pdf_import::PdfImportSource::File(path_str),
);
Task::none()
}
Message::PdfImportPickResult(Err(_)) => Task::none(),
Message::PdfAttachPickResult(Err(e)) => {
if e != "Cancelled" {
self.command_line