feat: complete p-rec-annot line connection pipeline

This commit is contained in:
codex
2026-08-07 09:59:54 +03:00
parent 01a0b711d0
commit 532c9dc1f2
11 changed files with 264 additions and 11 deletions
@@ -83,7 +83,9 @@ public sealed record LineConnectionBoundaryCandidate(
double Rotation, double Rotation,
string? FirstLineGroupStableId = null, string? FirstLineGroupStableId = null,
string? SecondLineGroupStableId = null); string? SecondLineGroupStableId = null);
public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons); public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons, string? FirstText = null, string? SecondText = null);
public sealed record LineConnectionAssets(string PolylineLayer, string TextLayer, string TextStyle, string BlockName = "BLOCK_LINENUMBER_CONNECTION");
public sealed record LineConnectionApplyResult(int Deleted, int Created);
// The geometry adapter supplies the paper-space connector point and orientation. // The geometry adapter supplies the paper-space connector point and orientation.
// Keeping it separate from the PnP relation snapshot prevents Autodesk objects // Keeping it separate from the PnP relation snapshot prevents Autodesk objects
// from crossing into the deterministic PXP-ALC planning layer. // from crossing into the deterministic PXP-ALC planning layer.
@@ -138,6 +140,7 @@ public sealed record GeometrySnapshot
public Bounds2Dto? ProjectedBounds { get; init; } public Bounds2Dto? ProjectedBounds { get; init; }
public double? Rotation { get; init; } public double? Rotation { get; init; }
public double? Elevation { get; init; } public double? Elevation { get; init; }
public Vector2Dto? PaperDirection { get; init; }
} }
// Platform-neutral result of the legacy GetComponentGeometry contract. The // Platform-neutral result of the legacy GetComponentGeometry contract. The
@@ -41,7 +41,7 @@ public sealed class LineConnectionBoundaryPlanner
continue; continue;
} }
var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray(); var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray();
result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty<string>())); result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty<string>(), first, second));
} }
return result; return result;
} }
@@ -0,0 +1,20 @@
using System.Xml.Linq;
using PRecAnnot.Abstractions;
namespace PRecAnnot.Core;
public sealed class LineConnectionSettingsLoader
{
public LineConnectionAssets Load(string xml)
{
try
{
var element = XDocument.Parse(xml).Root?.Element("LinenumberConnections") ?? throw new InvalidOperationException("LINE_CONNECTION_SETTINGS_MISSING");
string Required(string name) => string.IsNullOrWhiteSpace((string?)element.Attribute(name))
? throw new InvalidOperationException("LINE_CONNECTION_SETTING_MISSING:" + name)
: (string)element.Attribute(name)!;
return new(Required("polylinelayer"), Required("textlayer"), Required("textstyle"));
}
catch (System.Xml.XmlException exception) { throw new InvalidOperationException("LINE_CONNECTION_SETTINGS_XML_INVALID", exception); }
}
}
@@ -44,6 +44,7 @@ public sealed class EntryPoint : IExtensionApplication
[CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber(); [CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber();
[CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText(); [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText();
[CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition(); [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition();
[CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run();
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
@@ -0,0 +1,72 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using PRecAnnot.Abstractions;
namespace PRecAnnot.Plant3D2026;
internal sealed class LineConnectionAnnotationWriter
{
private const string ApplicationName = "PXP-ALC";
public LineConnectionApplyResult Replace(Document document, string viewportStableId, IReadOnlyList<LineConnectionBoundaryPlan> plans, LineConnectionAssets assets)
{
var insertable = plans.Where(plan => plan.Reasons.Count == 0).OrderBy(plan => plan.Position.X).ThenByDescending(plan => plan.Position.Y).ToArray();
using var documentLock = document.LockDocument();
var database = document.Database;
using var transaction = database.TransactionManager.StartTransaction();
var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
if (!blocks.Has(assets.BlockName)) throw new InvalidOperationException("LINE_CONNECTION_BLOCK_MISSING:" + assets.BlockName);
if (!layers.Has(assets.PolylineLayer)) throw new InvalidOperationException("LINE_CONNECTION_POLYLINE_LAYER_MISSING:" + assets.PolylineLayer);
if (!layers.Has(assets.TextLayer)) throw new InvalidOperationException("LINE_CONNECTION_TEXT_LAYER_MISSING:" + assets.TextLayer);
if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("LINE_CONNECTION_TEXT_STYLE_MISSING:" + assets.TextStyle);
EnsureRegApp(transaction, database);
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
var deleted = 0;
foreach (ObjectId id in paper)
{
var value = transaction.GetObject(id, OpenMode.ForRead);
var data = value.GetXDataForApplication(ApplicationName)?.AsArray();
if (data is null || !data.Any(item => item.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(item.Value?.ToString(), viewportStableId, StringComparison.Ordinal))) continue;
value.UpgradeOpen(); value.Erase(); deleted++;
}
var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BlockName], OpenMode.ForRead);
foreach (var plan in insertable)
{
using var reference = new BlockReference(new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z), definition.ObjectId) { LayerId = layers[assets.PolylineLayer], Rotation = plan.Rotation };
reference.XData = Ownership(viewportStableId, plan);
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
var values = new[] { plan.FirstText ?? string.Empty, plan.SecondText ?? string.Empty };
var index = 0;
foreach (ObjectId entityId in definition)
{
if (transaction.GetObject(entityId, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue;
using var attribute = new AttributeReference();
attribute.SetAttributeFromBlock(source, reference.BlockTransform);
attribute.TextString = values[Math.Min(index++, 1)].Replace("\n", "\\P", StringComparison.Ordinal);
attribute.LayerId = layers[assets.TextLayer]; attribute.TextStyleId = styles[assets.TextStyle];
attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
}
}
transaction.Commit();
return new(deleted, insertable.Length);
}
private static ResultBuffer Ownership(string viewport, LineConnectionBoundaryPlan plan) => new(
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.StableId),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LINENUMBER1"),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.FirstText ?? string.Empty),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LINENUMBER2"),
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.SecondText ?? string.Empty));
private static void EnsureRegApp(Transaction transaction, Database database)
{
var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
if (table.Has(ApplicationName)) return;
table.UpgradeOpen(); using var record = new RegAppTableRecord { Name = ApplicationName }; table.Add(record); transaction.AddNewlyCreatedDBObject(record, true);
}
}
@@ -0,0 +1,39 @@
using System.Text.Json;
using Autodesk.AutoCAD.ApplicationServices;
using PRecAnnot.Abstractions;
namespace PRecAnnot.Plant3D2026;
internal sealed record LineConnectionApplyPermit
{
public required bool AllowApply { get; init; }
public required string FixtureMarkerPath { get; init; }
public required string ExpectedViewportStableId { get; init; }
public required IReadOnlyList<string> ExpectedConnectorStableIds { get; init; }
}
internal static class LineConnectionApplyPermitReader
{
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_CONNECTION_APPLY";
public static LineConnectionApplyPermit Read()
{
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_CONNECTION_APPLY_PERMIT_PATH");
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_REQUIRED");
return JsonSerializer.Deserialize<LineConnectionApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
?? throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_INVALID");
}
public static void Validate(Document document, LineConnectionApplyPermit permit, string viewportId, IReadOnlyList<LineConnectionBoundaryPlan> plans)
{
var actual = plans.Where(plan => plan.Reasons.Count == 0).Select(plan => plan.StableId).OrderBy(value => value, StringComparer.Ordinal).ToArray();
var expected = permit.ExpectedConnectorStableIds.OrderBy(value => value, StringComparer.Ordinal).ToArray();
if (!permit.AllowApply || !string.Equals(permit.ExpectedViewportStableId, viewportId, StringComparison.Ordinal) || !actual.SequenceEqual(expected, StringComparer.Ordinal))
throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_PLAN_MISMATCH");
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
throw new InvalidOperationException("LINE_CONNECTION_APPLY_TEST_FIXTURE_REQUIRED");
var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_CONNECTION_APPLY_TEST_FIXTURE_REQUIRED");
}
}
@@ -0,0 +1,78 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.ProcessPower.P3dProjectParts;
using PRecAnnot.Abstractions;
using PRecAnnot.Core;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
namespace PRecAnnot.Plant3D2026;
internal static class LineConnectionCommand
{
public static void Run()
{
var document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null) return;
try
{
var options = new PromptEntityOptions("\nВыберите видовой экран для аннотаций стыков номеров линий: ");
options.SetRejectMessage("\nТребуется видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
var selected = document.Editor.GetEntity(options);
if (selected.Status != PromptStatus.OK) return;
OrthoViewportInfo viewport;
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
{
var entity = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
viewport = ResolveViewport(document, entity);
}
var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("LINE_CONNECTION_PIPING_DATALINKS_UNAVAILABLE");
var relationships = new PlantConnectorRelationshipReader(manager).Read();
if (!relationships.Success) throw new InvalidOperationException(relationships.FailureCode);
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
var byRow = objects.Where(value => value.MembershipStatus == ViewMembershipStatus.Resolved && value.Geometry.PaperPosition is not null)
.Select(value => (Object: value, Row: ReadRowId(value)))
.Where(value => value.Row.HasValue).ToDictionary(value => value.Row!.Value, value => value.Object);
var placements = new Dictionary<string, LineConnectionPlacement>(StringComparer.Ordinal);
foreach (var relationship in relationships.Connectors)
{
if (!byRow.TryGetValue(relationship.ConnectorRowId, out var connector) || connector.Geometry.PaperPosition is not { } point) continue;
var direction = connector.Geometry.PaperDirection ?? new Vector2Dto(1, 0);
var horizontalLeader = Math.Abs(direction.Y) >= Math.Abs(direction.X);
var block = horizontalLeader ? new AnnotationPoint(viewport.CenterX, point.Y) : new AnnotationPoint(point.X, viewport.CenterY);
var endpoint = new Point3Dto(point.X, point.Y, 0);
placements[relationship.StableId] = new(endpoint, endpoint, new(endpoint, endpoint), block, 0);
}
var configuration = LineNumberRuntimeConfigurationReader.Read();
var definition = configuration.Annotations.Annotations.OrderBy(value => value.Nickname, StringComparer.Ordinal).First();
var preview = new LineConnectionPreviewService(new LineNumberFieldResolver(new PlantDataLinksPropertyReader(manager)), new LineNumberAnnotationFormatter(), new LineConnectionBoundaryPlanner())
.Preview(definition, relationships.Connectors, placements);
var ready = preview.Plans.Count(plan => plan.Reasons.Count == 0);
document.Editor.WriteMessage($"\nPXP-ALC preview: connectors={relationships.Connectors.Count}; eligible={ready}; skipped={preview.Plans.Count - ready}. DWG не изменён.");
RuntimeDiagnostics.Write("LineConnectionPreviewCompleted", commandName: "PXP-ALC", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["ConnectorCount"] = relationships.Connectors.Count, ["EligibleCount"] = ready });
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
var permit = LineConnectionApplyPermitReader.Read();
LineConnectionApplyPermitReader.Validate(document, permit, viewport.Id, preview.Plans);
var result = new LineConnectionAnnotationWriter().Replace(document, viewport.Id, preview.Plans, configuration.Connections);
document.Editor.WriteMessage($"\nPXP-ALC Apply: deleted={result.Deleted}; created={result.Created}.");
RuntimeDiagnostics.Write("LineConnectionApplyCompleted", commandName: "PXP-ALC", resultStatus: "Applied", document: document, details: new Dictionary<string, object?> { ["Deleted"] = result.Deleted, ["Created"] = result.Created });
}
catch (Exception exception)
{
RuntimeDiagnostics.Write("LineConnectionCommandFailed", "ERROR", commandName: "PXP-ALC", resultStatus: exception.Message, exception: exception, document: document);
document.Editor.WriteMessage("\nPXP-ALC: " + exception.Message);
}
}
private static OrthoViewportInfo ResolveViewport(Document document, Viewport selected)
{
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("LINE_CONNECTION_ORTHO_PROJECT_UNAVAILABLE");
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
.Where(value => Math.Abs(value.CenterX - selected.CenterPoint.X) < 0.001 && Math.Abs(value.CenterY - selected.CenterPoint.Y) < 0.001 && Math.Abs(value.Width - selected.Width) < 0.001 && Math.Abs(value.Height - selected.Height) < 0.001)
.ToArray();
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "LINE_CONNECTION_ORTHO_VIEW_NOT_FOUND" : "LINE_CONNECTION_ORTHO_VIEW_AMBIGUOUS");
}
private static long? ReadRowId(PlantObjectSnapshot value) => value.Properties.TryGetValue("PRecAnnot.RowId", out var text) && long.TryParse(text, out var row) ? row : null;
}
@@ -3,7 +3,7 @@ using PRecAnnot.Core;
namespace PRecAnnot.Plant3D2026; namespace PRecAnnot.Plant3D2026;
internal sealed record LineNumberRuntimeConfiguration(LineNumberAnnotationSettings Annotations, AnnotateLineNumbersAssets Assets); internal sealed record LineNumberRuntimeConfiguration(LineNumberAnnotationSettings Annotations, AnnotateLineNumbersAssets Assets, LineConnectionAssets Connections);
internal static class LineNumberRuntimeConfigurationReader internal static class LineNumberRuntimeConfigurationReader
{ {
@@ -11,7 +11,8 @@ internal static class LineNumberRuntimeConfigurationReader
{ {
var annotationPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_ANNOTATION_SETTINGS_PATH"); var annotationPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_ANNOTATION_SETTINGS_PATH");
var drawingPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH"); var drawingPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
return new(new LineNumberAnnotationSettingsLoader().Load(File.ReadAllText(annotationPath)), new AnnotateLineNumbersSettingsLoader().Load(File.ReadAllText(drawingPath))); var drawingXml = File.ReadAllText(drawingPath);
return new(new LineNumberAnnotationSettingsLoader().Load(File.ReadAllText(annotationPath)), new AnnotateLineNumbersSettingsLoader().Load(drawingXml), new LineConnectionSettingsLoader().Load(drawingXml));
} }
private static string RequiredLocalFile(string variable) private static string RequiredLocalFile(string variable)
@@ -294,6 +294,7 @@ internal sealed class PlantObjectReader : IPlantObjectReader
var properties = dataLinks.GetAllProperties(rowId, true) var properties = dataLinks.GetAllProperties(rowId, true)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase); .ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
properties["PRecAnnot.RowId"] = rowId.ToString(System.Globalization.CultureInfo.InvariantCulture);
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value; string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
snapshots.Add(new PlantObjectSnapshot snapshots.Add(new PlantObjectSnapshot
{ {
@@ -447,7 +448,7 @@ internal sealed class PlantViewportMembershipReader
if (sourceRows.Contains(rowId)) if (sourceRows.Contains(rowId))
{ {
selectedRows.Add(rowId); selectedRows.Add(rowId);
if (TryReadPipePlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement; if (TryReadPlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement;
} }
} }
catch (DLException) catch (DLException)
@@ -476,7 +477,7 @@ internal sealed class PlantViewportMembershipReader
// whose PrjObjectId identifies the source entity and whose Xform projects // whose PrjObjectId identifies the source entity and whose Xform projects
// that source entity into the particular Ortho view. This is not a sheet // that source entity into the particular Ortho view. This is not a sheet
// coordinate heuristic. // coordinate heuristic.
private static bool TryReadPipePlacement(MdObjectId sourceId, DataLinksManager dataLinks, out GeometrySnapshot geometry) private static bool TryReadPlacement(MdObjectId sourceId, DataLinksManager dataLinks, out GeometrySnapshot geometry)
{ {
geometry = new GeometrySnapshot(); geometry = new GeometrySnapshot();
var stage = "GetDatabase"; var stage = "GetDatabase";
@@ -492,14 +493,27 @@ internal sealed class PlantViewportMembershipReader
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction(); using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
stage = "MakeAcDbObjectId"; stage = "MakeAcDbObjectId";
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase); var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
stage = "OpenPipe"; stage = "OpenPlantEntity";
if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false; var entity = transaction.GetObject(objectId, OpenMode.ForRead);
stage = "TransformStartPoint"; Autodesk.AutoCAD.Geometry.Point3d modelPoint;
var point = pipe.StartPoint.TransformBy(sourceId.Xform); Autodesk.AutoCAD.Geometry.Vector3d? modelDirection = null;
if (entity is Pipe pipe) modelPoint = pipe.StartPoint;
else if (entity is Connector connector)
{
var ports = connector.GetPorts(PortType.Both);
if (ports.Count != 2) return false;
modelPoint = ports[1].Position;
modelDirection = ports[0].Direction;
}
else return false;
stage = "TransformPlacement";
var point = modelPoint.TransformBy(sourceId.Xform);
var direction = modelDirection?.TransformBy(sourceId.Xform);
geometry = new GeometrySnapshot geometry = new GeometrySnapshot
{ {
PaperPosition = new Point2Dto(point.X, point.Y), PaperPosition = new Point2Dto(point.X, point.Y),
Rotation = 0 Rotation = 0,
PaperDirection = direction is null ? null : new Vector2Dto(direction.Value.X, direction.Value.Y)
}; };
return true; return true;
} }
@@ -17,6 +17,8 @@ public sealed class LineConnectionPreviewServiceTests
Assert.True(result.Success); Assert.True(result.Success);
Assert.Equal(["a", "b"], result.Plans.Select(plan => plan.StableId)); Assert.Equal(["a", "b"], result.Plans.Select(plan => plan.StableId));
Assert.Equal("L-100 / L-200", result.Plans[1].Text); Assert.Equal("L-100 / L-200", result.Plans[1].Text);
Assert.Equal("L-200", result.Plans[1].FirstText);
Assert.Equal("L-100", result.Plans[1].SecondText);
Assert.Empty(result.Plans[1].Reasons); Assert.Empty(result.Plans[1].Reasons);
} }
@@ -0,0 +1,23 @@
using PRecAnnot.Core;
namespace PRecAnnot.Core.Tests;
public sealed class LineConnectionSettingsLoaderTests
{
[Fact]
public void LoadsLegacyLineConnectionAssetContract()
{
var assets = new LineConnectionSettingsLoader().Load("<Main><LinenumberConnections polylinelayer='Осевые' textlayer='ТЕКСТ' textstyle='GOSTC'/></Main>");
Assert.Equal("Осевые", assets.PolylineLayer);
Assert.Equal("ТЕКСТ", assets.TextLayer);
Assert.Equal("GOSTC", assets.TextStyle);
Assert.Equal("BLOCK_LINENUMBER_CONNECTION", assets.BlockName);
}
[Fact]
public void RejectsMissingProductionSetting()
{
var exception = Assert.Throws<InvalidOperationException>(() => new LineConnectionSettingsLoader().Load("<Main><LinenumberConnections polylinelayer='Осевые'/></Main>"));
Assert.Equal("LINE_CONNECTION_SETTING_MISSING:textlayer", exception.Message);
}
}