forked from 1b-agent/opencadstudio-upstream
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:
1 parent
9c8f841f71
commit
8c61d89b1e
18 files changed
+1393
-373
No files matched your search
+127
-19
@@ -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
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user