Draw object transparency on screen, in exports and in plots (#764)
* Draw object transparency on screen, in exports and in plots Transparency was read and saved (entity group 440, layer xdata, ByLayer and ByBlock) but never drawn. Display lists now carry each object's effective transparency (its own, its layer's, or the block reference's), and: - the canvas (GPU and CPU paths), ui.render and PNG/SVG exports blend it; TRANSPARENCYDISPLAY turns it off on screen and is now on by default, as in AutoCAD (initial value 1); - plots draw it only when "Plot transparency" is on: a page setup option (PAGESETUP `plotTransparency`, a checkbox in the Page Setup Manager and the Plot dialog), overridable per plot, with PLOTTRANSPARENCYOVERRIDE (0 never, 1 the setting, 2 always). PDF gets /ExtGState alpha, PNG plots blend, SVG uses opacity. Plots without it are byte-identical to before. The public DXF Reference has no plot layout flag for plot transparency, so the option is kept in the session and in JSON, not in DXF/DWG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Transparency fixes: attributes, PDF overlaps, drag previews - Attributes draw with their own transparency (their definition's, layer 0 meaning the reference's layer, ByBlock the reference's) instead of the block reference's. - PDF plots stroke overlapping transparent objects one by one, so they build up where they cross as in PNG plots; opaque output is unchanged. - Drag and rubber-band previews follow TRANSPARENCYDISPLAY. - Effective transparency is worked out only when it is drawn, reusing the layer the property lookup already found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Transparency: one PDF stroke per transparent primitive; opaque rebuilt previews - PDF plots never batch two transparent primitives into one path. Block and viewport contents share their reference's handle, so the per-object flush missed them; each primitive now blends on its own, as in PNG plots and on screen. Opaque output is unchanged. - Previews the session rebuilds with the current properties (rubber bands, copies of objects) are drawn opaque again: they carry CLAYER's transparency, not the objects'. The GPU drag of the objects themselves keeps following TRANSPARENCYDISPLAY. - Tests for attributes on a transparent reference layer, attributes without a definition, and the drag cache across the TRANSPARENCYDISPLAY toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Selection like AutoCAD: colour highlight and the Options Selection tab Selecting or hovering an object outlined each filled primitive (TrueType glyphs, dimension and leader arrowheads, solid hatches, wide polylines) by the rectangle around its triangles, and dashed everything else. A selected dimension's text became a row of dashed boxes, one per glyph, and its closed arrowheads small rectangles. Selected objects are now redrawn in the selection colour: lines solid, fills filled half see-through (so the lines and text on a highlighted hatch stay visible), and the whole object reads as selected, text and arrowheads included. A wipeout is always outlined, since filling it would hide the drawing under it. The Options dialog's Selection tab is laid out as AutoCAD's, with the settings CADCraft can act on, each one AutoCAD's variable, kept in the user profile: - Selection modes: noun/verb selection (PICKFIRST), Shift to add (PICKADD), Object grouping (PICKSTYLE bit 1, now saved in the profile). - Selection effect color (selectionColor, an interface preference; null is the theme's). - Grips: Show grips (GRIPS) and the object selection limit for display of grips (GRIPOBJLIMIT, 100; 0 no limit): grips are neither drawn nor picked beyond it. - Selection preview (SELECTIONPREVIEW): the object under the cursor is highlighted when a command is active (2) and when none is (1). - Visual Effect Settings: the selection area effect (SELECTIONAREA, WINDOWAREACOLOR 150, CROSSINGAREACOLOR 100, SELECTIONAREAOPACITY 25) and the Selection Preview Filter (PREVIEWFILTER 3: 1 objects on locked layers, 2 xrefs, 4 tables, 8 multiline text, 16 hatches, 32 groups). As in AutoCAD these are typed, not on the dialog: HIGHLIGHT (0 shows only the grips of selected objects; not saved, so every session starts highlighted), SELECTIONEFFECT (1 the selection colour, 0 dashed lines with fills outlined along their real boundary) and PREVIEWEFFECT (the preview dashed 0, thicker 1 or both 2). Left for later, as CADCraft lacks what they control: lasso, grip tips and menus, group grips, grips within blocks, associative hatch selection, command and property preview, PICKDRAG, PICKAUTO and PROPOBJLIMIT. DisplayList::fill_outlines returns the boundary of the region a filled primitive covers as closed polylines (edges only one triangle has; the slab filler's horizontal cuts, which need not line up between trapezoids, are split at every x on their line and kept where an odd number cover them), falling back to the bounding box over 600,000 vertices. The GPU and CPU highlight paths and the drawing comparison overlay share the same looks. The new labels have Ukrainian rows (the required catalog); other languages fall back to English. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: EP995LAB <293597958+EP995LAB@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
29 files changed
+974
-105
No files matched your search
Generated
+1
@@ -674,6 +674,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -549,9 +549,7 @@ mod tests {
|
||||
let n = 20_000;
|
||||
let mut tags = vec![tag(93, "0"), tag(92, &n.to_string())];
|
||||
tags.extend((0..n).map(|_| tag(94, &n.to_string())));
|
||||
let ParameterKind::Visibility { states, .. } = visibility(Vec3::default(), &tags) else {
|
||||
panic!("a visibility parameter")
|
||||
};
|
||||
let ParameterKind::Visibility { states, .. } = visibility(Vec3::default(), &tags) else { panic!("a visibility parameter") };
|
||||
assert!(states.len() <= n && states.iter().all(|s| s.visible.is_empty()));
|
||||
|
||||
let mut tags = vec![tag(73, &n.to_string())];
|
||||
|
||||
@@ -747,6 +747,9 @@ pub struct PageSetup {
|
||||
/// 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,
|
||||
/// Plot objects with their transparency ("Plot transparency"; PLOTTRANSPARENCYOVERRIDE can
|
||||
/// override it). The DXF plot layout flags have no bit for it, so it is not saved in DXF.
|
||||
pub plot_transparency: bool,
|
||||
}
|
||||
|
||||
impl Default for PageSetup {
|
||||
@@ -765,6 +768,7 @@ impl Default for PageSetup {
|
||||
center: false,
|
||||
lineweights: true,
|
||||
show_plot_styles: false,
|
||||
plot_transparency: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("preview", "Plot Preview", run_preview)
|
||||
.menu(&["File", "Plot Preview"])
|
||||
.alias(&["pre"])
|
||||
.params("{layout?: current|\"Model\", paper?, landscape?, fit?, scale?, lineweights?, width?: pixels (default 1200)} → {data: base64 PNG, width, height, layout, paper}")
|
||||
.params("{layout?: current|\"Model\", paper?, landscape?, fit?, scale?, lineweights?, plotTransparency?, width?: pixels (default 1200)} → {data: base64 PNG, width, height, layout, paper}")
|
||||
.noundo(),
|
||||
CommandSpec::new("publish", "Batch Publish...", run_publish)
|
||||
.menu(&["File", "Batch Publish..."])
|
||||
@@ -318,6 +318,7 @@ fn run_preview(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let sheet = Sheet::from_page(&page, paper::paper_unit_mm(d));
|
||||
let lineweights = p.get("lineweights").and_then(Value::as_bool).unwrap_or(if space.is_paper() { page.lineweights } else { true });
|
||||
let transparency = super::plotters::plot_transparency(s, p).unwrap_or(page.plot_transparency);
|
||||
let fit = p.get("fit").and_then(Value::as_bool).unwrap_or(matches!(space, Space::Model));
|
||||
let scale = p.get("scale").and_then(Value::as_f64).filter(|v| v.is_finite() && *v > 0.0);
|
||||
let width = p.get("width").and_then(Value::as_u64).unwrap_or(PREVIEW_PX).clamp(16, MAX_PREVIEW_PX);
|
||||
@@ -333,6 +334,7 @@ fn run_preview(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
text: true,
|
||||
fill: true,
|
||||
lineweights,
|
||||
transparency,
|
||||
..Default::default()
|
||||
};
|
||||
let list = cadcraft_render::build_plot(d, &space, &ropts);
|
||||
|
||||
@@ -74,17 +74,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec::new("pagesetup", "Page Setup Manager...", run_pagesetup)
|
||||
.menu(&["File", "Page Setup Manager..."])
|
||||
.params(
|
||||
"{layout?: current, device?: plotter name (PLOTTERMANAGER) | \"None\", paper?: the device's 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}",
|
||||
"{layout?: current, device?: plotter name (PLOTTERMANAGER) | \"None\", paper?: the device's 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, plotTransparency?: bool}",
|
||||
),
|
||||
CommandSpec::new("plot", "Print...", |s, p| run_plot(s, &super::plotters::plot_options(s, p, "plot")?, "plot"))
|
||||
.menu(&["File", "Print..."])
|
||||
.key("Cmd+P")
|
||||
.alias(&["print"])
|
||||
.params("{path? (else returns base64 `data`), layout?: current|\"Model\", paper?, landscape?, fit?: bool, scale?: number | \"1:50\", center?: bool (Model, default true), offset?: [x,y] mm (Model, when center is false; each within ±100000), lineweights?: bool, plotArea?: extents|display|limits|window (Model) | layout, window?: [[x1,y1],[x2,y2]]} (typed or from the menu: the Plot dialog)")
|
||||
.params("{path? (else returns base64 `data`), layout?: current|\"Model\", paper?, landscape?, fit?: bool, scale?: number | \"1:50\", center?: bool (Model, default true), offset?: [x,y] mm (Model, when center is false; each within ±100000), lineweights?: bool, plotTransparency?: bool (default: the page setup's; PLOTTRANSPARENCYOVERRIDE 0/2 overrides it), plotArea?: extents|display|limits|window (Model) | layout, window?: [[x1,y1],[x2,y2]]} (typed or from the menu: the Plot dialog)")
|
||||
.noundo(),
|
||||
CommandSpec::new("exportpdf", "Export to PDF...", |s, p| run_plot(s, &super::plotters::plot_options(s, p, "exportpdf")?, "exportpdf"))
|
||||
.menu(&["File", "Export to PDF..."])
|
||||
.params("{path? (else returns base64 `data`), layout?, paper?, landscape?, fit?, scale?, center?: bool (Model, default true), offset?: [x,y] mm (Model, when center is false; each within ±100000), lineweights?, plotArea?, window?} (as PLOT)")
|
||||
.params("{path? (else returns base64 `data`), layout?, paper?, landscape?, fit?, scale?, center?: bool (Model, default true), offset?: [x,y] mm (Model, when center is false; each within ±100000), lineweights?, plotTransparency?, plotArea?, window?} (as PLOT)")
|
||||
.noundo(),
|
||||
CommandSpec::new("mspace", "Model Space (in viewport)", run_mspace)
|
||||
.alias(&["ms"])
|
||||
@@ -634,6 +634,7 @@ fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"center",
|
||||
"plotStyleTable",
|
||||
"displayPlotStyles",
|
||||
"plotTransparency",
|
||||
];
|
||||
let changes = keys.iter().any(|k| p.get(k).is_some());
|
||||
let name = match str_param(p, "layout") {
|
||||
@@ -715,6 +716,9 @@ fn run_pagesetup(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = p.get("displayPlotStyles").and_then(Value::as_bool) {
|
||||
page.show_plot_styles = v;
|
||||
}
|
||||
if let Some(v) = p.get("plotTransparency").and_then(Value::as_bool) {
|
||||
page.plot_transparency = v;
|
||||
}
|
||||
if changes {
|
||||
if let Some(l) = s.doc_mut()?.layouts.iter_mut().find(|l| l.name == name) {
|
||||
l.page = page.clone();
|
||||
@@ -803,6 +807,11 @@ fn run_plot(s: &mut Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
if let Some(w) = plot_window(s, p, cmd, &space)? {
|
||||
o.insert("window".into(), json!([[w.min.x, w.min.y], [w.max.x, w.max.y]]));
|
||||
}
|
||||
// Without one, the plotter uses each sheet's page setup.
|
||||
match super::plotters::plot_transparency(s, p) {
|
||||
Some(t) => o.insert("plotTransparency".into(), json!(t)),
|
||||
None => o.remove("plotTransparency"),
|
||||
};
|
||||
}
|
||||
let bytes = hook(s.doc()?, &space, &opts).map_err(|e| bad(cmd, e))?;
|
||||
let layout = match &sheets {
|
||||
|
||||
@@ -326,6 +326,16 @@ pub(crate) fn plot_options(s: &Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Whether a plot draws transparency, as PLOTTRANSPARENCYOVERRIDE decides: 0 never, 2 always,
|
||||
/// otherwise the plot's `plotTransparency` (`None` when it has none: the page setup decides).
|
||||
pub(crate) fn plot_transparency(s: &Session, p: &Value) -> Option<bool> {
|
||||
match s.settings.plot_transparency_override {
|
||||
0 => Some(false),
|
||||
2 => Some(true),
|
||||
_ => p.get("plotTransparency").and_then(Value::as_bool),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- PLOTWINDOW ----------
|
||||
|
||||
fn run_plotwindow(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
@@ -373,7 +373,11 @@ pub struct Settings {
|
||||
/// prompt's options there (on by default).
|
||||
pub dynprompt: bool,
|
||||
pub lwdisplay: bool,
|
||||
/// TRANSPARENCYDISPLAY: draw objects with their transparency on screen (on by default).
|
||||
pub transparency_display: bool,
|
||||
/// PLOTTRANSPARENCYOVERRIDE: 0 never plots transparency, 1 (default) uses the page setup's or
|
||||
/// the Plot dialog's "Plot transparency", 2 always plots it.
|
||||
pub plot_transparency_override: u8,
|
||||
pub selection_cycling: bool,
|
||||
/// SELECTSIMILARMODE: what SELECTSIMILAR matches besides the object type (bits: 1 colour,
|
||||
/// 2 layer, 4 linetype, 8 linetype scale, 16 lineweight, 32 plot style, 64 object style,
|
||||
@@ -463,7 +467,8 @@ impl Default for Settings {
|
||||
dynpi_absolute: false,
|
||||
dynprompt: true,
|
||||
lwdisplay: false,
|
||||
transparency_display: false,
|
||||
transparency_display: true,
|
||||
plot_transparency_override: 1,
|
||||
selection_cycling: false,
|
||||
selectsimilarmode: 130,
|
||||
otrack: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ const SESSION_VARS: &[&str] = &[
|
||||
"DYNPICOORDS",
|
||||
"DYNPROMPT",
|
||||
"LWDISPLAY",
|
||||
"PLOTTRANSPARENCYOVERRIDE",
|
||||
"SELECTSIMILARMODE",
|
||||
"OBJECTISOLATIONMODE",
|
||||
"QPMODE",
|
||||
@@ -100,6 +101,7 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {
|
||||
"DYNPICOORDS" => json!(i32::from(st.dynpi_absolute)),
|
||||
"DYNPROMPT" => json!(i32::from(st.dynprompt)),
|
||||
"LWDISPLAY" => json!(i32::from(st.lwdisplay)),
|
||||
"PLOTTRANSPARENCYOVERRIDE" => json!(st.plot_transparency_override),
|
||||
"SELECTSIMILARMODE" => json!(st.selectsimilarmode),
|
||||
// Isolation is temporary: hidden objects show again when the drawing is reopened.
|
||||
"OBJECTISOLATIONMODE" => json!(0),
|
||||
@@ -261,6 +263,9 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {
|
||||
"DYNPICOORDS" => st.dynpi_absolute = as_bool(v).ok_or_else(bad)?,
|
||||
"DYNPROMPT" => st.dynprompt = as_bool(v).ok_or_else(bad)?,
|
||||
"LWDISPLAY" => st.lwdisplay = as_bool(v).ok_or_else(bad)?,
|
||||
"PLOTTRANSPARENCYOVERRIDE" => {
|
||||
st.plot_transparency_override = as_i(v).filter(|i| (0..=2).contains(i)).and_then(|i| u8::try_from(i).ok()).ok_or_else(bad)?
|
||||
}
|
||||
"SELECTSIMILARMODE" => st.selectsimilarmode = as_i(v).filter(|b| (0..=255).contains(b)).ok_or_else(bad)? as u32,
|
||||
"OBJECTISOLATIONMODE" => match as_i(v) {
|
||||
Some(0) => {}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Plot transparency: the page setup's "Plot transparency" (PAGESETUP `plotTransparency`), the
|
||||
//! plot's own option, and PLOTTRANSPARENCYOVERRIDE (0 never, 1 the setting, 2 always) deciding
|
||||
//! what the plotter is told.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::cmd::file::{IoHooks, base64_decode, set_io};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A plotter that echoes the `plotTransparency` it was given (`none` when it got none).
|
||||
fn fake_plot(_: &cadcraft_doc::Drawing, _: &cadcraft_doc::Space, opts: &Value) -> Result<Vec<u8>, String> {
|
||||
Ok(opts.get("plotTransparency").map_or("none".to_string(), Value::to_string).into_bytes())
|
||||
}
|
||||
|
||||
fn session() -> Session {
|
||||
set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write: |_, _| Err("no writer in this test".into()), plot: Some(fake_plot) });
|
||||
let mut s = Session::new();
|
||||
s.execute("line", &json!({"points": [[0, 0], [100, 0]]})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// What the plotter was told for a PLOT with `params`.
|
||||
fn told(s: &mut Session, params: Value) -> String {
|
||||
let r = s.execute("plot", ¶ms).unwrap();
|
||||
String::from_utf8(base64_decode(r["data"].as_str().unwrap()).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_override_decides_what_the_plotter_is_told() {
|
||||
let mut s = session();
|
||||
// 1 (default): the plot's option, else nothing (the plotter reads the page setup).
|
||||
assert_eq!(s.settings.plot_transparency_override, 1);
|
||||
assert_eq!(told(&mut s, json!({})), "none");
|
||||
assert_eq!(told(&mut s, json!({"plotTransparency": true})), "true");
|
||||
assert_eq!(told(&mut s, json!({"plotTransparency": false})), "false");
|
||||
assert_eq!(told(&mut s, json!({"plotTransparency": "yes"})), "none", "not a boolean: ignored");
|
||||
// 0: never.
|
||||
s.execute("setvar", &json!({"name": "PLOTTRANSPARENCYOVERRIDE", "value": 0})).unwrap();
|
||||
assert_eq!(told(&mut s, json!({"plotTransparency": true})), "false");
|
||||
assert_eq!(told(&mut s, json!({})), "false");
|
||||
// 2: always.
|
||||
s.execute("setvar", &json!({"name": "PLOTTRANSPARENCYOVERRIDE", "value": 2})).unwrap();
|
||||
assert_eq!(told(&mut s, json!({"plotTransparency": false})), "true");
|
||||
assert_eq!(s.execute("exportpdf", &json!({})).map(|r| r["bytes"].clone()).unwrap(), json!(4), "EXPORTPDF too: `true`");
|
||||
let r = s.execute("getvar", &json!({"name": "PLOTTRANSPARENCYOVERRIDE"})).unwrap();
|
||||
assert_eq!(r["value"], json!(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_setup_stores_plot_transparency() {
|
||||
let mut s = session();
|
||||
let r = s.execute("pagesetup", &json!({"layout": "Layout1"})).unwrap();
|
||||
assert_eq!(r["page"]["plotTransparency"], json!(false), "off by default");
|
||||
let r = s.execute("pagesetup", &json!({"layout": "Layout1", "plotTransparency": true})).unwrap();
|
||||
assert_eq!(r["page"]["plotTransparency"], json!(true));
|
||||
assert!(s.doc().unwrap().layout("Layout1").unwrap().page.plot_transparency);
|
||||
// One undo step, like the other page setup changes.
|
||||
s.undo().unwrap();
|
||||
assert!(!s.doc().unwrap().layout("Layout1").unwrap().page.plot_transparency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_follows_the_plot_setting() {
|
||||
let mut s = session();
|
||||
// A red solid hatch at 50 % over the line.
|
||||
s.execute("rectang", &json!({"p1": [10, -10], "p2": [90, 10]})).unwrap();
|
||||
s.execute("hatch", &json!({"points": [[50, 5]], "pattern": "SOLID"})).unwrap();
|
||||
let hatch = s.doc().unwrap().model.iter().find(|e| matches!(e.kind, cadcraft_doc::EntityKind::Hatch(_))).map(|e| e.handle).unwrap();
|
||||
s.execute("draworder", &json!({"handles": [hatch.hex()], "order": "front"})).unwrap();
|
||||
s.execute("properties.set", &json!({"handles": [hatch.hex()], "color": 1, "transparency": 50})).unwrap();
|
||||
let preview = |s: &mut Session, p: Value| s.execute("preview", &p).unwrap()["data"].as_str().unwrap().to_string();
|
||||
let off = preview(&mut s, json!({"layout": "Model", "width": 200}));
|
||||
let on = preview(&mut s, json!({"layout": "Model", "width": 200, "plotTransparency": true}));
|
||||
assert_ne!(off, on, "the preview shows transparency when the plot does");
|
||||
s.execute("setvar", &json!({"name": "PLOTTRANSPARENCYOVERRIDE", "value": 0})).unwrap();
|
||||
assert_eq!(preview(&mut s, json!({"layout": "Model", "width": 200, "plotTransparency": true})), off);
|
||||
}
|
||||
@@ -31,7 +31,8 @@ fn defaults_are_the_ones_the_commands_use() {
|
||||
("LWDEFAULT", json!(25)),
|
||||
("DIMASSOC", json!(2)),
|
||||
("CPLOTSTYLE", json!("ByLayer")),
|
||||
("TRANSPARENCYDISPLAY", json!(0)),
|
||||
("TRANSPARENCYDISPLAY", json!(1)),
|
||||
("PLOTTRANSPARENCYOVERRIDE", json!(1)),
|
||||
] {
|
||||
assert_eq!(getvar(&mut s, name), v, "{name}");
|
||||
}
|
||||
@@ -56,8 +57,10 @@ fn values_reach_the_commands() {
|
||||
// Typed at the command line.
|
||||
s.cmdline("setvar hpname net").unwrap();
|
||||
assert_eq!(getvar(&mut s, "HPNAME"), json!("NET"));
|
||||
s.cmdline("setvar transparencydisplay 1").unwrap();
|
||||
assert!(s.settings.transparency_display);
|
||||
s.cmdline("setvar transparencydisplay 0").unwrap();
|
||||
assert!(!s.settings.transparency_display);
|
||||
s.cmdline("setvar plottransparencyoverride 2").unwrap();
|
||||
assert_eq!(s.settings.plot_transparency_override, 2);
|
||||
assert!(setvar(&mut s, "INSNAME", json!("Valve")));
|
||||
assert!(setvar(&mut s, "INSNAME", json!(".")));
|
||||
assert_eq!(getvar(&mut s, "INSNAME"), json!(""));
|
||||
@@ -77,6 +80,10 @@ fn bad_values_and_unknown_names_are_refused() {
|
||||
("CIRCLERAD", json!(-1)),
|
||||
("SPLINESEGS", json!(1e9)),
|
||||
("TRANSPARENCYDISPLAY", json!(5)),
|
||||
("PLOTTRANSPARENCYOVERRIDE", json!(3)),
|
||||
("PLOTTRANSPARENCYOVERRIDE", json!(-1)),
|
||||
("PLOTTRANSPARENCYOVERRIDE", json!("always")),
|
||||
("PLOTTRANSPARENCYOVERRIDE", json!(1.5)),
|
||||
("CHAMFERD", json!("abc")),
|
||||
("CPLOTSTYLE", json!("Style 1")),
|
||||
("OFFSETDIST", json!(f64::MAX.to_string() + "0")),
|
||||
|
||||
@@ -21,3 +21,6 @@ serde_json = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
miniz_oxide = "0.8"
|
||||
quick-xml = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tiny-skia = { workspace = true }
|
||||
@@ -163,9 +163,10 @@ pub fn png(d: &Drawing, space: &Space, width: u32, height: u32) -> Result<Vec<u8
|
||||
}
|
||||
|
||||
/// Render a space to PNG showing `window` (centred, fitted without margin), or fitted to its
|
||||
/// extents (5% margin) when `window` is `None`.
|
||||
/// extents (5% margin) when `window` is `None`. Objects are drawn with their transparency, as on
|
||||
/// screen.
|
||||
pub fn png_framed(d: &Drawing, space: &Space, width: u32, height: u32, window: Option<Bounds2>) -> Result<Vec<u8>> {
|
||||
let list = cadcraft_render::build(d, space, &cadcraft_render::Options::default());
|
||||
let list = cadcraft_render::build(d, space, &cadcraft_render::Options { transparency: true, ..cadcraft_render::Options::default() });
|
||||
let view = png_view(&list.bounds, width, height, window);
|
||||
cadcraft_render::raster::render_png(&list, &view, &cadcraft_render::raster::RasterOptions::default())
|
||||
.ok_or_else(|| IoError::Format("render failed".into()))
|
||||
|
||||
+55
-7
@@ -80,6 +80,9 @@ pub struct PdfOptions {
|
||||
/// Entity hyperlinks read from a drawing file (`PE_URL` extended data) become link
|
||||
/// annotations too, after [`PdfOptions::links`]. Default on.
|
||||
pub hyperlinks: bool,
|
||||
/// Plot objects with their transparency (PDF alpha). `None` = the page setup's "Plot
|
||||
/// transparency" (off for model space).
|
||||
pub plot_transparency: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for PdfOptions {
|
||||
@@ -103,6 +106,7 @@ impl Default for PdfOptions {
|
||||
text_as_geometry: false,
|
||||
margins: None,
|
||||
hyperlinks: true,
|
||||
plot_transparency: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +114,7 @@ impl Default for PdfOptions {
|
||||
impl PdfOptions {
|
||||
/// Parse `{paper?, width?, height?, landscape?, fit?, scale?, center?, offset?: [x,y] mm,
|
||||
/// lineweights?, compress?, title?, window?: [[x1, y1], [x2, y2]], margins?: [l, b, r, t] (mm),
|
||||
/// hyperlinks?: bool}`.
|
||||
/// hyperlinks?: bool, plotTransparency?: bool}`.
|
||||
pub fn from_json(v: &Value) -> PdfOptions {
|
||||
let num = |k: &str| v.get(k).and_then(Value::as_f64).filter(|x| x.is_finite());
|
||||
let paper_mm = match (num("width"), num("height")) {
|
||||
@@ -138,6 +142,7 @@ impl PdfOptions {
|
||||
window: window(v.get("window")),
|
||||
margins: margins(v.get("margins")),
|
||||
hyperlinks: v.get("hyperlinks").and_then(Value::as_bool).unwrap_or(true),
|
||||
plot_transparency: v.get("plotTransparency").and_then(Value::as_bool),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,6 +399,7 @@ fn sheet(d: &Drawing, space: &Space, o: &PdfOptions, links_to: &[(Handle, String
|
||||
let lineweights = o.lineweights.unwrap_or(if space.is_paper() { page.lineweights } else { true });
|
||||
let plot_style_table = cadcraft_doc::plot_style_table(d, &page.plot_style_table).map(std::sync::Arc::new);
|
||||
let ropts = cadcraft_render::Options {
|
||||
transparency: o.plot_transparency.unwrap_or(page.plot_transparency),
|
||||
tolerance: 0.001,
|
||||
min_dash: 0.0,
|
||||
text: true,
|
||||
@@ -584,13 +590,17 @@ fn color(out: &mut String, c: Rgb, op: &str) {
|
||||
///
|
||||
/// Primitives in `texts` (filled glyph outlines) are written as text instead: the glyph in its
|
||||
/// embedded font (`/F<font>`), placed by a text matrix.
|
||||
///
|
||||
/// Transparent primitives are drawn with a graphics state of their alpha (`/T<percent> gs`);
|
||||
/// the transparencies used are returned for the page's `/ExtGState` (see [`ext_gstates`]). A
|
||||
/// list without transparency writes no graphics states.
|
||||
fn content_stream(
|
||||
list: &cadcraft_render::DisplayList,
|
||||
map: &Map,
|
||||
clip_pt: &Bounds2,
|
||||
layers: &[u32],
|
||||
texts: &std::collections::HashMap<u32, (usize, &cadcraft_render::PlotText)>,
|
||||
) -> String {
|
||||
) -> (String, Vec<u8>) {
|
||||
let marked: std::collections::HashSet<u32> = layers.iter().copied().collect();
|
||||
let white = Rgb(255, 255, 255);
|
||||
let mut c = String::new();
|
||||
@@ -616,6 +626,9 @@ fn content_stream(
|
||||
let mut layer: Option<u32> = None;
|
||||
// Inside a text object (`BT` … `ET`), with this font set.
|
||||
let mut text_font: Option<Option<usize>> = None;
|
||||
// The transparency (percent) marks are drawn with, and those used on the page.
|
||||
let mut alpha: u8 = 0;
|
||||
let mut alphas: Vec<u8> = Vec::new();
|
||||
for (i, p) in list.prims.iter().enumerate() {
|
||||
let want = list.prim_layers.get(i).copied().filter(|l| marked.contains(l));
|
||||
let text = u32::try_from(i).ok().and_then(|i| texts.get(&i));
|
||||
@@ -638,6 +651,16 @@ fn content_stream(
|
||||
if raw.iter().any(|q| !q.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
// A wipeout's mask always hides what is under it.
|
||||
let t = if p.kind == Kind::Mask { 0 } else { p.transparency.min(cadcraft_render::MAX_TRANSPARENCY) };
|
||||
if t != alpha {
|
||||
flush(&mut c, &mut pending);
|
||||
let _ = writeln!(c, "/T{t} gs");
|
||||
if !alphas.contains(&t) {
|
||||
alphas.push(t);
|
||||
}
|
||||
alpha = t;
|
||||
}
|
||||
let w = f64::from(p.lw) * PT_PER_MM;
|
||||
let w = if w.is_finite() && w > 0.0 { w.min(100.0) } else { 0.0 };
|
||||
match p.kind {
|
||||
@@ -659,6 +682,13 @@ fn content_stream(
|
||||
if pts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
// A transparent primitive is stroked on its own (one path is painted once), so
|
||||
// where primitives overlap they build up, as in PNG plots and on screen. Block
|
||||
// contents and viewport contents share their reference's handle, so this goes
|
||||
// by primitive, not by object.
|
||||
if t > 0 {
|
||||
flush(&mut c, &mut pending);
|
||||
}
|
||||
if stroke != Some(rgb) || width != Some(w) {
|
||||
flush(&mut c, &mut pending);
|
||||
if stroke != Some(rgb) {
|
||||
@@ -727,7 +757,23 @@ fn content_stream(
|
||||
c.push_str("EMC\n");
|
||||
}
|
||||
c.push_str("Q\n");
|
||||
c
|
||||
(c, alphas)
|
||||
}
|
||||
|
||||
/// The page resources' graphics states for transparencies `alphas` (percent): `/T<percent>`
|
||||
/// sets the stroke and fill alpha (`/CA`, `/ca`). Empty when there are none.
|
||||
fn ext_gstates(alphas: &[u8]) -> String {
|
||||
if alphas.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let mut out = String::from(" /ExtGState <<");
|
||||
for t in alphas {
|
||||
let mut a = String::new();
|
||||
num(&mut a, f64::from(100 - u32::from((*t).min(cadcraft_render::MAX_TRANSPARENCY))) / 100.0);
|
||||
let _ = write!(out, " /T{t} << /Type /ExtGState /CA {a} /ca {a} >>");
|
||||
}
|
||||
out.push_str(" >>");
|
||||
out
|
||||
}
|
||||
|
||||
/// Write one glyph as text: opens a text object and sets the font when needed, then places the
|
||||
@@ -849,11 +895,12 @@ fn assemble(d: &Drawing, pages: &[SheetPage], fonts: &fonts::Fonts, o: &PdfOptio
|
||||
let mut ocgs: Vec<(u32, usize)> = Vec::new();
|
||||
let mut kids = Vec::new();
|
||||
// Page dictionaries are completed once the fonts are written: (page, its fonts, the rest).
|
||||
let mut page_dicts: Vec<(usize, Vec<usize>, String, String)> = Vec::new();
|
||||
let mut page_dicts: Vec<(usize, Vec<usize>, String, String, String)> = Vec::new();
|
||||
for p in pages {
|
||||
let page = objs.reserve();
|
||||
let texts = fonts.texts(&p.list);
|
||||
let content = objs.add(stream(content_stream(&p.list, &p.map, &p.clip_pt, &p.layers, &texts).as_bytes(), o.compress, ""));
|
||||
let (marks, alphas) = content_stream(&p.list, &p.map, &p.clip_pt, &p.layers, &texts);
|
||||
let content = objs.add(stream(marks.as_bytes(), o.compress, ""));
|
||||
let mut used: Vec<usize> = texts.values().map(|(f, _)| *f).collect();
|
||||
used.sort_unstable();
|
||||
used.dedup();
|
||||
@@ -883,15 +930,16 @@ fn assemble(d: &Drawing, pages: &[SheetPage], fonts: &fonts::Fonts, o: &PdfOptio
|
||||
let mut mb = String::new();
|
||||
pt(&mut mb, p.media);
|
||||
let annots = if annots.is_empty() { String::new() } else { format!(" /Annots [{}]", refs(annots)) };
|
||||
page_dicts.push((page, used, props, format!("/MediaBox [0 0 {mb}] /Contents {content} 0 R{annots}")));
|
||||
page_dicts.push((page, used, props, ext_gstates(&alphas), format!("/MediaBox [0 0 {mb}] /Contents {content} 0 R{annots}")));
|
||||
kids.push(page);
|
||||
}
|
||||
let font_ids = if fonts.is_empty() { Vec::new() } else { fonts.write(&mut objs, o.compress) };
|
||||
for (page, used, props, rest) in page_dicts {
|
||||
for (page, used, props, gstates, rest) in page_dicts {
|
||||
let mut res = String::new();
|
||||
if !props.is_empty() {
|
||||
let _ = write!(res, " /Properties <<{props} >>");
|
||||
}
|
||||
res.push_str(&gstates);
|
||||
let f: Vec<String> = used.iter().filter_map(|i| Some(format!("/F{i} {} 0 R", font_ids.get(*i)?))).collect();
|
||||
if !f.is_empty() {
|
||||
let _ = write!(res, " /Font << {} >>", f.join(" "));
|
||||
|
||||
@@ -84,7 +84,8 @@ fn png(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
|
||||
let est = if fit { fit_scale(&window.unwrap_or_else(|| d.extents(space)), &sheet, &o) } else { 1.0 };
|
||||
// Chord tolerance: a fifth of a pixel.
|
||||
let tolerance = (0.2 / (k * est)).max(1e-9);
|
||||
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, ..cadcraft_render::Options::default() };
|
||||
let transparency = o.plot_transparency.unwrap_or(page.plot_transparency);
|
||||
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, transparency, ..cadcraft_render::Options::default() };
|
||||
let mut list = cadcraft_render::build_plot(d, space, &ropts);
|
||||
let half = Vec2::new(f64::from(width), f64::from(height)) / (2.0 * k);
|
||||
let b = window.unwrap_or(list.bounds);
|
||||
@@ -108,8 +109,9 @@ fn png(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
|
||||
fn svg(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
|
||||
let o = PdfOptions::from_json(opts);
|
||||
let Space::Model = space else {
|
||||
let window = sheet(d, space, &o)?.0.bounds();
|
||||
return Ok(crate::svg::export_window(d, space, Some(window)).into_bytes());
|
||||
let (sheet, page) = sheet(d, space, &o)?;
|
||||
let transparency = o.plot_transparency.unwrap_or(page.plot_transparency);
|
||||
return Ok(crate::svg::export_window_with(d, space, Some(sheet.bounds()), transparency).into_bytes());
|
||||
};
|
||||
// Model space as plotted: the plot style table, lineweights, and the table's ends, joins and
|
||||
// fills at the scale that fits the window (or the extents) on the device's paper.
|
||||
@@ -121,7 +123,8 @@ fn svg(d: &Drawing, space: &Space, opts: &Value) -> Result<Vec<u8>> {
|
||||
// Chord tolerance: about 0.05 mm on paper.
|
||||
let tolerance = (0.05 / (sheet.unit_mm * est)).max(1e-9);
|
||||
let tolerance = if tolerance.is_finite() { tolerance } else { cadcraft_render::Options::default().tolerance };
|
||||
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, ..cadcraft_render::Options::default() };
|
||||
let transparency = o.plot_transparency.unwrap_or(page.plot_transparency);
|
||||
let ropts = cadcraft_render::Options { tolerance, lineweights, plot_style_table: table, transparency, ..cadcraft_render::Options::default() };
|
||||
let mut list = cadcraft_render::build_plot(d, space, &ropts);
|
||||
let s = fit_scale(&window.unwrap_or(list.bounds), &sheet, &o);
|
||||
cadcraft_render::pen::apply(&mut list, s * sheet.unit_mm);
|
||||
|
||||
+21
-6
@@ -13,12 +13,26 @@ pub fn export(d: &Drawing, space: &Space) -> String {
|
||||
}
|
||||
|
||||
/// Export showing exactly `window` (the `viewBox`; geometry outside it is clipped by the viewer),
|
||||
/// or the extents with a 2% margin when `window` is `None`.
|
||||
/// or the extents with a 2% margin when `window` is `None`. Objects are drawn with their
|
||||
/// transparency, as on screen.
|
||||
pub fn export_window(d: &Drawing, space: &Space, window: Option<Bounds2>) -> String {
|
||||
let list = cadcraft_render::build(d, space, &cadcraft_render::Options::default());
|
||||
export_window_with(d, space, window, true)
|
||||
}
|
||||
|
||||
/// [`export_window`], drawing transparency only when `transparency` is on (plots).
|
||||
pub fn export_window_with(d: &Drawing, space: &Space, window: Option<Bounds2>, transparency: bool) -> String {
|
||||
let list = cadcraft_render::build(d, space, &cadcraft_render::Options { transparency, ..cadcraft_render::Options::default() });
|
||||
export_list(&list, window)
|
||||
}
|
||||
|
||||
/// ` opacity="…"` for a transparent primitive (none for an opaque one or a wipeout's mask).
|
||||
fn opacity(p: &cadcraft_render::DPrim) -> String {
|
||||
if p.kind == Kind::Mask || p.transparency == 0 {
|
||||
return String::new();
|
||||
}
|
||||
format!(r#" opacity="{:.2}""#, f64::from(p.alpha()) / 255.0)
|
||||
}
|
||||
|
||||
/// Write a display list (as [`export_window`] does for a space's own).
|
||||
pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>) -> String {
|
||||
let window = window.filter(|w| !w.is_empty());
|
||||
@@ -41,12 +55,13 @@ pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>)
|
||||
let ty = |y: f64| y1 - y;
|
||||
for p in &list.prims {
|
||||
let c = p.display_rgb(white).hex();
|
||||
let op = opacity(p);
|
||||
let pts = list.points(p);
|
||||
match p.kind {
|
||||
Kind::Polyline => {
|
||||
let _ = write!(
|
||||
s,
|
||||
r#"<polyline fill="none" stroke="{c}" stroke-width="{stroke:.6}" stroke-linecap="round" stroke-linejoin="round" points=""#
|
||||
r#"<polyline fill="none" stroke="{c}"{op} stroke-width="{stroke:.6}" stroke-linecap="round" stroke-linejoin="round" points=""#
|
||||
);
|
||||
for q in pts {
|
||||
let _ = write!(s, "{:.6},{:.6} ", tx(q.x), ty(q.y));
|
||||
@@ -56,7 +71,7 @@ pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>)
|
||||
Kind::Tris | Kind::Mask => {
|
||||
// A wipeout's mask hides what lies under it with the white background.
|
||||
let c = if p.kind == Kind::Mask { white.hex() } else { c };
|
||||
let _ = write!(s, r#"<path fill="{c}" d=""#);
|
||||
let _ = write!(s, r#"<path fill="{c}"{op} d=""#);
|
||||
for t in pts.chunks(3) {
|
||||
if let [a, b2, cc] = t {
|
||||
let _ = write!(s, "M{:.6} {:.6}L{:.6} {:.6}L{:.6} {:.6}Z", tx(a.x), ty(a.y), tx(b2.x), ty(b2.y), tx(cc.x), ty(cc.y));
|
||||
@@ -66,7 +81,7 @@ pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>)
|
||||
}
|
||||
Kind::Point => {
|
||||
if let Some(q) = pts.first() {
|
||||
let _ = write!(s, r#"<circle cx="{:.6}" cy="{:.6}" r="{:.6}" fill="{c}"/>"#, tx(q.x), ty(q.y), stroke);
|
||||
let _ = write!(s, r#"<circle cx="{:.6}" cy="{:.6}" r="{:.6}" fill="{c}"{op}/>"#, tx(q.x), ty(q.y), stroke);
|
||||
}
|
||||
}
|
||||
Kind::Infinite { ray } => {
|
||||
@@ -77,7 +92,7 @@ pub fn export_list(list: &cadcraft_render::DisplayList, window: Option<Bounds2>)
|
||||
let e = *bp + *dir * far;
|
||||
let _ = write!(
|
||||
s,
|
||||
r#"<line x1="{:.6}" y1="{:.6}" x2="{:.6}" y2="{:.6}" stroke="{c}" stroke-width="{stroke:.6}"/>"#,
|
||||
r#"<line x1="{:.6}" y1="{:.6}" x2="{:.6}" y2="{:.6}" stroke="{c}"{op} stroke-width="{stroke:.6}"/>"#,
|
||||
tx(a.x),
|
||||
ty(a.y),
|
||||
tx(e.x),
|
||||
|
||||
@@ -2192,6 +2192,168 @@ fn kept_xrecord_application_group_roundtrips_as_read() {
|
||||
assert!(!out.contains("291\n2"), "no handle may be written under a bool code");
|
||||
}
|
||||
|
||||
// ---------- plot transparency ----------
|
||||
|
||||
/// A black line under a red solid at `percent` transparency (drawn after it, so on top).
|
||||
fn transparent_over_line(percent: u8) -> Drawing {
|
||||
let mut d = Drawing::new_metric();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(-10.0, 30.0, 0.0), b: Vec3::new(110.0, 30.0, 0.0) })).unwrap();
|
||||
let corners = [Vec3::new(0.0, 0.0, 0.0), Vec3::new(100.0, 0.0, 0.0), Vec3::new(0.0, 60.0, 0.0), Vec3::new(100.0, 60.0, 0.0)];
|
||||
let red = Common { color: Color::Index(1), transparency: Transparency::Percent(percent), ..Common::default() };
|
||||
d.add(&Space::Model, red, EntityKind::Solid(Solid { corners })).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_plots_transparency_only_when_asked() {
|
||||
let d = transparent_over_line(50);
|
||||
let opts = |extra: serde_json::Value| {
|
||||
let mut o = serde_json::json!({"paper": "A4", "compress": false});
|
||||
if let (Some(o), Some(e)) = (o.as_object_mut(), extra.as_object()) {
|
||||
o.extend(e.clone());
|
||||
}
|
||||
o
|
||||
};
|
||||
let on = plot(&d, &Space::Model, &opts(serde_json::json!({"plotTransparency": true}))).unwrap();
|
||||
let content = check_pdf(&on);
|
||||
let text = String::from_utf8_lossy(&on).to_string();
|
||||
assert!(text.contains("/ExtGState << /T50 << /Type /ExtGState /CA 0.5 /ca 0.5 >> >>"), "{text}");
|
||||
// The line is opaque, the solid at half alpha.
|
||||
assert!(content.contains("/T50 gs\n1 0 0 rg"), "{content}");
|
||||
// Off (the default, and explicitly): no graphics states, and the same bytes either way.
|
||||
let off = plot(&d, &Space::Model, &opts(serde_json::json!({}))).unwrap();
|
||||
assert_eq!(off, plot(&d, &Space::Model, &opts(serde_json::json!({"plotTransparency": false}))).unwrap());
|
||||
let off_text = String::from_utf8_lossy(&off).to_string();
|
||||
assert!(!off_text.contains("ExtGState") && !off_text.contains(" gs\n"));
|
||||
// On, but nothing transparent: the same bytes as off.
|
||||
let opaque = transparent_over_line(0);
|
||||
assert_eq!(
|
||||
plot(&opaque, &Space::Model, &opts(serde_json::json!({"plotTransparency": true}))).unwrap(),
|
||||
plot(&opaque, &Space::Model, &opts(serde_json::json!({}))).unwrap()
|
||||
);
|
||||
// A hostile flag is ignored (off).
|
||||
assert_eq!(plot(&d, &Space::Model, &opts(serde_json::json!({"plotTransparency": "yes"}))).unwrap(), off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_page_setup_plot_transparency_is_used() {
|
||||
let mut d = transparent_over_line(90);
|
||||
let paper = Space::Paper(d.layouts[0].name.clone());
|
||||
let corners = [Vec3::new(1.0, 1.0, 0.0), Vec3::new(5.0, 1.0, 0.0), Vec3::new(1.0, 4.0, 0.0), Vec3::new(5.0, 4.0, 0.0)];
|
||||
let faint = Common { color: Color::Index(5), transparency: Transparency::Percent(90), ..Common::default() };
|
||||
d.add(&paper, faint, EntityKind::Solid(Solid { corners })).unwrap();
|
||||
let raw = serde_json::json!({"compress": false});
|
||||
assert!(!String::from_utf8_lossy(&plot(&d, &paper, &raw).unwrap()).contains("ExtGState"));
|
||||
d.layouts[0].page.plot_transparency = true;
|
||||
let text = String::from_utf8_lossy(&plot(&d, &paper, &raw).unwrap()).to_string();
|
||||
assert!(text.contains("/T90 << /Type /ExtGState /CA 0.1 /ca 0.1 >>"), "{text}");
|
||||
// The plot's own option wins over the page setup.
|
||||
let off = serde_json::json!({"compress": false, "plotTransparency": false});
|
||||
assert!(!String::from_utf8_lossy(&plot(&d, &paper, &off).unwrap()).contains("ExtGState"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn png_and_svg_plots_blend_transparency_when_asked() {
|
||||
let d = transparent_over_line(50);
|
||||
let png = |on: Option<bool>| {
|
||||
let mut o = serde_json::json!({"output": "png", "resolution": 50, "width": 210, "height": 297, "landscape": true});
|
||||
if let (Some(on), Some(o)) = (on, o.as_object_mut()) {
|
||||
o.insert("plotTransparency".into(), serde_json::json!(on));
|
||||
}
|
||||
plotter::plot(&d, &Space::Model, &o).unwrap()
|
||||
};
|
||||
let decode = |bytes: &[u8]| tiny_skia::Pixmap::decode_png(bytes).unwrap();
|
||||
// The centre of the sheet is inside the solid (the drawing is fitted to the paper).
|
||||
let centre = |pm: &tiny_skia::Pixmap| {
|
||||
let c = pm.pixel(pm.width() / 2, pm.height() / 2 + 20).unwrap();
|
||||
(c.red(), c.green(), c.blue())
|
||||
};
|
||||
assert_eq!(centre(&decode(&png(None))), (255, 0, 0), "off: opaque red");
|
||||
assert_eq!(png(None), png(Some(false)));
|
||||
let (r, g, b) = centre(&decode(&png(Some(true))));
|
||||
assert!(r == 255 && g.abs_diff(127) <= 2 && b.abs_diff(127) <= 2, "half red on white paper: {:?}", (r, g, b));
|
||||
let svg = |on: bool| {
|
||||
let o = serde_json::json!({"output": "svg", "plotTransparency": on});
|
||||
String::from_utf8(plotter::plot(&d, &Space::Model, &o).unwrap()).unwrap()
|
||||
};
|
||||
assert!(svg(true).contains(r##"fill="#ff0000" opacity="0.50""##), "{}", svg(true));
|
||||
assert!(!svg(false).contains("opacity"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_exports_draw_transparency() {
|
||||
let d = transparent_over_line(50);
|
||||
let svg = String::from_utf8(write(&d, "a.svg").unwrap()).unwrap();
|
||||
assert!(svg.contains(r#"opacity="0.50""#));
|
||||
assert_ne!(write(&d, "a.png").unwrap(), write(&transparent_over_line(0), "a.png").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_strokes_overlapping_transparent_objects_one_by_one() {
|
||||
// Two crossing lines, ByLayer on layer `layer_t` percent transparent.
|
||||
let crossing = |layer_t: u8| {
|
||||
let mut d = Drawing::new_metric();
|
||||
d.layers.push(Layer { name: "Glass".into(), color: Color::Index(5), transparency: layer_t, ..Layer::default() });
|
||||
let on = || Common { layer: "Glass".into(), ..Common::default() };
|
||||
d.add(&Space::Model, on(), EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 0.0), b: Vec3::new(100.0, 100.0, 0.0) })).unwrap();
|
||||
d.add(&Space::Model, on(), EntityKind::Line(Line { a: Vec3::new(0.0, 100.0, 0.0), b: Vec3::new(100.0, 0.0, 0.0) })).unwrap();
|
||||
d
|
||||
};
|
||||
let content = |d: &Drawing, on: bool| {
|
||||
check_pdf(&plot(d, &Space::Model, &serde_json::json!({"paper": "A4", "compress": false, "plotTransparency": on})).unwrap())
|
||||
};
|
||||
// 50 %: each line is its own stroke, so they build up where they cross.
|
||||
let glass = content(&crossing(50), true);
|
||||
assert!(glass.contains("/T50 gs"), "{glass}");
|
||||
assert_eq!(glass.matches("S\n").count(), 2, "{glass}");
|
||||
// Opaque: one path as before, whether the option is on or off.
|
||||
let opaque = content(&crossing(0), true);
|
||||
assert_eq!(opaque.matches("S\n").count(), 1, "{opaque}");
|
||||
assert_eq!(opaque, content(&crossing(0), false));
|
||||
// Transparency off: the transparent layer plots as before too.
|
||||
assert_eq!(content(&crossing(50), false), opaque);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_strokes_each_transparent_primitive_inside_a_block() {
|
||||
// One block reference whose two children cross: they share the reference's handle.
|
||||
let crossing_block = |t: u8| {
|
||||
let mut d = Drawing::new_metric();
|
||||
let mut blk = Block::new("X");
|
||||
for (i, (a, b)) in [((0.0, 0.0), (100.0, 100.0)), ((0.0, 100.0), (100.0, 0.0))].into_iter().enumerate() {
|
||||
let line = Line { a: Vec3::new(a.0, a.1, 0.0), b: Vec3::new(b.0, b.1, 0.0) };
|
||||
let mut e = Entity::new(Handle(i as u64 + 1), EntityKind::Line(line));
|
||||
e.common = Common { color: Color::Index(1), transparency: Transparency::ByBlock, ..Common::default() };
|
||||
blk.entities.push(e);
|
||||
}
|
||||
d.blocks.insert("X".into(), std::sync::Arc::new(blk));
|
||||
let ins = Insert {
|
||||
block: "X".into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
d.add(&Space::Model, Common { transparency: Transparency::Percent(t), ..Common::default() }, EntityKind::Insert(ins)).unwrap();
|
||||
d
|
||||
};
|
||||
let content = |d: &Drawing, on: bool| {
|
||||
check_pdf(&plot(d, &Space::Model, &serde_json::json!({"paper": "A4", "compress": false, "plotTransparency": on})).unwrap())
|
||||
};
|
||||
let half = content(&crossing_block(50), true);
|
||||
assert!(half.contains("/T50 gs"), "{half}");
|
||||
assert_eq!(half.matches("S\n").count(), 2, "{half}");
|
||||
// Opaque: one path, the same with the option on or off.
|
||||
let opaque = content(&crossing_block(0), true);
|
||||
assert_eq!(opaque.matches("S\n").count(), 1, "{opaque}");
|
||||
assert_eq!(opaque, content(&crossing_block(0), false));
|
||||
assert_eq!(content(&crossing_block(50), false), opaque);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_table_reads_cell_text_height_and_keeps_style_scale() {
|
||||
// A table from another writer: no CADCraft xdata; cell 1 sets 140 before its value block
|
||||
|
||||
@@ -306,7 +306,7 @@ impl Backend for Headless {
|
||||
let h = p.get("height").and_then(Value::as_f64).unwrap_or(1000.0).clamp(16.0, 8192.0) as u32;
|
||||
let st = s.state().map_err(|e| e.to_string())?;
|
||||
let window = render_window(&p);
|
||||
let opts = cadcraft_render::Options { view: window, ..Default::default() };
|
||||
let opts = cadcraft_render::Options { view: window, transparency: s.settings.transparency_display, ..Default::default() };
|
||||
let list = cadcraft_render::build(&st.doc, &st.space, &opts);
|
||||
let fit = p.get("fit").and_then(Value::as_bool).unwrap_or(true);
|
||||
let side = |k: &str| p.get(k).and_then(Value::as_f64).map(|x| x as u32);
|
||||
|
||||
+147
-25
@@ -69,6 +69,9 @@ pub struct DPrim {
|
||||
pub aci7: bool,
|
||||
/// Lineweight in mm (0 = thinnest).
|
||||
pub lw: f32,
|
||||
/// Transparency in percent (0 = opaque, at most 90): the object's own, its layer's when
|
||||
/// ByLayer, the block reference's when ByBlock. Always 0 unless [`Options::transparency`].
|
||||
pub transparency: u8,
|
||||
pub kind: Kind,
|
||||
pub start: u32,
|
||||
pub len: u32,
|
||||
@@ -124,8 +127,21 @@ impl DPrim {
|
||||
pub fn display_rgb(&self, background: Rgb) -> Rgb {
|
||||
cadcraft_color::display_rgb(self.color, self.aci7, background)
|
||||
}
|
||||
/// The opacity to draw with: 255 when opaque, down to 26 at 90 % transparency.
|
||||
pub fn alpha(&self) -> u8 {
|
||||
alpha(self.transparency)
|
||||
}
|
||||
}
|
||||
|
||||
/// The opacity (0..=255) of a transparency percentage, which is clamped to 0..=90 first.
|
||||
pub fn alpha(transparency: u8) -> u8 {
|
||||
let t = u32::from(transparency.min(MAX_TRANSPARENCY));
|
||||
u8::try_from((255 * (100 - t) + 50) / 100).unwrap_or(u8::MAX)
|
||||
}
|
||||
|
||||
/// The most transparent an object can be, in percent.
|
||||
pub const MAX_TRANSPARENCY: u8 = 90;
|
||||
|
||||
/// 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)]
|
||||
@@ -200,6 +216,9 @@ pub struct Options {
|
||||
/// The area being looked at (world units of the space being built), for on-screen builds:
|
||||
/// hatch patterns are only generated inside it. `None` = everywhere (plots, exports, picks).
|
||||
pub view: Option<Bounds2>,
|
||||
/// Draw objects with their transparency ([`DPrim::transparency`]). Off by default: the screen
|
||||
/// follows TRANSPARENCYDISPLAY, plots the page setup's "Plot transparency".
|
||||
pub transparency: bool,
|
||||
/// A layout viewport whose model-space contents are left out (its border is still drawn):
|
||||
/// the canvas draws the active viewport's contents itself, see [`viewport_content`].
|
||||
pub skip_viewport: Option<Handle>,
|
||||
@@ -217,6 +236,7 @@ impl Default for Options {
|
||||
plot_style_table: None,
|
||||
view: None,
|
||||
skip_viewport: None,
|
||||
transparency: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +272,8 @@ struct Ctx<'a> {
|
||||
lt_factor: f64,
|
||||
/// Named plot style (table index) of the block reference being expanded, for `ByBlock`.
|
||||
block_pstyle: Option<usize>,
|
||||
/// Transparency (percent) of the block reference being expanded, for `ByBlock`.
|
||||
block_transparency: u8,
|
||||
}
|
||||
|
||||
impl Ctx<'_> {
|
||||
@@ -275,6 +297,8 @@ struct Builder<'a> {
|
||||
named: Option<usize>,
|
||||
/// The block-expansion budget shared by the parts of a list built in parallel.
|
||||
shared_budget: Option<&'a std::sync::atomic::AtomicUsize>,
|
||||
/// Transparency (percent) of the entity being drawn.
|
||||
transparency: u8,
|
||||
}
|
||||
|
||||
/// Upper bound on the block contents drawn per display list (each block reference, MINSERT copy
|
||||
@@ -313,7 +337,16 @@ impl Builder<'_> {
|
||||
self.list.bounds.add(q);
|
||||
self.list.verts.push(q);
|
||||
}
|
||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw, kind: Kind::Polyline, start, len: pts.len() as u32 });
|
||||
self.list.prims.push(DPrim {
|
||||
handle: ctx.top,
|
||||
color: color.rgb,
|
||||
aci7: color.aci7,
|
||||
lw,
|
||||
transparency: self.transparency,
|
||||
kind: Kind::Polyline,
|
||||
start,
|
||||
len: pts.len() as u32,
|
||||
});
|
||||
}
|
||||
fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) {
|
||||
self.tris_at(ctx, color, tris, ctx.xf);
|
||||
@@ -330,7 +363,16 @@ impl Builder<'_> {
|
||||
self.list.bounds.add(q);
|
||||
self.list.tris.push(q);
|
||||
}
|
||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Tris, start, len: tris.len() as u32 });
|
||||
self.list.prims.push(DPrim {
|
||||
handle: ctx.top,
|
||||
color: color.rgb,
|
||||
aci7: color.aci7,
|
||||
lw: 0.0,
|
||||
transparency: self.transparency,
|
||||
kind: Kind::Tris,
|
||||
start,
|
||||
len: tris.len() as u32,
|
||||
});
|
||||
}
|
||||
/// Shaped text: strokes as polylines; TrueType glyphs filled (TEXTFILL) or outlined.
|
||||
fn shaped(&mut self, ctx: &Ctx, color: Ink, lw: f32, sh: &cadcraft_fonts::Shaped) {
|
||||
@@ -381,14 +423,32 @@ impl Builder<'_> {
|
||||
self.list.bounds.add(q);
|
||||
let start = self.list.verts.len() as u32;
|
||||
self.list.verts.push(q);
|
||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Point, start, len: 1 });
|
||||
self.list.prims.push(DPrim {
|
||||
handle: ctx.top,
|
||||
color: color.rgb,
|
||||
aci7: color.aci7,
|
||||
lw: 0.0,
|
||||
transparency: self.transparency,
|
||||
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());
|
||||
self.list.prims.push(DPrim { handle: ctx.top, color: color.rgb, aci7: color.aci7, lw: 0.0, kind: Kind::Infinite { ray }, start, len: 2 });
|
||||
self.list.prims.push(DPrim {
|
||||
handle: ctx.top,
|
||||
color: color.rgb,
|
||||
aci7: color.aci7,
|
||||
lw: 0.0,
|
||||
transparency: self.transparency,
|
||||
kind: Kind::Infinite { ray },
|
||||
start,
|
||||
len: 2,
|
||||
});
|
||||
}
|
||||
/// Take one unit of the block-expansion budget; false once it is spent.
|
||||
fn expand(&mut self) -> bool {
|
||||
@@ -425,13 +485,14 @@ fn top_ctx<'a>(d: &'a Drawing, xf: Mat3, top: Handle, frozen: &'a [String], vp_c
|
||||
vp_colors,
|
||||
lt_factor: 1.0,
|
||||
block_pstyle: None,
|
||||
block_transparency: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_space(d: &Drawing, space: &Space, opts: &Options, plotting: bool) -> DisplayList {
|
||||
let opts = &with_point_view(d, space, opts);
|
||||
let styler = style_table(d, space, opts, plotting).map(|t| plotstyle::Styler::new(d, t));
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None, shared_budget: None };
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
|
||||
if let Space::Paper(name) = space {
|
||||
b.list.sheet = paper::sheet(d, name);
|
||||
}
|
||||
@@ -482,7 +543,8 @@ fn build_parallel(b: &mut Builder, d: &Drawing, store: &cadcraft_doc::EntityStor
|
||||
let parts: Vec<DisplayList> = ents
|
||||
.par_chunks(PARALLEL_CHUNK)
|
||||
.map(|chunk| {
|
||||
let mut part = Builder { list: DisplayList::default(), opts, plotting, styles: None, named: None, shared_budget: Some(&budget) };
|
||||
let mut part =
|
||||
Builder { list: DisplayList::default(), opts, plotting, styles: None, named: None, shared_budget: Some(&budget), transparency: 0 };
|
||||
for e in chunk {
|
||||
entity(&mut part, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
@@ -608,7 +670,8 @@ pub fn viewport_content(d: &Drawing, space: &Space, handle: Handle, area: Bounds
|
||||
}
|
||||
let opts = &with_point_view(d, space, opts);
|
||||
let styler = style_table(d, space, opts, false).map(|t| plotstyle::Styler::new(d, t));
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: styler.as_ref(), named: None, shared_budget: None };
|
||||
let mut b =
|
||||
Builder { list: DisplayList::default(), opts, plotting: false, styles: styler.as_ref(), named: None, shared_budget: None, transparency: 0 };
|
||||
contents(&mut b, d, vp, area, &Clip::Rect(area));
|
||||
b.list
|
||||
}
|
||||
@@ -647,7 +710,15 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
let eb = cadcraft_doc::entity_bounds(d, me, 0);
|
||||
eb.is_empty() || eb.intersects(&win)
|
||||
};
|
||||
let mut sub = Builder { list: DisplayList::default(), opts: b.opts, plotting: b.plotting, styles: b.styles, named: None, shared_budget: None };
|
||||
let mut sub = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts: b.opts,
|
||||
plotting: b.plotting,
|
||||
styles: b.styles,
|
||||
named: None,
|
||||
shared_budget: None,
|
||||
transparency: 0,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let parallel = !b.plotting && b.styles.is_none() && d.model.len() >= 2 * PARALLEL_CHUNK;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -663,8 +734,15 @@ fn contents(b: &mut Builder, d: &Drawing, vp: &cadcraft_doc::Viewport, area: Bou
|
||||
let parts: Vec<DisplayList> = ents
|
||||
.par_chunks(PARALLEL_CHUNK)
|
||||
.map(|chunk| {
|
||||
let mut part =
|
||||
Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None, shared_budget: Some(&budget) };
|
||||
let mut part = Builder {
|
||||
list: DisplayList::default(),
|
||||
opts,
|
||||
plotting: false,
|
||||
styles: None,
|
||||
named: None,
|
||||
shared_budget: Some(&budget),
|
||||
transparency: 0,
|
||||
};
|
||||
let c = ctx();
|
||||
for me in chunk.iter().filter(|me| seen(me)) {
|
||||
entity(&mut part, &c, me);
|
||||
@@ -787,7 +865,16 @@ fn push_raw(dst: &mut DisplayList, p: &DPrim, kind: Kind, pts: &[Vec2]) {
|
||||
} else {
|
||||
dst.verts.extend_from_slice(pts);
|
||||
}
|
||||
dst.prims.push(DPrim { handle: p.handle, color: p.color, aci7: p.aci7, lw: p.lw, kind, start, len: pts.len() as u32 });
|
||||
dst.prims.push(DPrim {
|
||||
handle: p.handle,
|
||||
color: p.color,
|
||||
aci7: p.aci7,
|
||||
lw: p.lw,
|
||||
transparency: p.transparency,
|
||||
kind,
|
||||
start,
|
||||
len: pts.len() as u32,
|
||||
});
|
||||
}
|
||||
|
||||
/// Build the display list for a set of loose entities (previews, rubber bands).
|
||||
@@ -801,7 +888,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, styles: None, named: None, shared_budget: None };
|
||||
let mut b = Builder { list: DisplayList::default(), opts, plotting: false, styles: None, named: None, shared_budget: None, transparency: 0 };
|
||||
for e in ents {
|
||||
entity(&mut b, &top_ctx(d, Mat3::IDENTITY, e.handle, &[], &[]), e);
|
||||
}
|
||||
@@ -844,12 +931,16 @@ fn take_viewport_layers(list: &mut DisplayList) {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
|
||||
/// An object's colour, lineweight (mm), linetype, linetype scale, visibility and the transparency
|
||||
/// of the layer it is drawn on (for ByLayer, see [`transparency`]).
|
||||
type Resolved = (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool, u8);
|
||||
|
||||
fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> Resolved {
|
||||
resolve_common(ctx, &e.common, plotting)
|
||||
}
|
||||
|
||||
/// [`resolve`] for an object's properties `c`.
|
||||
fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool) -> (Ink, f32, Option<cadcraft_doc::Linetype>, f64, bool) {
|
||||
fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool) -> Resolved {
|
||||
let d = ctx.d;
|
||||
// Layer "0" inside a block takes the insert's layer.
|
||||
let layer_name = if c.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { c.layer.as_str() };
|
||||
@@ -874,7 +965,20 @@ fn resolve_common(ctx: &Ctx, c: &cadcraft_doc::Common, plotting: bool) -> (Ink,
|
||||
};
|
||||
let lt = d.linetype(<_name).filter(|l| !l.pattern.is_empty()).cloned();
|
||||
let scale = d.header.f64("LTSCALE", 1.0) * c.ltscale * ctx.lt_factor;
|
||||
(rgb, lw_mm, lt, scale, visible)
|
||||
(rgb, lw_mm, lt, scale, visible, layer.map_or(0, |l| l.transparency))
|
||||
}
|
||||
|
||||
/// The transparency (percent, 0..=90) of an object whose own setting is `t`: the percentage, its
|
||||
/// layer's `layer` when ByLayer (layer 0 inside a block: the block reference's layer, as
|
||||
/// [`resolve_common`] finds it), the block reference's `block` when ByBlock (opaque outside
|
||||
/// blocks).
|
||||
fn transparency(t: cadcraft_doc::Transparency, layer: u8, block: u8) -> u8 {
|
||||
let t = match t {
|
||||
cadcraft_doc::Transparency::Percent(p) => p,
|
||||
cadcraft_doc::Transparency::ByBlock => block,
|
||||
cadcraft_doc::Transparency::ByLayer => layer,
|
||||
};
|
||||
t.min(MAX_TRANSPARENCY)
|
||||
}
|
||||
|
||||
/// The width in mm that the "Default" lineweight stands for: `LWDEFAULT` (hundredths of a mm,
|
||||
@@ -891,11 +995,13 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
if ctx.d.is_hidden_object(e.handle) {
|
||||
return;
|
||||
}
|
||||
// The entity's named plot style, for everything it draws (block contents set their own).
|
||||
let saved = b.named;
|
||||
// The entity's named plot style and transparency (set by `entity_parts`), for everything it
|
||||
// draws (block contents set their own).
|
||||
let (saved, saved_transparency) = (b.named, b.transparency);
|
||||
b.named = b.styles.and_then(|s| s.named_index(ctx, &e.common));
|
||||
entity_parts(b, ctx, e);
|
||||
b.named = saved;
|
||||
b.transparency = saved_transparency;
|
||||
if b.plotting && b.list.prim_layers.len() < b.list.prims.len() {
|
||||
// What it drew that its block contents didn't claim is on its own layer.
|
||||
let layer = entity_layer(ctx, e);
|
||||
@@ -930,10 +1036,13 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
cadcraft_doc::AnnoDraw::Plain => {}
|
||||
}
|
||||
}
|
||||
let (rgb, lw, lt, ltscale, visible) = resolve(ctx, e, b.plotting);
|
||||
let (rgb, lw, lt, ltscale, visible, layer_transparency) = resolve(ctx, e, b.plotting);
|
||||
if !visible {
|
||||
return;
|
||||
}
|
||||
if b.opts.transparency {
|
||||
b.transparency = transparency(e.common.transparency, layer_transparency, ctx.block_transparency);
|
||||
}
|
||||
// A plot style's linetype replaces the object's.
|
||||
let lt = match b.styles.and_then(|s| s.linetype(rgb, b.named)) {
|
||||
Some(over) => over,
|
||||
@@ -986,7 +1095,7 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
|
||||
if let Some(blk) = dm.block.as_ref().and_then(|n| ctx.d.block(n))
|
||||
&& ctx.depth < cadcraft_doc::MAX_BLOCK_DEPTH
|
||||
{
|
||||
let sub = Ctx { block_pstyle: b.named, ..sub_ctx(ctx, e, Mat3::IDENTITY) };
|
||||
let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, Mat3::IDENTITY) };
|
||||
for be in blk.entities.iter() {
|
||||
if !b.expand() {
|
||||
break;
|
||||
@@ -1372,6 +1481,8 @@ fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> {
|
||||
vp_colors: ctx.vp_colors,
|
||||
lt_factor: ctx.lt_factor,
|
||||
block_pstyle: ctx.block_pstyle,
|
||||
// The reference's own, set by the caller (`Builder::transparency` while it draws).
|
||||
block_transparency: ctx.block_transparency,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1389,7 +1500,7 @@ fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rg
|
||||
}
|
||||
let off = Vec2::new(ins.col_spacing * f64::from(c), ins.row_spacing * f64::from(r)).rotate(ins.rotation);
|
||||
let m = Mat3::translate(off).then_before(ins.transform(blk.base.xy()));
|
||||
let sub = Ctx { block_pstyle: b.named, ..sub_ctx(ctx, e, m) };
|
||||
let sub = Ctx { block_pstyle: b.named, block_transparency: b.transparency, ..sub_ctx(ctx, e, m) };
|
||||
for be in blk.entities.iter() {
|
||||
// Constant/visible attribute definitions inside blocks are not drawn; attribs are.
|
||||
if matches!(be.kind, EntityKind::AttDef(_)) {
|
||||
@@ -1406,6 +1517,8 @@ fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rg
|
||||
if b.opts.text {
|
||||
// ATTMODE (ATTDISP): 0 shows no attributes, 1 the visible ones, 2 all of them.
|
||||
let attmode = ctx.d.header.i64("ATTMODE", 1);
|
||||
// The reference's transparency; each attribute draws with its own.
|
||||
let reference = b.transparency;
|
||||
for a in &ins.attribs {
|
||||
if attmode == 0 || (a.invisible && attmode != 2) {
|
||||
continue;
|
||||
@@ -1417,15 +1530,17 @@ fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rg
|
||||
// on insertion), except those set on the attribute itself: ByBlock = the reference's,
|
||||
// layer 0 = the reference's layer.
|
||||
let def = ctx.d.block(&ins.block).and_then(|blk| blk.entities.iter().find(|be| attdef_tag(be, &a.tag)));
|
||||
let (ink, lw, shown) = match (def, a.props.is_empty()) {
|
||||
(None, true) => (rgb, resolve(ctx, e, b.plotting).1, true),
|
||||
let (ink, lw, shown, t) = match (def, a.props.is_empty()) {
|
||||
(None, true) => (rgb, resolve(ctx, e, b.plotting).1, true, reference),
|
||||
(def, _) => {
|
||||
let c = a.props.over(&def.map(|d| d.common.clone()).unwrap_or_else(by_block));
|
||||
let (ink, lw, _, _, shown) = resolve_common(&sub_ctx(ctx, e, Mat3::IDENTITY), &c, b.plotting);
|
||||
(ink, lw, shown)
|
||||
let (ink, lw, _, _, shown, layer_t) = resolve_common(&sub_ctx(ctx, e, Mat3::IDENTITY), &c, b.plotting);
|
||||
let t = if b.opts.transparency { transparency(c.transparency, layer_t, reference) } else { 0 };
|
||||
(ink, lw, shown, t)
|
||||
}
|
||||
};
|
||||
if shown {
|
||||
b.transparency = t;
|
||||
let lw = if b.opts.lineweights { lw } else { 0.0 };
|
||||
match (&multiline, &sh) {
|
||||
(Some(m), _) => mtext(b, ctx, m, ink, lw),
|
||||
@@ -1434,12 +1549,19 @@ fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rg
|
||||
}
|
||||
}
|
||||
}
|
||||
b.transparency = reference;
|
||||
}
|
||||
}
|
||||
|
||||
/// Properties that follow the block reference's (an attribute without a definition).
|
||||
fn by_block() -> cadcraft_doc::Common {
|
||||
cadcraft_doc::Common { color: Color::ByBlock, linetype: "ByBlock".into(), lineweight: Lineweight::ByBlock, ..cadcraft_doc::Common::default() }
|
||||
cadcraft_doc::Common {
|
||||
color: Color::ByBlock,
|
||||
linetype: "ByBlock".into(),
|
||||
lineweight: Lineweight::ByBlock,
|
||||
transparency: cadcraft_doc::Transparency::ByBlock,
|
||||
..cadcraft_doc::Common::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The attribute definition with tag `tag` (any case).
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(super) fn draw(b: &mut Builder, ctx: &Ctx, e: &Entity, m: &MLine, rgb: Ink,
|
||||
if !lt.is_empty() && !lt.eq_ignore_ascii_case("bylayer") && !lt.eq_ignore_ascii_case("byblock") {
|
||||
c.linetype = lt.to_string();
|
||||
}
|
||||
let (rgb, w, lt, scale, _) = resolve_common(ctx, &c, b.plotting);
|
||||
let (rgb, w, lt, scale, _, _) = resolve_common(ctx, &c, b.plotting);
|
||||
let lt = match b.styles.and_then(|s| s.linetype(rgb, b.named)) {
|
||||
Some(over) => over,
|
||||
None => lt,
|
||||
|
||||
@@ -81,7 +81,8 @@ pub fn render(list: &DisplayList, view: &View, o: &RasterOptions) -> Option<Pixm
|
||||
for p in &list.prims {
|
||||
let c = p.display_rgb(o.background);
|
||||
let mut paint = Paint::default();
|
||||
paint.set_color_rgba8(c.0, c.1, c.2, 255);
|
||||
// A transparent object blends with what is drawn under it.
|
||||
paint.set_color_rgba8(c.0, c.1, c.2, p.alpha());
|
||||
paint.anti_alias = o.antialias;
|
||||
let pts = list.points(p);
|
||||
match p.kind {
|
||||
|
||||
@@ -649,6 +649,214 @@ fn max_scale_is_the_most_stretched_axis() {
|
||||
assert!((max_scale(&Mat3::IDENTITY) - 1.0).abs() < 1e-12);
|
||||
}
|
||||
|
||||
// ---------- transparency ----------
|
||||
|
||||
fn line_common(c: Common) -> (Common, EntityKind) {
|
||||
(c, EntityKind::Line(cadcraft_doc::Line { a: Vec3::ZERO, b: Vec3::new(10.0, 0.0, 0.0) }))
|
||||
}
|
||||
|
||||
fn with_transparency() -> Options {
|
||||
Options { transparency: true, ..Default::default() }
|
||||
}
|
||||
|
||||
fn block_entity(handle: u64, common: Common, kind: EntityKind) -> cadcraft_doc::Entity {
|
||||
let mut e = cadcraft_doc::Entity::new(cadcraft_doc::Handle(handle), kind);
|
||||
e.common = common;
|
||||
e
|
||||
}
|
||||
|
||||
fn insert_of(block: &str) -> EntityKind {
|
||||
EntityKind::Insert(cadcraft_doc::Insert {
|
||||
block: block.into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: vec![],
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparency_resolves_by_object_layer_and_block() {
|
||||
use cadcraft_doc::Transparency::{ByBlock, ByLayer, Percent};
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.layers.push(cadcraft_doc::Layer { name: "Walls".into(), transparency: 40, ..Default::default() });
|
||||
let add = |d: &mut Drawing, c: Common| {
|
||||
let (c, k) = line_common(c);
|
||||
d.add(&Space::Model, c, k).unwrap();
|
||||
};
|
||||
add(&mut d, Common { transparency: Percent(25), ..Default::default() });
|
||||
add(&mut d, Common { layer: "Walls".into(), transparency: ByLayer, ..Default::default() });
|
||||
// ByBlock outside a block: opaque.
|
||||
add(&mut d, Common { transparency: ByBlock, ..Default::default() });
|
||||
// Block "Inner": a ByBlock line and a ByLayer line on layer 0 (the reference's layer).
|
||||
let mut inner = cadcraft_doc::Block::new("Inner");
|
||||
let (c, k) = line_common(Common { transparency: ByBlock, ..Default::default() });
|
||||
inner.entities.push(block_entity(1, c, k));
|
||||
let (c, k) = line_common(Common::default());
|
||||
inner.entities.push(block_entity(2, c, k));
|
||||
d.blocks.insert("Inner".into(), std::sync::Arc::new(inner));
|
||||
// Block "Outer": a ByBlock reference to Inner, on layer Walls.
|
||||
let mut outer = cadcraft_doc::Block::new("Outer");
|
||||
outer.entities.push(block_entity(3, Common { layer: "Walls".into(), transparency: ByBlock, ..Default::default() }, insert_of("Inner")));
|
||||
d.blocks.insert("Outer".into(), std::sync::Arc::new(outer));
|
||||
d.add(&Space::Model, Common { transparency: Percent(70), ..Default::default() }, insert_of("Outer")).unwrap();
|
||||
let l = build(&d, &Space::Model, &with_transparency());
|
||||
let t: Vec<u8> = l.prims.iter().map(|p| p.transparency).collect();
|
||||
// Nested: Inner's ByBlock line takes Outer's reference (70, through the ByBlock reference to
|
||||
// Inner); its layer-0 line takes the reference's layer, Walls (40).
|
||||
assert_eq!(t, vec![25, 40, 0, 70, 40]);
|
||||
// Off (the default): everything is opaque, as before.
|
||||
assert!(build(&d, &Space::Model, &Options::default()).prims.iter().all(|p| p.transparency == 0 && p.alpha() == 255));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_transparency_values_are_clamped() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
d.layers.push(cadcraft_doc::Layer { name: "Odd".into(), transparency: 250, ..Default::default() });
|
||||
let (c, k) = line_common(Common { transparency: cadcraft_doc::Transparency::Percent(u8::MAX), ..Default::default() });
|
||||
d.add(&Space::Model, c, k).unwrap();
|
||||
let (c, k) = line_common(Common { layer: "Odd".into(), ..Default::default() });
|
||||
d.add(&Space::Model, c, k).unwrap();
|
||||
// A ByLayer object on a layer that isn't in the table: opaque.
|
||||
let (c, k) = line_common(Common { layer: "Missing".into(), ..Default::default() });
|
||||
d.add(&Space::Model, c, k).unwrap();
|
||||
let l = build(&d, &Space::Model, &with_transparency());
|
||||
assert_eq!(l.prims.iter().map(|p| p.transparency).collect::<Vec<_>>(), vec![90, 90, 0]);
|
||||
assert_eq!((alpha(0), alpha(50), alpha(90), alpha(200)), (255, 128, 26, 26));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_solid_hatch_blends_with_the_line_under_it() {
|
||||
let mut d = Drawing::new_imperial();
|
||||
// The line runs mid-pixel (y = 2.025 is screen row 99.5) so it fills one row exactly.
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(cadcraft_doc::Line { a: Vec3::new(-2.0, 2.025, 0.0), b: Vec3::new(6.0, 2.025, 0.0) }))
|
||||
.unwrap();
|
||||
let solid = Hatch { solid: true, pattern: "SOLID".into(), ..pattern_hatch("SOLID", 1.0, (0.0, 0.0, 4.0, 4.0)) };
|
||||
d.add(
|
||||
&Space::Model,
|
||||
Common { color: Color::Index(1), transparency: cadcraft_doc::Transparency::Percent(50), ..Default::default() },
|
||||
EntityKind::Hatch(solid),
|
||||
)
|
||||
.unwrap();
|
||||
let pixel = |o: &Options, x: f64, y: f64| {
|
||||
let l = build(&d, &Space::Model, o);
|
||||
let v = View { center: Vec2::new(2.0, 2.0), scale: 20.0, width: 200, height: 200 };
|
||||
let pm = render(&l, &v, &RasterOptions { background: Rgb(0, 0, 0), antialias: false, ..Default::default() }).unwrap();
|
||||
let (px, py) = v.to_screen(Vec2::new(x, y));
|
||||
let c = pm.pixel(px as u32, py as u32).unwrap();
|
||||
(c.red(), c.green(), c.blue())
|
||||
};
|
||||
let near = |a: (u8, u8, u8), b: (u8, u8, u8)| [(a.0, b.0), (a.1, b.1), (a.2, b.2)].iter().all(|(x, y)| x.abs_diff(*y) <= 2);
|
||||
// 50 %: half red over black, and half red over the white line, which shows through.
|
||||
let inside = pixel(&with_transparency(), 1.0, 1.0);
|
||||
assert!(near(inside, (128, 0, 0)), "{inside:?}");
|
||||
let on_line = pixel(&with_transparency(), 1.0, 2.025);
|
||||
assert!(near(on_line, (255, 127, 127)), "{on_line:?}");
|
||||
// Transparency off: the hatch hides the line, as before.
|
||||
assert_eq!(pixel(&Options::default(), 1.0, 2.025), (255, 0, 0));
|
||||
}
|
||||
|
||||
fn attrib(tag: &str) -> cadcraft_doc::Attrib {
|
||||
cadcraft_doc::Attrib {
|
||||
tag: tag.into(),
|
||||
text: cadcraft_doc::Text {
|
||||
insert: Vec3::new(1.0, 1.0, 0.0),
|
||||
align_pt: None,
|
||||
height: 1.0,
|
||||
value: "A1".into(),
|
||||
rotation: 0.0,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
style: "Standard".into(),
|
||||
halign: Default::default(),
|
||||
valign: Default::default(),
|
||||
},
|
||||
invisible: false,
|
||||
constant: false,
|
||||
prompt: String::new(),
|
||||
props: Default::default(),
|
||||
multiline: None,
|
||||
modes: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A block "B" with a line (layer 0, ByLayer) and an attribute definition with `def`'s
|
||||
/// properties, and a reference to it with `reference`'s, carrying the attribute.
|
||||
fn attributed(def: Common, reference: Common) -> Drawing {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut blk = cadcraft_doc::Block::new("B");
|
||||
let (c, k) = line_common(Common::default());
|
||||
blk.entities.push(block_entity(1, c, k));
|
||||
blk.entities.push(block_entity(2, def, EntityKind::AttDef(attrib("TAG"))));
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
|
||||
let EntityKind::Insert(mut ins) = insert_of("B") else { panic!("an insert") };
|
||||
ins.attribs.push(attrib("TAG"));
|
||||
d.add(&Space::Model, reference, EntityKind::Insert(ins)).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// The transparency of the block's line and of the attribute's text, from a list built with
|
||||
/// transparency on (the line is drawn first, then the text).
|
||||
fn line_and_text(d: &Drawing) -> (u8, Vec<u8>) {
|
||||
let l = build(d, &Space::Model, &with_transparency());
|
||||
let (first, rest) = l.prims.split_first().unwrap();
|
||||
assert!(!rest.is_empty(), "the attribute is drawn");
|
||||
(first.transparency, rest.iter().map(|p| p.transparency).collect())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_draw_with_their_own_transparency() {
|
||||
use cadcraft_doc::Transparency::{ByBlock, Percent};
|
||||
// A reference at 60 % on an opaque layer: layer-0 ByLayer contents (the line and the
|
||||
// attribute) take the reference's layer, so both are opaque.
|
||||
let d = attributed(Common::default(), Common { transparency: Percent(60), ..Default::default() });
|
||||
let (line, text) = line_and_text(&d);
|
||||
assert_eq!(line, 0);
|
||||
assert!(text.iter().all(|t| *t == 0), "{text:?}");
|
||||
// An attribute definition with its own 50 % in an opaque reference.
|
||||
let d = attributed(Common { transparency: Percent(50), ..Default::default() }, Common::default());
|
||||
let (line, text) = line_and_text(&d);
|
||||
assert_eq!(line, 0);
|
||||
assert!(text.iter().all(|t| *t == 50), "{text:?}");
|
||||
// ByBlock follows the reference.
|
||||
let d = attributed(Common { transparency: ByBlock, ..Default::default() }, Common { transparency: Percent(30), ..Default::default() });
|
||||
assert!(line_and_text(&d).1.iter().all(|t| *t == 30));
|
||||
// Off: everything opaque.
|
||||
assert!(build(&d, &Space::Model, &Options::default()).prims.iter().all(|p| p.transparency == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_follow_the_reference_layer_and_have_no_definition() {
|
||||
use cadcraft_doc::Transparency::Percent;
|
||||
// A layer-0 ByLayer definition in a reference on a 40 % layer: the line and the attribute
|
||||
// both take the reference's layer.
|
||||
let mut d = attributed(Common::default(), Common { layer: "Glass".into(), ..Default::default() });
|
||||
// Adding the reference made the layer.
|
||||
d.layer_mut("Glass").unwrap().transparency = 40;
|
||||
let (line, text) = line_and_text(&d);
|
||||
assert_eq!(line, 40);
|
||||
assert!(text.iter().all(|t| *t == 40), "{text:?}");
|
||||
// No definition in the block: the attribute follows the reference (30 %), with or without
|
||||
// properties of its own.
|
||||
for props in [cadcraft_doc::AttribProps::default(), cadcraft_doc::AttribProps { color: Some(Color::Index(3)), ..Default::default() }] {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let mut blk = cadcraft_doc::Block::new("B");
|
||||
let (c, k) = line_common(Common::default());
|
||||
blk.entities.push(block_entity(1, c, k));
|
||||
d.blocks.insert("B".into(), std::sync::Arc::new(blk));
|
||||
let EntityKind::Insert(mut ins) = insert_of("B") else { panic!("an insert") };
|
||||
ins.attribs.push(cadcraft_doc::Attrib { props: props.clone(), ..attrib("TAG") });
|
||||
d.add(&Space::Model, Common { transparency: Percent(30), ..Default::default() }, EntityKind::Insert(ins)).unwrap();
|
||||
let (line, text) = line_and_text(&d);
|
||||
assert_eq!(line, 0, "layer-0 ByLayer contents take the reference's opaque layer");
|
||||
assert!(text.iter().all(|t| *t == 30), "{props:?}: {text:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mtext_exact_spacing_ignores_taller_lines() {
|
||||
// #711: a doubled middle line pushes the next baseline down with At least, not with Exactly.
|
||||
|
||||
+120
-22
@@ -38,7 +38,7 @@ impl Xf {
|
||||
#[derive(Default)]
|
||||
pub struct CanvasState {
|
||||
pub list: Option<DisplayList>,
|
||||
key: (u64, u64, i32, i32, bool, usize, u64),
|
||||
key: (u64, u64, i32, i32, bool, bool, usize, u64),
|
||||
/// The area `list` was built for (hatch patterns exist only inside it); a view leaving it
|
||||
/// rebuilds the list.
|
||||
window: Option<Bounds2>,
|
||||
@@ -174,10 +174,30 @@ fn prims_of(list: &DisplayList, handles: &[Handle]) -> Vec<usize> {
|
||||
list.prims.iter().enumerate().filter(|(_, pr)| set.contains(&pr.handle)).map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
/// Build options for previews at `scale` pixels per drawing unit (opaque).
|
||||
fn preview_options(scale: f64) -> cadcraft_render::Options {
|
||||
cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() }
|
||||
}
|
||||
|
||||
/// The display list of preview entities the session builds with the current properties
|
||||
/// ([`cadcraft_engine::Session::preview`], the extras of [`cadcraft_engine::Session::drag`]): the
|
||||
/// command's rubber band and rebuilt copies of existing objects. They carry CLAYER's properties,
|
||||
/// not the objects', so they are drawn opaque (the GPU drag of the objects themselves keeps their
|
||||
/// transparency, see [`draw_drag_gpu`]).
|
||||
fn rebuilt_preview<'a>(doc: &cadcraft_doc::Drawing, ents: impl IntoIterator<Item = &'a cadcraft_doc::Entity>, scale: f64) -> DisplayList {
|
||||
cadcraft_render::build_entities(doc, ents, &preview_options(scale))
|
||||
}
|
||||
|
||||
fn color32(c: Rgb) -> Color32 {
|
||||
Color32::from_rgb(c.0, c.1, c.2)
|
||||
}
|
||||
|
||||
/// A primitive's colour on `bg`, with its transparency.
|
||||
fn prim_color32(prim: &cadcraft_render::DPrim, bg: Rgb) -> Color32 {
|
||||
let c = prim.display_rgb(bg);
|
||||
Color32::from_rgba_unmultiplied(c.0, c.1, c.2, prim.alpha())
|
||||
}
|
||||
|
||||
/// Rebuild the display list when needed. `px`: drawing units per pixel in the current view;
|
||||
/// `views`: every area on screen that shows the list (the current view, other model tiles) with
|
||||
/// its drawing units per pixel.
|
||||
@@ -204,7 +224,17 @@ fn ensure_list(app: &mut CadApp, px: f64, views: &[(Bounds2, f64)]) {
|
||||
}
|
||||
s.finish()
|
||||
};
|
||||
let key = (st.uid, st.revision, band(px), lod_band, app.session.settings.lwdisplay, std::sync::Arc::as_ptr(&st.doc) as usize, vp_view);
|
||||
let settings = &app.session.settings;
|
||||
let key = (
|
||||
st.uid,
|
||||
st.revision,
|
||||
band(px),
|
||||
lod_band,
|
||||
settings.lwdisplay,
|
||||
settings.transparency_display,
|
||||
std::sync::Arc::as_ptr(&st.doc) as usize,
|
||||
vp_view,
|
||||
);
|
||||
let visible = views.iter().fold(Bounds2::EMPTY, |acc, v| acc.union(&v.0));
|
||||
let inside = app.canvas.window.is_none_or(|w| visible.is_empty() || w.contains_box(&visible));
|
||||
if app.canvas.list.is_some() && app.canvas.key == key && inside {
|
||||
@@ -222,6 +252,7 @@ fn ensure_list(app: &mut CadApp, px: f64, views: &[(Bounds2, f64)]) {
|
||||
view_height: st.paper_view().height,
|
||||
plot_style_table: None,
|
||||
view: build_window(views),
|
||||
transparency: app.session.settings.transparency_display,
|
||||
skip_viewport: split,
|
||||
};
|
||||
let space = st.space.clone();
|
||||
@@ -263,7 +294,7 @@ struct VpContent {
|
||||
list: DisplayList,
|
||||
/// What the list was built from: drawing, revision, document, viewport, tessellation band
|
||||
/// and lineweight display.
|
||||
key: Option<(u64, u64, usize, Handle, i32, bool)>,
|
||||
key: Option<(u64, u64, usize, Handle, i32, bool, bool)>,
|
||||
/// The viewport's view (centre, height) the list was built for, and the paper area it covers.
|
||||
view: (Vec2, f64),
|
||||
area: Bounds2,
|
||||
@@ -313,7 +344,7 @@ struct VpFrame {
|
||||
|
||||
/// Build or reuse the active viewport's [`VpContent`] for this frame.
|
||||
fn ensure_vp_content(app: &mut CadApp, xf_paper: &Xf, ctx: &egui::Context) -> Option<VpFrame> {
|
||||
let lwdisplay = app.session.settings.lwdisplay;
|
||||
let (lwdisplay, transparency) = (app.session.settings.lwdisplay, app.session.settings.transparency_display);
|
||||
let stencil = stencil_clip(app);
|
||||
let c = &mut app.canvas.vp;
|
||||
let Some((st, (h, vp, clip))) = app.session.state().ok().and_then(|st| Some((st, split_viewport(st, stencil)?))) else {
|
||||
@@ -330,7 +361,7 @@ fn ensure_vp_content(app: &mut CadApp, xf_paper: &Xf, ctx: &egui::Context) -> Op
|
||||
};
|
||||
let px = 1.0 / xf_paper.scale.max(1e-300);
|
||||
let band = px.max(1e-300).log2().floor() as i32;
|
||||
let key = (st.uid, st.revision, std::sync::Arc::as_ptr(&st.doc) as usize, h, band, lwdisplay);
|
||||
let key = (st.uid, st.revision, std::sync::Arc::as_ptr(&st.doc) as usize, h, band, lwdisplay, transparency);
|
||||
let now = crate::now_ms();
|
||||
let view = (vp.view_center, vp.view_height);
|
||||
if view != c.last_view {
|
||||
@@ -365,6 +396,7 @@ fn ensure_vp_content(app: &mut CadApp, xf_paper: &Xf, ctx: &egui::Context) -> Op
|
||||
plot_style_table: None,
|
||||
view: Some(area),
|
||||
skip_viewport: None,
|
||||
transparency,
|
||||
};
|
||||
c.list = cadcraft_render::viewport_content(&st.doc, &st.space, h, area, &opts);
|
||||
c.key = Some(key);
|
||||
@@ -449,7 +481,16 @@ fn draw_vp_mask(c: &mut VpContent, p: &egui::Painter, outline: &[Vec2], xf_paper
|
||||
}
|
||||
fan.bounds = Bounds2::from_points(outline.iter().copied());
|
||||
let len = u32::try_from(fan.tris.len()).unwrap_or(0);
|
||||
fan.prims.push(cadcraft_render::DPrim { handle: Handle(0), color: Rgb(0, 0, 0), aci7: false, lw: 0.0, kind: Kind::Tris, start: 0, len });
|
||||
fan.prims.push(cadcraft_render::DPrim {
|
||||
handle: Handle(0),
|
||||
color: Rgb(0, 0, 0),
|
||||
aci7: false,
|
||||
lw: 0.0,
|
||||
transparency: 0,
|
||||
kind: Kind::Tris,
|
||||
start: 0,
|
||||
len,
|
||||
});
|
||||
let origin = if fan.bounds.is_empty() { Vec2::ZERO } else { fan.bounds.center() };
|
||||
if let Ok(mut slot) = c.mask_slot.lock() {
|
||||
*slot = Some(crate::gpu::build_mesh(&fan, origin, Rgb(0, 0, 0), false));
|
||||
@@ -621,7 +662,7 @@ pub(crate) fn draw_list(p: &egui::Painter, xf: &Xf, list: &DisplayList, bg: Rgb,
|
||||
let mut shapes: Vec<Shape> = Vec::with_capacity(list.prims.len());
|
||||
let px = 1.0 / xf.scale;
|
||||
for prim in &list.prims {
|
||||
let col = color32(prim.display_rgb(bg));
|
||||
let col = prim_color32(prim, bg);
|
||||
let pts = list.points(prim);
|
||||
match prim.kind {
|
||||
Kind::Polyline => {
|
||||
@@ -739,6 +780,8 @@ fn draw_highlight_gpu(
|
||||
/// the transformed copies as egui shapes every frame was slow with heavy selections and could
|
||||
/// overflow the GPU buffer limit.
|
||||
/// `clipboard` is the session's clipboard and its serial (dragged when `drag.clipboard`, by PASTE).
|
||||
/// `transparency`: TRANSPARENCYDISPLAY (dragged objects keep their transparency).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_drag_gpu(
|
||||
c: &mut CanvasState,
|
||||
p: &egui::Painter,
|
||||
@@ -747,13 +790,14 @@ fn draw_drag_gpu(
|
||||
drag: &cadcraft_engine::Drag,
|
||||
clipboard: (&[cadcraft_doc::Entity], u64),
|
||||
bg: Rgb,
|
||||
transparency: bool,
|
||||
) {
|
||||
// Tessellate for a power-of-two zoom band so zooming mid-drag rebuilds only every 2x.
|
||||
let band = xf.scale.max(1e-300).log2().round() as i32;
|
||||
let key = {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(c.key, band, bg.0, bg.1, bg.2, &drag.handles).hash(&mut h);
|
||||
(c.key, band, bg.0, bg.1, bg.2, &drag.handles, transparency).hash(&mut h);
|
||||
if drag.clipboard {
|
||||
clipboard.1.hash(&mut h);
|
||||
}
|
||||
@@ -761,7 +805,7 @@ fn draw_drag_gpu(
|
||||
};
|
||||
if c.drag_key != Some(key) {
|
||||
let band_scale = 2f64.powi(band);
|
||||
let opts = cadcraft_render::Options { tolerance: 0.5 / band_scale, min_dash: 2.0 / band_scale, ..Default::default() };
|
||||
let opts = cadcraft_render::Options { transparency, ..preview_options(band_scale) };
|
||||
let list = if drag.clipboard {
|
||||
cadcraft_render::build_entities(doc, clipboard.0.iter(), &opts)
|
||||
} else {
|
||||
@@ -839,7 +883,7 @@ fn draw_list_gpu(c: &mut CanvasState, p: &egui::Painter, xf: &Xf, bg: Rgb, lwdis
|
||||
if let (Kind::Infinite { ray }, Some(b), Some(d)) = (prim.kind, pts.first(), pts.get(1))
|
||||
&& let Some(seg) = clip_infinite(xf, *b, *d, ray)
|
||||
{
|
||||
shapes.push(Shape::line_segment(seg, Stroke::new(1.0, color32(prim.display_rgb(bg)))));
|
||||
shapes.push(Shape::line_segment(seg, Stroke::new(1.0, prim_color32(prim, bg))));
|
||||
}
|
||||
}
|
||||
p.extend(shapes);
|
||||
@@ -1649,12 +1693,9 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
if let Some((drag, extra)) = drag {
|
||||
if let Ok(st) = app.session.state() {
|
||||
let serial = app.session.clipboard_source.as_ref().map_or(0, |c| c.serial);
|
||||
draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg);
|
||||
let list = cadcraft_render::build_entities(
|
||||
&st.doc,
|
||||
extra.iter(),
|
||||
&cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() },
|
||||
);
|
||||
let transparency = app.session.settings.transparency_display;
|
||||
draw_drag_gpu(&mut app.canvas, &painter, &xf, &st.doc, &drag, (&app.session.clipboard, serial), bg, transparency);
|
||||
let list = rebuilt_preview(&st.doc, extra.iter(), scale);
|
||||
draw_list(&painter, &xf, &list, bg, false);
|
||||
}
|
||||
} else if let (Some(c), true) = (app.canvas.cursor, app.session.prompting()) {
|
||||
@@ -1662,11 +1703,7 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
if !ents.is_empty()
|
||||
&& let Ok(st) = app.session.state()
|
||||
{
|
||||
let list = cadcraft_render::build_entities(
|
||||
&st.doc,
|
||||
ents.iter(),
|
||||
&cadcraft_render::Options { tolerance: 0.5 / scale, min_dash: 2.0 / scale, ..Default::default() },
|
||||
);
|
||||
let list = rebuilt_preview(&st.doc, ents.iter(), scale);
|
||||
draw_list(&painter, &xf, &list, bg, false);
|
||||
}
|
||||
}
|
||||
@@ -2240,7 +2277,8 @@ pub(crate) mod tests {
|
||||
use cadcraft_render::DPrim;
|
||||
|
||||
fn list(handles: &[u64]) -> DisplayList {
|
||||
let prim = |h| DPrim { handle: Handle(h), color: Rgb(255, 255, 255), aci7: true, lw: 0.0, kind: Kind::Polyline, start: 0, len: 0 };
|
||||
let prim =
|
||||
|h| DPrim { handle: Handle(h), color: Rgb(255, 255, 255), aci7: true, lw: 0.0, transparency: 0, kind: Kind::Polyline, start: 0, len: 0 };
|
||||
DisplayList { prims: handles.iter().map(|&h| prim(h)).collect(), ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -2248,6 +2286,66 @@ pub(crate) mod tests {
|
||||
v.iter().map(|&h| Handle(h)).collect()
|
||||
}
|
||||
|
||||
/// A dragged object keeps its transparency in the GPU preview when TRANSPARENCYDISPLAY is
|
||||
/// on, and is opaque when it is off.
|
||||
#[test]
|
||||
fn dragged_objects_keep_their_transparency() {
|
||||
let mut d = cadcraft_doc::Drawing::new_metric();
|
||||
let common = cadcraft_doc::Common { transparency: cadcraft_doc::Transparency::Percent(50), ..Default::default() };
|
||||
let line = cadcraft_doc::Line { a: cadcraft_geom::Vec3::ZERO, b: cadcraft_geom::Vec3::new(10.0, 0.0, 0.0) };
|
||||
let h = d.add(&cadcraft_doc::Space::Model, common, cadcraft_doc::EntityKind::Line(line)).unwrap();
|
||||
let drag = cadcraft_engine::Drag { handles: vec![h], matrix: cadcraft_geom::Mat3::IDENTITY, extra: Vec::new(), clipboard: false };
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(200.0, 100.0));
|
||||
let xf = Xf { rect, center: Vec2::new(5.0, 0.0), scale: 10.0 };
|
||||
let painter = egui::Painter::new(egui::Context::default(), egui::LayerId::background(), rect);
|
||||
// One canvas across the toggle: the cached drag mesh must be rebuilt.
|
||||
let mut c = CanvasState::default();
|
||||
let mut alpha = |transparency: bool| {
|
||||
draw_drag_gpu(&mut c, &painter, &xf, &d, &drag, (&[], 0), Rgb(0, 0, 0), transparency);
|
||||
let mesh = c.drag_slot.lock().unwrap().take().unwrap();
|
||||
// The first line vertex's colour alpha (after its 2 × f32 position).
|
||||
mesh.lines[11]
|
||||
};
|
||||
assert_eq!(alpha(true), 128);
|
||||
assert_eq!(alpha(false), 255);
|
||||
assert_eq!(alpha(true), 128);
|
||||
}
|
||||
|
||||
/// Previews the session rebuilds with the current properties (the rubber band, copies of
|
||||
/// objects) stay opaque even when CLAYER is transparent: they were opaque before transparency
|
||||
/// was drawn, and CLAYER's transparency isn't the objects'.
|
||||
#[test]
|
||||
fn rebuilt_previews_stay_opaque_on_a_transparent_current_layer() {
|
||||
let mut s = cadcraft_engine::Session::new();
|
||||
let d = s.doc_mut().unwrap();
|
||||
d.layers.push(cadcraft_doc::Layer { name: "Glass".into(), transparency: 40, ..Default::default() });
|
||||
d.header.set_str("CLAYER", "Glass");
|
||||
assert!(s.settings.transparency_display);
|
||||
// LINE: the segment from the first point to the cursor.
|
||||
s.cmdline("line").unwrap();
|
||||
s.cmdline("0,0").unwrap();
|
||||
let ents = s.preview(Vec2::new(10.0, 5.0));
|
||||
assert!(!ents.is_empty());
|
||||
assert_eq!(ents[0].common.layer, "Glass");
|
||||
let doc = s.doc().unwrap();
|
||||
// Built with transparency, the preview would take CLAYER's 40 %.
|
||||
let with = cadcraft_render::build_entities(doc, ents.iter(), &cadcraft_render::Options { transparency: true, ..preview_options(1.0) });
|
||||
assert!(with.prims.iter().all(|p| p.transparency == 40));
|
||||
let list = rebuilt_preview(doc, ents.iter(), 1.0);
|
||||
assert!(!list.prims.is_empty() && list.prims.iter().all(|p| p.transparency == 0));
|
||||
s.cancel();
|
||||
// MOVE's base-to-cursor rubber band (a drag extra).
|
||||
s.execute("line", &serde_json::json!({"points": [[0, 0], [10, 0]]})).unwrap();
|
||||
s.cmdline("move").unwrap();
|
||||
s.cmdline("all").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
s.cmdline("0,0").unwrap();
|
||||
let (_, extra) = s.drag(Vec2::new(4.0, 4.0)).unwrap();
|
||||
assert!(!extra.is_empty());
|
||||
let list = rebuilt_preview(s.doc().unwrap(), extra.iter(), 1.0);
|
||||
assert!(!list.prims.is_empty() && list.prims.iter().all(|p| p.transparency == 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_highlight_tolerance_is_constant_within_a_zoom_band() {
|
||||
assert_eq!(viewport_highlight_tolerance(4.0), viewport_highlight_tolerance(3.0));
|
||||
|
||||
@@ -473,7 +473,7 @@ pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Ou
|
||||
let Ok(st) = app.session.state() else { return err("no drawing") };
|
||||
// `window`: just that area, at the size given or matching its shape.
|
||||
let window = render_window(p);
|
||||
let opts = cadcraft_render::Options { view: window, ..Default::default() };
|
||||
let opts = cadcraft_render::Options { view: window, transparency: app.session.settings.transparency_display, ..Default::default() };
|
||||
let list = cadcraft_render::build(&st.doc, &st.space, &opts);
|
||||
let v = st.view();
|
||||
let fit = p.get("fit").and_then(Value::as_bool).unwrap_or(true);
|
||||
|
||||
@@ -23,7 +23,8 @@ use cadcraft_render::{DisplayList, Kind};
|
||||
use egui_wgpu::wgpu;
|
||||
use egui_wgpu::wgpu::util::DeviceExt;
|
||||
|
||||
/// Line/triangle vertex: position (2 × f32, relative to the origin) + sRGB colour (4 × u8).
|
||||
/// Line/triangle vertex: position (2 × f32, relative to the origin) + sRGB colour (4 × u8,
|
||||
/// premultiplied by its alpha: transparent objects blend with what is under them).
|
||||
const VERTEX_SIZE: usize = 12;
|
||||
/// Wide-line / point instance: a, b (2 × f32 each) + colour (4 × u8) + width in points (f32) +
|
||||
/// length along its polyline before `a`, in world units (f32; dashing).
|
||||
@@ -149,6 +150,12 @@ pub fn wide_width(lw: f32, lwdisplay: bool) -> Option<f32> {
|
||||
(w > 1.0).then_some(w)
|
||||
}
|
||||
|
||||
/// `c` with alpha `a`, premultiplied (as the pipelines blend).
|
||||
fn premultiplied(c: Rgb, a: u8) -> [u8; 4] {
|
||||
let k = |v: u8| u8::try_from((u32::from(v) * u32::from(a) + 127) / 255).unwrap_or(v);
|
||||
[k(c.0), k(c.1), k(c.2), a]
|
||||
}
|
||||
|
||||
/// Convert a display list into vertex data relative to `origin`, colours resolved against `bg`.
|
||||
pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) -> MeshData {
|
||||
let mut m = MeshData {
|
||||
@@ -158,8 +165,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) ->
|
||||
breaks: Vec::new(),
|
||||
};
|
||||
for prim in &list.prims {
|
||||
let c = prim.display_rgb(bg);
|
||||
let col = [c.0, c.1, c.2, 255];
|
||||
let col = premultiplied(prim.display_rgb(bg), prim.alpha());
|
||||
let pts = list.points(prim);
|
||||
match prim.kind {
|
||||
Kind::Polyline => match wide_width(prim.lw, lwdisplay) {
|
||||
@@ -396,11 +402,13 @@ fn px_to_clip(px: vec2<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(px.x / view.b.z * 2.0 - 1.0, 1.0 - px.y / view.b.w * 2.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// `c` is premultiplied sRGB; a linear target gets it linearised, still premultiplied.
|
||||
fn out_color(c: vec4<f32>) -> vec4<f32> {
|
||||
if (view.a.w > 0.5) {
|
||||
let lo = c.rgb / 12.92;
|
||||
let hi = pow((c.rgb + vec3<f32>(0.055)) / 1.055, vec3<f32>(2.4));
|
||||
return vec4<f32>(select(hi, lo, c.rgb <= vec3<f32>(0.04045)), c.a);
|
||||
let s = c.rgb / max(c.a, 0.0001);
|
||||
let lo = s / 12.92;
|
||||
let hi = pow((s + vec3<f32>(0.055)) / 1.055, vec3<f32>(2.4));
|
||||
return vec4<f32>(select(hi, lo, s <= vec3<f32>(0.04045)) * c.a, c.a);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
@@ -798,7 +806,7 @@ mod tests {
|
||||
tris: vec![Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0), Vec2::new(0.0, 1.0)],
|
||||
..Default::default()
|
||||
};
|
||||
let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), aci7: true, lw, kind, start, len };
|
||||
let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), aci7: true, lw, transparency: 0, kind, start, len };
|
||||
l.prims =
|
||||
vec![p(Kind::Polyline, 0, 3, 0.0), p(Kind::Tris, 0, 3, 0.0), p(Kind::Point, 3, 1, 0.0), p(Kind::Infinite { ray: false }, 0, 2, 0.0)];
|
||||
for v in l.verts.clone() {
|
||||
@@ -862,6 +870,25 @@ mod tests {
|
||||
assert_eq!(x, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent_objects_get_premultiplied_alpha() {
|
||||
let mut l = list();
|
||||
for p in &mut l.prims {
|
||||
p.transparency = 50;
|
||||
}
|
||||
// Colour 7 on a black background is white: half of it, premultiplied.
|
||||
let m = build_mesh(&l, Vec2::ZERO, Rgb(0, 0, 0), false);
|
||||
assert_eq!(&m.lines[8..12], &[128, 128, 128, 128]);
|
||||
assert_eq!(&m.tris[8..12], &[128, 128, 128, 128]);
|
||||
// A wipeout's mask stays opaque.
|
||||
if let Some(p) = l.prims.get_mut(1) {
|
||||
p.kind = Kind::Mask;
|
||||
}
|
||||
let m = build_mesh(&l, Vec2::ZERO, Rgb(10, 20, 30), false);
|
||||
assert_eq!(&m.tris[8..12], &[10, 20, 30, 255]);
|
||||
assert_eq!(premultiplied(Rgb(255, 0, 0), 255), [255, 0, 0, 255], "opaque colours are unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineweights_become_quads() {
|
||||
let mut l = list();
|
||||
|
||||
@@ -580,9 +580,10 @@ fn parse_ratio(t: &str) -> Option<f64> {
|
||||
}
|
||||
|
||||
/// The page-setup rows (device, paper, orientation, plot area, fit, scale, center, lineweights,
|
||||
/// plot style table) shared by the Page Setup Manager and the Plot dialog, inside a two-column
|
||||
/// grid. Returns the setting the user changed as a `pagesetup` parameter (`device`, `paper`,
|
||||
/// `landscape`, `plotArea`, `scaleToFit`, `scale`, `center`, `lineweights` or `plotStyleTable`).
|
||||
/// transparency, plot style table) shared by the Page Setup Manager and the Plot dialog, inside a
|
||||
/// two-column grid. Returns the setting the user changed as a `pagesetup` parameter (`device`,
|
||||
/// `paper`, `landscape`, `plotArea`, `scaleToFit`, `scale`, `center`, `lineweights`,
|
||||
/// `plotTransparency` or `plotStyleTable`).
|
||||
fn page_rows(
|
||||
ui: &mut egui::Ui,
|
||||
salt: egui::Id,
|
||||
@@ -706,6 +707,16 @@ fn page_rows(
|
||||
change = Some(("lineweights", json!(lw)));
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
let mut transparency = page.plot_transparency;
|
||||
if ui
|
||||
.checkbox(&mut transparency, "Plot transparency")
|
||||
.on_hover_text("Plot objects with their transparency (PLOTTRANSPARENCYOVERRIDE can override it)")
|
||||
.changed()
|
||||
{
|
||||
change = Some(("plotTransparency", json!(transparency)));
|
||||
}
|
||||
ui.end_row();
|
||||
ui.label("Plot style table");
|
||||
egui::ComboBox::from_id_salt(salt.with("styles")).selected_text(style_label(page)).width(190.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(style_label(page) == "None", "None").clicked() && style_label(page) != "None" {
|
||||
@@ -751,6 +762,7 @@ fn apply_page_change(page: &mut cadcraft_doc::PageSetup, key: &str, v: &Value) {
|
||||
"scaleToFit" => page.scale_to_fit = b,
|
||||
"center" => page.center = b,
|
||||
"lineweights" => page.lineweights = b,
|
||||
"plotTransparency" => page.plot_transparency = b,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -832,6 +844,7 @@ fn plot_params(st: &PlotState, preview_dpi: Option<f64>) -> Value {
|
||||
"landscape": page.landscape,
|
||||
"fit": page.scale_to_fit,
|
||||
"lineweights": page.lineweights,
|
||||
"plotTransparency": page.plot_transparency,
|
||||
});
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
let device = device_label(page) != "None";
|
||||
@@ -920,13 +933,7 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
set_temp(ctx, PLOT_NOW, false);
|
||||
let (mut cancel, mut action, mut pick, mut preview) = (false, None, false, false);
|
||||
let center = ctx.content_rect().center();
|
||||
egui::Window::new("Plot")
|
||||
.open(open)
|
||||
.collapsible(false)
|
||||
.resizable(false)
|
||||
.pivot(egui::Align2::CENTER_CENTER)
|
||||
.default_pos(center)
|
||||
.show(ctx, |ui| {
|
||||
egui::Window::new("Plot").open(open).collapsible(false).resizable(false).pivot(egui::Align2::CENTER_CENTER).default_pos(center).show(ctx, |ui| {
|
||||
egui::Grid::new("plot_target").num_columns(2).spacing(vec2(10.0, 6.0)).show(ui, |ui| {
|
||||
ui.label("Layout");
|
||||
egui::ComboBox::from_id_salt("plot_layout").selected_text(st.target.as_str()).width(190.0).show_ui(ui, |ui| {
|
||||
@@ -980,7 +987,14 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
let page = &st.page;
|
||||
action = Some((
|
||||
"pagesetup",
|
||||
json!({ "layout": st.target, "paper": page.paper, "landscape": page.landscape, "scaleToFit": page.scale_to_fit, "lineweights": page.lineweights }),
|
||||
json!({
|
||||
"layout": st.target,
|
||||
"paper": page.paper,
|
||||
"landscape": page.landscape,
|
||||
"scaleToFit": page.scale_to_fit,
|
||||
"lineweights": page.lineweights,
|
||||
"plotTransparency": page.plot_transparency,
|
||||
}),
|
||||
));
|
||||
}
|
||||
preview = ui.button("Preview").on_hover_text("The sheet as these settings plot it").clicked();
|
||||
@@ -989,7 +1003,7 @@ fn plot(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) -> Action {
|
||||
ok |= ui.button("OK").clicked();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
// The output file type follows the device.
|
||||
let ext = choices.devices.iter().find(|dv| dv.name.eq_ignore_ascii_case(&st.page.device)).map_or("pdf", |dv| dv.output.as_str());
|
||||
set_temp(ctx, "plot_state", st.clone());
|
||||
@@ -1169,6 +1183,25 @@ mod plot_dialog_tests {
|
||||
assert!(plot_params(&st, None).get("plotStyleTable").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plot_transparency_checkbox_reaches_the_plot() {
|
||||
let mut st = state("Model");
|
||||
assert!(!st.page.plot_transparency, "off by default");
|
||||
assert_eq!(plot_params(&st, None)["plotTransparency"], json!(false));
|
||||
apply_page_change(&mut st.page, "plotTransparency", &json!(true));
|
||||
assert!(st.page.plot_transparency);
|
||||
assert_eq!(plot_params(&st, None)["plotTransparency"], json!(true));
|
||||
// The preview plots it the same way.
|
||||
assert_eq!(plot_params(&st, Some(150.0))["plotTransparency"], json!(true));
|
||||
// Into a layout's page setup and back out to its Plot dialog.
|
||||
let mut s = Session::new();
|
||||
s.execute("pagesetup", &json!({ "layout": "Layout1", "plotTransparency": true })).unwrap();
|
||||
let page = plot_page(s.doc().unwrap(), "Layout1");
|
||||
assert!(page.plot_transparency);
|
||||
apply_page_change(&mut st.page, "plotTransparency", &json!(false));
|
||||
assert_eq!(plot_params(&st, None)["plotTransparency"], json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plot_center_and_offsets_are_model_only_and_omitted_for_svg() {
|
||||
let mut st = state("Model");
|
||||
|
||||
@@ -19,7 +19,8 @@ const STENCIL: wgpu::TextureFormat = wgpu::TextureFormat::Stencil8;
|
||||
fn tris(pts: Vec<Vec2>) -> DisplayList {
|
||||
let len = pts.len() as u32;
|
||||
let bounds = Bounds2::from_points(pts.iter().copied());
|
||||
let prim = DPrim { handle: Handle(1), color: cadcraft_color::Rgb(0, 0, 0), aci7: false, lw: 0.0, kind: Kind::Tris, start: 0, len };
|
||||
let prim =
|
||||
DPrim { handle: Handle(1), color: cadcraft_color::Rgb(0, 0, 0), aci7: false, lw: 0.0, transparency: 0, kind: Kind::Tris, start: 0, len };
|
||||
DisplayList { prims: vec![prim], tris: pts, bounds, ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -146,7 +147,16 @@ fn clipped_viewport_contents_draw_only_inside_their_outline() {
|
||||
// The selection highlight inside a clipped viewport stays inside its outline too: a wide
|
||||
// line across the whole target at world y 32, masked by the circle.
|
||||
let line = DisplayList {
|
||||
prims: vec![DPrim { handle: Handle(1), color: cadcraft_color::Rgb(0, 0, 0), aci7: false, lw: 0.0, kind: Kind::Polyline, start: 0, len: 2 }],
|
||||
prims: vec![DPrim {
|
||||
handle: Handle(1),
|
||||
color: cadcraft_color::Rgb(0, 0, 0),
|
||||
aci7: false,
|
||||
lw: 0.0,
|
||||
transparency: 0,
|
||||
kind: Kind::Polyline,
|
||||
start: 0,
|
||||
len: 2,
|
||||
}],
|
||||
verts: vec![Vec2::new(0.0, 32.0), Vec2::new(64.0, 32.0)],
|
||||
bounds: Bounds2::new(Vec2::new(0.0, 32.0), Vec2::new(64.0, 32.0)),
|
||||
..Default::default()
|
||||
|
||||
@@ -105,7 +105,7 @@ early (#175), but the throughput problem remains. **DWG ≈ 30% read, ≈ 15% wr
|
||||
|---|---|---|---|
|
||||
| DWT templates | open, save | NEW/QNEW from `.dwt` and `.dxf` templates, SAVEAS `.dwt` (#552) | 0–1 |
|
||||
| DWS standards | open, save, CHECKSTANDARDS | no | 6–10 |
|
||||
| PDF output (PLOT, EXPORTPDF) | multi-sheet, layers, searchable text, hyperlinks | vector PDF with lineweights, real text in embedded TrueType subsets, layers (OCG), several sheets per file and hyperlinks (#567, #571) | 1–2 |
|
||||
| PDF output (PLOT, EXPORTPDF) | multi-sheet, layers, searchable text, hyperlinks | vector PDF with lineweights, real text in embedded TrueType subsets, layers (OCG), several sheets per file and hyperlinks (#567, #571); object transparency as PDF alpha when "Plot transparency" is on (the option isn't saved in DXF/DWG: no public plot layout flag) | 1–2 |
|
||||
| PDF import (PDFIMPORT) and PDF underlays (PDFATTACH) | yes | no | 15–25 |
|
||||
| Raster images attach (PNG, JPEG, TIFF, BMP …) | yes | IMAGE entity exists, no attach/loading/display path | 6–10 |
|
||||
| PNG / JPEG / BMP / TIFF export | PNGOUT, JPGOUT, BMPOUT, TIFOUT | PNG; SVG too (AutoCAD doesn't export SVG) | 1–2 |
|
||||
@@ -135,6 +135,7 @@ our output in AutoCAD black-box.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | PDF output: object transparency plotted as PDF alpha (`/ExtGState` `/ca` `/CA`) when the page setup's or the plot's "Plot transparency" is on; PNG plots blend it, SVG uses `opacity`; plots without it are unchanged byte for byte |
|
||||
| 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space |
|
||||
| 2026-10-12 | minor | VIEWPORT frozen layers also written and read as AutoCAD's group 341 layer handles, so third-party viewers honour per-viewport freezing in DXF (DWG converter gap noted) |
|
||||
| 2026-10-12 | minor | HATCH: associative hatches inside blocks keep their source objects; boundaries carry `{ACAD_REACTORS}` |
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Where CADCraft falls short of AutoCAD
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Block Editor for static blocks landed; rows 4 and F3 narrowed) · **Target:** Autodesk AutoCAD 2027
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (multiline attributes read, drawn and saved: F17 narrowed) · **Target:** Autodesk AutoCAD 2027
|
||||
|
||||
Every known shortfall, one row each, ranked within each section by user impact. This is the work
|
||||
list: agents pick from here (top of a section first), and remove or update a row in the same PR
|
||||
@@ -35,8 +35,8 @@ do them first.
|
||||
| F9 | MTEXT is typed in an on-canvas editor with a formatting toolbar (font, height, bold, italic, underline, overline, colour, alignment, lists, tab and special characters) and a ruler for indents and tab stops (#532); left: the box does not lay the text out by the indents, tabs, columns or font weights while typing (bold shows wider, overline is not drawn), no justification (attachment) or columns controls in the editor, existing MTEXT cannot be edited (no DDEDIT/MTEDIT), stacked fractions and other codes show as placeholders; no fields (FIELD, UPDATEFIELD) and no spell check (SPELL): open PR #551. Paragraph indents, tabs, bullets, numbering and columns landed (#547) | `ui-egui/mtext_editor.rs`, `engine/mtext_edit.rs` | Medium | 10–18 |
|
||||
| F11 | Tables (cell styles, formulas, TABLEDIT, TABLEEXPORT, table breaking and CSV data links landed, #534): formulas aren't read from or written as AutoCAD FIELD objects (other programs see values); number formats apply to formula results only; cell text doesn't wrap; custom cell styles, breaks and data links travel as CADCraft xdata (not through DWG); data links read CSV only (no Excel ranges, no write-back, not on the web); no cell-style tab in the Table Style dialog | `cmd/table/*`, `io/dxf_table.rs` | Low-medium | 6–10 |
|
||||
| F12 | Annotative scaling done (#468): scale list (SCALELISTEDIT), CANNOSCALE, OBJECTSCALE, ANNOALLVISIBLE/ANNOAUTOSCALE, viewport scale, DXF, status bar list. Left: per-scale positions (ANNORESET is a no-op), a viewport annotation scale apart from its viewport scale, annotative objects inside blocks and old LEADERs, AutoCAD files' per-object scale contexts (read as not annotative) | `cmd/annoscale.rs`, `doc/src/anno.rs` | Low-medium | 4–8 |
|
||||
| F14 | **[alpha blocker]** Plotting leftovers: binary `.ctb`/`.stb`/`.pc3` files can't be read; model-space PDF plots keep the default margins (a device paper's margins apply to layouts and PNG plots); no system printers (H3); the page setup's "Plot transparency" is not saved in DXF/DWG (the public DXF Reference has no plot layout flag for it), so it lasts for the session and in JSON only. The Plot dialog (#160), Plot Preview and PUBLISH (#186), plot styles (CTB and STB, [plot-styles.md](plot-styles.md), #410) with their end, join and fill styles, plotter devices with paper sizes and printable areas ([plotters.md](plotters.md), #564), and plotted transparency (page setup and Plot dialog option, PLOTTRANSPARENCYOVERRIDE) landed | `io/src/plotter.rs`, `cmd/plotters.rs` | Medium | 4–8 |
|
||||
| F13 | Hatch: 23 patterns (AutoCAD ~85; ours must stay original); the Hatch and Gradient and Hatch Edit dialogs have no Preview or Inherit Properties and can't add or remove boundaries of an existing hatch (HPDLGMODE 0 keeps HATCH on the command line), no MPolygon; HATCHEDIT can't add or remove boundaries; a hatch copied with its boundary isn't linked to the copy; HPGAPTOL closes gaps between curve ends only | `doc/src/library.rs`, `cmd/hatch.rs`, `cmd/hatch/edit.rs` | Medium | 8–12 |
|
||||
| F14 | **[alpha blocker]** Plotting leftovers: binary `.ctb`/`.stb`/`.pc3` files can't be read; model-space PDF plots keep the default margins (a device paper's margins apply to layouts and PNG plots); no system printers (H3). The Plot dialog (#160), Plot Preview and PUBLISH (#186), plot styles (CTB and STB, [plot-styles.md](plot-styles.md), #410) with their end, join and fill styles, and plotter devices with paper sizes and printable areas ([plotters.md](plotters.md), #564) landed | `io/src/plotter.rs`, `cmd/plotters.rs` | Medium | 4–8 |
|
||||
| F15 | Viewports: MVIEW Restore prints "not available yet" (named viewport configurations into a layout, see U11); a clipped viewport and its clip boundary move and scale separately. Polygonal, object and clipped viewports, VPCLIP, MVIEW ON/OFF/Lock/2/3/4 and the status-bar scale list landed (#453) | `cmd/layout/viewports.rs` | Low | 2–4 |
|
||||
| F16 | Groups: no group bounding box or single grip when a group is selected (GROUPDISPLAYMODE), copying a whole group does not make a new group, no Object Grouping dialog (GROUP, GROUPEDIT, UNGROUP, -GROUP, PICKSTYLE group selection and the DXF/DWG round trip landed, #417); PICKSTYLE's associative-hatch bit is stored but unused | `cmd/groups.rs` | Low | 2–4 |
|
||||
| F17 | Attributes (ATTSYNC, global -ATTEDIT, BATTMAN editing and DATAEXTRACTION/EATTEXT landed, #433): no verify, preset or lock-position modes, and multiline attributes (read, drawn and saved since 2026-10-12) can't be created or switched in BATTMAN; data extraction covers block references and attribute values (CSV or a table) only — no wizard, nested blocks, other objects' properties, `.dxe` templates or spreadsheet output | `cmd/blocks/battman.rs`, `cmd/blocks/extract.rs` | Low | 4–6 |
|
||||
@@ -145,6 +145,7 @@ Detail: [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Object transparency (entity, ByLayer, ByBlock) is drawn: on screen (TRANSPARENCYDISPLAY, on by default), in PNG/SVG exports, and in plots when the page setup's "Plot transparency" is on (PDF alpha, PNG blending, SVG opacity; PLOTTRANSPARENCYOVERRIDE): F14 narrowed |
|
||||
| 2026-10-12 | minor | Hatch and Gradient dialog (HATCH, GRADIENT; HPDLGMODE) and Hatch Edit dialog (HATCHEDIT, right-click, double-click a hatch): F13 narrowed to Preview, Inherit Properties and boundary add/remove in edit mode |
|
||||
| 2026-10-12 | minor | A bare system variable name typed at the command line (ORTHOMODE, GRIDMODE…) runs SETVAR on that variable, as in AutoCAD; U10 narrowed |
|
||||
| 2026-10-13 | minor | STRETCH takes crossing and window polygons (CP, WP), BOX and a fence (F), typed and as JSON (`polygon`, `fence`): F1 narrowed |
|
||||
|
||||
@@ -111,3 +111,22 @@ In the file name, characters other than letters, digits, spaces and `-_()+,` bec
|
||||
Model-space PDF plots don't apply the paper's margins yet. They fit the drawing inside the
|
||||
default margins. PNG plots apply them. SVG plots show a layout's whole sheet, or the
|
||||
model-space extents.
|
||||
|
||||
## Plot transparency
|
||||
|
||||
Objects plot with their transparency (their own, their layer's when ByLayer, the block
|
||||
reference's when ByBlock) only when asked, as in AutoCAD:
|
||||
|
||||
- `pagesetup {layout, plotTransparency: bool}` stores the layout's "Plot transparency" (off by
|
||||
default). The Page Setup Manager and the Plot dialog have the checkbox.
|
||||
- `plot {plotTransparency?: bool}` (and `exportpdf`, `preview`) overrides it for one plot.
|
||||
Model-space plots have it off unless asked.
|
||||
- `PLOTTRANSPARENCYOVERRIDE` decides first: 0 never plots transparency, 1 (default) uses the
|
||||
setting above, 2 always plots it.
|
||||
|
||||
PDF plots write it as alpha (`/ExtGState` with `/ca` and `/CA`), PNG plots blend it, SVG plots use
|
||||
`opacity`. A plot without transparency is written exactly as before. The DXF plot layout flags
|
||||
have no bit for the option, so it isn't saved in DXF or DWG files.
|
||||
|
||||
On screen, `TRANSPARENCYDISPLAY` (on by default; the status bar's Transparency button) shows
|
||||
transparency; PNG and SVG exports of a drawing always draw it.
|
||||
Reference in new issue
Block a user