Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1563e2fae0 | ||
|
|
bd5f47d811 | ||
|
|
1bfb91bc4b |
@@ -177,12 +177,40 @@ public sealed record AnnotationPlanningRequest
|
||||
public required OrthoViewportInfo Viewport { get; init; }
|
||||
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
||||
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
||||
// Test-only limited Apply intent. It is part of the canonical plan and is
|
||||
// deliberately explicit: the planner must never choose an arbitrary Pipe.
|
||||
public string? LimitedApplyTargetSnapshotId { get; init; }
|
||||
public string? LimitedApplyRuleId { get; init; }
|
||||
}
|
||||
|
||||
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
||||
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
||||
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
||||
|
||||
public sealed record CreatePipeAnnotationOperation
|
||||
{
|
||||
public required string OperationId { get; init; }
|
||||
public required string SourceObjectStableKey { get; init; }
|
||||
public required string ViewStableKey { get; init; }
|
||||
public required string RuleId { get; init; }
|
||||
public required AnnotationPoint Placement { get; init; }
|
||||
public required string DuplicateKey { get; init; }
|
||||
}
|
||||
|
||||
public sealed record FirstLimitedApplyPermit
|
||||
{
|
||||
public required string FixtureMarker { get; init; }
|
||||
public required bool AllowApply { get; init; }
|
||||
public required string TargetSnapshotId { get; init; }
|
||||
public required string ExpectedPlanId { get; init; }
|
||||
}
|
||||
|
||||
public sealed record FirstLimitedApplyPreflight
|
||||
{
|
||||
public required string PlanId { get; init; }
|
||||
public required CreatePipeAnnotationOperation Operation { get; init; }
|
||||
}
|
||||
|
||||
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
||||
|
||||
public sealed record PlantObjectData
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
{
|
||||
public const int CurrentPlanSchemaVersion = 1;
|
||||
public const int CurrentPlanSchemaVersion = 2;
|
||||
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||
{
|
||||
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||
@@ -31,12 +31,65 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.LimitedApplyTargetSnapshotId) &&
|
||||
!string.Equals(item.SnapshotId, request.LimitedApplyTargetSnapshotId, StringComparison.Ordinal))
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "APPLY_NOT_SELECTED_TARGET" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.MembershipStatus is not ViewMembershipStatus.Resolved and not ViewMembershipStatus.PartiallyResolved)
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { MembershipReason(item.MembershipStatus) } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first Apply slice is intentionally narrower than the
|
||||
// historic bulk rules: exactly one explicitly selected, resolved
|
||||
// Pipe can become a PIPE block operation. All other objects keep
|
||||
// their normal diagnostic treatment.
|
||||
if (!string.IsNullOrWhiteSpace(request.LimitedApplyTargetSnapshotId) &&
|
||||
string.Equals(item.SnapshotId, request.LimitedApplyTargetSnapshotId, StringComparison.Ordinal))
|
||||
{
|
||||
if (!string.Equals(item.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "APPLY_UNSUPPORTED_OBJECT_CLASS" } });
|
||||
continue;
|
||||
}
|
||||
if (item.MembershipStatus != ViewMembershipStatus.Resolved)
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "APPLY_REQUIRES_RESOLVED_MEMBERSHIP" } });
|
||||
continue;
|
||||
}
|
||||
if (item.Geometry.PaperPosition is not { } pipePosition)
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "ANNOTATION_PLACEMENT_NOT_RESOLVED" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
var pipeIntent = new AnnotationIntent
|
||||
{
|
||||
StableKey = $"{request.Viewport.Id}:{item.SnapshotId}:{FirstLimitedApplyPlanner.RuleId}",
|
||||
Text = FirstLimitedApplyPlanner.RuleId,
|
||||
Position = new AnnotationPoint(pipePosition.X, pipePosition.Y),
|
||||
Rotation = item.Geometry.Rotation ?? 0,
|
||||
ClassName = item.ObjectClass,
|
||||
Layer = "PXP_ANNOTATION"
|
||||
};
|
||||
operations.Add(new AnnotationOperationPlan
|
||||
{
|
||||
SourceSnapshotId = item.SnapshotId,
|
||||
Kind = AnnotationOperationKind.Create,
|
||||
AnnotationType = "PIPE",
|
||||
Text = FirstLimitedApplyPlanner.RuleId,
|
||||
Position = pipePosition,
|
||||
Rotation = item.Geometry.Rotation,
|
||||
Reasons = Array.Empty<string>()
|
||||
});
|
||||
creates.Add(pipeIntent);
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
@@ -111,7 +164,9 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
var canonical = new StringBuilder("schema=").Append(CurrentPlanSchemaVersion)
|
||||
.Append("|command=").Append(Normalize(request.CommandName))
|
||||
.Append("|source=").Append(Normalize(Path.GetFileName(request.Viewport.SourceDrawingName)))
|
||||
.Append("|view=").Append(Normalize(request.Viewport.Id));
|
||||
.Append("|view=").Append(Normalize(request.Viewport.Id))
|
||||
.Append("|limited-target=").Append(Normalize(request.LimitedApplyTargetSnapshotId))
|
||||
.Append("|limited-rule=").Append(Normalize(request.LimitedApplyRuleId));
|
||||
foreach (var item in objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal))
|
||||
{
|
||||
canonical.Append("|object=").Append(Normalize(item.SnapshotId)).Append('|').Append(Normalize(item.ObjectClass))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class FirstLimitedApplyPlanner
|
||||
{
|
||||
public const string FixtureMarker = "FIRST_LIMITED_APPLY";
|
||||
public const string RuleId = "PXP-OA.PIPE.OPEN-END.V1";
|
||||
|
||||
public FirstLimitedApplyPreflight Prepare(
|
||||
PRecAnnotExecutionMode mode,
|
||||
AnnotationPlan plan,
|
||||
OrthoViewportInfo viewport,
|
||||
IReadOnlyList<PlantObjectSnapshot> objects,
|
||||
FirstLimitedApplyPermit permit)
|
||||
{
|
||||
if (mode != PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_REQUIRED");
|
||||
if (!permit.AllowApply || !string.Equals(permit.FixtureMarker, FixtureMarker, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||
if (!string.Equals(permit.ExpectedPlanId, plan.PlanId, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("APPLY_PLAN_STALE");
|
||||
|
||||
var targets = objects.Where(x => string.Equals(x.SnapshotId, permit.TargetSnapshotId, StringComparison.Ordinal)).ToArray();
|
||||
if (targets.Length != 1) throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_TARGET");
|
||||
var target = targets[0];
|
||||
if (target.MembershipStatus != ViewMembershipStatus.Resolved) throw new InvalidOperationException("APPLY_REQUIRES_RESOLVED_MEMBERSHIP");
|
||||
if (!string.Equals(target.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("APPLY_UNSUPPORTED_OBJECT_CLASS");
|
||||
if (target.Geometry.PaperPosition is not { } placement) throw new InvalidOperationException("ANNOTATION_PLACEMENT_NOT_RESOLVED");
|
||||
|
||||
var create = plan.Operations.Where(x => x.Kind == AnnotationOperationKind.Create && string.Equals(x.SourceSnapshotId, target.SnapshotId, StringComparison.Ordinal)).ToArray();
|
||||
if (create.Length != 1) throw new InvalidOperationException("APPLY_TARGET_CREATE_NOT_PLANNED");
|
||||
|
||||
var duplicateKey = Stable("duplicate", RuleId, viewport.Id, target.SnapshotId);
|
||||
return new FirstLimitedApplyPreflight
|
||||
{
|
||||
PlanId = plan.PlanId,
|
||||
Operation = new CreatePipeAnnotationOperation
|
||||
{
|
||||
OperationId = Stable("operation", RuleId, viewport.Id, target.SnapshotId),
|
||||
SourceObjectStableKey = target.SnapshotId,
|
||||
ViewStableKey = viewport.Id,
|
||||
RuleId = RuleId,
|
||||
Placement = new AnnotationPoint(placement.X, placement.Y),
|
||||
DuplicateKey = duplicateKey
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string Stable(params string[] values) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("|", values))))[..24];
|
||||
}
|
||||
@@ -43,7 +43,6 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
{
|
||||
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
||||
|
||||
var context = new PlantRuntimeContextReader().ReadCurrentContext();
|
||||
var details = new Dictionary<string, object?>
|
||||
@@ -75,6 +74,15 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == PRecAnnotExecutionMode.Apply && !string.Equals(commandName, "PXP-OA", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("APPLY_RULE_NOT_SUPPORTED");
|
||||
var hasLimitedApplyPermit = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("P_REC_ANNOT_APPLY_PERMIT_PATH"));
|
||||
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
||||
? FirstLimitedApplyPermitReader.ReadFor(document)
|
||||
: null;
|
||||
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
||||
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
||||
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var plans = new List<AnnotationPlan>();
|
||||
var objectsByViewport = new List<IReadOnlyList<PlantObjectSnapshot>>();
|
||||
@@ -82,7 +90,26 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
{
|
||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||
objectsByViewport.Add(objects);
|
||||
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
|
||||
plans.Add(builder.Build(new AnnotationPlanningRequest
|
||||
{
|
||||
CommandName = commandName,
|
||||
Viewport = viewport,
|
||||
Objects = objects,
|
||||
LimitedApplyTargetSnapshotId = applyPermit?.TargetSnapshotId,
|
||||
LimitedApplyRuleId = applyPermit is null ? null : FirstLimitedApplyPlanner.RuleId
|
||||
}));
|
||||
}
|
||||
|
||||
if (mode == PRecAnnotExecutionMode.DryRun &&
|
||||
string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_DIAGNOSTICS"), "FirstLimitedApply", StringComparison.Ordinal))
|
||||
{
|
||||
var candidates = objectsByViewport.SelectMany(x => x)
|
||||
.Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null)
|
||||
.Select(x => x.SnapshotId)
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
WriteEditor(document, $"\nP_Rec_Annot Apply preview candidates: {string.Join(",", candidates)}");
|
||||
details["ApplyPreviewCandidateCount"] = candidates.Length;
|
||||
}
|
||||
|
||||
var summary = new
|
||||
@@ -114,11 +141,32 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
details["PlanConflictCount"] = summary.ConflictCount;
|
||||
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
||||
details["ErrorCount"] = summary.ErrorCount;
|
||||
if (mode == PRecAnnotExecutionMode.Apply)
|
||||
{
|
||||
var corePermit = new FirstLimitedApplyPermit
|
||||
{
|
||||
FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker,
|
||||
AllowApply = applyPermit!.AllowApply,
|
||||
TargetSnapshotId = applyPermit.TargetSnapshotId,
|
||||
ExpectedPlanId = applyPermit.ExpectedPlanId
|
||||
};
|
||||
var preflight = new FirstLimitedApplyPlanner().Prepare(mode, plans.Single(), context.Viewports.Single(), objectsByViewport.Single(), corePermit);
|
||||
new RuntimeWriteGuard(mode).EnsureWriteAllowed(preflight.Operation.OperationId);
|
||||
RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document);
|
||||
RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document);
|
||||
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState);
|
||||
details["ApplyOutcome"] = outcome.ToString();
|
||||
details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0;
|
||||
details["AnnotationsSkipped"] = outcome == PipeAnnotationWriteOutcome.AlreadyExists ? 1 : 0;
|
||||
RuntimeDiagnostics.Write(outcome == PipeAnnotationWriteOutcome.Created ? "AnnotationCreated" : "AnnotationDuplicateDetected", operationId: operationId, commandName: commandName, resultStatus: outcome.ToString(), document: document);
|
||||
RuntimeDiagnostics.Write("AnnotationTransactionCommitted", operationId: operationId, commandName: commandName, resultStatus: outcome.ToString(), document: document);
|
||||
}
|
||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
WriteEditor(document, $"\nP_Rec_Annot — диагностический план\nПроект: {context.ProjectDisplayName}\nТип чертежа: Ortho\nViewport: {context.Viewports.Count}\nОбъектов найдено: {summary.ObjectsExamined}\nБудет создано: {summary.CreateCount}\nБудет обновлено: {summary.UpdateCount}\nБудет пропущено: {summary.SkipCount}\nКонфликтов: {summary.ConflictCount}\nОшибок: {summary.ErrorCount}\nDWG не изменён.");
|
||||
WriteEditor(document, $"\nPlan schema: {AnnotationPlanBuilder.CurrentPlanSchemaVersion}; PlanId: {planId ?? "multiple"}; membership resolved/partial/outside/unresolved: {membershipResolved}/{membershipPartial}/{membershipOutside}/{membershipUnresolved}.");
|
||||
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
var completedStatus = mode == PRecAnnotExecutionMode.Apply ? "ApplyCompleted" : "DryRunCompleted";
|
||||
WriteUsage(operationId, commandName, completedStatus, document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: completedStatus, document: document, details: details);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using System.Text.Json;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.AutoCAD.Geometry;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
// This file is deliberately runtime-only. Core receives only the anonymised
|
||||
// permit and canonical operation; no AutoCAD handle, database or project path
|
||||
// crosses the adapter boundary.
|
||||
internal sealed record FirstLimitedApplyRuntimePermit
|
||||
{
|
||||
public required bool AllowApply { get; init; }
|
||||
public required string FixtureMarkerPath { get; init; }
|
||||
public required string TargetSnapshotId { get; init; }
|
||||
public required string ExpectedPlanId { get; init; }
|
||||
public required string VisibilityState { get; init; }
|
||||
}
|
||||
|
||||
internal static class FirstLimitedApplyPermitReader
|
||||
{
|
||||
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=FIRST_LIMITED_APPLY";
|
||||
|
||||
public static FirstLimitedApplyRuntimePermit ReadFor(Document document)
|
||||
{
|
||||
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_APPLY_PERMIT_PATH");
|
||||
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("APPLY_PERMIT_PATH_REQUIRED");
|
||||
if (!File.Exists(path)) throw new InvalidOperationException("APPLY_PERMIT_MISSING");
|
||||
|
||||
var permit = JsonSerializer.Deserialize<FirstLimitedApplyRuntimePermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
if (permit is null || !permit.AllowApply || string.IsNullOrWhiteSpace(permit.TargetSnapshotId) || string.IsNullOrWhiteSpace(permit.ExpectedPlanId) || string.IsNullOrWhiteSpace(permit.VisibilityState))
|
||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath))
|
||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||
if (!string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||
|
||||
var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))!;
|
||||
var drawing = Path.GetFullPath(document.Name);
|
||||
if (!drawing.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||
return permit;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum PipeAnnotationWriteOutcome { Created, AlreadyExists }
|
||||
|
||||
internal sealed class FirstLimitedPipeAnnotationWriter
|
||||
{
|
||||
private const string BlockName = "PIPE";
|
||||
private const string XdataApplication = "PXP-OA";
|
||||
|
||||
public PipeAnnotationWriteOutcome Write(Document document, CreatePipeAnnotationOperation operation, string visibilityState)
|
||||
{
|
||||
var database = document.Database;
|
||||
using var transaction = database.TransactionManager.StartTransaction();
|
||||
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||
if (!table.Has(BlockName)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_BLOCK_MISSING");
|
||||
|
||||
var paper = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||
if (HasOwnedAnnotation(transaction, paper, operation.DuplicateKey)) return PipeAnnotationWriteOutcome.AlreadyExists;
|
||||
EnsureRegApp(transaction, database);
|
||||
|
||||
using var annotation = new BlockReference(
|
||||
new Point3d(operation.Placement.X, operation.Placement.Y, operation.Placement.Z),
|
||||
table[BlockName]);
|
||||
paper.AppendEntity(annotation);
|
||||
transaction.AddNewlyCreatedDBObject(annotation, true);
|
||||
SetConfirmedVisibility(annotation, visibilityState);
|
||||
annotation.XData = new ResultBuffer(
|
||||
new TypedValue((int)DxfCode.ExtendedDataRegAppName, XdataApplication),
|
||||
new TypedValue((int)DxfCode.ExtendedDataAsciiString, operation.DuplicateKey),
|
||||
new TypedValue((int)DxfCode.ExtendedDataAsciiString, operation.RuleId));
|
||||
transaction.Commit();
|
||||
return PipeAnnotationWriteOutcome.Created;
|
||||
}
|
||||
|
||||
private static bool HasOwnedAnnotation(Transaction transaction, BlockTableRecord paper, string duplicateKey)
|
||||
{
|
||||
foreach (ObjectId id in paper)
|
||||
{
|
||||
if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue;
|
||||
using var xdata = reference.GetXDataForApplication(XdataApplication);
|
||||
if (xdata?.AsArray().Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, duplicateKey, StringComparison.Ordinal)) == true)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void EnsureRegApp(Transaction transaction, Database database)
|
||||
{
|
||||
var apps = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
|
||||
if (apps.Has(XdataApplication)) return;
|
||||
apps.UpgradeOpen();
|
||||
using var record = new RegAppTableRecord { Name = XdataApplication };
|
||||
apps.Add(record);
|
||||
transaction.AddNewlyCreatedDBObject(record, true);
|
||||
}
|
||||
|
||||
// The "V" property and allowed values are the confirmed legacy PXP-OA
|
||||
// dynamic-block contract. Failure is pre-commit and leaves no entity.
|
||||
private static void SetConfirmedVisibility(BlockReference annotation, string visibilityState)
|
||||
{
|
||||
var property = annotation.DynamicBlockReferencePropertyCollection
|
||||
.Cast<DynamicBlockReferenceProperty>()
|
||||
.SingleOrDefault(value => string.Equals(value.PropertyName, "V", StringComparison.Ordinal));
|
||||
if (property is null || !property.GetAllowedValues().OfType<string>().Contains(visibilityState, StringComparer.Ordinal))
|
||||
throw new InvalidOperationException("ANNOTATION_VISIBILITY_STATE_UNSUPPORTED");
|
||||
property.Value = visibilityState;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.ProcessPower.DataLinks;
|
||||
using Autodesk.ProcessPower.Drawings2d;
|
||||
using Autodesk.ProcessPower.PnP3dObjects;
|
||||
using Autodesk.ProcessPower.P3dProjectParts;
|
||||
using Autodesk.ProcessPower.PlantInstance;
|
||||
using Autodesk.ProcessPower.ProjectManager;
|
||||
@@ -300,7 +301,7 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||
SupportNumber = Value("Support Number", "SupportNumber"),
|
||||
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
||||
Geometry = new GeometrySnapshot(),
|
||||
Geometry = membership.GeometryFor(rowId),
|
||||
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" },
|
||||
MembershipStatus = membership.StatusFor(rowId),
|
||||
@@ -391,7 +392,8 @@ internal sealed class PlantViewportMembershipReader
|
||||
internal readonly record struct MembershipReadResult(
|
||||
IReadOnlySet<int> SelectedRows,
|
||||
ViewMembershipStatus DefaultStatus,
|
||||
string DefaultCode)
|
||||
string DefaultCode,
|
||||
IReadOnlyDictionary<int, GeometrySnapshot>? Geometry = null)
|
||||
{
|
||||
public ViewMembershipStatus StatusFor(int rowId) =>
|
||||
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||
@@ -402,6 +404,11 @@ internal sealed class PlantViewportMembershipReader
|
||||
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||
? new[] { "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP" }
|
||||
: new[] { DefaultCode };
|
||||
|
||||
public GeometrySnapshot GeometryFor(int rowId) =>
|
||||
Geometry is not null && Geometry.TryGetValue(rowId, out var geometry)
|
||||
? geometry
|
||||
: new GeometrySnapshot();
|
||||
}
|
||||
|
||||
internal static MembershipReadResult Read(OrthoViewportInfo viewport, DataLinksManager dataLinks, Autodesk.ProcessPower.DataObjects.PnPRowIdArray sourceRows)
|
||||
@@ -426,6 +433,7 @@ internal sealed class PlantViewportMembershipReader
|
||||
}
|
||||
|
||||
var selectedRows = new HashSet<int>();
|
||||
var geometry = new Dictionary<int, GeometrySnapshot>();
|
||||
var selected = view.DwgQuery.Execute(new StringCollection(), new StringCollection());
|
||||
for (var index = 0; index < selected.Count; index++)
|
||||
{
|
||||
@@ -434,7 +442,11 @@ internal sealed class PlantViewportMembershipReader
|
||||
try
|
||||
{
|
||||
var rowId = dataLinks.FindAcPpRowId(sourceId.PrjObjectId);
|
||||
if (sourceRows.Contains(rowId)) selectedRows.Add(rowId);
|
||||
if (sourceRows.Contains(rowId))
|
||||
{
|
||||
selectedRows.Add(rowId);
|
||||
if (TryReadPipePlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement;
|
||||
}
|
||||
}
|
||||
catch (DLException)
|
||||
{
|
||||
@@ -447,7 +459,7 @@ internal sealed class PlantViewportMembershipReader
|
||||
["SelectedSourceRows"] = selectedRows.Count,
|
||||
["SourceRows"] = sourceRows.Count
|
||||
});
|
||||
return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID");
|
||||
return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", geometry);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
@@ -458,6 +470,41 @@ internal sealed class PlantViewportMembershipReader
|
||||
|
||||
private static MembershipReadResult Unsupported(string code) => new(new HashSet<int>(), ViewMembershipStatus.NotResolved, code);
|
||||
|
||||
// Confirmed Plant 3D 2026 linkage: DwgQuery.Execute returns MdObjectId,
|
||||
// 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)
|
||||
{
|
||||
geometry = new GeometrySnapshot();
|
||||
try
|
||||
{
|
||||
var openedByQuery = false;
|
||||
var sourceDatabase = sourceId.GetDatabase(false, ref openedByQuery);
|
||||
try
|
||||
{
|
||||
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
||||
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
||||
if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false;
|
||||
var point = pipe.StartPoint.TransformBy(sourceId.Xform);
|
||||
geometry = new GeometrySnapshot
|
||||
{
|
||||
PaperPosition = new Point2Dto(point.X, point.Y),
|
||||
Rotation = 0
|
||||
};
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (openedByQuery) sourceDatabase.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ortho viewport IDs are generated by PlantOrthoViewportReader from exactly this pair.
|
||||
// Only an exact regenerated ID is accepted; the reader never chooses an arbitrary view.
|
||||
private static string StableId(string sheetId, string? viewName)
|
||||
@@ -472,7 +519,7 @@ internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeW
|
||||
{
|
||||
public void EnsureWriteAllowed(string operation)
|
||||
{
|
||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
||||
if (mode == PRecAnnotExecutionMode.Apply) return;
|
||||
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +283,68 @@ public sealed class FoundationTests
|
||||
|
||||
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
||||
|
||||
[Fact] public void FirstLimitedApply_RequiresApplyMode()
|
||||
{
|
||||
var context = FirstApplyContext();
|
||||
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.DryRun, context.Plan, context.Viewport, context.Objects, context.Permit));
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApply_RequiresFreshPlan()
|
||||
{
|
||||
var context = FirstApplyContext();
|
||||
var permit = context.Permit with { ExpectedPlanId = "stale" };
|
||||
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, permit));
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApply_RequiresResolvedMembership()
|
||||
{
|
||||
var context = FirstApplyContext();
|
||||
var objects = new[] { context.Objects[0] with { MembershipStatus = ViewMembershipStatus.NotResolved } };
|
||||
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, objects, context.Permit));
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApply_RequiresExactlyOneTarget()
|
||||
{
|
||||
var context = FirstApplyContext();
|
||||
var permit = context.Permit with { TargetSnapshotId = "missing" };
|
||||
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, permit));
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApply_CreatesDeterministicOperation()
|
||||
{
|
||||
var context = FirstApplyContext();
|
||||
var planner = new FirstLimitedApplyPlanner();
|
||||
var first = planner.Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, context.Permit);
|
||||
var second = planner.Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, context.Permit);
|
||||
Assert.Equal(first.Operation.OperationId, second.Operation.OperationId);
|
||||
Assert.Equal(first.Operation.DuplicateKey, second.Operation.DuplicateKey);
|
||||
Assert.DoesNotContain("C:\\", first.Operation.DuplicateKey, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApplyPlan_OneResolvedPipeCreatesOnePipeOperation()
|
||||
{
|
||||
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||
{
|
||||
CommandName = "PXP-OA", Viewport = viewport,
|
||||
Objects = new[] { Snapshot("pipe", "Pipe", null), Snapshot("other", "Valve", "TEST") },
|
||||
LimitedApplyTargetSnapshotId = "pipe", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId
|
||||
});
|
||||
Assert.Equal(1, plan.Summary.CreateCount);
|
||||
Assert.Equal("PIPE", Assert.Single(plan.Operations.Where(x => x.Kind == AnnotationOperationKind.Create)).AnnotationType);
|
||||
Assert.Contains("APPLY_NOT_SELECTED_TARGET", plan.Operations.Single(x => x.SourceSnapshotId == "other").Reasons);
|
||||
}
|
||||
|
||||
[Fact] public void FirstLimitedApplyPlan_SelectionChangesCanonicalPlanId()
|
||||
{
|
||||
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||
var objects = new[] { Snapshot("one", "Pipe", null), Snapshot("two", "Pipe", null) };
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var one = builder.Build(new AnnotationPlanningRequest { CommandName = "PXP-OA", Viewport = viewport, Objects = objects, LimitedApplyTargetSnapshotId = "one", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId });
|
||||
var two = builder.Build(new AnnotationPlanningRequest { CommandName = "PXP-OA", Viewport = viewport, Objects = objects, LimitedApplyTargetSnapshotId = "two", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId });
|
||||
Assert.NotEqual(one.PlanId, two.PlanId);
|
||||
}
|
||||
|
||||
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
@@ -292,6 +354,21 @@ public sealed class FoundationTests
|
||||
Assert.DoesNotContain("SetProperties", source);
|
||||
}
|
||||
|
||||
private static (AnnotationPlan Plan, OrthoViewportInfo Viewport, PlantObjectSnapshot[] Objects, FirstLimitedApplyPermit Permit) FirstApplyContext()
|
||||
{
|
||||
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||
var objects = new[] { Snapshot("pipe", "Pipe", "TEST") };
|
||||
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||
{
|
||||
CommandName = "PXP-OA",
|
||||
Viewport = viewport,
|
||||
Objects = objects,
|
||||
LimitedApplyTargetSnapshotId = "pipe",
|
||||
LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId
|
||||
});
|
||||
return (plan, viewport, objects, new FirstLimitedApplyPermit { FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker, AllowApply = true, TargetSnapshotId = "pipe", ExpectedPlanId = plan.PlanId });
|
||||
}
|
||||
|
||||
private static AnnotationPlan Build(params PlantObjectSnapshot[] objects) => new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" }, Objects = objects });
|
||||
private static PlantObjectSnapshot Snapshot(string id, string type, string? line, string? tag = "TAG") => new() { SnapshotId = id, ObjectClass = type, LineNumber = line, Tag = tag, Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(1, 2) }, MembershipStatus = ViewMembershipStatus.Resolved };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user