Textové styly: zachovat jména ze zdroje a opravit MTEXT
Šablona má styly pojmenované česky („Popis základní", „Název stavby"), export je zahazoval a vyráběl náhradní jména podle fontu (Verdana, Verdana_B, Arial_1) – pro každou kombinaci fontu a sklonu nový záznam. Ve správci stylů v CADu pak nešlo nic najít ani hromadně změnit. Jméno stylu se teď nese v `TextStyle.Nazev` až do zápisu; jeden zdrojový styl = jeden styl ve výstupu. Dál: - SHX styly (vektorové fonty AutoCADu) se přenášejí i s odkazem na soubor a příznakem `IsShape`. Kreslí se pořád náhradním fontem – SHX se jako TrueType načíst nedá –, ale v exportu zůstane původní styl, ne Standard. - MTEXT si drží natočení. Nešlo přiřadit `Rotation` (v ACadSharp je jen ke čtení), takže se nastavuje směrovým vektorem osy X; natočený víceřádkový atribut dřív vycházel vodorovně. - MTEXT si drží šířkový faktor přes kód \W. Jako vlastnost entity ho nemá. Čtečka ho zase odloupne – ACadSharp ho v PlainText nechává, takže bez toho by se „W0.8;" vykreslilo jako text. - Hodnota MTEXTu se escapuje celá (zpětné lomítko, složené závorky), ne jen zlomy řádků. Cesta typu C:\projekt v poli dřív rozbila formátování. 99 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
using System.IO;
|
||||
using ACadSharp.Entities;
|
||||
using ACadSharp.IO;
|
||||
using Rozpisky.Dxf;
|
||||
using Rozpisky.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace Rozpisky.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Textové styly a MTEXT tam a zpět. Šablona má styly pojmenované česky („Popis základní",
|
||||
/// „Název stavby"); export je dřív zahodil a vyrobil náhradní jména podle fontu (Verdana_1),
|
||||
/// takže ve správci stylů v CADu nešlo nic najít ani hromadně změnit.
|
||||
/// </summary>
|
||||
public class TextStylyRoundTripTesty : IDisposable
|
||||
{
|
||||
private readonly string _soubor = Path.Combine(Path.GetTempPath(), "rozpisky-styly-" + Guid.NewGuid().ToString("N") + ".dxf");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_soubor); } catch (IOException) { }
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private ACadSharp.CadDocument Kolecko(Action<IProfileRenderer> kresba)
|
||||
{
|
||||
var dxf = new DxfProfileRenderer();
|
||||
kresba(dxf);
|
||||
DxfWriter.Write(_soubor, dxf.Document, binary: true);
|
||||
return DxfReader.Read(_soubor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StylySiDrziJmenaZeZdroje()
|
||||
{
|
||||
var doc = Kolecko(g => new DxfTemplate(TestData.Sablona).Replay(g));
|
||||
var jmena = doc.TextStyles.Select(s => s.Name).ToList();
|
||||
|
||||
Assert.Contains("Popis základní", jmena);
|
||||
Assert.Contains("Název stavby", jmena);
|
||||
// Náhradní jména podle fontu nesmí vzniknout, když zdroj styl pojmenovaný má.
|
||||
Assert.DoesNotContain(jmena, j => j.StartsWith("Verdana", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JedenZdrojovyStylDaJedenStylVeVystupu()
|
||||
{
|
||||
// Dřív vznikal nový styl pro každou kombinaci fontu a sklonu, takže jich ve výstupu
|
||||
// bylo víc než ve zdroji a texty se rozpadly mezi ně.
|
||||
var zdroj = DxfReader.Read(TestData.Sablona);
|
||||
var doc = Kolecko(g => new DxfTemplate(TestData.Sablona).Replay(g));
|
||||
|
||||
var pouzite = doc.Entities.OfType<TextEntity>().Select(t => t.Style?.Name)
|
||||
.Concat(doc.Entities.OfType<MText>().Select(m => m.Style?.Name))
|
||||
.Where(n => n is not null).Distinct().ToList();
|
||||
|
||||
Assert.NotEmpty(pouzite);
|
||||
foreach (var n in pouzite)
|
||||
Assert.True(zdroj.TextStyles.Contains(n!), $"styl {n} ve zdroji není");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MtextSiDrziNatoceni()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "dva\nřádky", 3, maxWidthMm: 40, rotationDeg: 30));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Equal(30, mt.Rotation * 180 / Math.PI, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MtextSiDrziSirkovyFaktor()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "text", 3, maxWidthMm: 40, widthFactor: 0.8));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.StartsWith(@"\W0.8;", mt.Value);
|
||||
|
||||
// A vlastní čtečka ho musí zase odloupnout, jinak by se „W0.8;" vykreslilo jako text.
|
||||
var (cisty, wf) = DxfTemplate.MTextSirka(mt.Value, mt.PlainText);
|
||||
Assert.Equal("text", cisty);
|
||||
Assert.Equal(0.8, wf!.Value, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZpetneLomitkoAZavorkyNerozbijiMtext()
|
||||
{
|
||||
// Reálná hodnota z pole: cesta nebo poznámka se zpětným lomítkem. Bez escapování
|
||||
// by ji CAD přečetl jako formátovací kód a část textu by zmizela.
|
||||
const string hodnota = @"C:\projekt {2025} \P";
|
||||
var doc = Kolecko(g => g.Text(0, 0, hodnota, 3, maxWidthMm: 60));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Equal(hodnota, mt.PlainText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZlomRadkuSePreloziNaMtextovyKod()
|
||||
{
|
||||
var doc = Kolecko(g => g.Text(0, 0, "první\ndruhý", 3, maxWidthMm: 60));
|
||||
var mt = Assert.Single(doc.Entities.OfType<MText>());
|
||||
Assert.Contains(@"\P", mt.Value);
|
||||
Assert.Equal("první\ndruhý", mt.PlainText.Replace("\r\n", "\n"));
|
||||
}
|
||||
}
|
||||
@@ -166,8 +166,12 @@ public sealed partial class DxfTemplate
|
||||
case MText mt:
|
||||
{
|
||||
var (h, v) = MapMTextAlign(mt.AttachmentPoint);
|
||||
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, mt.PlainText, mt.Height, h, v, mt.Rotation * RadToDeg,
|
||||
StyleOf(mt.Style), 1, mt.RectangleWidth);
|
||||
// MTEXT nemá šířkový faktor jako vlastnost entity – nese ho buď kód \W
|
||||
// na začátku hodnoty, nebo (jinak) styl textu.
|
||||
var (text, wfKod) = MTextSirka(mt.Value, mt.PlainText);
|
||||
double wf = wfKod ?? (mt.Style?.Width > 0 ? mt.Style.Width : 1);
|
||||
g.Text(mt.InsertPoint.X, mt.InsertPoint.Y, text, mt.Height, h, v, mt.Rotation * RadToDeg,
|
||||
StyleOf(mt.Style), wf, mt.RectangleWidth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -219,6 +223,25 @@ public sealed partial class DxfTemplate
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Odloupne z MTEXTu úvodní kód šířkového faktoru <c>\W1.2;</c>. ACadSharp ho
|
||||
/// v <c>PlainText</c> nechává (jen bez zpětného lomítka), takže by se jinak vykreslil jako
|
||||
/// text. Vrací čistý text a faktor, když tam byl.</summary>
|
||||
internal static (string Text, double? WidthFactor) MTextSirka(string? hodnota, string? plainText)
|
||||
{
|
||||
string plain = plainText ?? "";
|
||||
if (hodnota is null || !hodnota.StartsWith(@"\W", StringComparison.Ordinal)) return (plain, null);
|
||||
int strednik = hodnota.IndexOf(';', 2);
|
||||
if (strednik < 0) return (plain, null);
|
||||
if (!double.TryParse(hodnota[2..strednik], System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out double wf) || wf <= 0)
|
||||
return (plain, null);
|
||||
|
||||
// Z PlainText ubyla jen zpětná lomítka, tedy „W1.2;" bez prvního znaku kódu.
|
||||
string predpona = hodnota[1..(strednik + 1)];
|
||||
if (plain.StartsWith(predpona, StringComparison.Ordinal)) plain = plain[predpona.Length..];
|
||||
return (plain, wf);
|
||||
}
|
||||
|
||||
/// <summary>Popis hladiny pro kreslítko – celý, ať se dá ve výstupu postavit znovu
|
||||
/// (barva z tabulky hladin, tloušťka, typ čáry, příznak tisku).</summary>
|
||||
private static HladinaStav HladinaZ(ACadSharp.Tables.Layer? layer)
|
||||
@@ -275,13 +298,14 @@ public sealed partial class DxfTemplate
|
||||
{
|
||||
double obliqueRad = entityObliqueRad != 0 ? entityObliqueRad : s?.ObliqueAngle ?? 0;
|
||||
if (s is null) return new(ObliqueDeg: obliqueRad * RadToDeg);
|
||||
string? file = s.IsShapeFile || string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
|
||||
string? file = string.IsNullOrWhiteSpace(s.Filename) ? null : s.Filename;
|
||||
string? ttf = s.IsShapeFile ? null : file;
|
||||
bool bold = s.TrueType.HasFlag(FontFlags.Bold);
|
||||
bool italic = s.TrueType.HasFlag(FontFlags.Italic);
|
||||
// Rodina písma: z .ttf souboru; když styl soubor nemá (TTF zadané jen jménem), vezmi
|
||||
// název typu písma z XData(ACAD) – tam AutoCAD ukládá rodinu (např. „Arial Black").
|
||||
string? family = file is not null ? Path.GetFileNameWithoutExtension(file) : FontRodinaZeXData(s);
|
||||
return new(family, bold, italic, file, obliqueRad * RadToDeg);
|
||||
string? family = ttf is not null ? Path.GetFileNameWithoutExtension(ttf) : FontRodinaZeXData(s);
|
||||
return new(family, bold, italic, file, obliqueRad * RadToDeg, s.Name, s.IsShapeFile);
|
||||
}
|
||||
|
||||
/// <summary>Název rodiny TrueType písma uložený ve stylu jako XData appid „ACAD" (první ASCII
|
||||
|
||||
@@ -124,9 +124,11 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
{
|
||||
var mtext = new MText
|
||||
{
|
||||
Value = text.Replace("\r\n", "\\P").Replace("\n", "\\P"),
|
||||
Value = MTextHodnota(text, widthFactor),
|
||||
InsertPoint = new XYZ(x, y, 0),
|
||||
Height = heightMm,
|
||||
// MTEXT nemá zapisovatelné natočení – nese ho směrový vektor osy X (group 11).
|
||||
AlignmentPoint = SmerOsyX(rotationDeg),
|
||||
RectangleWidth = maxWidthMm,
|
||||
AttachmentPoint = MapAttach(h, v),
|
||||
};
|
||||
@@ -185,6 +187,40 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Směrový vektor osy X textu pro dané natočení – tím se natáčí MTEXT.</summary>
|
||||
private static XYZ SmerOsyX(double rotationDeg)
|
||||
{
|
||||
double rad = rotationDeg * Math.PI / 180.0;
|
||||
return new XYZ(Math.Cos(rad), Math.Sin(rad), 0);
|
||||
}
|
||||
|
||||
/// <summary>Text pro MTEXT. Escapuje znaky, které jsou v MTEXTu řídicí (zpětné lomítko a
|
||||
/// složené závorky) – bez toho by uživatelova hodnota se zpětným lomítkem rozbila formátování –,
|
||||
/// zlomy řádků převede na <c>\P</c> a šířkový faktor doplní kódem <c>\W</c>: MTEXT ho na rozdíl
|
||||
/// od TEXTu nemá jako vlastnost entity, takže jinak by se ztratil.</summary>
|
||||
private static string MTextHodnota(string text, double widthFactor)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
if (widthFactor > 0 && Math.Abs(widthFactor - 1) > 1e-9)
|
||||
sb.Append(@"\W")
|
||||
.Append(widthFactor.ToString("0.####", System.Globalization.CultureInfo.InvariantCulture))
|
||||
.Append(';');
|
||||
|
||||
foreach (var znak in text)
|
||||
{
|
||||
switch (znak)
|
||||
{
|
||||
case '\\': sb.Append(@"\\"); break;
|
||||
case '{': sb.Append(@"\{"); break;
|
||||
case '}': sb.Append(@"\}"); break;
|
||||
case '\n': sb.Append(@"\P"); break;
|
||||
case '\r': break;
|
||||
default: sb.Append(znak); break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Převede analytickou hranu obrysu na DXF hraniční edge šrafy (úsečka / kruhový oblouk).
|
||||
/// U oblouku: záporný rozsah (CW) se v DXF vyjádří <c>CounterClockWise=false</c>, kde čtečka úhly
|
||||
/// zrcadlí přes osu X – proto pro CW meze <c>−A0</c> / <c>−(A0+Sweep)</c>, znormalizované do [0,2π).</summary>
|
||||
@@ -227,20 +263,41 @@ public sealed class DxfProfileRenderer : IProfileRenderer
|
||||
/// stylu (použije se výchozí styl dokumentu).</summary>
|
||||
private AcTextStyle? StylProEntitu(TextStyle s)
|
||||
{
|
||||
// Soubor fontu: přednostně .ttf z předlohy, jinak odvozeno z rodiny písma.
|
||||
// Soubor fontu: přednostně ten z předlohy (.ttf i .shx), jinak odvozeno z rodiny písma.
|
||||
string? file = s.FontFile ?? (string.IsNullOrWhiteSpace(s.FontFamily) ? null : s.FontFamily + ".ttf");
|
||||
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily)) return null;
|
||||
if (file is null && string.IsNullOrWhiteSpace(s.FontFamily) && string.IsNullOrWhiteSpace(s.Nazev))
|
||||
return null;
|
||||
|
||||
string klic = $"{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}";
|
||||
// Klíčem je jméno stylu ze zdroje, pokud ho zná – jeden zdrojový styl = jeden styl ve
|
||||
// výstupu. Bez toho vznikal styl pro každou kombinaci fontu a sklonu a jména se
|
||||
// vymýšlela (Verdana_1, Arial_2), takže se ve správci stylů nedalo nic najít.
|
||||
string klic = string.IsNullOrWhiteSpace(s.Nazev)
|
||||
? $"\0odvozeny|{file}|{s.FontFamily}|{(s.Bold ? "B" : "")}{(s.Italic ? "I" : "")}|{s.ObliqueDeg}"
|
||||
: s.Nazev!;
|
||||
if (_styly.TryGetValue(klic, out var styl)) return styl;
|
||||
|
||||
var ff = FontFlags.Regular;
|
||||
if (s.Bold) ff |= FontFlags.Bold;
|
||||
if (s.Italic) ff |= FontFlags.Italic;
|
||||
|
||||
string nazev = UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic);
|
||||
string nazev = string.IsNullOrWhiteSpace(s.Nazev)
|
||||
? UnikatniNazevStylu(s.FontFamily ?? Path.GetFileNameWithoutExtension(file) ?? "Styl", s.Bold, s.Italic)
|
||||
: s.Nazev!;
|
||||
|
||||
if (_doc.TextStyles.TryGetValue(nazev, out var existujici))
|
||||
{
|
||||
_styly[klic] = existujici;
|
||||
return existujici;
|
||||
}
|
||||
|
||||
// Oblique i na stylu – TEXT ho nese sám (group 51), ale MTEXT sklon přebírá jen ze stylu.
|
||||
styl = new AcTextStyle(nazev) { Filename = file ?? "", TrueType = ff, ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0 };
|
||||
styl = new AcTextStyle(nazev)
|
||||
{
|
||||
Filename = file ?? "",
|
||||
TrueType = ff,
|
||||
ObliqueAngle = s.ObliqueDeg * Math.PI / 180.0,
|
||||
Flags = s.JeShx ? ACadSharp.Tables.StyleFlags.IsShape : ACadSharp.Tables.StyleFlags.None,
|
||||
};
|
||||
_doc.TextStyles.Add(styl);
|
||||
_styly[klic] = styl;
|
||||
return styl;
|
||||
|
||||
@@ -8,12 +8,18 @@ public enum VAlign { Baseline, Bottom, Middle, Top }
|
||||
|
||||
/// <summary>
|
||||
/// Vzhled písma přenesený z DXF textového stylu (ROZPISKY.md §3): rodina fontu + řez,
|
||||
/// volitelně přímo soubor fontu (.ttf) pro věrný řez. <c>default</c> = fallback font rendereru.
|
||||
/// volitelně přímo soubor fontu pro věrný řez. <c>default</c> = fallback font rendereru.
|
||||
/// <paramref name="ObliqueDeg"/> = sklon znaků ve stupních (DXF oblique angle, group 51 entity /
|
||||
/// group 50 stylu); kladný = naklonění doprava, 0 = vzpřímeně.
|
||||
/// <para><paramref name="Nazev"/> = jméno stylu ve zdrojovém výkresu („Popis základní",
|
||||
/// „Název stavby"). Nese se dál, aby ho DXF export zapsal beze změny – dřív se zahazovalo
|
||||
/// a v exportu vznikala náhradní jména typu <c>Verdana_1</c>. <paramref name="JeShx"/> = styl
|
||||
/// odkazuje na vektorový font AutoCADu (.shx); ten se nedá načíst jako TrueType, takže se
|
||||
/// kreslí náhradním fontem, ale do DXF se zapíše původní.</para>
|
||||
/// </summary>
|
||||
public readonly record struct TextStyle(string? FontFamily = null, bool Bold = false, bool Italic = false,
|
||||
string? FontFile = null, double ObliqueDeg = 0);
|
||||
string? FontFile = null, double ObliqueDeg = 0,
|
||||
string? Nazev = null, bool JeShx = false);
|
||||
|
||||
/// <summary>Druh hrany obrysu: úsečka nebo kruhový oblouk. Elipsy a spliny se do obrysu vkládají
|
||||
/// jako jemná řada úseček (viz <see cref="Edge"/>), takže renderery vystačí se dvěma případy.</summary>
|
||||
|
||||
@@ -66,7 +66,8 @@ public sealed class SkiaProfileRenderer : IProfileRenderer, IDisposable
|
||||
private SKTypeface Typeface(TextStyle style)
|
||||
{
|
||||
// 1) Přesný font ze souboru, na který odkazuje DXF styl (.ttf v systémové složce Fonts).
|
||||
if (!string.IsNullOrWhiteSpace(style.FontFile))
|
||||
// SHX (vektorový font AutoCADu) se takhle načíst nedá – jde rovnou na náhradu.
|
||||
if (!style.JeShx && !string.IsNullOrWhiteSpace(style.FontFile))
|
||||
{
|
||||
var fileKey = ("\0file:" + style.FontFile, 0, false);
|
||||
if (_typefaces.TryGetValue(fileKey, out var tff)) return tff;
|
||||
|
||||
Reference in New Issue
Block a user