forked from 1b-agent/opencadstudio-upstream
PDF follow-ups: import layers, arcs and joins, names, background, page placement, snaps
- Unlayered raster images go with the PDF layer drawn last before them. - Circular Bezier arcs import as arcs: bulged polyline segments with Join, ARC entities without it; straight chains still join. - Imported PNGs go to "PDF Images" next to the PDF; import blocks are numbered imp31, imp32, ... - Background darkness is its HSL lightness below one half. - All chosen pages attach at the same insertion point. - PDF geometry snaps as separate pieces: centre from the rim, intersections and perpendiculars. - Automation: snap query with an optional base point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
857f2f2e58
commit
edbbc9d135
10 files changed
+347
-63
No files matched your search
@@ -82,13 +82,8 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
let mut result = pdf_import::convert(&content, &underlay, &area, &naming);
|
let mut result = pdf_import::convert(&content, &underlay, &area, &naming);
|
||||||
let images = std::mem::take(&mut result.images);
|
let images = std::mem::take(&mut result.images);
|
||||||
let image_dir = self.tabs[i]
|
// PDFIMPORTIMAGEPATH "PDF Images": a folder next to the PDF.
|
||||||
.current_path
|
let image_dir = std::path::Path::new(&path).parent().map(|dir| dir.join("PDF Images"));
|
||||||
.as_deref()
|
|
||||||
.and_then(|p| p.parent())
|
|
||||||
.map(|p| p.to_path_buf())
|
|
||||||
.or_else(|| std::path::Path::new(&path).parent().map(|p| p.to_path_buf()))
|
|
||||||
.map(|dir| dir.join("PDF Images"));
|
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "PDFIMPORT");
|
self.push_undo_snapshot(i, "PDFIMPORT");
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
@@ -101,7 +96,7 @@ impl OpenCADStudio {
|
|||||||
let _ = scene.document.line_types.add(lt);
|
let _ = scene.document.line_types.add(lt);
|
||||||
}
|
}
|
||||||
// Raster images: each written as a PNG under "PDF Images" next to the
|
// Raster images: each written as a PNG under "PDF Images" next to the
|
||||||
// drawing (or the PDF while the drawing is unsaved), then referenced.
|
// PDF, named after it with 8 hex digits, then referenced.
|
||||||
let stem = std::path::Path::new(&path.replace('\\', "/"))
|
let stem = std::path::Path::new(&path.replace('\\', "/"))
|
||||||
.file_stem()
|
.file_stem()
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
@@ -128,7 +123,9 @@ impl OpenCADStudio {
|
|||||||
raster.u_vector = image.u;
|
raster.u_vector = image.u;
|
||||||
raster.v_vector = image.v;
|
raster.v_vector = image.v;
|
||||||
raster.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
|
raster.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
|
||||||
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
|
| codec::entities::ImageDisplayFlags::SHOW_NOT_ALIGNED
|
||||||
|
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY
|
||||||
|
| codec::entities::ImageDisplayFlags::TRANSPARENCY_ON;
|
||||||
raster.common.layer = image.layer.clone();
|
raster.common.layer = image.layer.clone();
|
||||||
result.entities.push(codec::EntityType::RasterImage(raster));
|
result.entities.push(codec::EntityType::RasterImage(raster));
|
||||||
}
|
}
|
||||||
@@ -154,8 +151,8 @@ impl OpenCADStudio {
|
|||||||
let mut name = stem.clone();
|
let mut name = stem.clone();
|
||||||
let mut n = 1;
|
let mut n = 1;
|
||||||
while scene.document.block_records.get(&name).is_some() {
|
while scene.document.block_records.get(&name).is_some() {
|
||||||
|
name = format!("{stem}{n}");
|
||||||
n += 1;
|
n += 1;
|
||||||
name = format!("{stem}({n})");
|
|
||||||
}
|
}
|
||||||
if scene
|
if scene
|
||||||
.define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO)
|
.define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO)
|
||||||
|
|||||||
@@ -459,6 +459,7 @@ impl OpenCADStudio {
|
|||||||
| "commands"
|
| "commands"
|
||||||
| "properties"
|
| "properties"
|
||||||
| "measure"
|
| "measure"
|
||||||
|
| "snap"
|
||||||
| "query"
|
| "query"
|
||||||
| "records"
|
| "records"
|
||||||
| "record_schema"
|
| "record_schema"
|
||||||
@@ -705,6 +706,28 @@ impl OpenCADStudio {
|
|||||||
let response = match op {
|
let response = match op {
|
||||||
"properties" => self.control_properties(),
|
"properties" => self.control_properties(),
|
||||||
"measure" => self.control_measure(&req),
|
"measure" => self.control_measure(&req),
|
||||||
|
// The object snap a cursor over `point` (world) would get;
|
||||||
|
// `from` is the base for perpendicular and tangent.
|
||||||
|
"snap" => {
|
||||||
|
let p = &req["point"];
|
||||||
|
let world = glam::DVec3::new(
|
||||||
|
p[0].as_f64().unwrap_or(0.0),
|
||||||
|
p[1].as_f64().unwrap_or(0.0),
|
||||||
|
p[2].as_f64().unwrap_or(0.0),
|
||||||
|
);
|
||||||
|
let from = req["from"].as_array().map(|f| {
|
||||||
|
glam::DVec3::new(
|
||||||
|
f.first().and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
f.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
f.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let i = self.active_tab;
|
||||||
|
match self.snap_query(i, world, from) {
|
||||||
|
Some(hit) => json!({"ok":true,"snap":format!("{:?}", hit.snap_type),"world":[hit.world.x,hit.world.y,hit.world.z]}),
|
||||||
|
None => json!({"ok":true,"snap":null}),
|
||||||
|
}
|
||||||
|
}
|
||||||
"xdata_get" => self.xdata_read(&req),
|
"xdata_get" => self.xdata_read(&req),
|
||||||
"get_selection" => self.control_get_selection(),
|
"get_selection" => self.control_get_selection(),
|
||||||
"history" => {
|
"history" => {
|
||||||
|
|||||||
@@ -2872,6 +2872,49 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The object snap the cursor would get over `world` in the model view,
|
||||||
|
/// through the same candidate gathering and snapper as a cursor move
|
||||||
|
/// (automation's `snap` query).
|
||||||
|
pub(in crate::app) fn snap_query(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
world: glam::DVec3,
|
||||||
|
from: Option<glam::DVec3>,
|
||||||
|
) -> Option<crate::snap::SnapResult> {
|
||||||
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
|
if vw <= 1.0 || vh <= 1.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let tile_b = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||||
|
let bounds = iced::Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: tile_b.width,
|
||||||
|
height: tile_b.height,
|
||||||
|
};
|
||||||
|
let (view_rot, eye, px) = {
|
||||||
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
|
(cam.view_proj_rte(bounds), cam.eye(), cam.project(world, bounds)?)
|
||||||
|
};
|
||||||
|
let p = Point::new(px.x, px.y);
|
||||||
|
let raw = self.cursor_model_point(i, &None, p, bounds);
|
||||||
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||||
|
let candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||||
|
all_wires,
|
||||||
|
raw,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
self.snapper.osnap_radius_px,
|
||||||
|
);
|
||||||
|
// Perpendicular and tangent measure from `from`, else the running
|
||||||
|
// command's last point.
|
||||||
|
self.snapper.from_point = from.or(self.last_point).map(|p| p.as_vec3());
|
||||||
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
|
self.snapper
|
||||||
|
.snap(raw, p, &candidates, view_rot, eye, bounds, go, gr, None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply one frame of a UCS-icon grip drag: map the cursor onto the active
|
/// Apply one frame of a UCS-icon grip drag: map the cursor onto the active
|
||||||
/// UCS plane (so the move stays in-plane) and either slide the origin there
|
/// UCS plane (so the move stays in-plane) and either slide the origin there
|
||||||
/// or rotate the chosen axis to point at it, keeping a right-handed frame
|
/// or rotate the chosen axis to point at it, keeping a right-handed frame
|
||||||
|
|||||||
+2
-1
@@ -35,6 +35,7 @@ const READ_OPS: &[&str] = &[
|
|||||||
"header",
|
"header",
|
||||||
"properties",
|
"properties",
|
||||||
"measure",
|
"measure",
|
||||||
|
"snap",
|
||||||
"history",
|
"history",
|
||||||
"commands",
|
"commands",
|
||||||
"events",
|
"events",
|
||||||
@@ -1198,7 +1199,7 @@ fn tool_definitions() -> Value {
|
|||||||
{
|
{
|
||||||
"name":"ocs_read",
|
"name":"ocs_read",
|
||||||
"description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
"description":"Discover capabilities and record schemas, or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command or record-type search."},"document_id":{"type":"integer","minimum":0},"path":{"type":"string","description":"Optional intended output path for audit; extension determines target format."},"target_format":{"type":"string","enum":["dwg","dxf"],"description":"Intended output format for audit."},"target_version":{"type":"string","enum":["R14","2000","2004","2007","2010","2013","2018","AC1014","AC1015","AC1018","AC1021","AC1024","AC1027","AC1032"],"description":"Intended CAD output version for audit."},"collection":{"type":"string","description":"Record collection, all for records, or omit to discover collections and schema types."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter; for record_schema, returns its complete type graph and writable field paths."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"World point whose object snap the snap op reports."},"from":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Base point for perpendicular and tangent snaps (snap op)."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||||
"outputSchema":read_output_schema(),
|
"outputSchema":read_output_schema(),
|
||||||
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ pub struct PdfAttachCommand {
|
|||||||
path: String,
|
path: String,
|
||||||
/// The path the definition stores (full, relative or file name only).
|
/// The path the definition stores (full, relative or file name only).
|
||||||
stored_path: Option<String>,
|
stored_path: Option<String>,
|
||||||
/// Pages after the first, attached beside it (dialog selection).
|
/// Pages after the first, attached at the same point (dialog selection).
|
||||||
extra_pages: Vec<String>,
|
extra_pages: Vec<String>,
|
||||||
/// Scale and rotation fixed in the dialog: their prompts are skipped.
|
/// Scale and rotation fixed in the dialog: their prompts are skipped.
|
||||||
preset_scale: Option<f64>,
|
preset_scale: Option<f64>,
|
||||||
@@ -301,7 +301,6 @@ impl PdfAttachCommand {
|
|||||||
.chain(self.extra_pages.iter().cloned())
|
.chain(self.extra_pages.iter().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
let placed = underlays_for_pages(
|
let placed = underlays_for_pages(
|
||||||
&self.path,
|
|
||||||
&pages,
|
&pages,
|
||||||
self.plane.to_local(self.insertion),
|
self.plane.to_local(self.insertion),
|
||||||
self.scale,
|
self.scale,
|
||||||
@@ -427,24 +426,19 @@ impl CadCommand for PdfAttachCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Underlays for `pages` of a file: the first at `insertion`, each next one
|
/// Underlays for `pages` of a file, all at `insertion` with the same scale
|
||||||
/// beside the previous along the rotated X axis (page width times scale).
|
/// and rotation (the reference stacks the chosen pages there).
|
||||||
// ponytail: side-by-side placement of the pages after the first is not
|
|
||||||
// measured against the reference.
|
|
||||||
pub fn underlays_for_pages(
|
pub fn underlays_for_pages(
|
||||||
read_path: &str,
|
|
||||||
pages: &[String],
|
pages: &[String],
|
||||||
insertion: DVec3,
|
insertion: DVec3,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
rotation: f64,
|
rotation: f64,
|
||||||
) -> Vec<(String, EntityType)> {
|
) -> Vec<(String, EntityType)> {
|
||||||
let mut origin = insertion;
|
|
||||||
let (c, s) = (rotation.cos(), rotation.sin());
|
|
||||||
pages
|
pages
|
||||||
.iter()
|
.iter()
|
||||||
.map(|page| {
|
.map(|page| {
|
||||||
let mut underlay = Underlay::pdf();
|
let mut underlay = Underlay::pdf();
|
||||||
underlay.insertion_point = Vector3::new(origin.x, origin.y, origin.z);
|
underlay.insertion_point = Vector3::new(insertion.x, insertion.y, insertion.z);
|
||||||
underlay.set_scale(scale);
|
underlay.set_scale(scale);
|
||||||
underlay.rotation = rotation;
|
underlay.rotation = rotation;
|
||||||
// On, clipped by its boundary and colour-adjusted for the
|
// On, clipped by its boundary and colour-adjusted for the
|
||||||
@@ -452,9 +446,6 @@ pub fn underlays_for_pages(
|
|||||||
underlay.flags = UnderlayDisplayFlags::ON
|
underlay.flags = UnderlayDisplayFlags::ON
|
||||||
| UnderlayDisplayFlags::CLIPPING
|
| UnderlayDisplayFlags::CLIPPING
|
||||||
| UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND;
|
| 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))
|
(page.clone(), EntityType::Underlay(underlay))
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
// Content of a PDF layer goes to PDF_<layer>; the rest to PDF_Geometry,
|
// 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
|
// 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
|
// 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
|
// Bézier that is a circular arc an arc segment of them (with "Join" on) or
|
||||||
// splines, filled triangles and quadrilaterals SOLIDs at 50% transparency
|
// 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
|
// (other fills solid hatches), text runs MTEXT in a "PDF <font>" style and
|
||||||
// raster images PNG files referenced by IMAGE objects.
|
// raster images PNG files referenced by IMAGE objects.
|
||||||
|
|
||||||
use codec::entities::{
|
use codec::entities::{
|
||||||
AttachmentPoint, BoundaryEdge, BoundaryPath, Circle, Hatch, LwPolyline, MText, PolylineEdge,
|
Arc, AttachmentPoint, BoundaryEdge, BoundaryPath, Circle, Hatch, LwPolyline, MText, PolylineEdge,
|
||||||
Solid, Spline, Underlay, UnderlayType,
|
Solid, Spline, Underlay, UnderlayType,
|
||||||
};
|
};
|
||||||
use codec::types::{Color, Handle, LineWeight, Vector2, Vector3};
|
use codec::types::{Color, Handle, LineWeight, Vector2, Vector3};
|
||||||
@@ -557,27 +558,75 @@ pub struct ImageImport {
|
|||||||
/// space 0.2, scaled per object to the dash length.
|
/// space 0.2, scaled per object to the dash length.
|
||||||
pub const DASH_LINETYPE: &str = "PDF_IMPORT";
|
pub const DASH_LINETYPE: &str = "PDF_IMPORT";
|
||||||
|
|
||||||
/// A stroke before it becomes an object: straight chains can still be joined
|
/// A stroke before it becomes an object: chains can still be joined or read
|
||||||
/// or read as dashes.
|
/// 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 {
|
struct Stroke {
|
||||||
layer: String,
|
layer: String,
|
||||||
color: Color,
|
color: Color,
|
||||||
weight: LineWeight,
|
weight: LineWeight,
|
||||||
points: Vec<[f64; 2]>,
|
points: Vec<[f64; 2]>,
|
||||||
|
bulges: Vec<f64>,
|
||||||
closed: bool,
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
fn same_point(a: [f64; 2], b: [f64; 2]) -> 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
|
(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
|
/// Open strokes of one layer, colour and weight that meet end to end become
|
||||||
/// end become one polyline (closed when the chain returns to its start).
|
/// one polyline (closed when the chain returns to its start). Straight ones
|
||||||
fn join_strokes(strokes: Vec<Stroke>) -> Vec<Stroke> {
|
/// 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 out: Vec<Stroke> = Vec::new();
|
||||||
let mut open: Vec<Stroke> = Vec::new();
|
let mut open: Vec<Stroke> = Vec::new();
|
||||||
for s in strokes {
|
for s in strokes {
|
||||||
if s.closed {
|
if s.closed || (!join && !s.straight()) {
|
||||||
out.push(s);
|
out.push(s);
|
||||||
} else {
|
} else {
|
||||||
open.push(s);
|
open.push(s);
|
||||||
@@ -597,26 +646,32 @@ fn join_strokes(strokes: Vec<Stroke>) -> Vec<Stroke> {
|
|||||||
}) {
|
}) {
|
||||||
let mut next = open.remove(i);
|
let mut next = open.remove(i);
|
||||||
if !same_point(next.points[0], end) {
|
if !same_point(next.points[0], end) {
|
||||||
next.points.reverse();
|
next.reverse();
|
||||||
}
|
}
|
||||||
chain.points.extend(next.points.into_iter().skip(1));
|
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| {
|
} else if let Some(i) = open.iter().position(|o| {
|
||||||
fits(o)
|
fits(o)
|
||||||
&& (same_point(*o.points.last().unwrap(), start) || same_point(o.points[0], start))
|
&& (same_point(*o.points.last().unwrap(), start) || same_point(o.points[0], start))
|
||||||
}) {
|
}) {
|
||||||
let mut prev = open.remove(i);
|
let mut prev = open.remove(i);
|
||||||
if !same_point(*prev.points.last().unwrap(), start) {
|
if !same_point(*prev.points.last().unwrap(), start) {
|
||||||
prev.points.reverse();
|
prev.reverse();
|
||||||
}
|
}
|
||||||
prev.points.pop();
|
prev.points.pop();
|
||||||
prev.points.extend(chain.points);
|
prev.points.extend(chain.points);
|
||||||
chain.points = prev.points;
|
chain.points = prev.points;
|
||||||
|
prev.bulges.pop();
|
||||||
|
prev.bulges.extend(chain.bulges);
|
||||||
|
chain.bulges = prev.bulges;
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if chain.points.len() > 3 && same_point(chain.points[0], *chain.points.last().unwrap()) {
|
if chain.points.len() > 3 && same_point(chain.points[0], *chain.points.last().unwrap()) {
|
||||||
chain.points.pop();
|
chain.points.pop();
|
||||||
|
chain.bulges.pop();
|
||||||
chain.closed = true;
|
chain.closed = true;
|
||||||
}
|
}
|
||||||
out.push(chain);
|
out.push(chain);
|
||||||
@@ -624,13 +679,45 @@ fn join_strokes(strokes: Vec<Stroke>) -> Vec<Stroke> {
|
|||||||
out
|
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 {
|
||||||
|
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
|
/// 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
|
/// line from the first dash's start to the last's end, with the dash length
|
||||||
/// (the linetype scale).
|
/// (the linetype scale).
|
||||||
// ponytail: dashes are matched along one direction at a time in drawing
|
// ponytail: dashes are matched along one direction at a time in drawing
|
||||||
// order; patterns of mixed dash lengths stay separate lines.
|
// order; patterns of mixed dash lengths stay separate lines.
|
||||||
fn infer_dashes(strokes: Vec<Stroke>) -> Vec<(Stroke, Option<f64>)> {
|
fn infer_dashes(strokes: Vec<Stroke>) -> Vec<(Stroke, Option<f64>)> {
|
||||||
let is_dash = |s: &Stroke| !s.closed && s.points.len() == 2;
|
let is_dash = |s: &Stroke| !s.closed && s.points.len() == 2 && s.straight();
|
||||||
let len = |s: &Stroke| {
|
let len = |s: &Stroke| {
|
||||||
let (a, b) = (s.points[0], s.points[1]);
|
let (a, b) = (s.points[0], s.points[1]);
|
||||||
(b[0] - a[0]).hypot(b[1] - a[1])
|
(b[0] - a[0]).hypot(b[1] - a[1])
|
||||||
@@ -777,19 +864,36 @@ pub fn convert(
|
|||||||
(common.layer, common.color, common.line_weight) =
|
(common.layer, common.color, common.line_weight) =
|
||||||
(layer.clone(), color, weight);
|
(layer.clone(), color, weight);
|
||||||
curves.push(circle);
|
curves.push(circle);
|
||||||
} else if sp.segments.iter().all(|s| matches!(s, Segment::Line(..))) {
|
} else if let Some(mut bulges) = sp
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
|
Segment::Line(..) => Some(0.0),
|
||||||
|
cubic => arc_bulge(cubic),
|
||||||
|
})
|
||||||
|
.collect::<Option<Vec<f64>>>()
|
||||||
|
{
|
||||||
let mut points = vec![sp.segments[0].start()];
|
let mut points = vec![sp.segments[0].start()];
|
||||||
points.extend(sp.segments.iter().map(|s| s.end()));
|
points.extend(sp.segments.iter().map(|s| s.end()));
|
||||||
if sp.closed && points.len() > 2 {
|
if sp.closed && points.len() > 2 {
|
||||||
points.pop();
|
points.pop();
|
||||||
|
} else {
|
||||||
|
bulges.push(0.0);
|
||||||
}
|
}
|
||||||
strokes.push(Stroke {
|
let stroke = Stroke {
|
||||||
layer: layer.clone(),
|
layer: layer.clone(),
|
||||||
color,
|
color,
|
||||||
weight,
|
weight,
|
||||||
points,
|
points,
|
||||||
|
bulges,
|
||||||
closed: sp.closed,
|
closed: sp.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));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut spline = EntityType::Spline(spline_of(sp, &world3));
|
let mut spline = EntityType::Spline(spline_of(sp, &world3));
|
||||||
let common = spline.common_mut();
|
let common = spline.common_mut();
|
||||||
@@ -873,14 +977,42 @@ pub fn convert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The joined strokes, then (when asked) dashes read as one dashed line.
|
// The joined strokes, then (when asked) dashes read as one dashed line.
|
||||||
let strokes = join_strokes(strokes);
|
let strokes = join_strokes(strokes, settings.join);
|
||||||
let strokes: Vec<(Stroke, Option<f64>)> = if settings.linetypes {
|
let strokes: Vec<(Stroke, Option<f64>)> = if settings.linetypes {
|
||||||
infer_dashes(strokes)
|
infer_dashes(strokes)
|
||||||
} else {
|
} else {
|
||||||
strokes.into_iter().map(|s| (s, None)).collect()
|
strokes.into_iter().map(|s| (s, None)).collect()
|
||||||
};
|
};
|
||||||
|
// A mirrored placement turns arcs the other way.
|
||||||
|
let turn = (underlay.x_scale * underlay.y_scale).signum();
|
||||||
for (stroke, dash) in strokes {
|
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);
|
||||||
|
result.entities.push(EntityType::Arc(arc));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut pl = LwPolyline::from_points(stroke.points.iter().map(|p| world2(*p)).collect());
|
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.is_closed = stroke.closed;
|
||||||
pl.elevation = z;
|
pl.elevation = z;
|
||||||
(pl.common.layer, pl.common.color, pl.common.line_weight) =
|
(pl.common.layer, pl.common.color, pl.common.line_weight) =
|
||||||
|
|||||||
@@ -1286,16 +1286,28 @@ fn tessellate_entity_inner(
|
|||||||
b.set_fixed_screen_width(2.0);
|
b.set_fixed_screen_width(2.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// PDF underlay geometry: a hidden, unplotted wire that object snaps
|
// PDF underlay geometry: hidden, unplotted wires that object snaps
|
||||||
// (nearest, intersection, perpendicular) find, beside the page frame.
|
// (nearest, intersection, perpendicular, centre) find, beside the page
|
||||||
|
// frame — one per segment or circle, so they intersect each other.
|
||||||
if let EntityType::Underlay(underlay) = e {
|
if let EntityType::Underlay(underlay) = e {
|
||||||
let geometry = crate::scene::model::pdf_vector::underlay_snap_geometry(underlay, document);
|
for piece in crate::scene::model::pdf_vector::underlay_snap_geometry(underlay, document) {
|
||||||
if !geometry.is_empty() {
|
let (points, points_low) = convert::tessellate::points_to_ds(piece.points);
|
||||||
let (points, points_low) = convert::tessellate::points_to_ds(geometry);
|
|
||||||
let mut wire = WireModel::solid(h.value().to_string(), points, entity_color, sel);
|
let mut wire = WireModel::solid(h.value().to_string(), points, entity_color, sel);
|
||||||
wire.points_low = points_low;
|
wire.points_low = points_low;
|
||||||
wire.display_visible = false;
|
wire.display_visible = false;
|
||||||
wire.plot_visible = false;
|
wire.plot_visible = false;
|
||||||
|
if let Some((center, radius)) = piece.circle {
|
||||||
|
wire.tangent_geoms = vec![crate::scene::model::wire_model::TangentGeom::PlanarCircle {
|
||||||
|
center,
|
||||||
|
axis_x: [1.0, 0.0, 0.0],
|
||||||
|
axis_y: [0.0, 1.0, 0.0],
|
||||||
|
radius,
|
||||||
|
}];
|
||||||
|
wire.snap_pts = vec![(
|
||||||
|
glam::DVec3::from(center),
|
||||||
|
crate::scene::model::wire_model::SnapHint::Center,
|
||||||
|
)];
|
||||||
|
}
|
||||||
set_wire_aabb(&mut wire, aabb);
|
set_wire_aabb(&mut wire, aabb);
|
||||||
bases.push(wire);
|
bases.push(wire);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,7 +217,11 @@ impl ImageModel {
|
|||||||
// Hidden PDF layers come from the underlay's layer overrides.
|
// Hidden PDF layers come from the underlay's layer overrides.
|
||||||
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
let source = super::pdf_layers::underlay_source(u, &def.file_path);
|
||||||
let raster = pdf_raster::rasterize_page_display(&source, page)?;
|
let raster = pdf_raster::rasterize_page_display(&source, page)?;
|
||||||
let bg_lum = 0.299 * background[0] + 0.587 * background[1] + 0.114 * background[2];
|
// Dark means an HSL lightness under one half: pure blue counts as
|
||||||
|
// light, (0, 128, 0) as dark.
|
||||||
|
let bg_max = background[0].max(background[1]).max(background[2]);
|
||||||
|
let bg_min = background[0].min(background[1]).min(background[2]);
|
||||||
|
let bg_lum = (bg_max + bg_min) / 2.0;
|
||||||
let pixels = pdf_raster::adjusted_pixels(
|
let pixels = pdf_raster::adjusted_pixels(
|
||||||
&source,
|
&source,
|
||||||
page,
|
page,
|
||||||
@@ -226,7 +230,7 @@ impl ImageModel {
|
|||||||
contrast: u.contrast.min(100),
|
contrast: u.contrast.min(100),
|
||||||
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
monochrome: u.flags.contains(UnderlayDisplayFlags::MONOCHROME),
|
||||||
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
adjust_for_background: u.flags.contains(UnderlayDisplayFlags::ADJUST_FOR_BACKGROUND),
|
||||||
dark_background: bg_lum <= 0.5,
|
dark_background: bg_lum < 0.5,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -252,8 +252,6 @@ pub fn adjusted_pixels(path: &str, page: &str, raster: &PdfPage, adjust: PageAdj
|
|||||||
// (plus 20/255, capped at full), hue and saturation kept: black
|
// (plus 20/255, capped at full), hue and saturation kept: black
|
||||||
// becomes white, dark red light pink. On a light background the
|
// becomes white, dark red light pink. On a light background the
|
||||||
// colours stay as drawn.
|
// colours stay as drawn.
|
||||||
// ponytail: offset and the light-background rule measured on the
|
|
||||||
// default backgrounds only (pure white on paper not measured).
|
|
||||||
let (h, s, l) = rgb_to_hsl(rgb);
|
let (h, s, l) = rgb_to_hsl(rgb);
|
||||||
rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0));
|
rgb = hsl_to_rgb(h, s, (1.0 - l + 20.0 / 255.0).min(1.0));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,16 @@ pub struct PageVectors {
|
|||||||
pub texts: Vec<PdfText>,
|
pub texts: Vec<PdfText>,
|
||||||
/// Filled only when images are asked for (`page_content`).
|
/// Filled only when images are asked for (`page_content`).
|
||||||
pub images: Vec<PdfImage>,
|
pub images: Vec<PdfImage>,
|
||||||
|
/// Paths, texts and images in the order the page draws them.
|
||||||
|
pub order: Vec<Drawn>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One drawn item: an index into `paths`, `texts` or `images`.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Drawn {
|
||||||
|
Path(usize),
|
||||||
|
Text(usize),
|
||||||
|
Image(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Key = (String, String);
|
type Key = (String, String);
|
||||||
@@ -168,10 +178,54 @@ pub fn page_content_by_layer(
|
|||||||
only.images.retain(|i| !base.images.contains(i));
|
only.images.retain(|i| !base.images.contains(i));
|
||||||
out.push((Some(layer.name.clone()), only));
|
out.push((Some(layer.name.clone()), only));
|
||||||
}
|
}
|
||||||
|
let mut base = base;
|
||||||
|
images_to_last_layer(path, page, &mut base, &mut out);
|
||||||
out.insert(0, (None, base));
|
out.insert(0, (None, base));
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An image that no layer holds goes with the layer drawn last before it
|
||||||
|
/// (with none before it, it stays unlayered): the page is read with every
|
||||||
|
/// layer on for the drawing order.
|
||||||
|
fn images_to_last_layer(
|
||||||
|
path: &str,
|
||||||
|
page: &str,
|
||||||
|
base: &mut PageVectors,
|
||||||
|
layered: &mut [(Option<String>, PageVectors)],
|
||||||
|
) {
|
||||||
|
if base.images.is_empty() || layered.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(full) = page_content(&super::pdf_layers::source_with_layers(path, &[]), page) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let key = |s: &dyn std::fmt::Debug| format!("{s:?}");
|
||||||
|
let owner = |item: String| {
|
||||||
|
layered.iter().position(|(_, pv)| {
|
||||||
|
pv.paths.iter().any(|p| key(p) == item) || pv.texts.iter().any(|t| key(t) == item)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut last: Option<usize> = None;
|
||||||
|
let mut moves: Vec<(usize, PdfImage)> = Vec::new();
|
||||||
|
for drawn in &full.order {
|
||||||
|
match *drawn {
|
||||||
|
Drawn::Path(i) => last = owner(key(&full.paths[i])).or(last),
|
||||||
|
Drawn::Text(i) => last = owner(key(&full.texts[i])).or(last),
|
||||||
|
Drawn::Image(i) => {
|
||||||
|
let image = &full.images[i];
|
||||||
|
if let Some(n) = layered.iter().position(|(_, pv)| pv.images.contains(image)) {
|
||||||
|
last = Some(n);
|
||||||
|
} else if let (Some(n), Some(at)) = (last, base.images.iter().position(|b| b == image)) {
|
||||||
|
moves.push((n, base.images.remove(at)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (n, image) in moves {
|
||||||
|
layered[n].1.images.push(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
fn read_page(path: &str, page: &str) -> Option<PageVectors> {
|
||||||
read_page_with(path, page, false)
|
read_page_with(path, page, false)
|
||||||
}
|
}
|
||||||
@@ -229,6 +283,7 @@ fn read_page_with(path: &str, page: &str, images: bool) -> Option<PageVectors> {
|
|||||||
paths: device.paths,
|
paths: device.paths,
|
||||||
texts: texts.into_iter().map(|t| t.text).collect(),
|
texts: texts.into_iter().map(|t| t.text).collect(),
|
||||||
images: device.images,
|
images: device.images,
|
||||||
|
order: device.order,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,12 +305,14 @@ struct Collector {
|
|||||||
font_order: Vec<u128>,
|
font_order: Vec<u128>,
|
||||||
want_images: bool,
|
want_images: bool,
|
||||||
images: Vec<PdfImage>,
|
images: Vec<PdfImage>,
|
||||||
|
order: Vec<Drawn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Collector {
|
impl Collector {
|
||||||
fn finish_text(&mut self) {
|
fn finish_text(&mut self) {
|
||||||
if let Some(text) = self.current.take() {
|
if let Some(text) = self.current.take() {
|
||||||
if !text.text.text.trim().is_empty() {
|
if !text.text.text.trim().is_empty() {
|
||||||
|
self.order.push(Drawn::Text(self.texts.len()));
|
||||||
self.texts.push(text);
|
self.texts.push(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,6 +408,7 @@ impl<'a> Device<'a> for Collector {
|
|||||||
}
|
}
|
||||||
PathDrawMode::Fill(_) => (None, Some(rgb(paint))),
|
PathDrawMode::Fill(_) => (None, Some(rgb(paint))),
|
||||||
};
|
};
|
||||||
|
self.order.push(Drawn::Path(self.paths.len()));
|
||||||
self.paths.push(PdfPath { subpaths, stroke, fill });
|
self.paths.push(PdfPath { subpaths, stroke, fill });
|
||||||
}
|
}
|
||||||
fn push_clip_path(&mut self, _: &ClipPath) {}
|
fn push_clip_path(&mut self, _: &ClipPath) {}
|
||||||
@@ -482,7 +540,10 @@ impl<'a> Device<'a> for Collector {
|
|||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
self.images.extend(decoded);
|
if let Some(image) = decoded {
|
||||||
|
self.order.push(Drawn::Image(self.images.len()));
|
||||||
|
self.images.push(image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn pop_clip_path(&mut self) {}
|
fn pop_clip_path(&mut self) {}
|
||||||
fn pop_transparency_group(&mut self) {}
|
fn pop_transparency_group(&mut self) {}
|
||||||
@@ -605,13 +666,21 @@ pub fn underlay_snap_points(
|
|||||||
// ponytail: flat cap; a spatial index over page segments for huge drawings.
|
// ponytail: flat cap; a spatial index over page segments for huge drawings.
|
||||||
const MAX_GEOMETRY_POINTS: usize = 200_000;
|
const MAX_GEOMETRY_POINTS: usize = 200_000;
|
||||||
|
|
||||||
/// The underlay's PDF geometry as world polylines (NaN-separated, curves
|
/// One piece of an underlay's PDF geometry for snapping: a world polyline
|
||||||
/// flattened) for nearest, intersection and perpendicular snaps. Empty when
|
/// (curves flattened), and for a circle its centre and radius.
|
||||||
|
pub struct SnapPiece {
|
||||||
|
pub points: Vec<[f64; 3]>,
|
||||||
|
pub circle: Option<([f64; 3], f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The underlay's PDF geometry for nearest, intersection, perpendicular and
|
||||||
|
/// centre snaps: each straight segment, each curve and each circle its own
|
||||||
|
/// piece, so pieces cross each other as separate objects do. Empty when
|
||||||
/// PDFOSNAP is off or the page is not shown.
|
/// PDFOSNAP is off or the page is not shown.
|
||||||
pub fn underlay_snap_geometry(
|
pub fn underlay_snap_geometry(
|
||||||
u: &codec::entities::Underlay,
|
u: &codec::entities::Underlay,
|
||||||
document: &codec::CadDocument,
|
document: &codec::CadDocument,
|
||||||
) -> Vec<[f64; 3]> {
|
) -> Vec<SnapPiece> {
|
||||||
if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
if !pdf_osnap() || !u.flags.contains(codec::entities::UnderlayDisplayFlags::ON) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
@@ -626,21 +695,35 @@ pub fn underlay_snap_geometry(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
|
let world = |p: [f64; 2]| crate::entities::underlay::local_to_world(u, p);
|
||||||
let mut out: Vec<[f64; 3]> = Vec::new();
|
// A circle stays one only while the placement scales both axes alike.
|
||||||
|
let uniform = (u.x_scale.abs() - u.y_scale.abs()).abs() <= 1e-9 * u.x_scale.abs().max(1e-12);
|
||||||
|
let flatten = |seg: &Segment, points: &mut Vec<[f64; 3]>| {
|
||||||
|
let steps = if matches!(seg, Segment::Cubic(..)) { 12 } else { 1 };
|
||||||
|
for k in 1..=steps {
|
||||||
|
points.push(world(bezier(seg, k as f64 / steps as f64)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut out: Vec<SnapPiece> = Vec::new();
|
||||||
|
let mut total = 0usize;
|
||||||
for path in &vectors.paths {
|
for path in &vectors.paths {
|
||||||
for sp in &path.subpaths {
|
for sp in &path.subpaths {
|
||||||
if out.len() >= MAX_GEOMETRY_POINTS {
|
if total >= MAX_GEOMETRY_POINTS {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
if !out.is_empty() {
|
if let (Some((c, r)), true) = (circle_of(sp), uniform) {
|
||||||
out.push([f64::NAN; 3]);
|
let mut points = vec![world(sp.segments[0].start())];
|
||||||
}
|
for seg in &sp.segments {
|
||||||
out.push(world(sp.segments[0].start()));
|
flatten(seg, &mut points);
|
||||||
for seg in &sp.segments {
|
|
||||||
let steps = if matches!(seg, Segment::Cubic(..)) { 12 } else { 1 };
|
|
||||||
for k in 1..=steps {
|
|
||||||
out.push(world(bezier(seg, k as f64 / steps as f64)));
|
|
||||||
}
|
}
|
||||||
|
total += points.len();
|
||||||
|
out.push(SnapPiece { points, circle: Some((world(c), r * u.x_scale.abs())) });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for seg in &sp.segments {
|
||||||
|
let mut points = vec![world(seg.start())];
|
||||||
|
flatten(seg, &mut points);
|
||||||
|
total += points.len();
|
||||||
|
out.push(SnapPiece { points, circle: None });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user