Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions CADability/ExportDxf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -697,9 +697,11 @@ private Entity ExportText(GeoObject.Text text)
if (text.Bold) fs |= System.Drawing.FontStyle.Bold;
if (text.Italic) fs |= System.Drawing.FontStyle.Italic;

// TextSize is the raw DXF cap-height (group 40) stored unchanged during import.
// Do not apply any font-metric scaling here — it would corrupt the value.
double height = text.TextSize;
// TextSize is the em height of the font, while the DXF height (group 40) is the
// height of the capital letters, which AutoCAD and other viewers scale the font to.
// Writing TextSize unchanged makes the text 1.4 (Arial) to 1.6 (Consolas) times too
// large in every other program (issue #506). The import divides by the same factor.
double height = text.TextSize * text.CapHeightFactor;

string fontName = text.Font ?? "Standard";
if (!createdTextStyles.TryGetValue(fontName, out ACadSharp.Tables.TextStyle textStyle))
Expand Down Expand Up @@ -765,6 +767,10 @@ private Entity ExportText(GeoObject.Text text)
Normal = ToXYZ(normal),
Rotation = rotation,
};
// Group 41: CADability stretches or compresses a text by the length of its line
// direction relative to its glyph direction.
double widthFactor = text.LineDirection.Length / text.GlyphDirection.Length;
if (widthFactor > 0.0 && Math.Abs(widthFactor - 1.0) > 1e-9) res.WidthFactor = widthFactor;
// Set group 11 whenever it is meaningful. Leaving it at its (0,0,0) default while
// groups 72/73 say it is authoritative would place the text at the origin.
if (!defaultAlign)
Expand Down Expand Up @@ -814,9 +820,10 @@ private ACadSharp.Entities.MText BuildMText(GeoObject.Text text, string textStri
if (text.ColumnWidth > 0) res.RectangleWidth = text.ColumnWidth;
if (text.LineSpacing > 0)
{
// Import stores the DXF spacing factor as 5/3 * factor (AutoCAD's MTEXT
// baseline distance); invert that here and keep it inside the DXF range.
double factor = text.LineSpacing * 3.0 / 5.0;
// AutoCAD spaces MTEXT baselines 5/3 * factor of the DXF height apart, and the
// import converts that into a factor of TextSize; invert that here and keep it
// inside the DXF range.
double factor = text.LineSpacing * 3.0 / 5.0 / text.CapHeightFactor;
res.LineSpacing = Math.Max(0.25, Math.Min(4.0, factor));
// Group 73 must name a real spacing style, otherwise the factor in group 44
// is ignored; the default (None/0) is not one the DXF reference allows here.
Expand Down Expand Up @@ -1121,7 +1128,10 @@ private ACadSharp.Tables.DimensionStyle GetOrCreateDimensionStyle(CADability.Att
found = new ACadSharp.Tables.DimensionStyle(candidate);
doc.DimensionStyles.Add(found);
}
if (style.TextSize > 0.0) found.TextHeight = style.TextSize;
// DIMTXT is the height of the capital letters like the height of a TEXT, while
// CADability draws the dimension text with TextSize as the em height (issue #506).
if (style.TextSize > 0.0)
found.TextHeight = style.TextSize * GeoObject.Text.GetCapHeightFactor(style.TextFont, false, false);
found.ArrowSize = Math.Max(0.0, style.SymbolSize);
found.ExtensionLineOffset = style.ExtLineOffset;
found.ExtensionLineExtension = style.ExtLineExtension;
Expand Down
21 changes: 14 additions & 7 deletions CADability/ImportDxf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,9 @@ private IGeoObject CreateText(ACadSharp.Entities.TextEntity txt, bool pointsInWc
text.Location = pointsInWcs ? GeoPoint(anchor) : OcsToWcs(anchor, txt.Normal);
text.LineDirection = linedir;
text.GlyphDirection = glyphdir;
// The DXF height is the height of the capital letters, TextSize the em height of
// the font (issue #506). The export multiplies by the same factor.
h /= text.CapHeightFactor;
text.TextSize = h;
linedir.Length = h * txt.WidthFactor;
if (!linedir.IsNullVector()) text.LineDirection = linedir;
Expand Down Expand Up @@ -2273,8 +2276,6 @@ private void MapDimensionStyle(ACadSharp.Tables.DimensionStyle acad,
{
double scale = acad.ScaleFactor > 0.0 ? acad.ScaleFactor : 1.0;
style.Types = Attribute.DimensionStyle.ETypeFlag.DimAllTypes;
style.TextSize = acad.TextHeight * scale;
style.TextSizeTol = acad.TextHeight * acad.ToleranceScaleFactor * scale;
style.SymbolSize = acad.ArrowSize * scale;
style.ExtLineOffset = acad.ExtensionLineOffset * scale;
style.ExtLineExtension = acad.ExtensionLineExtension * scale;
Expand All @@ -2299,12 +2300,17 @@ private void MapDimensionStyle(ACadSharp.Tables.DimensionStyle acad,
style.TextPrefix = arcSymbol + (acad.Prefix ?? "");
style.TextPostfix = acad.Suffix ?? "";
style.TextFont = FontNameFromStyle(acad.Style);
// DIMTXT is the height of the capital letters like the height of a TEXT, while
// CADability draws the dimension text with TextSize as the em height (issue #506).
double emHeight = acad.TextHeight * scale / GeoObject.Text.GetCapHeightFactor(style.TextFont, false, false);
style.TextSize = emHeight;
style.TextSizeTol = emHeight * acad.ToleranceScaleFactor;
// DIMTAD says whether the text sits on the dimension line or above it, DIMGAP how
// far; CADability measures that distance in text heights.
// far; CADability measures that distance in multiples of TextSize.
style.TextDist = acad.TextVerticalAlignment == DimensionTextVerticalAlignment.Centered
|| acad.TextHeight <= 0.0
|| emHeight <= 0.0
? 0.0
: Math.Abs(acad.DimensionLineGap) / acad.TextHeight;
: Math.Abs(acad.DimensionLineGap) * scale / emHeight;
style.Symbol = MapArrowSymbol(acad);
style.DimNoExtLine1 = acad.SuppressFirstExtensionLine;
style.DimNoExtLine2 = acad.SuppressSecondExtensionLine;
Expand Down Expand Up @@ -2559,9 +2565,10 @@ private IGeoObject CreateMText(ACadSharp.Entities.MText mText)
// Automatic word wrap at the MTEXT column width (DXF group code 41)
if (mText.RectangleWidth > 0) text.ColumnWidth = mText.RectangleWidth;
// AutoCAD spaces MTEXT baselines 5/3 of the text height apart, scaled by the
// file's line spacing factor (DXF group code 44, default 1.0)
// file's line spacing factor (DXF group code 44, default 1.0). The text height
// is that of the capital letters, LineSpacing a factor of the em height.
double factor = mText.LineSpacing > 0 ? mText.LineSpacing : 1.0;
text.LineSpacing = 5.0 / 3.0 * factor;
text.LineSpacing = 5.0 / 3.0 * factor * text.CapHeightFactor;
}
return geo;
}
Expand Down
53 changes: 53 additions & 0 deletions CADability/Text.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ struct FontCharacteristics
readonly Dictionary<DictKey, CenterLineVal> centerLineCache; // Cache für die CenterLines, Größe 1
readonly Dictionary<FontKey, FontCharacteristics> fontCharacteristicsCache; // Cache für die FontCharacteristics
readonly Dictionary<FontKey, FontFace> faces; // the faces of FontProvider.Current, per font name and bold/italic
readonly Dictionary<FontKey, double> capHeights; // see GetCapHeight
public FontCache()
{
FontPrecision = Settings.GlobalSettings.GetIntValue("Font.Precision", 1); // |grob|mittel|fein
Expand All @@ -485,6 +486,7 @@ public FontCache()
centerLineCache = new Dictionary<DictKey, CenterLineVal>();
fontCharacteristicsCache = new Dictionary<FontKey, FontCharacteristics>();
faces = new Dictionary<FontKey, FontFace>();
capHeights = new Dictionary<FontKey, double>();
}
/// <summary>
/// Returns the face of <see cref="FontProvider.Current"/> for a font name and a font style (the flags of
Expand Down Expand Up @@ -521,6 +523,39 @@ internal FontFace TryGetFace(string fontName, int fontStyle)
return null;
}
}
/// <summary>
/// The height of the capital letters of a font in units of the em height, measured at the outline of "H". A DXF or
/// DWG text height (group 40) is this height, while <see cref="Text.TextSize"/> is the em height (issue #506).
/// Returns 1.0 when there is no font at all or the font has no "H", then both heights are taken to be the same.
/// </summary>
internal double GetCapHeight(string fontName, int fontStyle)
{
FontKey key = new FontKey(fontName ?? "", fontStyle & 3);
lock (this)
{
if (capHeights.TryGetValue(key, out double res)) return res;
res = 1.0;
FontFace face = TryGetFace(fontName, fontStyle & 3);
if (face != null && face.EmHeight > 0)
{
try
{ // the outline is y downwards with the baseline at CellAscent/EmHeight, see FontFace.GetGlyphPath
face.GetGlyphPath('H', false, false, out PointF[] points, out byte[] _);
if (points.Length > 0)
{
double top = points.Min(p => p.Y);
double height = (double)face.CellAscent / face.EmHeight - top;
if (height > 0.0 && height <= 2.0) res = height;
}
}
catch (Exception ex) when (!(ex is ThreadAbortException))
{ // a glyph that cannot be read leaves the heights the same
}
}
capHeights[key] = res;
return res;
}
}
// The caches are shared by all threads that measure or paint text, and Clear may empty them at any time: every access
// goes through lock (this). The lock is reentrant, Get and GetCenterLines call GetOutline2D. The faces are locked
// separately and always after this, so the two locks are taken in the same order everywhere.
Expand Down Expand Up @@ -754,6 +789,7 @@ public void Clear()
cache.Clear();
centerLineCache.Clear();
fontCharacteristicsCache.Clear();
capHeights.Clear();
lock (faces) faces.Clear();
}
}
Expand Down Expand Up @@ -2358,6 +2394,23 @@ public double TextSize
}
}
/// <summary>
/// The height of the capital letters of the font of this text as a fraction of <see cref="TextSize"/>, which is the
/// em height. DXF and DWG give the height of a text as the height of its capital letters, so import and export
/// convert with this factor (issue #506).
/// </summary>
internal double CapHeightFactor => GetCapHeightFactor(fontName, Bold, Italic);
/// <summary>
/// The height of the capital letters of the font <paramref name="fontName"/> as a fraction of its em height, see
/// <see cref="CapHeightFactor"/>. Dimensions use it for the text size of their style.
/// </summary>
internal static double GetCapHeightFactor(string fontName, bool bold, bool italic)
{
int fs = 0;
if (bold) fs |= (int)FontStyle.Bold;
if (italic) fs |= (int)FontStyle.Italic;
return FontCache.GlobalFontCache.GetCapHeight(fontName, fs);
}
/// <summary>
/// Width of the text column for automatic word wrapping (in world units, e.g. from a DXF MTEXT).
/// The displayed text is broken into additional lines at word boundaries so that no line gets
/// wider than this value. 0.0 (the default) means no automatic wrapping, only explicit line
Expand Down
30 changes: 26 additions & 4 deletions tests/CADability.ImportTests/DxfDimensionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,9 @@ public void export_dimension_writes_a_dxf_dimension()
"the measurement text");
}
Assert.AreEqual(project.DimensionStyleList.Current.Name, linear.Style.Name);
Assert.AreEqual(project.DimensionStyleList.Current.TextSize, linear.Style.TextHeight, 1e-12);
CADability.Attribute.DimensionStyle current = project.DimensionStyleList.Current;
Assert.AreEqual(CapitalHeight(current.TextFont, current.TextSize), linear.Style.TextHeight, 1e-8,
"DIMTXT is the height of the capital letters CADability draws (issue #506)");
}

/// <summary>
Expand Down Expand Up @@ -981,7 +983,8 @@ public void import_dxf_dimension_translates_the_dimension_style()
Assert.IsNotNull(style, "a rebuilt dimension needs a style to draw with");
Assert.AreEqual("LOGICAL", style.Name, "the style should keep the name from the file");
Assert.AreSame(style, project.DimensionStyleList.Find("LOGICAL"), "the style is part of the project");
Assert.AreEqual(7.0, style.TextSize, 1e-8, "DIMTXT 3.5 times DIMSCALE 2");
Assert.AreEqual(7.0, CapitalHeight(style.TextFont, style.TextSize), 1e-8,
"capital letters as high as DIMTXT 3.5 times DIMSCALE 2 (issue #506)");
Assert.AreEqual(3.0, style.SymbolSize, 1e-8, "DIMASZ 1.5 times DIMSCALE 2");
Assert.AreEqual(1.0, style.ExtLineOffset, 1e-8, "DIMEXO 0.5 times DIMSCALE 2");
Assert.AreEqual(2.5, style.ExtLineExtension, 1e-8, "DIMEXE 1.25 times DIMSCALE 2");
Expand All @@ -1002,7 +1005,8 @@ public void import_dxf_dimension_uses_the_standard_style_of_the_file()
Project project = ImportProjectAsDimensions(StyledPrologue + standard + Epilogue);
Dimension dim = SingleDimensionObject(project.GetActiveModel());

Assert.AreEqual(0.18, dim.DimensionStyle.TextSize, 1e-8, "DIMTXT of AutoCAD's Standard style");
Assert.AreEqual(0.18, CapitalHeight(dim.DimensionStyle.TextFont, dim.DimensionStyle.TextSize), 1e-8,
"capital letters as high as DIMTXT of AutoCAD's Standard style");
Assert.AreEqual(0.18, dim.DimensionStyle.SymbolSize, 1e-8, "DIMASZ of AutoCAD's Standard style");
}

Expand Down Expand Up @@ -1047,7 +1051,8 @@ public void import_dxf_dimension_style_override_does_not_leak()
Dimension[] dims = model.AllObjects.OfType<Dimension>().ToArray();
Assert.AreEqual(2, dims.Length, "both dimensions should be rebuilt");

double[] sizes = dims.Select(d => d.DimensionStyle.TextSize).OrderBy(t => t).ToArray();
double[] sizes = dims.Select(d => CapitalHeight(d.DimensionStyle.TextFont, d.DimensionStyle.TextSize))
.OrderBy(t => t).ToArray();
Assert.AreEqual(7.0, sizes[0], 1e-8, "the plain dimension keeps DIMTXT 3.5 times DIMSCALE 2");
Assert.AreEqual(18.0, sizes[1], 1e-8, "the overridden one gets 9.0 times DIMSCALE 2");
Assert.AreNotSame(dims[0].DimensionStyle, dims[1].DimensionStyle,
Expand Down Expand Up @@ -1603,6 +1608,23 @@ private Model ExportAndImport(Project project)

private static double Distance(CSMath.XYZ p, GeoPoint q) => new GeoPoint(p.X, p.Y, p.Z) | q;

/// <summary>
/// The height of an "H" that CADability draws in <paramref name="font"/> with the text size
/// <paramref name="textSize"/>, which is the em height. DIMTXT is this height (issue #506).
/// </summary>
private static double CapitalHeight(string font, double textSize)
{
Text text = Text.Construct();
text.Font = font;
text.TextString = "H";
text.LineDirection = GeoVector.XAxis;
text.GlyphDirection = GeoVector.YAxis;
text.TextSize = textSize;
BoundingCube ext = BoundingCube.EmptyBoundingCube;
foreach (IGeoObject go in text.GetOutline()) ext.MinMax(go.GetBoundingCube());
return ext.Ymax - ext.Ymin;
}

private static Block SingleDimensionBlock(Model model)
{
Assert.AreEqual(1, model.Count, "the DIMENSION should import as one object");
Expand Down
Loading
Loading