forked from 1b-agent/opencadstudio-upstream
feat(io): export hatch + wipeout fills to PDF
The PDF / print pipeline only walked `WireModel.points` (lines), so every
hatched region disappeared the moment a sheet left H7CAD. Route the same
`paper_canvas_hatches()` / `paper_canvas_wipeouts()` lists that the
on-screen canvas consumes into `pdf_export::export_pdf` and
`print_to_printer::print_wires`, and emit each one as an
`Op::DrawPolygon` with the EvenOdd winding rule so multi-ring boundaries
(islands / holes) render correctly:
- Solid → fill the boundary polygon in the hatch colour.
- Pattern → stroke the outline (PAT line families stay procedural;
follow-up work will rasterise them into segments).
- Gradient → fill with the averaged colour, mirroring paper_canvas.
Wipeouts emit first, then hatches, then wires — same z-order as the
on-screen canvas, so wires draw on top of the fills they belong to.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
dbed40effd
commit
c2f3844cb6
3 files changed
+125
-5
No files matched your search
@@ -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,
|
||||
|
||||
+110
-4
@@ -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<std::path::PathBuf> {
|
||||
|
||||
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<f32> = 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<f32> = 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<Op>, 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<Op>, 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<PolygonRing> = Vec::new();
|
||||
let mut current: Vec<LinePoint> = 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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<WireModel>,
|
||||
hatches: Vec<HatchModel>,
|
||||
wipeouts: Vec<HatchModel>,
|
||||
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,
|
||||
|
||||
Reference in new issue
Block a user