Files
OneCAD-Draft/src/modules/insert/pdf_import.rs
T
ramox81andClaude Opus 5.5 2eb60f333f fix(pdfshxtext): reference grouping, object share, wider glyph set, drawing-order import
Text blocks join side by side or stacked within two heights; oversized
shapes join no group; the group share counts selected objects; Latin-1,
Latin Extended, Greek, Cyrillic and the diameter sign are compared from
the font's own glyphs with look-alikes resolved to the lowest code; a lone
circle reads 0; MTEXT direction is by style. PDFIMPORT keeps drawing order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 11:03:48 +03:00

1731 lines
60 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PDFIMPORT — turn the vector content of a PDF underlay into drawing objects.
//
// Select PDF underlay or [File] <File>:
// Specify first corner of area to import or [Polygonal/All/Settings] <All>:
// Specify opposite corner:
// Keep, Detach or Unload PDF underlay? [Keep/Detach/Unload] <Unload>:
// Binding PDF file <path>, page <n> ...
//
// 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
// Bézier that is a circular arc an arc segment of them (with "Join" on) or
// an ARC (off), 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 codec::entities::{
Arc, AttachmentPoint, BoundaryEdge, BoundaryPath, Circle, Hatch, LwPolyline, MText,
PolylineEdge, Solid, Spline, Underlay, UnderlayType,
};
use codec::types::{Color, Handle, LineWeight, Vector2, Vector3};
use codec::EntityType;
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
use crate::scene::model::pdf_vector::{
bezier, circle_of as as_circle, PageVectors, PdfPath, Segment, SubPath,
};
use crate::scene::model::wire_model::WireModel;
/// 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);
}
}
static IMAGE_PATH: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
/// PDFIMPORTIMAGEPATH: the folder raster images are extracted to — relative
/// to the folder of the PDF, absolute as given, empty for that folder itself.
pub fn image_path() -> String {
IMAGE_PATH
.lock()
.ok()
.and_then(|p| p.clone())
.unwrap_or_else(|| "PDF Images".to_string())
}
pub fn set_image_path(path: String) {
if let Ok(mut p) = IMAGE_PATH.lock() {
*p = Some(path);
}
}
/// The extraction folder for the PDF at `pdf`.
pub fn image_dir(pdf: &str) -> Option<std::path::PathBuf> {
let setting = image_path();
let folder = std::path::Path::new(&setting);
if folder.is_absolute() {
return Some(folder.to_path_buf());
}
let parent = std::path::Path::new(pdf).parent()?;
Some(if setting.is_empty() {
parent.to_path_buf()
} else {
parent.join(folder)
})
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImportArea {
All,
Polygon(Vec<DVec3>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnderlayMode {
Keep,
Detach,
Unload,
}
#[derive(Clone, Debug)]
pub struct PdfImportRequest {
pub underlay: Handle,
pub area: ImportArea,
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 {
/// Where the page's content goes: one drawing unit per page inch times
/// the scale, or, on a page with a viewport measure (a plot), its
/// measured units (a 1:1 millimetre plot comes in at its millimetres).
pub fn placement(&self) -> Underlay {
let mut u = Underlay::pdf();
u.insertion_point = Vector3::new(self.insertion.x, self.insertion.y, self.insertion.z);
let units = crate::scene::model::pdf_raster::page_units_per_inch(&self.path, &self.page)
.unwrap_or(1.0);
u.set_scale(self.scale * units);
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,
Area,
Opposite,
Polygon,
Mode,
// The File option on the command line (-PDFIMPORT, or FILEDIA 0).
FileName,
Page,
PageList,
Insertion,
Scale,
Rotation,
}
pub struct PdfImportCommand {
step: Step,
handle: Handle,
picked: Option<EntityType>,
points: Vec<DVec3>,
area: ImportArea,
/// -PDFIMPORT: the file is always asked on the command line.
dash: bool,
file: PdfFileImport,
pages: usize,
}
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,
handle: Handle::NULL,
picked: None,
points: Vec::new(),
area: ImportArea::All,
dash: false,
file: PdfFileImport {
path: String::new(),
page: "1".to_string(),
scale: 1.0,
rotation: 0.0,
insertion: DVec3::ZERO,
},
pages: 0,
}
}
/// -PDFIMPORT.
pub fn command_line() -> Self {
Self {
dash: true,
..Self::new()
}
}
/// The File option: the file dialog while FILEDIA is on (PDFIMPORT
/// only), otherwise the file name prompt.
fn file_option(&mut self) -> CmdResult {
if !self.dash && crate::sys::file_dia() {
return CmdResult::Dispatch("_PDFIMPORTFILE".to_string());
}
self.step = Step::FileName;
CmdResult::NeedPoint
}
fn file_name(&mut self, text: &str) -> CmdResult {
let typed = text.trim().trim_matches('"');
if typed.is_empty() {
return CmdResult::ReportError("Filename cannot be blank".to_string());
}
// A name without an extension is a .pdf file.
let path = if std::path::Path::new(typed).extension().is_none() {
format!("{typed}.pdf")
} else {
typed.to_string()
};
let shown = crate::entities::underlay::display_path(&path);
let Ok(bytes) = std::fs::read(&path) else {
return CmdResult::ReportError(format!("File {shown} does not exist."));
};
crate::scene::model::pdf_raster::register_source(&path, std::sync::Arc::new(bytes));
let Some(pages) = crate::scene::model::pdf_raster::page_count(&path) else {
// ponytail: wording for an unreadable PDF not measured.
return CmdResult::ReportError(format!("{shown} is not a valid PDF file."));
};
self.pages = pages;
self.file.path = path;
self.step = Step::Page;
CmdResult::NeedPoint
}
fn page(&mut self, text: &str) -> CmdResult {
let token = text.trim();
if token == "?" {
self.step = Step::PageList;
return CmdResult::NeedPoint;
}
let page = if token.is_empty() {
1
} else {
match token.parse::<i64>() {
Ok(n) if n > 0 => n as usize,
// Zero is asked again without a message.
Ok(0) => return CmdResult::NeedPoint,
_ => {
return CmdResult::ReportError("Please enter page number or '?'.".to_string())
}
}
};
// A page past the last one is asked again without a message.
if page > self.pages {
return CmdResult::NeedPoint;
}
self.file.page = page.to_string();
self.step = Step::Insertion;
CmdResult::NeedPoint
}
fn page_list(&mut self, text: &str) -> CmdResult {
let token = text.trim();
self.step = Step::Page;
if token.is_empty() || token == "*" {
let all: Vec<String> = (1..=self.pages).map(|n| n.to_string()).collect();
return CmdResult::ReportMeasurement(all.join("\n"));
}
match token.parse::<usize>() {
Ok(n) if (1..=self.pages).contains(&n) => {
CmdResult::ReportMeasurement(format!("Found page {n}."))
}
_ => CmdResult::ReportMeasurement("No matching page found.".to_string()),
}
}
fn scale(&mut self, value: f64) -> CmdResult {
if value <= 0.0 {
return CmdResult::ReportError("Value must be positive and nonzero.".to_string());
}
self.file.scale = value;
self.step = Step::Rotation;
CmdResult::NeedPoint
}
fn rotation(&mut self, radians: f64) -> CmdResult {
self.file.rotation = radians;
CmdResult::PdfImportFile(self.file.clone())
}
fn area_choice(&mut self, text: &str) -> CmdResult {
match text.trim().to_ascii_uppercase().as_str() {
"" | "A" | "ALL" => {
self.area = ImportArea::All;
self.step = Step::Mode;
CmdResult::NeedPoint
}
"P" | "POLYGONAL" => {
self.points.clear();
self.step = Step::Polygon;
CmdResult::NeedPoint
}
"S" | "SETTINGS" => CmdResult::OpenPdfImportSettings,
_ => CmdResult::ReportError("Invalid option keyword.".to_string()),
}
}
fn mode_choice(&mut self, text: &str) -> CmdResult {
let mode = match text.trim().to_ascii_uppercase().as_str() {
"" | "U" | "UNLOAD" => UnderlayMode::Unload,
"K" | "KEEP" => UnderlayMode::Keep,
"D" | "DETACH" => UnderlayMode::Detach,
_ => return CmdResult::ReportError("Invalid option keyword.".to_string()),
};
CmdResult::PdfImport(PdfImportRequest {
underlay: self.handle,
area: self.area.clone(),
mode,
})
}
}
impl CadCommand for PdfImportCommand {
fn name(&self) -> &'static str {
if self.dash {
"-PDFIMPORT"
} else {
"PDFIMPORT"
}
}
fn prompt(&self) -> String {
match self.step {
Step::Select => "Select PDF underlay or [File] <File>:".to_string(),
Step::Area => {
"Specify first corner of area to import or [Polygonal/All/Settings] <All>:"
.to_string()
}
Step::Opposite => "Specify opposite corner:".to_string(),
Step::Polygon if self.points.is_empty() => "Specify first point:".to_string(),
Step::Polygon => "Specify next point or [Undo]:".to_string(),
Step::Mode => {
"Keep, Detach or Unload PDF underlay? [Keep/Detach/Unload] <Unload>:".to_string()
}
Step::FileName => "Enter filename for PDF import:".to_string(),
Step::Page => "Enter page number or [?] <1>:".to_string(),
Step::PageList => "Enter page to list <*>:".to_string(),
Step::Insertion => "Specify insertion point <0,0>:".to_string(),
Step::Scale => "Specify scale factor <1.0>:".to_string(),
Step::Rotation => "Specify rotation <0>:".to_string(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Select => vec![CmdOption::new("File", "F")],
Step::Area => vec![
CmdOption::new("Polygonal", "P"),
CmdOption::new("All", "A"),
CmdOption::new("Settings", "S"),
],
Step::Polygon if !self.points.is_empty() => vec![CmdOption::new("Undo", "U")],
Step::Mode => vec![
CmdOption::new("Keep", "K"),
CmdOption::new("Detach", "D"),
CmdOption::new("Unload", "U"),
],
_ => Vec::new(),
}
}
fn input_kind(&self) -> InputKind {
match self.step {
Step::Mode | Step::Page | Step::PageList => InputKind::SingleToken,
Step::FileName => InputKind::FreeText,
_ => InputKind::Point,
}
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(
self.step,
Step::Select | Step::Area | Step::Polygon | Step::Scale | Step::Rotation
)
}
fn needs_entity_pick(&self) -> bool {
self.step == Step::Select
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
match self.picked.take() {
Some(EntityType::Underlay(u)) if u.underlay_type == UnderlayType::Pdf => {
self.handle = handle;
self.step = Step::Area;
CmdResult::NeedPoint
}
_ => CmdResult::ReportError("Object selected was not a PDF underlay.".to_string()),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match self.step {
Step::Area => {
self.points = vec![pt];
self.step = Step::Opposite;
CmdResult::NeedPoint
}
Step::Opposite => {
let a = self.points[0];
self.area = ImportArea::Polygon(vec![
a,
DVec3::new(pt.x, a.y, a.z),
pt,
DVec3::new(a.x, pt.y, a.z),
]);
self.step = Step::Mode;
CmdResult::NeedPoint
}
Step::Polygon => {
self.points.push(pt);
CmdResult::NeedPoint
}
Step::Insertion => {
self.file.insertion = pt;
self.step = Step::Scale;
CmdResult::NeedPoint
}
// A second point gives the scale, or the angle, from the
// insertion point.
Step::Scale => {
let d = (pt - self.file.insertion).truncate().length();
self.scale(d)
}
Step::Rotation => {
let d = (pt - self.file.insertion).truncate();
self.rotation(d.y.atan2(d.x))
}
_ => CmdResult::NeedPoint,
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let token = text.trim().to_ascii_uppercase();
Some(match self.step {
Step::Select if matches!(token.as_str(), "F" | "FILE" | "_F" | "_FILE") => {
self.file_option()
}
Step::Area => self.area_choice(&token),
Step::Polygon if token == "U" || token == "UNDO" => {
self.points.pop();
CmdResult::NeedPoint
}
Step::Mode => self.mode_choice(&token),
Step::FileName => self.file_name(text),
Step::Page => self.page(text),
Step::PageList => self.page_list(text),
Step::Scale => match token.parse::<f64>() {
Ok(v) => self.scale(v),
Err(_) if token.is_empty() => self.scale(1.0),
// ponytail: wording for a non-number scale not measured.
Err(_) => CmdResult::ReportError(
"Requires numeric distance or second point.".to_string(),
),
},
Step::Rotation => match token.parse::<f64>() {
Ok(v) => self.rotation(v.to_radians()),
Err(_) if token.is_empty() => self.rotation(0.0),
Err(_) => CmdResult::ReportError(
"Requires valid numeric angle or second point.".to_string(),
),
},
_ => return None,
})
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::Select => self.file_option(),
Step::Area => self.area_choice(""),
Step::Polygon if self.points.len() >= 3 => {
self.area = ImportArea::Polygon(self.points.clone());
self.step = Step::Mode;
CmdResult::NeedPoint
}
Step::Polygon => CmdResult::NeedPoint,
Step::Mode => self.mode_choice(""),
Step::Opposite => CmdResult::NeedPoint,
Step::FileName => self.file_name(""),
Step::Page => self.page(""),
Step::PageList => self.page_list(""),
Step::Insertion => self.on_point(DVec3::ZERO),
Step::Scale => self.scale(1.0),
Step::Rotation => self.rotation(0.0),
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let ring: Vec<DVec3> = match self.step {
Step::Opposite => {
let a = self.points[0];
vec![
a,
DVec3::new(pt.x, a.y, a.z),
pt,
DVec3::new(a.x, pt.y, a.z),
]
}
Step::Polygon if !self.points.is_empty() => {
let mut ring = self.points.clone();
ring.push(pt);
ring
}
_ => return None,
};
let mut points: Vec<[f64; 3]> = ring.iter().map(|p| p.to_array()).collect();
points.push(points[0]);
Some(WireModel::solid_f64(
"pdf_import_area".into(),
points,
WireModel::CYAN,
false,
))
}
}
// ── Conversion ───────────────────────────────────────────────────────────────
/// What an import adds: objects, and the layers and text styles they use
/// (name, colour / font file), created when missing.
#[derive(Default)]
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,
/// otherwise as a true colour.
fn color_of(rgb: [u8; 3]) -> Color {
match rgb {
[255, 0, 0] => Color::Index(1),
[255, 255, 0] => Color::Index(2),
[0, 255, 0] => Color::Index(3),
[0, 255, 255] => Color::Index(4),
[0, 0, 255] => Color::Index(5),
[255, 0, 255] => Color::Index(6),
[0, 0, 0] | [255, 255, 255] => Color::Index(7),
[r, g, b] => Color::Rgb { r, g, b },
}
}
/// Stroke width in points → the nearest standard lineweight.
fn lineweight_of(width_pt: f64) -> LineWeight {
const STANDARD: [i16; 24] = [
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158,
200, 211,
];
if width_pt <= 0.0 {
return LineWeight::ByLayer;
}
let hundredths = width_pt * 25.4 / 72.0 * 100.0;
let best = STANDARD
.iter()
.copied()
.min_by(|a, b| {
(*a as f64 - hundredths)
.abs()
.total_cmp(&(*b as f64 - hundredths).abs())
})
.unwrap_or(0);
LineWeight::Value(best)
}
fn font_file_for(font: &str) -> String {
let f = font.to_ascii_lowercase();
if f.contains("times") {
"times.ttf"
} else if f.contains("courier") {
"cour.ttf"
} else {
"arial.ttf"
}
.to_string()
}
fn inside(poly: &[[f64; 2]], p: [f64; 2]) -> bool {
let mut odd = false;
let mut j = poly.len() - 1;
for i in 0..poly.len() {
let (a, b) = (poly[i], poly[j]);
if (a[1] > p[1]) != (b[1] > p[1])
&& p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]
{
odd = !odd;
}
j = i;
}
odd
}
/// Whether a path reaches into the area: any sampled point inside it.
fn path_in_area(path: &PdfPath, area: Option<&[[f64; 2]]>) -> bool {
let Some(area) = area else { return true };
path.subpaths.iter().any(|sp| {
sp.segments
.iter()
.any(|s| [0.0, 0.5, 1.0].iter().any(|t| inside(area, bezier(s, *t))))
})
}
/// 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: chains can still be joined or read
/// as dashes. `bulges[i]` bends the segment from point i to the next (the
/// closing one on a closed stroke; 0 after an open stroke's last point).
struct Stroke {
/// Drawing position: the objects are created in drawing order.
order: usize,
layer: String,
color: Color,
weight: LineWeight,
points: Vec<[f64; 2]>,
bulges: Vec<f64>,
closed: bool,
}
impl Stroke {
fn straight(&self) -> bool {
self.bulges.iter().all(|b| *b == 0.0)
}
fn reverse(&mut self) {
self.points.reverse();
let n = self.bulges.len();
let mut bulges: Vec<f64> = self.bulges[..n - 1].iter().rev().map(|b| -b).collect();
bulges.push(0.0);
self.bulges = bulges;
}
}
/// The bulge of a Bézier that is a circular arc (its quarter, half and
/// three-quarter points on the circle through its ends and midpoint), from
/// its midpoint's height over the chord; positive counter-clockwise.
fn arc_bulge(seg: &Segment) -> Option<f64> {
let Segment::Cubic(p0, _, _, p3) = seg else {
return None;
};
let m = bezier(seg, 0.5);
let (ax, ay) = (m[0] - p0[0], m[1] - p0[1]);
let (bx, by) = (p3[0] - p0[0], p3[1] - p0[1]);
let d = 2.0 * (ax * by - ay * bx);
let chord = bx.hypot(by);
if d.abs() < 1e-12 || chord < 1e-9 {
return None;
}
// Circle through p0, m, p3 (relative to p0).
let (a2, b2) = (ax * ax + ay * ay, bx * bx + by * by);
let c = [(by * a2 - ay * b2) / d, (ax * b2 - bx * a2) / d];
let r = c[0].hypot(c[1]);
let tol = r * 2e-3;
let on = [0.25, 0.75].iter().all(|t| {
let q = bezier(seg, *t);
((q[0] - p0[0] - c[0]).hypot(q[1] - p0[1] - c[1]) - r).abs() <= tol
});
if !on {
return None;
}
let h = (ax * by - ay * bx) / chord;
Some(2.0 * h / chord)
}
/// A subpath cut where its curves are not circular arcs.
enum Piece {
/// Points, bulges (as in [`Stroke`]) and whether it closes.
Run(Vec<[f64; 2]>, Vec<f64>, bool),
/// A cubic that is no circular arc.
Curve(Segment),
}
/// The straight-and-arc runs of a subpath between its other curves. A
/// subpath without other curves is one run (closed when it closes).
fn run_strokes(segments: &[Segment], closed: bool) -> Vec<Piece> {
let whole = !segments
.iter()
.any(|s| matches!(s, Segment::Cubic(..)) && arc_bulge(s).is_none());
let mut out = Vec::new();
let mut points: Vec<[f64; 2]> = Vec::new();
let mut bulges: Vec<f64> = Vec::new();
let flush = |points: &mut Vec<[f64; 2]>, bulges: &mut Vec<f64>, out: &mut Vec<Piece>| {
if points.len() > 1 {
bulges.push(0.0);
out.push(Piece::Run(
std::mem::take(points),
std::mem::take(bulges),
false,
));
}
points.clear();
bulges.clear();
};
for seg in segments {
let bulge = match seg {
Segment::Line(..) => Some(0.0),
cubic => arc_bulge(cubic),
};
match bulge {
Some(b) => {
if points.is_empty() {
points.push(seg.start());
}
points.push(seg.end());
bulges.push(b);
}
None => {
flush(&mut points, &mut bulges, &mut out);
out.push(Piece::Curve(seg.clone()));
}
}
}
if whole && closed && points.len() > 2 {
// The closing point is the first again.
points.pop();
out.push(Piece::Run(points, bulges, true));
} else {
flush(&mut points, &mut bulges, &mut out);
}
out
}
/// Circle through three points: centre and radius.
fn circle_through(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> Option<([f64; 2], f64)> {
let d = 2.0 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
if d.abs() < 1e-12 {
return None;
}
let (a2, b2, c2) = (
a[0] * a[0] + a[1] * a[1],
b[0] * b[0] + b[1] * b[1],
c[0] * c[0] + c[1] * c[1],
);
let x = (a2 * (b[1] - c[1]) + b2 * (c[1] - a[1]) + c2 * (a[1] - b[1])) / d;
let y = (a2 * (c[0] - b[0]) + b2 * (a[0] - c[0]) + c2 * (b[0] - a[0])) / d;
Some(([x, y], (a[0] - x).hypot(a[1] - y)))
}
/// An ellipse: centre, unit major direction, semi-axes.
struct Conic {
center: [f64; 2],
major: [f64; 2],
a: f64,
b: f64,
}
impl Conic {
/// The ellipse through five points (none at their centroid).
fn through(points: &[[f64; 2]; 5]) -> Option<Self> {
let o = [
points.iter().map(|p| p[0]).sum::<f64>() / 5.0,
points.iter().map(|p| p[1]).sum::<f64>() / 5.0,
];
// A x² + B xy + C y² + D x + E y = 1 about the centroid.
let mut m = [[0.0f64; 6]; 5];
for (row, p) in m.iter_mut().zip(points) {
let (x, y) = (p[0] - o[0], p[1] - o[1]);
*row = [x * x, x * y, y * y, x, y, 1.0];
}
for col in 0..5 {
let pivot = (col..5).max_by(|i, j| m[*i][col].abs().total_cmp(&m[*j][col].abs()))?;
m.swap(col, pivot);
if m[col][col].abs() < 1e-18 {
return None;
}
for row in 0..5 {
if row != col {
let f = m[row][col] / m[col][col];
for k in col..6 {
m[row][k] -= f * m[col][k];
}
}
}
}
let s: Vec<f64> = (0..5).map(|i| m[i][5] / m[i][i]).collect();
let (a, b, c, d, e) = (s[0], s[1], s[2], s[3], s[4]);
let det = 4.0 * a * c - b * b;
if det <= 0.0 {
return None;
}
let x0 = (b * e - 2.0 * c * d) / det;
let y0 = (b * d - 2.0 * a * e) / det;
let f = a * x0 * x0 + b * x0 * y0 + c * y0 * y0 + d * x0 + e * y0 - 1.0;
if f >= 0.0 {
return None;
}
// Eigen-directions of the quadratic part; the smaller value is the
// major axis.
let theta = 0.5 * b.atan2(a - c);
let (ct, st) = (theta.cos(), theta.sin());
let q1 = a * ct * ct + b * ct * st + c * st * st;
let q2 = a * st * st - b * ct * st + c * ct * ct;
let (r1, r2) = ((-f / q1).sqrt(), (-f / q2).sqrt());
let (major, ra, rb) = if r1 >= r2 {
([ct, st], r1, r2)
} else {
([-st, ct], r2, r1)
};
Some(Self {
center: [x0 + o[0], y0 + o[1]],
major,
a: ra,
b: rb,
})
}
/// (u, v) of a point in the ellipse's own frame, scaled to the unit
/// circle.
fn unit(&self, p: [f64; 2]) -> (f64, f64) {
let (dx, dy) = (p[0] - self.center[0], p[1] - self.center[1]);
let u = dx * self.major[0] + dy * self.major[1];
let v = -dx * self.major[1] + dy * self.major[0];
(u / self.a, v / self.b)
}
}
/// Turning of a run of points about a centre: positive counter-clockwise.
fn sweep_sign(points: &[[f64; 2]], c: [f64; 2]) -> f64 {
points
.windows(2)
.map(|w| (w[0][0] - c[0]) * (w[1][1] - c[1]) - (w[0][1] - c[1]) * (w[1][0] - c[0]))
.sum::<f64>()
.signum()
}
/// Minimum segments of a straight polyline read as a curve.
const CURVE_MIN_SEGMENTS: usize = 9;
/// Share of the vertices that must lie within tolerance of the curve.
const CURVE_INLIERS: f64 = 0.85;
/// Radial tolerance of an arc through the first, middle and last vertex.
const ARC_TOLERANCE: f64 = 0.07;
/// Tolerance of an ellipse through five vertices (in its unit circle).
const ELLIPSE_TOLERANCE: f64 = 0.015;
/// An ellipse at least this round is a circle.
const CIRCLE_RATIO: f64 = 0.96;
/// A joined stroke read as a curve, in drawing coordinates (`points`):
/// a run of arcs on one circle is an ARC (a CIRCLE when it closes); a
/// straight polyline of nine or more segments whose vertices lie on a
/// circular arc an ARC, on an ellipse a CIRCLE (round) or an ELLIPSE (an
/// elliptical arc when open).
// ponytail: tolerances fitted to measured samples (glyph outlines, noisy
// and exact polygons); vertices off the curve are allowed for 15%.
fn curve_of(points: &[[f64; 2]], bulges: &[f64], closed: bool, z: f64) -> Option<EntityType> {
use std::f64::consts::TAU;
let n = points.len();
let segs = if closed { n } else { n.saturating_sub(1) };
if segs == 0 {
return None;
}
let arc = |c: [f64; 2], r: f64, first: [f64; 2], last: [f64; 2], ccw: bool| {
let angle = |p: [f64; 2]| (p[1] - c[1]).atan2(p[0] - c[0]).rem_euclid(TAU);
let (start, end) = if ccw {
(angle(first), angle(last))
} else {
(angle(last), angle(first))
};
EntityType::Arc(Arc::from_center_radius_angles(
Vector3::new(c[0], c[1], z),
r,
start,
end,
))
};
// Arcs of one circle, all turning the same way.
if bulges[..segs].iter().all(|b| *b != 0.0) {
let turn = bulges[0].signum();
let arcs: Vec<_> = (0..segs)
.map(|i| {
crate::entities::common::BulgeArc::from_bulge(
points[i],
points[(i + 1) % n],
bulges[i],
)
})
.collect::<Option<_>>()?;
let first = &arcs[0];
let same = arcs.iter().zip(&bulges[..segs]).all(|(a, b)| {
b.signum() == turn
&& (a.radius - first.radius).abs() <= first.radius * 1e-4
&& (a.center[0] - first.center[0]).hypot(a.center[1] - first.center[1])
<= first.radius * 1e-4
});
if !same || segs < 2 {
return None;
}
return Some(if closed {
EntityType::Circle(Circle::from_center_radius(
Vector3::new(first.center[0], first.center[1], z),
first.radius,
))
} else {
arc(first.center, first.radius, points[0], points[n - 1], turn > 0.0)
});
}
if bulges.iter().any(|b| *b != 0.0) || segs < CURVE_MIN_SEGMENTS {
return None;
}
let enough = |inliers: usize| inliers as f64 >= CURVE_INLIERS * n as f64;
if !closed {
if let Some((c, r)) = circle_through(points[0], points[n / 2], points[n - 1]) {
let inliers = points
.iter()
.filter(|p| ((p[0] - c[0]).hypot(p[1] - c[1]) - r).abs() <= ARC_TOLERANCE * r)
.count();
if enough(inliers) {
return Some(arc(c, r, points[0], points[n - 1], sweep_sign(points, c) > 0.0));
}
}
}
// Five vertices spread along the run: a fifth of a closed loop apart,
// or the ends and quarters of an open one.
let step = if closed {
(n as f64 / 5.0).round()
} else {
(n - 1) as f64 / 4.0
};
let idx: Vec<usize> = (0..5)
.map(|k| ((k as f64 * step).round() as usize) % n)
.collect();
let five = [
points[idx[0]],
points[idx[1]],
points[idx[2]],
points[idx[3]],
points[idx[4]],
];
let e = Conic::through(&five)?;
let inliers = points
.iter()
.filter(|p| {
let (u, v) = e.unit(**p);
(u.hypot(v) - 1.0).abs() <= ELLIPSE_TOLERANCE
})
.count();
if !enough(inliers) {
return None;
}
if closed && e.b / e.a >= CIRCLE_RATIO {
let (c, r) = circle_through(five[0], five[2], five[4])?;
return Some(EntityType::Circle(Circle::from_center_radius(
Vector3::new(c[0], c[1], z),
r,
)));
}
let param = |p: [f64; 2]| {
let (u, v) = e.unit(p);
v.atan2(u).rem_euclid(TAU)
};
let (start, mut end) = if closed {
let s = param(points[0]);
(s, s + TAU)
} else if sweep_sign(points, e.center) > 0.0 {
(param(points[0]), param(points[n - 1]))
} else {
(param(points[n - 1]), param(points[0]))
};
if end <= start {
end += TAU;
}
let mut ellipse = codec::entities::Ellipse::from_center_axes(
Vector3::new(e.center[0], e.center[1], z),
Vector3::new(e.major[0] * e.a, e.major[1] * e.a, 0.0),
e.b / e.a,
);
ellipse.start_parameter = start;
ellipse.end_parameter = end;
Some(EntityType::Ellipse(ellipse))
}
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 strokes of one layer, colour and weight that meet end to end become
/// one polyline (closed when the chain returns to its start). Straight ones
/// always do; ones with arcs only with `join` (off, their arcs stay apart).
fn join_strokes(strokes: Vec<Stroke>, join: bool) -> Vec<Stroke> {
let mut out: Vec<Stroke> = Vec::new();
let mut open: Vec<Stroke> = Vec::new();
for s in strokes {
if s.closed || (!join && !s.straight()) {
out.push(s);
} else {
open.push(s);
}
}
// Drawing order is kept: dashes read as a run only in the order drawn.
while !open.is_empty() {
let mut chain = open.remove(0);
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.remove(i);
if !same_point(next.points[0], end) {
next.reverse();
}
chain.points.extend(next.points.into_iter().skip(1));
chain.bulges.pop();
chain.bulges.extend(next.bulges);
} 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.remove(i);
if !same_point(*prev.points.last().unwrap(), start) {
prev.reverse();
}
prev.points.pop();
prev.points.extend(chain.points);
chain.points = prev.points;
prev.bulges.pop();
prev.bulges.extend(chain.bulges);
chain.bulges = prev.bulges;
} else {
break;
}
}
if chain.points.len() > 3 && same_point(chain.points[0], *chain.points.last().unwrap()) {
chain.points.pop();
chain.bulges.pop();
chain.closed = true;
}
out.push(chain);
}
// A chain takes the place of its first stroke.
out.sort_by_key(|s| s.order);
out
}
/// An open stroke cut into its straight runs and one stroke per arc.
fn split_at_arcs(stroke: Stroke) -> Vec<Stroke> {
let mut out = Vec::new();
let piece = |points: Vec<[f64; 2]>, bulges: Vec<f64>| Stroke {
order: stroke.order,
layer: stroke.layer.clone(),
color: stroke.color,
weight: stroke.weight,
points,
bulges,
closed: false,
};
let mut run: Vec<[f64; 2]> = vec![stroke.points[0]];
for i in 0..stroke.points.len() - 1 {
let (a, b, bulge) = (stroke.points[i], stroke.points[i + 1], stroke.bulges[i]);
if bulge == 0.0 {
run.push(b);
continue;
}
if run.len() > 1 {
let n = run.len();
out.push(piece(std::mem::take(&mut run), vec![0.0; n]));
}
out.push(piece(vec![a, b], vec![bulge, 0.0]));
run = vec![b];
}
if run.len() > 1 {
let n = run.len();
out.push(piece(run, vec![0.0; n]));
}
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 && s.straight();
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.
let area_local: Option<Vec<[f64; 2]>> = match area {
ImportArea::All => None,
ImportArea::Polygon(points) => Some(
points
.iter()
.map(|p| {
let (c, s) = (underlay.rotation.cos(), underlay.rotation.sin());
let dx = p.x - underlay.insertion_point.x;
let dy = p.y - underlay.insertion_point.y;
[
(dx * c + dy * s) / underlay.x_scale,
(-dx * s + dy * c) / underlay.y_scale,
]
})
.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 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 color == layer_color {
Color::ByLayer
} else {
color
}
};
let fill_transparency = codec::types::Transparency::from_alpha_value(0x0200_007F);
let mut strokes: Vec<Stroke> = Vec::new();
// Circles and splines, by drawing position like the strokes.
let mut curves: Vec<(usize, 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;
}
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 {
let order = strokes.len() + curves.len();
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((order, circle));
} else {
// Straight and circular runs become polylines;
// each other curve its own one-span SPLINE.
let all_runs = run_strokes(&sp.segments, sp.closed);
for piece in all_runs {
match piece {
Piece::Run(points, bulges, closed) => {
let stroke = Stroke {
order,
layer: layer.clone(),
color,
weight,
points,
bulges,
closed,
};
if settings.join || stroke.straight() || stroke.closed
{
strokes.push(stroke);
} else {
// Join off: each arc apart, straight runs
// between them.
strokes.extend(split_at_arcs(stroke));
}
}
Piece::Curve(seg) => {
let one = SubPath {
segments: vec![seg],
closed: false,
};
let mut spline =
EntityType::Spline(spline_of(&one, &world3));
let common = spline.common_mut();
(common.layer, common.color, common.line_weight) =
(layer.clone(), color, weight);
curves.push((order, 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);
}
}
_ => {}
}
}
}
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),
});
}
}
}
// The joined strokes, then (when asked) dashes read as one dashed line.
let strokes = join_strokes(strokes, settings.join);
// A mirrored placement turns arcs the other way.
let turn = (underlay.x_scale * underlay.y_scale).signum();
// Joined strokes that trace circles, arcs and ellipses become those.
let strokes: Vec<Stroke> = strokes
.into_iter()
.filter_map(|stroke| {
let points: Vec<[f64; 2]> = stroke
.points
.iter()
.map(|p| {
let w = world2(*p);
[w.x, w.y]
})
.collect();
let bulges: Vec<f64> = stroke.bulges.iter().map(|b| b * turn).collect();
match curve_of(&points, &bulges, stroke.closed, z) {
Some(mut curve) => {
let common = curve.common_mut();
(common.layer, common.color, common.line_weight) =
(stroke.layer, stroke.color, stroke.weight);
curves.push((stroke.order, curve));
None
}
None => Some(stroke),
}
})
.collect();
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 {
// A lone arc is an ARC.
if !stroke.closed && stroke.points.len() == 2 && stroke.bulges[0] != 0.0 {
let (a, b) = (world2(stroke.points[0]), world2(stroke.points[1]));
let bulge = stroke.bulges[0] * turn;
if let Some(ba) =
crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge)
{
let (start, end) = if bulge > 0.0 {
(ba.start_angle, ba.end_angle)
} else {
(ba.end_angle, ba.start_angle)
};
let tau = std::f64::consts::TAU;
let mut arc = Arc::from_center_radius_angles(
Vector3::new(ba.center[0], ba.center[1], z),
ba.radius,
start.rem_euclid(tau),
end.rem_euclid(tau),
);
(arc.common.layer, arc.common.color, arc.common.line_weight) =
(stroke.layer, stroke.color, stroke.weight);
curves.push((stroke.order, EntityType::Arc(arc)));
continue;
}
}
let mut pl = LwPolyline::from_points(stroke.points.iter().map(|p| world2(*p)).collect());
for (vertex, bulge) in pl.vertices.iter_mut().zip(&stroke.bulges) {
vertex.bulge = bulge * turn;
}
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;
}
curves.push((stroke.order, EntityType::LwPolyline(pl)));
}
// In drawing order (the sort is stable: a subpath's pieces keep theirs).
curves.sort_by_key(|(order, _)| *order);
result.entities.extend(curves.into_iter().map(|(_, entity)| entity));
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", "-PDFIMPORT"]
});