diff --git a/Rozpisky.Tests/TextStylyRoundTripTesty.cs b/Rozpisky.Tests/TextStylyRoundTripTesty.cs
new file mode 100644
index 0000000..c0f6d2f
--- /dev/null
+++ b/Rozpisky.Tests/TextStylyRoundTripTesty.cs
@@ -0,0 +1,102 @@
+using System.IO;
+using ACadSharp.Entities;
+using ACadSharp.IO;
+using Rozpisky.Dxf;
+using Rozpisky.Rendering;
+using Xunit;
+
+namespace Rozpisky.Tests;
+
+///
+/// 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.
+///
+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 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().Select(t => t.Style?.Name)
+ .Concat(doc.Entities.OfType().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());
+ 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());
+ 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());
+ 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());
+ Assert.Contains(@"\P", mt.Value);
+ Assert.Equal("první\ndruhý", mt.PlainText.Replace("\r\n", "\n"));
+ }
+}
diff --git a/Rozpisky/Dxf/DxfTemplate.cs b/Rozpisky/Dxf/DxfTemplate.cs
index dceeb12..4435b49 100644
--- a/Rozpisky/Dxf/DxfTemplate.cs
+++ b/Rozpisky/Dxf/DxfTemplate.cs
@@ -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;
}
+ /// Odloupne z MTEXTu úvodní kód šířkového faktoru \W1.2;. ACadSharp ho
+ /// v PlainText 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.
+ 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);
+ }
+
/// 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).
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);
}
/// Název rodiny TrueType písma uložený ve stylu jako XData appid „ACAD" (první ASCII
diff --git a/Rozpisky/Rendering/DxfProfileRenderer.cs b/Rozpisky/Rendering/DxfProfileRenderer.cs
index a655c4e..fb228d1 100644
--- a/Rozpisky/Rendering/DxfProfileRenderer.cs
+++ b/Rozpisky/Rendering/DxfProfileRenderer.cs
@@ -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
}
}
+ /// Směrový vektor osy X textu pro dané natočení – tím se natáčí MTEXT.
+ private static XYZ SmerOsyX(double rotationDeg)
+ {
+ double rad = rotationDeg * Math.PI / 180.0;
+ return new XYZ(Math.Cos(rad), Math.Sin(rad), 0);
+ }
+
+ /// 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 \P a šířkový faktor doplní kódem \W: MTEXT ho na rozdíl
+ /// od TEXTu nemá jako vlastnost entity, takže jinak by se ztratil.
+ 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();
+ }
+
/// 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ří CounterClockWise=false, kde čtečka úhly
/// zrcadlí přes osu X – proto pro CW meze −A0 / −(A0+Sweep), znormalizované do [0,2π).
@@ -227,20 +263,41 @@ public sealed class DxfProfileRenderer : IProfileRenderer
/// stylu (použije se výchozí styl dokumentu).
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;
diff --git a/Rozpisky/Rendering/IProfileRenderer.cs b/Rozpisky/Rendering/IProfileRenderer.cs
index c5cd576..6994fec 100644
--- a/Rozpisky/Rendering/IProfileRenderer.cs
+++ b/Rozpisky/Rendering/IProfileRenderer.cs
@@ -8,12 +8,18 @@ public enum VAlign { Baseline, Bottom, Middle, Top }
///
/// 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. default = fallback font rendereru.
+/// volitelně přímo soubor fontu pro věrný řez. default = fallback font rendereru.
/// = sklon znaků ve stupních (DXF oblique angle, group 51 entity /
/// group 50 stylu); kladný = naklonění doprava, 0 = vzpřímeně.
+/// = 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 Verdana_1. = 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í.
///
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);
/// Druh hrany obrysu: úsečka nebo kruhový oblouk. Elipsy a spliny se do obrysu vkládají
/// jako jemná řada úseček (viz ), takže renderery vystačí se dvěma případy.
diff --git a/Rozpisky/Rendering/SkiaProfileRenderer.cs b/Rozpisky/Rendering/SkiaProfileRenderer.cs
index 87df9d0..d33f52e 100644
--- a/Rozpisky/Rendering/SkiaProfileRenderer.cs
+++ b/Rozpisky/Rendering/SkiaProfileRenderer.cs
@@ -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;