Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37d3cacc13 | ||
|
|
713e620485 | ||
|
|
6f32371418 | ||
|
|
243080a1aa |
@@ -63,6 +63,16 @@ DataLinks активного Ortho drawing не являются источни
|
||||
rows читаются из source model drawing и Piping project part, разрешённых через
|
||||
Ortho sheet/view definitions.
|
||||
|
||||
Перед предположениями об Autodesk runtime API необходимо исследовать текущий
|
||||
код, доступный legacy usage, assembly metadata и реальные runtime types.
|
||||
Reflection/runtime inspection применяется только для подтверждения данных,
|
||||
которых нельзя установить статически. Production resolver не угадывает
|
||||
Autodesk properties по строковым именам.
|
||||
|
||||
Подтверждённый контракт Plant 3D 2026: `IDwgViewDef.DwgQuery` →
|
||||
`IDwgQuery.ModelFiles` → `ModelFile[].ModelFileName`. При несовпадении типа
|
||||
возвращать `ORTHO_MODEL_FILES_API_UNSUPPORTED`, без fallback-эвристики.
|
||||
|
||||
Проверка DCF выполняется на парных `control` и `DryRun` fixtures, созданных из
|
||||
одного закрытого состояния проекта. Сравнивать нужно `control delta` с
|
||||
`DryRun delta`: одинаковые изменения классифицируются как baseline behavior
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Annotation-plan determinism
|
||||
|
||||
`AnnotationPlanBuilder` uses schema version `1`. Its canonical input is the normalized command, source drawing basename, view ID, source snapshots sorted by stable ID, properties sorted by key, membership state, and operations sorted by stable source key and type. Absolute paths, process data, timestamps and runtime ObjectIds are excluded.
|
||||
|
||||
Runtime DryRun confirmed equal PlanIds and summaries for two calls of each supported command. Changing a relevant normalized input changes the PlanId.
|
||||
@@ -0,0 +1,3 @@
|
||||
# DCF control comparison
|
||||
|
||||
`DBMOD` covers only the active DWG. DCF and project metadata require paired control and DryRun fixture copies created from one fully closed state. This comparison has not yet been executed, so plugin-only DCF and metadata deltas remain unverified. Apply remains blocked.
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"executionMode": "DryRun",
|
||||
"planSchemaVersion": 1,
|
||||
"sourceObjectsRead": 7,
|
||||
"membershipResolved": 0,
|
||||
"membershipPartial": 0,
|
||||
"membershipUnresolved": 7,
|
||||
"phpRecAnnotPlanIds": ["plan-C5D40DC32505D5D9CFE6D38B", "plan-C5D40DC32505D5D9CFE6D38B"],
|
||||
"pxpAavPlanIds": ["plan-E4877CA13418D43A17F47AAA", "plan-E4877CA13418D43A17F47AAA"],
|
||||
"pxpOaPlanIds": ["plan-6C794D25C292E73AA3BDDC31", "plan-6C794D25C292E73AA3BDDC31"],
|
||||
"summariesDeterministic": true,
|
||||
"dbmodBefore": 0,
|
||||
"dbmodAfter": 0,
|
||||
"launcherSession": "RolledBack",
|
||||
"dcfControlComparison": null,
|
||||
"applyModeUsed": false
|
||||
}
|
||||
@@ -6,16 +6,23 @@
|
||||
"orthoDetection": "Confirmed",
|
||||
"viewportCount": 1,
|
||||
"dataLinksAvailable": true,
|
||||
"dataLinksSheetToSourceMapping": "blocked",
|
||||
"objectsRead": 0,
|
||||
"phpRecAnnotRuns": 2,
|
||||
"pxpAavRuns": 2,
|
||||
"pxpOaRuns": 1,
|
||||
"dbmodBefore": 0,
|
||||
"dbmodAfter": 0,
|
||||
"dataLinksSheetToSourceMapping": "resolved-through-piping-project-drawing-id",
|
||||
"sourceDrawingsResolved": 1,
|
||||
"sourceDataLinksAvailable": true,
|
||||
"objectsRead": 7,
|
||||
"pipeObjectsRead": 7,
|
||||
"phpRecAnnotRuns": 1,
|
||||
"pxpAavRuns": null,
|
||||
"pxpOaRuns": null,
|
||||
"planId": null,
|
||||
"repeatedPlanIdEqual": null,
|
||||
"dbmodBefore": null,
|
||||
"dbmodAfter": null,
|
||||
"dwgModified": null,
|
||||
"dcfModified": true,
|
||||
"changedProjectFiles": ["Piping.dcf"],
|
||||
"dcfModified": null,
|
||||
"changedProjectFiles": null,
|
||||
"unhandledExceptions": 0,
|
||||
"applyModeUsed": false
|
||||
"applyModeUsed": false,
|
||||
"launcherRollback": "RolledBack",
|
||||
"exactViewportMembership": "not-verified"
|
||||
}
|
||||
|
||||
@@ -10,13 +10,19 @@ viewport. In `DryRun`, `PHP_REC_ANNOT` ran twice, `PXP-AAV` ran twice, and
|
||||
`PlantProject.Name` was available for this fixture and rendered as
|
||||
`PRecAnnotTestProject`.
|
||||
|
||||
The Ortho sheet did not expose direct Piping rows through
|
||||
`DataLinksManager.SelectAcPpRowIds`; that API threw `DLException: NotFound`.
|
||||
The adapter now converts this to an empty read-only result. Consequently the
|
||||
run read zero source Plant objects and produced an empty plan. Mapping the
|
||||
Ortho sheet to source-model objects remains the blocker for non-empty
|
||||
AnnotationPlan validation.
|
||||
The active Ortho sheet does not expose direct Piping rows and must not be used
|
||||
as a DataLinks source. Metadata inspection and a sanitized runtime probe
|
||||
confirmed `IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName` as the source-model
|
||||
reference. The adapter resolves that reference through
|
||||
`PipingProject.GetPnPDrawingFiles()` and reads
|
||||
`DataLinksManager.SelectAcPpRowIds(PnPProjectDrawing.Id)`.
|
||||
|
||||
On the latest real DryRun, one source drawing was resolved and seven source
|
||||
Plant objects were read. `PHP_REC_ANNOT` reported seven skipped operations,
|
||||
zero writes, zero conflicts, and zero errors. This verifies source-model
|
||||
DataLinks access; it does not yet verify exact source-object-to-viewport
|
||||
membership or repeated-plan determinism.
|
||||
|
||||
The test fixture SHA baseline changed for `Piping.dcf` before the first DryRun
|
||||
command. This run therefore does not prove DCF invariance, although no plugin
|
||||
write operation was performed and DBMOD remained unchanged.
|
||||
command. This run therefore does not prove DCF invariance. A paired control
|
||||
fixture versus DryRun fixture comparison remains required.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Ortho source-model mapping
|
||||
|
||||
## Confirmed Plant 3D 2026 read path
|
||||
|
||||
```text
|
||||
Active Ortho document
|
||||
→ OrthoProject.DwgSheetDefinitions
|
||||
→ IDwgSheetDef.DwgViewDefs
|
||||
→ IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName
|
||||
→ PipingProject.GetPnPDrawingFiles()
|
||||
→ exact normalized-path match to PnPProjectDrawing
|
||||
→ PnPProjectDrawing.Id
|
||||
→ DataLinksManager.SelectAcPpRowIds(int dwgid)
|
||||
→ GetObjectClassname / GetAllProperties
|
||||
→ independent PlantObjectSnapshot
|
||||
→ AnnotationPlanBuilder
|
||||
```
|
||||
|
||||
`PlantOrthoViewportReader.ReadViewports` reads the typed DwgQuery contract and
|
||||
creates an Ortho viewport snapshot. `PlantObjectReader.ReadObjects` resolves
|
||||
the resulting model-file reference through the Piping project drawing list and
|
||||
reads DataLinks by project drawing ID. The active Ortho database is not a
|
||||
Piping-row source.
|
||||
|
||||
All calls are read-only. The adapter opens no write transaction and does not
|
||||
save a drawing, DCF, or project metadata.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
The source reader returns stable telemetry diagnostics rather than suppressing
|
||||
the result of a failed query: `ORTHO_SOURCE_DRAWING_NOT_RESOLVED`,
|
||||
`ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT`, `ORTHO_SOURCE_DRAWING_AMBIGUOUS`,
|
||||
`PIPING_PROJECT_PART_NOT_FOUND`, `SOURCE_DATALINKS_UNAVAILABLE`,
|
||||
`SOURCE_DATALINKS_NO_ROWS`, `SOURCE_DATABASE_READ_FAILED`,
|
||||
`SOURCE_PIPE_ROWS_NOT_FOUND`, or `SOURCE_DATALINKS_READ`.
|
||||
|
||||
Exact membership of a source object in a particular Ortho viewport is not yet
|
||||
implemented. A source-level DryRun plan must not be presented as proof of
|
||||
viewport membership.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Ortho viewport membership
|
||||
|
||||
Every source snapshot has an explicit `ViewMembershipStatus`. The current Plant 3D 2026 adapter has no confirmed official object-to-view mapping API, so the seven source objects are `NotResolved` and skipped with `VIEW_MEMBERSHIP_NOT_RESOLVED`. Source presence is not treated as viewport membership.
|
||||
|
||||
No geometric heuristic is used as an exact mapping. The next step requires a confirmed generated-entity linkage or documented view mapping.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Plant 3D 2026 DwgQuery / ModelFiles API contract
|
||||
|
||||
## Scope and method
|
||||
|
||||
This report is based on metadata-only inspection of the installed Plant 3D 2026
|
||||
assemblies. The assemblies were read from `<PLANT3D_2026_INSTALL>` and were not
|
||||
copied into the repository. Metadata-only inspection was necessary because the
|
||||
normal PowerShell host cannot load the product's .NET 8 dependencies safely.
|
||||
|
||||
Legacy source at the documented local path was not present on this machine, so
|
||||
there is no legacy API claim in this report.
|
||||
|
||||
## Confirmed assemblies
|
||||
|
||||
| Assembly | Version | Relevant type |
|
||||
| --- | --- | --- |
|
||||
| `PnPDwg2dDef` | `0.0.0.0` | `Autodesk.ProcessPower.Drawings2d.DwgQuery`, `IDwgQuery`, `DwgViewDef`, `IDwgViewDef`, `DwgSheetDef`, `IDwgSheetDef` |
|
||||
| `PnPDwg2dUtil` | `0.0.0.0` | `Autodesk.ProcessPower.Drawings2d.ModelFile` |
|
||||
| `PnP3dOrthoProjectPart` | `17.0.60.1` | Ortho project-part integration assembly; the metadata scan found no public DwgQuery declaration there |
|
||||
|
||||
`PnPDwg2dDef` and `PnPDwg2dUtil` use `0.0.0.0` assembly versions in the
|
||||
installed metadata. The supported runtime contract is therefore identified by
|
||||
the exact public type names below, not by an invented product-version number.
|
||||
|
||||
## Confirmed contract
|
||||
|
||||
```text
|
||||
Autodesk.ProcessPower.Drawings2d.IDwgViewDef.DwgQuery
|
||||
: Autodesk.ProcessPower.Drawings2d.IDwgQuery
|
||||
|
||||
Autodesk.ProcessPower.Drawings2d.IDwgQuery.ModelFiles
|
||||
: Autodesk.ProcessPower.Drawings2d.ModelFile[]
|
||||
|
||||
Autodesk.ProcessPower.Drawings2d.ModelFile.ModelFileName
|
||||
: System.String
|
||||
|
||||
Autodesk.ProcessPower.Drawings2d.ModelFile.ModelFileGuid
|
||||
: System.String
|
||||
```
|
||||
|
||||
`DwgViewDef` implements `IDwgViewDef`; `DwgQuery` implements `IDwgQuery` and
|
||||
`IDisposable`; `ModelFile` implements `IComparable<ModelFile>`.
|
||||
|
||||
`ModelFiles` is an array, not an untyped enumerable. `ModelFileName` is the
|
||||
only confirmed string field representing the model-file reference. `ModelFileGuid`
|
||||
is a separate identity field. `IsReferencedDwg` and `Xform` describe reference
|
||||
state and transform, not a substitute path.
|
||||
|
||||
## Public members relevant to source resolution
|
||||
|
||||
`IDwgViewDef` exposes `DwgQuery`, `ViewName`, layout/orientation properties,
|
||||
and `SetClippingPlanes`, `SetLayout`, `SetOrientation`, `Erase`, and `New`.
|
||||
|
||||
`IDwgQuery` exposes `ModelFiles`, `Execute`, and `Erase`.
|
||||
|
||||
`ModelFile` exposes `ModelFileName`, `ModelFileGuid`, `IsReferencedDwg`, and
|
||||
`Xform`; its constructors accept a file-name string and optional transform,
|
||||
reference flag, and GUID.
|
||||
|
||||
`IDwgSheetDef.DwgViewDefs` exposes the view-definition collection. Sheet
|
||||
`DwgName`/`DwgPath` identify the Ortho sheet and must not be used as the source
|
||||
of Piping DataLinks.
|
||||
|
||||
## Current and legacy usage
|
||||
|
||||
The original adapter used reflection and tried several possible names
|
||||
(`ModelFileName`, `Value`, and arbitrary string values). That was not a valid
|
||||
Plant 3D 2026 contract.
|
||||
|
||||
The production read path now uses the confirmed typed chain only:
|
||||
|
||||
```text
|
||||
IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName
|
||||
```
|
||||
|
||||
The legacy source path was unavailable locally, so no legacy source-model or
|
||||
DataLinks workaround has been imported.
|
||||
|
||||
## Runtime evidence
|
||||
|
||||
Static metadata cannot prove whether `ModelFileName` is project-relative or
|
||||
absolute in a concrete Ortho fixture, or whether a particular view has zero,
|
||||
one, or multiple model files. In DryRun only, setting
|
||||
`P_REC_ANNOT_DIAGNOSTICS=OrthoStructure` enables a sanitized structure probe.
|
||||
It records type names, collection counts, null state, string length, and a
|
||||
one-way value hash; it does not emit paths or engineering data.
|
||||
|
||||
On the local DryRun fixture, the runtime probe recorded one view with one
|
||||
`ModelFile`. The concrete types were `DwgViewDef`, `DwgQueryByBox`, and
|
||||
`ModelFile[]`; `ModelFileName` was non-null and an absolute fixture-model
|
||||
reference. The path itself was not retained in repository evidence.
|
||||
|
||||
The resolver resolves that reference to exactly one
|
||||
`PipingProject.GetPnPDrawingFiles()` entry by normalized full-path equality,
|
||||
then uses the confirmed `DataLinksManager.SelectAcPpRowIds(int dwgid)` overload
|
||||
with the resolved `PnPProjectDrawing.Id`. It does not open or query the active
|
||||
Ortho database for Piping rows.
|
||||
|
||||
The production resolver has no fallback property-name search. If the exact
|
||||
contract is unavailable at runtime it reports
|
||||
`ORTHO_MODEL_FILES_API_UNSUPPORTED` rather than guessing.
|
||||
@@ -58,8 +58,12 @@ public sealed record PlantObjectSnapshot
|
||||
public required GeometrySnapshot Geometry { get; init; }
|
||||
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
|
||||
public ViewMembershipStatus MembershipStatus { get; init; } = ViewMembershipStatus.NotResolved;
|
||||
public IReadOnlyList<string> MembershipEvidenceCodes { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public enum ViewMembershipStatus { NotResolved, Resolved, PartiallyResolved, ObjectOutsideView, Unsupported, Failed }
|
||||
|
||||
public sealed record PlantRuntimeContext
|
||||
{
|
||||
public bool HasActiveDocument { get; init; }
|
||||
@@ -206,6 +210,7 @@ public sealed record AnnotationIntent
|
||||
|
||||
public sealed record AnnotationPlan
|
||||
{
|
||||
public int PlanSchemaVersion { get; init; }
|
||||
public string PlanId { get; init; } = string.Empty;
|
||||
public string CommandName { get; init; } = string.Empty;
|
||||
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
{
|
||||
public const int CurrentPlanSchemaVersion = 1;
|
||||
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||
{
|
||||
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||
@@ -30,6 +31,12 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.MembershipStatus is not ViewMembershipStatus.Resolved and not ViewMembershipStatus.PartiallyResolved)
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "VIEW_MEMBERSHIP_NOT_RESOLVED" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
@@ -84,7 +91,8 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
|
||||
return new AnnotationPlan
|
||||
{
|
||||
PlanId = BuildPlanId(request, objects),
|
||||
PlanSchemaVersion = CurrentPlanSchemaVersion,
|
||||
PlanId = BuildPlanId(request, objects, operations),
|
||||
CommandName = request.CommandName,
|
||||
Operations = operations,
|
||||
Diagnostics = diagnostics,
|
||||
@@ -92,18 +100,31 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
Create = creates,
|
||||
Update = updates,
|
||||
Delete = Array.Empty<string>(),
|
||||
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
|
||||
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).OrderBy(x => x, StringComparer.Ordinal).ToArray(),
|
||||
ExaminedCount = objects.Length,
|
||||
IsDeterministic = true
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
|
||||
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects, IReadOnlyList<AnnotationOperationPlan> operations)
|
||||
{
|
||||
var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id);
|
||||
foreach (var item in objects)
|
||||
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
|
||||
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));
|
||||
foreach (var item in objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal))
|
||||
{
|
||||
canonical.Append("|object=").Append(Normalize(item.SnapshotId)).Append('|').Append(Normalize(item.ObjectClass))
|
||||
.Append('|').Append(Normalize(item.Tag)).Append('|').Append(Normalize(item.LineNumber))
|
||||
.Append('|').Append(item.MembershipStatus);
|
||||
foreach (var property in item.Properties.OrderBy(x => x.Key, StringComparer.Ordinal))
|
||||
canonical.Append("|property=").Append(Normalize(property.Key)).Append('=').Append(Normalize(property.Value));
|
||||
}
|
||||
foreach (var operation in operations.OrderBy(x => x.SourceSnapshotId, StringComparer.Ordinal).ThenBy(x => x.Kind))
|
||||
canonical.Append("|operation=").Append(Normalize(operation.SourceSnapshotId)).Append(':').Append(operation.Kind).Append(':').Append(string.Join(',', operation.Reasons.OrderBy(x => x, StringComparer.Ordinal)));
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||
}
|
||||
|
||||
private static string Normalize(string? value) => (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
|
||||
ObjectClass = item.ClassName,
|
||||
Tag = item.Tag,
|
||||
LineNumber = item.LineNumber,
|
||||
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
|
||||
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation },
|
||||
MembershipStatus = ViewMembershipStatus.Resolved
|
||||
}).ToArray();
|
||||
|
||||
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||
|
||||
@@ -77,9 +77,11 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var plans = new List<AnnotationPlan>();
|
||||
var objectsByViewport = new List<IReadOnlyList<PlantObjectSnapshot>>();
|
||||
foreach (var viewport in context.Viewports)
|
||||
{
|
||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||
objectsByViewport.Add(objects);
|
||||
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
|
||||
}
|
||||
|
||||
@@ -94,7 +96,16 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
||||
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
||||
};
|
||||
var membershipResolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved);
|
||||
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 planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||
details["PlanId"] = planId;
|
||||
details["MembershipResolved"] = membershipResolved;
|
||||
details["MembershipPartial"] = membershipPartial;
|
||||
details["MembershipUnresolved"] = membershipUnresolved;
|
||||
details["PlanCreateCount"] = summary.CreateCount;
|
||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||
details["PlanSkipCount"] = summary.SkipCount;
|
||||
@@ -103,6 +114,7 @@ public sealed class EntryPoint : IExtensionApplication
|
||||
details["ErrorCount"] = summary.ErrorCount;
|
||||
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/unresolved: {membershipResolved}/{membershipPartial}/{membershipUnresolved}.");
|
||||
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Autodesk.ProcessPower.Drawings2d;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
// This is a diagnostic-only probe. It is never part of source resolution and is
|
||||
// called only by the explicit DryRun/OrthoStructure environment flag combination.
|
||||
internal static class OrthoRuntimeStructureInspector
|
||||
{
|
||||
public static void Inspect(IDwgViewDef view)
|
||||
{
|
||||
if (!RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled) return;
|
||||
|
||||
try
|
||||
{
|
||||
var query = view.DwgQuery;
|
||||
var modelFiles = query?.ModelFiles;
|
||||
var details = new Dictionary<string, object?>
|
||||
{
|
||||
["ViewRuntimeType"] = TypeName(view.GetType()),
|
||||
["DwgQueryRuntimeType"] = query is null ? null : TypeName(query.GetType()),
|
||||
["ModelFilesDeclaredType"] = typeof(ModelFile[]).FullName,
|
||||
["ModelFilesRuntimeType"] = modelFiles?.GetType().AssemblyQualifiedName,
|
||||
["ModelFilesCount"] = modelFiles?.Length ?? 0,
|
||||
["ModelFileElements"] = modelFiles?.Select(DescribeModelFile).ToArray() ?? Array.Empty<IReadOnlyDictionary<string, object?>>()
|
||||
};
|
||||
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspected", resultStatus: "Captured", details: details);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspectionFailed", "WARNING", resultStatus: "Failed", exception: exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, object?> DescribeModelFile(ModelFile? modelFile)
|
||||
{
|
||||
if (modelFile is null)
|
||||
{
|
||||
return new Dictionary<string, object?> { ["IsNull"] = true };
|
||||
}
|
||||
|
||||
var properties = modelFile.GetType()
|
||||
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(property => property.CanRead && property.GetIndexParameters().Length == 0)
|
||||
.OrderBy(property => property.Name, StringComparer.Ordinal)
|
||||
.Select(property => DescribeProperty(modelFile, property))
|
||||
.ToArray();
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["RuntimeType"] = TypeName(modelFile.GetType()),
|
||||
["Properties"] = properties
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, object?> DescribeProperty(ModelFile modelFile, PropertyInfo property)
|
||||
{
|
||||
object? value;
|
||||
try { value = property.GetValue(modelFile); }
|
||||
catch { value = null; }
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["PropertyName"] = property.Name,
|
||||
["PropertyType"] = property.PropertyType.FullName,
|
||||
["ValueKind"] = value?.GetType().FullName ?? "null",
|
||||
["IsNull"] = value is null,
|
||||
["StringLength"] = value is string text ? text.Length : null,
|
||||
["PathKind"] = property.Name == "ModelFileName" && value is string path
|
||||
? (Path.IsPathRooted(path) ? "Absolute" : "ProjectRelative")
|
||||
: null,
|
||||
["CollectionCount"] = value is ICollection collection ? collection.Count : null,
|
||||
["StableHash"] = value is null ? null : Hash(value.ToString() ?? string.Empty)
|
||||
};
|
||||
}
|
||||
|
||||
private static string TypeName(Type type) => $"{type.FullName}, {type.Assembly.GetName().Name}, {type.Assembly.GetName().Version}";
|
||||
|
||||
private static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..16];
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026ManagedDir)\PnPProjectManagerMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnP3dProjectPartsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Text;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.ProcessPower.DataLinks;
|
||||
using Autodesk.ProcessPower.Drawings2d;
|
||||
using Autodesk.ProcessPower.P3dProjectParts;
|
||||
using Autodesk.ProcessPower.PlantInstance;
|
||||
using Autodesk.ProcessPower.ProjectManager;
|
||||
@@ -151,50 +152,134 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
||||
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||
}
|
||||
|
||||
private static string? ReadSourceDrawingPath(object view)
|
||||
private static string? ReadSourceDrawingPath(IDwgViewDef view)
|
||||
{
|
||||
// The Plant 3D 2026 view definition owns a DwgQuery. Its ModelFiles are
|
||||
// the authoritative source-model references; sheet.DwgName is the Ortho
|
||||
// sheet and must not be used for Piping DataLinks.
|
||||
// Confirmed Plant 3D 2026 contract (PnPDwg2dDef/PnPDwg2dUtil):
|
||||
// IDwgViewDef.DwgQuery -> IDwgQuery.ModelFiles -> ModelFile.ModelFileName.
|
||||
// The Ortho sheet is never used as a Piping DataLinks source.
|
||||
try
|
||||
{
|
||||
var query = view.GetType().GetProperty("DwgQuery")?.GetValue(view);
|
||||
var files = query?.GetType().GetProperty("ModelFiles")?.GetValue(query) as System.Collections.IEnumerable;
|
||||
if (files is null) return null;
|
||||
foreach (var file in files)
|
||||
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
||||
{
|
||||
var value = file as string
|
||||
?? file?.GetType().GetProperty("ModelFileName")?.GetValue(file)?.ToString()
|
||||
?? file?.GetType().GetProperty("Value")?.GetValue(file)?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||
RuntimeDiagnostics.Write("OrthoModelFilesContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled)
|
||||
{
|
||||
OrthoRuntimeStructureInspector.Inspect(view);
|
||||
}
|
||||
|
||||
return view.DwgQuery?.ModelFiles
|
||||
.Select(file => file.ModelFileName)
|
||||
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
RuntimeDiagnostics.Write("OrthoModelFilesReadFailed", "WARNING", resultStatus: "ORTHO_MODEL_FILES_API_UNSUPPORTED", exception: exception);
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class OrthoModelFilesContract
|
||||
{
|
||||
private const string ExpectedViewType = "Autodesk.ProcessPower.Drawings2d.DwgViewDef";
|
||||
private const string ExpectedQueryType = "Autodesk.ProcessPower.Drawings2d.DwgQueryByBox";
|
||||
private const string ExpectedModelFileType = "Autodesk.ProcessPower.Drawings2d.ModelFile";
|
||||
private const string ExpectedDefAssembly = "PnPDwg2dDef";
|
||||
private const string ExpectedUtilAssembly = "PnPDwg2dUtil";
|
||||
private const string ExpectedVersion = "0.0.0.0";
|
||||
|
||||
public static bool IsSupported(IDwgViewDef view, out OrthoModelFilesContractDiagnostic diagnostic)
|
||||
{
|
||||
var query = view.DwgQuery;
|
||||
var files = query?.ModelFiles;
|
||||
var valid = HasExpectedType(view, ExpectedViewType, ExpectedDefAssembly) &&
|
||||
query is not null && HasExpectedType(query, ExpectedQueryType, ExpectedDefAssembly) &&
|
||||
files is not null && files.GetType() == typeof(ModelFile[]) &&
|
||||
files.All(file => file is not null && HasExpectedType(file, ExpectedModelFileType, ExpectedUtilAssembly));
|
||||
diagnostic = new OrthoModelFilesContractDiagnostic(
|
||||
valid ? "ORTHO_MODEL_FILES_API_SUPPORTED" : "ORTHO_MODEL_FILES_API_UNSUPPORTED",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["ExpectedViewType"] = ExpectedViewType,
|
||||
["ActualViewType"] = TypeName(view),
|
||||
["ExpectedQueryType"] = ExpectedQueryType,
|
||||
["ActualQueryType"] = query is null ? null : TypeName(query),
|
||||
["ExpectedModelFileType"] = ExpectedModelFileType,
|
||||
["ActualModelFilesType"] = files?.GetType().AssemblyQualifiedName,
|
||||
["ExpectedAssemblyVersion"] = ExpectedVersion,
|
||||
["ActualViewAssemblyVersion"] = view.GetType().Assembly.GetName().Version?.ToString(),
|
||||
["ActualQueryAssemblyVersion"] = query?.GetType().Assembly.GetName().Version?.ToString(),
|
||||
["ActualModelFilesAssemblyVersion"] = files?.GetType().Assembly.GetName().Version?.ToString()
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
private static bool HasExpectedType(object value, string expectedType, string expectedAssembly) =>
|
||||
string.Equals(value.GetType().FullName, expectedType, StringComparison.Ordinal) &&
|
||||
string.Equals(value.GetType().Assembly.GetName().Name, expectedAssembly, StringComparison.Ordinal) &&
|
||||
string.Equals(value.GetType().Assembly.GetName().Version?.ToString(), ExpectedVersion, StringComparison.Ordinal);
|
||||
|
||||
private static string TypeName(object value) => $"{value.GetType().FullName}, {value.GetType().Assembly.GetName().Name}, {value.GetType().Assembly.GetName().Version}";
|
||||
}
|
||||
|
||||
internal sealed record OrthoModelFilesContractDiagnostic(string Code, IReadOnlyDictionary<string, object?> Details);
|
||||
|
||||
internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
{
|
||||
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
||||
{
|
||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||
if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName))
|
||||
{
|
||||
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", viewport);
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
|
||||
PipingProject? pipingProject;
|
||||
try { pipingProject = PlantApplication.ProjectPart("Piping") as PipingProject; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport, exception);
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
if (pipingProject is null)
|
||||
{
|
||||
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport);
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
|
||||
var sourceDrawing = ResolveProjectDrawing(pipingProject, viewport.SourceDrawingName);
|
||||
if (sourceDrawing is null) return Array.Empty<PlantObjectSnapshot>();
|
||||
|
||||
DataLinksManager? dataLinks;
|
||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
||||
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
||||
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
|
||||
|
||||
if (!Path.IsPathRooted(viewport.SourceDrawingName) || !File.Exists(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||
try { dataLinks = pipingProject.DataLinksManager; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
if (dataLinks is null)
|
||||
{
|
||||
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport);
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
|
||||
var snapshots = new List<PlantObjectSnapshot>();
|
||||
try
|
||||
{
|
||||
using var sourceDatabase = new Database(false, true);
|
||||
sourceDatabase.ReadDwgFile(viewport.SourceDrawingName, FileShare.ReadWrite, true, null);
|
||||
sourceDatabase.CloseInput(true);
|
||||
foreach (var rowId in dataLinks.SelectAcPpRowIds(sourceDatabase))
|
||||
// Confirmed Plant 3D 2026 contract: resolve the source model to the
|
||||
// Piping project drawing record, then read its DataLinks by drawing Id.
|
||||
// The active Ortho database is deliberately never passed to DataLinks.
|
||||
var rowIds = dataLinks.SelectAcPpRowIds(sourceDrawing.Id);
|
||||
if (rowIds.Count == 0)
|
||||
{
|
||||
WriteOutcome("SOURCE_DATALINKS_NO_ROWS", viewport, details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||
return Array.Empty<PlantObjectSnapshot>();
|
||||
}
|
||||
|
||||
foreach (var rowId in rowIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -217,20 +302,78 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
||||
});
|
||||
}
|
||||
catch
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Keep the read path best-effort; no write operation is attempted.
|
||||
RuntimeDiagnostics.Write("SourceDataLinksRowReadFailed", "WARNING", resultStatus: "SOURCE_DATABASE_READ_FAILED", exception: exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (DLException)
|
||||
catch (DLException exception)
|
||||
{
|
||||
// An Ortho sheet can have no direct Piping DataLinks rows. The caller
|
||||
// receives an empty read-only result rather than a command failure.
|
||||
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteOutcome("SOURCE_DATABASE_READ_FAILED", viewport, exception);
|
||||
}
|
||||
|
||||
WriteOutcome(snapshots.Count == 0 ? "SOURCE_PIPE_ROWS_NOT_FOUND" : "SOURCE_DATALINKS_READ", viewport,
|
||||
details: new Dictionary<string, object?> { ["SourcePlantObjectsRead"] = snapshots.Count, ["PipeObjectsRead"] = snapshots.Count });
|
||||
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static PnPProjectDrawing? ResolveProjectDrawing(PipingProject pipingProject, string sourceDrawingPath)
|
||||
{
|
||||
if (!Path.IsPathRooted(sourceDrawingPath))
|
||||
{
|
||||
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", null);
|
||||
return null;
|
||||
}
|
||||
|
||||
string normalizedSource;
|
||||
try { normalizedSource = Path.GetFullPath(sourceDrawingPath); }
|
||||
catch (Exception exception)
|
||||
{
|
||||
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", null, exception);
|
||||
return null;
|
||||
}
|
||||
|
||||
var matches = pipingProject.GetPnPDrawingFiles()
|
||||
.Where(drawing => MatchesSourcePath(drawing, normalizedSource))
|
||||
.OrderBy(drawing => drawing.Id)
|
||||
.ToArray();
|
||||
if (matches.Length == 1)
|
||||
{
|
||||
RuntimeDiagnostics.Write("SourceProjectDrawingResolved", resultStatus: "SOURCE_FROM_PROJECT_DRAWING_ROW", details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
WriteOutcome(matches.Length == 0 ? "ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT" : "ORTHO_SOURCE_DRAWING_AMBIGUOUS", null,
|
||||
details: new Dictionary<string, object?> { ["CandidateCount"] = matches.Length });
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool MatchesSourcePath(PnPProjectDrawing drawing, string normalizedSource) =>
|
||||
EquivalentPath(drawing.AbsoluteFileName, normalizedSource) ||
|
||||
EquivalentPath(drawing.ResolvedFilePath, normalizedSource);
|
||||
|
||||
private static bool EquivalentPath(string? candidate, string normalizedSource)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(candidate)) return false;
|
||||
try { return string.Equals(Path.GetFullPath(candidate), normalizedSource, StringComparison.OrdinalIgnoreCase); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static void WriteOutcome(string code, OrthoViewportInfo? viewport, Exception? exception = null, IReadOnlyDictionary<string, object?>? details = null)
|
||||
{
|
||||
var sanitizedDetails = new Dictionary<string, object?>
|
||||
{
|
||||
["SourceResolved"] = viewport?.SourceDrawingName is not null
|
||||
};
|
||||
if (details is not null) foreach (var detail in details) sanitizedDetails[detail.Key] = detail.Value;
|
||||
RuntimeDiagnostics.Write("SourceDataLinksReadCompleted", code == "SOURCE_DATALINKS_READ" ? "INFO" : "WARNING", resultStatus: code, exception: exception, details: sanitizedDetails);
|
||||
}
|
||||
|
||||
private static string StableId(string drawingName, int rowId, string? className)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
||||
|
||||
@@ -14,6 +14,10 @@ internal static class RuntimeDiagnostics
|
||||
|
||||
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
||||
|
||||
public static bool IsOrthoStructureDiagnosticsEnabled =>
|
||||
string.Equals(RuntimeMode, "DryRun", StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_DIAGNOSTICS"), "OrthoStructure", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static void Write(
|
||||
string eventType,
|
||||
string level = "INFO",
|
||||
|
||||
@@ -104,6 +104,37 @@ public sealed class FoundationTests
|
||||
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_ReversedObjectOrder_HasSamePlanId()
|
||||
{
|
||||
var first = Build(Snapshot("a", "Pipe", "L-1"), Snapshot("b", "Pipe", "L-2"));
|
||||
var second = Build(Snapshot("b", "Pipe", "L-2"), Snapshot("a", "Pipe", "L-1"));
|
||||
Assert.Equal(first.PlanId, second.PlanId);
|
||||
Assert.Equal(first.Summary, second.Summary);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_AbsolutePath_DoesNotAffectPlanId()
|
||||
{
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var item = Snapshot("a", "Pipe", "L-1");
|
||||
var first = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "v", DisplayName = "v", SourceDrawingName = "C:\\one\\model.dwg" }, Objects = new[] { item } });
|
||||
var second = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "v", DisplayName = "v", SourceDrawingName = "D:\\two\\model.dwg" }, Objects = new[] { item } });
|
||||
Assert.Equal(first.PlanId, second.PlanId);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_ChangedRelevantProperty_ChangesPlanId()
|
||||
{
|
||||
var first = Build(Snapshot("a", "Pipe", "L-1"));
|
||||
var second = Build(Snapshot("a", "Pipe", "L-2"));
|
||||
Assert.NotEqual(first.PlanId, second.PlanId);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_UnresolvedMembership_IsStableSkip()
|
||||
{
|
||||
var plan = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.NotResolved });
|
||||
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||
Assert.Contains("VIEW_MEMBERSHIP_NOT_RESOLVED", plan.SkipReasons);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||
{
|
||||
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||
@@ -164,6 +195,51 @@ public sealed class FoundationTests
|
||||
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", source.EvidenceCodes);
|
||||
}
|
||||
|
||||
[Fact] public void DwgQueryContract_UsesConfirmedModelFileContract()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.Contains("view.DwgQuery?.ModelFiles", source);
|
||||
Assert.Contains("file.ModelFileName", source);
|
||||
Assert.Contains("ORTHO_MODEL_FILES_API_UNSUPPORTED", source);
|
||||
}
|
||||
|
||||
[Fact] public void DwgQueryContract_DoesNotGuessAlternativePropertyNames()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.DoesNotContain("GetProperty(\"ModelFileName\")", source);
|
||||
Assert.DoesNotContain("GetProperty(\"Value\")", source);
|
||||
Assert.DoesNotContain("GetProperty(\"FileName\")", source);
|
||||
}
|
||||
|
||||
[Fact] public void OrthoSourceReader_UsesResolvedPipingProjectDrawingId()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.Contains("pipingProject.GetPnPDrawingFiles()", source);
|
||||
Assert.Contains("dataLinks.SelectAcPpRowIds(sourceDrawing.Id)", source);
|
||||
}
|
||||
|
||||
[Fact] public void OrthoSourceReader_DoesNotQueryTheActiveOrthoDatabase()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.DoesNotContain("SelectAcPpRowIds(sourceDatabase)", source);
|
||||
Assert.DoesNotContain("ReadDwgFile(viewport.SourceDrawingName", source);
|
||||
}
|
||||
|
||||
[Fact] public void OrthoSourceReader_ReportsDistinctSourceDiagnostics()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", source);
|
||||
Assert.Contains("SOURCE_DATALINKS_UNAVAILABLE", source);
|
||||
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
||||
}
|
||||
|
||||
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
||||
Assert.Contains("P_REC_ANNOT_DIAGNOSTICS", source);
|
||||
Assert.Contains("OrthoStructure", source);
|
||||
}
|
||||
|
||||
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
||||
|
||||
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||
@@ -176,5 +252,5 @@ public sealed class FoundationTests
|
||||
}
|
||||
|
||||
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) } };
|
||||
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