mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-15 06:46:45 -04:00
A PlotStyleTable model in doc with our own built-in tables (default.ctb keeps object properties, monochrome.ctb prints black, grayscale.ctb prints the grey of each colour's luminance). The render builder resolves each object's style by its index colour (true colours by the nearest index) and applies colour, screening, linetype and lineweight. PLOT/EXPORTPDF use the page setup's table (or the plotStyleTable option); layouts with Display plot styles (PAGESETUP displayPlotStyles, DXF plot flag 2) show it on screen and in PNG/SVG exports. Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
f537377355
commit
ee34f423cc
15 files changed
+638
-15
No files matched your search
@@ -11,6 +11,7 @@ mod entity;
|
||||
mod extents;
|
||||
mod header;
|
||||
pub mod library;
|
||||
mod plotstyle;
|
||||
mod store;
|
||||
mod tables;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub use constraint::*;
|
||||
pub use entity::*;
|
||||
pub use extents::{MAX_BLOCK_DEPTH, entity_bounds};
|
||||
pub use header::{HVal, Header};
|
||||
pub use plotstyle::*;
|
||||
pub use store::EntityStore;
|
||||
pub use tables::*;
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
//! Plot style tables: how objects print.
|
||||
//!
|
||||
//! A colour-dependent table (`.ctb`) has one plot style per index colour 1..=255: an object
|
||||
//! prints with the style of its colour. A named table (`.stb`) holds styles that layers and
|
||||
//! objects name directly. Each style can replace the object's colour, fade it (screening), turn
|
||||
//! it grey, and replace its linetype and lineweight.
|
||||
//!
|
||||
//! CADCraft's built-in tables are our own definitions, not copies of any vendor's files:
|
||||
//! [`DEFAULT_CTB`] keeps the object's properties, [`MONOCHROME_CTB`] prints everything black
|
||||
//! and [`GRAYSCALE_CTB`] prints every colour as the grey of its luminance.
|
||||
|
||||
use cadcraft_color::Rgb;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Built-in colour-dependent table that prints objects with their own properties.
|
||||
pub const DEFAULT_CTB: &str = "default.ctb";
|
||||
/// Built-in colour-dependent table that prints everything black.
|
||||
pub const MONOCHROME_CTB: &str = "monochrome.ctb";
|
||||
/// Built-in colour-dependent table that prints every colour as a grey of its luminance.
|
||||
pub const GRAYSCALE_CTB: &str = "grayscale.ctb";
|
||||
|
||||
/// The names of the built-in tables.
|
||||
pub const BUILTIN_PLOT_STYLE_TABLES: &[&str] = &[DEFAULT_CTB, MONOCHROME_CTB, GRAYSCALE_CTB];
|
||||
|
||||
/// Number of styles in a colour-dependent table (one per index colour 1..=255).
|
||||
pub const CTB_STYLES: usize = 255;
|
||||
|
||||
/// Line end style of a plot style.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EndStyle {
|
||||
#[default]
|
||||
Object,
|
||||
Butt,
|
||||
Square,
|
||||
Round,
|
||||
Diamond,
|
||||
}
|
||||
|
||||
/// Line join style of a plot style.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum JoinStyle {
|
||||
#[default]
|
||||
Object,
|
||||
Miter,
|
||||
Bevel,
|
||||
Round,
|
||||
Diamond,
|
||||
}
|
||||
|
||||
/// Fill style of a plot style (for filled areas).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FillStyle {
|
||||
#[default]
|
||||
Object,
|
||||
Solid,
|
||||
Checkerboard,
|
||||
Crosshatch,
|
||||
Diamonds,
|
||||
HorizontalBars,
|
||||
SlantLeft,
|
||||
SlantRight,
|
||||
SquareDots,
|
||||
VerticalBars,
|
||||
}
|
||||
|
||||
/// One plot style.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct PlotStyle {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// Output colour; `None` uses the object's colour.
|
||||
pub color: Option<Rgb>,
|
||||
/// Print the colour as the grey of its luminance.
|
||||
pub grayscale: bool,
|
||||
/// Ink intensity in percent, 0 (white) to 100 (full colour).
|
||||
pub screening: u8,
|
||||
/// Linetype name; `None` uses the object's linetype.
|
||||
pub linetype: Option<String>,
|
||||
/// Lineweight in hundredths of a millimetre (0..=211); `None` uses the object's lineweight.
|
||||
pub lineweight: Option<u16>,
|
||||
pub end: EndStyle,
|
||||
pub join: JoinStyle,
|
||||
pub fill: FillStyle,
|
||||
}
|
||||
|
||||
impl Default for PlotStyle {
|
||||
fn default() -> Self {
|
||||
PlotStyle::object("")
|
||||
}
|
||||
}
|
||||
|
||||
impl PlotStyle {
|
||||
/// A style that keeps every object property.
|
||||
pub fn object(name: &str) -> PlotStyle {
|
||||
PlotStyle {
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
color: None,
|
||||
grayscale: false,
|
||||
screening: 100,
|
||||
linetype: None,
|
||||
lineweight: None,
|
||||
end: EndStyle::Object,
|
||||
join: JoinStyle::Object,
|
||||
fill: FillStyle::Object,
|
||||
}
|
||||
}
|
||||
/// Whether the style leaves the colour alone.
|
||||
pub fn keeps_color(&self) -> bool {
|
||||
self.color.is_none() && !self.grayscale && self.screening >= 100
|
||||
}
|
||||
/// The printed colour of an object drawn in `rgb`: the style's colour (else `rgb`), turned
|
||||
/// grey when asked, then screened toward white.
|
||||
pub fn apply_color(&self, rgb: Rgb) -> Rgb {
|
||||
let c = self.color.unwrap_or(rgb);
|
||||
let c = if self.grayscale {
|
||||
let g = (c.luma().clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
Rgb(g, g, g)
|
||||
} else {
|
||||
c
|
||||
};
|
||||
let s = u16::from(self.screening.min(100));
|
||||
// Screening mixes the ink with the white paper: 100 % = full ink, 0 % = white.
|
||||
let screen = |v: u8| (255 - (255 - u16::from(v)) * s / 100) as u8;
|
||||
Rgb(screen(c.0), screen(c.1), screen(c.2))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a table maps colours or names to styles.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum PlotStyleKind {
|
||||
/// Colour-dependent (`.ctb`): style `i - 1` applies to index colour `i`.
|
||||
#[default]
|
||||
ColorDependent,
|
||||
/// Named (`.stb`): styles are picked by name.
|
||||
Named,
|
||||
}
|
||||
|
||||
/// A plot style table.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct PlotStyleTable {
|
||||
/// File name, such as `monochrome.ctb`.
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub kind: PlotStyleKind,
|
||||
pub styles: Vec<PlotStyle>,
|
||||
}
|
||||
|
||||
impl PlotStyleTable {
|
||||
/// A colour-dependent table whose style for index colour `i` is `f(i)` (else object
|
||||
/// properties), named `Color_i`.
|
||||
pub fn color_dependent(name: &str, description: &str, f: impl Fn(u8) -> Option<PlotStyle>) -> PlotStyleTable {
|
||||
let styles = (1..=255u8)
|
||||
.map(|i| {
|
||||
let label = format!("Color_{i}");
|
||||
let mut s = f(i).unwrap_or_else(|| PlotStyle::object(&label));
|
||||
s.name = label;
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
PlotStyleTable { name: name.into(), description: description.into(), kind: PlotStyleKind::ColorDependent, styles }
|
||||
}
|
||||
|
||||
/// A built-in table by name (case-insensitive; the extension may be left out).
|
||||
pub fn builtin(name: &str) -> Option<PlotStyleTable> {
|
||||
let n = name.trim().to_ascii_lowercase();
|
||||
let n = if n.ends_with(".ctb") { n } else { format!("{n}.ctb") };
|
||||
match n.as_str() {
|
||||
DEFAULT_CTB => Some(PlotStyleTable::color_dependent(DEFAULT_CTB, "Object colours, linetypes and lineweights", |_| None)),
|
||||
MONOCHROME_CTB => Some(PlotStyleTable::color_dependent(MONOCHROME_CTB, "Every colour prints black", |_| {
|
||||
Some(PlotStyle { color: Some(Rgb(0, 0, 0)), ..PlotStyle::default() })
|
||||
})),
|
||||
GRAYSCALE_CTB => Some(PlotStyleTable::color_dependent(GRAYSCALE_CTB, "Every colour prints as the grey of its luminance", |_| {
|
||||
Some(PlotStyle { grayscale: true, ..PlotStyle::default() })
|
||||
})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this table is colour-dependent.
|
||||
pub fn is_color_dependent(&self) -> bool {
|
||||
self.kind == PlotStyleKind::ColorDependent
|
||||
}
|
||||
|
||||
/// The style for index colour `aci` (1..=255) of a colour-dependent table.
|
||||
pub fn by_color(&self, aci: u8) -> Option<&PlotStyle> {
|
||||
if !self.is_color_dependent() || aci == 0 {
|
||||
return None;
|
||||
}
|
||||
self.styles.get(usize::from(aci) - 1)
|
||||
}
|
||||
|
||||
/// The style called `name` (case-insensitive).
|
||||
pub fn by_name(&self, name: &str) -> Option<&PlotStyle> {
|
||||
self.styles.iter().find(|s| s.name.eq_ignore_ascii_case(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a page setup's table name means "no plot style table".
|
||||
pub fn no_plot_style_table(name: &str) -> bool {
|
||||
let n = name.trim();
|
||||
n.is_empty() || n.eq_ignore_ascii_case("none")
|
||||
}
|
||||
|
||||
/// The plot style table a page setup names, if it is one we know; `None` for no table
|
||||
/// (empty or "None") and for tables we don't have.
|
||||
pub fn plot_style_table(_d: &crate::Drawing, name: &str) -> Option<PlotStyleTable> {
|
||||
if no_plot_style_table(name) {
|
||||
return None;
|
||||
}
|
||||
PlotStyleTable::builtin(name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builtins_are_complete_color_dependent_tables() {
|
||||
for n in BUILTIN_PLOT_STYLE_TABLES {
|
||||
let t = PlotStyleTable::builtin(n).unwrap();
|
||||
assert_eq!(&t.name, n);
|
||||
assert!(t.is_color_dependent());
|
||||
assert_eq!(t.styles.len(), CTB_STYLES);
|
||||
assert_eq!(t.by_color(1).unwrap().name, "Color_1");
|
||||
assert_eq!(t.by_color(255).unwrap().name, "Color_255");
|
||||
assert!(t.by_color(0).is_none());
|
||||
}
|
||||
assert!(PlotStyleTable::builtin("Monochrome").is_some());
|
||||
assert!(PlotStyleTable::builtin("nope.ctb").is_none());
|
||||
assert!(no_plot_style_table("") && no_plot_style_table(" None ") && !no_plot_style_table("monochrome.ctb"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colour_transforms() {
|
||||
let mono = PlotStyleTable::builtin(MONOCHROME_CTB).unwrap();
|
||||
assert_eq!(mono.by_color(1).unwrap().apply_color(Rgb(255, 0, 0)), Rgb(0, 0, 0));
|
||||
let gray = PlotStyleTable::builtin(GRAYSCALE_CTB).unwrap();
|
||||
let g = gray.by_color(3).unwrap().apply_color(Rgb(0, 255, 0));
|
||||
assert!(g.0 == g.1 && g.1 == g.2 && g.0 > 128, "{g:?}");
|
||||
let half = PlotStyle { color: Some(Rgb(0, 0, 0)), screening: 50, ..PlotStyle::default() };
|
||||
assert_eq!(half.apply_color(Rgb(9, 9, 9)), Rgb(128, 128, 128));
|
||||
let none = PlotStyle { screening: 0, ..PlotStyle::default() };
|
||||
assert_eq!(none.apply_color(Rgb(10, 20, 30)), Rgb(255, 255, 255));
|
||||
assert!(PlotStyle::default().keeps_color() && !half.keeps_color());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_round_trip_with_defaults() {
|
||||
let t = PlotStyleTable::builtin(GRAYSCALE_CTB).unwrap();
|
||||
let back: PlotStyleTable = serde_json::from_value(serde_json::to_value(&t).unwrap()).unwrap();
|
||||
assert_eq!(back, t);
|
||||
let s: PlotStyle = serde_json::from_value(serde_json::json!({"name": "Thick", "lineweight": 50})).unwrap();
|
||||
assert_eq!((s.screening, s.lineweight, s.color), (100, Some(50), None));
|
||||
}
|
||||
}
|
||||
@@ -538,6 +538,9 @@ pub struct PageSetup {
|
||||
pub plot_style_table: String,
|
||||
pub center: bool,
|
||||
pub lineweights: bool,
|
||||
/// Draw the layout on screen (and in its PNG/SVG exports) with its plot style table
|
||||
/// applied ("Display plot styles"; DXF plot layout flag 2).
|
||||
pub show_plot_styles: bool,
|
||||
}
|
||||
|
||||
impl Default for PageSetup {
|
||||
@@ -555,6 +558,7 @@ impl Default for PageSetup {
|
||||
plot_style_table: String::new(),
|
||||
center: false,
|
||||
lineweights: true,
|
||||
show_plot_styles: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("pagesetup", "Page Setup Manager...", run_pagesetup)
|
||||
.menu(&["File", "Page Setup Manager..."])
|
||||
.params(
|
||||
"{layout?: current, paper?: \"A4\"|\"A3\"|\"Letter\"|\"ANSI B\"|…, width?, height? (mm), landscape?, margins?: [l,b,r,t] mm, lineweights?, plotArea?, scale?, scaleToFit?, center?, plotStyleTable?}",
|
||||
"{layout?: current, paper?: \"A4\"|\"A3\"|\"Letter\"|\"ANSI B\"|…, width?, height? (mm), landscape?, margins?: [l,b,r,t] mm, lineweights?, plotArea?, scale?, scaleToFit?, center?, plotStyleTable?: \"monochrome.ctb\"|\"grayscale.ctb\"|\"default.ctb\"|\"None\"|…, displayPlotStyles?: bool}",
|
||||
),
|
||||
CommandSpec::new("plot", "Print...", |s, p| run_plot(s, p, "plot"))
|
||||
.menu(&["File", "Print..."])
|
||||
@@ -692,7 +692,20 @@ fn run_viewport_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "pagesetup";
|
||||
let keys = ["paper", "width", "height", "landscape", "margins", "lineweights", "plotArea", "scale", "scaleToFit", "center", "plotStyleTable"];
|
||||
let keys = [
|
||||
"paper",
|
||||
"width",
|
||||
"height",
|
||||
"landscape",
|
||||
"margins",
|
||||
"lineweights",
|
||||
"plotArea",
|
||||
"scale",
|
||||
"scaleToFit",
|
||||
"center",
|
||||
"plotStyleTable",
|
||||
"displayPlotStyles",
|
||||
];
|
||||
let changes = keys.iter().any(|k| p.get(k).is_some());
|
||||
let name = match (str_param(p, "layout"), s.space()) {
|
||||
(Some(n), _) => Some(layout_name(cmd, s.doc()?, n)?),
|
||||
@@ -708,6 +721,7 @@ fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Ok(json!({
|
||||
"layouts": d.layouts.iter().map(|l| json!({"name": l.name, "page": serde_json::to_value(&l.page).unwrap_or(Value::Null)})).collect::<Vec<_>>(),
|
||||
"papers": cadcraft_render::PAPER_SIZES.iter().map(|p| p.name).collect::<Vec<_>>(),
|
||||
"plotStyleTables": cadcraft_doc::BUILTIN_PLOT_STYLE_TABLES,
|
||||
}));
|
||||
};
|
||||
let mut page = s.doc()?.layout(&name).map(|l| l.page.clone()).unwrap_or_default();
|
||||
@@ -763,6 +777,9 @@ fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = str_param(p, "plotStyleTable") {
|
||||
page.plot_style_table = v.chars().take(260).collect();
|
||||
}
|
||||
if let Some(v) = p.get("displayPlotStyles").and_then(Value::as_bool) {
|
||||
page.show_plot_styles = v;
|
||||
}
|
||||
if changes {
|
||||
if let Some(l) = s.doc_mut()?.layouts.iter_mut().find(|l| l.name == name) {
|
||||
l.page = page.clone();
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//! PAGESETUP: the plot style table and "Display plot styles" of a layout (issue #410).
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn pagesetup_sets_table_and_display_flag() {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("pagesetup", &json!({"layout": "Layout1", "plotStyleTable": "monochrome.ctb", "displayPlotStyles": true})).unwrap();
|
||||
assert_eq!(r["page"]["plotStyleTable"], "monochrome.ctb");
|
||||
assert_eq!(r["page"]["showPlotStyles"], true);
|
||||
let page = &s.doc().unwrap().layout("Layout1").unwrap().page;
|
||||
assert!(page.show_plot_styles && page.plot_style_table == "monochrome.ctb");
|
||||
// The report lists the built-in tables.
|
||||
let report = s.execute("pagesetup", &json!({})).unwrap();
|
||||
let tables: Vec<&str> = report["plotStyleTables"].as_array().unwrap().iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(tables.contains(&"monochrome.ctb") && tables.contains(&"grayscale.ctb"), "{tables:?}");
|
||||
s.execute("pagesetup", &json!({"layout": "Layout1", "displayPlotStyles": false})).unwrap();
|
||||
assert!(!s.doc().unwrap().layout("Layout1").unwrap().page.show_plot_styles);
|
||||
}
|
||||
@@ -1686,8 +1686,10 @@ fn page_setup(t: &T) -> PageSetup {
|
||||
if let Some(dev) = t.s(2).map(|s| s.trim().to_string()) {
|
||||
p.device = if dev.is_empty() || dev.eq_ignore_ascii_case("none_device") { "None".into() } else { dev };
|
||||
}
|
||||
// Plot layout flags: 4 centred, 16 standard scale (75 = 0: scaled to fit), 128 lineweights.
|
||||
// Plot layout flags: 2 display plot styles, 4 centred, 16 standard scale (75 = 0: scaled
|
||||
// to fit), 128 lineweights.
|
||||
if let Some(flags) = t.i(70) {
|
||||
p.show_plot_styles = flags & 2 != 0;
|
||||
p.center = flags & 4 != 0;
|
||||
p.lineweights = flags & 128 != 0;
|
||||
p.scale_to_fit = flags & 16 != 0 && t.i(75) == Some(0);
|
||||
|
||||
@@ -1859,7 +1859,12 @@ pub fn write(d: &Drawing) -> String {
|
||||
// per 143 drawing units).
|
||||
let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 };
|
||||
let standard = page.scale_to_fit || scale == 1.0;
|
||||
let plot_flags = 512 | 32 | if standard { 16 } else { 0 } | if page.lineweights { 128 } else { 0 } | if page.center { 4 } else { 0 };
|
||||
let plot_flags = 512
|
||||
| 32
|
||||
| if standard { 16 } else { 0 }
|
||||
| if page.lineweights { 128 } else { 0 }
|
||||
| if page.center { 4 } else { 0 }
|
||||
| if page.show_plot_styles { 2 } else { 0 };
|
||||
let plot_type = match page.plot_area.as_str() {
|
||||
"display" => 0,
|
||||
"extents" => 1,
|
||||
|
||||
+10
-2
@@ -3,7 +3,8 @@
|
||||
//! One page per plot: model-space extents fitted to a sheet, or a layout at 1:1 on its paper
|
||||
//! (paper units are millimetres in metric drawings, inches otherwise). Lines are stroked paths
|
||||
//! (`m`/`l`/`S`), fills are `f` paths, colours are RGB with colour 7 printing black on white
|
||||
//! paper, and line widths come from lineweights when enabled.
|
||||
//! paper, and line widths come from lineweights when enabled. The page setup's plot style
|
||||
//! table (or the `plotStyleTable` option) sets the printed colours, linetypes and lineweights.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
@@ -24,6 +25,8 @@ const MAX_SHEET_MM: f64 = 5080.0;
|
||||
/// model space).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PdfOptions {
|
||||
/// Plot style table name, replacing the page setup's (`"None"` plots without one).
|
||||
pub plot_style_table: Option<String>,
|
||||
/// Paper name from [`cadcraft_render::PAPER_SIZES`] (`"A4"`, `"Letter"`, `"ANSI B"`…).
|
||||
pub paper: Option<String>,
|
||||
/// Custom paper size in millimetres (portrait width, height).
|
||||
@@ -52,6 +55,7 @@ impl PdfOptions {
|
||||
_ => None,
|
||||
};
|
||||
PdfOptions {
|
||||
plot_style_table: v.get("plotStyleTable").and_then(Value::as_str).map(|s| s.chars().take(260).collect()),
|
||||
paper: v.get("paper").and_then(Value::as_str).map(str::to_string),
|
||||
paper_mm,
|
||||
landscape: v.get("landscape").and_then(Value::as_bool),
|
||||
@@ -100,6 +104,9 @@ pub fn page_for(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<PageSetup>
|
||||
if let Some(l) = o.landscape {
|
||||
page.landscape = l;
|
||||
}
|
||||
if let Some(t) = &o.plot_style_table {
|
||||
page.plot_style_table = t.clone();
|
||||
}
|
||||
page.width_mm = page.width_mm.clamp(1.0, MAX_SHEET_MM);
|
||||
page.height_mm = page.height_mm.clamp(1.0, MAX_SHEET_MM);
|
||||
Ok(page)
|
||||
@@ -130,7 +137,8 @@ pub fn pdf(d: &Drawing, space: &Space, o: &PdfOptions) -> Result<Vec<u8>> {
|
||||
Space::Paper(_) => page.lineweights,
|
||||
Space::Model => true,
|
||||
});
|
||||
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights, view_height: 0.0 };
|
||||
let plot_style_table = cadcraft_doc::plot_style_table(d, &page.plot_style_table).map(std::sync::Arc::new);
|
||||
let ropts = cadcraft_render::Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights, view_height: 0.0, plot_style_table };
|
||||
let k = unit_mm * PT_PER_MM;
|
||||
let window = o.window.filter(|w| matches!(space, Space::Model) && !w.is_empty());
|
||||
let fit = window.is_some() || o.fit.unwrap_or(matches!(space, Space::Model));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Plot style tables in plots and exports (issue #410): the PDF content stream carries the
|
||||
//! table's colours and pen widths, layout SVG exports follow "Display plot styles", and the
|
||||
//! page setup's table and flag survive a DXF round trip.
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Line, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_io::{read_dxf, svg, write_dxf};
|
||||
use serde_json::json;
|
||||
|
||||
fn layout_with_red_line() -> Drawing {
|
||||
let mut d = Drawing::new_metric();
|
||||
let line = EntityKind::Line(Line { a: Vec3::new(10.0, 10.0, 0.0), b: Vec3::new(100.0, 10.0, 0.0) });
|
||||
d.add(&Space::Paper("Layout1".into()), Common { color: Color::Index(1), ..Default::default() }, line).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// The uncompressed PDF of Layout1 with extra plot options.
|
||||
fn pdf_text(d: &Drawing, extra: serde_json::Value) -> String {
|
||||
let mut opts = json!({"compress": false});
|
||||
if let (Some(o), Some(e)) = (opts.as_object_mut(), extra.as_object()) {
|
||||
o.extend(e.clone());
|
||||
}
|
||||
let bytes = cadcraft_io::plot(d, &Space::Paper("Layout1".into()), &opts).unwrap();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_uses_the_page_setup_table() {
|
||||
let mut d = layout_with_red_line();
|
||||
assert!(pdf_text(&d, json!({})).contains("1 0 0 RG"), "no table: red");
|
||||
d.layouts[0].page.plot_style_table = "monochrome.ctb".into();
|
||||
let mono = pdf_text(&d, json!({}));
|
||||
assert!(mono.contains("0 0 0 RG") && !mono.contains("1 0 0 RG"), "{mono}");
|
||||
// The plot's own option replaces the page setup's; "None" plots object colours.
|
||||
assert!(pdf_text(&d, json!({"plotStyleTable": "None"})).contains("1 0 0 RG"));
|
||||
let gray = pdf_text(&d, json!({"plotStyleTable": "grayscale.ctb"}));
|
||||
assert!(!gray.contains("1 0 0 RG") && !gray.contains("0 0 0 RG"), "red prints mid grey: {gray}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_svg_follows_display_plot_styles() {
|
||||
let mut d = layout_with_red_line();
|
||||
d.layouts[0].page.plot_style_table = "monochrome.ctb".into();
|
||||
let space = Space::Paper("Layout1".into());
|
||||
assert!(svg::export(&d, &space).contains("#ff0000"));
|
||||
d.layouts[0].page.show_plot_styles = true;
|
||||
let s = svg::export(&d, &space);
|
||||
assert!(!s.contains("#ff0000") && s.contains("#000000"), "{s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_setup_table_and_display_flag_round_trip() {
|
||||
let mut d = layout_with_red_line();
|
||||
d.layouts[0].page.plot_style_table = "grayscale.ctb".into();
|
||||
d.layouts[0].page.show_plot_styles = true;
|
||||
let back = read_dxf(write_dxf(&d).as_bytes()).unwrap();
|
||||
let page = &back.layout("Layout1").unwrap().page;
|
||||
assert_eq!(page.plot_style_table, "grayscale.ctb");
|
||||
assert!(page.show_plot_styles);
|
||||
assert!(!read_dxf(write_dxf(&layout_with_red_line()).as_bytes()).unwrap().layout("Layout1").unwrap().page.show_plot_styles);
|
||||
}
|
||||
@@ -120,7 +120,9 @@ fn gradient(b: &mut Builder, ctx: &Ctx, g: &Gradient, layer: Color, tris: &[Vec2
|
||||
/// `a` blended towards `b` by `t`; colour 7 only when both ends are.
|
||||
fn mix(a: Ink, b: Ink, t: f64) -> Ink {
|
||||
let l = |x: u8, y: u8| (f64::from(x) + (f64::from(y) - f64::from(x)) * t).round().clamp(0.0, 255.0) as u8;
|
||||
Ink { rgb: Rgb(l(a.rgb.0, b.rgb.0), l(a.rgb.1, b.rgb.1), l(a.rgb.2, b.rgb.2)), aci7: a.aci7 && b.aci7 }
|
||||
// The plot style follows the nearer end's colour.
|
||||
let aci = if t < 0.5 { a.aci } else { b.aci };
|
||||
Ink { rgb: Rgb(l(a.rgb.0, b.rgb.0), l(a.rgb.1, b.rgb.1), l(a.rgb.2, b.rgb.2)), aci7: a.aci7 && b.aci7, aci }
|
||||
}
|
||||
|
||||
/// Gradient shapes by name (the `INV` forms swap the two colours).
|
||||
|
||||
@@ -14,6 +14,7 @@ mod hatch;
|
||||
mod linetype;
|
||||
mod linetype_text;
|
||||
pub mod paper;
|
||||
mod plotstyle;
|
||||
mod point;
|
||||
pub mod raster;
|
||||
pub mod units;
|
||||
@@ -82,11 +83,13 @@ impl DPrim {
|
||||
}
|
||||
}
|
||||
|
||||
/// A resolved colour: its RGB and whether it is colour 7.
|
||||
/// A resolved colour: its RGB, whether it is colour 7, and its index colour (0 = a true
|
||||
/// colour), which picks the style of a colour-dependent plot style table.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct Ink {
|
||||
rgb: Rgb,
|
||||
aci7: bool,
|
||||
aci: u8,
|
||||
}
|
||||
|
||||
/// Resolve `c` like [`Color::resolve`], noting whether it comes out as colour 7.
|
||||
@@ -98,7 +101,17 @@ fn ink(c: Color, layer: Color, block: Color) -> Ink {
|
||||
Color::ByBlock => is7(block),
|
||||
c => c == Color::Index(7),
|
||||
};
|
||||
Ink { rgb: c.resolve(layer, block), aci7 }
|
||||
let aci = match c {
|
||||
Color::ByLayer => layer,
|
||||
Color::ByBlock => block,
|
||||
c => c,
|
||||
};
|
||||
let aci = match aci {
|
||||
Color::Index(i) => i,
|
||||
Color::True(_) => 0,
|
||||
Color::ByLayer | Color::ByBlock => 7,
|
||||
};
|
||||
Ink { rgb: c.resolve(layer, block), aci7, aci }
|
||||
}
|
||||
|
||||
impl DisplayList {
|
||||
@@ -129,11 +142,14 @@ pub struct Options {
|
||||
/// Height of the visible area in world units of the space being built. Relative point
|
||||
/// sizes (`PDSIZE` <= 0) are a percentage of it; 0 = the height of the space's extents.
|
||||
pub view_height: f64,
|
||||
/// Plot style table to draw with. When `None`, [`build`] of a layout whose page setup has
|
||||
/// "Display plot styles" on uses the layout's table; [`build_plot`] uses none.
|
||||
pub plot_style_table: Option<std::sync::Arc<cadcraft_doc::PlotStyleTable>>,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0 }
|
||||
Options { tolerance: 0.001, min_dash: 0.0, text: true, fill: true, lineweights: false, view_height: 0.0, plot_style_table: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +188,8 @@ struct Builder<'a> {
|
||||
opts: &'a Options,
|
||||
/// Plotting: skip layers marked "do not plot".
|
||||
plotting: bool,
|
||||
/// The plot style table drawn with.
|
||||
styles: Option<&'a plotstyle::Styler>,
|
||||
}
|
||||
|
||||
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
|
||||
@@ -181,10 +199,18 @@ struct Builder<'a> {
|
||||
pub const MAX_BLOCK_EXPANSION: usize = 2_000_000;
|
||||
|
||||
impl Builder<'_> {
|
||||
/// `color` and `lw` as the plot style table prints them.
|
||||
fn styled(&self, color: Ink, lw: f32) -> (Ink, f32) {
|
||||
match self.styles {
|
||||
Some(s) => s.apply(color, lw, self.opts.lineweights),
|
||||
None => (color, lw),
|
||||
}
|
||||
}
|
||||
fn polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) {
|
||||
if pts.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let (color, lw) = self.styled(color, lw);
|
||||
let start = self.list.verts.len() as u32;
|
||||
for p in pts {
|
||||
let q = ctx.xf.apply(*p);
|
||||
@@ -197,6 +223,7 @@ impl Builder<'_> {
|
||||
if tris.len() < 3 {
|
||||
return;
|
||||
}
|
||||
let (color, _) = self.styled(color, 0.0);
|
||||
let start = self.list.tris.len() as u32;
|
||||
for p in tris {
|
||||
let q = ctx.xf.apply(*p);
|
||||
@@ -226,6 +253,7 @@ impl Builder<'_> {
|
||||
}
|
||||
}
|
||||
fn point(&mut self, ctx: &Ctx, color: Ink, p: Vec2) {
|
||||
let (color, _) = self.styled(color, 0.0);
|
||||
let q = ctx.xf.apply(p);
|
||||
self.list.bounds.add(q);
|
||||
let start = self.list.verts.len() as u32;
|
||||
@@ -233,6 +261,7 @@ impl Builder<'_> {
|
||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Point, start, len: 1 });
|
||||
}
|
||||
fn infinite(&mut self, ctx: &Ctx, color: Ink, base: Vec2, dir: Vec2, ray: bool) {
|
||||
let (color, _) = self.styled(color, 0.0);
|
||||
let start = self.list.verts.len() as u32;
|
||||
self.list.verts.push(ctx.xf.apply(base));
|
||||
self.list.verts.push(ctx.xf.apply_vec(dir).normalized());
|
||||
@@ -274,7 +303,16 @@ fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_c
|
||||
|
||||
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
|
||||
let opts = &with_point_view(d, space, opts);
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting };
|
||||
let table = opts.plot_style_table.clone().or_else(|| match space {
|
||||
Space::Paper(n) if !plotting => d
|
||||
.layout(n)
|
||||
.filter(|l| l.page.show_plot_styles)
|
||||
.and_then(|l| cadcraft_doc::plot_style_table(d, &l.page.plot_style_table))
|
||||
.map(std::sync::Arc::new),
|
||||
_ => None,
|
||||
});
|
||||
let styler = table.map(|t| plotstyle::Styler::new(d, t));
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref() };
|
||||
if let Space::Paper(name) = space {
|
||||
b.list.sheet = paper::sheet(d, name);
|
||||
}
|
||||
@@ -340,7 +378,7 @@ fn viewport(b: &mut Builder, d: &Drawing, e: &Entity, vp: &cadcraft_doc::Viewpor
|
||||
let mhalf = half / s;
|
||||
// Model window with slack: entity bounds are approximate (text, dimensions).
|
||||
let win = Bounds2::new(vp.view_center - mhalf, vp.view_center + mhalf).expand(mhalf.x.max(mhalf.y) * 0.1);
|
||||
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting };
|
||||
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting, styles: b.styles };
|
||||
// PSLTSCALE on: model-space dashes are scaled so they measure the same on paper as in
|
||||
// paper space, whatever the viewport scale.
|
||||
let lt_factor = if d.header.i64("PSLTSCALE", 1) != 0 { 1.0 / s } else { 1.0 };
|
||||
@@ -425,7 +463,7 @@ pub fn build_entities<'a, I: IntoIterator<Item = &'a Entity>>(d: &Drawing, ents:
|
||||
} else {
|
||||
opts
|
||||
};
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting: false };
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None };
|
||||
for e in ents {
|
||||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
@@ -479,6 +517,11 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
if !visible {
|
||||
return;
|
||||
}
|
||||
// A plot style's linetype replaces the object's.
|
||||
let lt = match b.styles.and_then(|s| s.linetype(rgb)) {
|
||||
Some(over) => over,
|
||||
None => lt,
|
||||
};
|
||||
let tol = b.opts.tolerance / ctx.xf.scale_factor().max(1e-12);
|
||||
let lw = if b.opts.lineweights { lw } else { 0.0 };
|
||||
// Stroke a polyline with the entity's linetype.
|
||||
@@ -736,7 +779,7 @@ fn mtext_color(c: Option<cadcraft_fonts::MTextColor>, rgb: Ink) -> Ink {
|
||||
u8::try_from(i).ok().filter(|i| *i > 0).map(|i| ink(Color::Index(i), Color::Index(7), Color::Index(7))).unwrap_or(rgb)
|
||||
}
|
||||
Some(cadcraft_fonts::MTextColor::Rgb(v)) => {
|
||||
Ink { rgb: Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), aci7: false }
|
||||
Ink { rgb: Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), aci7: false, aci: 0 }
|
||||
}
|
||||
None => rgb,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//! Plot style resolution: a plot style table applied to the display list while it is built.
|
||||
//!
|
||||
//! Plots (PDF) and layouts shown with "Display plot styles" (screen, PNG and SVG) draw each
|
||||
//! object with the plot style that applies to it. In a colour-dependent table that is the style
|
||||
//! of the object's index colour; a true colour uses the style of its nearest index colour. The
|
||||
//! style replaces or keeps the colour, turns it grey and screens it (toward the white paper),
|
||||
//! and replaces the linetype and lineweight. Colour 7 prints black, as on paper.
|
||||
|
||||
use cadcraft_color::{Rgb, nearest_aci};
|
||||
use cadcraft_doc::{Drawing, Linetype, PlotStyle, PlotStyleTable};
|
||||
|
||||
use crate::Ink;
|
||||
|
||||
/// A table ready for the builder: styles plus their resolved linetype overrides.
|
||||
pub(crate) struct Styler {
|
||||
table: std::sync::Arc<PlotStyleTable>,
|
||||
/// Per style: `None` keeps the object's linetype; `Some(None)` draws continuous;
|
||||
/// `Some(Some(lt))` uses `lt`.
|
||||
linetypes: Vec<Option<Option<Linetype>>>,
|
||||
}
|
||||
|
||||
impl Styler {
|
||||
pub(crate) fn new(d: &Drawing, table: std::sync::Arc<PlotStyleTable>) -> Styler {
|
||||
let mut library: Option<Vec<Linetype>> = None;
|
||||
let linetypes = table
|
||||
.styles
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let name = s.linetype.as_deref()?;
|
||||
if name.eq_ignore_ascii_case("continuous") || name.eq_ignore_ascii_case("solid") {
|
||||
return Some(None);
|
||||
}
|
||||
let found = d.linetype(name).cloned().or_else(|| {
|
||||
let lib = library.get_or_insert_with(|| cadcraft_doc::library::standard_linetypes_for(d));
|
||||
lib.iter().find(|l| l.name.eq_ignore_ascii_case(name)).cloned()
|
||||
})?;
|
||||
Some((!found.pattern.is_empty()).then_some(found))
|
||||
})
|
||||
.collect();
|
||||
Styler { table, linetypes }
|
||||
}
|
||||
|
||||
/// Index of the style that applies to `ink`.
|
||||
fn index(&self, ink: Ink) -> Option<usize> {
|
||||
if !self.table.is_color_dependent() {
|
||||
return None;
|
||||
}
|
||||
let aci = if ink.aci == 0 { nearest_aci(ink.rgb) } else { ink.aci };
|
||||
let i = usize::from(aci).checked_sub(1)?;
|
||||
(i < self.table.styles.len()).then_some(i)
|
||||
}
|
||||
|
||||
fn style(&self, ink: Ink) -> Option<&PlotStyle> {
|
||||
self.table.styles.get(self.index(ink)?)
|
||||
}
|
||||
|
||||
/// `ink` and lineweight `lw` (mm) as the plot style prints them. `lineweights`: lineweights
|
||||
/// are drawn at all (a style's lineweight replaces the object's only then).
|
||||
pub(crate) fn apply(&self, ink: Ink, lw: f32, lineweights: bool) -> (Ink, f32) {
|
||||
let Some(s) = self.style(ink) else { return (ink, lw) };
|
||||
let lw = match s.lineweight {
|
||||
Some(w) if lineweights => f32::from(w.min(211)) / 100.0,
|
||||
_ => lw,
|
||||
};
|
||||
if s.keeps_color() {
|
||||
return (ink, lw);
|
||||
}
|
||||
// Colour 7 is black on the paper the style prints on.
|
||||
let base = if ink.aci7 { Rgb(0, 0, 0) } else { ink.rgb };
|
||||
(Ink { rgb: s.apply_color(base), aci7: false, ..ink }, lw)
|
||||
}
|
||||
|
||||
/// The linetype override of the style that applies to `ink`: `None` keeps the object's,
|
||||
/// `Some(None)` draws continuous.
|
||||
pub(crate) fn linetype(&self, ink: Ink) -> Option<Option<Linetype>> {
|
||||
self.linetypes.get(self.index(ink)?)?.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Plot style tables applied to the display list (issue #410): colour-dependent tables map an
|
||||
//! object's colour to its printed colour, screening, linetype and lineweight; layouts show them
|
||||
//! when "Display plot styles" is on.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_color::{Color, Rgb};
|
||||
use cadcraft_doc::{Common, Drawing, EntityKind, Line, Lineweight, PlotStyle, PlotStyleKind, PlotStyleTable, Space};
|
||||
use cadcraft_geom::Vec3;
|
||||
use cadcraft_render::{DisplayList, Options, build, build_plot};
|
||||
|
||||
const BLACK: Rgb = Rgb(0, 0, 0);
|
||||
|
||||
fn line(y: f64) -> EntityKind {
|
||||
EntityKind::Line(Line { a: Vec3::new(0.0, y, 0.0), b: Vec3::new(10.0, y, 0.0) })
|
||||
}
|
||||
|
||||
/// Lines in red (1), green (3), colour 7 (ByLayer on layer 0) and a true orange, in `space`.
|
||||
fn drawing(space: &Space) -> Drawing {
|
||||
let mut d = Drawing::new_metric();
|
||||
let c = |color| Common { color, ..Default::default() };
|
||||
d.add(space, c(Color::Index(1)), line(0.0)).unwrap();
|
||||
d.add(space, c(Color::Index(3)), line(1.0)).unwrap();
|
||||
d.add(space, Common::default(), line(2.0)).unwrap();
|
||||
d.add(space, c(Color::True(Rgb(255, 128, 0))), line(3.0)).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn with_table(t: PlotStyleTable) -> Options {
|
||||
Options { plot_style_table: Some(Arc::new(t)), lineweights: true, ..Default::default() }
|
||||
}
|
||||
|
||||
fn colours(l: &DisplayList) -> Vec<Rgb> {
|
||||
l.prims.iter().map(|p| p.display_rgb(Rgb(255, 255, 255))).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn monochrome_prints_every_colour_black() {
|
||||
let d = drawing(&Space::Model);
|
||||
let l = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("monochrome.ctb").unwrap()));
|
||||
assert_eq!(l.prims.len(), 4);
|
||||
assert!(l.prims.iter().all(|p| p.color == BLACK && !p.aci7), "{:?}", l.prims);
|
||||
// Without a table the colours are the objects'.
|
||||
let plain = build_plot(&d, &Space::Model, &Options::default());
|
||||
assert_eq!(colours(&plain), [Rgb(255, 0, 0), Rgb(0, 255, 0), BLACK, Rgb(255, 128, 0)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grayscale_and_default_tables() {
|
||||
let d = drawing(&Space::Model);
|
||||
let l = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("grayscale.ctb").unwrap()));
|
||||
for c in colours(&l) {
|
||||
assert!(c.0 == c.1 && c.1 == c.2, "grey: {c:?}");
|
||||
}
|
||||
let c = colours(&l);
|
||||
assert!(c[1].0 > c[0].0, "green is lighter than red");
|
||||
assert_eq!(c[2], BLACK, "colour 7 prints black");
|
||||
let keep = build_plot(&d, &Space::Model, &with_table(PlotStyleTable::builtin("default.ctb").unwrap()));
|
||||
assert_eq!(colours(&keep), colours(&build_plot(&d, &Space::Model, &Options::default())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screening_lineweight_and_linetype_overrides() {
|
||||
let mut d = Drawing::new_metric();
|
||||
d.header.set_f64("LTSCALE", 0.05);
|
||||
d.add(&Space::Model, Common { color: Color::Index(1), lineweight: Lineweight::Mm100(13), ..Default::default() }, line(0.0)).unwrap();
|
||||
d.add(&Space::Model, Common { color: Color::Index(5), ..Default::default() }, line(1.0)).unwrap();
|
||||
let table = PlotStyleTable::color_dependent("pens.ctb", "", |i| {
|
||||
(i == 1).then(|| PlotStyle {
|
||||
color: Some(BLACK),
|
||||
screening: 50,
|
||||
lineweight: Some(70),
|
||||
linetype: Some("DASHED".into()),
|
||||
..PlotStyle::default()
|
||||
})
|
||||
});
|
||||
let l = build_plot(&d, &Space::Model, &with_table(table.clone()));
|
||||
let red: Vec<_> = l.prims.iter().filter(|p| p.color == Rgb(128, 128, 128)).collect();
|
||||
assert!(red.len() > 1, "dashed into several pieces: {}", red.len());
|
||||
assert!(red.iter().all(|p| p.lw == 0.7), "pen lineweight replaces 0.13 mm");
|
||||
let blue: Vec<_> = l.prims.iter().filter(|p| p.color == Rgb(0, 0, 255)).collect();
|
||||
assert_eq!(blue.len(), 1, "colour 5 keeps its own style");
|
||||
assert_eq!(blue[0].lw, 0.25, "object lineweight (LWDEFAULT)");
|
||||
// With lineweights off nothing gets a width.
|
||||
let thin = build_plot(&d, &Space::Model, &Options { lineweights: false, ..with_table(table) });
|
||||
assert!(thin.prims.iter().all(|p| p.lw == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_display_plot_styles_flag() {
|
||||
let space = Space::Paper("Layout1".into());
|
||||
let mut d = drawing(&space);
|
||||
d.layouts[0].page.plot_style_table = "monochrome.ctb".into();
|
||||
let red = |l: &DisplayList| l.prims.iter().any(|p| p.color == Rgb(255, 0, 0));
|
||||
assert!(red(&build(&d, &space, &Options::default())), "flag off: object colours on screen");
|
||||
d.layouts[0].page.show_plot_styles = true;
|
||||
assert!(!red(&build(&d, &space, &Options::default())), "flag on: monochrome on screen");
|
||||
// Plotting takes its table from the options (the plotter passes the page's).
|
||||
assert!(red(&build_plot(&d, &space, &Options::default())));
|
||||
// Model space and unknown tables draw object colours.
|
||||
d.layouts[0].page.plot_style_table = "unknown.ctb".into();
|
||||
assert!(red(&build(&d, &space, &Options::default())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_tables_do_not_panic() {
|
||||
let d = drawing(&Space::Model);
|
||||
for styles in [Vec::new(), vec![PlotStyle { screening: 255, lineweight: Some(u16::MAX), linetype: Some("NOPE".into()), ..PlotStyle::default() }]]
|
||||
{
|
||||
let t = PlotStyleTable { name: "x.ctb".into(), styles, ..PlotStyleTable::default() };
|
||||
let l = build_plot(&d, &Space::Model, &with_table(t));
|
||||
assert_eq!(l.prims.len(), 4);
|
||||
assert!(l.prims.iter().all(|p| p.lw <= 2.11));
|
||||
}
|
||||
let named = PlotStyleTable { kind: PlotStyleKind::Named, styles: vec![PlotStyle::object("Normal")], ..PlotStyleTable::default() };
|
||||
assert_eq!(build_plot(&d, &Space::Model, &with_table(named)).prims.len(), 4);
|
||||
}
|
||||
@@ -191,6 +191,7 @@ fn ensure_list(app: &mut CadApp, px: f64) {
|
||||
fill: true,
|
||||
lineweights: app.session.settings.lwdisplay,
|
||||
view_height: st.paper_view().height,
|
||||
plot_style_table: None,
|
||||
};
|
||||
let opts = cadcraft_render::Options { tolerance: 2f64.powi(band) * 0.5, ..opts };
|
||||
let space = st.space.clone();
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ do them first.
|
||||
| F11 | Tables: no cell styles, formulas, data links, table breaking | `cmd/table.rs` | Medium | 10–15 |
|
||||
| F12 | Annotative scaling: the flag is stored, nothing scales; no scale list, no "Annotative Object Scale" commands | 4 uncovered menu items | Medium-high in layouts | 10–15 |
|
||||
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); gradients render as a flat colour; associativity not updated when the boundary moves; origin always 0,0; island style always normal; no gap tolerance, separate hatches, hatch dialog, MPolygon | `doc/src/library.rs`, `render/lib.rs:430-437`, `cmd/hatch.rs:198,201` | Medium | 15–25 |
|
||||
| F14 | **[alpha blocker]** Plot styles (CTB/STB) not applied; no plot preview, plotter devices, PUBLISH/batch plot; Print opens no plot dialog (#160, PR #200) | `palettes.rs:666` | High for sheet output | 25–35 |
|
||||
| F14 | **[alpha blocker]** Named plot styles (STB, PlotStyle property) and plot style table editing/files missing (colour-dependent tables apply to PDF plots and "Display plot styles" layouts since #410: built-in `default`/`monochrome`/`grayscale.ctb`); no plot preview, plotter devices, PUBLISH/batch plot; Print opens no plot dialog (#160, PR #200) | `palettes.rs:666` | High for sheet output | 20–30 |
|
||||
| F15 | Viewports: no polygonal, object or clipped viewports; no viewport scale control in the UI | menu items uncovered | Medium | 6–10 |
|
||||
| F16 | Groups (GROUP, GROUPEDIT, PICKSTYLE) missing; ACAD_GROUP written empty | `dxf_write.rs` | Medium | 4–6 |
|
||||
| F17 | Attributes: BATTMAN lists only; no ATTSYNC, EATTEXT, global ATTEDIT | `blocks.rs:300` | Medium | 6–10 |
|
||||
|
||||
Reference in new issue
Block a user