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

+127 -19
View File
@@ -25,6 +25,7 @@ use std::io::Write;
use std::path::Path;
#[derive(Clone, Debug)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub struct PlotWire {
pub wire: WireModel,
pub draw_depth: f32,
@@ -120,6 +121,7 @@ pub struct PdfPageInput {
pub scale: f32,
pub clip: Option<(f32, f32, f32, f32)>,
pub options: PdfPlotOptions,
pub plot_style: Option<PlotStyleTable>,
}
impl Default for PdfPlotOptions {
@@ -270,7 +272,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>)
page.rotation_deg,
page.scale,
page.clip,
plot_style,
page.plot_style.as_ref().or(plot_style),
page.options,
);
}
@@ -440,6 +442,8 @@ fn append_pdf_page(
let mut last_color: Option<[f32; 3]> = None;
let mut last_lw: Option<f32> = None;
let mut last_cap = Some(LineCapStyle::Round);
let mut last_join = Some(LineJoinStyle::Round);
// 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;
@@ -453,7 +457,9 @@ fn append_pdf_page(
ox,
oy,
plot_style,
scale,
options,
normal_blend.as_ref(),
);
last_color = None;
last_lw = None;
@@ -506,6 +512,8 @@ fn append_pdf_page(
let mut lw_override: Option<f32> = None;
let mut screening = 1.0;
let mut color_overridden = false;
let mut cap = None;
let mut join = None;
if let Some(ctb) = plot_style {
if wire.aci > 0 {
if let Some([cr, cg, cb]) = ctb.resolve_color(wire.aci) {
@@ -518,6 +526,20 @@ fn append_pdf_page(
.resolve_lineweight(wire.aci)
.map(|mm| (mm * MM_TO_PT).max(0.1));
screening = ctb.resolve_screening(wire.aci);
if let Some(entry) = ctb.aci_entries.get(wire.aci as usize) {
cap = match entry.end_style {
0 => Some(LineCapStyle::Butt),
1 | 3 => Some(LineCapStyle::ProjectingSquare),
2 => Some(LineCapStyle::Round),
_ => None,
};
join = match entry.join_style {
0 => Some(LineJoinStyle::Miter),
1 | 3 => Some(LineJoinStyle::Bevel),
2 => Some(LineJoinStyle::Round),
_ => None,
};
}
}
}
// Near-white and near-yellow (viewport active border) → dark grey for print
@@ -539,6 +561,17 @@ fn append_pdf_page(
}
[r, g, b] = plotted_color([r, g, b], a, screening, options);
let cap = cap.unwrap_or(LineCapStyle::Round);
if last_cap != Some(cap) {
ops.push(Op::SetLineCapStyle { cap });
last_cap = Some(cap);
}
let join = join.unwrap_or(LineJoinStyle::Round);
if last_join != Some(join) {
ops.push(Op::SetLineJoinStyle { join });
last_join = Some(join);
}
if last_color
.map(|c| (c[0] - r).abs() > 0.01 || (c[1] - g).abs() > 0.01 || (c[2] - b).abs() > 0.01)
.unwrap_or(true)
@@ -576,13 +609,11 @@ fn append_pdf_page(
let lw_pt = if wire.world_width > 0.0 {
wire.world_width * MM_TO_PT
} else {
let physical = lw_override.unwrap_or_else(|| {
if options.object_lineweights {
(wire.line_weight_px * LW_PX_TO_PT).max(0.1)
} else {
0.1
}
});
let physical = if options.object_lineweights {
lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1))
} else {
0.1
};
physical / pen_divisor
};
if last_lw.map(|l| (l - lw_pt).abs() > 0.01).unwrap_or(true) {
@@ -852,12 +883,71 @@ fn emit_wire_fills(
ox: f64,
oy: f64,
plot_style: Option<&PlotStyleTable>,
scale: f32,
options: PdfPlotOptions,
normal_blend: Option<&ExtendedGraphicsStateId>,
) {
for wire in wires {
if wire.fill_tris.is_empty() {
continue;
}
let styled_pattern = plot_style.and_then(|table| {
(wire.aci > 0)
.then(|| table.aci_entries.get(wire.aci as usize))
.flatten()
.and_then(|entry| {
(65..=72)
.contains(&entry.fill_style)
.then(|| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
.flatten()
})
});
if let Some(pattern) = styled_pattern {
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]);
let hatch = HatchModel {
render_instance: wire.render_instance.clone(),
world_origin: [0.0, 0.0],
boundary: std::sync::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: wire.color,
aci: wire.aci,
line_weight_px: wire.line_weight_px,
angle_offset: 0.0,
scale: 1.0 / scale.max(1.0e-6),
draw_depth: wire.depth_override.unwrap_or(0.0),
};
emit_hatch(
ops,
&hatch,
ox,
oy,
plot_style,
scale,
options,
normal_blend,
);
}
continue;
}
let [mut r, mut g, mut b, a] = wire.color;
if a < 0.01 {
continue;
@@ -964,6 +1054,26 @@ fn emit_hatch(
if hatch.boundary.is_empty() {
return;
}
let mut styled_hatch = None;
if let Some(table) = plot_style {
if hatch.aci > 0 && matches!(hatch.pattern, HatchPattern::Solid) {
if let Some(pattern) = table
.aci_entries
.get(hatch.aci as usize)
.and_then(|entry| {
crate::scene::model::hatch_model::plot_style_fill_pattern(
entry.fill_style,
)
})
{
let mut model = hatch.clone();
model.pattern = pattern;
model.scale = 1.0 / scale.max(1.0e-6);
styled_hatch = Some(model);
}
}
}
let hatch = styled_hatch.as_ref().unwrap_or(hatch);
let [mut r, mut g, mut b, a] = hatch.color;
if a < 0.01 {
return;
@@ -1082,13 +1192,11 @@ fn emit_hatch(
// Pattern hatches: rasterise the family lines clipped to the boundary
// and emit each as a stroked line. Skips the polygon outline entirely.
if matches!(hatch.pattern, HatchPattern::Pattern(_)) {
let physical = lw_override.unwrap_or_else(|| {
if options.object_lineweights {
(hatch.line_weight_px * LW_PX_TO_PT).max(0.1)
} else {
0.1
}
});
let physical = if options.object_lineweights {
lw_override.unwrap_or_else(|| (hatch.line_weight_px * LW_PX_TO_PT).max(0.1))
} else {
0.1
};
let divisor = if options.scale_lineweights {
1.0
} else {
@@ -1248,16 +1356,16 @@ fn emit_text(
if let Some(ctb) = plot_style {
if wire.aci > 0 {
ctb_color = ctb.resolve_color(wire.aci);
lw_override = ctb
.resolve_lineweight(wire.aci)
.map(|mm| {
lw_override = options.object_lineweights.then(|| {
ctb.resolve_lineweight(wire.aci).map(|mm| {
let divisor = if options.scale_lineweights {
1.0
} else {
scale.max(1e-6)
};
(mm * MM_TO_PT).max(0.1) / divisor
});
})
}).flatten();
screening = ctb.resolve_screening(wire.aci);
}
}
+88 -10
View File
@@ -1,7 +1,7 @@
//! Plot Style Table — CTB (color-based) and STB (named) file support.
//!
//! CTB files map indexed drawing colors (ACI, 1-255) to pen properties:
//! RGB color override, lineweight, and screeing percentage.
//! RGB color override, lineweight, and screening percentage.
//!
//! File format: a fixed 60-byte header followed by zlib-compressed text.
//!
@@ -17,6 +17,21 @@ use std::path::PathBuf;
pub const DEFAULT_PLOT_STYLE: &str = "ocad.ctb";
pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.ctb";
pub const GRAYSCALE_PLOT_STYLE: &str = "Grayscale.ctb";
pub const FILL_PATTERNS_PLOT_STYLE: &str = "Fill Patterns.ctb";
pub const SCREENING_100_PLOT_STYLE: &str = "Screening 100%.ctb";
pub const SCREENING_75_PLOT_STYLE: &str = "Screening 75%.ctb";
pub const SCREENING_50_PLOT_STYLE: &str = "Screening 50%.ctb";
pub const SCREENING_25_PLOT_STYLE: &str = "Screening 25%.ctb";
const STANDARD_PLOT_STYLES: &[&str] = &[
GRAYSCALE_PLOT_STYLE,
FILL_PATTERNS_PLOT_STYLE,
SCREENING_100_PLOT_STYLE,
SCREENING_75_PLOT_STYLE,
SCREENING_50_PLOT_STYLE,
SCREENING_25_PLOT_STYLE,
];
#[cfg(not(target_arch = "wasm32"))]
pub fn plot_styles_dir() -> Result<PathBuf, String> {
@@ -52,10 +67,14 @@ pub fn available_ctb_names() -> Vec<String> {
#[cfg(not(target_arch = "wasm32"))]
{
let Ok(dir) = ensure_plot_styles_dir() else {
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
return names;
};
let Ok(entries) = std::fs::read_dir(dir) else {
return vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
return names;
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
@@ -67,13 +86,16 @@ pub fn available_ctb_names() -> Vec<String> {
.then(|| entry.file_name().to_string_lossy().into_owned())
})
.collect();
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
names.sort_by_key(|name| name.to_ascii_lowercase());
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
names
}
#[cfg(target_arch = "wasm32")]
{
vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()]
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
names
}
}
@@ -199,10 +221,44 @@ impl PlotStyleTable {
MONOCHROME_PLOT_STYLE,
include_bytes!("../../assets/plotstyles/monochrome.ctb"),
),
"grayscale.ctb" => {
let mut table = Self::identity(GRAYSCALE_PLOT_STYLE);
for aci in 1..=255u8 {
if let Some((r, g, b)) = acadrust::types::aci_to_rgb(aci) {
let gray = if aci == 7 {
0
} else {
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32)
.round() as u8
};
table.aci_entries[aci as usize].color = Some([gray, gray, gray]);
}
}
Ok(table)
}
"fill patterns.ctb" => {
let mut table = Self::identity(FILL_PATTERNS_PLOT_STYLE);
for aci in 1..=9usize {
table.aci_entries[aci].fill_style = 63 + aci as u8;
}
Ok(table)
}
"screening 100%.ctb" => Ok(Self::screening(SCREENING_100_PLOT_STYLE, 100)),
"screening 75%.ctb" => Ok(Self::screening(SCREENING_75_PLOT_STYLE, 75)),
"screening 50%.ctb" => Ok(Self::screening(SCREENING_50_PLOT_STYLE, 50)),
"screening 25%.ctb" => Ok(Self::screening(SCREENING_25_PLOT_STYLE, 25)),
_ => Err(format!("Unknown built-in plot style: {name}")),
}
}
fn screening(name: &str, percent: u8) -> Self {
let mut table = Self::identity(name);
for entry in table.aci_entries.iter_mut().skip(1) {
entry.screening = percent;
}
table
}
/// Load one CTB by file name from the per-user plot styles folder.
pub fn load_named(name: &str) -> Result<Self, String> {
let path = Path::new(name);
@@ -227,9 +283,11 @@ impl PlotStyleTable {
.to_string_lossy()
.eq_ignore_ascii_case(name)
})
.map(|entry| entry.path())
.ok_or_else(|| format!("Plot style not found: {name}"))?;
return Self::load(&matched);
.map(|entry| entry.path());
return match matched {
Some(path) => Self::load(&path),
None => Self::builtin(name),
};
}
#[cfg(target_arch = "wasm32")]
@@ -248,9 +306,23 @@ impl PlotStyleTable {
/// Returns None if no override (use object color).
pub fn resolve_color(&self, aci: u8) -> Option<[f32; 3]> {
let entry = self.aci_entries.get(aci as usize)?;
entry
let mut color = entry
.color
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
.map(|[r, g, b]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]);
if entry.color_policy & 2 != 0 {
let rgb = color.or_else(|| {
if aci == 7 {
Some([0.0; 3])
} else {
acadrust::types::aci_to_rgb(aci).map(|(r, g, b)| {
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
})
}
})?;
let gray = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
color = Some([gray; 3]);
}
color
}
/// Resolve the effective lineweight in mm for the given ACI index.
@@ -260,7 +332,13 @@ impl PlotStyleTable {
if matches!(entry.lineweight, 0 | 255) {
None
} else {
self.lineweights.get(entry.lineweight as usize).copied()
self.lineweights.get(entry.lineweight as usize).copied().map(|weight| {
if self.apply_factor {
weight * self.scale_factor.max(0.0)
} else {
weight
}
})
}
}
+5
View File
@@ -29,6 +29,11 @@ pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
))
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn temp_pdf_path(kind: &str) -> std::path::PathBuf {
std::path::PathBuf::from(format!("{kind}.pdf"))
}
/// 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
/// options. Windows queues repeated jobs when more than one copy is requested;