diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index a80735e18..88209381b 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -1126,9 +1126,14 @@ pub fn line_total_width( pub fn resolve_inline_color(c: &InlineColor) -> Option<[f32; 3]> { match c { InlineColor::Aci(idx) => aci_to_rgb(*idx).map(|(r, g, b)| { - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0] + let rgb = [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]; + if *idx == 7 { + rgb + } else { + crate::scene::convert::tess_util::authored_rgb(rgb) + } }), - InlineColor::True(rgb) => Some(*rgb), + InlineColor::True(rgb) => Some(crate::scene::convert::tess_util::authored_rgb(*rgb)), } } diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 5fef9656f..f75a1a6ed 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -719,7 +719,11 @@ fn append_pdf_page( // Near-white and near-yellow (viewport active border) → dark grey for print // (only when no CTB override was applied). if !color_overridden { - let is_light = r > 0.80 && g > 0.80 && b > 0.80; + // An authored white (not colour 7) plots as drawn, like on screen. + let is_light = r > 0.80 + && g > 0.80 + && b > 0.80 + && !crate::scene::convert::tess_util::is_authored_white([r, g, b]); let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; if is_light || is_yellow { @@ -1301,7 +1305,11 @@ fn emit_hatch( && hatch.aci != 0 && !(hatch.aci == 7 && matches!(hatch.pattern, HatchPattern::Solid)) { - let is_light = r > 0.80 && g > 0.80 && b > 0.80; + // An authored white (not colour 7) plots as drawn, like on screen. + let is_light = r > 0.80 + && g > 0.80 + && b > 0.80 + && !crate::scene::convert::tess_util::is_authored_white([r, g, b]); let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; if is_light || is_yellow { @@ -1482,7 +1490,11 @@ fn glyph_world_xy(v: &crate::scene::pipeline::text_gpu::TextVertex) -> [f64; 2] /// near-white / near-yellow (colour-7-on-white) → black, near-cyan → dark blue. #[cfg(not(target_arch = "wasm32"))] fn adapt_text_color([r, g, b]: [f32; 3]) -> [f32; 3] { - let is_light = r > 0.80 && g > 0.80 && b > 0.80; + // An authored white (not colour 7) plots as drawn, like on screen. + let is_light = r > 0.80 + && g > 0.80 + && b > 0.80 + && !crate::scene::convert::tess_util::is_authored_white([r, g, b]); let is_yellow = r > 0.80 && g > 0.70 && b < 0.30; let is_cyan = r < 0.30 && g > 0.70 && b > 0.70; if is_light || is_yellow { diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 5af7d3cd5..f72740a81 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -3136,19 +3136,22 @@ mod bg_resolution_tests { assert_eq!(colors(&once), colors(&twice)); } - /// Why the raw colour is stored instead of re-adapting the resolved one. - /// If this ever starts passing, `raw_color` could be dropped — and if it - /// is removed while this still fails, near-white geometry loses its tint - /// on the first layout switch. + /// Only colour 7's exact white / black swap with the background; a colour + /// drawn as authored — near-white, or a true-colour white mask — keeps its + /// value on any background. (#1500) #[test] - fn adapt_to_bg_cannot_be_reapplied() { - let once = adapt_to_bg(NEAR_WHITE, LIGHT); - assert_eq!(once, [0.0, 0.0, 0.0, 1.0], "near-white snaps to pure black"); - assert_ne!( - adapt_to_bg(once, DARK), - adapt_to_bg(NEAR_WHITE, DARK), - "re-adapting the resolved colour must not equal adapting the raw one", - ); + fn only_colour_seven_swaps_with_the_background() { + assert_eq!(adapt_to_bg([1.0, 1.0, 1.0, 1.0], LIGHT), [0.0, 0.0, 0.0, 1.0]); + assert_eq!(adapt_to_bg([0.0, 0.0, 0.0, 1.0], DARK), [1.0, 1.0, 1.0, 1.0]); + assert_eq!(adapt_to_bg(NEAR_WHITE, LIGHT), NEAR_WHITE); + let mask = crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::Rgb { + r: 255, + g: 255, + b: 255, + }); + assert_eq!(adapt_to_bg(mask, LIGHT), mask); + let seven = crate::scene::convert::tess_util::aci_to_rgba(&codec::types::Color::Index(7)); + assert_eq!(adapt_to_bg(seven, LIGHT), [0.0, 0.0, 0.0, 1.0]); } } diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index 24678504d..2247fe1c7 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -794,10 +794,14 @@ fn tessellate_entity_inner( a, ), ProxyColor::Rgb(r, g, b) => ( - view::render::adapt_to_bg( - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0], - bg_color, - ), + { + let [r, g, b] = convert::tess_util::authored_rgb([ + r as f32 / 255.0, + g as f32 / 255.0, + b as f32 / 255.0, + ]); + [r, g, b, 1.0] + }, 0, ), ProxyColor::Inherit => (entity_color, aci), diff --git a/src/scene/convert/tess_util.rs b/src/scene/convert/tess_util.rs index b2bba27a3..8a5cd99fd 100644 --- a/src/scene/convert/tess_util.rs +++ b/src/scene/convert/tess_util.rs @@ -31,8 +31,36 @@ pub type FallbackGeometry = ( /// Convert an opencadcodec Color (ACI index or true-color) to a GPU RGBA value. pub fn aci_to_rgba(color: &AcadColor) -> [f32; 4] { if let Some((r, g, b)) = color.rgb() { - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] + let rgb = [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]; + // Only colour 7 swaps white/black with the background; any other + // colour — a true colour, or ACI 255 — is drawn as authored. + let [r, g, b] = if matches!(color, AcadColor::Index(7)) { + rgb + } else { + authored_rgb(rgb) + }; + [r, g, b, 1.0] } else { WireModel::WHITE } } + +/// Pure white and pure black are what the background adaptation swaps, and +/// only colour 7 may be swapped. A colour chosen as-is (255,255,255 used as a +/// white mask on a light sheet, say) is kept a hair off them so it is drawn +/// exactly as authored; the difference is below one display step. +pub fn authored_rgb(rgb: [f32; 3]) -> [f32; 3] { + const OFF: f32 = 1.0 / 1024.0; + if rgb == [1.0; 3] { + [1.0 - OFF; 3] + } else if rgb == [0.0; 3] { + [OFF; 3] + } else { + rgb + } +} + +/// True for a colour [`authored_rgb`] moved off pure white. +pub fn is_authored_white(rgb: [f32; 3]) -> bool { + rgb == authored_rgb([1.0; 3]) +} diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index df488c8e5..7137d4d8d 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2868,12 +2868,10 @@ impl DimGeom { /// `ByLayer` / `ByBlock` (assumes those are already resolved upstream). pub(crate) fn color_or_inherit(c: &AcadColor, inherited: [f32; 4]) -> [f32; 4] { match c.rgb() { - Some((r, g, b)) => [ - r as f32 / 255.0, - g as f32 / 255.0, - b as f32 / 255.0, - inherited[3], - ], + Some(_) => { + let [r, g, b, _] = crate::scene::convert::tess_util::aci_to_rgba(c); + [r, g, b, inherited[3]] + } None => inherited, } } diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 143c0b1f7..09f986ea2 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -3774,8 +3774,11 @@ pub(crate) fn render_style_for_block_sub_viewport( /// backgrounds become white. All other colors pass through unchanged. pub(crate) fn adapt_to_bg(color: [f32; 4], bg: [f32; 4]) -> [f32; 4] { let lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]; - let is_white = color[0] > 0.95 && color[1] > 0.95 && color[2] > 0.95; - let is_black = color[0] < 0.05 && color[1] < 0.05 && color[2] < 0.05; + // Only exact white / black swap: that is colour 7 (and interface colours). + // Authored colours reach here nudged off both, so they stay as drawn — + // a true-colour white mask on a light sheet remains white. (#1500) + let is_white = color[..3] == [1.0; 3]; + let is_black = color[..3] == [0.0; 3]; if is_white && lum > 0.5 { [0.0, 0.0, 0.0, color[3]] } else if is_black && lum <= 0.5 {