diff --git a/src/app/update.rs b/src/app/update.rs index 04f4e1458..7a3f4380f 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -3898,6 +3898,8 @@ impl H7CAD { let scene = &self.tabs[i].scene; let layout_name = scene.current_layout.clone(); let wires = scene.entity_wires(); + let hatches = scene.paper_canvas_hatches(); + let wipeouts = scene.paper_canvas_wipeouts(); // Read PlotSettings for current layout (if available). use acadrust::objects::{ObjectType, PlotType}; @@ -3993,6 +3995,8 @@ impl H7CAD { match crate::io::pdf_export::export_pdf( &wires, + hatches.as_slice(), + wipeouts.as_slice(), eff_w, eff_h, draw_ox as f32, @@ -4016,6 +4020,8 @@ impl H7CAD { let scene = &self.tabs[i].scene; let layout_name = scene.current_layout.clone(); let wires = scene.entity_wires(); + let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone(); + let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone(); use acadrust::objects::{ObjectType, PlotType}; let ps_snap = scene.document.objects.values().find_map(|obj| { if let ObjectType::PlotSettings(ps) = obj { @@ -4071,6 +4077,8 @@ impl H7CAD { async move { crate::io::print_to_printer::print_wires( wires, + hatches, + wipeouts, eff_w, eff_h, draw_ox as f32, diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 9b3e50ac2..f707bfaab 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -9,10 +9,11 @@ // drawing origin at the paper origin. use crate::io::plot_style::PlotStyleTable; +use crate::scene::hatch_model::{HatchModel, HatchPattern}; use crate::scene::WireModel; use printpdf::{ - Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, PdfPage, - PdfSaveOptions, Point, Pt, Rgb, + Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PaintMode, PdfDocument, PdfPage, + PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, WindingOrder, }; use std::io::Write; use std::path::Path; @@ -27,6 +28,8 @@ use std::path::Path; /// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page. pub fn export_pdf( wires: &[WireModel], + hatches: &[HatchModel], + wipeouts: &[HatchModel], paper_w: f64, paper_h: f64, offset_x: f32, @@ -37,6 +40,8 @@ pub fn export_pdf( ) -> Result<(), String> { let bytes = build_pdf( wires, + hatches, + wipeouts, paper_w as f32, paper_h as f32, offset_x, @@ -64,6 +69,8 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option { fn build_pdf( wires: &[WireModel], + hatches: &[HatchModel], + wipeouts: &[HatchModel], paper_w: f32, paper_h: f32, ox: f32, @@ -123,13 +130,21 @@ fn build_pdf( }); } - let mut last_color: Option<[f32; 3]> = None; - let mut last_lw: Option = None; // mm to PDF points (1 mm = 2.834645 pt). const MM_TO_PT: f32 = 2.834645; // Screen px to PDF points (approximate at 96 dpi). const PX_TO_PT: f32 = 0.35278; + // ── Hatch / wipeout fills (rendered before wires so wires draw on top, + // matching paper_canvas ordering). Each `emit_hatch` sets its own + // fill / stroke colour, so the wire pass below starts fresh. + for hatch in wipeouts.iter().chain(hatches.iter()) { + emit_hatch(&mut ops, hatch, ox, oy); + } + + let mut last_color: Option<[f32; 3]> = None; + let mut last_lw: Option = None; + for wire in wires { let [mut r, mut g, mut b, a] = wire.color; if a < 0.01 { @@ -231,3 +246,94 @@ fn flush_line(ops: &mut Vec, pts: &[LinePoint]) { }, }); } + +/// Emit a single hatch / wipeout as a filled (or stroked, for pattern fills) +/// polygon. NaN sentinels in `hatch.boundary` split the path into multiple +/// rings so islands and holes render correctly under the even-odd rule. +/// Mirrors `scene::paper_canvas::draw_hatch`: solid → fill, pattern → outline, +/// gradient → solid fill of the averaged colour. +fn emit_hatch(ops: &mut Vec, hatch: &HatchModel, ox: f32, oy: f32) { + if hatch.boundary.is_empty() { + return; + } + let [r, g, b, a] = hatch.color; + if a < 0.01 { + return; + } + let world_ox = hatch.world_origin[0] as f32; + let world_oy = hatch.world_origin[1] as f32; + + // Split the boundary into rings on every NaN-NaN separator. + let mut rings: Vec = Vec::new(); + let mut current: Vec = Vec::new(); + for &[bx, by] in hatch.boundary.iter() { + if bx.is_nan() || by.is_nan() { + if current.len() >= 3 { + rings.push(PolygonRing { points: std::mem::take(&mut current) }); + } else { + current.clear(); + } + continue; + } + current.push(LinePoint { + p: Point::new(Mm(bx + world_ox + ox), Mm(by + world_oy + oy)), + bezier: false, + }); + } + if current.len() >= 3 { + rings.push(PolygonRing { points: current }); + } + if rings.is_empty() { + return; + } + + let (paint_mode, fill_color) = match &hatch.pattern { + HatchPattern::Solid => (PaintMode::Fill, [r, g, b]), + HatchPattern::Pattern(_) => { + // Stroke the outline only — pattern lines themselves are + // procedural and not synthesised on the CPU. + (PaintMode::Stroke, [r, g, b]) + } + HatchPattern::Gradient { color2, .. } => { + // PDF gradients are stored in resource dictionaries; for the + // fast path we average the two colours, matching paper_canvas. + let avg = [ + (r + color2[0]) * 0.5, + (g + color2[1]) * 0.5, + (b + color2[2]) * 0.5, + ]; + (PaintMode::Fill, avg) + } + }; + + match paint_mode { + PaintMode::Fill | PaintMode::FillStroke => { + ops.push(Op::SetFillColor { + col: Color::Rgb(Rgb { + r: fill_color[0], + g: fill_color[1], + b: fill_color[2], + icc_profile: None, + }), + }); + } + _ => {} + } + if matches!(paint_mode, PaintMode::Stroke | PaintMode::FillStroke) { + ops.push(Op::SetOutlineColor { + col: Color::Rgb(Rgb { + r: fill_color[0], + g: fill_color[1], + b: fill_color[2], + icc_profile: None, + }), + }); + } + ops.push(Op::DrawPolygon { + polygon: Polygon { + rings, + mode: paint_mode, + winding_order: WindingOrder::EvenOdd, + }, + }); +} diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 7e827a64e..14276f4d6 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -9,13 +9,17 @@ use crate::io::pdf_export; use crate::io::plot_style::PlotStyleTable; +use crate::scene::hatch_model::HatchModel; use crate::scene::WireModel; -/// Render `wires` to a temp PDF and dispatch it to the default system printer. +/// Render `wires` (plus hatch / wipeout fills) to a temp PDF and dispatch it +/// to the default system printer. /// /// Returns `Ok(printer_name)` on success or `Err(message)` on failure. pub async fn print_wires( wires: Vec, + hatches: Vec, + wipeouts: Vec, paper_w: f64, paper_h: f64, offset_x: f32, @@ -27,6 +31,8 @@ pub async fn print_wires( let tmp_path = std::env::temp_dir().join("h7cad_print.pdf"); pdf_export::export_pdf( &wires, + &hatches, + &wipeouts, paper_w, paper_h, offset_x,