ACAD_TABLE from other writers: keep style text height, read cell text height, lay out cell text as MTEXT (#802) (#806)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
4257674dfa
commit
966356545d
|
|
@ -831,6 +831,10 @@ pub struct TableCell {
|
|||
/// A formula (`=Sum(A1:B3)`); `text` holds its value.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub formula: Option<String>,
|
||||
/// The cell's own text height (DXF 140 in the cell record), before the table's scale;
|
||||
/// `None`: its cell style's.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub text_height: Option<f64>,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
|||
|
|
@ -527,7 +527,18 @@ fn acad_table(tags: &[Tag]) -> Table {
|
|||
(true, Some(rs), Some(cs)) if rs >= 1 && cs >= 1 && (rs, cs) != (1, 1) => Some((rs, cs)),
|
||||
_ => None,
|
||||
};
|
||||
flat.push(TableCell { text, merged, ..TableCell::default() });
|
||||
// The cell's own text height: 140 outside the value block (301 … 304), where it is a number.
|
||||
let mut in_value = false;
|
||||
let mut text_height = None;
|
||||
for x in cell {
|
||||
match x.code {
|
||||
301 => in_value = true,
|
||||
304 => in_value = false,
|
||||
140 if !in_value => text_height = Some(x.f64()).filter(|h| h.is_finite() && *h > 0.0 && *h < 1e9),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
flat.push(TableCell { text, merged, text_height, ..TableCell::default() });
|
||||
}
|
||||
flat.resize(rows * cols, TableCell::default());
|
||||
let cells: Vec<Vec<TableCell>> = if cols == 0 { vec![Vec::new(); rows] } else { flat.chunks(cols).map(<[TableCell]>::to_vec).collect() };
|
||||
|
|
@ -541,7 +552,8 @@ fn acad_table(tags: &[Tag]) -> Table {
|
|||
(flags.first().is_some_and(|v| *v != 0), flags.get(1).is_some_and(|v| *v != 0), h, style)
|
||||
}
|
||||
// Other writers: a first row merged across every column reads as a title.
|
||||
None => (cols > 1 && cells.first().and_then(|r| r.first()).and_then(|c| c.merged) == Some((1, cols as u32)), false, 0.18, "Standard".into()),
|
||||
// Their text height is resolved to the table style's once the styles are read (`apply`).
|
||||
None => (cols > 1 && cells.first().and_then(|r| r.first()).and_then(|c| c.merged) == Some((1, cols as u32)), false, 0.0, "Standard".into()),
|
||||
};
|
||||
let mut t = Table { insert: T(tags).p(10), col_widths, row_heights, cells, style, text_height, title, header, ..Table::default() };
|
||||
crate::dxf_table::read_table_xdata(tags, &mut t);
|
||||
|
|
@ -1012,8 +1024,8 @@ fn entity_extras(kind: &str, tags: &[Tag], h: Handle, k: &mut EntityKind, rx: &m
|
|||
rx.table_blocks.insert(b.to_ascii_uppercase());
|
||||
}
|
||||
let ours = crate::dxf_ext::xdata(tags, crate::dxf_ext::APP).iter().any(|t| t.code == 1000 && t.str() == "TABLE");
|
||||
if !ours && let Some(sh) = t.s(342) {
|
||||
rx.table_style_fix.push((h, sh.trim().to_ascii_uppercase()));
|
||||
if !ours {
|
||||
rx.table_style_fix.push((h, t.s(342).map(|sh| sh.trim().to_ascii_uppercase()).unwrap_or_default()));
|
||||
}
|
||||
}
|
||||
("INSERT", EntityKind::Insert(ins)) => {
|
||||
|
|
@ -1894,13 +1906,24 @@ impl Objects {
|
|||
}
|
||||
}
|
||||
for (h, sh) in &rx.table_style_fix {
|
||||
if let Some(name) = self.names.get(sh) {
|
||||
let _ = d.modify_entity(*h, |e| {
|
||||
if let EntityKind::Table(t) = &mut e.kind {
|
||||
t.style = name.clone();
|
||||
let name = self.names.get(sh).cloned();
|
||||
// A table from another writer has no scale of its own: its text height is its
|
||||
// style's, so every cell style height applies as it is.
|
||||
let height = {
|
||||
let style = name.as_deref().unwrap_or("Standard");
|
||||
d.table_styles
|
||||
.iter()
|
||||
.find(|s| s.name.eq_ignore_ascii_case(style))
|
||||
.map_or(cadcraft_doc::TableStyle::default().text_height, |s| s.text_height)
|
||||
};
|
||||
let _ = d.modify_entity(*h, |e| {
|
||||
if let EntityKind::Table(t) = &mut e.kind {
|
||||
if let Some(name) = name {
|
||||
t.style = name;
|
||||
}
|
||||
});
|
||||
}
|
||||
t.text_height = height;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Parametric constraints and parameters.
|
||||
if let Some((c, p)) = self.xrecord_text(crate::dxf_ext::CONSTRAINTS_KEY).and_then(|t| crate::dxf_ext::parse_constraints(&t)) {
|
||||
|
|
|
|||
|
|
@ -856,6 +856,9 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
|
|||
w.i(91, 0);
|
||||
w.i(178, 0);
|
||||
w.f(145, 0.0);
|
||||
if let Some(h) = cell.and_then(|x| x.text_height).filter(|h| h.is_finite() && *h > 0.0) {
|
||||
w.f(140, h);
|
||||
}
|
||||
// Long text: 250-character code-2 chunks, the rest in code 1.
|
||||
let chars: Vec<char> = cell.map(|x| x.text.chars().collect()).unwrap_or_default();
|
||||
let chunks: Vec<String> = chars.chunks(250).map(|c| c.iter().collect()).collect();
|
||||
|
|
|
|||
|
|
@ -1375,6 +1375,8 @@ fn hostile_extension_data_never_panics() {
|
|||
// Tables: enormous or negative counts, cells without bodies, spans past the edge.
|
||||
ent("0\nACAD_TABLE\n2\n*T1\n100\nAcDbTable\n91\n999999999\n92\n999999999\n171\n1\n0\nACAD_TABLE\n100\nAcDbTable\n91\n-4\n92\n3\n171\n"),
|
||||
ent("0\nACAD_TABLE\n100\nAcDbTable\n91\n2\n92\n2\n141\nnan\n142\n-1\n171\n1\n173\n1\n175\n99999\n176\n-3\n1\nx\n171\n171\n171\n171\n171\n2\nzzz\n1001\nCADCRAFT\n1000\nTABLE\n1040\n-1\n"),
|
||||
// Cell text heights (140): junk, huge, unbalanced value block.
|
||||
ent("0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n3\n171\n1\n140\nnan\n171\n1\n140\n-5\n301\nCELL_VALUE\n140\n1e308\n171\n1\n304\nACVALUE_END\n140\n1e999\n1\nx\n"),
|
||||
// DIMASSOC pointing at nothing, or at a non-dimension.
|
||||
format!(
|
||||
"{}{}",
|
||||
|
|
@ -2136,3 +2138,23 @@ fn kept_xrecord_application_group_roundtrips_as_read() {
|
|||
// And no handle is written under a bool code anywhere.
|
||||
assert!(!out.contains("291\n2"), "no handle may be written under a bool code");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn foreign_table_reads_cell_text_height_and_keeps_style_scale() {
|
||||
// A table from another writer: no CADCraft xdata; cell 1 sets 140 before its value block
|
||||
// (the cell's text height), cell 2 only has a 140 inside the value block (a number).
|
||||
let dxf = "0\nSECTION\n2\nENTITIES\n0\nACAD_TABLE\n100\nAcDbTable\n91\n1\n92\n2\n141\n300\n142\n900\n142\n900\n\
|
||||
171\n1\n140\n150\n301\nCELL_VALUE\n1\nA\n304\nACVALUE_END\n\
|
||||
171\n1\n301\nCELL_VALUE\n140\n42\n1\nB\n304\nACVALUE_END\n0\nENDSEC\n0\nEOF\n";
|
||||
let d = read_dxf(dxf.as_bytes()).unwrap();
|
||||
let t = first(&d, |k| if let EntityKind::Table(x) = k { Some(x.clone()) } else { None });
|
||||
assert_eq!(t.cells[0][0].text_height, Some(150.0));
|
||||
assert_eq!(t.cells[0][1].text_height, None);
|
||||
// No scale of its own: the table takes its style's height.
|
||||
assert_eq!(t.text_height, TableStyle::default().text_height);
|
||||
// Written back and read again.
|
||||
let back = roundtrip(&d);
|
||||
let t2 = first(&back, |k| if let EntityKind::Table(x) = k { Some(x.clone()) } else { None });
|
||||
assert_eq!(t2.cells[0][0].text_height, Some(150.0));
|
||||
assert_eq!(t2.cells[0][1].text_height, None);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ pub fn table_style_of(d: &Drawing, t: &Table) -> TableStyle {
|
|||
/// The scale of a table relative to its style: SCALE changes the table's text height, and every
|
||||
/// cell style height and margin follows.
|
||||
fn style_scale(st: &TableStyle, t: &Table) -> f64 {
|
||||
// A table from another writer is resolved to its style's height on reading (scale 1); a
|
||||
// non-positive height here is the same "no scale of its own".
|
||||
let k = t.text_height / st.text_height;
|
||||
if k.is_finite() && k > 0.0 { k } else { 1.0 }
|
||||
}
|
||||
|
|
@ -312,7 +314,7 @@ fn layout(d: &Drawing, t: &Table) -> TableGeom {
|
|||
col: a.col,
|
||||
at: p(x, y),
|
||||
attach,
|
||||
height: cs.text_height,
|
||||
height: cell.text_height.filter(|h| h.is_finite() && *h > 0.0).map_or(cs.text_height, |h| h * k),
|
||||
color: cs.text_color,
|
||||
style: cs.text_style.clone(),
|
||||
text: cell.text.clone(),
|
||||
|
|
@ -352,27 +354,18 @@ pub(crate) fn draw(b: &mut Builder, ctx: &Ctx, t: &Table, rgb: Ink, lw: f32) {
|
|||
for tx in &g.texts {
|
||||
let ts = crate::text_style(ctx.d, &tx.style);
|
||||
let font = cadcraft_fonts::TextFont::resolve(&ts.font);
|
||||
let h = match (tx.attach - 1) % 3 {
|
||||
0 => cadcraft_fonts::Align::Left,
|
||||
// Middle centre is TEXT's "Middle" (centred both ways).
|
||||
1 if tx.attach == 5 => cadcraft_fonts::Align::Middle,
|
||||
1 => cadcraft_fonts::Align::Center,
|
||||
_ => cadcraft_fonts::Align::Right,
|
||||
};
|
||||
let v = match (tx.attach - 1) / 3 {
|
||||
0 => cadcraft_fonts::VAlign::Top,
|
||||
1 => cadcraft_fonts::VAlign::Middle,
|
||||
_ => cadcraft_fonts::VAlign::Bottom,
|
||||
};
|
||||
let params = cadcraft_fonts::TextParams {
|
||||
align_pt: Some(tx.at),
|
||||
h,
|
||||
v,
|
||||
// Cell content is MTEXT (`\P`, `{\Q15;…}`, `\F…;`): laid out as such, at the cell's
|
||||
// alignment point (its numbering is MTEXT's attachment).
|
||||
let mp = cadcraft_fonts::MTextParams {
|
||||
attach: tx.attach,
|
||||
font,
|
||||
width_factor: ts.width_factor,
|
||||
oblique: ts.oblique,
|
||||
..cadcraft_fonts::TextParams::new(tx.at, tx.height)
|
||||
..cadcraft_fonts::MTextParams::new(tx.at, tx.height)
|
||||
};
|
||||
let (sh, _) = cadcraft_fonts::place(&font, &tx.text, ¶ms);
|
||||
b.shaped(ctx, style_ink(tx.color, rgb), lw, &sh);
|
||||
let layout = cadcraft_fonts::layout_mtext_with(&tx.text, &mp);
|
||||
for piece in &layout.pieces {
|
||||
b.shaped(ctx, style_ink(tx.color, rgb), lw, &piece.shaped);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -676,3 +676,24 @@ fn mtext_exact_spacing_ignores_taller_lines() {
|
|||
assert!((height("A\\PBB\\PCC", 1.0, exact) - (1.0 + 2.0 * 5.0 / 3.0)).abs() < 1e-9, "uniform lines, exact {exact}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_cell_text_height_scales_with_the_table() {
|
||||
let height = |table_h: f64, own: Option<f64>| {
|
||||
let d = Drawing::new_imperial();
|
||||
let st = crate::table_style_of(&d, &cadcraft_doc::Table::default());
|
||||
let cell = cadcraft_doc::TableCell { text: "a".into(), text_height: own, ..Default::default() };
|
||||
let t = cadcraft_doc::Table {
|
||||
col_widths: vec![500.0],
|
||||
row_heights: vec![500.0],
|
||||
cells: vec![vec![cell]],
|
||||
text_height: table_h * st.text_height,
|
||||
..Default::default()
|
||||
};
|
||||
crate::table_geometry(&d, &t).texts.first().map(|x| x.height)
|
||||
};
|
||||
let base = height(1.0, None).unwrap();
|
||||
assert_eq!(height(1.0, Some(150.0)), Some(150.0));
|
||||
assert_eq!(height(2.0, Some(150.0)), Some(300.0));
|
||||
assert_eq!(height(2.0, None), Some(base * 2.0));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue