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); } // Legacy PXP-LN materialises its selected marker/text pairs as a paper-space // LN_LIST block. The list ownership key makes repeat Apply update that // aggregate rather than append another list. public LineNumberApplyResult ApplyList(Document document, LineNumberListApplyPreflight plan, AnnotateLineNumbersAssets assets) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(plan); if (plan.Rows.Count == 0) return new(0, 0, 1); if (plan.Rows.Select(row => row.MarkerNumber).Distinct().Count() != plan.Rows.Count) throw new InvalidOperationException("LINE_NUMBER_LIST_DUPLICATE_MARKER_NUMBER"); using var documentLock = document.LockDocument(); var database = document.Database; using var transaction = database.TransactionManager.StartTransaction(); var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead); ValidateListAssets(transaction, database, assets.List); EnsureRegApp(transaction, database); var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite); var existing = FindOwnedMarker(transaction, paper, plan.StableId); if (existing is not null) { var changed = UpdateList(transaction, existing, plan); transaction.Commit(); return new(0, changed ? 1 : 0, changed ? 0 : 1); } var definition = CreateListDefinition(transaction, database, blocks, plan, assets.List); using var reference = new BlockReference(new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z), definition) { Rotation = plan.Rotation, Layer = assets.List.Layer, XData = ListOwnership(plan.StableId) }; paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true); AppendListAttributes(transaction, definition, reference, plan.Rows); transaction.Commit(); return new(1, 0, 0); } public LineNumberApplyResult ApplyEdits(Document document, IReadOnlyList mutations) { if (mutations.Count == 0) return new(0, 0, 1); 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 paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead); var updated = 0; foreach (var mutation in mutations) { var marker = FindOwnedMarker(transaction, paper, mutation.SourceStableId) ?? throw new InvalidOperationException("LINE_NUMBER_OWNED_MARKER_NOT_FOUND"); foreach (ObjectId id in marker.AttributeCollection) { var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite); if (!string.Equals(attribute.Tag, MarkerAttributeTag, StringComparison.OrdinalIgnoreCase)) continue; attribute.TextString = mutation.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture); if (mutation.MarkerAttributePosition is { } reset) attribute.Position = new Point3d(reset.X, reset.Y, reset.Z).TransformBy(marker.BlockTransform); } foreach (ObjectId entityId in paper) { if (transaction.GetObject(entityId, OpenMode.ForRead) is not BlockReference list || !list.Name.StartsWith("LN_LIST", StringComparison.OrdinalIgnoreCase)) continue; foreach (ObjectId attributeId in list.AttributeCollection) { var attribute = (AttributeReference)transaction.GetObject(attributeId, OpenMode.ForWrite); if (string.Equals(attribute.Tag, $"NUMBER{mutation.PreviousMarkerNumber}", StringComparison.OrdinalIgnoreCase)) { attribute.Tag = $"NUMBER{mutation.MarkerNumber}"; attribute.TextString = mutation.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture); } else if (string.Equals(attribute.Tag, $"LINENUMBER{mutation.PreviousMarkerNumber}", StringComparison.OrdinalIgnoreCase)) { attribute.Tag = $"LINENUMBER{mutation.MarkerNumber}"; attribute.TextString = mutation.Text.Replace("\n", "\\P", StringComparison.Ordinal); attribute.UpdateMTextAttribute(); } } } updated++; } transaction.Commit(); return new(0, updated, 0); } 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 void ValidateListAssets(Transaction transaction, Database database, LineNumberListAsset list) { var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead); var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead); foreach (var layer in new[] { list.Layer, list.MarkerText.Layer, list.LineNumberText.Layer, list.Line.Layer }.Distinct(StringComparer.OrdinalIgnoreCase)) if (!layers.Has(layer)) throw new InvalidOperationException($"LINE_NUMBER_LAYER_MISSING:{layer}"); foreach (var style in new[] { list.MarkerText.TextStyle, list.LineNumberText.TextStyle }.Distinct(StringComparer.OrdinalIgnoreCase)) if (!styles.Has(style)) throw new InvalidOperationException($"LINE_NUMBER_TEXT_STYLE_MISSING:{style}"); if (list.LineHeight <= 0) throw new InvalidOperationException("LINE_NUMBER_LIST_HEIGHT_INVALID"); } private static ObjectId CreateListDefinition(Transaction transaction, Database database, BlockTable blocks, LineNumberListApplyPreflight plan, LineNumberListAsset list) { blocks.UpgradeOpen(); var baseName = $"LN_LIST{plan.Rows.Count}"; var name = baseName; for (var suffix = 1; blocks.Has(name); suffix++) name = $"{baseName}_{suffix}"; using var definition = new BlockTableRecord { Name = name, Origin = Point3d.Origin }; var id = 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); for (var index = 0; index < plan.Rows.Count; index++) { var row = plan.Rows[index]; AppendListAttribute(transaction, definition, row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture), $"NUMBER{row.MarkerNumber}", "Номер маркера:", list.MarkerText, index, list.LineHeight, list.MaskScaleFactor, layers, styles); AppendListAttribute(transaction, definition, row.Text.Replace("\n", "(|)", StringComparison.Ordinal), $"LINENUMBER{row.MarkerNumber}", "Номер линии:", list.LineNumberText, index, list.LineHeight, list.MaskScaleFactor, layers, styles); using var horizontal = new Line( Shift(list.Line.Start, index, list.LineHeight), Shift(list.Line.End, index, list.LineHeight)) { LayerId = layers[list.Line.Layer] }; definition.AppendEntity(horizontal); transaction.AddNewlyCreatedDBObject(horizontal, true); } using var vertical = new Line( new Point3d(list.Line.Start.X, list.Line.Start.Y + list.LineHeight, 0), Shift(list.Line.Start, plan.Rows.Count - 1, list.LineHeight)) { LayerId = layers[list.Line.Layer] }; definition.AppendEntity(vertical); transaction.AddNewlyCreatedDBObject(vertical, true); return id; } private static void AppendListAttribute(Transaction transaction, BlockTableRecord definition, string text, string tag, string prompt, LineNumberTextAsset asset, int row, double lineHeight, double maskScale, LayerTable layers, TextStyleTable styles) { var position = new Point3d(asset.Position.X, asset.Position.Y - lineHeight * row, 0); using var attribute = new AttributeDefinition(position, text, tag, prompt, styles[asset.TextStyle]) { Height = asset.Size, IsMTextAttributeDefinition = true, LayerId = layers[asset.Layer] }; var mtext = attribute.MTextAttributeDefinition; mtext.BackgroundFill = true; mtext.BackgroundScaleFactor = maskScale; mtext.UseBackgroundColor = true; attribute.MTextAttributeDefinition = mtext; definition.AppendEntity(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); } private static Point3d Shift(Point2Dto source, int row, double height) => new(source.X, source.Y - height * row, 0); private static void AppendListAttributes(Transaction transaction, ObjectId definitionId, BlockReference reference, IReadOnlyList rows) { var values = rows.SelectMany(row => new[] { new KeyValuePair($"NUMBER{row.MarkerNumber}", row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture)), new KeyValuePair($"LINENUMBER{row.MarkerNumber}", row.Text.Replace("\n", "\\P", StringComparison.Ordinal)) }).ToDictionary(value => value.Key, value => value.Value, StringComparer.OrdinalIgnoreCase); 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 = values[source.Tag]; attribute.UpdateMTextAttribute(); var row = rows.Single(value => source.Tag.EndsWith(value.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal)); attribute.XData = new ResultBuffer( new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, row.StableId), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source.Tag.StartsWith("NUMBER", StringComparison.OrdinalIgnoreCase) ? "MARKER" : "LINENUMBER"), new TypedValue((int)DxfCode.ExtendedDataInteger32, row.MarkerNumber)); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); } } private static bool UpdateList(Transaction transaction, BlockReference reference, LineNumberListApplyPreflight plan) { var position = new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z); var changed = reference.Position != position || reference.Rotation != plan.Rotation; reference.UpgradeOpen(); reference.Position = position; reference.Rotation = plan.Rotation; var values = plan.Rows.SelectMany(row => new[] { new KeyValuePair($"NUMBER{row.MarkerNumber}", row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture)), new KeyValuePair($"LINENUMBER{row.MarkerNumber}", row.Text.Replace("\n", "\\P", StringComparison.Ordinal)) }).ToDictionary(value => value.Key, value => value.Value, StringComparer.OrdinalIgnoreCase); foreach (ObjectId id in reference.AttributeCollection) { var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite); if (!values.TryGetValue(attribute.Tag, out var value)) continue; changed |= !string.Equals(attribute.TextString, value, StringComparison.Ordinal); attribute.TextString = value; attribute.UpdateMTextAttribute(); } return changed; } 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 ResultBuffer ListOwnership(string stableId) => new( new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, stableId), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LIST")); 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); } }