diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index d020227e4..64616fd4e 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -2293,11 +2293,18 @@ impl MultiLeaderTess for MultiLeader { // Run-less stroke groups are decoration geometry (underline / // overline / strike-through, stacked-fraction bars) — they have // no glyphs to replace them, so they must be drawn as lines. + // Web runs that require bidi or joined-script shaping keep their + // already shaped vector geometry because the per-glyph SDF path has no + // cluster-position data. let mut deco_pts: Vec<[f32; 3]> = Vec::new(); let mut deco_fill: Vec<[f32; 3]> = Vec::new(); if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() { for ts in &layout.strokes { - let Some(run) = &ts.run else { + let is_shaped = ts + .run + .as_ref() + .is_some_and(|r| crate::scene::text::web_font::requires_shaping(&r.text)); + if ts.run.is_none() || is_shaped { for stroke in &ts.strokes { if stroke.len() < 2 { continue; @@ -2321,7 +2328,8 @@ impl MultiLeaderTess for MultiLeader { ]); } continue; - }; + } + let run = ts.run.as_ref().unwrap(); let quads = crate::scene::text::glyph_quads::layout_glyph_quads( &mut atlas, run.height, @@ -2349,7 +2357,7 @@ impl MultiLeaderTess for MultiLeader { ); } } - if !sdf_verts.is_empty() || !deco_pts.is_empty() { + if !sdf_verts.is_empty() || !deco_pts.is_empty() || !deco_fill.is_empty() { // Pick box from the glyph quads (f64 accumulate → f32). let (mut nx, mut ny, mut xx, mut xy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN); @@ -2367,6 +2375,12 @@ impl MultiLeaderTess for MultiLeader { ny = ny.min(p[1] as f64); xy = xy.max(p[1] as f64); } + for p in deco_fill.iter().filter(|p| p[0].is_finite()) { + nx = nx.min(p[0] as f64); + xx = xx.max(p[0] as f64); + ny = ny.min(p[1] as f64); + xy = xy.max(p[1] as f64); + } wires.push(WireModel { bg_adapt: None, point_marker: None, @@ -2618,3 +2632,51 @@ impl MultiLeaderTess for MultiLeader { wires } } + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::CadDocument; + + #[test] + fn test_mleader_arabic_text_tessellation() { + let doc = CadDocument::default(); + let mut ml = MultiLeader::default(); + ml.content_type = LeaderContentType::MText; + ml.context.text_string = "عربي".to_string(); + ml.context.text_height = 10.0; + ml.path_type = MultiLeaderPathType::StraightLineSegments; + + let wires = ml.tessellate( + &doc, + acadrust::Handle::default(), + false, + [1.0, 1.0, 1.0, 1.0], + 1.0, + 1.0, + None, + None, + None, + None, + None, + [0.0, 0.0, 0.0, 1.0], + ); + + // Find the text wire model: + // Arabic text requires shaping, so it should emit vector fill_tris + // instead of unshaped SDF glyph quads (text_verts). + let text_wire = wires + .iter() + .find(|w| !w.fill_tris.is_empty() || !w.text_verts.is_empty()) + .expect("Should produce a text wire for mleader"); + + assert!( + text_wire.text_verts.is_empty(), + "Arabic text must not be emitted as unshaped SDF glyph quads" + ); + assert!( + !text_wire.fill_tris.is_empty(), + "Arabic text must emit shaped vector fill triangles" + ); + } +} diff --git a/src/entities/table.rs b/src/entities/table.rs index cb1c8c9c9..8039b4615 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -1476,7 +1476,11 @@ pub fn tessellate_table( rustc_hash::FxHashSet::default(); let sel_col = WireModel::SELECTED; - let mut add_edge = |a: Vec3, b: Vec3, col: [f32; 4], lw: f32| { + let mut add_edge = |a: Vec3, + b: Vec3, + col: [f32; 4], + lw: f32, + borders: &mut HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)>| { let k = ( (a.x * 1000.0) as i32, (a.y * 1000.0) as i32, @@ -1627,19 +1631,19 @@ pub fn tessellate_table( }; let (tv, tc, tw) = edge(0); if tv { - add_edge(tl, tr, tc, tw); + add_edge(tl, tr, tc, tw, &mut borders); } let (rv, rc, rw) = edge(1); if rv { - add_edge(tr, br_, rc, rw); + add_edge(tr, br_, rc, rw, &mut borders); } let (bv, bc, bw) = edge(2); if bv { - add_edge(bl, br_, bc, bw); + add_edge(bl, br_, bc, bw, &mut borders); } let (lv, lc, lw) = edge(3); if lv { - add_edge(tl, bl, lc, lw); + add_edge(tl, bl, lc, lw, &mut borders); } let value_contents: Vec<_> = cell @@ -1960,6 +1964,41 @@ pub fn tessellate_table( let Some(run) = &stroke.run else { continue; }; + if crate::scene::text::web_font::requires_shaping(&run.text) { + if !stroke.fill_tris.is_empty() { + let entry = fills + .entry(key4(tcol)) + .or_insert_with(|| (tcol, Vec::new())); + for &[x, y] in &stroke.fill_tris { + entry.1.push([ + stroke.origin[0] as f32 + x, + stroke.origin[1] as f32 + y, + to.z as f32, + ]); + } + } + if !stroke.strokes.is_empty() { + let entry = borders + .entry((key4(tcol), (line_weight_px * 100.0) as u32)) + .or_insert_with(|| (tcol, line_weight_px, Vec::new())); + for s in &stroke.strokes { + if s.len() < 2 { + continue; + } + if !entry.2.is_empty() { + entry.2.push([f32::NAN; 3]); + } + for &[x, y] in s { + entry.2.push([ + stroke.origin[0] as f32 + x, + stroke.origin[1] as f32 + y, + to.z as f32, + ]); + } + } + } + continue; + } let quads = crate::scene::text::glyph_quads::layout_glyph_quads( &mut atlas, run.height,