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 <noreply@anthropic.com>
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 scene = &self.tabs[i].scene;
|
||||||
let layout_name = scene.current_layout.clone();
|
let layout_name = scene.current_layout.clone();
|
||||||
let wires = scene.entity_wires();
|
let wires = scene.entity_wires();
|
||||||
|
let hatches = scene.paper_canvas_hatches();
|
||||||
|
let wipeouts = scene.paper_canvas_wipeouts();
|
||||||
|
|
||||||
// Read PlotSettings for current layout (if available).
|
// Read PlotSettings for current layout (if available).
|
||||||
use acadrust::objects::{ObjectType, PlotType};
|
use acadrust::objects::{ObjectType, PlotType};
|
||||||
@@ -3993,6 +3995,8 @@ impl H7CAD {
|
|||||||
|
|
||||||
match crate::io::pdf_export::export_pdf(
|
match crate::io::pdf_export::export_pdf(
|
||||||
&wires,
|
&wires,
|
||||||
|
hatches.as_slice(),
|
||||||
|
wipeouts.as_slice(),
|
||||||
eff_w,
|
eff_w,
|
||||||
eff_h,
|
eff_h,
|
||||||
draw_ox as f32,
|
draw_ox as f32,
|
||||||
@@ -4016,6 +4020,8 @@ impl H7CAD {
|
|||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
let layout_name = scene.current_layout.clone();
|
let layout_name = scene.current_layout.clone();
|
||||||
let wires = scene.entity_wires();
|
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};
|
use acadrust::objects::{ObjectType, PlotType};
|
||||||
let ps_snap = scene.document.objects.values().find_map(|obj| {
|
let ps_snap = scene.document.objects.values().find_map(|obj| {
|
||||||
if let ObjectType::PlotSettings(ps) = obj {
|
if let ObjectType::PlotSettings(ps) = obj {
|
||||||
@@ -4071,6 +4077,8 @@ impl H7CAD {
|
|||||||
async move {
|
async move {
|
||||||
crate::io::print_to_printer::print_wires(
|
crate::io::print_to_printer::print_wires(
|
||||||
wires,
|
wires,
|
||||||
|
hatches,
|
||||||
|
wipeouts,
|
||||||
eff_w,
|
eff_w,
|
||||||
eff_h,
|
eff_h,
|
||||||
draw_ox as f32,
|
draw_ox as f32,
|
||||||
|
|||||||
+110
-4
@@ -9,10 +9,11 @@
|
|||||||
// drawing origin at the paper origin.
|
// drawing origin at the paper origin.
|
||||||
|
|
||||||
use crate::io::plot_style::PlotStyleTable;
|
use crate::io::plot_style::PlotStyleTable;
|
||||||
|
use crate::scene::hatch_model::{HatchModel, HatchPattern};
|
||||||
use crate::scene::WireModel;
|
use crate::scene::WireModel;
|
||||||
use printpdf::{
|
use printpdf::{
|
||||||
Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PdfDocument, PdfPage,
|
Color, Line, LineCapStyle, LineJoinStyle, LinePoint, Mm, Op, PaintMode, PdfDocument, PdfPage,
|
||||||
PdfSaveOptions, Point, Pt, Rgb,
|
PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, WindingOrder,
|
||||||
};
|
};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::Path;
|
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.
|
/// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page.
|
||||||
pub fn export_pdf(
|
pub fn export_pdf(
|
||||||
wires: &[WireModel],
|
wires: &[WireModel],
|
||||||
|
hatches: &[HatchModel],
|
||||||
|
wipeouts: &[HatchModel],
|
||||||
paper_w: f64,
|
paper_w: f64,
|
||||||
paper_h: f64,
|
paper_h: f64,
|
||||||
offset_x: f32,
|
offset_x: f32,
|
||||||
@@ -37,6 +40,8 @@ pub fn export_pdf(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let bytes = build_pdf(
|
let bytes = build_pdf(
|
||||||
wires,
|
wires,
|
||||||
|
hatches,
|
||||||
|
wipeouts,
|
||||||
paper_w as f32,
|
paper_w as f32,
|
||||||
paper_h as f32,
|
paper_h as f32,
|
||||||
offset_x,
|
offset_x,
|
||||||
@@ -64,6 +69,8 @@ pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
|
|||||||
|
|
||||||
fn build_pdf(
|
fn build_pdf(
|
||||||
wires: &[WireModel],
|
wires: &[WireModel],
|
||||||
|
hatches: &[HatchModel],
|
||||||
|
wipeouts: &[HatchModel],
|
||||||
paper_w: f32,
|
paper_w: f32,
|
||||||
paper_h: f32,
|
paper_h: f32,
|
||||||
ox: 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).
|
// mm to PDF points (1 mm = 2.834645 pt).
|
||||||
const MM_TO_PT: f32 = 2.834645;
|
const MM_TO_PT: f32 = 2.834645;
|
||||||
// Screen px to PDF points (approximate at 96 dpi).
|
// Screen px to PDF points (approximate at 96 dpi).
|
||||||
const PX_TO_PT: f32 = 0.35278;
|
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 {
|
for wire in wires {
|
||||||
let [mut r, mut g, mut b, a] = wire.color;
|
let [mut r, mut g, mut b, a] = wire.color;
|
||||||
if a < 0.01 {
|
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::pdf_export;
|
||||||
use crate::io::plot_style::PlotStyleTable;
|
use crate::io::plot_style::PlotStyleTable;
|
||||||
|
use crate::scene::hatch_model::HatchModel;
|
||||||
use crate::scene::WireModel;
|
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.
|
/// Returns `Ok(printer_name)` on success or `Err(message)` on failure.
|
||||||
pub async fn print_wires(
|
pub async fn print_wires(
|
||||||
wires: Vec<WireModel>,
|
wires: Vec<WireModel>,
|
||||||
|
hatches: Vec<HatchModel>,
|
||||||
|
wipeouts: Vec<HatchModel>,
|
||||||
paper_w: f64,
|
paper_w: f64,
|
||||||
paper_h: f64,
|
paper_h: f64,
|
||||||
offset_x: f32,
|
offset_x: f32,
|
||||||
@@ -27,6 +31,8 @@ pub async fn print_wires(
|
|||||||
let tmp_path = std::env::temp_dir().join("h7cad_print.pdf");
|
let tmp_path = std::env::temp_dir().join("h7cad_print.pdf");
|
||||||
pdf_export::export_pdf(
|
pdf_export::export_pdf(
|
||||||
&wires,
|
&wires,
|
||||||
|
&hatches,
|
||||||
|
&wipeouts,
|
||||||
paper_w,
|
paper_w,
|
||||||
paper_h,
|
paper_h,
|
||||||
offset_x,
|
offset_x,
|
||||||
|
|||||||
Reference in new issue
Block a user