forked from 1b-agent/opencadstudio-upstream
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:
1 parent
cc9d5edbff
commit
0c50013ca7
50 files changed
+2770
-325
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user