feat: expand p-rec-annot ortho annotation workflows
This commit is contained in:
@@ -155,6 +155,10 @@ public sealed record ComponentGeometryDefinition(
|
||||
bool ImperialDimensions);
|
||||
public sealed record ComponentGeometryReadResult(bool Success, ComponentGeometryDefinition? Geometry, string? FailureCode = null);
|
||||
public sealed record ViewportLayerPlan(bool RequiresChange, string TargetLayer, string? FailureCode = null);
|
||||
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 interface IPlantComponentGeometryReader
|
||||
{
|
||||
ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class OrthoAnnotationClassifier
|
||||
{
|
||||
public OrthoAnnotationClassification Classify(PlantObjectSnapshot source, OrthoAnnotationOptions options)
|
||||
{
|
||||
string Property(params string[] names) => source.Properties.FirstOrDefault(x => names.Any(n => string.Equals(n, x.Key, StringComparison.OrdinalIgnoreCase))).Value?.Trim() ?? string.Empty;
|
||||
var className = source.ObjectClass.Trim();
|
||||
if (className.Equals("Support", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var title = Property("Title"); var text = Normalize(Property("Annotation"));
|
||||
return options.Supports && title.StartsWith("НЕСТАНДАРТНЫЕ", StringComparison.OrdinalIgnoreCase) && text.Length > 0
|
||||
? Ready(source, OrthoAnnotationRepresentation.MultiLeader, text)
|
||||
: Skip(source, "SUPPORT_NOT_NKR_OR_DISABLED");
|
||||
}
|
||||
if (className.Equals("Valve", StringComparison.OrdinalIgnoreCase) || className.Equals("InlineInstrument", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = Normalize(className.Equals("Valve", StringComparison.OrdinalIgnoreCase) ? Property("Annotation") : Property("Tag"));
|
||||
if (value.Contains('$')) return options.Instruments ? Ready(source, OrthoAnnotationRepresentation.BubbleBlock, value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "INSTRUMENT_ANNOTATION_DISABLED");
|
||||
return HasAlphaNumeric(value) && options.Valves ? Ready(source, OrthoAnnotationRepresentation.MultiLeader, value) : Skip(source, "VALVE_ANNOTATION_EMPTY_OR_DISABLED");
|
||||
}
|
||||
if (className.Equals("Plug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = Normalize(Property("TieInNumber"));
|
||||
if (!options.Plugs || !HasAlphaNumeric(value)) return Skip(source, "PLUG_ANNOTATION_EMPTY_OR_DISABLED");
|
||||
var representation = value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader;
|
||||
return Ready(source, representation, "Для подключения\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal));
|
||||
}
|
||||
if (className.Equals("Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = Normalize(Property("Tag", "Annotation"));
|
||||
return options.Boards && value.Length > 0 ? Ready(source, value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader, "Шкаф КИП для прибора\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "BOARD_ANNOTATION_EMPTY_OR_DISABLED");
|
||||
}
|
||||
return Skip(source, "ORTHO_ANNOTATION_CLASS_UNSUPPORTED");
|
||||
}
|
||||
|
||||
private static bool HasAlphaNumeric(string value) => Regex.IsMatch(value, "[a-zA-Z0-9]");
|
||||
private static string Normalize(string value) => value.Replace("{nl}", "\\P", StringComparison.Ordinal);
|
||||
private static OrthoAnnotationClassification Ready(PlantObjectSnapshot source, OrthoAnnotationRepresentation representation, string text) => new(source.SnapshotId, representation, text, Array.Empty<string>());
|
||||
private static OrthoAnnotationClassification Skip(PlantObjectSnapshot source, string reason) => new(source.SnapshotId, OrthoAnnotationRepresentation.None, string.Empty, [reason]);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Xml.Linq;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class OrthoAnnotationSettingsLoader
|
||||
{
|
||||
public OrthoAnnotationAssets Load(string xml)
|
||||
{
|
||||
var value = XElement.Parse(xml).Element("Annotations") ?? throw Invalid("Annotations");
|
||||
return new(Required(value, "layer"), Required(value, "textstyle"), Required(value, "multileaderstyle"));
|
||||
}
|
||||
private static string Required(XElement value, string name) => value.Attribute(name) is { Value: var result } && !string.IsNullOrWhiteSpace(result) ? result : throw Invalid(name);
|
||||
private static InvalidDataException Invalid(string value) => new("ORTHO_ANNOTATION_SETTINGS_INVALID:" + value);
|
||||
}
|
||||
@@ -27,6 +27,8 @@ internal static class AutoAnnotateViewCommand
|
||||
viewport = matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "AUTO_ANNOTATE_VIEW_NOT_FOUND" : "AUTO_ANNOTATE_VIEW_AMBIGUOUS");
|
||||
}
|
||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||
var classifications = objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal)
|
||||
.Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray();
|
||||
var permit = string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)
|
||||
? FirstLimitedApplyPermitReader.ReadFor(document) : null;
|
||||
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||
@@ -35,9 +37,24 @@ internal static class AutoAnnotateViewCommand
|
||||
LimitedApplyTargetSnapshotId = permit?.TargetSnapshotId,
|
||||
LimitedApplyRuleId = permit is null ? null : FirstLimitedApplyPlanner.RuleId
|
||||
});
|
||||
document.Editor.WriteMessage($"\nPXP-AAV preview: examined={plan.Summary.ObjectsExamined}; create={plan.Summary.CreateCount}; update={plan.Summary.UpdateCount}; skip={plan.Summary.SkipCount}; conflicts={plan.Summary.ConflictCount}; PlanId={plan.PlanId}. DWG не изменён.");
|
||||
RuntimeDiagnostics.Write("AutoAnnotateViewPreviewCompleted", commandName: "PXP-AAV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId, ["ObjectsExamined"] = plan.Summary.ObjectsExamined });
|
||||
var leaders = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader);
|
||||
var bubbles = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock);
|
||||
document.Editor.WriteMessage($"\nPXP-AAV preview: examined={plan.Summary.ObjectsExamined}; create={plan.Summary.CreateCount}; update={plan.Summary.UpdateCount}; leaders={leaders}; bubbles={bubbles}; skip={plan.Summary.SkipCount}; conflicts={plan.Summary.ConflictCount}; PlanId={plan.PlanId}. DWG не изменён.");
|
||||
RuntimeDiagnostics.Write("AutoAnnotateViewPreviewCompleted", commandName: "PXP-AAV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId, ["ObjectsExamined"] = plan.Summary.ObjectsExamined, ["MultiLeaderCandidates"] = leaders, ["BubbleCandidates"] = bubbles });
|
||||
if (permit is null) return;
|
||||
if (!string.Equals(permit.ExpectedPlanId, plan.PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("APPLY_PLAN_ID_MISMATCH");
|
||||
var selectedSource = objects.SingleOrDefault(x => string.Equals(x.SnapshotId, permit.TargetSnapshotId, StringComparison.Ordinal)) ?? throw new InvalidOperationException("APPLY_TARGET_NOT_FOUND");
|
||||
var selectedClassification = classifications.Single(x => string.Equals(x.StableId, permit.TargetSnapshotId, StringComparison.Ordinal));
|
||||
if (selectedClassification.Representation != OrthoAnnotationRepresentation.None)
|
||||
{
|
||||
var settingsPath = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||
if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath)) throw new InvalidOperationException("ORTHO_ANNOTATION_CONFIGURATION_REQUIRED");
|
||||
var assets = new OrthoAnnotationSettingsLoader().Load(File.ReadAllText(settingsPath));
|
||||
var result = new OrthoAnnotationWriter().Replace(document, viewport, selectedSource, selectedClassification, assets);
|
||||
document.Editor.WriteMessage("\nPXP-AAV Apply: " + result + "; AutoCAD UNDO is available.");
|
||||
RuntimeDiagnostics.Write("AutoAnnotateViewApplyCompleted", commandName: "PXP-AAV", resultStatus: result, document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId, ["Representation"] = selectedClassification.Representation.ToString() });
|
||||
return;
|
||||
}
|
||||
var corePermit = new FirstLimitedApplyPermit { FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker, AllowApply = permit.AllowApply, TargetSnapshotId = permit.TargetSnapshotId, ExpectedPlanId = permit.ExpectedPlanId };
|
||||
var preflight = new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, plan, viewport, objects, corePermit);
|
||||
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document, preflight.Operation, permit.VisibilityState, permit.AnnotationTemplateDwgPath);
|
||||
|
||||
@@ -228,6 +228,8 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
var membershipPartial = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.PartiallyResolved);
|
||||
var membershipUnresolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.NotResolved);
|
||||
var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||
var orthoClassifications = objectsByViewport.SelectMany(x => x).OrderBy(x => x.SnapshotId, StringComparer.Ordinal)
|
||||
.Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray();
|
||||
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||
@@ -236,6 +238,8 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
details["MembershipPartial"] = membershipPartial;
|
||||
details["MembershipUnresolved"] = membershipUnresolved;
|
||||
details["MembershipOutsideView"] = membershipOutside;
|
||||
details["MultiLeaderCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader);
|
||||
details["BubbleCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock);
|
||||
details["PlanCreateCount"] = summary.CreateCount;
|
||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||
details["PlanSkipCount"] = summary.SkipCount;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.AutoCAD.Geometry;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
internal sealed class OrthoAnnotationWriter
|
||||
{
|
||||
private const string ApplicationName = "PXP-OA";
|
||||
public string Replace(Document document, OrthoViewportInfo viewport, PlantObjectSnapshot source, OrthoAnnotationClassification classification, OrthoAnnotationAssets assets)
|
||||
{
|
||||
if (source.Geometry.PaperPosition is not { } point || classification.Representation == OrthoAnnotationRepresentation.None) throw new InvalidOperationException("ORTHO_ANNOTATION_OPERATION_INVALID");
|
||||
using var documentLock = document.LockDocument(); using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||
var styles = (TextStyleTable)transaction.GetObject(document.Database.TextStyleTableId, OpenMode.ForRead);
|
||||
var leaders = (DBDictionary)transaction.GetObject(document.Database.MLeaderStyleDictionaryId, OpenMode.ForRead);
|
||||
if (!layers.Has(assets.Layer)) throw new InvalidOperationException("ORTHO_ANNOTATION_LAYER_MISSING:" + assets.Layer);
|
||||
if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_TEXT_STYLE_MISSING:" + assets.TextStyle);
|
||||
if (!leaders.Contains(assets.MultiLeaderStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_MLEADER_STYLE_MISSING:" + assets.MultiLeaderStyle);
|
||||
if (classification.Representation == OrthoAnnotationRepresentation.BubbleBlock && !blocks.Has(assets.BubbleBlock)) throw new InvalidOperationException("ORTHO_ANNOTATION_BUBBLE_BLOCK_MISSING:" + assets.BubbleBlock);
|
||||
EnsureRegApp(transaction, document.Database);
|
||||
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||
var deleted = 0;
|
||||
foreach (ObjectId id in paper)
|
||||
{
|
||||
var entity = transaction.GetObject(id, OpenMode.ForRead) as Entity; if (entity is null || !OwnedBy(entity, source.SnapshotId)) continue;
|
||||
entity.UpgradeOpen(); entity.Erase(); deleted++;
|
||||
}
|
||||
var start = new Point3d(point.X, point.Y, 0); var right = point.X >= viewport.CenterX;
|
||||
var land = new Point3d(viewport.CenterX + (right ? viewport.Width / 2d + 5d : -viewport.Width / 2d - 5d), point.Y, 0);
|
||||
if (classification.Representation == OrthoAnnotationRepresentation.MultiLeader)
|
||||
{
|
||||
using var leader = new MLeader { LayerId = layers[assets.Layer], MLeaderStyle = leaders.GetAt(assets.MultiLeaderStyle), ContentType = ContentType.MTextContent, LeaderLineType = LeaderType.StraightLeader };
|
||||
var leaderId = leader.AddLeader(); var lineId = leader.AddLeaderLine(leaderId); leader.AddFirstVertex(lineId, start); leader.AddLastVertex(lineId, land);
|
||||
using var text = new MText { Contents = classification.Text, TextStyleId = styles[assets.TextStyle], Location = land, BackgroundFill = true, UseBackgroundColor = true, BackgroundScaleFactor = 1.1 };
|
||||
leader.MText = text; leader.TextLocation = land; leader.XData = Ownership(viewport.Id, source.SnapshotId, classification);
|
||||
paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BubbleBlock], OpenMode.ForRead);
|
||||
using var reference = new BlockReference(start, definition.ObjectId) { LayerId = layers[assets.Layer], XData = Ownership(viewport.Id, source.SnapshotId, classification) };
|
||||
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
|
||||
foreach (ObjectId id in definition)
|
||||
{
|
||||
if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition definitionAttribute || definitionAttribute.Constant) continue;
|
||||
using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(definitionAttribute, reference.BlockTransform); attribute.TextString = classification.Text; attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||
}
|
||||
if (reference.IsDynamicBlock) foreach (DynamicBlockReferenceProperty property in reference.DynamicBlockReferencePropertyCollection)
|
||||
{
|
||||
if (property.ReadOnly) continue;
|
||||
if (string.Equals(property.PropertyName, "P1 X", StringComparison.OrdinalIgnoreCase)) property.Value = land.X - start.X;
|
||||
else if (string.Equals(property.PropertyName, "P1 Y", StringComparison.OrdinalIgnoreCase)) property.Value = land.Y - start.Y;
|
||||
}
|
||||
}
|
||||
transaction.Commit(); return deleted == 0 ? "Created" : "Updated";
|
||||
}
|
||||
private static bool OwnedBy(Entity entity, string source) => entity.GetXDataForApplication(ApplicationName)?.AsArray().Any(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(x.Value?.ToString(), source, StringComparison.Ordinal)) == true;
|
||||
private static ResultBuffer Ownership(string viewport, string source, OrthoAnnotationClassification value) => new(new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "VIEWPORT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "ANNOTATIONTEXT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Text), new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Representation.ToString()));
|
||||
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,23 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
using PRecAnnot.Core;
|
||||
|
||||
namespace PRecAnnot.Core.Tests;
|
||||
|
||||
public sealed class OrthoAnnotationClassifierTests
|
||||
{
|
||||
private static readonly OrthoAnnotationOptions All = new(true, true, true, true, true);
|
||||
[Theory]
|
||||
[InlineData("Valve", "Annotation", "V-1", OrthoAnnotationRepresentation.MultiLeader, "V-1")]
|
||||
[InlineData("Valve", "Annotation", "PIC$101", OrthoAnnotationRepresentation.BubbleBlock, "PIC\\P101")]
|
||||
[InlineData("InlineInstrument", "Tag", "TI$12", OrthoAnnotationRepresentation.BubbleBlock, "TI\\P12")]
|
||||
[InlineData("Plug", "TieInNumber", "T-4", OrthoAnnotationRepresentation.MultiLeader, "Для подключения\\Pпоз. T-4")]
|
||||
public void PreservesLegacyClassAndDelimiterRules(string type, string field, string value, OrthoAnnotationRepresentation representation, string text)
|
||||
{
|
||||
var result = new OrthoAnnotationClassifier().Classify(Source(type, new Dictionary<string, string?> { [field] = value }), All);
|
||||
Assert.Equal(representation, result.Representation); Assert.Equal(text, result.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresNonstandardSupportTitle() => Assert.Equal(OrthoAnnotationRepresentation.None, new OrthoAnnotationClassifier().Classify(Source("Support", new Dictionary<string, string?> { ["Title"] = "STANDARD", ["Annotation"] = "S1" }), All).Representation);
|
||||
private static PlantObjectSnapshot Source(string type, IReadOnlyDictionary<string, string?> properties) => new() { SnapshotId = "x", ObjectClass = type, Geometry = new(), Properties = properties };
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
using PRecAnnot.Core;
|
||||
namespace PRecAnnot.Core.Tests;
|
||||
public sealed class OrthoAnnotationSettingsLoaderTests
|
||||
{
|
||||
[Fact] public void ReadsLegacyAnnotationAssets() { var value = new OrthoAnnotationSettingsLoader().Load("<Main><Annotations layer='A' textstyle='T' multileaderstyle='M'/></Main>"); Assert.Equal("A", value.Layer); Assert.Equal("M", value.MultiLeaderStyle); }
|
||||
[Fact] public void RejectsMissingStyle() => Assert.Throws<InvalidDataException>(() => new OrthoAnnotationSettingsLoader().Load("<Main><Annotations layer='A' textstyle='T'/></Main>"));
|
||||
}
|
||||
Reference in New Issue
Block a user