Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
209b8b89b8 | ||
|
|
31787fc05a | ||
|
|
11cf97c476 | ||
|
|
76d362d2c3 | ||
|
|
37d3cacc13 | ||
|
|
713e620485 |
@@ -57,6 +57,16 @@ Bundle содержит только фактический runtime dependency g
|
|||||||
Fixture: local Plant 3D 2026 project вне Git с настоящей Pipe, synthetic line
|
Fixture: local Plant 3D 2026 project вне Git с настоящей Pipe, synthetic line
|
||||||
number, Ortho drawing/viewport и SHA baseline до DryRun.
|
number, Ortho drawing/viewport и SHA baseline до DryRun.
|
||||||
|
|
||||||
|
## Viewport membership and source DataLinks
|
||||||
|
|
||||||
|
Legacy P_Rec_Annot source for read-only analysis is
|
||||||
|
`\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo`.
|
||||||
|
|
||||||
|
A source object is not a member of an Ortho viewport without confirmed
|
||||||
|
evidence. A geometry-only filter is at most `PartiallyResolved`. Apply is
|
||||||
|
blocked by any `PLUGIN_ONLY_CHANGE` in a DWG, DCF, or project metadata found by
|
||||||
|
a paired CONTROL/DryRun comparison.
|
||||||
|
|
||||||
## Ortho source DataLinks and DCF control
|
## Ortho source DataLinks and DCF control
|
||||||
|
|
||||||
DataLinks активного Ortho drawing не являются источником Piping rows. Piping
|
DataLinks активного Ortho drawing не являются источником Piping rows. Piping
|
||||||
|
|||||||
@@ -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,26 @@
|
|||||||
|
# CONTROL versus DryRun DCF comparison
|
||||||
|
|
||||||
|
DBMOD covers only the active drawing. This procedure separately compares
|
||||||
|
content-bearing project files after identical CONTROL and DryRun sessions.
|
||||||
|
|
||||||
|
1. With Plant 3D closed and the Launcher session `RolledBack`, copy the local
|
||||||
|
fixture to `<FROZEN_FIXTURE>`.
|
||||||
|
2. Create `<CONTROL_FIXTURE>` and `<DRYRUN_FIXTURE>` from that same frozen
|
||||||
|
state; calculate complete local SHA-256 inventories before each run.
|
||||||
|
3. Open model and Ortho drawings in CONTROL through the same Launcher DryRun
|
||||||
|
launch contract, but invoke no P_Rec_Annot command. Close without saving,
|
||||||
|
wait for Launcher rollback, then calculate the after inventory.
|
||||||
|
4. Repeat with DRYRUN and invoke each supported command twice. Close without
|
||||||
|
saving, wait for rollback, then calculate the after inventory.
|
||||||
|
5. Compare deltas by logical relative path. Classify each content-bearing file
|
||||||
|
as `UNCHANGED`, `AUTODESK_BASELINE_CHANGE`, `PLUGIN_ONLY_CHANGE`, or
|
||||||
|
`INCONCLUSIVE`.
|
||||||
|
|
||||||
|
Content-bearing groups include model DWG, Ortho DWG, `Piping.dcf`, remaining
|
||||||
|
DCF/database files, `Project.xml` or equivalent project metadata, and Ortho
|
||||||
|
metadata. Lock, cache, session, log, preview, and temporary files are reported
|
||||||
|
separately and cannot establish project invariance.
|
||||||
|
|
||||||
|
Any `PLUGIN_ONLY_CHANGE` blocks Apply. Full paths and hashes remain local;
|
||||||
|
repository evidence contains only logical fixture markers, groups, counts, and
|
||||||
|
classifications.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"fixture": {
|
||||||
|
"frozen": "<FROZEN_FIXTURE>",
|
||||||
|
"control": "<CONTROL_FIXTURE>",
|
||||||
|
"dryRun": "<DRYRUN_FIXTURE>",
|
||||||
|
"trackedFileCount": 210
|
||||||
|
},
|
||||||
|
"membership": {
|
||||||
|
"mechanism": "IDwgViewDef.DwgQuery.Execute -> MdObjectId.PrjObjectId -> DataLinksManager.FindAcPpRowId",
|
||||||
|
"orthoViews": 1,
|
||||||
|
"sourceObjects": 7,
|
||||||
|
"resolved": 5,
|
||||||
|
"partiallyResolved": 0,
|
||||||
|
"outsideView": 2,
|
||||||
|
"notResolved": 0,
|
||||||
|
"evidenceCode": "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"PHP_REC_ANNOT": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-AD701C7AA38D99E4C28D7D6D",
|
||||||
|
"run2PlanId": "plan-AD701C7AA38D99E4C28D7D6D",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
},
|
||||||
|
"PXP-AAV": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-C4FCD62F4EEBA1D1948FA22B",
|
||||||
|
"run2PlanId": "plan-C4FCD62F4EEBA1D1948FA22B",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
},
|
||||||
|
"PXP-OA": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-C30D133E2D5B4BF4C0AF2ED1",
|
||||||
|
"run2PlanId": "plan-C30D133E2D5B4BF4C0AF2ED1",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectFileComparison": {
|
||||||
|
"controlDeltaCount": 0,
|
||||||
|
"dryRunDeltaCount": 0,
|
||||||
|
"modelDwgPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"orthoDwgPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"pipingDcfPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"projectMetadataPluginOnlyDelta": "UNCHANGED"
|
||||||
|
},
|
||||||
|
"dbmod": {
|
||||||
|
"pairedRun": null,
|
||||||
|
"reason": "Command-window output was not retained."
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"controlSession": "RolledBack",
|
||||||
|
"dryRunSession": "RolledBack",
|
||||||
|
"netloadCount": 1,
|
||||||
|
"profileArgumentPresent": false
|
||||||
|
},
|
||||||
|
"apply": {
|
||||||
|
"performed": false,
|
||||||
|
"authorised": false,
|
||||||
|
"reason": "All operations remain Skip; annotation rules are not yet implemented."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
Date: 2026-08-03
|
Date: 2026-08-03
|
||||||
|
|
||||||
The local test fixture's `TestPipeOrtho.dwg` was detected as Ortho with one
|
The local test fixture's `TestPipeOrtho.dwg` was detected as Ortho with one
|
||||||
viewport. In `DryRun`, `PHP_REC_ANNOT` ran twice, `PXP-AAV` ran twice, and
|
viewport. In the paired `DryRun`, `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA`
|
||||||
`PXP-OA` ran once. All completed without an unhandled exception; `DBMOD` was
|
each ran twice. Plugin telemetry recorded all six preflight completions without
|
||||||
`0` before and after.
|
an unhandled exception. The command-window DBMOD output was not retained for
|
||||||
|
this paired run, so its DBMOD value is recorded as `NOT RECORDED` rather than
|
||||||
|
inferred from telemetry.
|
||||||
|
|
||||||
`PlantProject.Name` was available for this fixture and rendered as
|
`PlantProject.Name` was available for this fixture and rendered as
|
||||||
`PRecAnnotTestProject`.
|
`PRecAnnotTestProject`.
|
||||||
@@ -17,12 +19,28 @@ reference. The adapter resolves that reference through
|
|||||||
`PipingProject.GetPnPDrawingFiles()` and reads
|
`PipingProject.GetPnPDrawingFiles()` and reads
|
||||||
`DataLinksManager.SelectAcPpRowIds(PnPProjectDrawing.Id)`.
|
`DataLinksManager.SelectAcPpRowIds(PnPProjectDrawing.Id)`.
|
||||||
|
|
||||||
On the latest real DryRun, one source drawing was resolved and seven source
|
On the paired real DryRun, one source drawing was resolved and seven source
|
||||||
Plant objects were read. `PHP_REC_ANNOT` reported seven skipped operations,
|
Plant objects were read. The confirmed view query resolved five source rows to
|
||||||
zero writes, zero conflicts, and zero errors. This verifies source-model
|
the active viewport and classified two as outside the view. Each command
|
||||||
DataLinks access; it does not yet verify exact source-object-to-viewport
|
reported seven skipped operations, zero writes, zero conflicts, and zero
|
||||||
membership or repeated-plan determinism.
|
errors. The repeated plans were deterministic:
|
||||||
|
|
||||||
The test fixture SHA baseline changed for `Piping.dcf` before the first DryRun
|
| Command | Run 1 / run 2 PlanId |
|
||||||
command. This run therefore does not prove DCF invariance. A paired control
|
| --- | --- |
|
||||||
fixture versus DryRun fixture comparison remains required.
|
| `PHP_REC_ANNOT` | `plan-AD701C7AA38D99E4C28D7D6D` |
|
||||||
|
| `PXP-AAV` | `plan-C4FCD62F4EEBA1D1948FA22B` |
|
||||||
|
| `PXP-OA` | `plan-C30D133E2D5B4BF4C0AF2ED1` |
|
||||||
|
|
||||||
|
This verifies source-model DataLinks access, exact view membership through the
|
||||||
|
stored view query, and repeated-plan determinism. The remaining functional
|
||||||
|
limitation is annotation-rule coverage: all seven operations remain safe
|
||||||
|
`Skip` operations, so no first Apply is authorised.
|
||||||
|
|
||||||
|
The earlier single-fixture SHA baseline changed before the first command and
|
||||||
|
was therefore inconclusive. A fresh paired comparison used 210-file CONTROL
|
||||||
|
and DRYRUN copies created from the same closed frozen fixture. Both inventories
|
||||||
|
had zero SHA-256 delta after their respective sessions. The CONTROL session
|
||||||
|
invoked no plugin command; the DRYRUN session invoked all six commands. Thus
|
||||||
|
the model DWG, Ortho DWG, `Piping.dcf`, project metadata, and other tracked
|
||||||
|
content-bearing files have no plugin-only delta in this comparison. Both
|
||||||
|
Launcher sessions completed automatic rollback to `RolledBack`.
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Ortho viewport membership
|
||||||
|
|
||||||
|
## Runtime path
|
||||||
|
|
||||||
|
`EntryPoint.RunDiagnostic` reads each `OrthoViewportInfo` and calls
|
||||||
|
`PlantObjectReader.ReadObjects`. The reader resolves source Piping rows, then
|
||||||
|
`PlantViewportMembershipReader.Read` evaluates the stored query for that exact
|
||||||
|
view. It maps `MdObjectId.PrjObjectId` to a source DataLinks row and assigns a
|
||||||
|
membership status to every independent `PlantObjectSnapshot`.
|
||||||
|
|
||||||
|
| Status | Meaning | Diagnostic evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Resolved` | The source row was returned by the exact view query. | `VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID` |
|
||||||
|
| `ObjectOutsideView` | The source row was not returned by that confirmed query. | `VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP` |
|
||||||
|
| `NotResolved` | The view contract is unavailable, ambiguous, or failed. | `VIEW_MEMBERSHIP_API_UNSUPPORTED` |
|
||||||
|
|
||||||
|
No source object is treated as visible solely because it belongs to the same
|
||||||
|
model drawing. No geometry-only result is upgraded to `Resolved`.
|
||||||
|
|
||||||
|
## Plan effect
|
||||||
|
|
||||||
|
`AnnotationPlanBuilder` includes membership status in its canonical plan input.
|
||||||
|
`ObjectOutsideView`, unsupported, and unresolved objects are `Skip` operations
|
||||||
|
with stable reason codes. `Resolved` membership can progress only through the
|
||||||
|
existing annotation rules; this DryRun slice never writes an annotation.
|
||||||
|
|
||||||
|
## Paired DryRun evidence
|
||||||
|
|
||||||
|
The paired local DryRun resolved five of seven source rows through the exact
|
||||||
|
view query and classified the remaining two as `ObjectOutsideView`. Each of
|
||||||
|
the three supported commands produced an identical PlanId on its repeated
|
||||||
|
invocation. Both Launcher sessions reached `RolledBack`.
|
||||||
|
|
||||||
|
The paired command-window DBMOD output was not retained. It is intentionally
|
||||||
|
reported as `NOT RECORDED`; project-file SHA-256 inventories nevertheless had
|
||||||
|
zero delta for both CONTROL and DRYRUN copies.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Ortho viewport membership API
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This report records the Plant 3D 2026 contract used to map source-model
|
||||||
|
DataLinks objects to one Ortho view. Autodesk binaries were inspected in
|
||||||
|
`<PLANT3D_2026_INSTALL>` and are not part of this repository.
|
||||||
|
|
||||||
|
## Confirmed Plant 3D 2026 contract
|
||||||
|
|
||||||
|
| Assembly | Version | Type/member | Purpose |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `IDwgViewDef.DwgQuery` | Returns the query stored by an Ortho view. |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `DwgQueryByBox.Execute(StringCollection, StringCollection)` | Executes the stored view selection and returns `IDwgSet`. |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `IDwgSet.GetItem(int, ref MdObjectId)` | Enumerates selected source identities. |
|
||||||
|
| `PnPDwg2dUtil` | `0.0.0.0` | `MdObjectId.PrjObjectId` | Exposes the selected project object identity. |
|
||||||
|
| `PnPDataLinks` | `17.0.60.1` | `DataLinksManager.FindAcPpRowId(PpObjectId)` | Maps a selected project object to its DataLinks row. |
|
||||||
|
|
||||||
|
`DwgQueryByBox` also exposes `CoordinateSystem`, `XLength`, `YLength`,
|
||||||
|
`ZLength`, and `SelectionMask`. `DwgViewDef` exposes orientation, clipping,
|
||||||
|
and layout fields. They are not used as a heuristic in this slice because the
|
||||||
|
query result provides a stronger project-object identity.
|
||||||
|
|
||||||
|
## Selected implementation
|
||||||
|
|
||||||
|
`PlantViewportMembershipReader.Read` locates exactly one view by the stable
|
||||||
|
ID already generated from the sheet GUID and view name. It executes that
|
||||||
|
view's stored `DwgQuery`, converts every returned `MdObjectId.PrjObjectId`
|
||||||
|
through `DataLinksManager.FindAcPpRowId`, and intersects the result with the
|
||||||
|
source drawing's Piping rows.
|
||||||
|
|
||||||
|
Matching rows receive `Resolved` with
|
||||||
|
`VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID`. Source rows absent from the confirmed
|
||||||
|
query result receive `ObjectOutsideView` with
|
||||||
|
`VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP`. An unsupported or ambiguous runtime
|
||||||
|
contract stays `NotResolved` with `VIEW_MEMBERSHIP_API_UNSUPPORTED`; the code
|
||||||
|
does not guess alternate Autodesk properties or select an arbitrary view.
|
||||||
|
|
||||||
|
## Legacy analysis
|
||||||
|
|
||||||
|
The read-only legacy source at
|
||||||
|
`\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo` was available. Its
|
||||||
|
`UpdateOrthoDrawings/Commands.cs` configured `DwgQuery.ModelFiles` and called
|
||||||
|
`DwgQuery.Execute`; `OrthoAutoAnnotate/Commands.cs` and
|
||||||
|
`AutoAnnotateView/ViewPorts.cs` also used viewport and entity extents. Those
|
||||||
|
extents checks are a geometric filter, not an authoritative object identity,
|
||||||
|
so they were not ported as `Resolved` membership.
|
||||||
|
|
||||||
|
## Side effects and failure handling
|
||||||
|
|
||||||
|
The reader opens no Autodesk write transaction and calls no persistence API.
|
||||||
|
It does not modify DWG, DCF, or project metadata. `DLException` while mapping
|
||||||
|
an individual generated identity is ignored only for that non-Piping identity;
|
||||||
|
other failures yield the stable API diagnostic and preserve no false positive.
|
||||||
|
Runtime DryRun and paired control/DryRun hash comparison remain required to
|
||||||
|
verify actual no-write behavior.
|
||||||
@@ -58,6 +58,19 @@ public sealed record PlantObjectSnapshot
|
|||||||
public required GeometrySnapshot Geometry { get; init; }
|
public required GeometrySnapshot Geometry { get; init; }
|
||||||
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||||
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<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 SourceObjectViewportMembership
|
||||||
|
{
|
||||||
|
public required string SourceObjectStableKey { get; init; }
|
||||||
|
public required string ViewStableKey { get; init; }
|
||||||
|
public required ViewMembershipStatus Status { get; init; }
|
||||||
|
public IReadOnlyList<string> EvidenceCodes { get; init; } = Array.Empty<string>();
|
||||||
|
public string? ReasonCode { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PlantRuntimeContext
|
public sealed record PlantRuntimeContext
|
||||||
@@ -206,6 +219,7 @@ public sealed record AnnotationIntent
|
|||||||
|
|
||||||
public sealed record AnnotationPlan
|
public sealed record AnnotationPlan
|
||||||
{
|
{
|
||||||
|
public int PlanSchemaVersion { get; init; }
|
||||||
public string PlanId { get; init; } = string.Empty;
|
public string PlanId { get; init; } = string.Empty;
|
||||||
public string CommandName { get; init; } = string.Empty;
|
public string CommandName { get; init; } = string.Empty;
|
||||||
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace PRecAnnot.Core;
|
|||||||
|
|
||||||
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||||
{
|
{
|
||||||
|
public const int CurrentPlanSchemaVersion = 1;
|
||||||
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||||
{
|
{
|
||||||
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
@@ -30,6 +31,12 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
continue;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
@@ -84,7 +91,8 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
|
|
||||||
return new AnnotationPlan
|
return new AnnotationPlan
|
||||||
{
|
{
|
||||||
PlanId = BuildPlanId(request, objects),
|
PlanSchemaVersion = CurrentPlanSchemaVersion,
|
||||||
|
PlanId = BuildPlanId(request, objects, operations),
|
||||||
CommandName = request.CommandName,
|
CommandName = request.CommandName,
|
||||||
Operations = operations,
|
Operations = operations,
|
||||||
Diagnostics = diagnostics,
|
Diagnostics = diagnostics,
|
||||||
@@ -92,18 +100,39 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
Create = creates,
|
Create = creates,
|
||||||
Update = updates,
|
Update = updates,
|
||||||
Delete = Array.Empty<string>(),
|
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,
|
ExaminedCount = objects.Length,
|
||||||
IsDeterministic = true
|
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);
|
var canonical = new StringBuilder("schema=").Append(CurrentPlanSchemaVersion)
|
||||||
foreach (var item in objects)
|
.Append("|command=").Append(Normalize(request.CommandName))
|
||||||
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
|
.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()));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||||
return "plan-" + Convert.ToHexString(hash)[..24];
|
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string? value) => (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormC);
|
||||||
|
|
||||||
|
private static string MembershipReason(ViewMembershipStatus status) => status switch
|
||||||
|
{
|
||||||
|
ViewMembershipStatus.ObjectOutsideView => "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP",
|
||||||
|
ViewMembershipStatus.Unsupported => "VIEW_MEMBERSHIP_API_UNSUPPORTED",
|
||||||
|
ViewMembershipStatus.Failed => "VIEW_MEMBERSHIP_TRANSFORM_FAILED",
|
||||||
|
_ => "VIEW_MEMBERSHIP_NOT_RESOLVED"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
|
|||||||
ObjectClass = item.ClassName,
|
ObjectClass = item.ClassName,
|
||||||
Tag = item.Tag,
|
Tag = item.Tag,
|
||||||
LineNumber = item.LineNumber,
|
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();
|
}).ToArray();
|
||||||
|
|
||||||
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
|
|
||||||
var builder = new AnnotationPlanBuilder();
|
var builder = new AnnotationPlanBuilder();
|
||||||
var plans = new List<AnnotationPlan>();
|
var plans = new List<AnnotationPlan>();
|
||||||
|
var objectsByViewport = new List<IReadOnlyList<PlantObjectSnapshot>>();
|
||||||
foreach (var viewport in context.Viewports)
|
foreach (var viewport in context.Viewports)
|
||||||
{
|
{
|
||||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
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 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,18 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
||||||
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
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 membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||||
|
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||||
|
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||||
|
details["PlanId"] = planId;
|
||||||
|
details["MembershipResolved"] = membershipResolved;
|
||||||
|
details["MembershipPartial"] = membershipPartial;
|
||||||
|
details["MembershipUnresolved"] = membershipUnresolved;
|
||||||
|
details["MembershipOutsideView"] = membershipOutside;
|
||||||
details["PlanCreateCount"] = summary.CreateCount;
|
details["PlanCreateCount"] = summary.CreateCount;
|
||||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||||
details["PlanSkipCount"] = summary.SkipCount;
|
details["PlanSkipCount"] = summary.SkipCount;
|
||||||
@@ -103,6 +116,7 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
details["ErrorCount"] = summary.ErrorCount;
|
details["ErrorCount"] = summary.ErrorCount;
|
||||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
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, $"\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);
|
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
using Autodesk.AutoCAD.DatabaseServices;
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
@@ -279,6 +280,8 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
return Array.Empty<PlantObjectSnapshot>();
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var membership = PlantViewportMembershipReader.Read(viewport, dataLinks, rowIds);
|
||||||
|
|
||||||
foreach (var rowId in rowIds)
|
foreach (var rowId in rowIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -299,7 +302,9 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
||||||
Geometry = new GeometrySnapshot(),
|
Geometry = new GeometrySnapshot(),
|
||||||
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
||||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" },
|
||||||
|
MembershipStatus = membership.StatusFor(rowId),
|
||||||
|
MembershipEvidenceCodes = membership.EvidenceFor(rowId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -381,6 +386,88 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class PlantViewportMembershipReader
|
||||||
|
{
|
||||||
|
internal readonly record struct MembershipReadResult(
|
||||||
|
IReadOnlySet<int> SelectedRows,
|
||||||
|
ViewMembershipStatus DefaultStatus,
|
||||||
|
string DefaultCode)
|
||||||
|
{
|
||||||
|
public ViewMembershipStatus StatusFor(int rowId) =>
|
||||||
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||||
|
? ViewMembershipStatus.ObjectOutsideView
|
||||||
|
: DefaultStatus;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> EvidenceFor(int rowId) =>
|
||||||
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||||
|
? new[] { "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP" }
|
||||||
|
: new[] { DefaultCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MembershipReadResult Read(OrthoViewportInfo viewport, DataLinksManager dataLinks, Autodesk.ProcessPower.DataObjects.PnPRowIdArray sourceRows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject;
|
||||||
|
if (project is null) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
|
||||||
|
var candidates = project.DwgSheetDefinitions.AsArray()
|
||||||
|
.SelectMany(sheet => sheet.DwgViewDefs.AsArray()
|
||||||
|
.Where(view => string.Equals(StableId(sheet.DwgGuid.ToString(), view.ViewName), viewport.Id, StringComparison.Ordinal))
|
||||||
|
.Select(view => (sheet, view)))
|
||||||
|
.ToArray();
|
||||||
|
if (candidates.Length != 1) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
|
||||||
|
var view = candidates[0].view;
|
||||||
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
||||||
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedRows = new HashSet<int>();
|
||||||
|
var selected = view.DwgQuery.Execute(new StringCollection(), new StringCollection());
|
||||||
|
for (var index = 0; index < selected.Count; index++)
|
||||||
|
{
|
||||||
|
Autodesk.ProcessPower.Drawings2d.MdObjectId sourceId = default;
|
||||||
|
if (!selected.GetItem(index, ref sourceId) || sourceId.IsNull || sourceId.PrjObjectId.IsNull) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rowId = dataLinks.FindAcPpRowId(sourceId.PrjObjectId);
|
||||||
|
if (sourceRows.Contains(rowId)) selectedRows.Add(rowId);
|
||||||
|
}
|
||||||
|
catch (DLException)
|
||||||
|
{
|
||||||
|
// A generated entity without a Piping DataLinks row is outside this reader's source set.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "INFO", resultStatus: "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", details: new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["SelectedSourceRows"] = selectedRows.Count,
|
||||||
|
["SourceRows"] = sourceRows.Count
|
||||||
|
});
|
||||||
|
return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "WARNING", resultStatus: "VIEW_MEMBERSHIP_API_UNSUPPORTED", exception: exception);
|
||||||
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MembershipReadResult Unsupported(string code) => new(new HashSet<int>(), ViewMembershipStatus.NotResolved, code);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + (viewName ?? string.Empty)));
|
||||||
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||||
{
|
{
|
||||||
public void EnsureWriteAllowed(string operation)
|
public void EnsureWriteAllowed(string operation)
|
||||||
|
|||||||
@@ -104,6 +104,62 @@ public sealed class FoundationTests
|
|||||||
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
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 ViewportMembership_OutsideClip_IsSkippedWithStableReason()
|
||||||
|
{
|
||||||
|
var plan = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||||
|
Assert.Contains("VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP", plan.SkipReasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ResolvedMembership_AffectsPlanId()
|
||||||
|
{
|
||||||
|
var resolved = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||||
|
var outside = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
Assert.NotEqual(resolved.PlanId, outside.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ReorderedMembership_HasSamePlanId()
|
||||||
|
{
|
||||||
|
var first = Build(
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved },
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
var second = Build(
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView },
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||||
|
Assert.Equal(first.PlanId, second.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||||
{
|
{
|
||||||
var plan = Build(Snapshot("a", "Pipe", null, null));
|
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||||
@@ -202,6 +258,22 @@ public sealed class FoundationTests
|
|||||||
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void ViewportMembership_UsesConfirmedPnpIdentityContract()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("view.DwgQuery.Execute", source);
|
||||||
|
Assert.Contains("sourceId.PrjObjectId", source);
|
||||||
|
Assert.Contains("dataLinks.FindAcPpRowId", source);
|
||||||
|
Assert.Contains("VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ViewportMembership_DoesNotSelectFirstViewport()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("DwgViewDefs.AsArray().First", source);
|
||||||
|
Assert.Contains("candidates.Length != 1", source);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
||||||
{
|
{
|
||||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
||||||
@@ -221,5 +293,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 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