diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index 2975e69..5eea3ac 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -208,16 +208,16 @@ pub fn nearest_aci(c: Rgb) -> u8 { best.1 } -/// Canvas display colour: ACI 7 (and any near-white/near-black) flips to contrast with the -/// background, as CAD programs do on dark or light model space. -pub fn display_rgb(c: Rgb, background: Rgb) -> Rgb { - let dark_bg = background.luma() < 0.5; - if dark_bg && c.luma() < 0.06 { - Rgb(255, 255, 255) - } else if !dark_bg && c.luma() > 0.94 { - Rgb(0, 0, 0) - } else { +/// Display colour of `c` on `background`. Colour 7 (`aci7`) flips to contrast with the +/// background, white on dark and black on light (paper), as CAD programs draw it; every other +/// colour, including a true-colour or index white or black, keeps its RGB. +pub fn display_rgb(c: Rgb, aci7: bool, background: Rgb) -> Rgb { + if !aci7 { c + } else if background.luma() < 0.5 { + Rgb(255, 255, 255) + } else { + Rgb(0, 0, 0) } } @@ -259,7 +259,10 @@ mod tests { #[test] fn white_flips_on_light_background() { - assert_eq!(display_rgb(Rgb(255, 255, 255), Rgb(255, 255, 255)), Rgb(0, 0, 0)); - assert_eq!(display_rgb(Rgb(255, 255, 255), Rgb(33, 40, 48)), Rgb(255, 255, 255)); + assert_eq!(display_rgb(Rgb(255, 255, 255), true, Rgb(255, 255, 255)), Rgb(0, 0, 0)); + assert_eq!(display_rgb(Rgb(255, 255, 255), true, Rgb(33, 40, 48)), Rgb(255, 255, 255)); + // Only colour 7 flips: ACI 255 / true-colour white and dark index colours keep their RGB. + assert_eq!(display_rgb(Rgb(255, 255, 255), false, Rgb(255, 255, 255)), Rgb(255, 255, 255)); + assert_eq!(display_rgb(aci_rgb(250), false, Rgb(33, 40, 48)), aci_rgb(250)); } } diff --git a/crates/io/src/pdf.rs b/crates/io/src/pdf.rs index 75d638c..8d3b281 100644 --- a/crates/io/src/pdf.rs +++ b/crates/io/src/pdf.rs @@ -7,7 +7,7 @@ use std::fmt::Write as _; -use cadcraft_color::{Rgb, display_rgb}; +use cadcraft_color::Rgb; use cadcraft_doc::{Drawing, PageSetup, Space}; use cadcraft_geom::{Bounds2, Vec2}; use cadcraft_render::{Kind, Sheet, clip, paper}; @@ -238,7 +238,7 @@ fn content_stream(list: &cadcraft_render::DisplayList, map: &Map, clip_pt: &Boun } }; for p in &list.prims { - let rgb = display_rgb(p.color, white); + let rgb = p.display_rgb(white); let raw = list.points(p); if raw.iter().any(|q| !q.is_finite()) { continue; diff --git a/crates/io/src/svg.rs b/crates/io/src/svg.rs index 01f465d..04f6a39 100644 --- a/crates/io/src/svg.rs +++ b/crates/io/src/svg.rs @@ -2,7 +2,7 @@ use std::fmt::Write as _; -use cadcraft_color::{Rgb, display_rgb}; +use cadcraft_color::Rgb; use cadcraft_doc::{Drawing, Space}; use cadcraft_geom::Bounds2; use cadcraft_render::Kind; @@ -35,7 +35,7 @@ pub fn export_window(d: &Drawing, space: &Space, window: Option) -> Str let tx = |x: f64| x - x0; let ty = |y: f64| y1 - y; for p in &list.prims { - let c = display_rgb(p.color, white).hex(); + let c = p.display_rgb(white).hex(); let pts = list.points(p); match p.kind { Kind::Polyline => { diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index a3292d0..326a1c9 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -46,6 +46,9 @@ pub enum Kind { pub struct DPrim { pub handle: Handle, pub color: Rgb, + /// Colour 7 (directly, by layer or by block): drawn white on a dark background and black on + /// a light one, see [`DPrim::display_rgb`]. Other colours keep their RGB. + pub aci7: bool, /// Lineweight in mm (0 = thinnest). pub lw: f32, pub kind: Kind, @@ -66,6 +69,32 @@ pub struct DisplayList { pub expanded: usize, } +impl DPrim { + /// The colour to draw with on `background`. + pub fn display_rgb(&self, background: Rgb) -> Rgb { + cadcraft_color::display_rgb(self.color, self.aci7, background) + } +} + +/// A resolved colour: its RGB and whether it is colour 7. +#[derive(Clone, Copy, Debug, PartialEq)] +struct Ink { + rgb: Rgb, + aci7: bool, +} + +/// Resolve `c` like [`Color::resolve`], noting whether it comes out as colour 7. +fn ink(c: Color, layer: Color, block: Color) -> Ink { + // ByLayer / ByBlock left unresolved (layer or block colour itself logical) mean colour 7. + let is7 = |c: Color| matches!(c, Color::ByLayer | Color::ByBlock | Color::Index(7)); + let aci7 = match c { + Color::ByLayer => is7(layer), + Color::ByBlock => is7(block), + c => c == Color::Index(7), + }; + Ink { rgb: c.resolve(layer, block), aci7 } +} + impl DisplayList { pub fn segment_count(&self) -> usize { self.prims.iter().filter(|p| p.kind == Kind::Polyline).map(|p| p.len.saturating_sub(1) as usize).sum() @@ -146,7 +175,7 @@ struct Builder<'a> { pub const MAX_BLOCK_EXPANSION: usize = 2_000_000; impl Builder<'_> { - fn polyline(&mut self, ctx: &Ctx, color: Rgb, lw: f32, pts: &[Vec2]) { + fn polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) { if pts.len() < 2 { return; } @@ -156,9 +185,9 @@ impl Builder<'_> { self.list.bounds.add(q); self.list.verts.push(q); } - self.list.prims.push(DPrim { handle: ctx.top, color, 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, kind: Kind::Polyline, start, len: pts.len() as u32 }); } - fn tris(&mut self, ctx: &Ctx, color: Rgb, tris: &[Vec2]) { + fn tris(&mut self, ctx: &Ctx, color: Ink, tris: &[Vec2]) { if tris.len() < 3 { return; } @@ -168,10 +197,10 @@ impl Builder<'_> { self.list.bounds.add(q); self.list.tris.push(q); } - self.list.prims.push(DPrim { handle: ctx.top, color, 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, 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: Rgb, lw: f32, sh: &cadcraft_fonts::Shaped) { + fn shaped(&mut self, ctx: &Ctx, color: Ink, lw: f32, sh: &cadcraft_fonts::Shaped) { for s in &sh.strokes { self.polyline(ctx, color, lw, s); } @@ -190,18 +219,18 @@ impl Builder<'_> { } } } - fn point(&mut self, ctx: &Ctx, color: Rgb, p: Vec2) { + fn point(&mut self, ctx: &Ctx, color: Ink, p: Vec2) { let q = ctx.xf.apply(p); 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, 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, kind: Kind::Point, start, len: 1 }); } - fn infinite(&mut self, ctx: &Ctx, color: Rgb, base: Vec2, dir: Vec2, ray: bool) { + fn infinite(&mut self, ctx: &Ctx, color: Ink, base: Vec2, dir: Vec2, ray: bool) { 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, 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, 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 { @@ -372,7 +401,7 @@ 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, 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, kind, start, len: pts.len() as u32 }); } /// Build the display list for a set of loose entities (previews, rubber bands). @@ -393,7 +422,7 @@ pub fn build_entities<'a, I: IntoIterator>(d: &Drawing, ents: b.list } -fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Rgb, f32, Option, f64, bool) { +fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Ink, f32, Option, f64, bool) { let d = ctx.d; // Layer "0" inside a block takes the insert's layer. let layer_name = if e.common.layer == "0" { ctx.block_layer.as_deref().unwrap_or("0") } else { e.common.layer.as_str() }; @@ -402,7 +431,7 @@ fn resolve(ctx: &Ctx, e: &Entity, plotting: bool) -> (Rgb, f32, Option layer.map(|l| l.lineweight).unwrap_or(Lineweight::Default), Lineweight::ByBlock => ctx.block_lw, @@ -495,7 +524,7 @@ fn entity(b: &mut Builder, ctx: &Ctx, e: &Entity) { h.loops.iter().map(|l| Polyline { vertices: l.vertices.clone(), closed: true }.tessellate(tol)).collect(); let tris = fill::triangulate_evenodd(&loops); let c = match &h.gradient { - Some(g) => g.color1.resolve(Color::Index(7), ctx.block_color), + Some(g) => ink(g.color1, Color::Index(7), ctx.block_color), None => rgb, }; b.tris(ctx, c, &tris); @@ -674,17 +703,19 @@ pub fn layout_mtext_entity(d: &Drawing, t: &cadcraft_doc::MText) -> cadcraft_fon cadcraft_fonts::layout_mtext_with(&t.contents, ¶ms) } -fn mtext_color(c: Option, rgb: Rgb) -> Rgb { +fn mtext_color(c: Option, rgb: Ink) -> Ink { match c { Some(cadcraft_fonts::MTextColor::Aci(i)) => { - u8::try_from(i).ok().filter(|i| *i > 0).map(|i| Color::Index(i).resolve(Color::Index(7), Color::Index(7))).unwrap_or(rgb) + u8::try_from(i).ok().filter(|i| *i > 0).map(|i| ink(Color::Index(i), Color::Index(7), Color::Index(7))).unwrap_or(rgb) + } + Some(cadcraft_fonts::MTextColor::Rgb(v)) => { + Ink { rgb: Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), aci7: false } } - Some(cadcraft_fonts::MTextColor::Rgb(v)) => Rgb(((v >> 16) & 0xFF) as u8, ((v >> 8) & 0xFF) as u8, (v & 0xFF) as u8), None => rgb, } } -fn mtext(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::MText, rgb: Rgb, lw: f32) { +fn mtext(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::MText, rgb: Ink, lw: f32) { let l = layout_mtext_entity(ctx.d, t); for p in &l.pieces { b.shaped(ctx, mtext_color(p.color, rgb), lw, &p.shaped); @@ -716,9 +747,9 @@ fn dimension(b: &mut Builder, ctx: &Ctx, e: &Entity, dm: &cadcraft_doc::Dimensio Color::ByBlock => ctx.block_color, c => c, }; - let dim_rgb = style.dim_line_color.resolve(layer_color, own); - let ext_rgb = style.ext_line_color.resolve(layer_color, own); - let txt_rgb = style.text_color.resolve(layer_color, own); + let dim_rgb = ink(style.dim_line_color, layer_color, own); + let ext_rgb = ink(style.ext_line_color, layer_color, own); + let txt_rgb = ink(style.text_color, layer_color, own); for (l, role) in g.lines.iter().zip(g.line_roles.iter().chain(std::iter::repeat(&LineRole::Dim))) { b.polyline(ctx, if *role == LineRole::Ext { ext_rgb } else { dim_rgb }, lw, l); } @@ -754,7 +785,7 @@ pub fn table_covered(t: &cadcraft_doc::Table) -> Vec> { cov } -fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Rgb, lw: f32) { +fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32) { let o = t.insert.xy(); let rows = t.row_heights.len().min(10_000); let cols = t.col_widths.len().min(10_000); @@ -867,7 +898,7 @@ fn sub_ctx<'a>(ctx: &Ctx<'a>, e: &Entity, m: Mat3) -> Ctx<'a> { } } -fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rgb: Rgb) { +fn insert(b: &mut Builder, ctx: &Ctx, e: &Entity, ins: &cadcraft_doc::Insert, rgb: Ink) { if ctx.depth >= cadcraft_doc::MAX_BLOCK_DEPTH { return; } diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index c04905c..b6ccccb 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -1,6 +1,6 @@ //! CPU rasteriser: draws a display list with tiny-skia (PNG export, plot preview, tests). -use cadcraft_color::{Rgb, display_rgb}; +use cadcraft_color::Rgb; use cadcraft_geom::{Bounds2, Vec2}; use tiny_skia::{FillRule, LineCap, LineJoin, Paint, PathBuilder, Pixmap, Stroke, Transform}; @@ -63,7 +63,7 @@ pub fn render(list: &DisplayList, view: &View, o: &RasterOptions) -> Option EntityKind { + EntityKind::Line(Line { a: Vec3::new(0.0, y, 0.0), b: Vec3::new(10.0, y, 0.0) }) +} + +/// An insert of block "B" (one ByBlock line along y = 0) at height `y`. +fn insert(y: f64) -> EntityKind { + EntityKind::Insert(Insert { + block: "B".into(), + insert: Vec3::new(0.0, y, 0.0), + 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, + }) +} + +/// One horizontal line per case at y = 0, 1, 2, ... and, per case, its name and the expected +/// colour on paper (white) and on the dark canvas. +fn cases() -> (Drawing, Vec<(&'static str, Rgb, Rgb)>) { + let mut d = Drawing::new_imperial(); + let mut blk = Block::new("B"); + let mut by_block = Entity::new(Handle(1), line(0.0)); + by_block.common.color = Color::ByBlock; + blk.entities.push(by_block); + d.blocks.insert("B".into(), std::sync::Arc::new(blk)); + let light_yellow = aci_rgb(51); + let dark_grey = aci_rgb(250); + let c = |color| Common { color, ..Default::default() }; + let rows: Vec<(&str, Common, EntityKind, Rgb, Rgb)> = vec![ + ("ByLayer on layer 0 (colour 7)", Common::default(), line(0.0), BLACK, WHITE), + ("index 7", c(Color::Index(7)), line(1.0), BLACK, WHITE), + ("ByBlock in a colour-7 insert", c(Color::Index(7)), insert(2.0), BLACK, WHITE), + ("index 51 (light yellow)", c(Color::Index(51)), line(3.0), light_yellow, light_yellow), + ("index 255 (white)", c(Color::Index(255)), line(4.0), WHITE, WHITE), + ("true-colour white", c(Color::True(WHITE)), line(5.0), WHITE, WHITE), + ("index 250 (dark grey)", c(Color::Index(250)), line(6.0), dark_grey, dark_grey), + ("true-colour black", c(Color::True(BLACK)), line(7.0), BLACK, BLACK), + ("ByBlock in an index-255 insert", c(Color::Index(255)), insert(8.0), WHITE, WHITE), + ]; + let mut out = Vec::new(); + for (name, common, kind, paper, canvas) in rows { + d.add(&Space::Model, common, kind).unwrap(); + out.push((name, paper, canvas)); + } + (d, out) +} + +#[test] +fn only_colour_7_flips_with_the_background() { + let (d, cases) = cases(); + let l = build(&d, &Space::Model, &Options::default()); + assert_eq!(l.prims.len(), cases.len()); + for (i, (name, paper, canvas)) in cases.iter().enumerate() { + let p = l.prims.iter().find(|p| (l.points(p)[0].y - i as f64).abs() < 1e-9).unwrap(); + assert_eq!(p.display_rgb(WHITE), *paper, "{name} on paper"); + assert_eq!(p.display_rgb(DARK), *canvas, "{name} on the dark canvas"); + } +} + +#[test] +fn png_draws_true_colours() { + let (d, cases) = cases(); + let l = build(&d, &Space::Model, &Options::default()); + let view = View { center: Vec2::new(5.0, 4.0), scale: 10.0, width: 120, height: 120 }; + for (bg, paper) in [(WHITE, true), (DARK, false)] { + let pm = render(&l, &view, &RasterOptions { background: bg, antialias: false, ..Default::default() }).unwrap(); + for (i, (name, on_paper, on_canvas)) in cases.iter().enumerate() { + let (x, y) = view.to_screen(Vec2::new(5.0, i as f64)); + let px = pm.pixel(x as u32, y as u32).unwrap(); + let want = if paper { on_paper } else { on_canvas }; + assert_eq!(Rgb(px.red(), px.green(), px.blue()), *want, "{name} on {bg:?}"); + } + } +} diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3517e4b..9a04167 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1,7 +1,7 @@ //! The drawing area: grid, entities, selection highlighting, grips, rubber bands, object-snap //! markers, crosshair cursor, ViewCube, UCS icon and mouse handling. -use cadcraft_color::{Rgb, display_rgb}; +use cadcraft_color::Rgb; use cadcraft_doc::Handle; use cadcraft_engine::Input; use cadcraft_engine::snap::{self, SnapHit}; @@ -255,7 +255,7 @@ fn draw_list(p: &egui::Painter, xf: &Xf, list: &DisplayList, bg: Rgb, lwdisplay: let mut shapes: Vec = Vec::with_capacity(list.prims.len()); let px = 1.0 / xf.scale; for prim in &list.prims { - let col = color32(display_rgb(prim.color, bg)); + let col = color32(prim.display_rgb(bg)); let pts = list.points(prim); match prim.kind { Kind::Polyline => { @@ -351,7 +351,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(display_rgb(prim.color, bg))))); + shapes.push(Shape::line_segment(seg, Stroke::new(1.0, color32(prim.display_rgb(bg))))); } } p.extend(shapes); @@ -1138,7 +1138,7 @@ mod tests { use cadcraft_render::DPrim; fn list(handles: &[u64]) -> DisplayList { - let prim = |h| DPrim { handle: Handle(h), color: Rgb(255, 255, 255), 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, kind: Kind::Polyline, start: 0, len: 0 }; DisplayList { prims: handles.iter().map(|&h| prim(h)).collect(), ..Default::default() } } diff --git a/crates/ui-egui/src/gpu.rs b/crates/ui-egui/src/gpu.rs index b52059f..a6b6bac 100644 --- a/crates/ui-egui/src/gpu.rs +++ b/crates/ui-egui/src/gpu.rs @@ -15,7 +15,7 @@ use std::sync::{Arc, Mutex}; -use cadcraft_color::{Rgb, display_rgb}; +use cadcraft_color::Rgb; use cadcraft_geom::{Bounds2, Vec2}; use cadcraft_render::{DisplayList, Kind}; use egui_wgpu::wgpu; @@ -147,7 +147,7 @@ pub fn build_mesh(list: &DisplayList, origin: Vec2, bg: Rgb, lwdisplay: bool) -> quads: Vec::new(), }; for prim in &list.prims { - let c = display_rgb(prim.color, bg); + let c = prim.display_rgb(bg); let col = [c.0, c.1, c.2, 255]; let pts = list.points(prim); match prim.kind { @@ -515,7 +515,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), lw, kind, start, len }; + let p = |kind, start, len, lw| DPrim { handle: Handle(1), color: Rgb(255, 255, 255), aci7: true, lw, 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() { @@ -534,7 +534,7 @@ mod tests { assert_eq!(m.quad_instances(), 1, "the point marker"); // First vertex is stored relative to the origin, so it's exactly zero. assert_eq!(&m.lines[0..8], &[0u8; 8]); - // White on a white background inverts to black (display_rgb). + // Colour 7 on a white background inverts to black (display_rgb). assert_eq!(&m.lines[8..12], &[0, 0, 0, 255]); let x = f32::from_le_bytes([m.lines[12], m.lines[13], m.lines[14], m.lines[15]]); assert_eq!(x, 1.0);