fix(pdf): follow-ups to the PDF underlay merge

- Pin cadcodec upstream at d290dbe (the merged underlay change) and
  cadkernel at 4134ae3, which follows it; drop the fork patch, move the
  plugin-API and web-worker crates and the Python plugin lock to the same
  codec so one copy is built.
- "Import as block" with raster images left IMAGE entities without an
  image definition: block definition now creates it like add_entity does,
  through one shared helper.
- The three preview outlines (attach frame, clip boundary, import area)
  stay f64 instead of casting world points to f32.
- Page size / total / PDF scale entries use the catalog's positional
  placeholder; the bare {name} form failed to format in 20 locales.
- The Underlay Layers bulb is the SVG layer icon, not a text glyph.
- PDF dialogs use the shared dialog buttons and radios.
- Options opened from a PDF dialog clears its saved state on return.
- Paper-space underlay background reuses the model block handle already
  resolved; dead `let _ = i` lines and a misplaced doc comment removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-24 23:24:51 +03:00
1 parent 2e60c4da1b
commit d5016c7044
34 files changed
+129 -156

No files matched your search

+2 -4
View File
@@ -137,6 +137,8 @@ impl OpenCADStudio {
// Options opened from a PDF dialog returns to it.
if self.active_modal == Some(Options) {
if let Some(parent) = self.options_parent.take() {
self.options_saved = None;
self.options_close_confirm = false;
self.active_modal = Some(parent);
return;
}
@@ -1326,9 +1328,7 @@ impl OpenCADStudio {
),
Message::PdfAttachPickResult(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);
// Pages, placement and path type are chosen in the Attach
// dialog; the definition is created with the underlay.
@@ -1355,9 +1355,7 @@ impl OpenCADStudio {
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()
+3 -3
View File
@@ -1556,13 +1556,13 @@ pub enum CmdResult {
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
/// Erase `handle` and replace with new entities; command stays active.
ReplaceEntity(Handle, Vec<EntityType>),
/// Attach PDF pages: each page's underlay with its file (as stored) and
/// page; the host creates or reuses the definitions and commits them all
/// in one undo step, then ends the command.
/// Open the PDF Import Settings dialog; the command keeps its prompt.
OpenPdfImportSettings,
/// Import a page of a file (PDFIMPORT File); ends the command.
PdfImportFile(crate::modules::insert::pdf_import::PdfFileImport),
/// Attach PDF pages: each page's underlay with its file (as stored) and
/// page; the host creates or reuses the definitions and commits them all
/// in one undo step, then ends the command.
AttachPdfPages {
path: String,
pages: Vec<(String, EntityType)>,
+3 -6
View File
@@ -329,15 +329,12 @@ impl PdfAttachCommand {
}
}
fn frame_at(&self, pt: DVec3) -> Vec<[f32; 3]> {
fn frame_at(&self, pt: DVec3) -> Vec<[f64; 3]> {
let (w, h) = (self.page_size.0 * self.scale, self.page_size.1 * self.scale);
let x = self.plane.vector_to_world(DVec3::new(w, 0.0, 0.0));
let y = self.plane.vector_to_world(DVec3::new(0.0, h, 0.0));
let corners = [pt, pt + x, pt + x + y, pt + y, pt];
corners
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.collect()
corners.iter().map(|p| p.to_array()).collect()
}
}
@@ -434,7 +431,7 @@ impl CadCommand for PdfAttachCommand {
if self.step != Step::Insertion || self.page_size.0 <= 0.0 {
return None;
}
Some(WireModel::solid(
Some(WireModel::solid_f64(
"pdf_attach_frame".into(),
self.frame_at(pt),
WireModel::CYAN,
+2 -2
View File
@@ -336,9 +336,9 @@ impl CadCommand for PdfClipCommand {
}
_ => return None,
};
let mut points: Vec<[f32; 3]> = ring.iter().map(|p| [p.x as f32, p.y as f32, p.z as f32]).collect();
let mut points: Vec<[f64; 3]> = ring.iter().map(|p| p.to_array()).collect();
points.push(points[0]);
Some(WireModel::solid("pdf_clip_boundary".into(), points, WireModel::CYAN, false))
Some(WireModel::solid_f64("pdf_clip_boundary".into(), points, WireModel::CYAN, false))
}
}
+2 -3
View File
@@ -414,10 +414,9 @@ impl CadCommand for PdfImportCommand {
}
_ => return None,
};
let mut points: Vec<[f32; 3]> =
ring.iter().map(|p| [p.x as f32, p.y as f32, p.z as f32]).collect();
let mut points: Vec<[f64; 3]> = ring.iter().map(|p| p.to_array()).collect();
points.push(points[0]);
Some(WireModel::solid("pdf_import_area".into(), points, WireModel::CYAN, false))
Some(WireModel::solid_f64("pdf_import_area".into(), points, WireModel::CYAN, false))
}
}
+26 -22
View File
@@ -343,28 +343,7 @@ impl Scene {
None
};
// Auto-create an ImageDefinition object for new RasterImage entities
// that don't already reference one.
if let EntityType::RasterImage(ref mut img) = entity {
if img.definition_handle.is_none() {
use acadrust::objects::{ImageDefinition, ObjectType};
let def_handle = Handle::new(self.document.next_handle());
if self.is_recording_undo() {
self.record_undo_object_before(def_handle, None);
}
let mut img_def = ImageDefinition::with_dimensions(
&img.file_path,
img.size.x as u32,
img.size.y as u32,
);
img_def.handle = def_handle;
img_def.is_loaded = true;
self.document
.objects
.insert(def_handle, ObjectType::ImageDefinition(img_def));
img.definition_handle = Some(def_handle);
}
}
self.ensure_image_definition(&mut entity);
// Register the entity's layer if it names one no LAYER command created
// (e.g. a plugin-supplied layer) so it survives a DWG save instead of
@@ -1069,6 +1048,30 @@ impl Scene {
/// caller starts an interactive insert so paste-as-block can prompt for the
/// drop point. The geometry comes from the clipboard rather than live
/// entities, so there is nothing to stage or erase. (#129)
/// Give a new RasterImage the ImageDefinition object it must reference,
/// unless it already names one.
fn ensure_image_definition(&mut self, entity: &mut EntityType) {
let EntityType::RasterImage(img) = entity else {
return;
};
if img.definition_handle.is_some() {
return;
}
use acadrust::objects::{ImageDefinition, ObjectType};
let def_handle = Handle::new(self.document.next_handle());
if self.is_recording_undo() {
self.record_undo_object_before(def_handle, None);
}
let mut img_def =
ImageDefinition::with_dimensions(&img.file_path, img.size.x as u32, img.size.y as u32);
img_def.handle = def_handle;
img_def.is_loaded = true;
self.document
.objects
.insert(def_handle, ObjectType::ImageDefinition(img_def));
img.definition_handle = Some(def_handle);
}
pub fn define_block_from_owned_entities(
&mut self,
entities: Vec<EntityType>,
@@ -1125,6 +1128,7 @@ impl Scene {
Self::reset_clone_subhandles(&mut self.document, &mut entity);
entity.common_mut().handle = Handle::NULL;
entity.common_mut().owner_handle = br_handle;
self.ensure_image_definition(&mut entity);
let handle = self
.document
.add_entity(entity)
+1 -7
View File
@@ -1268,21 +1268,15 @@ fn build_derived_caches_impl(
EntityType::Ole2Frame(ole) => ImageModel::from_ole2frame(ole).map(|m| (handle, m)),
EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) {
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
{
// Paper-space underlays adjust to the white sheet.
let model = doc.objects.values().find_map(|object| match object {
acadrust::objects::ObjectType::Layout(l) if l.name == "Model" => Some(l.block_record),
_ => None,
});
let owner = u.common.owner_handle;
let background = if owner.is_null() || model.is_none_or(|m| m == owner) {
let background = if owner.is_null() || owner == model_block {
LOAD_BG
} else {
[1.0, 1.0, 1.0, 1.0]
};
ImageModel::from_underlay(u, def, background).map(|m| (handle, m))
}
}
_ => None,
},
_ => None,
+18 -34
View File
@@ -4,8 +4,8 @@
use std::fmt;
use iced::widget::{
button, column, combo_box, container, image, pick_list, radio, row, scrollable, text,
text_input, Space,
button, column, combo_box, container, image, pick_list, row, scrollable, text, text_input,
Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
@@ -14,7 +14,7 @@ use crate::io::xref_model::Pathtype;
use crate::modules::insert::pdf_import::{ImportLayers, PdfImportSettings};
use crate::t;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, field_style};
use crate::ui::style::form::{button_style, dialog_button, field_style, form_radio};
use crate::ui::window::block_definition::{group, labeled_checkbox};
/// One edit in any of the PDF dialogs.
@@ -260,18 +260,9 @@ fn read_only<'a>(label: String, value: &'a str, label_width: f32) -> Element<'a,
fn footer<'a>(ok: PdfDialogMsg, help: &'static str) -> iced::widget::Row<'a, Message> {
row![
Space::new().width(Fill),
button(text(t!("OK")).size(11))
.on_press(msg(ok))
.style(button_style(true))
.padding([4, 16]),
button(text(t!("Cancel")).size(11))
.on_press(Message::CloseModal)
.style(button_style(false))
.padding([4, 12]),
button(text(t!("Help")).size(11))
.on_press(msg(PdfDialogMsg::Help(help)))
.style(button_style(false))
.padding([4, 12]),
dialog_button(t!("OK"), msg(ok), true),
dialog_button(t!("Cancel"), Message::CloseModal, false),
dialog_button(t!("Help"), msg(PdfDialogMsg::Help(help)), false),
]
.spacing(6)
.align_y(iced::Center)
@@ -513,10 +504,11 @@ pub fn view_attach<'a>(
} else {
t!("Show Details")
};
let footer = row![button(text(details_label).size(11))
.on_press(msg(PdfDialogMsg::AttachDetails(!state.details)))
.style(button_style(false))
.padding([4, 12])]
let footer = row![dialog_button(
details_label,
msg(PdfDialogMsg::AttachDetails(!state.details)),
false
)]
.push(footer(PdfDialogMsg::AttachOk, "attach"))
.align_y(iced::Center);
@@ -582,13 +574,10 @@ pub fn view_layers<'a>(
.filter(|name| needle.is_empty() || name.to_lowercase().contains(&needle))
.map(|name| {
let on = !t.hidden.contains(name);
let bulb = button(text("●").size(12).style(move |theme: &Theme| text::Style {
color: Some(if on {
theme.palette().warning.base.color
} else {
theme.palette().background.strong.color
}),
}))
let bulb = button(crate::ui::icons::semantic(
crate::ui::icons::layer_visible(on),
14.0,
))
.on_press(msg(PdfDialogMsg::LayersToggle(name.clone())))
.padding([0, 4])
.style(|_: &Theme, _| button::Style::default());
@@ -649,9 +638,7 @@ fn settings_groups<'a>(s: &'a PdfImportSettings) -> (Element<'a, Message>, Eleme
Length::Fill,
);
let layer_radio = |label: std::borrow::Cow<'static, str>, value: ImportLayers| {
radio(label, value, Some(s.layers), |v| msg(PdfDialogMsg::Layers(v)))
.size(14)
.text_size(11)
form_radio(label, value, Some(s.layers), |v| msg(PdfDialogMsg::Layers(v)))
};
let layers = group(
t!("Layers").into_owned(),
@@ -827,11 +814,8 @@ pub fn view_import_file<'a>(
.width(Length::FillPortion(1));
let body = row![container(page_group).width(Length::FillPortion(1)), right].spacing(8);
let footer = row![button(text(t!("Options...")).size(11))
.on_press(msg(PdfDialogMsg::Options))
.style(button_style(false))
.padding([4, 12])]
.push(footer(PdfDialogMsg::ImportOk, "import"))
let footer = row![dialog_button(t!("Options..."), msg(PdfDialogMsg::Options), false)]
.push(footer(PdfDialogMsg::ImportOk, "import"))
.align_y(iced::Center);
column![file_row, body, footer]