DWF and DGN underlays: attach, display, snaps, frames and references

DWFATTACH / -DWFATTACH and DGNATTACH / -DGNATTACH attach a DWF / DWFx sheet
or a DGN (V7 or V8) model with the reference's prompts; ATTACH routes the
file types to them. The sheet or model is read into vector paths, drawn as a
transparent underlay raster (contrast, fade, monochrome and background
adjustment apply) and offers its geometry to object snaps. Definitions are
kept in ACAD_DWFDEFINITIONS / ACAD_DGNDEFINITIONS, the External References
palette lists and reloads them, and image textures get line-keeping mip
levels so thin underlay lines stay visible when zoomed out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-28 12:53:25 +03:00
1 parent 7f8f8457c7
commit cb7860e50e
46 files changed
+2723 -129

No files matched your search

+199 -55
View File
@@ -1,19 +1,21 @@
// PDFATTACH / -PDFATTACH — attach a PDF page as an underlay.
// PDFATTACH / -PDFATTACH — attach a PDF page as an underlay; DWFATTACH /
// DGNATTACH (and their - forms) attach a DWF sheet or a DGN model.
//
// Path to PDF file to attach: (-PDFATTACH only)
// Enter page number or [?] <1>:
// Enter page number or [?] <1>: (DWF: name of sheet, DGN: model)
// Specify conversion units [Master/Sub] <Master>: (DGN only)
// Specify insertion point:
// Base image size: Width: 7.8740, Height: 3.9369, Meters
// Specify scale factor or [Unit] <1>:
// Specify rotation <0>:
// Specify rotation <last>:
//
// The definition (one per file and page, named "<file> - <page>" in the
// ACAD_PDFDEFINITIONS dictionary) is created with the underlay when it is
// ACAD_PDFDEFINITIONS, ACAD_DWFDEFINITIONS or ACAD_DGNDEFINITIONS dictionary) is created with the underlay when it is
// placed, so a cancelled attach leaves nothing and undo removes both.
use std::sync::Mutex;
use codec::entities::{Underlay, UnderlayDisplayFlags};
use codec::entities::{Underlay, UnderlayDisplayFlags, UnderlayType};
use codec::objects::{Dictionary, ObjectType, UnderlayDefinition};
use codec::types::{Handle, Vector3};
use codec::{CadDocument, EntityType};
@@ -51,6 +53,14 @@ const UNITS: [(&str, &str); 9] = [
/// The unit last chosen with the Unit option; it stays the default.
static LAST_UNIT: Mutex<Option<usize>> = Mutex::new(None);
/// The rotation (degrees) of the last underlay attached; the next attach
/// offers it.
static LAST_ROTATION: Mutex<f64> = Mutex::new(0.0);
fn last_rotation() -> f64 {
LAST_ROTATION.lock().map(|r| *r).unwrap_or(0.0)
}
/// The Unit option's index for the drawing's INSUNITS.
fn unit_for_insunits(insunits: i16) -> usize {
match insunits {
@@ -93,6 +103,7 @@ enum Step {
Path,
Page,
ListPages,
Conversion,
Insertion,
Scale,
Unit,
@@ -100,6 +111,7 @@ enum Step {
}
pub struct PdfAttachCommand {
kind: UnderlayType,
step: Step,
/// The file read for the page (absolute or registered source).
path: String,
@@ -112,7 +124,14 @@ pub struct PdfAttachCommand {
preset_rotation: Option<f64>,
page: String,
page_count: usize,
page_size: (f64, f64),
/// DWF sheet or DGN model names of the file.
items: Vec<String>,
/// The page (PDF inches from its corner) or sheet rectangle.
rect: [f64; 4],
/// DGN sub units per master unit (the Sub conversion's scale).
sub_per_master: f64,
/// Scale the scale prompt offers.
default_scale: f64,
insertion: DVec3,
scale: f64,
unit: usize,
@@ -122,12 +141,18 @@ pub struct PdfAttachCommand {
impl PdfAttachCommand {
/// `-PDFATTACH`: starts by asking for the file.
pub fn new(insunits: i16) -> Self {
Self::for_kind(UnderlayType::Pdf, insunits)
}
/// `-DWFATTACH` / `-DGNATTACH` (or `-PDFATTACH`): asks for the file.
pub fn for_kind(kind: UnderlayType, insunits: i16) -> Self {
let unit = LAST_UNIT
.lock()
.ok()
.and_then(|last| *last)
.unwrap_or_else(|| unit_for_insunits(insunits));
Self {
kind,
step: Step::Path,
path: String::new(),
stored_path: None,
@@ -136,7 +161,10 @@ impl PdfAttachCommand {
preset_rotation: None,
page: "1".to_string(),
page_count: 0,
page_size: (0.0, 0.0),
items: Vec::new(),
rect: [0.0; 4],
sub_per_master: 1.0,
default_scale: 1.0,
insertion: DVec3::ZERO,
scale: 1.0,
unit,
@@ -146,15 +174,48 @@ impl PdfAttachCommand {
/// PDFATTACH with a file already chosen: starts at the page prompt.
pub fn with_file(path: &str, insunits: i16) -> Self {
let mut command = Self::new(insunits);
if let Some(count) = crate::scene::model::pdf_raster::page_count(path) {
command.path = path.to_string();
command.page_count = count;
command.step = Step::Page;
}
Self::with_kind_file(UnderlayType::Pdf, path, insunits)
}
/// DWFATTACH / DGNATTACH / PDFATTACH with the file chosen: starts at the
/// page, sheet or model prompt.
pub fn with_kind_file(kind: UnderlayType, path: &str, insunits: i16) -> Self {
let mut command = Self::for_kind(kind, insunits);
command.open(path);
command
}
/// Reads the file's pages or sheet / model names; false when it cannot.
fn open(&mut self, path: &str) -> bool {
let found = match self.kind {
UnderlayType::Pdf => crate::scene::model::pdf_raster::page_count(path).filter(|n| *n > 0).map(|n| {
self.page_count = n;
}),
kind => crate::scene::model::underlay_vector::item_names(kind, path)
.filter(|names| !names.is_empty())
.map(|names| {
self.page = names[0].clone();
self.items = names;
}),
};
if found.is_some() {
self.path = path.to_string();
self.step = Step::Page;
}
found.is_some()
}
fn page_rect(&self, page: &str) -> [f64; 4] {
match self.kind {
UnderlayType::Pdf => crate::scene::model::pdf_raster::page_size_inches(&self.path, page)
.map(|(w, h)| [0.0, 0.0, w, h])
.unwrap_or([0.0; 4]),
kind => crate::scene::model::underlay_vector::sheet(kind, &self.path, page)
.map(|s| s.rect)
.unwrap_or([0.0; 4]),
}
}
/// The dialog's attach with the insertion point asked on screen: pages
/// chosen, the path to store, and any scale / rotation already given.
pub fn from_dialog(
@@ -171,9 +232,7 @@ impl PdfAttachCommand {
command.page = first.clone();
command.extra_pages = rest.to_vec();
}
command.page_size =
crate::scene::model::pdf_raster::page_size_inches(read_path, &command.page)
.unwrap_or((0.0, 0.0));
command.rect = command.page_rect(&command.page);
command.preset_scale = scale;
if let Some(s) = scale {
command.scale = s;
@@ -198,7 +257,9 @@ impl PdfAttachCommand {
fn base_image_size(&self) -> String {
format!(
"Base image size: Width: {:.4}, Height: {:.4}, {}",
self.page_size.0, self.page_size.1, UNITS[self.unit].1
self.rect[2] - self.rect[0],
self.rect[3] - self.rect[1],
UNITS[self.unit].1
)
}
@@ -208,16 +269,15 @@ impl PdfAttachCommand {
return CmdResult::NeedPoint;
}
if std::path::Path::new(&path).extension().is_none() {
path.push_str(".pdf");
path.push_str(match self.kind {
UnderlayType::Pdf => ".pdf",
UnderlayType::Dwf => ".dwf",
UnderlayType::Dgn => ".dgn",
});
}
match crate::scene::model::pdf_raster::page_count(&path) {
Some(count) if count > 0 => {
self.path = path;
self.page_count = count;
self.step = Step::Page;
CmdResult::NeedPoint
}
_ => CmdResult::ReportError(format!(
match self.open(&path) {
true => CmdResult::NeedPoint,
false => CmdResult::ReportError(format!(
"{} not found.",
crate::entities::underlay::display_path(&path)
)),
@@ -230,12 +290,34 @@ impl PdfAttachCommand {
self.step = Step::ListPages;
return CmdResult::NeedPoint;
}
if self.kind != UnderlayType::Pdf {
let name = if text.is_empty() { self.items[0].clone() } else { text.to_string() };
let Some(found) = self.items.iter().find(|n| n.eq_ignore_ascii_case(&name)) else {
// ponytail: the PDF wording; the reference's message for an
// unknown sheet / model name was not measured.
return CmdResult::ReportError(format!(
"There is no {name} in {}.",
crate::entities::underlay::display_path(&self.path)
));
};
self.page = found.clone();
self.rect = self.page_rect(&self.page);
if self.kind == UnderlayType::Dgn {
self.sub_per_master = crate::scene::model::underlay_vector::sheet(self.kind, &self.path, &self.page)
.map(|s| s.sub_per_master)
.filter(|v| *v > 0.0)
.unwrap_or(1.0);
self.step = Step::Conversion;
} else {
self.step = Step::Insertion;
}
return CmdResult::NeedPoint;
}
let page = if text.is_empty() { "1" } else { text };
match page.parse::<usize>() {
Ok(n) if n >= 1 && n <= self.page_count => {
self.page = n.to_string();
self.page_size = crate::scene::model::pdf_raster::page_size_inches(&self.path, &self.page)
.unwrap_or((0.0, 0.0));
self.rect = self.page_rect(&self.page);
self.step = Step::Insertion;
CmdResult::NeedPoint
}
@@ -246,10 +328,30 @@ impl PdfAttachCommand {
}
}
/// DGN: Master keeps the model's master units, Sub offers the scale that
/// turns sub units into master units.
fn accept_conversion(&mut self, text: &str) -> CmdResult {
let t = text.trim().to_ascii_uppercase();
let sub = match t.as_str() {
"" => false,
_ if "MASTER".starts_with(&t) => false,
_ if "SUB".starts_with(&t) => true,
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
};
self.default_scale = if sub { 1.0 / self.sub_per_master } else { 1.0 };
self.scale = self.default_scale;
self.step = Step::Insertion;
CmdResult::NeedPoint
}
fn list_pages(&mut self, pattern: &str) -> CmdResult {
let pattern = if pattern.trim().is_empty() { "*" } else { pattern.trim() };
let lines: Vec<String> = (1..=self.page_count)
.map(|n| n.to_string())
let names: Vec<String> = match self.kind {
UnderlayType::Pdf => (1..=self.page_count).map(|n| n.to_string()).collect(),
_ => self.items.clone(),
};
let lines: Vec<String> = names
.into_iter()
.filter(|n| crate::io::xref_model::wildcard_match(n, pattern))
.collect();
self.step = Step::Page;
@@ -295,7 +397,7 @@ impl PdfAttachCommand {
fn accept_rotation(&mut self, text: &str) -> CmdResult {
let text = text.trim();
let degrees = if text.is_empty() {
0.0
last_rotation()
} else {
match crate::entities::common::parse_f64(text) {
Some(v) => v,
@@ -306,6 +408,9 @@ impl PdfAttachCommand {
}
}
};
if let Ok(mut last) = LAST_ROTATION.lock() {
*last = degrees;
}
self.commit(degrees.to_radians())
}
@@ -314,6 +419,7 @@ impl PdfAttachCommand {
.chain(self.extra_pages.iter().cloned())
.collect();
let placed = underlays_for_pages(
self.kind,
&pages,
self.plane.to_local(self.insertion),
self.scale,
@@ -323,16 +429,16 @@ impl PdfAttachCommand {
.map(|(page, underlay)| (page, self.plane.place_entity(underlay)))
.collect();
CmdResult::AttachPdfPages {
kind: self.kind,
path: self.stored_path.clone().unwrap_or_else(|| self.path.clone()),
pages: placed,
}
}
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];
let at = |x: f64, y: f64| pt + self.plane.vector_to_world(DVec3::new(x * self.scale, y * self.scale, 0.0));
let [x0, y0, x1, y1] = self.rect;
let corners = [at(x0, y0), at(x1, y0), at(x1, y1), at(x0, y1), at(x0, y0)];
corners.iter().map(|p| p.to_array()).collect()
}
}
@@ -343,27 +449,43 @@ impl CadCommand for PdfAttachCommand {
}
fn name(&self) -> &'static str {
"PDFATTACH"
match self.kind {
UnderlayType::Pdf => "PDFATTACH",
UnderlayType::Dwf => "DWFATTACH",
UnderlayType::Dgn => "DGNATTACH",
}
}
fn prompt(&self) -> String {
let item = if self.kind == UnderlayType::Dwf { "sheet" } else { "model" };
match self.step {
Step::Path => "Path to PDF file to attach:".to_string(),
Step::Page => "Enter page number or [?] <1>:".to_string(),
Step::ListPages => "Enter page(s) to list <*>:".to_string(),
Step::Path => format!(
"Path to {} file to attach:",
match self.kind {
UnderlayType::Pdf => "PDF",
UnderlayType::Dwf => "DWF",
UnderlayType::Dgn => "DGN",
}
),
Step::Page if self.kind == UnderlayType::Pdf => "Enter page number or [?] <1>:".to_string(),
Step::Page => format!("Enter name of {item} or [?] <{}>:", self.items.first().map_or("", |s| s)),
Step::ListPages if self.kind == UnderlayType::Pdf => "Enter page(s) to list <*>:".to_string(),
Step::ListPages => format!("Enter {item}(s) to list <*>:"),
Step::Conversion => "Specify conversion units [Master/Sub] <Master>:".to_string(),
Step::Insertion => "Specify insertion point:".to_string(),
Step::Scale => "Specify scale factor or [Unit] <1>:".to_string(),
Step::Scale => format!("Specify scale factor or [Unit] <{}>:", self.default_scale),
Step::Unit => format!(
"Enter unit [MM/Centimeter/Meter/Kilometer/Inch/Foot/Yard/MILe/Unitless] <{}>:",
UNITS[self.unit].0
),
Step::Rotation => "Specify rotation <0>:".to_string(),
Step::Rotation => format!("Specify rotation <{}>:", last_rotation()),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Page => vec![CmdOption::new("?", "?")],
Step::Conversion => vec![CmdOption::new("Master", "M"), CmdOption::new("Sub", "S")],
Step::Scale => vec![CmdOption::new("Unit", "U")],
Step::Unit => vec![
CmdOption::new("MM", "MM"),
@@ -389,6 +511,9 @@ impl CadCommand for PdfAttachCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.step == Step::Conversion {
return CmdResult::ReportError("Invalid option keyword.".to_string());
}
if self.step != Step::Insertion {
return CmdResult::NeedPoint;
}
@@ -405,6 +530,7 @@ impl CadCommand for PdfAttachCommand {
Step::Path => self.accept_path(text),
Step::Page => self.accept_page(text),
Step::ListPages => self.list_pages(text),
Step::Conversion => self.accept_conversion(text),
Step::Insertion => return None,
Step::Scale => self.accept_scale(text),
Step::Unit => self.accept_unit(text),
@@ -417,6 +543,7 @@ impl CadCommand for PdfAttachCommand {
Step::Path => CmdResult::Cancel,
Step::Page => self.accept_page(""),
Step::ListPages => self.list_pages("*"),
Step::Conversion => self.accept_conversion(""),
Step::Insertion => {
CmdResult::ReportError("Point or option keyword required.".to_string())
}
@@ -427,7 +554,7 @@ impl CadCommand for PdfAttachCommand {
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if self.step != Step::Insertion || self.page_size.0 <= 0.0 {
if self.step != Step::Insertion || self.rect[2] <= self.rect[0] {
return None;
}
Some(WireModel::solid_f64(
@@ -442,6 +569,7 @@ impl CadCommand for PdfAttachCommand {
/// Underlays for `pages` of a file, all at `insertion` with the same scale
/// and rotation (the reference stacks the chosen pages there).
pub fn underlays_for_pages(
kind: UnderlayType,
pages: &[String],
insertion: DVec3,
scale: f64,
@@ -450,7 +578,7 @@ pub fn underlays_for_pages(
pages
.iter()
.map(|page| {
let mut underlay = Underlay::pdf();
let mut underlay = Underlay::new(kind);
underlay.insertion_point = Vector3::new(insertion.x, insertion.y, insertion.z);
underlay.set_scale(scale);
underlay.rotation = rotation;
@@ -459,19 +587,30 @@ pub fn underlays_for_pages(
underlay.flags = UnderlayDisplayFlags::ON
| UnderlayDisplayFlags::CLIPPING
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
if kind != UnderlayType::Pdf {
// The reference creates DWF and DGN underlays at contrast 75,
// fade 25.
underlay.contrast = 75;
underlay.fade = 25;
}
(page.clone(), EntityType::Underlay(underlay))
})
.collect()
}
/// The `ACAD_PDFDEFINITIONS` dictionary under the named-objects root,
/// created when the drawing has none.
fn ensure_pdf_dictionary(document: &mut CadDocument) -> Option<Handle> {
/// The kind's definitions dictionary (`ACAD_PDFDEFINITIONS` …) under the
/// named-objects root, created when the drawing has none.
fn ensure_definitions_dictionary(document: &mut CadDocument, kind: UnderlayType) -> Option<Handle> {
let name = match kind {
UnderlayType::Pdf => "ACAD_PDFDEFINITIONS",
UnderlayType::Dwf => "ACAD_DWFDEFINITIONS",
UnderlayType::Dgn => "ACAD_DGNDEFINITIONS",
};
let root = document.header.named_objects_dict_handle;
let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get(&root) else {
return None;
};
if let Some(existing) = root_dictionary.get("ACAD_PDFDEFINITIONS") {
if let Some(existing) = root_dictionary.get(name) {
if matches!(document.objects.get(&existing), Some(ObjectType::Dictionary(_))) {
return Some(existing);
}
@@ -482,7 +621,7 @@ fn ensure_pdf_dictionary(document: &mut CadDocument) -> Option<Handle> {
dictionary.owner = root;
document.objects.insert(handle, ObjectType::Dictionary(dictionary));
if let Some(ObjectType::Dictionary(root_dictionary)) = document.objects.get_mut(&root) {
root_dictionary.add_entry("ACAD_PDFDEFINITIONS", handle);
root_dictionary.add_entry(name, handle);
}
Some(handle)
}
@@ -492,12 +631,13 @@ fn same_path(a: &str, b: &str) -> bool {
norm(a) == norm(b)
}
/// The PDF definition for a file and page: the existing one, or a new one
/// registered in ACAD_PDFDEFINITIONS as "<file> - <page>".
pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str) -> Handle {
/// The definition for a file and page / sheet / model of the kind: the
/// existing one, or a new one registered in its dictionary as
/// "<file> - <page>".
pub fn ensure_underlay_definition(document: &mut CadDocument, kind: UnderlayType, path: &str, page: &str) -> Handle {
let existing = document.objects.iter().find_map(|(handle, object)| match object {
ObjectType::UnderlayDefinition(def)
if matches!(def.underlay_type, codec::entities::UnderlayType::Pdf)
if def.underlay_type == kind
&& same_path(&def.file_path, path)
&& crate::entities::underlay::page_of(def) == page =>
{
@@ -509,10 +649,14 @@ pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str)
return handle;
}
let handle = document.allocate_handle();
let mut definition = UnderlayDefinition::pdf(path, page);
let mut definition = match kind {
UnderlayType::Pdf => UnderlayDefinition::pdf(path, page),
UnderlayType::Dwf => UnderlayDefinition::dwf(path, page),
UnderlayType::Dgn => UnderlayDefinition::dgn(path, page),
};
definition.handle = handle;
let base = crate::entities::underlay::definition_display_name(&definition);
if let Some(dictionary) = ensure_pdf_dictionary(document) {
if let Some(dictionary) = ensure_definitions_dictionary(document, kind) {
definition.owner_handle = dictionary;
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
let mut key = base.clone();
@@ -531,5 +675,5 @@ pub fn ensure_pdf_definition(document: &mut CadDocument, path: &str, page: &str)
}
inventory::submit!(crate::command::CommandRegistration {
names: &["PDFATTACH", "-PDFATTACH"]
names: &["PDFATTACH", "-PDFATTACH", "DWFATTACH", "-DWFATTACH", "DGNATTACH", "-DGNATTACH"]
});