use acadrust::entities::{Insert, MText}; 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() + w.text_verts.len() }) .sum() } /// Build a one-block document holding a single MTEXT and expand the INSERT /// through the block cache, returning the finalized wires. `mtext_at` is the /// MTEXT's position inside the block; `insert_at` is where the block is placed. fn expand_block_mtext( value: &str, font_file: &str, mtext_at: Vector3, insert_at: Vector3, ) -> Vec { let mut doc = CadDocument::new(); let mut style = TextStyle::new("SHOP"); style.font_file = font_file.to_string(); doc.text_styles.add(style).unwrap(); let br_h = Handle::new(doc.next_handle()); let mut br = BlockRecord::new("LABEL_BLOCK"); br.handle = br_h; doc.block_records.add(br).unwrap(); let mut mtext = MText::with_value(value, mtext_at); mtext.style = "SHOP".to_string(); mtext.height = 20.0; mtext.rectangle_width = 0.0; let mut sub = EntityType::MText(mtext); sub.common_mut().owner_handle = br_h; doc.add_entity(sub).unwrap(); let ins = Insert::new("LABEL_BLOCK", insert_at); doc.add_entity(EntityType::Insert(ins.clone())).unwrap(); let cache = BlockCache::build( &doc, 1.0, None, true, [0.0, 0.0, 0.0, 1.0], None, &Default::default(), ); expand_insert( &cache, &ins, Handle::new(999), [1.0, 1.0, 1.0, 1.0], 0, 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, }, 0, false, false, 1.0, None, None, false, [0.0, 0.0, 0.0, 1.0], // Annotation scale: 1.0 = no annotative scaling, matching the // `BlockCache::build(&doc, 1.0, ...)` call above. 1.0, ) .expect("block defn is cached") } #[test] fn block_nested_mtext_uses_its_style_font() { // NB: on a host without Arial, `arial.ttf` resolves to the LFF stroke // fallback, so this exercises the "something renders" path rather than the // TTF canonicalisation specifically. The font-canonicalisation logic itself // is covered by the unit tests in `text_support`; the colour-split and // fill-pairing tests below cover the block-cache rendering changes. let wires = expand_block_mtext( "FERRAGAMO", "arial.ttf", Vector3::new(0.0, 0.0, 0.0), Vector3::new(100.0, 50.0, 0.0), ); assert!( drawable_point_count(&wires) > 0, "block-nested MTEXT should render through its text style font" ); assert!( wires .iter() .all(|w| w.points.is_empty() || w.fill_tris.is_empty()), "outline and fill wires should be separate for correct GPU classification" ); } #[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-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", Vector3::new(0.0, 0.0, 0.0), Vector3::new(100.0, 50.0, 0.0), ); let mut colours: Vec<[u8; 3]> = wires .iter() .flat_map(|w| w.text_verts.iter()) .map(|v| { [ (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(); colours.sort(); colours.dedup(); assert!( colours.len() >= 2, "colour-split MTEXT in a block must keep ≥2 distinct glyph colours, got {colours:?}" ); } #[test] fn block_nested_mtext_fill_tris_keep_paired_low_half_at_utm() { // TTF glyph fills are host-dependent (builtin LFF stroke fonts produce // none). When the host resolves a TTF and fills ARE produced, every fill // wire must carry an index-paired `fill_tris_low`: emit_wire reconstructs // `fill_tris[i] + fill_tris_low[i]`, so an unpaired wire panics (release // too) and a dropped low half quantizes fills to ~0.5 m at UTM scale. // The MTEXT sits at a UTM-scale coordinate so the low half is significant. // "DejaVu Sans" is present on most Linux hosts; where no TTF resolves the // fill set is empty and the assertions are skipped (gate below). let wires = expand_block_mtext( "FERRAGAMO", "DejaVu Sans", Vector3::new(500_000.0, 4_000_000.0, 0.0), Vector3::new(0.0, 0.0, 0.0), ); let fill_wires: Vec<&WireModel> = wires.iter().filter(|w| !w.fill_tris.is_empty()).collect(); if fill_wires.is_empty() { eprintln!("no TTF fills resolvable on this host; skipping fill-pairing assertions"); return; } for w in &fill_wires { assert_eq!( w.fill_tris.len(), w.fill_tris_low.len(), "fill_tris and fill_tris_low must be index-paired for emit_wire" ); } let any_nonzero_low = fill_wires .iter() .flat_map(|w| w.fill_tris_low.iter()) .any(|p| p.iter().any(|&c| c != 0.0)); assert!( any_nonzero_low, "UTM-scale fills must keep a non-zero low half — a dropped fill_tris_low \ silently quantizes fill triangles to ~0.5 m" ); }