mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
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:
co-authored by
Claude Opus 5.5
parent
7164b6b5b1
commit
f0e4fef003
@@ -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}"))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user