From e5b84ab8de296219d668db5c4f6fdf26b14f4d2b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 22 Aug 2026 15:33:43 +0300 Subject: [PATCH] fix(plot): honor wipeout draw order --- src/app/update/file.rs | 24 +++++-- src/io/pdf_export.rs | 124 +++++++++++++++++++++++++++++-------- src/io/print_to_printer.rs | 6 +- src/scene/mod.rs | 8 +++ src/scene/project.rs | 4 +- 5 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 720f86cc2..20cb17008 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -93,7 +93,7 @@ fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool { } type LayoutPlotParams = ( - std::sync::Arc>, + std::sync::Arc>, Vec, Vec, crate::io::pdf_export::PlotGroupSplits, @@ -107,7 +107,7 @@ type LayoutPlotParams = ( ); type ClippedPlotParams = ( - Vec, + Vec, Vec, Vec, crate::io::pdf_export::PlotGroupSplits, @@ -121,7 +121,7 @@ type ClippedPlotParams = ( ); fn plot_content_extents( - wires: &[crate::scene::WireModel], + wires: &[crate::io::pdf_export::PlotWire], hatches: &[crate::scene::model::hatch_model::HatchModel], wipeouts: &[crate::scene::model::hatch_model::HatchModel], ) -> Option<(f64, f64, f64, f64)> { @@ -163,7 +163,7 @@ fn plot_scene_content( paper_space_last: bool, render_mode_override: Option, ) -> ( - std::sync::Arc>, + std::sync::Arc>, Vec, Vec, crate::io::pdf_export::PlotGroupSplits, @@ -171,6 +171,16 @@ fn plot_scene_content( let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override); paper_wires.retain(|wire| wire.plot_visible); model_wires.retain(|wire| wire.plot_visible); + let with_depth = |wires: Vec| { + let depths = scene.plot_wire_depths(&wires); + wires + .into_iter() + .zip(depths) + .map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth }) + .collect::>() + }; + let paper_wires = with_depth(paper_wires); + let model_wires = with_depth(model_wires); let paper_hatches = scene.paper_plot_hatches().as_ref().clone(); let paper_wipeouts = scene.paper_plot_wipeouts().as_ref().clone(); if scene.current_layout == "Model" { @@ -188,7 +198,11 @@ fn plot_scene_content( } let (mut model_pattern_wires, model_hatches, model_wipeouts) = scene.viewport_plot_fills(); - model_pattern_wires.retain(|wire| wire.plot_visible); + model_pattern_wires.retain(|(wire, _)| wire.plot_visible); + let model_pattern_wires = model_pattern_wires + .into_iter() + .map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth }) + .collect::>(); let (wires, hatches, wipeouts, splits) = if paper_space_last { let splits = crate::io::pdf_export::PlotGroupSplits { diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index d7923f49e..b164c2701 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -24,12 +24,26 @@ use printpdf::{ use std::io::Write; use std::path::Path; +#[derive(Clone, Debug)] +pub struct PlotWire { + pub wire: WireModel, + pub draw_depth: f32, +} + +impl std::ops::Deref for PlotWire { + type Target = WireModel; + + fn deref(&self) -> &Self::Target { + &self.wire + } +} + // The web build has no `printpdf` (it pulls a wasm-incompatible `memchr` via // lopdf → nom_locate) and no filesystem, so PDF export is native-only; the web // build gets these stubs so the call sites still compile. #[cfg(target_arch = "wasm32")] pub fn export_pdf( - _wires: &[WireModel], + _wires: &[PlotWire], _hatches: &[HatchModel], _wipeouts: &[HatchModel], _paper_w: f64, @@ -95,7 +109,7 @@ pub struct PlotGroupSplits { /// Owned render data for one page in a multi-page PDF. #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub struct PdfPageInput { - pub wires: std::sync::Arc>, + pub wires: std::sync::Arc>, pub hatches: Vec, pub wipeouts: Vec, pub paper_w: f64, @@ -131,7 +145,7 @@ impl Default for PdfPlotOptions { /// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page. #[cfg(not(target_arch = "wasm32"))] pub fn export_pdf( - wires: &[WireModel], + wires: &[PlotWire], hatches: &[HatchModel], wipeouts: &[HatchModel], paper_w: f64, @@ -204,7 +218,7 @@ pub fn pick_pdf_path_owned( #[cfg(not(target_arch = "wasm32"))] fn build_pdf( - wires: &[WireModel], + wires: &[PlotWire], hatches: &[HatchModel], wipeouts: &[HatchModel], paper_w: f32, @@ -267,7 +281,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>) #[cfg(not(target_arch = "wasm32"))] fn append_pdf_page( doc: &mut PdfDocument, - wires: &[WireModel], + wires: &[PlotWire], hatches: &[HatchModel], wipeouts: &[HatchModel], paper_w: f32, @@ -393,29 +407,92 @@ fn append_pdf_page( (first_wires, first_hatches, first_wipeouts), (second_wires, second_hatches, second_wipeouts), ] { - emit_wire_fills(&mut ops, wires, ox, oy, plot_style, options); - - // Hatch / wipeout fills render before wires so linework stays visible. - for hatch in wipeouts.iter().chain(hatches.iter()) { - emit_hatch( - &mut ops, - hatch, - ox, - oy, - plot_style, - scale, - options, - normal_blend.as_ref(), - ); + enum DrawItem<'a> { + WireFill(&'a PlotWire), + Hatch(&'a HatchModel), + Wire(&'a PlotWire), + Text(&'a PlotWire), } + let mut draw_items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len()); + let mut sequence = 0usize; + for wire in wires { + if !wire.fill_tris.is_empty() { + draw_items.push((wire.draw_depth, 0u8, sequence, DrawItem::WireFill(wire))); + sequence += 1; + } + draw_items.push((wire.draw_depth, 2u8, sequence, DrawItem::Wire(wire))); + sequence += 1; + if !wire.text_verts.is_empty() { + draw_items.push((wire.draw_depth, 3u8, sequence, DrawItem::Text(wire))); + sequence += 1; + } + } + for hatch in wipeouts.iter().chain(hatches.iter()) { + draw_items.push((hatch.draw_depth, 1u8, sequence, DrawItem::Hatch(hatch))); + sequence += 1; + } + draw_items.sort_by(|a, b| { + a.0.total_cmp(&b.0) + .then_with(|| a.1.cmp(&b.1)) + .then_with(|| a.2.cmp(&b.2)) + }); + let mut last_color: Option<[f32; 3]> = None; let mut last_lw: Option = None; // Current PDF dash array (empty = solid). Tracked so the dash op is only // re-emitted when it actually changes between wires. let mut last_dash: Option> = None; - for wire in wires { + for (_, _, _, item) in draw_items { + let wire = match item { + DrawItem::WireFill(wire) => { + emit_wire_fills( + &mut ops, + std::slice::from_ref(&wire.wire), + ox, + oy, + plot_style, + options, + ); + last_color = None; + last_lw = None; + last_dash = None; + continue; + } + DrawItem::Hatch(hatch) => { + emit_hatch( + &mut ops, + hatch, + ox, + oy, + plot_style, + scale, + options, + normal_blend.as_ref(), + ); + last_color = None; + last_lw = None; + last_dash = None; + continue; + } + DrawItem::Text(wire) => { + emit_text( + &mut ops, + std::slice::from_ref(&wire.wire), + ox, + oy, + scale, + plot_style, + options, + ); + last_color = None; + last_lw = None; + last_dash = None; + continue; + } + DrawItem::Wire(wire) => wire, + }; let [mut r, mut g, mut b, a] = wire.color; if a < 0.01 { continue; @@ -552,13 +629,6 @@ fn append_pdf_page( } flush_line(&mut ops, &segment, dot_radius); } - - // Text (SDF glyph quads) — re-emitted as vector strokes / fills. Text now - // renders on-screen only as textured SDF quads (`wire.text_verts`), which - // this CPU exporter can't sample, so without this pass all text — including - // dimension text — is missing from the PDF (issue #385). Drawn after the - // wires (on top) and under the same rotation/scale/clip CTM. - emit_text(&mut ops, wires, ox, oy, scale, plot_style, options); } if needs_state { diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs index 29606def1..61084e33b 100644 --- a/src/io/print_to_printer.rs +++ b/src/io/print_to_printer.rs @@ -11,7 +11,7 @@ use crate::io::pdf_export; use crate::io::plot_style::PlotStyleTable; use crate::scene::model::hatch_model::HatchModel; -use crate::scene::WireModel; +use crate::io::pdf_export::PlotWire; /// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp` /// flags / `-o` options. On Windows the generated PDF already carries render @@ -38,7 +38,7 @@ pub fn list_printers() -> Vec { #[cfg(target_arch = "wasm32")] #[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - _wires: std::sync::Arc>, + _wires: std::sync::Arc>, _hatches: Vec, _wipeouts: Vec, _paper_w: f64, @@ -153,7 +153,7 @@ pub fn open_printer_properties(_printer: Option<&str>) -> Result<(), String> { #[cfg(not(target_arch = "wasm32"))] #[allow(clippy::too_many_arguments)] pub async fn print_wires_with( - wires: std::sync::Arc>, + wires: std::sync::Arc>, hatches: Vec, wipeouts: Vec, paper_w: f64, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index db72b0084..f37218280 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -5454,6 +5454,14 @@ impl Scene { (self.paper_sheet_wires_arc().as_ref().clone(), model_wires) } + pub(crate) fn plot_wire_depths(&self, wires: &[WireModel]) -> Vec { + let depths = self.draw_depth_map(); + wires + .iter() + .map(|wire| pipeline::wire_gpu::wire_draw_depth(wire, depths.as_ref())) + .collect() + } + /// Per-entity stable draw-order depth, keyed by entity handle value. /// A full build assigns sparse labels in effective draw order. Incremental /// Add/Remove then changes only the named handle: existing siblings retain diff --git a/src/scene/project.rs b/src/scene/project.rs index e1b4ff775..9da2ade65 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -354,7 +354,7 @@ impl Scene { /// needs the equivalent paper-space geometry explicitly. pub fn viewport_plot_fills( &self, - ) -> (Vec, Vec, Vec) { + ) -> (Vec<(WireModel, f32)>, Vec, Vec) { use acadrust::entities::Viewport; use model::hatch_model::HatchPattern; @@ -478,7 +478,7 @@ impl Scene { wire.aci = hatch.aci; wire.line_weight_px = hatch.line_weight_px; wire.aabb = aabb; - pattern_wires.push(wire); + pattern_wires.push((wire, hatch.draw_depth)); } continue; }