fix(plot): honor wipeout draw order

This commit is contained in:
Hakan Seven 2026-08-22 15:33:43 +03:00
commit e5b84ab8de
5 changed files with 127 additions and 35 deletions

View file

@ -93,7 +93,7 @@ fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool {
}
type LayoutPlotParams = (
std::sync::Arc<Vec<crate::scene::WireModel>>,
std::sync::Arc<Vec<crate::io::pdf_export::PlotWire>>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
crate::io::pdf_export::PlotGroupSplits,
@ -107,7 +107,7 @@ type LayoutPlotParams = (
);
type ClippedPlotParams = (
Vec<crate::scene::WireModel>,
Vec<crate::io::pdf_export::PlotWire>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
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<acadrust::entities::ViewportRenderMode>,
) -> (
std::sync::Arc<Vec<crate::scene::WireModel>>,
std::sync::Arc<Vec<crate::io::pdf_export::PlotWire>>,
Vec<crate::scene::model::hatch_model::HatchModel>,
Vec<crate::scene::model::hatch_model::HatchModel>,
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<crate::scene::WireModel>| {
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::<Vec<_>>()
};
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::<Vec<_>>();
let (wires, hatches, wipeouts, splits) = if paper_space_last {
let splits = crate::io::pdf_export::PlotGroupSplits {

View file

@ -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<Vec<WireModel>>,
pub wires: std::sync::Arc<Vec<PlotWire>>,
pub hatches: Vec<HatchModel>,
pub wipeouts: Vec<HatchModel>,
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<f32> = 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<Vec<i64>> = 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 {

View file

@ -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<String> {
#[cfg(target_arch = "wasm32")]
#[allow(clippy::too_many_arguments)]
pub async fn print_wires_with(
_wires: std::sync::Arc<Vec<WireModel>>,
_wires: std::sync::Arc<Vec<PlotWire>>,
_hatches: Vec<HatchModel>,
_wipeouts: Vec<HatchModel>,
_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<Vec<WireModel>>,
wires: std::sync::Arc<Vec<PlotWire>>,
hatches: Vec<HatchModel>,
wipeouts: Vec<HatchModel>,
paper_w: f64,

View file

@ -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<f32> {
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

View file

@ -354,7 +354,7 @@ impl Scene {
/// needs the equivalent paper-space geometry explicitly.
pub fn viewport_plot_fills(
&self,
) -> (Vec<WireModel>, Vec<HatchModel>, Vec<HatchModel>) {
) -> (Vec<(WireModel, f32)>, Vec<HatchModel>, Vec<HatchModel>) {
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;
}