Add visual template coordinate editor

This commit is contained in:
2026-08-30 19:29:06 +02:00
parent 2f2761caf8
commit 3f71f7f371
7 changed files with 483 additions and 10 deletions
@@ -30,6 +30,14 @@ public partial class DesignerViewModel : ObservableObject
[ObservableProperty] private DesignerPlaceholder? _selectedPlaceholder;
[ObservableProperty] private DesignerAsset? _selectedAsset;
[ObservableProperty] private StarterTemplateItem? _selectedStarterTemplate;
[ObservableProperty] private OverlayEditorMode _overlayMode = OverlayEditorMode.Measure;
[ObservableProperty] private bool _snapOverlayToGrid = true;
[ObservableProperty] private double _overlayGridSize = 1;
[ObservableProperty] private string _overlayCoordinates = "x= · y=";
[ObservableProperty] private string _selectedOverlayElement = "Kein Element ausgewählt";
[ObservableProperty] private double _overlayPageWidth = 210;
[ObservableProperty] private double _overlayPageHeight = 297;
[ObservableProperty] private string _overlayPageUnit = "mm";
public IReadOnlyList<string> Units { get; } = ["mm", "cm", "pt", "in"];
public IReadOnlyList<PlaceholderType> PlaceholderTypes { get; } = Enum.GetValues<PlaceholderType>();
@@ -190,7 +198,54 @@ public partial class DesignerViewModel : ObservableObject
: $"Asset „{selected.Name}“ und {removedReferences} Layout-Referenz(en) wurden entfernt.", false);
}
partial void OnLayoutSourceChanged(string value) => RefreshAssetUsage();
public void ApplyMeasurement(OverlayMeasurement measurement)
{
NewX = FormatNumber(measurement.X); NewY = FormatNumber(measurement.Y);
if (measurement.HasArea)
{
NewWidth = FormatNumber(measurement.Width); NewHeight = FormatNumber(measurement.Height);
SetStatus($"Bereich übernommen: x={NewX}, y={NewY}, b={NewWidth}, h={NewHeight} {OverlayPageUnit}.", false);
}
else SetStatus($"Koordinate übernommen: x={NewX}, y={NewY} {OverlayPageUnit}.", false);
}
public void ApplyElementGeometry(OverlayElementGeometry geometry)
{
var layout = new LayoutParser().Parse(LayoutSource);
var element = layout.Elements.FirstOrDefault(x => x.Line == geometry.Line)
?? throw new InvalidDataException($"Element in Zeile {geometry.Line} wurde nicht gefunden.");
var lines = LayoutSource.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n').ToList();
var index = geometry.Line - 1;
if (index < 0 || index >= lines.Count) throw new InvalidDataException("Elementzeile liegt außerhalb des Layouts.");
var x = FormatNumber(geometry.X); var y = FormatNumber(geometry.Y);
var width = geometry.Width; var height = geometry.Height;
lines[index] = element switch
{
ImageElement image => BuildImageLine(image, x, y, width, height),
TextElement text => $"TEXT {x} {y} {Content(text.Content, text.Placeholder, text.Format)}{Attributes(text.Attributes)}",
TextBoxElement box => $"TEXTBOX {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
+ $"{Content(box.Content, box.Placeholder, box.Format)}{Attributes(box.Attributes)}",
TableElement table => $"TABLE {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
+ $"${table.Placeholder}{Attributes(table.Attributes)}",
ChartElement chart => $"CHART {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
+ $"${chart.Placeholder}{Attributes(chart.Attributes)}",
_ => lines[index],
};
LayoutSource = string.Join('\n', lines); CanExport = false;
SelectedOverlayElement = $"{geometry.Keyword} · Zeile {geometry.Line}";
SetStatus($"{geometry.Keyword} verschoben/skalisiert. PDF-Vorschau wird aktualisiert.", false);
}
partial void OnLayoutSourceChanged(string value)
{
RefreshAssetUsage();
try
{
var page = new LayoutParser().Parse(value);
OverlayPageWidth = page.Width; OverlayPageHeight = page.Height; OverlayPageUnit = page.Unit;
}
catch (TemplateValidationException) { }
}
private DesignerAsset AddOrReplaceAsset(string sourceName, byte[] bytes, bool keepName)
{
@@ -253,6 +308,19 @@ public partial class DesignerViewModel : ObservableObject
return value.ToString("0.###", CultureInfo.InvariantCulture);
}
private static string BuildImageLine(ImageElement image, string x, string y, double effectiveWidth, double effectiveHeight)
{
var scale = image.Attributes.TryGetValue("scale", out var raw) ? LayoutParser.Percentage(raw) : 1f;
return $"IMG {image.Path} {x} {y} {FormatNumber(effectiveWidth / scale)} "
+ $"{FormatNumber(effectiveHeight / scale)}{Attributes(image.Attributes)}";
}
private static string Content(string original, string? placeholder, string? format) => placeholder is null
? $"\"{original.Replace("\"", "\\\"")}\""
: $"${placeholder}{(format is null ? "" : "|" + format)}";
private static string Attributes(IReadOnlyDictionary<string, string> attributes) => attributes.Count == 0
? "" : " " + string.Join(' ', attributes.Select(x => $"{x.Key}={x.Value}"));
private static string FormatNumber(double value) => value.ToString("0.###", CultureInfo.InvariantCulture);
public void SetStatus(string text, bool error)
{ Status = text; StatusColor = error ? "#B91C1C" : "#166534"; }
private static string QuoteIfLiteral(string value) => value.StartsWith('$') ? value : $"\"{value.Replace("\"", "\\\"")}\"";