fix(plot): align layout output with viewport
Keep nested model hatches, PAT origins and dots, and Layout lineweight scaling consistent across screen and vector output.\n\nRefs #618
This commit is contained in:
parent
796d7d91d0
commit
380cc4c9a8
6 changed files with 69 additions and 40 deletions
|
|
@ -2168,7 +2168,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
&mut self,
|
&mut self,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
|
|
||||||
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
||||||
self.direct_plot_params()
|
self.direct_plot_params()
|
||||||
else {
|
else {
|
||||||
|
|
@ -2177,10 +2176,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let plot_style = self.dialog_plot_style(&self.plot_dialog);
|
let plot_style = self.dialog_plot_style(&self.plot_dialog);
|
||||||
let mut render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits);
|
let render_options = Self::pdf_plot_options(&self.plot_dialog, group_splits);
|
||||||
if paper_space {
|
|
||||||
render_options.scale_lineweights = false;
|
|
||||||
}
|
|
||||||
let worker_path = path.clone();
|
let worker_path = path.clone();
|
||||||
let work = move || {
|
let work = move || {
|
||||||
crate::io::pdf_export::export_pdf(
|
crate::io::pdf_export::export_pdf(
|
||||||
|
|
@ -2527,7 +2523,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
||||||
let paper_space = self.tabs[self.active_tab].scene.current_layout != "Model";
|
|
||||||
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
let Some((wires, hatches, wipeouts, group_splits, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
||||||
self.direct_plot_params()
|
self.direct_plot_params()
|
||||||
else {
|
else {
|
||||||
|
|
@ -2536,10 +2531,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let plot_style = self.dialog_plot_style(&self.plot_dialog);
|
let plot_style = self.dialog_plot_style(&self.plot_dialog);
|
||||||
let mut options = self.plot_print_options(&self.plot_dialog, group_splits);
|
let options = self.plot_print_options(&self.plot_dialog, group_splits);
|
||||||
if paper_space {
|
|
||||||
options.render.scale_lineweights = false;
|
|
||||||
}
|
|
||||||
self.command_line.push_info("Sending to system printer…");
|
self.command_line.push_info("Sending to system printer…");
|
||||||
background_task(
|
background_task(
|
||||||
move || {
|
move || {
|
||||||
|
|
@ -2850,7 +2842,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlotFlag::Center if d.area != "Layout" => d.center = !d.center,
|
PlotFlag::Center if d.area != "Layout" => d.center = !d.center,
|
||||||
PlotFlag::ScaleLw if d.area != "Layout" && !d.fit_to_paper => {
|
PlotFlag::ScaleLw if !d.fit_to_paper => {
|
||||||
d.scale_lw = !d.scale_lw
|
d.scale_lw = !d.scale_lw
|
||||||
}
|
}
|
||||||
PlotFlag::UpsideDown => {
|
PlotFlag::UpsideDown => {
|
||||||
|
|
@ -3244,7 +3236,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
.or_else(|| d.scales.first().map(|(name, _)| name.clone()))
|
.or_else(|| d.scales.first().map(|(name, _)| name.clone()))
|
||||||
.unwrap_or_else(|| "1:1".into());
|
.unwrap_or_else(|| "1:1".into());
|
||||||
}
|
}
|
||||||
d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper && d.area != "Layout";
|
d.scale_lw = ps.flags.scale_lineweights && !d.fit_to_paper;
|
||||||
d.lineweights = ps.flags.print_lineweights;
|
d.lineweights = ps.flags.print_lineweights;
|
||||||
d.paperspace_last = ps.flags.draw_viewports_first;
|
d.paperspace_last = ps.flags.draw_viewports_first;
|
||||||
d.shade = match ps.shade_plot_mode {
|
d.shade = match ps.shade_plot_mode {
|
||||||
|
|
@ -3460,7 +3452,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
) -> crate::io::pdf_export::PdfPlotOptions {
|
) -> crate::io::pdf_export::PdfPlotOptions {
|
||||||
crate::io::pdf_export::PdfPlotOptions {
|
crate::io::pdf_export::PdfPlotOptions {
|
||||||
object_lineweights: d.lineweights,
|
object_lineweights: d.lineweights,
|
||||||
scale_lineweights: d.scale_lw && !d.fit_to_paper && d.area != "Layout",
|
scale_lineweights: d.scale_lw && !d.fit_to_paper,
|
||||||
transparency: d.transparency,
|
transparency: d.transparency,
|
||||||
stamp: d.stamp,
|
stamp: d.stamp,
|
||||||
merge_lines: d.merge_lines,
|
merge_lines: d.merge_lines,
|
||||||
|
|
|
||||||
|
|
@ -766,18 +766,6 @@ fn emit_hatch(
|
||||||
// Pattern hatches: rasterise the family lines clipped to the boundary
|
// Pattern hatches: rasterise the family lines clipped to the boundary
|
||||||
// and emit each as a stroked line. Skips the polygon outline entirely.
|
// and emit each as a stroked line. Skips the polygon outline entirely.
|
||||||
if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
|
if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
|
||||||
let segments = hatch.pattern_segments();
|
|
||||||
if segments.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ops.push(Op::SetOutlineColor {
|
|
||||||
col: Color::Rgb(Rgb {
|
|
||||||
r,
|
|
||||||
g,
|
|
||||||
b,
|
|
||||||
icc_profile: None,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
let physical = lw_override.unwrap_or_else(|| {
|
let physical = lw_override.unwrap_or_else(|| {
|
||||||
if options.object_lineweights {
|
if options.object_lineweights {
|
||||||
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1)
|
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1)
|
||||||
|
|
@ -790,6 +778,21 @@ fn emit_hatch(
|
||||||
} else {
|
} else {
|
||||||
scale.max(1e-6)
|
scale.max(1e-6)
|
||||||
};
|
};
|
||||||
|
let screen_dot_mm = 25.4 / 96.0;
|
||||||
|
let dot_length = (physical / MM_TO_PT / divisor)
|
||||||
|
.max(screen_dot_mm / scale.max(1e-6));
|
||||||
|
let segments = hatch.pattern_segments_for_plot(dot_length as f64);
|
||||||
|
if segments.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ops.push(Op::SetOutlineColor {
|
||||||
|
col: Color::Rgb(Rgb {
|
||||||
|
r,
|
||||||
|
g,
|
||||||
|
b,
|
||||||
|
icc_profile: None,
|
||||||
|
}),
|
||||||
|
});
|
||||||
ops.push(Op::SetOutlineThickness {
|
ops.push(Op::SetOutlineThickness {
|
||||||
pt: Pt(physical / divisor),
|
pt: Pt(physical / divisor),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -974,10 +974,20 @@ impl Scene {
|
||||||
self.bg_color
|
self.bg_color
|
||||||
};
|
};
|
||||||
// Block-internal hatch fills: explode every visible INSERT of this
|
// Block-internal hatch fills: explode every visible INSERT of this
|
||||||
// layout block and materialize its fills at world position. Shared with
|
// layout block and materialize its fills at world position. Paper
|
||||||
// the export path so a plot draws them identically. (tint_selected =
|
// content viewports also need the model block's INSERT fills; without
|
||||||
// true applies the screen selection highlight.)
|
// them the screen showed an empty/white block while plotting correctly
|
||||||
|
// materialized its solid hatch. (tint_selected = true applies the
|
||||||
|
// screen selection highlight.)
|
||||||
models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true, frozen));
|
models.extend(self.exploded_insert_hatch_models(layout_block, hatch_bg, true, frozen));
|
||||||
|
if self.current_layout != "Model" {
|
||||||
|
models.extend(self.exploded_insert_hatch_models(
|
||||||
|
self.model_space_block_handle(),
|
||||||
|
hatch_bg,
|
||||||
|
true,
|
||||||
|
frozen,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at
|
// Wide LwPolyline / Polyline2D bands are no longer hatch fills at
|
||||||
// model level: a flat band is drawn by expanding its centre-line wire
|
// model level: a flat band is drawn by expanding its centre-line wire
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,29 @@ impl HatchModel {
|
||||||
/// `world_origin + boundary[i]` resolved. Solid / gradient hatches return an
|
/// `world_origin + boundary[i]` resolved. Solid / gradient hatches return an
|
||||||
/// empty vec — callers fall back to their solid-fill path.
|
/// empty vec — callers fall back to their solid-fill path.
|
||||||
///
|
///
|
||||||
/// The whole rasterisation is f64 because the pattern is anchored at world
|
/// The whole rasterisation is f64 because boundaries and family origins are
|
||||||
/// (0, 0): a hatch at UTM sits ~5e5 away from that anchor, so the line index
|
/// resolved in absolute WCS. Narrowing them first at UTM magnitudes would
|
||||||
/// `k` runs to ~1e7 and `origin + k · step` in f32 quantises the family lines
|
/// quantise both the generated lines and the `edge − line` intersections.
|
||||||
/// into visible garbage, as does the `edge − line` cancellation below.
|
|
||||||
pub fn pattern_segments(&self) -> Vec<[[f64; 2]; 2]> {
|
pub fn pattern_segments(&self) -> Vec<[[f64; 2]; 2]> {
|
||||||
|
self.pattern_segments_with_dot_length(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plot variant of [`Self::pattern_segments`] that renders PAT zero-length
|
||||||
|
/// dash entries at a caller-provided world length. Screen rendering snaps
|
||||||
|
/// these entries to one pixel; plot callers know the model-to-paper scale
|
||||||
|
/// and can therefore choose the equivalent physical mark without tying its
|
||||||
|
/// size to the pattern period.
|
||||||
|
pub(crate) fn pattern_segments_for_plot(
|
||||||
|
&self,
|
||||||
|
dot_length: f64,
|
||||||
|
) -> Vec<[[f64; 2]; 2]> {
|
||||||
|
self.pattern_segments_with_dot_length(Some(dot_length.max(1e-9)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pattern_segments_with_dot_length(
|
||||||
|
&self,
|
||||||
|
plot_dot_length: Option<f64>,
|
||||||
|
) -> Vec<[[f64; 2]; 2]> {
|
||||||
let HatchPattern::Pattern(families) = &self.pattern else {
|
let HatchPattern::Pattern(families) = &self.pattern else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
@ -308,8 +326,8 @@ impl HatchModel {
|
||||||
// on-screen render.
|
// on-screen render.
|
||||||
let (fx0, fy0) = (family.x0 as f64, family.y0 as f64);
|
let (fx0, fy0) = (family.x0 as f64, family.y0 as f64);
|
||||||
let origin = [
|
let origin = [
|
||||||
(fx0 * cos_off - fy0 * sin_off) * scale,
|
ox + (fx0 * cos_off - fy0 * sin_off) * scale,
|
||||||
(fx0 * sin_off + fy0 * cos_off) * scale,
|
oy + (fx0 * sin_off + fy0 * cos_off) * scale,
|
||||||
];
|
];
|
||||||
let mut p_min = f64::INFINITY;
|
let mut p_min = f64::INFINITY;
|
||||||
let mut p_max = f64::NEG_INFINITY;
|
let mut p_max = f64::NEG_INFINITY;
|
||||||
|
|
@ -394,10 +412,10 @@ impl HatchModel {
|
||||||
// absolute phase (so the pattern aligns across spans,
|
// absolute phase (so the pattern aligns across spans,
|
||||||
// matching the GPU shader). Positive entries are
|
// matching the GPU shader). Positive entries are
|
||||||
// dashes, negative are gaps, and a zero-length entry is
|
// dashes, negative are gaps, and a zero-length entry is
|
||||||
// a dot — rendered as a short mark so dot patterns
|
// a round-capped point.
|
||||||
// (e.g. DOTS) are visible instead of drawing nothing.
|
|
||||||
let n = family.dashes.len();
|
let n = family.dashes.len();
|
||||||
let dot_len: f64 = (period * 0.06).max(1e-3);
|
let dot_len = plot_dot_length
|
||||||
|
.unwrap_or_else(|| (period * 0.06).max(1e-3));
|
||||||
let phase = t0.rem_euclid(period);
|
let phase = t0.rem_euclid(period);
|
||||||
// Start at the period boundary at or before t0; the
|
// Start at the period boundary at or before t0; the
|
||||||
// span clip below drops anything before t0.
|
// span clip below drops anything before t0.
|
||||||
|
|
@ -418,7 +436,6 @@ impl HatchModel {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else if d == 0.0 && seg_t >= t0 - 1e-6 && seg_t <= t1 + 1e-6 {
|
} else if d == 0.0 && seg_t >= t0 - 1e-6 && seg_t <= t1 + 1e-6 {
|
||||||
// Short centered mark for the dot.
|
|
||||||
let a = (seg_t - dot_len * 0.5).max(t0);
|
let a = (seg_t - dot_len * 0.5).max(t0);
|
||||||
let b = (seg_t + dot_len * 0.5).min(t1);
|
let b = (seg_t + dot_len * 0.5).min(t1);
|
||||||
if b > a {
|
if b > a {
|
||||||
|
|
|
||||||
|
|
@ -433,6 +433,13 @@ impl Scene {
|
||||||
let hatches = self.plot_hatches_for_block(model_block, Some(&frozen));
|
let hatches = self.plot_hatches_for_block(model_block, Some(&frozen));
|
||||||
for hatch in hatches {
|
for hatch in hatches {
|
||||||
if matches!(&hatch.pattern, HatchPattern::Pattern(_)) {
|
if matches!(&hatch.pattern, HatchPattern::Pattern(_)) {
|
||||||
|
// The GPU draws PAT zero-length dash entries as one-pixel
|
||||||
|
// dots. Convert that display pixel to model units before
|
||||||
|
// projecting so vector output keeps the same dot size
|
||||||
|
// instead of expanding it from the pattern period.
|
||||||
|
let dot_length = (25.4 / 96.0)
|
||||||
|
* self.paper_space_unit_factor()
|
||||||
|
/ viewport_scale.max(1e-9);
|
||||||
let mut points = Vec::new();
|
let mut points = Vec::new();
|
||||||
let mut aabb = [
|
let mut aabb = [
|
||||||
f32::INFINITY,
|
f32::INFINITY,
|
||||||
|
|
@ -440,7 +447,7 @@ impl Scene {
|
||||||
f32::NEG_INFINITY,
|
f32::NEG_INFINITY,
|
||||||
f32::NEG_INFINITY,
|
f32::NEG_INFINITY,
|
||||||
];
|
];
|
||||||
for [a, b] in hatch.pattern_segments() {
|
for [a, b] in hatch.pattern_segments_for_plot(dot_length) {
|
||||||
let (Some(a), Some(b)) =
|
let (Some(a), Some(b)) =
|
||||||
(project(a[0], a[1]), project(b[0], b[1]))
|
(project(a[0], a[1]), project(b[0], b[1]))
|
||||||
else {
|
else {
|
||||||
|
|
|
||||||
|
|
@ -681,7 +681,7 @@ pub fn view_window(
|
||||||
"Scale lineweights",
|
"Scale lineweights",
|
||||||
s.scale_lw && !s.fit_to_paper,
|
s.scale_lw && !s.fit_to_paper,
|
||||||
PlotFlag::ScaleLw,
|
PlotFlag::ScaleLw,
|
||||||
common_area && !s.fit_to_paper,
|
!s.fit_to_paper,
|
||||||
),
|
),
|
||||||
].spacing(7));
|
].spacing(7));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue