From 0964e97cfa597d0a80196c581492bc38356e294b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 7 Jul 2026 22:17:27 +0300 Subject: [PATCH] fix(text): keep inline \C colours through block colour inheritance (SDF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports PR #301 (by Kevin Griffin) to the SDF text renderer. Block-nested MTEXT with inline colour codes (\C1;, \c…) collapsed to a single colour: the block cache applies ByBlock / layer-0 colour inheritance per entity, so at emit time every segment's colour was replaced with the inherited colour — including segments carrying an explicit inline override. - block_cache tessellate_sub_local: gate ByBlock / layer-0 colour inheritance on `wire_on_base_color` (a wire whose colour differs from the entity base carries an explicit override and keeps it) — #301's fix, still correct for the remaining stroke wires. - block_cache emit_wire: the per-vertex analogue for SDF text — a glyph whose colour equals the wire's base inherits the resolved colour; a glyph with an inline \C / \c override keeps it. (Previously every block glyph was recoloured with final_color, collapsing colour-split MTEXT.) - tests/text_font_rendering.rs: restore the block colour-split regression test (it had stopped compiling on main when expand_insert gained the InheritStyle param) and adapt it — and drawable_point_count — to SDF text_verts, since block MTEXT now renders as glyph quads, not colour-split stroke wires. \C (ACI) / \c (true-colour) are parsed by acadrust (mtext_format SpanProperties.color) → OCS InlineColor → the glyph's per-vertex colour. Co-Authored-By: Kevin Griffin <117586586+KevinGriffin-new@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- src/scene/cache/block_cache.rs | 24 ++++++++++++++++++++--- tests/text_font_rendering.rs | 36 ++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 31b9bba1..28c939b6 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -447,6 +447,13 @@ fn tessellate_sub_local( .chain(wire.text_verts.iter().map(|v| v.pos)), ); let is_fill_only = wire.points.is_empty() && !wire.fill_tris.is_empty(); + // A wire whose colour differs from the entity's resolved base colour + // carries an explicit per-segment override (e.g. an MTEXT `\C1;` inline + // colour). ByBlock / layer-0 inheritance applies only to wires still on + // the base colour — folding an explicit segment into the inherited + // colour would collapse colour-split geometry to one colour. (PR #301, + // Kevin Griffin — extended to SDF text per-vertex in emit_wire.) + let wire_on_base_color = wire.color == sub_color; result.push(LocalWire { points: wire.points, @@ -464,10 +471,10 @@ fn tessellate_sub_local( line_weight_px: lw_px, plinegen: wire.plinegen, is_fill_only, - color_is_byblock, + color_is_byblock: color_is_byblock && wire_on_base_color, lt_is_byblock, lw_is_byblock, - color_l0, + color_l0: color_l0 && wire_on_base_color, lt_l0, lw_l0, aabb_local, @@ -1243,11 +1250,22 @@ fn emit_wire( if hy > entry.max_y { entry.max_y = hy; } + // Base glyphs inherit the resolved (ByBlock / layer-0) colour; a glyph + // carrying an inline `\C` / `\c` override — colour differs from the + // wire's base — keeps it, so block-nested colour-split MTEXT stays + // multi-colour. Per-vertex analogue of PR #301's wire-level gate. + let rgb = if [tv.color[0], tv.color[1], tv.color[2]] + == [lw.color[0], lw.color[1], lw.color[2]] + { + [final_color[0], final_color[1], final_color[2]] + } else { + [tv.color[0], tv.color[1], tv.color[2]] + }; entry.text_verts.push(crate::scene::pipeline::text_gpu::TextVertex { pos: [hx, hy, hz], pos_low: [lx, ly, lz], uv: tv.uv, - color: [final_color[0], final_color[1], final_color[2], tv.color[3]], + color: [rgb[0], rgb[1], rgb[2], tv.color[3]], draw_depth: tv.draw_depth, }); } diff --git a/tests/text_font_rendering.rs b/tests/text_font_rendering.rs index b0e1aa47..c309ec83 100644 --- a/tests/text_font_rendering.rs +++ b/tests/text_font_rendering.rs @@ -3,12 +3,19 @@ use acadrust::tables::{BlockRecord, TextStyle}; use acadrust::types::Vector3; use acadrust::{CadDocument, EntityType, Handle}; use OpenCADStudio::scene::cache::block_cache::{expand_insert, BlockCache}; +use OpenCADStudio::scene::view::render::InheritStyle; use OpenCADStudio::scene::WireModel; fn drawable_point_count(wires: &[WireModel]) -> usize { + // SDF text carries glyph quads on `text_verts` (no stroke points/fills), so + // count those too — otherwise "did the text render?" reads as zero. wires .iter() - .map(|w| w.points.iter().filter(|p| p[0].is_finite()).count() + w.fill_tris.len()) + .map(|w| { + w.points.iter().filter(|p| p[0].is_finite()).count() + + w.fill_tris.len() + + w.text_verts.len() + }) .sum() } @@ -51,6 +58,14 @@ fn expand_block_mtext( 0.0, [0.0; 8], 1.0, + // The INSERT sits on layer "0" which resolves to the white/Continuous + // fallback — matching the resolved style passed above. + InheritStyle { + color: [1.0, 1.0, 1.0, 1.0], + pat_len: 0.0, + pat: [0.0; 8], + lw_px: 1.0, + }, false, 1.0, None, @@ -91,9 +106,10 @@ fn block_nested_mtext_uses_its_style_font() { #[test] fn block_nested_colour_split_mtext_keeps_per_wire_colour() { // `\C1;` = ACI red, `\C2;` = ACI yellow → two colour bins. The block cache - // must keep them as separate per-wire colours; the fold-to-one-colour bug - // this PR fixes silently collapsed every segment to the first colour. Uses - // the builtin stroke font so the split is observable without a system TTF. + // must keep them as separate per-glyph colours; the fold-to-one-colour bug + // (PR #301, Kevin Griffin) collapsed every segment to the inherited colour. + // SDF text carries per-glyph colour on `text_verts`, so the split is checked + // there (not per-wire). Uses the builtin stroke font so it works without TTF. let wires = expand_block_mtext( "\\C1;AAA\\C2;BBB", "txt", @@ -103,12 +119,12 @@ fn block_nested_colour_split_mtext_keeps_per_wire_colour() { let mut colours: Vec<[u8; 3]> = wires .iter() - .filter(|w| !w.points.is_empty() || !w.fill_tris.is_empty()) - .map(|w| { + .flat_map(|w| w.text_verts.iter()) + .map(|v| { [ - (w.color[0] * 255.0).round() as u8, - (w.color[1] * 255.0).round() as u8, - (w.color[2] * 255.0).round() as u8, + (v.color[0] * 255.0).round() as u8, + (v.color[1] * 255.0).round() as u8, + (v.color[2] * 255.0).round() as u8, ] }) .collect(); @@ -117,7 +133,7 @@ fn block_nested_colour_split_mtext_keeps_per_wire_colour() { assert!( colours.len() >= 2, - "colour-split MTEXT in a block must keep ≥2 distinct wire colours, got {colours:?}" + "colour-split MTEXT in a block must keep ≥2 distinct glyph colours, got {colours:?}" ); }