AutoCAD interop: justified text, DIMPOST, current dimension style, *D block colours (#1062)

* AutoCAD interop: justified text, DIMPOST, current dimension style, *D block colours

What AutoCAD takes from CADCraft's DWG/DXF as it is:

- Justified TEXT/ATTRIB/ATTDEF (#1050): group 10 is now where the
  baseline starts, from the alignment point and the text's width, not
  the alignment point itself; AutoCAD drew centred and right-aligned
  text from DWG starting at its alignment point. A stored point close to
  CADCraft's own (AutoCAD's, exact for its fonts) is kept.
- DIMPOST (#1053) is written even when empty: the DWG bridge defaulted a
  missing one to "<>", and AutoCAD then dropped the "R" of radius
  dimensions when it regenerated them.
- The current dimension style (#1051): the DXF header now carries its
  DIM* variables (AutoCAD draws new and updated dimensions from them);
  the DWG bridge points $DIMSTYLE at the style by name (its handle was
  renumbered, so the writer fell back to Standard), and reading a DWG
  gives the current dimension and text styles their names back.
- *D blocks (#1052): lines, arrows, extension lines and text take the
  style's DIMCLRD/DIMCLRE/DIMCLRT instead of ByBlock.

#1054 (group 42 of a rotated linear dimension) was already fixed on
main by #855; acad_interop.rs keeps it covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Test: angular dimensions store their angle in radians (group 42)

Three-point (60°, 174°) and two-line (120°) angular dimensions, read back
from the DWG as DXF. Already right on main since #855; this guards it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Justified text made here: Left Middle and Left Bottom write their baseline start too

text_insert kept a stored insertion point that was within 0.1 x an
estimated width + 0.5 x height of CADCraft's baseline start, taking it for
AutoCAD's own. The width was estimated from the alignment point's offset,
which for Left Middle is only h/2 (vertical), so a text made here, whose
stored point is the alignment point, passed and kept it: AutoCAD drew it
half a height high (Left Bottom: a third). Reported with a test by
@lee-cyrille-maskell.

A stored point equal to the alignment point is now always stale. Otherwise
the offset is split along and across the baseline: along it the same slack
as before (a tenth of the text's width plus half its height, as fonts and
their substitutes differ most in width), across it a fifth of the height.
Multiline attribute values keep the distance-only check, since their lines
are placed by the embedded MTEXT.

On 12 drawings made in AutoCAD (1,055 justified texts) the insertion points
written are the same as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
aldozar
2026-10-19 02:28:47 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7164b6b5b1
commit f0e4fef003
7 changed files with 496 additions and 11 deletions
+26
View File
@@ -85,6 +85,7 @@ mod native {
}
let notes = read_notes(&outcome.stats);
let mut doc = outcome.document;
current_style_names(&mut doc);
table_merges(&mut doc);
table_style_rows(&mut doc, false);
let mut out = CappedWriter { buf: Vec::new(), limit: limits.max_dxf_bytes, exceeded: false };
@@ -385,6 +386,30 @@ mod native {
convert(dxf, Some(v))
}
/// The current dimension and text styles' names from their handles: a DWG stores the
/// handles, and the reader leaves the names at "Standard", so a drawing reopened from DWG
/// lost its current dimension style (#1051).
fn current_style_names(doc: &mut acadrust::CadDocument) {
let h = doc.header.current_dimstyle_handle;
if let Some(n) = doc.dim_styles.iter().find(|ds| ds.handle == h && !h.is_null()).map(|ds| ds.name.clone()) {
doc.header.current_dimstyle_name = n;
}
let h = doc.header.current_text_style_handle;
if let Some(n) = doc.text_styles.iter().find(|ts| ts.handle == h && !h.is_null()).map(|ts| ts.name.clone()) {
doc.header.current_text_style_name = n;
}
}
/// The current dimension style (`$DIMSTYLE`) by its name: the DXF gives the name, and the
/// handle read with it needn't be the style's in the document, in which case the DWG writer
/// fell back to Standard and AutoCAD opened the drawing with Standard current (#1051).
fn current_dimstyle(doc: &mut acadrust::CadDocument) {
let name = doc.header.current_dimstyle_name.clone();
if let Some(h) = doc.dim_styles.iter().find(|ds| ds.name.eq_ignore_ascii_case(&name)).map(|ds| ds.handle) {
doc.header.current_dimstyle_handle = h;
}
}
fn convert(dxf: &[u8], version: Option<acadrust::DxfVersion>) -> Result<Vec<u8>, String> {
let data = dxf.to_vec();
let mut doc = std::panic::catch_unwind(move || acadrust::DxfReader::from_reader(Cursor::new(data)).and_then(|r| r.read()))
@@ -400,6 +425,7 @@ mod native {
}
table_style_rows(&mut doc, true);
drop_unwritable(&mut doc);
current_dimstyle(&mut doc);
std::panic::catch_unwind(AssertUnwindSafe(|| acadrust::DwgWriter::write_to_vec(&doc)))
.map_err(|_| "the DWG writer failed on this drawing".to_string())?
.map_err(|e| format!("DWG write: {e}"))
+39
View File
@@ -490,6 +490,45 @@ impl TextParams {
}
}
/// Where a single-line TEXT's baseline starts (the left end of its first character's baseline),
/// in world coordinates: what DXF/DWG store as a justified text's insertion point (group 10),
/// computed from its alignment point (group 11) and its width in `font`.
pub fn baseline_start(font: &TextFont, s: &str, p: &TextParams) -> Vec2 {
let (origin, local, rot, mirror) = frame(font, s, p);
origin + Vec2::new(local.x * mirror.x, local.y * mirror.y).rotate(rot)
}
/// How [`place`] puts a line in the world: the point it is placed by, the offset of its baseline
/// start from that point (along the text), its rotation and its mirroring (backwards, upside
/// down). Aligned and fit text are placed by their insertion point.
fn frame(font: &TextFont, s: &str, p: &TextParams) -> (Vec2, Vec2, f64, Vec2) {
let height = if p.height > 0.0 && p.height.is_finite() { p.height } else { 1.0 };
let rot = if p.rotation.is_finite() { p.rotation } else { 0.0 };
if matches!(p.h, Align::Aligned | Align::Fit) {
let rot = p.align_pt.map_or(rot, |p2| p.insert.angle_to(p2));
return (p.insert, Vec2::ZERO, rot, Vec2::new(1.0, 1.0));
}
let width = shape_line(font, s, height, p.width_factor, p.oblique).width;
let origin = match (p.h, p.v) {
(Align::Left, VAlign::Baseline) => p.insert,
_ => p.align_pt.unwrap_or(p.insert),
};
let dx = match p.h {
Align::Left | Align::Aligned | Align::Fit => 0.0,
Align::Center | Align::Middle => -width / 2.0,
Align::Right => -width,
};
let dy = match (p.h, p.v) {
(Align::Middle, _) => -height / 2.0,
(_, VAlign::Baseline) => 0.0,
(_, VAlign::Bottom) => height / 3.0,
(_, VAlign::Middle) => -height / 2.0,
(_, VAlign::Top) => -height,
};
let mirror = Vec2::new(if p.backwards { -1.0 } else { 1.0 }, if p.upside_down { -1.0 } else { 1.0 });
(origin, Vec2::new(dx, dy), rot, mirror)
}
/// Place a single-line TEXT in world coordinates with any font. Returns the shaped text and its
/// bounding box.
pub fn place(font: &TextFont, s: &str, p: &TextParams) -> (Shaped, Bounds2) {
+2
View File
@@ -140,6 +140,8 @@ pub(crate) const DIM_CODES: &[(&str, i32, K)] = &[
/// DIMSAH: separate arrow blocks in use.
pub(crate) const DIMSAH: i32 = 173;
/// DIMPOST's group in DIMSTYLE records.
pub(crate) const DIMPOST: i32 = 3;
/// Variables newer than R2000: DIMFXL, DIMJOGANG, DIMARCSYM, DIMFXLON, DIMLTYPE, DIMLTEX1,
/// DIMLTEX2. Files of [`NEWER_SINCE`] or later get them as DIMSTYLE groups; R2000 files get them
+125 -9
View File
@@ -291,6 +291,50 @@ const HEADER_VARS: &[(&str, i32)] = &[
("PSLTSCALE", 70),
];
/// The current dimension style's variables as header variables (`$DIMTXT`, `$DIMDEC`…), after
/// `$DIMSTYLE`: AutoCAD draws new and updated dimensions with the header's DIM* values, which
/// otherwise stay its defaults (0.18 text, 4 decimals) whatever the current style says (#1051).
/// `$DIMSCALE` is a header variable of its own.
fn dim_header_vars(w: &mut W, d: &Drawing) {
let name = d.header.str("DIMSTYLE", "Standard");
let Some(style) = d.dim_styles.iter().find(|s| s.name.eq_ignore_ascii_case(&name)) else { return };
let v = serde_json::to_value(style).unwrap_or_default();
let none = |_: &str| None;
for (var, field) in cadcraft_doc::DIMVARS {
if *var == "DIMSCALE" {
continue;
}
let (Some(value), Some((_, _, kind))) = (v.get(*field), dxf_ext::DIM_CODES.iter().find(|(f, _, _)| f == field)) else { continue };
match kind {
// Names, not handles, in the header.
K::TextStyle | K::Block => {
let Some(n) = value.as_str() else { continue };
if *kind == K::TextStyle && n.trim().is_empty() {
continue;
}
w.s(9, format!("${var}"));
w.s(if *kind == K::TextStyle { 7 } else { 1 }, n);
}
K::Linetype => {}
_ => match dxf_ext::encode(*kind, value, &none, &none, &none) {
Some(DimVal::Real(x)) => {
w.s(9, format!("${var}"));
w.f(40, x);
}
Some(DimVal::Int(i)) if i16::try_from(i).is_ok() => {
w.s(9, format!("${var}"));
w.i(70, i);
}
Some(DimVal::Str(t)) => {
w.s(9, format!("${var}"));
w.s(1, t);
}
_ => {}
},
}
}
}
fn header_vars(w: &mut W, d: &Drawing) {
for (name, code) in HEADER_VARS {
let Some(v) = d.header.get(name) else { continue };
@@ -329,6 +373,7 @@ fn header_vars(w: &mut W, d: &Drawing) {
}
}
}
dim_header_vars(w, d);
}
/// An entity's group 440 (DXF Reference): ByBlock is `0x01000000`, a fixed transparency its alpha
@@ -458,8 +503,61 @@ fn attrib_flags(a: &Attrib) -> i64 {
/// A TEXT's groups, or an attribute's (`attrib`, in a file of version `target`) with its
/// subclass: the subclass version (`280 0`, 2010 and later), prompt, tag, flags and alignment,
/// then Lock position (`280`, 2007 and later; readers take the 280 after the tag as it).
fn text_tags(w: &mut W, t: &Text, attrib: Option<(&Attrib, Version)>, attdef_prompt: Option<&str>) {
w.p(10, t.insert);
/// The insertion point (group 10) to write for `t`. A justified text is placed by its alignment
/// point (group 11), and its insertion point is the start of its baseline, which AutoCAD reads
/// from DWG as is (it recomputes it only for DXF): writing the alignment point there drew centred
/// and right-aligned text from the wrong end in AutoCAD (#1050).
///
/// The stored point is kept when it is close to where CADCraft's font puts the baseline start:
/// then it is AutoCAD's own (exact for its fonts). It is replaced when it is clearly stale: the
/// alignment point itself (text made here) or a text whose length changed since. Close means
/// along the baseline within a tenth of the text's width plus half its height (AutoCAD's fonts,
/// or the ones CADCraft substitutes for them, differ most in width, and most on short texts) and
/// across it within a fifth of its height: Left Middle and Left Bottom text made here, whose
/// alignment point is only half or a third of a height from the baseline start, must not pass
/// for AutoCAD's.
fn text_insert(d: &Drawing, t: &Text) -> Vec3 {
let computed = computed_text_insert(d, t);
if t.align_pt.is_some_and(|a| a.xy() == t.insert.xy()) {
return computed;
}
let h = t.height.abs();
let off = (t.insert.xy() - computed.xy()).rotate(-t.rotation);
let close = if t.value.contains('\n') {
// A multiline attribute's lines are placed by its embedded MTEXT: a one-line baseline
// start says little about them, so only the distance counts.
let width = t.align_pt.map_or(0.0, |a| 2.0 * a.xy().dist(computed.xy()));
off.len() <= 0.1 * width + 0.5 * h
} else {
let width = cadcraft_fonts::line_width(&t.value, h, t.width_factor.abs());
off.x.abs() <= 0.1 * width + 0.5 * h && off.y.abs() <= 0.2 * h
};
if close { t.insert } else { computed }
}
fn computed_text_insert(d: &Drawing, t: &Text) -> Vec3 {
let justified = !matches!((t.halign, t.valign), (HAlign::Left, VAlign::Baseline) | (HAlign::Aligned | HAlign::Fit, _));
let Some(a) = t.align_pt.filter(|_| justified) else { return t.insert };
let ts = d.text_style(&t.style).cloned().unwrap_or_default();
let (h, v) = cadcraft_render::text_align(t.halign, t.valign);
let p = cadcraft_fonts::TextParams {
insert: t.insert.xy(),
align_pt: Some(a.xy()),
height: t.height,
rotation: t.rotation,
width_factor: t.width_factor,
oblique: t.oblique,
h,
v,
backwards: ts.backwards,
upside_down: ts.upside_down,
};
let q = cadcraft_fonts::baseline_start(&cadcraft_fonts::TextFont::resolve(&ts.font), &t.value, &p);
if q.is_finite() { Vec3::new(q.x, q.y, a.z) } else { t.insert }
}
fn text_tags(w: &mut W, d: &Drawing, t: &Text, attrib: Option<(&Attrib, Version)>, attdef_prompt: Option<&str>) {
w.p(10, text_insert(d, t));
w.f(40, t.height);
w.s(1, &t.value);
if t.rotation != 0.0 {
@@ -692,7 +790,7 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
w.s(0, "TEXT");
common(w, e, owner, paper, "AcDbText");
thickness(w, e);
text_tags(w, t, None, None);
text_tags(w, d, t, None, None);
}
// Closed polylines, one per loop (`dxf_region`): REGION's own data is ACIS.
EntityKind::Region(r) => {
@@ -712,7 +810,7 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
EntityKind::AttDef(a) => {
w.s(0, "ATTDEF");
common(w, e, owner, paper, "AcDbText");
text_tags(w, &a.text, Some((a, cx.target)), Some(&a.prompt));
text_tags(w, d, &a.text, Some((a, cx.target)), Some(&a.prompt));
if cx.target >= Version::R2007 {
multiline_tags(w, a, true);
}
@@ -776,7 +874,7 @@ fn entity(w: &mut W, d: &Drawing, e: &Entity, owner: &str, paper: bool, cx: &Ctx
w.i(60, 1);
}
w.s(100, "AcDbText");
text_tags(w, &a.text, Some((a, cx.target)), None);
text_tags(w, d, &a.text, Some((a, cx.target)), None);
if cx.target >= Version::R2007 {
multiline_tags(w, a, false);
}
@@ -1285,8 +1383,16 @@ fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec<Entity> {
let style = d.dim_style(&dm.style).cloned().unwrap_or_default().with_overrides(&dm.overrides);
let g = cadcraft_render::dimension_in(d, dm);
let mut out = Vec::new();
let c = Common { layer: layer.into(), color: Color::ByBlock, ..Common::default() };
for l in &g.lines {
// Each part in its style colour (DIMCLRD, DIMCLRE, DIMCLRT), ByBlock when the style says so,
// as AutoCAD writes them: readers that draw the block drew every part ByBlock, i.e. in the
// dimension's own colour (#1052).
let in_color = |color: Color| Common { layer: layer.into(), color, ..Common::default() };
let (dim_c, ext_c, text_c) = (in_color(style.dim_line_color), in_color(style.ext_line_color), in_color(style.text_color));
for (i, l) in g.lines.iter().enumerate() {
let c = match g.line_roles.get(i) {
Some(cadcraft_render::LineRole::Ext) => &ext_c,
_ => &dim_c,
};
for seg in l.windows(2) {
if let [a, b] = seg {
out.push(Entity { handle: Handle(0), common: c.clone(), kind: EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) }) });
@@ -1297,7 +1403,7 @@ fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec<Entity> {
if let [a, b, cc] = t.as_slice() {
out.push(Entity {
handle: Handle(0),
common: c.clone(),
common: dim_c.clone(),
kind: EntityKind::Solid(Solid { corners: [a.to3(0.0), b.to3(0.0), cc.to3(0.0), cc.to3(0.0)] }),
});
}
@@ -1305,7 +1411,7 @@ fn dim_block_entities(d: &Drawing, dm: &Dimension, layer: &str) -> Vec<Entity> {
if !g.value.is_empty() {
out.push(Entity {
handle: Handle(0),
common: c,
common: text_c,
kind: EntityKind::MText(MText {
insert: g.text_pos.to3(0.0),
height: g.text_height,
@@ -2057,6 +2163,16 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> {
let mut newer = W { t: Vec::new(), next: 0, pstyles: HashMap::new(), layers: HashMap::new(), reactors: HashMap::new() };
for (field, code, kind) in dxf_ext::DIM_CODES {
let Some(enc) = v.get(*field).and_then(|x| cx.encode(*kind, x)) else { continue };
// DIMPOST is written even when empty: a reader that defaults a missing one to "<>"
// (the DWG bridge's) would otherwise give radius and diameter dimensions no "R" or
// "Ø" in AutoCAD (#1053).
if *code == dxf_ext::DIMPOST
&& let DimVal::Str(t) = &enc
&& t.is_empty()
{
w.s(*code, "");
continue;
}
if !dxf_ext::NEWER_CODES.contains(code) {
w.dimval(*code, enc, false);
continue;
+3 -2
View File
@@ -1880,8 +1880,9 @@ fn autocad_header_dim_variables_are_not_overrides() {
assert_eq!(back.header.str("DIMDSEP", ""), ",");
assert!(back.header.get("DIMTXT").is_some() && back.header.get("DIMCLRD").is_some());
assert_eq!(back.dim_overrides(), serde_json::Map::new());
// A variable that really differs from the style is an override.
let text = text.replacen("$DIMDEC\r\n70\r\n3\r\n", "$DIMDEC\r\n70\r\n5\r\n", 1);
// A variable that really differs from the style is an override. CADCraft writes the current
// style's variables in the header too (#1051), so both copies change.
let text = text.replacen("$DIMDEC\r\n70\r\n3\r\n", "$DIMDEC\r\n70\r\n5\r\n", 1).replacen("$DIMDEC\r\n 70\r\n3\r\n", "$DIMDEC\r\n 70\r\n5\r\n", 1);
assert_eq!(read_dxf(text.as_bytes()).unwrap().dim_overrides().get("decimals"), Some(&serde_json::json!(5)));
}
+296
View File
@@ -0,0 +1,296 @@
//! What AutoCAD reads from CADCraft's DWG and DXF as it is, without recomputing it: justified
//! text's insertion point (#1050), DIMPOST (#1053), the current dimension style and its header
//! variables (#1051), the colours of a dimension's `*D` block (#1052) and the stored measurement
//! of a rotated linear dimension (#1054) and of angular dimensions.
use cadcraft_color::Color;
use cadcraft_doc::{Common, DimKind, DimStyle, Dimension, Drawing, EntityKind, HAlign, Space, Text, VAlign};
use cadcraft_geom::Vec3;
use cadcraft_io::{SaveOptions, Version, read, write_dxf, write_with};
fn dwg_roundtrip(d: &Drawing) -> Drawing {
let dwg = write_with(d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap();
read(&dwg, "case.dwg").unwrap()
}
/// `(code, value)` pairs of a DXF, values trimmed.
fn tags(dxf: &str) -> Vec<(i32, String)> {
let lines: Vec<&str> = dxf.lines().collect();
lines.chunks(2).filter_map(|c| Some((c.first()?.trim().parse().ok()?, c.get(1)?.trim().to_string()))).collect()
}
fn text(value: &str, at: Vec3, h: HAlign, v: VAlign) -> EntityKind {
EntityKind::Text(Text {
insert: at,
align_pt: Some(at),
height: 5.0,
value: value.into(),
rotation: 0.0,
width_factor: 1.0,
oblique: 0.0,
style: "Standard".into(),
halign: h,
valign: v,
})
}
#[test]
fn justified_text_is_written_with_its_baseline_start_as_insertion_point() {
let mut d = Drawing::new_metric();
d.add(&Space::Model, Common::default(), text("MIDDLE CENTER", Vec3::new(100.0, 50.0, 0.0), HAlign::Center, VAlign::Middle)).unwrap();
d.add(&Space::Model, Common::default(), text("BOTTOM RIGHT", Vec3::new(100.0, 30.0, 0.0), HAlign::Right, VAlign::Bottom)).unwrap();
let width = |s: &str| cadcraft_fonts::line_width(s, 5.0, 1.0);
let near = |a: f64, b: f64| (a - b).abs() < 1e-6;
// DXF: group 10 is where the baseline starts, group 11 the alignment point.
let t = tags(&write_dxf(&d));
let texts: Vec<usize> = t.iter().enumerate().filter(|(_, (c, v))| *c == 0 && v == "TEXT").map(|(i, _)| i).collect();
let group = |start: usize, code: i32| -> f64 { t[start..].iter().find(|(c, _)| *c == code).unwrap().1.parse().unwrap() };
assert_eq!(texts.len(), 2);
assert!(near(group(texts[0], 10), 100.0 - width("MIDDLE CENTER") / 2.0), "{}", group(texts[0], 10));
assert!(near(group(texts[0], 11), 100.0));
assert!(near(group(texts[1], 10), 100.0 - width("BOTTOM RIGHT")), "{}", group(texts[1], 10));
// DWG: the same, through the bridge.
let back = dwg_roundtrip(&d);
let starts: Vec<(f64, f64)> = back
.model
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Text(t) => Some((t.insert.x, t.align_pt.map_or(f64::NAN, |a| a.x))),
_ => None,
})
.collect();
assert_eq!(starts.len(), 2);
assert!(near(starts[0].0, 100.0 - width("MIDDLE CENTER") / 2.0) && near(starts[0].1, 100.0), "{starts:?}");
assert!(near(starts[1].0, 100.0 - width("BOTTOM RIGHT")) && near(starts[1].1, 100.0), "{starts:?}");
}
/// A metric drawing whose current style is S25 (text 0.25, 2 decimals, DIMSCALE 25, red lines,
/// green text), with one dimension.
fn s25(kind: DimKind, p13: Vec3, p14: Vec3, defpt: Vec3) -> Drawing {
let mut d = Drawing::new_metric();
d.dim_styles.push(DimStyle {
name: "S25".into(),
scale: 25.0,
text_height: 0.25,
decimals: 2,
dim_line_color: Color::from_aci(1),
ext_line_color: Color::from_aci(1),
text_color: Color::from_aci(3),
..DimStyle::default()
});
d.header.set_str("DIMSTYLE", "S25");
let dm = Dimension {
kind,
defpt,
text_mid: Vec3::ZERO,
p13,
p14,
p15: Vec3::ZERO,
p16: Vec3::ZERO,
text: String::new(),
style: "S25".into(),
measurement: 0.0,
text_rotation: 0.0,
user_text_pos: false,
block: None,
overrides: Default::default(),
assoc: Vec::new(),
marks: Default::default(),
};
d.add(&Space::Model, Common::default(), EntityKind::Dimension(dm)).unwrap();
d
}
fn linear(d_rotation: f64) -> Drawing {
s25(DimKind::Linear { rotation: d_rotation }, Vec3::ZERO, Vec3::new(100.0, 0.0, 0.0), Vec3::new(50.0, -20.0, 0.0))
}
#[test]
fn the_current_dimension_style_and_its_variables_reach_autocad() {
let d = linear(0.0);
let t = tags(&write_dxf(&d));
let var = |name: &str| t.windows(2).find(|w| w[0] == (9, name.to_string())).map(|w| w[1].1.clone());
assert_eq!(var("$DIMSTYLE").as_deref(), Some("S25"));
assert_eq!(var("$DIMTXT").and_then(|v| v.parse::<f64>().ok()), Some(0.25));
assert_eq!(var("$DIMDEC").as_deref(), Some("2"));
assert_eq!(var("$DIMCLRT").as_deref(), Some("3"));
// DIMPOST is written even when empty (#1053).
let back = dwg_roundtrip(&d);
assert_eq!(back.header.str("DIMSTYLE", ""), "S25", "the DWG keeps the current style");
for s in &back.dim_styles {
assert_eq!(s.post, "", "{}: DIMPOST stays empty, not \"<>\"", s.name);
}
}
#[test]
fn a_dimension_block_takes_the_style_colours() {
let d = linear(0.0);
let t = tags(&write_dxf(&d));
let blocks = t.iter().position(|(c, v)| *c == 2 && v == "BLOCKS").unwrap();
let start = blocks + t[blocks..].iter().position(|(c, v)| *c == 2 && v.starts_with("*D")).unwrap();
let end = start + t[start..].iter().position(|(c, v)| *c == 0 && v == "ENDBLK").unwrap();
let mut colors: Vec<(String, i64)> = Vec::new();
let mut kind = String::new();
for (c, v) in &t[start..end] {
match c {
0 => kind = v.clone(),
62 => colors.push((kind.clone(), v.parse().unwrap())),
_ => {}
}
}
assert!(colors.iter().any(|(k, c)| k == "LINE" && *c == 1), "{colors:?}");
assert!(colors.iter().any(|(k, c)| k == "MTEXT" && *c == 3), "{colors:?}");
assert!(!colors.iter().any(|(_, c)| *c == 0), "nothing ByBlock: {colors:?}");
}
#[test]
fn a_rotated_linear_dimension_stores_its_projected_length() {
let d = s25(
DimKind::Linear { rotation: std::f64::consts::FRAC_PI_2 },
Vec3::new(-5.0, 0.0, 0.0),
Vec3::new(0.0, 210.0, 0.0),
Vec3::new(-40.0, 105.0, 0.0),
);
let dwg = write_with(&d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap();
let dxf = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
let t = tags(&dxf);
let dim = t.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
let m: f64 = t[dim..].iter().find(|(c, _)| *c == 42).unwrap().1.parse().unwrap();
assert!((m - 210.0).abs() < 1e-9, "group 42 = {m}");
}
/// Group 10 and the value of every TEXT in a DXF, in file order.
fn text_starts(dxf: &str) -> Vec<(String, cadcraft_geom::Vec2)> {
let t = tags(dxf);
let mut out = Vec::new();
for (i, (c, v)) in t.iter().enumerate() {
if *c == 0 && v == "TEXT" {
let end = t[i + 1..].iter().position(|(c, _)| *c == 0).map_or(t.len(), |k| i + 1 + k);
let g = |code: i32| t[i + 1..end].iter().find(|(c, _)| *c == code).map(|(_, v)| v.clone());
let num = |code: i32| g(code).and_then(|v| v.parse::<f64>().ok()).unwrap_or(f64::NAN);
out.push((g(1).unwrap_or_default(), cadcraft_geom::Vec2::new(num(10), num(20))));
}
}
out
}
/// Justified TEXT made in CADCraft (insertion point = alignment point) is written with its
/// baseline start in group 10, for every justification and rotated too: placed baseline-left at
/// the written group 10, each text covers what CADCraft draws for the justified one. Left Middle
/// and Left Bottom were kept at the alignment point, half or a third of a height off, because
/// that point passed for AutoCAD's own (test by @lee-cyrille-maskell, from their fix for #1050).
#[test]
fn justified_text_made_here_writes_its_baseline_start_for_every_justification() {
use HAlign::{Center, Left, Right};
use VAlign::{Baseline, Bottom, Top};
let just = [
(Left, Baseline),
(Center, Baseline),
(Right, Baseline),
(HAlign::Middle, Baseline),
(Left, Top),
(Center, Top),
(Right, Top),
(Left, VAlign::Middle),
(Center, VAlign::Middle),
(Right, VAlign::Middle),
(Left, Bottom),
(Center, Bottom),
(Right, Bottom),
];
let mut d = Drawing::new_imperial();
let mut made = Vec::new();
for (i, &(h, v)) in just.iter().enumerate() {
for rotation in [0.0, 0.5] {
let at = Vec3::new(100.0, 400.0 - 22.0 * i as f64, 0.0);
let left = h == Left && v == Baseline;
let t = Text {
insert: at,
align_pt: if left { None } else { Some(at) },
height: 5.0,
value: format!("{h:?} {v:?} {rotation} Hxg"),
rotation,
width_factor: 0.8,
oblique: 0.0,
style: "Standard".into(),
halign: h,
valign: v,
};
d.add(&Space::Model, Common::default(), EntityKind::Text(t.clone())).unwrap();
made.push(t);
}
}
let dxf = write_dxf(&d);
let starts = text_starts(&dxf);
assert_eq!(starts.len(), made.len());
let near = |a: cadcraft_geom::Vec2, b: cadcraft_geom::Vec2| (a.x - b.x).abs() < 1e-6 && (a.y - b.y).abs() < 1e-6;
for (t, (value, p)) in made.iter().zip(&starts) {
assert_eq!(&t.value, value);
let (_, want) = cadcraft_render::place_text_entity(&d, t, &t.value);
let start = Text { insert: Vec3::new(p.x, p.y, 0.0), align_pt: None, halign: Left, valign: Baseline, ..t.clone() };
let (_, got) = cadcraft_render::place_text_entity(&d, &start, &start.value);
assert!(near(got.min, want.min) && near(got.max, want.max), "{value}: drawn {want:?}, from group 10 {got:?}");
}
// Read back and saved again, group 10 stays put.
let back = read(dxf.as_bytes(), "t.dxf").unwrap();
assert_eq!(text_starts(&write_dxf(&back)), starts);
}
/// Group 42 of a dimension in the DXF AutoCAD reads from `d`'s DWG.
fn stored_measurement(d: &Drawing) -> f64 {
let dwg = write_with(d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap();
let dxf = String::from_utf8(cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).unwrap();
let t = tags(&dxf);
let dim = t.iter().position(|(c, v)| *c == 0 && v == "DIMENSION").unwrap();
t[dim..].iter().find(|(c, _)| *c == 42).unwrap().1.parse().unwrap()
}
fn polar(deg: f64, r: f64) -> Vec3 {
let a = deg.to_radians();
Vec3::new(r * a.cos(), r * a.sin(), 0.0)
}
/// Angular dimensions store their angle too, in radians as AutoCAD does (0.0 in 0.6.2).
#[test]
fn an_angular_dimension_stores_its_angle_in_radians() {
// Three points: vertex (p15) at the origin, legs through p13 and p14, the arc through defpt.
for deg in [60.0, 174.0] {
let d = s25(DimKind::Angular3P, polar(0.0, 20.0), polar(deg, 20.0), polar(deg / 2.0, 15.0));
let m = stored_measurement(&d);
assert!((m - f64::to_radians(deg)).abs() < 1e-9, "{deg}°: group 42 = {m}");
}
// Two lines, 120° apart: p13-p14 and defpt-p15, the arc through p16.
let mut d = s25(DimKind::Angular, polar(0.0, 5.0), polar(0.0, 20.0), polar(120.0, 5.0));
let h = d.model.iter().find(|e| matches!(e.kind, EntityKind::Dimension(_))).unwrap().handle;
d.modify_entity(h, |e| {
if let EntityKind::Dimension(dm) = &mut e.kind {
dm.p15 = polar(120.0, 20.0);
dm.p16 = polar(60.0, 15.0);
}
})
.unwrap();
let m = stored_measurement(&d);
assert!((m - f64::to_radians(120.0)).abs() < 1e-9, "two lines: group 42 = {m}");
}
/// A justified text read from AutoCAD keeps AutoCAD's insertion point (exact for its own fonts)
/// when it is near CADCraft's; only a stale one (the alignment point, or a text that has since
/// changed length) is replaced.
#[test]
fn an_insertion_point_from_autocad_is_kept() {
let mut d = Drawing::new_metric();
let at = Vec3::new(100.0, 50.0, 0.0);
let ours = 100.0 - cadcraft_fonts::line_width("MIDDLE CENTER", 5.0, 1.0) / 2.0;
let mut k = text("MIDDLE CENTER", at, HAlign::Center, VAlign::Middle);
if let EntityKind::Text(t) = &mut k {
// AutoCAD's font is a little narrower than CADCraft's.
t.insert = Vec3::new(ours + 2.5, 47.5, 0.0);
}
d.add(&Space::Model, Common::default(), k).unwrap();
let t = tags(&write_dxf(&d));
let i = t.iter().position(|(c, v)| *c == 0 && v == "TEXT").unwrap();
let x: f64 = t[i..].iter().find(|(c, _)| *c == 10).unwrap().1.parse().unwrap();
assert!((x - (ours + 2.5)).abs() < 1e-9, "kept: {x}");
}
+5
View File
@@ -1445,6 +1445,11 @@ fn table(b: &mut Builder, ctx: &Ctx, t: &cadcraft_doc::Table, rgb: Ink, lw: f32)
table::draw(b, ctx, t, rgb, lw);
}
/// A TEXT's justification as the fonts' alignment (used where text is placed outside a build).
pub fn text_align(h: cadcraft_doc::HAlign, v: cadcraft_doc::VAlign) -> (cadcraft_fonts::Align, cadcraft_fonts::VAlign) {
align(h, v)
}
fn align(h: cadcraft_doc::HAlign, v: cadcraft_doc::VAlign) -> (cadcraft_fonts::Align, cadcraft_fonts::VAlign) {
use cadcraft_fonts::{Align as A, VAlign as V};
let ha = match h {