diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 62e7bab..e2a741c 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -83,7 +83,9 @@ public sealed record LineConnectionBoundaryCandidate( double Rotation, string? FirstLineGroupStableId = null, string? SecondLineGroupStableId = null); -public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList Reasons); +public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList 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. // Keeping it separate from the PnP relation snapshot prevents Autodesk objects // from crossing into the deterministic PXP-ALC planning layer. @@ -138,6 +140,7 @@ public sealed record GeometrySnapshot public Bounds2Dto? ProjectedBounds { get; init; } public double? Rotation { get; init; } public double? Elevation { get; init; } + public Vector2Dto? PaperDirection { get; init; } } // Platform-neutral result of the legacy GetComponentGeometry contract. The diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs index 7de3d63..112190a 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionBoundaryPlanner.cs @@ -41,7 +41,7 @@ public sealed class LineConnectionBoundaryPlanner continue; } 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())); + result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty(), first, second)); } return result; } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionSettingsLoader.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionSettingsLoader.cs new file mode 100644 index 0000000..3f0166f --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineConnectionSettingsLoader.cs @@ -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); } + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs index feedf6d..2c2b10f 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPoint.cs @@ -44,6 +44,7 @@ public sealed class EntryPoint : IExtensionApplication [CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber(); [CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText(); [CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition(); + [CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run(); [CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)] public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm(); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs new file mode 100644 index 0000000..b365ee4 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionAnnotationWriter.cs @@ -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 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); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionApplyPermit.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionApplyPermit.cs new file mode 100644 index 0000000..b7b2b22 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionApplyPermit.cs @@ -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 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(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 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"); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionCommand.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionCommand.cs new file mode 100644 index 0000000..dce2246 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineConnectionCommand.cs @@ -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(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 { ["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 { ["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; +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRuntimeConfiguration.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRuntimeConfiguration.cs index ad0cbd5..0e4de93 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRuntimeConfiguration.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberRuntimeConfiguration.cs @@ -3,7 +3,7 @@ using PRecAnnot.Core; 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 { @@ -11,7 +11,8 @@ internal static class LineNumberRuntimeConfigurationReader { var annotationPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_ANNOTATION_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) diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs index b140aad..ebef83e 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PlantReadPath.cs @@ -294,6 +294,7 @@ internal sealed class PlantObjectReader : IPlantObjectReader var properties = dataLinks.GetAllProperties(rowId, true) .GroupBy(x => x.Key, 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; snapshots.Add(new PlantObjectSnapshot { @@ -447,7 +448,7 @@ internal sealed class PlantViewportMembershipReader if (sourceRows.Contains(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) @@ -476,7 +477,7 @@ internal sealed class PlantViewportMembershipReader // whose PrjObjectId identifies the source entity and whose Xform projects // that source entity into the particular Ortho view. This is not a sheet // 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(); var stage = "GetDatabase"; @@ -492,14 +493,27 @@ internal sealed class PlantViewportMembershipReader using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction(); stage = "MakeAcDbObjectId"; var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase); - stage = "OpenPipe"; - if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false; - stage = "TransformStartPoint"; - var point = pipe.StartPoint.TransformBy(sourceId.Xform); + stage = "OpenPlantEntity"; + var entity = transaction.GetObject(objectId, OpenMode.ForRead); + Autodesk.AutoCAD.Geometry.Point3d modelPoint; + 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 { 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; } diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionPreviewServiceTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionPreviewServiceTests.cs index cc9fcdc..c57755f 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionPreviewServiceTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionPreviewServiceTests.cs @@ -17,6 +17,8 @@ public sealed class LineConnectionPreviewServiceTests Assert.True(result.Success); Assert.Equal(["a", "b"], result.Plans.Select(plan => plan.StableId)); 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); } diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionSettingsLoaderTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionSettingsLoaderTests.cs new file mode 100644 index 0000000..fc11105 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineConnectionSettingsLoaderTests.cs @@ -0,0 +1,23 @@ +using PRecAnnot.Core; + +namespace PRecAnnot.Core.Tests; + +public sealed class LineConnectionSettingsLoaderTests +{ + [Fact] + public void LoadsLegacyLineConnectionAssetContract() + { + var assets = new LineConnectionSettingsLoader().Load("
"); + 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(() => new LineConnectionSettingsLoader().Load("
")); + Assert.Equal("LINE_CONNECTION_SETTING_MISSING:textlayer", exception.Message); + } +}