feat: port p-rec-annot ortho batch and layer rules
This commit is contained in:
@@ -159,6 +159,16 @@ public enum OrthoAnnotationRepresentation { None, MultiLeader, BubbleBlock }
|
||||
public sealed record OrthoAnnotationOptions(bool Supports, bool Valves, bool Instruments, bool Plugs, bool Boards);
|
||||
public sealed record OrthoAnnotationClassification(string StableId, OrthoAnnotationRepresentation Representation, string Text, IReadOnlyList<string> Reasons);
|
||||
public sealed record OrthoAnnotationAssets(string Layer, string TextStyle, string MultiLeaderStyle, string BubbleBlock = "BLOCK_ANNO_POSITION");
|
||||
public sealed record OrthoBatchDrawing(string DrawingGuid, string DisplayName, string RelativePath, IReadOnlyList<string> Views);
|
||||
public sealed record OrthoBatchSelection(string RelativePath, IReadOnlyList<string> Views);
|
||||
public sealed record OrthoBatchOperation(string DrawingGuid, string DisplayName, string RelativePath, string ViewName);
|
||||
public sealed record OrthoBatchPlan(IReadOnlyList<OrthoBatchOperation> Operations, IReadOnlyList<string> Diagnostics);
|
||||
public enum OrthoLayerFilterKind { Status, Layer, Table }
|
||||
public sealed record OrthoLayerRule(OrthoLayerFilterKind Kind, string Filter, int? LineWeight, string LineType, short? ColorIndex, byte? Red, byte? Green, byte? Blue)
|
||||
{
|
||||
public bool MatchesStatus(string status) => Kind == OrthoLayerFilterKind.Status && string.Equals(Filter, status, StringComparison.OrdinalIgnoreCase);
|
||||
public bool MatchesLayer(string viewName, string layer) => Kind == OrthoLayerFilterKind.Layer && string.Equals(viewName + "-" + Filter, layer, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public interface IPlantComponentGeometryReader
|
||||
{
|
||||
ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
// v1.0.4.2 identity rule: a selected drawing is matched by its normalized
|
||||
// project-relative path, never by the display/file name alone.
|
||||
public sealed class OrthoBatchPlanner
|
||||
{
|
||||
public OrthoBatchPlan Build(IEnumerable<OrthoBatchDrawing> drawings, IEnumerable<OrthoBatchSelection> selections)
|
||||
{
|
||||
var diagnostics = new List<string>(); var operations = new List<OrthoBatchOperation>();
|
||||
var source = drawings.OrderBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase).ThenBy(x => x.DrawingGuid, StringComparer.Ordinal).ToArray();
|
||||
var duplicateIds = source.GroupBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase).Where(x => x.Count() > 1).Select(x => x.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var selection in selections.OrderBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var relative = Normalize(selection.RelativePath);
|
||||
if (duplicateIds.Contains(relative)) { diagnostics.Add("ORTHO_DRAWING_RELATIVE_PATH_AMBIGUOUS:" + relative); continue; }
|
||||
var drawing = source.SingleOrDefault(x => string.Equals(Normalize(x.RelativePath), relative, StringComparison.OrdinalIgnoreCase));
|
||||
if (drawing is null) { diagnostics.Add("ORTHO_DRAWING_NOT_FOUND:" + relative); continue; }
|
||||
foreach (var view in selection.Views.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||
if (drawing.Views.Contains(view, StringComparer.OrdinalIgnoreCase)) operations.Add(new(drawing.DrawingGuid, drawing.DisplayName, relative, view));
|
||||
else diagnostics.Add($"ORTHO_VIEW_NOT_FOUND:{relative}:{view}");
|
||||
}
|
||||
return new(operations.OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.ViewName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.DrawingGuid, StringComparer.Ordinal).ToArray(), diagnostics.OrderBy(x => x, StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
public static string Normalize(string relativePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relativePath) || Path.IsPathRooted(relativePath)) throw new ArgumentException("ORTHO_RELATIVE_PATH_REQUIRED", nameof(relativePath));
|
||||
var parts = relativePath.Replace('/', '\\').Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Any(x => x == "..")) throw new ArgumentException("ORTHO_RELATIVE_PATH_ESCAPES_PROJECT", nameof(relativePath));
|
||||
return string.Join('\\', parts.Where(x => x != "."));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Xml.Linq;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class OrthoLayerSettingsLoader
|
||||
{
|
||||
public IReadOnlyList<OrthoLayerRule> Load(string xml)
|
||||
{
|
||||
var root = XElement.Parse(xml).Element("LayersSetup") ?? throw Invalid("LayersSetup");
|
||||
var result = root.Elements("LayerSetup").Select(Parse).ToArray();
|
||||
return result.Length > 0 ? result : throw Invalid("LayerSetup");
|
||||
}
|
||||
private static OrthoLayerRule Parse(XElement value)
|
||||
{
|
||||
var kind = Required(value, "filtertype").ToUpperInvariant() switch { "STATUS" => OrthoLayerFilterKind.Status, "LAYER" => OrthoLayerFilterKind.Layer, _ => OrthoLayerFilterKind.Table };
|
||||
var filter = Required(value, "filtervalue", true); if (filter.Length == 0) filter = "(Undefined)";
|
||||
int? weight = int.TryParse(Required(value, "lineweight", true), out var parsedWeight) ? parsedWeight : null;
|
||||
var color = Required(value, "color", true); short? index = short.TryParse(color, out var parsedIndex) ? parsedIndex : null; byte? red = null, green = null, blue = null;
|
||||
var rgb = color.Split(','); if (rgb.Length == 3 && byte.TryParse(rgb[0], out var r) && byte.TryParse(rgb[1], out var g) && byte.TryParse(rgb[2], out var b)) { red = r; green = g; blue = b; index = null; }
|
||||
return new(kind, filter, weight, Required(value, "linetype", true), index, red, green, blue);
|
||||
}
|
||||
private static string Required(XElement value, string name, bool empty = false) => value.Attribute(name) is { Value: var result } && (empty || !string.IsNullOrWhiteSpace(result)) ? result : throw Invalid(name);
|
||||
private static InvalidDataException Invalid(string field) => new("ORTHO_LAYER_SETTINGS_INVALID:" + field);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
using PRecAnnot.Core;
|
||||
|
||||
namespace PRecAnnot.Core.Tests;
|
||||
|
||||
public sealed class OrthoBatchPlannerTests
|
||||
{
|
||||
[Fact]
|
||||
public void KeepsSameNameDrawingsDistinctByRelativePath()
|
||||
{
|
||||
var plan = new OrthoBatchPlanner().Build([
|
||||
new("g2", "A.dwg", "Ortho\\Area2\\A.dwg", ["V2"]),
|
||||
new("g1", "A.dwg", "Ortho\\Area1\\A.dwg", ["V1"])],
|
||||
[new("Ortho/Area2/A.dwg", ["V2"]), new("Ortho\\Area1\\A.dwg", ["V1"])]);
|
||||
Assert.Collection(plan.Operations, x => Assert.Equal("g1", x.DrawingGuid), x => Assert.Equal("g2", x.DrawingGuid));
|
||||
Assert.Empty(plan.Diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsolatesMissingDrawingAndViewFailures()
|
||||
{
|
||||
var plan = new OrthoBatchPlanner().Build([new("g", "A", "Ortho\\A.dwg", ["V1"])], [new("Ortho\\A.dwg", ["missing", "V1"]), new("Ortho\\B.dwg", ["V2"])]);
|
||||
Assert.Single(plan.Operations); Assert.Equal(2, plan.Diagnostics.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsEscapingAndAbsolutePaths()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => OrthoBatchPlanner.Normalize("..\\x.dwg"));
|
||||
Assert.Throws<ArgumentException>(() => OrthoBatchPlanner.Normalize("C:\\x.dwg"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
using PRecAnnot.Core;
|
||||
namespace PRecAnnot.Core.Tests;
|
||||
public sealed class OrthoLayerSettingsLoaderTests
|
||||
{
|
||||
[Fact] public void ParsesStatusLayerAndRgbRules()
|
||||
{
|
||||
var rules = new OrthoLayerSettingsLoader().Load("<Main><LayersSetup><LayerSetup filtertype='Status' filtervalue='Reserved' lineweight='30' linetype='Continuous' color='1'/><LayerSetup filtertype='Layer' filtervalue='Axis' lineweight='bad' linetype='' color='1,2,3'/></LayersSetup></Main>");
|
||||
Assert.True(rules[0].MatchesStatus("reserved")); Assert.Equal(30, rules[0].LineWeight); Assert.Equal((byte)2, rules[1].Green); Assert.True(rules[1].MatchesLayer("V1", "v1-axis"));
|
||||
}
|
||||
[Fact] public void RejectsOldOrEmptyShape() => Assert.Throws<InvalidDataException>(() => new OrthoLayerSettingsLoader().Load("<Main><LayersSetup/></Main>"));
|
||||
}
|
||||
Reference in New Issue
Block a user