fix(layout): complete paperspace integration

Apply each layout's embedded page setup and plot style consistently across display, PDF, printing, and multi-layout output.\n\nRefs #844
This commit is contained in:
Hakan Seven committed 2026-08-26 08:58:44 +03:00
1 parent 9c8f841f71
commit 8c61d89b1e
18 files changed
+1393 -373

No files matched your search

+2 -19
View File
@@ -72,8 +72,7 @@ impl Scene {
// Remove the Layout object itself.
self.document.objects.remove(&layout_handle);
// Drop the layout's entry from the ACAD_LAYOUT dictionary so it does not
// dangle (and so AutoCAD doesn't try to recover a now-missing layout).
// Drop the layout's dictionary entry so it does not dangle.
let dict_handle = self.document.header.acad_layout_dict_handle;
if let Some(ObjectType::Dictionary(d)) = self.document.objects.get_mut(&dict_handle) {
d.entries.retain(|(k, _)| k != name);
@@ -90,20 +89,6 @@ impl Scene {
self.document.block_records.remove(&bn);
}
// Drop any standalone PlotSettings page setup tied to this layout.
let ps_handles: Vec<Handle> = self
.document
.objects
.iter()
.filter_map(|(h, o)| match o {
ObjectType::PlotSettings(ps) if ps.page_name == name => Some(*h),
_ => None,
})
.collect();
for h in ps_handles {
self.document.objects.remove(&h);
}
// If the deleted layout was active, fall back to Model space.
if self.current_layout == name {
self.current_layout = "Model".to_string();
@@ -498,9 +483,7 @@ impl Scene {
/// Reconstruct a `pane_grid` configuration from a set of (tile-index, rect)
/// items covering `region`, by recursively finding a full vertical or
/// horizontal guillotine cut. AutoCAD tiled configs (and anything pane_grid
/// produces) are guillotine layouts, so this round-trips them. A non-guillotine
/// set falls back to chaining panes so nothing is lost.
/// horizontal guillotine cut. Non-guillotine input falls back to chained panes.
fn config_from_rects(
items: &[(usize, iced::Rectangle)],
region: iced::Rectangle,
+1
View File
@@ -91,6 +91,7 @@ impl Scene {
self.document.header.limit_check = enabled;
} else {
self.document.header.paper_space_limit_check = enabled;
self.persist_current_layout_state();
}
}
+181 -19
View File
@@ -1614,6 +1614,15 @@ pub struct Scene {
/// movement scanned the whole document for wipeouts and recreated the same
/// Arcs, making Paper frame construction CPU-bound on large drawings.
paper_sheet_render_cache: RefCell<HashMap<String, PaperSheetRenderCache>>,
display_plot_style_cache:
RefCell<HashMap<(String, String), Option<Arc<crate::io::plot_style::PlotStyleTable>>>>,
styled_wire_cache: RefCell<
HashMap<(u64, String), (u64, Arc<Vec<WireModel>>)>,
>,
styled_hatch_cache:
RefCell<HashMap<(u64, usize, usize, String, u32), Arc<Vec<HatchModel>>>>,
styled_wire_fill_cache:
RefCell<HashMap<(u64, String, u32), Arc<Vec<HatchModel>>>>,
/// Per-viewport projected wire cache for paper-space content viewports.
/// Stores projected + clipped wires in paper-space coordinates.
/// Maps vp_handle → (geometry_epoch, Vec<WireModel>).
@@ -1880,6 +1889,10 @@ impl Scene {
paper_sheet_cache: RefCell::new(HashMap::default()),
paper_viewport_cache: RefCell::new(HashMap::default()),
paper_sheet_render_cache: RefCell::new(HashMap::default()),
display_plot_style_cache: RefCell::new(HashMap::default()),
styled_wire_cache: RefCell::new(HashMap::default()),
styled_hatch_cache: RefCell::new(HashMap::default()),
styled_wire_fill_cache: RefCell::new(HashMap::default()),
paper_projected_cache: RefCell::new(HashMap::default()),
current_layout: "Model".to_string(),
block_edit_block: None,
@@ -2878,7 +2891,9 @@ impl Scene {
/// active layout or its background are dropped.
pub fn set_current_layout(&mut self, name: String) {
if self.current_layout != name {
self.persist_current_layout_state();
self.current_layout = name;
self.load_current_layout_state();
self.sync_active_space_to_document();
self.recolor_meshes();
*self.wire_cache.borrow_mut() = None;
@@ -2888,6 +2903,91 @@ impl Scene {
}
}
pub fn load_current_layout_state(&mut self) {
self.display_plot_style_cache
.borrow_mut()
.retain(|(layout, _), _| layout != &self.current_layout);
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
if self.current_layout == "Model" {
return;
}
if let Some((flags, insertion_base, min_extents, max_extents, min_limits, max_limits)) =
self.document.objects.values().find_map(|object| {
let ObjectType::Layout(layout) = object else {
return None;
};
(layout.name == self.current_layout).then_some((
layout.flags,
layout.insertion_base,
layout.min_extents,
layout.max_extents,
layout.min_limits,
layout.max_limits,
))
}) {
self.document.header.paper_space_linetype_scaling = flags & 1 != 0;
self.document.header.paper_space_limit_check = flags & 2 != 0;
self.document.header.paper_space_insertion_base = acadrust::types::Vector3::new(
insertion_base.0,
insertion_base.1,
insertion_base.2,
);
self.document.header.paper_space_extents_min = acadrust::types::Vector3::new(
min_extents.0,
min_extents.1,
min_extents.2,
);
self.document.header.paper_space_extents_max = acadrust::types::Vector3::new(
max_extents.0,
max_extents.1,
max_extents.2,
);
self.document.header.paper_space_limits_min =
acadrust::types::Vector2::new(min_limits.0, min_limits.1);
self.document.header.paper_space_limits_max =
acadrust::types::Vector2::new(max_limits.0, max_limits.1);
}
}
pub fn persist_current_layout_state(&mut self) {
if self.current_layout == "Model" {
return;
}
let header = &self.document.header;
let psltscale = header.paper_space_linetype_scaling;
let plimcheck = header.paper_space_limit_check;
let insertion_base = header.paper_space_insertion_base;
let min_extents = header.paper_space_extents_min;
let max_extents = header.paper_space_extents_max;
let min_limits = header.paper_space_limits_min;
let max_limits = header.paper_space_limits_max;
for object in self.document.objects.values_mut() {
let ObjectType::Layout(layout) = object else {
continue;
};
if layout.name == self.current_layout {
layout.flags = if psltscale {
layout.flags | 1
} else {
layout.flags & !1
};
layout.flags = if plimcheck {
layout.flags | 2
} else {
layout.flags & !2
};
layout.insertion_base = (insertion_base.x, insertion_base.y, insertion_base.z);
layout.min_extents = (min_extents.x, min_extents.y, min_extents.z);
layout.max_extents = (max_extents.x, max_extents.y, max_extents.z);
layout.min_limits = (min_limits.x, min_limits.y);
layout.max_limits = (max_limits.x, max_limits.y);
break;
}
}
}
/// Mirror the active space (Model tile-mode vs a paper layout) into the
/// document's persisted settings so it round-trips on save: the `$TILEMODE`
/// header (`show_model_space`) and the `CTAB` current-tab variable. The
@@ -3305,32 +3405,17 @@ impl Scene {
Some(wire)
}
/// The effective plot settings for the current layout: a standalone
/// PlotSettings page setup if one exists, otherwise the settings embedded in
/// the LAYOUT object (paper size, margins, origin, rotation, scale). Loaded
/// The fallback preserves rotation, origin and scale from loaded files.
/// The effective plot settings embedded in the current layout.
pub fn effective_plot_settings(&self) -> Option<acadrust::objects::PlotSettings> {
self.plot_settings_for(&self.current_layout)
}
/// Plot settings for a specific layout by name: its standalone
/// `PlotSettings` object if one exists, else synthesized from the `Layout`
/// object's embedded fields.
/// Plot settings embedded in a specific layout.
pub fn plot_settings_for(&self, name: &str) -> Option<acadrust::objects::PlotSettings> {
use acadrust::objects::{
ObjectType, PaperMargin, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
PlotWindow, ScaledType,
PlotWindow, ScaledType, ShadePlotMode, ShadePlotResolutionLevel,
};
if let Some(ps) = self.document.objects.values().find_map(|o| {
if let ObjectType::PlotSettings(ps) = o {
if ps.page_name.as_str() == name {
return Some(ps.clone());
}
}
None
}) {
return Some(ps);
}
self.document.objects.values().find_map(|o| {
let ObjectType::Layout(l) = o else {
return None;
@@ -3338,10 +3423,13 @@ impl Scene {
if l.name.as_str() != name {
return None;
}
let mut ps = PlotSettings::new(l.name.clone());
let mut ps = PlotSettings::new(l.plot_page_name.clone());
ps.printer_name = l.plot_printer_name.clone();
ps.paper_width = l.paper_width;
ps.paper_height = l.paper_height;
ps.paper_size = l.paper_size.clone();
ps.plot_view_name = l.plot_view_name.clone();
ps.current_style_sheet = l.plot_style_sheet.clone();
ps.margins = PaperMargin::new(
l.plot_margin_left,
l.plot_margin_bottom,
@@ -3362,10 +3450,84 @@ impl Scene {
ps.scale_type = ScaledType::from_code(l.plot_scale_type);
ps.scale_numerator = l.plot_scale_numerator;
ps.scale_denominator = l.plot_scale_denominator;
ps.flags = l.plot_flags;
ps.standard_scale_factor = l.plot_scale_factor;
ps.paper_image_origin_x = l.paper_image_origin_x;
ps.paper_image_origin_y = l.paper_image_origin_y;
ps.shade_plot_mode = ShadePlotMode::from_code(l.shade_plot_mode);
ps.shade_plot_resolution =
ShadePlotResolutionLevel::from_code(l.shade_plot_resolution);
ps.shade_plot_dpi = l.shade_plot_dpi;
ps.plot_view_handle = l.plot_view_handle;
ps.visual_style_handle = l.visual_style_handle;
Some(ps)
})
}
/// Replace the plot-settings portion embedded in one layout.
pub fn set_layout_plot_settings(
&mut self,
name: &str,
ps: &acadrust::objects::PlotSettings,
) -> bool {
let Some(layout) = self.document.objects.values_mut().find_map(|object| {
let ObjectType::Layout(layout) = object else {
return None;
};
(layout.name == name).then_some(layout)
}) else {
return false;
};
layout.plot_page_name = ps.page_name.clone();
layout.plot_printer_name = ps.printer_name.clone();
layout.paper_size = ps.paper_size.clone();
layout.plot_view_name = ps.plot_view_name.clone();
layout.plot_style_sheet = ps.current_style_sheet.clone();
layout.plot_margin_left = ps.margins.left;
layout.plot_margin_bottom = ps.margins.bottom;
layout.plot_margin_right = ps.margins.right;
layout.plot_margin_top = ps.margins.top;
layout.paper_width = ps.paper_width;
layout.paper_height = ps.paper_height;
layout.plot_origin_x = ps.origin_x;
layout.plot_origin_y = ps.origin_y;
layout.plot_window_min_x = ps.plot_window.lower_left_x;
layout.plot_window_min_y = ps.plot_window.lower_left_y;
layout.plot_window_max_x = ps.plot_window.upper_right_x;
layout.plot_window_max_y = ps.plot_window.upper_right_y;
layout.plot_scale_numerator = ps.scale_numerator;
layout.plot_scale_denominator = ps.scale_denominator;
layout.plot_paper_units = ps.paper_units.to_code();
layout.plot_rotation = ps.rotation.to_code();
layout.plot_type = ps.plot_type.to_code();
layout.plot_scale_type = ps.scale_type.to_code();
layout.shade_plot_mode = ps.shade_plot_mode.to_code();
layout.shade_plot_resolution = ps.shade_plot_resolution.to_code();
layout.shade_plot_dpi = ps.shade_plot_dpi;
layout.plot_flags = ps.flags;
layout.plot_scale_factor = ps.standard_scale_factor;
layout.paper_image_origin_x = ps.paper_image_origin_x;
layout.paper_image_origin_y = ps.paper_image_origin_y;
layout.plot_view_handle = ps.plot_view_handle;
layout.visual_style_handle = ps.visual_style_handle;
layout.raw_plot_settings_codes = None;
self.display_plot_style_cache
.borrow_mut()
.retain(|(layout_name, _), _| layout_name != name);
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
true
}
pub fn invalidate_display_plot_style(&self) {
self.display_plot_style_cache.borrow_mut().clear();
self.styled_wire_cache.borrow_mut().clear();
self.styled_hatch_cache.borrow_mut().clear();
self.styled_wire_fill_cache.borrow_mut().clear();
}
/// PlotSettings store the paper size and plot margins in millimetres, but a
/// layout's paper space is laid out in its own units — its viewports,
/// drawing limits and the sheet the user sees can be in inches, millimetres,
+33
View File
@@ -139,6 +139,39 @@ impl GradientKind {
}
}
pub(crate) fn plot_style_fill_pattern(style: u8) -> Option<HatchPattern> {
let family = |angle_deg, dx, dy, dashes: &[f32]| PatFamily {
angle_deg,
x0: 0.0,
y0: 0.0,
dx,
dy,
dashes: dashes.to_vec(),
};
let pattern = match style {
64 => HatchPattern::Solid,
65 => HatchPattern::Pattern(vec![
family(0.0, 0.0, 2.0, &[2.0, -2.0]),
family(90.0, 2.0, 0.0, &[2.0, -2.0]),
]),
66 => HatchPattern::Pattern(vec![
family(0.0, 0.0, 2.0, &[]),
family(90.0, 2.0, 0.0, &[]),
]),
67 => HatchPattern::Pattern(vec![
family(45.0, 0.0, 2.0, &[]),
family(135.0, 0.0, 2.0, &[]),
]),
68 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[])]),
69 => HatchPattern::Pattern(vec![family(135.0, 0.0, 2.0, &[])]),
70 => HatchPattern::Pattern(vec![family(45.0, 0.0, 2.0, &[])]),
71 => HatchPattern::Pattern(vec![family(0.0, 0.0, 2.0, &[0.0, -2.0])]),
72 => HatchPattern::Pattern(vec![family(90.0, 2.0, 0.0, &[])]),
_ => return None,
};
Some(pattern)
}
/// Hatch fill pattern.
#[derive(Clone, Debug)]
pub enum HatchPattern {
+271 -3
View File
@@ -22,6 +22,8 @@ use crate::scene::{
SceneLight, Uniforms, ViewportInstance, WireModel,
};
const DISPLAY_STYLE_CACHE_LIMIT: usize = if cfg!(target_arch = "wasm32") { 4 } else { 24 };
// ── Camera hover state (shader::Program::State) ───────────────────────────
#[derive(Clone, Default)]
@@ -2496,6 +2498,238 @@ impl Scene {
};
(final_color, pl, pat, lw, aci)
}
fn display_plot_style(&self) -> Option<Arc<crate::io::plot_style::PlotStyleTable>> {
if self.current_layout == "Model" {
return None;
}
let settings = self.effective_plot_settings()?;
if !settings.flags.show_plot_styles || settings.current_style_sheet.is_empty() {
return None;
}
let key = (
self.current_layout.clone(),
settings.current_style_sheet.to_ascii_lowercase(),
);
if let Some(style) = self.display_plot_style_cache.borrow().get(&key) {
return style.clone();
}
let style = crate::io::plot_style::PlotStyleTable::load_named(
&settings.current_style_sheet,
)
.ok()
.map(Arc::new);
self.display_plot_style_cache
.borrow_mut()
.insert(key, style.clone());
style
}
fn apply_display_plot_style(
&self,
color: &mut [f32; 4],
aci: u8,
style: &crate::io::plot_style::PlotStyleTable,
) {
if aci == 0 {
return;
}
if let Some(rgb) = style.resolve_color(aci) {
color[..3].copy_from_slice(&rgb);
}
let screening = style.resolve_screening(aci);
for (channel, paper) in color[..3]
.iter_mut()
.zip(self.paper_bg_color[..3].iter())
{
*channel = *channel * screening + *paper * (1.0 - screening);
}
}
fn display_styled_wires(
&self,
source: Arc<Vec<WireModel>>,
source_gen: u64,
) -> (Arc<Vec<WireModel>>, u64) {
let Some(style) = self.display_plot_style() else {
return (source, source_gen);
};
let key = (source_gen, style.name.to_ascii_lowercase());
if let Some((gen, wires)) = self.styled_wire_cache.borrow().get(&key) {
return (Arc::clone(wires), *gen);
}
let mut wires = source.as_ref().clone();
for wire in &mut wires {
self.apply_display_plot_style(&mut wire.color, wire.aci, &style);
if wire.aci > 0 {
if wire.fill_is_2d_solid
&& style
.aci_entries
.get(wire.aci as usize)
.is_some_and(|entry| (65..=72).contains(&entry.fill_style))
{
wire.fill_tris.clear();
wire.fill_tris_low.clear();
}
if let Some(mm) = style.resolve_lineweight(wire.aci) {
wire.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
}
for vertex in &mut wire.text_verts {
self.apply_display_plot_style(&mut vertex.color, wire.aci, &style);
}
}
}
let wires = Arc::new(wires);
let gen = crate::scene::WIRE_CONTENT_GEN
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut cache = self.styled_wire_cache.borrow_mut();
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
cache.clear();
}
cache.insert(key, (gen, Arc::clone(&wires)));
(wires, gen)
}
fn display_styled_hatches(
&self,
source: Arc<Vec<HatchModel>>,
wire_fills: Option<&Arc<Vec<HatchModel>>>,
pattern_scale: f32,
) -> Arc<Vec<HatchModel>> {
let Some(style) = self.display_plot_style() else {
return source;
};
let key = (
self.geometry_epoch,
Arc::as_ptr(&source) as usize,
wire_fills.map_or(0, |fills| Arc::as_ptr(fills) as usize),
style.name.to_ascii_lowercase(),
pattern_scale.to_bits(),
);
if let Some(hatches) = self.styled_hatch_cache.borrow().get(&key) {
return Arc::clone(hatches);
}
let mut hatches = source.as_ref().clone();
for hatch in &mut hatches {
self.apply_display_plot_style(&mut hatch.color, hatch.aci, &style);
if hatch.aci > 0 {
if matches!(hatch.pattern, crate::scene::model::hatch_model::HatchPattern::Solid) {
if let Some(fill_style) = style
.aci_entries
.get(hatch.aci as usize)
.and_then(|entry| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
{
hatch.pattern = fill_style;
hatch.scale = pattern_scale;
}
}
if let Some(mm) = style.resolve_lineweight(hatch.aci) {
hatch.line_weight_px = (mm * (96.0 / 25.4) * 2.0).max(1.0);
}
if let crate::scene::model::hatch_model::HatchPattern::Gradient {
color2, ..
} = &mut hatch.pattern
{
self.apply_display_plot_style(color2, hatch.aci, &style);
}
}
}
if let Some(wire_fills) = wire_fills {
hatches.extend(wire_fills.iter().cloned());
}
let hatches = Arc::new(hatches);
let mut cache = self.styled_hatch_cache.borrow_mut();
cache.retain(|(epoch, _, _, _, _), _| *epoch == self.geometry_epoch);
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT * 2 {
cache.clear();
}
cache.insert(key, Arc::clone(&hatches));
hatches
}
fn display_styled_wire_fills(
&self,
wires: &Arc<Vec<WireModel>>,
source_gen: u64,
pattern_scale: f32,
) -> Option<Arc<Vec<HatchModel>>> {
let Some(style) = self.display_plot_style() else {
return None;
};
let key = (
source_gen,
style.name.to_ascii_lowercase(),
pattern_scale.to_bits(),
);
if let Some(hatches) = self.styled_wire_fill_cache.borrow().get(&key) {
return Some(Arc::clone(hatches));
}
let mut hatches = Vec::new();
for wire in wires.iter().filter(|wire| wire.fill_is_2d_solid && wire.aci > 0) {
let Some(pattern) = style
.aci_entries
.get(wire.aci as usize)
.and_then(|entry| {
(65..=72)
.contains(&entry.fill_style)
.then(|| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
.flatten()
})
else {
continue;
};
let mut color = wire.color;
self.apply_display_plot_style(&mut color, wire.aci, &style);
let line_weight_px = style
.resolve_lineweight(wire.aci)
.map(|mm| (mm * (96.0 / 25.4) * 2.0).max(1.0))
.unwrap_or(wire.line_weight_px);
for (triangle_index, triangle) in wire.fill_tris.chunks_exact(3).enumerate() {
let mut boundary = Vec::with_capacity(4);
for (point_index, point) in triangle.iter().enumerate() {
let index = triangle_index * 3 + point_index;
let low = wire.fill_tris_low.get(index).copied().unwrap_or([0.0; 3]);
boundary.push([point[0] + low[0], point[1] + low[1]]);
}
boundary.push(boundary[0]);
hatches.push(HatchModel {
render_instance: wire.render_instance.clone(),
world_origin: [0.0, 0.0],
boundary: Arc::new(boundary),
boundary_wcs: None,
fill_plane: None,
fill_plane_boundary: None,
boundary_exterior: None,
boundary_sources: None,
boundary_paths: None,
style: acadrust::entities::HatchStyleType::Normal,
pattern: pattern.clone(),
name: "PLOTSTYLE".to_string(),
color,
aci: 0,
line_weight_px,
angle_offset: 0.0,
scale: pattern_scale,
draw_depth: wire.depth_override.unwrap_or(0.0),
});
}
}
let hatches = Arc::new(hatches);
let mut cache = self.styled_wire_fill_cache.borrow_mut();
if cache.len() >= DISPLAY_STYLE_CACHE_LIMIT {
cache.clear();
}
cache.insert(key, Arc::clone(&hatches));
Some(hatches)
}
}
/// Whether an entity sits on a locked layer (via the document's layer table).
@@ -3403,11 +3637,15 @@ impl Scene {
} else {
self.model_wires_for_viewport_arc(inst.handle, full.height)
};
let source_gen = self.last_model_wire_gen.get();
let styled_fill_source = Arc::clone(&base_arc);
let (base_arc, styled_gen) = self.display_styled_wires(base_arc, source_gen);
self.last_model_wire_gen.set(styled_gen);
// Wire-buffer content id for the upload gate. Preview / interim wires
// are NOT part of this buffer anymore (they go in a separate per-frame
// overlay buffer below), so the base id is the source's stable content
// gen — a drag or camera move never re-uploads the base wire set.
let base_wire_content_id = self.last_model_wire_gen.get();
let base_wire_content_id = styled_gen;
let base_wire_patch = self.model_wire_patch_for(base_wire_content_id);
// Split Face3D wires from the rest. The split is content-only (keyed
// by the wire-set content id), so while the geometry is unchanged it's
@@ -3548,8 +3786,15 @@ impl Scene {
width: full.width.max(1.0),
height: full.height.max(1.0),
};
let mut uniforms =
Uniforms::new(&inst.camera, full_bounds, self.document.header.lineweight_display);
let display_plot_lineweights = self.current_layout != "Model"
&& self.effective_plot_settings().is_some_and(|settings| {
settings.flags.show_plot_styles && settings.flags.print_lineweights
});
let mut uniforms = Uniforms::new(
&inst.camera,
full_bounds,
self.document.header.lineweight_display || display_plot_lineweights,
);
if self.document.header.paper_space_linetype_scaling
&& !inst.paper_sheet
&& inst.tile_idx.is_none()
@@ -3642,6 +3887,29 @@ impl Scene {
None,
)
};
let viewport_scale = if inst.paper_sheet {
1.0
} else {
self.document
.get_entity(inst.handle)
.and_then(|entity| match entity {
EntityType::Viewport(viewport) => Some(vp_effective_scale(
viewport.custom_scale,
viewport.view_height,
viewport.height,
)),
_ => None,
})
.unwrap_or(1.0)
};
let pattern_scale = (self.paper_space_unit_factor() / viewport_scale.max(1.0e-9)) as f32;
let styled_wire_fills =
self.display_styled_wire_fills(&styled_fill_source, source_gen, pattern_scale);
let hatches = self.display_styled_hatches(
hatches,
styled_wire_fills.as_ref().filter(|fills| !fills.is_empty()),
pattern_scale,
);
let images = if let Some(images) = paper_images {
images
} else {