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 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-24 18:31:40 +03:00
1 parent 0c50013ca7
commit 3f6bdf8c59
57 files changed
+5049 -365

No files matched your search

+107 -19
View File
@@ -101,7 +101,15 @@ enum Step {
pub struct PdfAttachCommand {
step: Step,
/// The file read for the page (absolute or registered source).
path: String,
/// The path the definition stores (full, relative or file name only).
stored_path: Option<String>,
/// Pages after the first, attached beside it (dialog selection).
extra_pages: Vec<String>,
/// Scale and rotation fixed in the dialog: their prompts are skipped.
preset_scale: Option<f64>,
preset_rotation: Option<f64>,
page: String,
page_count: usize,
page_size: (f64, f64),
@@ -122,6 +130,10 @@ impl PdfAttachCommand {
Self {
step: Step::Path,
path: String::new(),
stored_path: None,
extra_pages: Vec::new(),
preset_scale: None,
preset_rotation: None,
page: "1".to_string(),
page_count: 0,
page_size: (0.0, 0.0),
@@ -143,6 +155,46 @@ impl PdfAttachCommand {
command
}
/// 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(
read_path: &str,
stored_path: &str,
pages: &[String],
scale: Option<f64>,
rotation_deg: Option<f64>,
insunits: i16,
) -> Self {
let mut command = Self::with_file(read_path, insunits);
command.stored_path = Some(stored_path.to_string());
if let Some((first, rest)) = pages.split_first() {
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.preset_scale = scale;
if let Some(s) = scale {
command.scale = s;
}
command.preset_rotation = rotation_deg;
command.step = Step::Insertion;
command
}
/// After the insertion point or the scale: the next prompt, or the
/// attach itself when the dialog fixed what is left.
fn after_scale(&mut self) -> CmdResult {
match self.preset_rotation {
Some(deg) => self.commit(deg.to_radians()),
None => {
self.step = Step::Rotation;
CmdResult::NeedPoint
}
}
}
fn base_image_size(&self) -> String {
format!(
"Base image size: Width: {:.4}, Height: {:.4}, {}",
@@ -207,8 +259,7 @@ impl PdfAttachCommand {
fn accept_scale(&mut self, text: &str) -> CmdResult {
let text = text.trim();
if text.is_empty() {
self.step = Step::Rotation;
return CmdResult::NeedPoint;
return self.after_scale();
}
if text.eq_ignore_ascii_case("U") || text.eq_ignore_ascii_case("UNIT") {
self.step = Step::Unit;
@@ -217,8 +268,7 @@ impl PdfAttachCommand {
match crate::entities::common::parse_f64(text) {
Some(v) if v > 0.0 => {
self.scale = v;
self.step = Step::Rotation;
CmdResult::NeedPoint
self.after_scale()
}
Some(_) => CmdResult::ReportError("Value must be positive and nonzero.".to_string()),
None => CmdResult::ReportError("Requires numeric value or option keyword.".to_string()),
@@ -260,17 +310,23 @@ impl PdfAttachCommand {
}
fn commit(&mut self, rotation: f64) -> CmdResult {
let point = self.plane.to_local(self.insertion);
let mut underlay = Underlay::pdf();
underlay.insertion_point = Vector3::new(point.x, point.y, point.z);
underlay.set_scale(self.scale);
underlay.rotation = rotation;
// On, clipped by its boundary and colour-adjusted for the background,
// as the reference creates an underlay.
underlay.flags = UnderlayDisplayFlags::ON
| UnderlayDisplayFlags::CLIPPING
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Underlay(underlay)))
let pages: Vec<String> = std::iter::once(self.page.clone())
.chain(self.extra_pages.iter().cloned())
.collect();
let placed = underlays_for_pages(
&self.path,
&pages,
self.plane.to_local(self.insertion),
self.scale,
rotation,
)
.into_iter()
.map(|(page, underlay)| (page, self.plane.place_entity(underlay)))
.collect();
CmdResult::AttachPdfPages {
path: self.stored_path.clone().unwrap_or_else(|| self.path.clone()),
pages: placed,
}
}
fn frame_at(&self, pt: DVec3) -> Vec<[f32; 3]> {
@@ -336,15 +392,14 @@ impl CadCommand for PdfAttachCommand {
}
}
fn pdf_attach_source(&self) -> Option<(String, String)> {
(!self.path.is_empty()).then(|| (self.path.clone(), self.page.clone()))
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.step != Step::Insertion {
return CmdResult::NeedPoint;
}
self.insertion = pt;
if self.preset_scale.is_some() {
return self.after_scale();
}
self.step = Step::Scale;
CmdResult::ReportMeasurement(self.base_image_size())
}
@@ -388,6 +443,39 @@ impl CadCommand for PdfAttachCommand {
}
}
/// Underlays for `pages` of a file: the first at `insertion`, each next one
/// beside the previous along the rotated X axis (page width times scale).
// ponytail: side-by-side placement of the pages after the first is not
// measured against the reference.
pub fn underlays_for_pages(
read_path: &str,
pages: &[String],
insertion: DVec3,
scale: f64,
rotation: f64,
) -> Vec<(String, EntityType)> {
let mut origin = insertion;
let (c, s) = (rotation.cos(), rotation.sin());
pages
.iter()
.map(|page| {
let mut underlay = Underlay::pdf();
underlay.insertion_point = Vector3::new(origin.x, origin.y, origin.z);
underlay.set_scale(scale);
underlay.rotation = rotation;
// On, clipped by its boundary and colour-adjusted for the
// background, as the reference creates an underlay.
underlay.flags = UnderlayDisplayFlags::ON
| UnderlayDisplayFlags::CLIPPING
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
let width = crate::scene::model::pdf_raster::page_size_inches(read_path, page)
.map_or(0.0, |(w, _)| w * scale);
origin += DVec3::new(c * width, s * width, 0.0);
(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> {
+12
View File
@@ -58,6 +58,18 @@ impl PdfClipCommand {
}
}
/// A new boundary for an underlay already chosen (the underlay tab's
/// Create Clipping Boundary): the New boundary branch at once. Returns the
/// command and the result of its first step.
pub fn new_boundary(handle: Handle, underlay: Underlay) -> (Self, CmdResult) {
let mut command = Self::new(false);
command.handle = handle;
command.underlay = Some(underlay);
command.step = Step::Option;
let first = command.option("N");
(command, first)
}
fn finish(&self, underlay: Underlay) -> CmdResult {
CmdResult::UpdateEntityAndFinish {
handle: self.handle,
+625 -189
View File
@@ -6,26 +6,132 @@
// Keep, Detach or Unload PDF underlay? [Keep/Detach/Unload] <Unload>:
// Binding PDF file <path>, page <n> ...
//
// Geometry goes to PDF_Geometry and text to PDF_Text (the PDF has no layers
// of its own to use): chains of straight segments become polylines, a closed
// four-arc Bézier loop that is a circle becomes a CIRCLE, other curves become
// splines, filled areas solid hatches and text runs MTEXT in a "PDF <font>"
// text style.
// Content of a PDF layer goes to PDF_<layer>; the rest to PDF_Geometry,
// PDF_Solid Fills, PDF_Text and PDF_Images (or those alone, or the current
// layer, per the settings). Straight strokes that meet become polylines, a
// closed four-arc Bézier loop that is a circle a CIRCLE, other curves
// splines, filled triangles and quadrilaterals SOLIDs at 50% transparency
// (other fills solid hatches), text runs MTEXT in a "PDF <font>" style and
// raster images PNG files referenced by IMAGE objects.
use acadrust::entities::{
AttachmentPoint, BoundaryEdge, BoundaryPath, Circle, Hatch, LwPolyline, MText, PolylineEdge,
Spline, Underlay, UnderlayType,
Solid, Spline, Underlay, UnderlayType,
};
use acadrust::types::{Color, Handle, LineWeight, Vector2, Vector3};
use acadrust::EntityType;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::scene::model::pdf_vector::{PageVectors, PdfPath, Segment, SubPath};
use crate::scene::model::pdf_vector::{
bezier, circle_of as as_circle, PageVectors, PdfPath, Segment, SubPath,
};
use crate::scene::model::wire_model::WireModel;
pub const GEOMETRY_LAYER: &str = "PDF_Geometry";
pub const TEXT_LAYER: &str = "PDF_Text";
/// Which layers imported objects go to.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ImportLayers {
/// The PDF's own layers ("PDF_<name>"); content outside them to
/// PDF_Geometry / PDF_Text.
Pdf,
/// One layer per kind of object.
Object,
/// The current layer.
Current,
}
/// The PDF Import Settings, kept for the session. The options below the
/// data section are PDFIMPORTMODE's bits.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PdfImportSettings {
pub vector: bool,
pub fills: bool,
pub text: bool,
pub raster: bool,
pub layers: ImportLayers,
pub as_block: bool,
pub join: bool,
pub hatches: bool,
pub lineweights: bool,
pub linetypes: bool,
}
impl Default for PdfImportSettings {
fn default() -> Self {
Self {
vector: true,
fills: true,
text: true,
raster: false,
layers: ImportLayers::Pdf,
as_block: false,
join: true,
hatches: false,
lineweights: true,
linetypes: false,
}
}
}
impl PdfImportSettings {
/// PDFIMPORTMODE: 1 import as block, 2 apply lineweights, 4 join
/// segments, 8 fills as hatches, 16 infer linetypes (default 6).
pub fn mode(&self) -> i16 {
i16::from(self.as_block)
| i16::from(self.lineweights) << 1
| i16::from(self.join) << 2
| i16::from(self.hatches) << 3
| i16::from(self.linetypes) << 4
}
/// PDFIMPORTFILTER: the kinds left out — 1 vector geometry, 2 text,
/// 4 solid fills, 8 raster images (default 8).
pub fn filter(&self) -> i16 {
i16::from(!self.vector)
| i16::from(!self.text) << 1
| i16::from(!self.fills) << 2
| i16::from(!self.raster) << 3
}
pub fn set_filter(&mut self, filter: i16) {
self.vector = filter & 1 == 0;
self.text = filter & 2 == 0;
self.fills = filter & 4 == 0;
self.raster = filter & 8 == 0;
}
/// PDFIMPORTLAYERS: 0 PDF layers, 1 object layers, 2 current layer.
pub fn layers_value(&self) -> i16 {
match self.layers {
ImportLayers::Pdf => 0,
ImportLayers::Object => 1,
ImportLayers::Current => 2,
}
}
pub fn set_mode(&mut self, mode: i16) {
self.as_block = mode & 1 != 0;
self.lineweights = mode & 2 != 0;
self.join = mode & 4 != 0;
self.hatches = mode & 8 != 0;
self.linetypes = mode & 16 != 0;
}
}
static SETTINGS: std::sync::Mutex<Option<PdfImportSettings>> = std::sync::Mutex::new(None);
pub fn import_settings() -> PdfImportSettings {
SETTINGS
.lock()
.map(|s| s.unwrap_or_default())
.unwrap_or_default()
}
pub fn set_import_settings(settings: PdfImportSettings) {
if let Ok(mut s) = SETTINGS.lock() {
*s = Some(settings);
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImportArea {
@@ -47,6 +153,55 @@ pub struct PdfImportRequest {
pub mode: UnderlayMode,
}
/// A page of a file imported without an underlay (the File option): placed
/// at `insertion` with a scale and rotation.
#[derive(Clone, Debug)]
pub struct PdfFileImport {
pub path: String,
pub page: String,
pub scale: f64,
pub rotation: f64,
pub insertion: DVec3,
}
impl PdfFileImport {
/// The underlay the page would be as an attachment, for placing it.
pub fn placement(&self) -> Underlay {
let mut u = Underlay::pdf();
u.insertion_point = Vector3::new(self.insertion.x, self.insertion.y, self.insertion.z);
u.set_scale(self.scale);
u.rotation = self.rotation;
u
}
}
/// Import PDF with the insertion point asked on screen.
pub struct PdfImportPointCommand {
import: PdfFileImport,
}
impl PdfImportPointCommand {
pub fn new(import: PdfFileImport) -> Self {
Self { import }
}
}
impl CadCommand for PdfImportPointCommand {
fn name(&self) -> &'static str {
"PDFIMPORT"
}
fn prompt(&self) -> String {
"Specify insertion point:".to_string()
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.import.insertion = pt;
CmdResult::PdfImportFile(self.import.clone())
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Step {
Select,
@@ -65,6 +220,16 @@ pub struct PdfImportCommand {
}
impl PdfImportCommand {
/// Import from an underlay already chosen (the underlay tab's button):
/// starts at the area prompt.
pub fn for_underlay(handle: Handle) -> Self {
Self {
step: Step::Area,
handle,
..Self::new()
}
}
pub fn new() -> Self {
Self {
step: Step::Select,
@@ -87,9 +252,7 @@ impl PdfImportCommand {
self.step = Step::Polygon;
CmdResult::NeedPoint
}
"S" | "SETTINGS" => {
CmdResult::ReportError("PDF import settings are not available.".to_string())
}
"S" | "SETTINGS" => CmdResult::OpenPdfImportSettings,
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
}
}
@@ -267,6 +430,9 @@ pub struct ImportResult {
pub entities: Vec<EntityType>,
pub layers: Vec<(String, Color)>,
pub text_styles: Vec<(String, String)>,
pub images: Vec<ImageImport>,
/// Some line uses the dash linetype.
pub dashed: bool,
}
/// A PDF colour as an index colour when it is one of the basic seven,
@@ -318,51 +484,6 @@ fn font_file_for(font: &str) -> String {
.to_string()
}
fn bezier(seg: &Segment, t: f64) -> [f64; 2] {
match seg {
Segment::Line(a, b) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
Segment::Cubic(p0, p1, p2, p3) => {
let u = 1.0 - t;
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
[
a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0],
a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1],
]
}
}
}
/// Centre and radius when a closed loop of cubics traces a circle.
fn as_circle(sp: &SubPath) -> Option<([f64; 2], f64)> {
if !sp.closed || sp.segments.len() < 4 {
return None;
}
let cubics: Vec<&Segment> = sp
.segments
.iter()
.filter(|s| matches!(s, Segment::Cubic(..)))
.collect();
if cubics.len() != sp.segments.len() && sp.segments.len() - cubics.len() > 1 {
return None;
}
let starts: Vec<[f64; 2]> = cubics.iter().map(|s| s.start()).collect();
let n = starts.len() as f64;
let c = [
starts.iter().map(|p| p[0]).sum::<f64>() / n,
starts.iter().map(|p| p[1]).sum::<f64>() / n,
];
let dist = |p: [f64; 2]| ((p[0] - c[0]).powi(2) + (p[1] - c[1]).powi(2)).sqrt();
let r = starts.iter().map(|p| dist(*p)).sum::<f64>() / n;
if r <= 0.0 {
return None;
}
let tol = r * 2e-3;
let on_circle = cubics.iter().all(|s| {
[0.0, 0.25, 0.5, 0.75].iter().all(|t| (dist(bezier(s, *t)) - r).abs() <= tol)
});
on_circle.then_some((c, r))
}
fn inside(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
let mut odd = false;
let mut j = poly.len() - 1;
@@ -388,14 +509,202 @@ fn path_in_area(path: &PdfPath, area: Option<&[[f64; 2]]>) -> bool {
})
}
/// The objects an import of `page` creates for an underlay, with the
/// area in world coordinates.
pub fn convert(page: &PageVectors, underlay: &Underlay, area: &ImportArea) -> ImportResult {
/// Where imported objects of each kind go.
pub struct LayerNaming {
pub settings: PdfImportSettings,
/// "PDF_", or "PDF2_", "PDF3_" … for the next files imported.
pub prefix: String,
/// The current layer, for [`ImportLayers::Current`].
pub current: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Kind {
Geometry,
Fill,
Text,
Image,
}
impl LayerNaming {
fn layer(&self, pdf_layer: Option<&str>, kind: Kind) -> String {
let object = |kind| match kind {
Kind::Geometry => "Geometry",
Kind::Fill => "Solid Fills",
Kind::Text => "Text",
Kind::Image => "Images",
};
match (self.settings.layers, pdf_layer) {
(ImportLayers::Current, _) => self.current.clone(),
(ImportLayers::Pdf, Some(name)) => format!("{}{name}", self.prefix),
_ => format!("{}{}", self.prefix, object(kind)),
}
}
}
/// An image the import places: its pixels go to a PNG file named by the
/// caller, then a raster image at `insertion` spanning `u` × `v` per pixel.
pub struct ImageImport {
pub rgba: Vec<u8>,
pub width: u32,
pub height: u32,
pub insertion: Vector3,
pub u: Vector3,
pub v: Vector3,
pub layer: String,
}
/// The linetype collinear dashes become ("Infer linetypes"): dash 0.8,
/// space 0.2, scaled per object to the dash length.
pub const DASH_LINETYPE: &str = "PDF_IMPORT";
/// A stroke before it becomes an object: straight chains can still be joined
/// or read as dashes.
struct Stroke {
layer: String,
color: Color,
weight: LineWeight,
points: Vec<[f64; 2]>,
closed: bool,
}
fn same_point(a: [f64; 2], b: [f64; 2]) -> bool {
(a[0] - b[0]).abs() < 1e-7 && (a[1] - b[1]).abs() < 1e-7
}
/// Open straight strokes of one layer, colour and weight that meet end to
/// end become one polyline (closed when the chain returns to its start).
fn join_strokes(strokes: Vec<Stroke>) -> Vec<Stroke> {
let mut out: Vec<Stroke> = Vec::new();
let mut open: Vec<Stroke> = Vec::new();
for s in strokes {
if s.closed {
out.push(s);
} else {
open.push(s);
}
}
while let Some(mut chain) = open.pop() {
loop {
let end = *chain.points.last().expect("strokes have points");
let start = chain.points[0];
let fits = |o: &Stroke| {
o.layer == chain.layer && o.color == chain.color && o.weight == chain.weight
};
if let Some(i) = open.iter().position(|o| {
fits(o) && (same_point(o.points[0], end) || same_point(*o.points.last().unwrap(), end))
}) {
let mut next = open.swap_remove(i);
if !same_point(next.points[0], end) {
next.points.reverse();
}
chain.points.extend(next.points.into_iter().skip(1));
} else if let Some(i) = open.iter().position(|o| {
fits(o)
&& (same_point(*o.points.last().unwrap(), start) || same_point(o.points[0], start))
}) {
let mut prev = open.swap_remove(i);
if !same_point(*prev.points.last().unwrap(), start) {
prev.points.reverse();
}
prev.points.pop();
prev.points.extend(chain.points);
chain.points = prev.points;
} else {
break;
}
}
if chain.points.len() > 3 && same_point(chain.points[0], *chain.points.last().unwrap()) {
chain.points.pop();
chain.closed = true;
}
out.push(chain);
}
out
}
/// Runs of three or more equal collinear dashes with equal gaps become one
/// line from the first dash's start to the last's end, with the dash length
/// (the linetype scale).
// ponytail: dashes are matched along one direction at a time in drawing
// order; patterns of mixed dash lengths stay separate lines.
fn infer_dashes(strokes: Vec<Stroke>) -> Vec<(Stroke, Option<f64>)> {
let is_dash = |s: &Stroke| !s.closed && s.points.len() == 2;
let len = |s: &Stroke| {
let (a, b) = (s.points[0], s.points[1]);
(b[0] - a[0]).hypot(b[1] - a[1])
};
let mut out: Vec<(Stroke, Option<f64>)> = Vec::new();
let mut run: Vec<Stroke> = Vec::new();
let flush = |run: &mut Vec<Stroke>, out: &mut Vec<(Stroke, Option<f64>)>| {
if run.len() >= 3 {
let dash = len(&run[0]);
let mut line = run.remove(0);
let last = run.pop().expect("three or more");
line.points[1] = last.points[1];
run.clear();
out.push((line, Some(dash)));
} else {
out.extend(run.drain(..).map(|s| (s, None)));
}
};
for s in strokes {
if !is_dash(&s) {
flush(&mut run, &mut out);
out.push((s, None));
continue;
}
let continues = run.last().is_some_and(|prev| {
let first = &run[0];
let (a, b) = (prev.points[0], prev.points[1]);
let dir = [(b[0] - a[0]) / len(prev), (b[1] - a[1]) / len(prev)];
let (c, d) = (s.points[0], s.points[1]);
let gap = [c[0] - b[0], c[1] - b[1]];
let along = gap[0] * dir[0] + gap[1] * dir[1];
let across = (gap[0] * dir[1] - gap[1] * dir[0]).abs();
let seg = [(d[0] - c[0]) / len(&s), (d[1] - c[1]) / len(&s)];
let parallel = (seg[0] * dir[1] - seg[1] * dir[0]).abs() < 1e-6
&& seg[0] * dir[0] + seg[1] * dir[1] > 0.0;
let first_gap = run.get(1).map(|second| {
let (e, f) = (first.points[1], second.points[0]);
(f[0] - e[0]).hypot(f[1] - e[1])
});
s.layer == prev.layer
&& s.color == prev.color
&& s.weight == prev.weight
&& parallel
&& across < 1e-6
&& along > 1e-9
&& (len(&s) - len(first)).abs() < 1e-6
&& first_gap.is_none_or(|g| (g - along).abs() < 1e-6)
});
if !continues {
flush(&mut run, &mut out);
}
run.push(s);
}
flush(&mut run, &mut out);
out
}
/// The objects an import creates from a page's content split by PDF layer
/// (see `page_content_by_layer`), placed as `underlay` places the page.
pub fn convert(
content: &[(Option<String>, PageVectors)],
underlay: &Underlay,
area: &ImportArea,
naming: &LayerNaming,
) -> ImportResult {
let settings = &naming.settings;
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(underlay, p);
let world2 = |p: [f64; 2]| {
let w = world(p);
Vector2::new(w[0], w[1])
};
let world3 = |p: [f64; 2]| {
let w = world(p);
Vector3::new(w[0], w[1], w[2])
};
let z = underlay.insertion_point.z;
let scale = underlay.x_scale.abs();
// The area is tested in page space, where the page content lives.
@@ -416,152 +725,279 @@ pub fn convert(page: &PageVectors, underlay: &Underlay, area: &ImportArea) -> Im
.collect(),
),
};
let in_area = |p: [f64; 2]| area_local.as_deref().is_none_or(|a| inside(a, p));
let mut result = ImportResult::default();
let mut geometry_color: Option<Color> = None;
let mut text_color: Option<Color> = None;
// The layer takes the colour of its first object; objects of that
// colour then follow the layer.
fn push(
result: &mut ImportResult,
mut entity: EntityType,
color: Color,
layer: &str,
layer_color: &mut Option<Color>,
) {
let layer_color = *layer_color.get_or_insert(color);
let common = entity.common_mut();
common.layer = layer.to_string();
common.color = if color == layer_color { Color::ByLayer } else { color };
result.entities.push(entity);
}
for path in &page.paths {
if !path_in_area(path, area_local.as_deref()) {
continue;
}
let (color, weight) = match (path.stroke, path.fill) {
(Some((rgb, width)), _) => (color_of(rgb), lineweight_of(width)),
(None, Some(rgb)) => (color_of(rgb), LineWeight::ByLayer),
(None, None) => continue,
let mut layer_colors: Vec<(String, Color)> = Vec::new();
// A layer takes the colour of its first object; objects of that colour
// then follow the layer.
let mut color_on = |layer: &str, color: Color| -> Color {
let layer_color = match layer_colors.iter().find(|(n, _)| n == layer) {
Some((_, c)) => *c,
None => {
layer_colors.push((layer.to_string(), color));
color
}
};
if path.stroke.is_none() {
// A filled area: one solid hatch over its loops.
let mut hatch = Hatch::solid();
for sp in &path.subpaths {
let mut ring: Vec<Vector2> = Vec::new();
for seg in &sp.segments {
let steps = if matches!(seg, Segment::Cubic(..)) { 8 } else { 1 };
for k in 0..steps {
ring.push(world2(bezier(seg, k as f64 / steps as f64)));
}
}
if ring.len() < 3 {
if color == layer_color {
Color::ByLayer
} else {
color
}
};
let fill_transparency = acadrust::types::Transparency::from_alpha_value(0x0200_007F);
let mut strokes: Vec<Stroke> = Vec::new();
let mut curves: Vec<EntityType> = Vec::new();
for (pdf_layer, page) in content {
let pdf_layer = pdf_layer.as_deref();
if settings.vector {
for path in &page.paths {
if !path_in_area(path, area_local.as_deref()) {
continue;
}
let mut boundary = BoundaryPath::external();
boundary.flags.set_polyline(true);
boundary.edges.push(BoundaryEdge::Polyline(PolylineEdge::new(ring, true)));
hatch.add_path(boundary);
}
if hatch.paths.is_empty() {
continue;
}
hatch.elevation = z;
push(&mut result, EntityType::Hatch(hatch), color, GEOMETRY_LAYER, &mut geometry_color);
continue;
}
for sp in &path.subpaths {
let mut entity = if let Some((c, r)) = as_circle(sp) {
let w = world(c);
EntityType::Circle(Circle::from_center_radius(
Vector3::new(w[0], w[1], z),
r * scale,
))
} else if sp.segments.iter().all(|s| matches!(s, Segment::Line(..))) {
let mut points: Vec<Vector2> = vec![world2(sp.segments[0].start())];
points.extend(sp.segments.iter().map(|s| world2(s.end())));
if sp.closed && points.len() > 2 {
points.pop();
}
let mut pl = LwPolyline::from_points(points);
pl.is_closed = sp.closed;
pl.elevation = z;
EntityType::LwPolyline(pl)
} else {
// Mixed or curved: one cubic B-spline through the Bézier
// control points (lines as degree-elevated cubics).
let mut spline = Spline::new();
spline.degree = 3;
let mut controls: Vec<Vector3> = Vec::new();
for (i, seg) in sp.segments.iter().enumerate() {
let (p0, p1, p2, p3) = match seg {
Segment::Cubic(p0, p1, p2, p3) => (*p0, *p1, *p2, *p3),
Segment::Line(a, b) => (
*a,
[a[0] + (b[0] - a[0]) / 3.0, a[1] + (b[1] - a[1]) / 3.0],
[a[0] + (b[0] - a[0]) * 2.0 / 3.0, a[1] + (b[1] - a[1]) * 2.0 / 3.0],
*b,
),
};
let pts = if i == 0 { vec![p0, p1, p2, p3] } else { vec![p1, p2, p3] };
for p in pts {
let w = world(p);
controls.push(Vector3::new(w[0], w[1], z));
match (path.stroke, path.fill) {
(Some((rgb, width)), _) => {
let layer = naming.layer(pdf_layer, Kind::Geometry);
let color = color_on(&layer, color_of(rgb));
let weight = if settings.lineweights {
lineweight_of(width)
} else {
LineWeight::ByLayer
};
for sp in &path.subpaths {
if let Some((c, r)) = as_circle(sp) {
let w = world(c);
let mut circle = EntityType::Circle(Circle::from_center_radius(
Vector3::new(w[0], w[1], z),
r * scale,
));
let common = circle.common_mut();
(common.layer, common.color, common.line_weight) =
(layer.clone(), color, weight);
curves.push(circle);
} else if sp.segments.iter().all(|s| matches!(s, Segment::Line(..))) {
let mut points = vec![sp.segments[0].start()];
points.extend(sp.segments.iter().map(|s| s.end()));
if sp.closed && points.len() > 2 {
points.pop();
}
strokes.push(Stroke {
layer: layer.clone(),
color,
weight,
points,
closed: sp.closed,
});
} else {
let mut spline = EntityType::Spline(spline_of(sp, &world3));
let common = spline.common_mut();
(common.layer, common.color, common.line_weight) =
(layer.clone(), color, weight);
curves.push(spline);
}
}
}
(None, Some(rgb)) if settings.fills => {
let layer = naming.layer(pdf_layer, Kind::Fill);
let color = color_on(&layer, color_of(rgb));
for mut entity in fill_entities(path, settings.hatches, &world2, &world3, z) {
let common = entity.common_mut();
(common.layer, common.color, common.transparency) =
(layer.clone(), color, fill_transparency);
result.entities.push(entity);
}
}
_ => {}
}
let spans = sp.segments.len();
let mut knots = vec![0.0; 4];
for k in 1..spans {
knots.extend([k as f64; 3]);
}
knots.extend([spans as f64; 4]);
spline.control_points = controls;
spline.weights = Vec::new();
spline.knots = knots;
spline.flags.closed = sp.closed;
EntityType::Spline(spline)
};
entity.common_mut().line_weight = weight;
push(&mut result, entity, color, GEOMETRY_LAYER, &mut geometry_color);
}
}
for text in &page.texts {
let cap = if text.cap_height > 0.0 { text.cap_height } else { 0.0 };
let probe = [text.origin[0] + text.width / 2.0, text.origin[1] + cap / 2.0];
if let Some(area) = &area_local {
if !inside(area, probe) && !inside(area, text.origin) {
continue;
}
}
let style = format!("PDF {}", text.font);
if !result.text_styles.iter().any(|(name, _)| *name == style) {
result.text_styles.push((style.clone(), font_file_for(&text.font)));
if settings.text {
for text in &page.texts {
let cap = text.cap_height.max(0.0);
let probe = [text.origin[0] + text.width / 2.0, text.origin[1] + cap / 2.0];
if !in_area(probe) && !in_area(text.origin) {
continue;
}
let style = format!("PDF {}", text.font);
if !result.text_styles.iter().any(|(name, _)| *name == style) {
result.text_styles.push((style.clone(), font_file_for(&text.font)));
}
let (dx, dy) = (-text.rotation.sin(), text.rotation.cos());
let mid = [text.origin[0] + dx * cap / 2.0, text.origin[1] + dy * cap / 2.0];
let mut mtext = MText::new();
mtext.value = text.text.trim_end().to_string();
mtext.insertion_point = world3(mid);
mtext.height = cap * scale;
mtext.rectangle_width = 0.0;
mtext.rotation = text.rotation + underlay.rotation;
mtext.style = style;
mtext.attachment_point = AttachmentPoint::MiddleLeft;
let layer = naming.layer(pdf_layer, Kind::Text);
mtext.common.color = color_on(&layer, color_of(text.color));
mtext.common.layer = layer;
result.entities.push(EntityType::MText(mtext));
}
}
if settings.raster {
let (c, s) = (underlay.rotation.cos(), underlay.rotation.sin());
let turn = |d: [f64; 2]| {
Vector3::new(
(d[0] * c - d[1] * s) * underlay.x_scale,
(d[0] * s + d[1] * c) * underlay.y_scale,
0.0,
)
};
for image in &page.images {
let mid = [
image.origin[0] + (image.u[0] * image.width as f64 + image.v[0] * image.height as f64) / 2.0,
image.origin[1] + (image.u[1] * image.width as f64 + image.v[1] * image.height as f64) / 2.0,
];
if !in_area(mid) {
continue;
}
result.images.push(ImageImport {
rgba: image.rgba.clone(),
width: image.width,
height: image.height,
insertion: world3(image.origin),
u: turn(image.u),
v: turn(image.v),
layer: naming.layer(pdf_layer, Kind::Image),
});
}
}
let (dx, dy) = (-text.rotation.sin(), text.rotation.cos());
let mid = [text.origin[0] + dx * cap / 2.0, text.origin[1] + dy * cap / 2.0];
let w = world(mid);
let mut mtext = MText::new();
mtext.value = text.text.trim_end().to_string();
mtext.insertion_point = Vector3::new(w[0], w[1], z);
mtext.height = cap * scale;
mtext.rectangle_width = 0.0;
mtext.rotation = text.rotation + underlay.rotation;
mtext.style = style;
mtext.attachment_point = AttachmentPoint::MiddleLeft;
push(&mut result, EntityType::MText(mtext), color_of(text.color), TEXT_LAYER, &mut text_color);
}
if let Some(color) = geometry_color {
result.layers.push((GEOMETRY_LAYER.to_string(), color));
// The joined strokes, then (when asked) dashes read as one dashed line.
let strokes = join_strokes(strokes);
let strokes: Vec<(Stroke, Option<f64>)> = if settings.linetypes {
infer_dashes(strokes)
} else {
strokes.into_iter().map(|s| (s, None)).collect()
};
for (stroke, dash) in strokes {
let mut pl = LwPolyline::from_points(stroke.points.iter().map(|p| world2(*p)).collect());
pl.is_closed = stroke.closed;
pl.elevation = z;
(pl.common.layer, pl.common.color, pl.common.line_weight) =
(stroke.layer, stroke.color, stroke.weight);
if let Some(dash) = dash {
pl.common.linetype = DASH_LINETYPE.to_string();
pl.common.linetype_scale = dash * scale;
result.dashed = true;
}
result.entities.push(EntityType::LwPolyline(pl));
}
if let Some(color) = text_color {
result.layers.push((TEXT_LAYER.to_string(), color));
result.entities.extend(curves);
result.layers = layer_colors;
for image in &result.images {
if !result.layers.iter().any(|(n, _)| *n == image.layer) {
result.layers.push((image.layer.clone(), Color::Index(7)));
}
}
result
}
/// One cubic B-spline through a subpath's Bézier control points (lines as
/// degree-elevated cubics).
fn spline_of(sp: &SubPath, world3: &dyn Fn([f64; 2]) -> Vector3) -> Spline {
let mut spline = Spline::new();
spline.degree = 3;
let mut controls: Vec<Vector3> = Vec::new();
for (i, seg) in sp.segments.iter().enumerate() {
let (p0, p1, p2, p3) = match seg {
Segment::Cubic(p0, p1, p2, p3) => (*p0, *p1, *p2, *p3),
Segment::Line(a, b) => (
*a,
[a[0] + (b[0] - a[0]) / 3.0, a[1] + (b[1] - a[1]) / 3.0],
[a[0] + (b[0] - a[0]) * 2.0 / 3.0, a[1] + (b[1] - a[1]) * 2.0 / 3.0],
*b,
),
};
let pts = if i == 0 { vec![p0, p1, p2, p3] } else { vec![p1, p2, p3] };
controls.extend(pts.into_iter().map(world3));
}
let spans = sp.segments.len();
let mut knots = vec![0.0; 4];
for k in 1..spans {
knots.extend([k as f64; 3]);
}
knots.extend([spans as f64; 4]);
spline.control_points = controls;
spline.weights = Vec::new();
spline.knots = knots;
spline.flags.closed = sp.closed;
spline
}
/// A filled area: a triangle or four-cornered straight loop is a SOLID; any
/// other loop, and every loop but a triangle when fills become hatches, one
/// solid HATCH.
fn fill_entities(
path: &PdfPath,
hatches: bool,
world2: &dyn Fn([f64; 2]) -> Vector2,
world3: &dyn Fn([f64; 2]) -> Vector3,
z: f64,
) -> Vec<EntityType> {
let straight = |sp: &SubPath| sp.segments.iter().all(|s| matches!(s, Segment::Line(..)));
let corners = |sp: &SubPath| {
let mut pts: Vec<[f64; 2]> = sp.segments.iter().map(|s| s.start()).collect();
pts.dedup_by(|a, b| same_point(*a, *b));
if pts.len() > 1 && same_point(pts[0], *pts.last().unwrap()) {
pts.pop();
}
pts
};
if path.subpaths.len() == 1 && straight(&path.subpaths[0]) {
let pts = corners(&path.subpaths[0]);
match pts.len() {
3 => {
return vec![EntityType::Solid(Solid::triangle(
world3(pts[0]),
world3(pts[1]),
world3(pts[2]),
))]
}
4 if !hatches => {
// SOLID corners run 1-2-4-3 (the second edge crosses).
return vec![EntityType::Solid(Solid::new(
world3(pts[0]),
world3(pts[1]),
world3(pts[3]),
world3(pts[2]),
))];
}
_ => {}
}
}
let mut hatch = Hatch::solid();
for sp in &path.subpaths {
let mut ring: Vec<Vector2> = Vec::new();
for seg in &sp.segments {
let steps = if matches!(seg, Segment::Cubic(..)) { 8 } else { 1 };
for k in 0..steps {
ring.push(world2(bezier(seg, k as f64 / steps as f64)));
}
}
if ring.len() < 3 {
continue;
}
let mut boundary = BoundaryPath::external();
boundary.flags.set_polyline(true);
boundary.edges.push(BoundaryEdge::Polyline(PolylineEdge::new(ring, true)));
hatch.add_path(boundary);
}
if hatch.paths.is_empty() {
return Vec::new();
}
hatch.elevation = z;
vec![EntityType::Hatch(hatch)]
}
inventory::submit!(crate::command::CommandRegistration {
names: &["PDFIMPORT"]
});