diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 2b2cc4f..016b1aa 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -59,7 +59,8 @@ public sealed record LineNumberCandidate(string StableId, long RowId, Annotation public sealed record ExistingLineNumberAnnotation(string StableId, string SourceStableId, string Text); public sealed record LineNumberCommandOperation(string StableId, AnnotationOperationKind Kind, string? Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyList Reasons); public sealed record LineNumberCommandPlan(IReadOnlyList Operations); -public sealed record LineNumberApplyPreflight(string StableId, string Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle); +public sealed record LineNumberApplyPreflight(string StableId, int MarkerNumber, string Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle); +public sealed record LineNumberApplyResult(int Created, int Updated, int Unchanged); public sealed record LineNumberTextAsset(string Layer, string TextStyle, double Size, string TextValue, Point2Dto Position); public sealed record LineNumberLineAsset(string Layer, Point2Dto Start, Point2Dto End); public sealed record LineNumberMarkerAsset(string Layer, double MaskScaleFactor, IReadOnlyList Texts, IReadOnlyList Lines); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs index c899658..ec4e796 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/LineNumberCommandPlanner.cs @@ -21,5 +21,5 @@ public sealed class LineNumberCommandPlanner(ILineNumberAnnotationFormatter form public static IReadOnlyList PrepareApply(LineNumberCommandPlan plan) => plan.Operations .Where(x => x.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update) .OrderBy(x => x.StableId, StringComparer.Ordinal) - .Select(x => new LineNumberApplyPreflight(x.StableId, x.Text!, x.Position, x.Rotation, x.Layer, x.TextStyle)).ToArray(); + .Select((x, index) => new LineNumberApplyPreflight(x.StableId, index + 1, x.Text!, x.Position, x.Rotation, x.Layer, x.TextStyle)).ToArray(); } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberAnnotationWriter.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberAnnotationWriter.cs new file mode 100644 index 0000000..c640554 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/LineNumberAnnotationWriter.cs @@ -0,0 +1,172 @@ +using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Geometry; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Plant3D2026; + +internal sealed class LineNumberAnnotationWriter +{ + private const string ApplicationName = "PXP-LN"; + private const string MarkerBlockName = "LN_MARKER"; + private const string MarkerAttributeTag = "NUMBER"; + + public LineNumberApplyResult Apply(Document document, IReadOnlyList plan, AnnotateLineNumbersAssets assets) + { + ArgumentNullException.ThrowIfNull(document); + ArgumentNullException.ThrowIfNull(plan); + ArgumentNullException.ThrowIfNull(assets); + if (plan.Select(item => item.StableId).Distinct(StringComparer.Ordinal).Count() != plan.Count) + throw new InvalidOperationException("LINE_NUMBER_APPLY_DUPLICATE_STABLE_ID"); + + var created = 0; + var updated = 0; + var unchanged = 0; + var database = document.Database; + using var documentLock = document.LockDocument(); + using var transaction = database.TransactionManager.StartTransaction(); + var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead); + ValidateAssets(transaction, database, assets.Marker); + EnsureRegApp(transaction, database); + var markerDefinition = EnsureMarkerDefinition(transaction, database, blocks, assets.Marker); + var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite); + + foreach (var item in plan.OrderBy(value => value.MarkerNumber)) + { + var existing = FindOwnedMarker(transaction, paper, item.StableId); + if (existing is null) + { + CreateMarker(transaction, paper, markerDefinition, item, assets.Marker); + created++; + } + else if (UpdateMarker(transaction, existing, item, assets.Marker.Layer)) updated++; + else unchanged++; + } + + transaction.Commit(); + return new(created, updated, unchanged); + } + + private static void ValidateAssets(Transaction transaction, Database database, LineNumberMarkerAsset marker) + { + var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead); + var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead); + foreach (var layer in marker.Texts.Select(value => value.Layer).Append(marker.Layer).Concat(marker.Lines.Select(value => value.Layer)).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase)) + if (!layers.Has(layer)) throw new InvalidOperationException($"LINE_NUMBER_LAYER_MISSING:{layer}"); + foreach (var style in marker.Texts.Select(value => value.TextStyle).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase)) + if (!styles.Has(style)) throw new InvalidOperationException($"LINE_NUMBER_TEXT_STYLE_MISSING:{style}"); + if (marker.Texts.Count == 0) throw new InvalidOperationException("LINE_NUMBER_MARKER_TEXT_ASSET_MISSING"); + } + + private static ObjectId EnsureMarkerDefinition(Transaction transaction, Database database, BlockTable blocks, LineNumberMarkerAsset marker) + { + if (blocks.Has(MarkerBlockName)) return blocks[MarkerBlockName]; + blocks.UpgradeOpen(); + using var definition = new BlockTableRecord { Name = MarkerBlockName, Origin = Point3d.Origin }; + var definitionId = blocks.Add(definition); + transaction.AddNewlyCreatedDBObject(definition, true); + var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead); + var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead); + + foreach (var text in marker.Texts) + { + using var attribute = new AttributeDefinition( + new Point3d(text.Position.X, text.Position.Y, 0), text.TextValue, MarkerAttributeTag, "Номер маркера:", styles[text.TextStyle]) + { + Height = text.Size, + IsMTextAttributeDefinition = true, + LayerId = layers[text.Layer] + }; + definition.AppendEntity(attribute); + transaction.AddNewlyCreatedDBObject(attribute, true); + } + foreach (var source in marker.Lines) + { + using var line = new Line(new Point3d(source.Start.X, source.Start.Y, 0), new Point3d(source.End.X, source.End.Y, 0)) { LayerId = layers[source.Layer] }; + definition.AppendEntity(line); + transaction.AddNewlyCreatedDBObject(line, true); + } + return definitionId; + } + + private static void CreateMarker(Transaction transaction, BlockTableRecord paper, ObjectId definitionId, LineNumberApplyPreflight item, LineNumberMarkerAsset marker) + { + using var reference = new BlockReference(new Point3d(item.Position.X, item.Position.Y, item.Position.Z), definitionId) + { + Rotation = item.Rotation, + Layer = marker.Layer + }; + paper.AppendEntity(reference); + transaction.AddNewlyCreatedDBObject(reference, true); + reference.XData = Ownership(item); + + var definition = (BlockTableRecord)transaction.GetObject(definitionId, OpenMode.ForRead); + foreach (ObjectId id in definition) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue; + using var attribute = new AttributeReference(); + attribute.SetAttributeFromBlock(source, reference.BlockTransform); + attribute.TextString = item.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture); + attribute.XData = new ResultBuffer( + new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), + new TypedValue((int)DxfCode.ExtendedDataAsciiString, "MARKER"), + new TypedValue((int)DxfCode.ExtendedDataInteger32, item.MarkerNumber)); + reference.AttributeCollection.AppendAttribute(attribute); + transaction.AddNewlyCreatedDBObject(attribute, true); + } + } + + private static BlockReference? FindOwnedMarker(Transaction transaction, BlockTableRecord paper, string stableId) + { + foreach (ObjectId id in paper) + { + if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue; + using var data = reference.GetXDataForApplication(ApplicationName); + var values = data?.AsArray(); + if (values?.Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, stableId, StringComparison.Ordinal)) == true) + return reference; + } + return null; + } + + private static bool UpdateMarker(Transaction transaction, BlockReference marker, LineNumberApplyPreflight item, string layer) + { + var position = new Point3d(item.Position.X, item.Position.Y, item.Position.Z); + using var existingOwnership = marker.GetXDataForApplication(ApplicationName); + var ownership = existingOwnership?.AsArray() ?? []; + var changed = marker.Position != position || marker.Rotation != item.Rotation || + !string.Equals(marker.Layer, layer, StringComparison.OrdinalIgnoreCase) || + !ownership.Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, item.Text, StringComparison.Ordinal)); + marker.UpgradeOpen(); + marker.Position = position; + marker.Rotation = item.Rotation; + marker.Layer = layer; + marker.XData = Ownership(item); + foreach (ObjectId id in marker.AttributeCollection) + { + var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite); + if (!string.Equals(attribute.Tag, MarkerAttributeTag, StringComparison.OrdinalIgnoreCase)) continue; + var value = item.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture); + changed |= !string.Equals(attribute.TextString, value, StringComparison.Ordinal); + attribute.TextString = value; + } + return changed; + } + + private static ResultBuffer Ownership(LineNumberApplyPreflight item) => new( + new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), + new TypedValue((int)DxfCode.ExtendedDataAsciiString, item.StableId), + new TypedValue((int)DxfCode.ExtendedDataAsciiString, "MARKER"), + new TypedValue((int)DxfCode.ExtendedDataInteger32, item.MarkerNumber), + new TypedValue((int)DxfCode.ExtendedDataAsciiString, item.Text)); + + 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/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs index fd82dbb..efec2f7 100644 --- a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/LineNumberCommandPlannerTests.cs @@ -9,6 +9,8 @@ public sealed class LineNumberCommandPlannerTests var c = new LineNumberCandidate("b", 1, new(0,0), 0, "PXP", "Standard", new Dictionary { ["EngineeringItems.LineNumber"]="L-2" }); var plan = new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()).Build(def, [c, c, c with { StableId="a" }], [new("x", "b", "old")]); Assert.Collection(plan.Operations, x => Assert.Equal(AnnotationOperationKind.Create,x.Kind), x => Assert.Equal(AnnotationOperationKind.Update,x.Kind), x => Assert.Equal(AnnotationOperationKind.Conflict,x.Kind)); - Assert.Equal(new[] { "a", "b" }, LineNumberCommandPlanner.PrepareApply(plan).Select(x => x.StableId)); + var apply = LineNumberCommandPlanner.PrepareApply(plan); + Assert.Equal(new[] { "a", "b" }, apply.Select(x => x.StableId)); + Assert.Equal(new[] { 1, 2 }, apply.Select(x => x.MarkerNumber)); } }