Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b33351117b | ||
|
|
56dcce42f6 | ||
|
|
ea3a9e157e | ||
|
|
837ac1d994 | ||
|
|
142b5f8b93 | ||
|
|
dcf95d7e3a | ||
|
|
c733a27cbb | ||
|
|
9a7a6cd678 |
@@ -1,4 +1,6 @@
|
|||||||
artifacts/
|
artifacts/
|
||||||
|
.local-dotnet-sdk/
|
||||||
|
.local-tools/
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
.vs/
|
.vs/
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<DebugType>portable</DebugType>
|
<DebugType>portable</DebugType>
|
||||||
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == ''">$(ProgramFiles)\Autodesk\AutoCAD 2026</Plant3D2026InstallDir>
|
<AutoCAD2026ManagedDir Condition="'$(AutoCAD2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)</AutoCAD2026ManagedDir>
|
||||||
|
<Plant3D2026ManagedDir Condition="'$(Plant3D2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)\PLNT3D</Plant3D2026ManagedDir>
|
||||||
|
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == '' and '$(AutoCAD2026ManagedDir)' != ''">$(AutoCAD2026ManagedDir)</Plant3D2026InstallDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +1,29 @@
|
|||||||
# Build
|
# Build
|
||||||
|
|
||||||
SDK 8.0.423 is installed. Plant 3D 2026 is not installed on the audit machine, so Autodesk references are currently absent and the adapter is a deliberate blocker. Build Core/tests independently first; after installation pass `Plant3D2026InstallDir` and validate exact assembly/member compatibility. No Autodesk DLL is copied to Git or bundle. `SeriesMin`, `SeriesMax` and the exact platform token are intentionally absent from the manifest until verified against the installed product and official package contract.
|
The workstation has .NET SDK 8.0.423 and Autodesk AutoCAD Plant 3D 2026 installed. The paths are deliberately supplied at build time; no machine-specific Autodesk path is stored in project files.
|
||||||
|
|
||||||
|
Detected assembly inventory:
|
||||||
|
|
||||||
|
| Assembly | Assembly version | Build input |
|
||||||
|
|---|---:|---|
|
||||||
|
| AcMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
|
||||||
|
| AcDbMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
|
||||||
|
| AcCoreMgd.dll | 25.1.0.0 | `AutoCAD2026ManagedDir` |
|
||||||
|
| AdWindows.dll | 5.2.1.1 | `AutoCAD2026ManagedDir` |
|
||||||
|
| PnP3dObjectsMgd.dll | 17.0.60.1 | `Plant3D2026ManagedDir` |
|
||||||
|
| PnPProjectManagerMgd.dll | 17.0.60.1 | `Plant3D2026ManagedDir` |
|
||||||
|
|
||||||
|
The actual build used:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet restore .\Plant3D2026\P_Rec_Annot\PRecAnnot2026.sln
|
||||||
|
dotnet build .\Plant3D2026\P_Rec_Annot\PRecAnnot2026.sln --configuration Release `
|
||||||
|
/p:AutoCAD2026ManagedDir="<AutoCAD managed directory>" `
|
||||||
|
/p:Plant3D2026ManagedDir="<Plant 3D managed directory>"
|
||||||
|
```
|
||||||
|
|
||||||
|
`PRecAnnot.Plant3D2026` now compiles against the real AutoCAD/Plant 3D 2026 metadata. The adapter has real `IExtensionApplication`, `CommandMethod`, `PlantApplication.CurrentProject`, `Document`, `Editor`, and `Autodesk.Windows` Ribbon references. The previous conditional foundation-only adapter was removed. Autodesk references are `Private=false` and `CopyLocal=false`.
|
||||||
|
|
||||||
|
Release emits one non-fatal WindowsBase reference conflict caused by Autodesk's legacy reference surface versus the .NET 8 reference pack. It does not copy Autodesk assemblies to output. Autodesk DLLs are excluded from Git, NuGet, bundle, and publish output.
|
||||||
|
|
||||||
|
The local smoke bundle is created at `Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle`. Deploy is NOT PERFORMED.
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"runtimeExecuted": false,
|
||||||
|
"plantProjectDetected": null,
|
||||||
|
"orthoDetection": "NotExecuted",
|
||||||
|
"viewportCount": null,
|
||||||
|
"dataLinksAvailable": null,
|
||||||
|
"objectsRead": null,
|
||||||
|
"plan": {
|
||||||
|
"create": null,
|
||||||
|
"update": null,
|
||||||
|
"skip": null,
|
||||||
|
"conflict": null,
|
||||||
|
"unsupported": null
|
||||||
|
},
|
||||||
|
"planDeterministic": true,
|
||||||
|
"dbmodBefore": null,
|
||||||
|
"dbmodAfter": null,
|
||||||
|
"dwgModified": null,
|
||||||
|
"dcfModified": null,
|
||||||
|
"projectXmlModified": null,
|
||||||
|
"unhandledExceptions": null,
|
||||||
|
"blocker": "No controlled test Ortho DWG/project path was supplied or discovered; runtime DryRun was not claimed."
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"product": "Autodesk AutoCAD Plant 3D 2026",
|
||||||
|
"manualNetload": true,
|
||||||
|
"bundleLoaded": true,
|
||||||
|
"automaticAppLoaderChecked": false,
|
||||||
|
"smokeModeConfirmedByOperator": true,
|
||||||
|
"observedCommandLogRuntimeMode": "Normal",
|
||||||
|
"assemblyInitializeCountObserved": 1,
|
||||||
|
"commands": {
|
||||||
|
"PHP_REC_ANNOT": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
|
||||||
|
"PXP-AAV": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
|
||||||
|
"PXP-OA": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" }
|
||||||
|
},
|
||||||
|
"ribbon": { "created": true, "duplicateCheckedExhaustively": false },
|
||||||
|
"plantContext": { "projectDetected": true, "projectDisplayName": "DefaultProject", "observedOrthoDetected": false, "dataLinksAvailable": true },
|
||||||
|
"writesBlocked": true,
|
||||||
|
"dbmodChecked": false,
|
||||||
|
"dwgModified": false,
|
||||||
|
"dcfModified": false,
|
||||||
|
"projectXmlModified": false,
|
||||||
|
"telemetryEventsExpected": 3,
|
||||||
|
"telemetryEventsActual": 3,
|
||||||
|
"unhandledExceptions": 0,
|
||||||
|
"temporaryInstallationRemoved": true,
|
||||||
|
"productionDeploy": false
|
||||||
|
}
|
||||||
@@ -1,3 +1,18 @@
|
|||||||
# Migration dashboard
|
# Migration dashboard
|
||||||
|
|
||||||
The active command set is mapped in `command-migration-matrix.md`; all currently published target descriptors are `BLOCKED_WITH_REASON` because Plant 3D 2026 assemblies/runtime are unavailable. Coverage is not reported as a percentage until every active use case has a runtime result. Current status: mapped 3 foundation descriptors, implemented Core planning/registry/telemetry contracts, runtime verified 0, native replacements approved 0, retirements approved 0, blockers 3.
|
The first real Plant 3D 2026 build is complete against the workstation assemblies (AutoCAD managed 25.1.0.0; Plant managed 17.0.60.1; AdWindows 5.2.1.1). Core remains Autodesk-free.
|
||||||
|
|
||||||
|
| Area | Status |
|
||||||
|
|---|---|
|
||||||
|
| `PHP_REC_ANNOT` | Registered real `CommandMethod`; primary diagnostic entry point |
|
||||||
|
| `PXP-AAV` | Registered real `CommandMethod`; diagnostic alias |
|
||||||
|
| `PXP-OA` | Registered real `CommandMethod`; diagnostic alias |
|
||||||
|
| Ribbon | Implemented with `Autodesk.Windows`; idempotent tab/panel guard |
|
||||||
|
| Adapter | Compiled against real 2026 assemblies |
|
||||||
|
| Bundle | Local smoke package built and checked for Autodesk DLL exclusion |
|
||||||
|
| Runtime smoke | Manual NETLOAD confirmed on Plant 3D 2026; automatic AppLoader and Ortho DryRun remain unverified |
|
||||||
|
| Ortho read path | Compiled against `OrthoDocUtils`, `OrthoProject`, `DwgSheetDefinitions`, `DwgViewDefs`, and `DataLinksManager`; runtime fixture pending |
|
||||||
|
| Dry-run plan | Deterministic Core builder with DTO boundary and Apply guard; no annotation writes |
|
||||||
|
| Production deploy | NOT PERFORMED |
|
||||||
|
|
||||||
|
Fixed API assumptions: foundation-only lifecycle became `IExtensionApplication`; command descriptors became actual `CommandMethod` registrations; Ortho detection uses `OrthoDocUtils.IsValidOrthoDrawing` plus the Ortho project part; viewport metadata uses `DwgSheetDefinitions`/`DwgViewDefs`; object properties use read-only `DataLinksManager.SelectAcPpRowIds` and `GetAllProperties`; Core receives only immutable DTOs. Remaining blockers are a controlled Ortho fixture, geometry/object-to-viewport mapping, legacy source provenance, and future Apply implementation.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# PXP-AAV read-path analysis
|
||||||
|
|
||||||
|
Status: traced to the existing command registration and the shared dry-run plan path; annotation writes are not implemented in this slice.
|
||||||
|
|
||||||
|
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| PXP-AAV-001 | PXP-AAV | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is recorded, but the requested legacy checkout is absent on this workstation | read objects visible to an Ortho view and build a dry-run plan | adapter DTO -> `AnnotationPlan` | `AnnotationPlan_InputOrderDoesNotChangeResult` | partial |
|
||||||
|
|
||||||
|
The implementation deliberately does not claim behavioral equivalence. The next required input is a read-only copy of the legacy source or a golden test Ortho drawing with expected diagnostics.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# PXP-OA read-path analysis
|
||||||
|
|
||||||
|
Status: command is a real `CommandMethod` and uses the same safe context/read-plan pipeline. No Ortho annotation is created, updated, or deleted.
|
||||||
|
|
||||||
|
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|
||||||
|
|---|---|---|---|---|---|---|---|---|
|
||||||
|
| PXP-OA-001 | PXP-OA | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is available, but source checkout was not found locally | Ortho context and DataLinks read diagnostics | adapter DTO -> `AnnotationPlan` | `ApplyMode_IsBlocked` | partial |
|
||||||
|
|
||||||
|
Exact object filters, annotation text, placement rules, and existing-annotation detection remain unconfirmed until the legacy source or a controlled golden fixture is supplied.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Plant 3D 2026 runtime smoke results
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The smoke test used a temporary per-user bundle installation only. Production deploy was not performed. The temporary installation was removed after AutoCAD closed.
|
||||||
|
|
||||||
|
## Confirmed
|
||||||
|
|
||||||
|
- Manual `NETLOAD` loaded the P_Rec_Annot entry assembly in AutoCAD Plant 3D 2026.
|
||||||
|
- `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA` were accepted by the command line.
|
||||||
|
- The active Plant project was detected as `DefaultProject`.
|
||||||
|
- The user confirmed Smoke mode behavior and no DWG mutation during the manual smoke interaction.
|
||||||
|
- The runtime log contains one assembly initialization and one normal termination for the observed process.
|
||||||
|
- The runtime log records Ribbon creation with status `Created`.
|
||||||
|
- The runtime log records exactly one `CommandUsageEvent` for each of the three commands.
|
||||||
|
- The observed Plant context reported `DataLinksAvailable=true` and `IsOrthoDrawing=false` for `Floor Plan Sample.dwg`.
|
||||||
|
- No unhandled exception was reported in the captured runtime log.
|
||||||
|
|
||||||
|
## Not confirmed
|
||||||
|
|
||||||
|
- Automatic AppLoader loading without `NETLOAD` was not tested.
|
||||||
|
- DBMOD and byte-level DWG/DCF/Project.xml comparisons were not captured by the smoke evidence.
|
||||||
|
- Ribbon duplicate behavior across workspace/document lifecycle was not exhaustively tested.
|
||||||
|
- A command invocation logged as `RuntimeMode=Smoke` is not present in the captured JSONL; the three detailed command records have `RuntimeMode=Normal`.
|
||||||
|
|
||||||
|
## Evidence handling
|
||||||
|
|
||||||
|
The detailed runtime JSONL remains local under `%LOCALAPPDATA%\Promhimproekt\PRecAnnot2026\Logs\` and is not committed. No DWG, DCF, Project.xml, Autodesk DLL, user identity, project path, or full local runtime path is included in the repository.
|
||||||
|
|
||||||
|
Temporary installation: PERFORMED AND REMOVED.
|
||||||
|
Production deploy: NOT PERFORMED.
|
||||||
@@ -1,3 +1,19 @@
|
|||||||
# Runtime test plan
|
# Runtime test plan
|
||||||
|
|
||||||
On a controlled Plant 3D 2026 test machine: install the bundle outside production, start Plant, verify exactly one Ribbon tab/panel, open a test project and Ortho DWG, run `PHP_REC_ANNOT`, inspect dry-run counts, confirm one transaction, save/reopen/audit, repeat command, test no duplicates and Undo, then compare entities/text/layers/positions/links with the 2019 golden sample. This plan is prepared but not executed.
|
Runtime smoke is ready but has not been executed in AutoCAD. The local package is `Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle`.
|
||||||
|
|
||||||
|
Next-step installation command (intentionally not executed):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item -Recurse -Force .\Plant3D2026\P_Rec_Annot\artifacts\P_Rec_Annot.bundle `
|
||||||
|
"$env:APPDATA\Autodesk\ApplicationPlugins\P_Rec_Annot.bundle"
|
||||||
|
```
|
||||||
|
|
||||||
|
In a controlled Plant 3D 2026 session, verify that the entry assembly loads, the `Промхимпроект` tab and `Аннотации` panel occur once, and the `Аннотировать` button invokes `PHP_REC_ANNOT`. Open a Plant project and active DWG, run all three commands, verify the project guard and diagnostic-only behavior, and confirm no unhandled exception. Repeat initialization and confirm no Ribbon duplicate. DWG mutation, Ortho viewport/data-link behavior, undo, repeat-run idempotence, and golden-DWG comparison remain runtime blockers for the implementation wave.
|
||||||
|
|
||||||
|
Deploy status: NOT PERFORMED.
|
||||||
|
# Current slice: Ortho read / dry-run
|
||||||
|
|
||||||
|
`P_REC_ANNOT_RUNTIME_MODE=Smoke` reads context only. `DryRun` reads Ortho view metadata and DataLinks properties, builds an immutable deterministic `AnnotationPlan`, and never applies it. `Apply` is blocked with `APPLY_MODE_NOT_IMPLEMENTED`; unknown values resolve to `DryRun`.
|
||||||
|
|
||||||
|
The read adapter uses `OrthoDocUtils.IsValidOrthoDrawing`, `OrthoProject.DwgSheetDefinitions`, `IDwgViewDef`, and read-only `DataLinksManager` methods. The adapter does not expose AutoCAD or Plant objects to Core and contains no write calls. A runtime DryRun result is not published until a controlled test Ortho DWG is available; see `docs/evidence/ortho-dry-run-summary.json`.
|
||||||
|
|||||||
@@ -1,5 +1,117 @@
|
|||||||
namespace PRecAnnot.Abstractions;
|
namespace PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
public sealed record RuntimeDiagnostic
|
||||||
|
{
|
||||||
|
public required string Code { get; init; }
|
||||||
|
public required string Message { get; init; }
|
||||||
|
public string Severity { get; init; } = "Warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
|
||||||
|
|
||||||
|
public sealed record OrthoDetectionResult
|
||||||
|
{
|
||||||
|
public required OrthoDetectionStatus Status { get; init; }
|
||||||
|
public required IReadOnlyList<string> Evidence { get; init; }
|
||||||
|
public required IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct Point2Dto(double X, double Y);
|
||||||
|
public readonly record struct Point3Dto(double X, double Y, double Z);
|
||||||
|
public readonly record struct Vector2Dto(double X, double Y);
|
||||||
|
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
|
||||||
|
|
||||||
|
public sealed record GeometrySnapshot
|
||||||
|
{
|
||||||
|
public Point3Dto? WorldPosition { get; init; }
|
||||||
|
public Point2Dto? PaperPosition { get; init; }
|
||||||
|
public Bounds2Dto? ProjectedBounds { get; init; }
|
||||||
|
public double? Rotation { get; init; }
|
||||||
|
public double? Elevation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoViewportInfo
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
public double CenterX { get; init; }
|
||||||
|
public double CenterY { get; init; }
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { get; init; }
|
||||||
|
public double Scale { get; init; }
|
||||||
|
public double TwistAngle { get; init; }
|
||||||
|
public bool IsOn { get; init; }
|
||||||
|
public bool IsLocked { get; init; }
|
||||||
|
public string? SourceDrawingName { get; init; }
|
||||||
|
public string? ViewName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlantObjectSnapshot
|
||||||
|
{
|
||||||
|
public required string SnapshotId { get; init; }
|
||||||
|
public required string ObjectClass { get; init; }
|
||||||
|
public string? Tag { get; init; }
|
||||||
|
public string? LineNumber { get; init; }
|
||||||
|
public string? SupportNumber { get; init; }
|
||||||
|
public string? EquipmentNumber { get; init; }
|
||||||
|
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 sealed record PlantRuntimeContext
|
||||||
|
{
|
||||||
|
public bool HasActiveDocument { get; init; }
|
||||||
|
public bool HasPlantProject { get; init; }
|
||||||
|
public bool IsOrthoDrawing { get; init; }
|
||||||
|
public string? DrawingName { get; init; }
|
||||||
|
public string? ProjectDisplayName { get; init; }
|
||||||
|
public IReadOnlyList<OrthoViewportInfo> Viewports { get; init; } = Array.Empty<OrthoViewportInfo>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty<string>(), Diagnostics = Array.Empty<RuntimeDiagnostic>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
||||||
|
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
||||||
|
|
||||||
|
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
||||||
|
|
||||||
|
public sealed record AnnotationOperationPlan
|
||||||
|
{
|
||||||
|
public required string SourceSnapshotId { get; init; }
|
||||||
|
public required AnnotationOperationKind Kind { get; init; }
|
||||||
|
public string? AnnotationType { get; init; }
|
||||||
|
public string? Text { get; init; }
|
||||||
|
public Point2Dto? Position { get; init; }
|
||||||
|
public double? Rotation { get; init; }
|
||||||
|
public string? ExistingAnnotationId { get; init; }
|
||||||
|
public required IReadOnlyList<string> Reasons { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AnnotationPlanSummary
|
||||||
|
{
|
||||||
|
public int ObjectsExamined { get; init; }
|
||||||
|
public int CreateCount { get; init; }
|
||||||
|
public int UpdateCount { get; init; }
|
||||||
|
public int SkipCount { get; init; }
|
||||||
|
public int ConflictCount { get; init; }
|
||||||
|
public int UnsupportedCount { get; init; }
|
||||||
|
public int WarningCount { get; init; }
|
||||||
|
public int ErrorCount { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
public required string CommandName { get; init; }
|
||||||
|
public required OrthoViewportInfo Viewport { get; init; }
|
||||||
|
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
||||||
|
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
||||||
|
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
||||||
|
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
||||||
|
|
||||||
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
||||||
|
|
||||||
public sealed record PlantObjectData
|
public sealed record PlantObjectData
|
||||||
@@ -36,6 +148,11 @@ public sealed record AnnotationIntent
|
|||||||
|
|
||||||
public sealed record AnnotationPlan
|
public sealed record AnnotationPlan
|
||||||
{
|
{
|
||||||
|
public string PlanId { get; init; } = string.Empty;
|
||||||
|
public string CommandName { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
public AnnotationPlanSummary Summary { get; init; } = new();
|
||||||
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
|
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
|
||||||
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
||||||
public required IReadOnlyList<string> Delete { get; init; }
|
public required IReadOnlyList<string> Delete { get; init; }
|
||||||
@@ -58,7 +175,7 @@ public interface IOrthoDrawingContext
|
|||||||
|
|
||||||
public interface IPlantObjectReader
|
public interface IPlantObjectReader
|
||||||
{
|
{
|
||||||
IReadOnlyList<PlantObjectData> ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken);
|
IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAnnotationRepository
|
public interface IAnnotationRepository
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||||
|
{
|
||||||
|
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||||
|
{
|
||||||
|
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
|
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var operations = new List<AnnotationOperationPlan>(objects.Length);
|
||||||
|
var creates = new List<AnnotationIntent>();
|
||||||
|
var updates = new List<AnnotationIntent>();
|
||||||
|
var diagnostics = new List<RuntimeDiagnostic>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var item in objects)
|
||||||
|
{
|
||||||
|
if (!seen.Add(item.SnapshotId))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Conflict, Reasons = new[] { "duplicate-snapshot-id" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(item.ObjectClass))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "missing-object-class" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "missing-line-number-or-tag" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = item.Geometry.PaperPosition;
|
||||||
|
if (position is null)
|
||||||
|
{
|
||||||
|
diagnostics.Add(new RuntimeDiagnostic { Code = "MISSING_GEOMETRY", Message = $"No paper position for {item.SnapshotId}." });
|
||||||
|
}
|
||||||
|
|
||||||
|
var stableKey = $"{request.Viewport.SourceDrawingName ?? string.Empty}:{request.Viewport.Id}:{item.SnapshotId}";
|
||||||
|
var existingAnnotation = existing.TryGetValue(stableKey, out var match) ? match : existing.TryGetValue(item.SnapshotId, out var directMatch) ? directMatch : null;
|
||||||
|
var kind = existingAnnotation is null ? AnnotationOperationKind.Create : AnnotationOperationKind.Update;
|
||||||
|
operations.Add(new AnnotationOperationPlan
|
||||||
|
{
|
||||||
|
SourceSnapshotId = item.SnapshotId,
|
||||||
|
Kind = kind,
|
||||||
|
AnnotationType = "PlantObject",
|
||||||
|
Text = text,
|
||||||
|
Position = position,
|
||||||
|
Rotation = item.Geometry.Rotation,
|
||||||
|
ExistingAnnotationId = existingAnnotation?.StableKey,
|
||||||
|
Reasons = Array.Empty<string>()
|
||||||
|
});
|
||||||
|
|
||||||
|
var intent = new AnnotationIntent
|
||||||
|
{
|
||||||
|
StableKey = stableKey,
|
||||||
|
Text = text,
|
||||||
|
Position = position is { } point ? new AnnotationPoint(point.X, point.Y) : new AnnotationPoint(0, 0),
|
||||||
|
Rotation = item.Geometry.Rotation ?? 0,
|
||||||
|
ClassName = item.ObjectClass,
|
||||||
|
Layer = "PXP_ANNOTATION"
|
||||||
|
};
|
||||||
|
if (kind == AnnotationOperationKind.Create) creates.Add(intent); else updates.Add(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = new AnnotationPlanSummary
|
||||||
|
{
|
||||||
|
ObjectsExamined = objects.Length,
|
||||||
|
CreateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Create),
|
||||||
|
UpdateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Update),
|
||||||
|
SkipCount = operations.Count(x => x.Kind == AnnotationOperationKind.Skip),
|
||||||
|
ConflictCount = operations.Count(x => x.Kind == AnnotationOperationKind.Conflict),
|
||||||
|
UnsupportedCount = operations.Count(x => x.Kind == AnnotationOperationKind.Unsupported),
|
||||||
|
WarningCount = diagnostics.Count(x => !string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
ErrorCount = diagnostics.Count(x => string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase))
|
||||||
|
};
|
||||||
|
|
||||||
|
return new AnnotationPlan
|
||||||
|
{
|
||||||
|
PlanId = BuildPlanId(request, objects),
|
||||||
|
CommandName = request.CommandName,
|
||||||
|
Operations = operations,
|
||||||
|
Diagnostics = diagnostics,
|
||||||
|
Summary = summary,
|
||||||
|
Create = creates,
|
||||||
|
Update = updates,
|
||||||
|
Delete = Array.Empty<string>(),
|
||||||
|
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
|
||||||
|
ExaminedCount = objects.Length,
|
||||||
|
IsDeterministic = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
|
||||||
|
{
|
||||||
|
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 hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||||
|
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,43 +6,21 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
|
|||||||
{
|
{
|
||||||
public AnnotationPlan BuildPlan(AnnotationRequest request)
|
public AnnotationPlan BuildPlan(AnnotationRequest request)
|
||||||
{
|
{
|
||||||
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
|
var objects = request.Objects.Select(item => new PlantObjectSnapshot
|
||||||
var create = new List<AnnotationIntent>();
|
|
||||||
var update = new List<AnnotationIntent>();
|
|
||||||
var skipped = new List<string>();
|
|
||||||
|
|
||||||
foreach (var item in request.Objects.OrderBy(x => x.ObjectId, StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag))
|
SnapshotId = item.ObjectId,
|
||||||
|
ObjectClass = item.ClassName,
|
||||||
|
Tag = item.Tag,
|
||||||
|
LineNumber = item.LineNumber,
|
||||||
|
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
{
|
{
|
||||||
skipped.Add($"{item.ObjectId}:missing-line-number-or-tag");
|
CommandName = "PHP_REC_ANNOT",
|
||||||
continue;
|
Viewport = new OrthoViewportInfo { Id = request.Viewport.ViewportId, DisplayName = request.Viewport.ViewportId, SourceDrawingName = request.Viewport.DrawingId },
|
||||||
|
Objects = objects,
|
||||||
|
Existing = request.Existing
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var text = BuildText(item);
|
|
||||||
var intent = new AnnotationIntent
|
|
||||||
{
|
|
||||||
StableKey = $"{request.Viewport.DrawingId}:{request.Viewport.ViewportId}:{item.ObjectId}",
|
|
||||||
Text = text,
|
|
||||||
Position = item.Position,
|
|
||||||
Rotation = item.Rotation,
|
|
||||||
ClassName = item.ClassName,
|
|
||||||
Layer = "PXP_ANNOTATION"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existing.ContainsKey(intent.StableKey)) update.Add(intent); else create.Add(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AnnotationPlan
|
|
||||||
{
|
|
||||||
Create = create,
|
|
||||||
Update = update,
|
|
||||||
Delete = Array.Empty<string>(),
|
|
||||||
SkipReasons = skipped,
|
|
||||||
ExaminedCount = request.Objects.Count
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildText(PlantObjectData item) =>
|
|
||||||
string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class ExecutionModeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||||
|
{
|
||||||
|
public void EnsureWriteAllowed(string operation) => throw new InvalidOperationException(mode == PRecAnnotExecutionMode.Apply ? "APPLY_MODE_NOT_IMPLEMENTED" : "SMOKE_WRITE_BLOCKED");
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class OrthoDetectionClassifier
|
||||||
|
{
|
||||||
|
public static OrthoDetectionResult Classify(bool hasPlantProject, bool hasOrthoPart, bool apiConfirmed, bool metadataAmbiguous = false)
|
||||||
|
{
|
||||||
|
if (!hasPlantProject) return Result(OrthoDetectionStatus.NotOrtho, "NO_PLANT_PROJECT", "Active Plant project is absent.");
|
||||||
|
if (metadataAmbiguous) return Result(OrthoDetectionStatus.Ambiguous, "AMBIGUOUS_ORTHO_METADATA", "Plant metadata is insufficient to confirm Ortho.");
|
||||||
|
if (!hasOrthoPart) return Result(OrthoDetectionStatus.Unsupported, "ORTHO_PART_UNAVAILABLE", "Ortho project part is unavailable.");
|
||||||
|
return apiConfirmed
|
||||||
|
? Result(OrthoDetectionStatus.Confirmed, "ORTHO_API_CONFIRMED", "OrthoDocUtils confirmed the active drawing.")
|
||||||
|
: Result(OrthoDetectionStatus.NotOrtho, "ORTHO_API_NEGATIVE", "OrthoDocUtils did not confirm the active drawing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoDetectionResult Result(OrthoDetectionStatus status, string code, string message) => new()
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
Evidence = new[] { code },
|
||||||
|
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } }
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.Runtime;
|
||||||
|
using Autodesk.Windows;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
public sealed class EntryPoint : IExtensionApplication
|
||||||
|
{
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
||||||
|
RibbonIntegration.Register();
|
||||||
|
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Terminate()
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("AssemblyTerminateStarted");
|
||||||
|
RibbonIntegration.Terminate();
|
||||||
|
RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
|
||||||
|
}
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_ANNOT")]
|
||||||
|
public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT");
|
||||||
|
|
||||||
|
[CommandMethod("PXP-AAV")]
|
||||||
|
public void AutoAnnotateViewCommand() => RunDiagnostic("PXP-AAV");
|
||||||
|
|
||||||
|
[CommandMethod("PXP-OA")]
|
||||||
|
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
|
||||||
|
|
||||||
|
private static void RunDiagnostic(string commandName)
|
||||||
|
{
|
||||||
|
var operationId = Guid.NewGuid().ToString("N");
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
Document? document = null;
|
||||||
|
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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?>
|
||||||
|
{
|
||||||
|
["ExecutionMode"] = mode.ToString(),
|
||||||
|
["IsPlantProjectDetected"] = context.HasPlantProject,
|
||||||
|
["IsPlantProjectOpen"] = context.HasPlantProject,
|
||||||
|
["IsOrthoDrawing"] = context.IsOrthoDrawing,
|
||||||
|
["ViewportCount"] = context.Viewports.Count,
|
||||||
|
["DataLinksAvailable"] = DataLinksAvailable(context.HasPlantProject)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document is null || !context.HasPlantProject)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
|
||||||
|
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
|
||||||
|
WriteUsage(operationId, commandName, "PreflightRejected", document, details, stopwatch.ElapsedMilliseconds);
|
||||||
|
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing)
|
||||||
|
{
|
||||||
|
var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho";
|
||||||
|
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
|
||||||
|
WriteEditor(document, $"\nP_Rec_Annot {commandName}: проект '{context.ProjectDisplayName}' найден; тип чертежа: {(context.IsOrthoDrawing ? "Ortho" : "NotOrtho")}; DWG не изменён.");
|
||||||
|
WriteUsage(operationId, commandName, status, document, details, stopwatch.ElapsedMilliseconds);
|
||||||
|
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new AnnotationPlanBuilder();
|
||||||
|
var plans = new List<AnnotationPlan>();
|
||||||
|
foreach (var viewport in context.Viewports)
|
||||||
|
{
|
||||||
|
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||||
|
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = new
|
||||||
|
{
|
||||||
|
ObjectsExamined = plans.Sum(x => x.Summary.ObjectsExamined),
|
||||||
|
CreateCount = plans.Sum(x => x.Summary.CreateCount),
|
||||||
|
UpdateCount = plans.Sum(x => x.Summary.UpdateCount),
|
||||||
|
SkipCount = plans.Sum(x => x.Summary.SkipCount),
|
||||||
|
ConflictCount = plans.Sum(x => x.Summary.ConflictCount),
|
||||||
|
UnsupportedCount = plans.Sum(x => x.Summary.UnsupportedCount),
|
||||||
|
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
||||||
|
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
||||||
|
};
|
||||||
|
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||||
|
details["PlanCreateCount"] = summary.CreateCount;
|
||||||
|
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||||
|
details["PlanSkipCount"] = summary.SkipCount;
|
||||||
|
details["PlanConflictCount"] = summary.ConflictCount;
|
||||||
|
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
||||||
|
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 не изменён.");
|
||||||
|
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||||
|
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
|
||||||
|
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
|
||||||
|
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PRecAnnotExecutionMode ParseMode(string value) =>
|
||||||
|
value.Equals("Smoke", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Smoke :
|
||||||
|
value.Equals("Apply", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Apply : PRecAnnotExecutionMode.DryRun;
|
||||||
|
|
||||||
|
private static bool DataLinksAvailable(bool hasPlantProject)
|
||||||
|
{
|
||||||
|
try { return hasPlantProject && Autodesk.ProcessPower.PlantInstance.PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteUsage(string operationId, string commandName, string status, Document? document, IReadOnlyDictionary<string, object?>? details, long duration, System.Exception? exception = null)
|
||||||
|
{
|
||||||
|
var eventDetails = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0,
|
||||||
|
["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0,
|
||||||
|
["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration
|
||||||
|
};
|
||||||
|
if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value;
|
||||||
|
RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, status, exception, document, eventDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteEditor(Document? document, string message)
|
||||||
|
{
|
||||||
|
try { document?.Editor.WriteMessage(message); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class RibbonIntegration
|
||||||
|
{
|
||||||
|
private const string TabId = "PRecAnnot.Plant3D2026";
|
||||||
|
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
|
||||||
|
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
|
||||||
|
private static bool deferred;
|
||||||
|
|
||||||
|
public static void Register()
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("RibbonInitializationStarted");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ComponentManager.Ribbon is null)
|
||||||
|
{
|
||||||
|
if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; }
|
||||||
|
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RegisterNow();
|
||||||
|
}
|
||||||
|
catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnItemInitialized(object? sender, EventArgs args)
|
||||||
|
{
|
||||||
|
if (ComponentManager.Ribbon is null) return;
|
||||||
|
ComponentManager.ItemInitialized -= OnItemInitialized;
|
||||||
|
deferred = false;
|
||||||
|
Register();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterNow()
|
||||||
|
{
|
||||||
|
var ribbon = ComponentManager.Ribbon!;
|
||||||
|
var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
|
||||||
|
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
|
||||||
|
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
|
||||||
|
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
|
||||||
|
tab.Panels.Add(new RibbonPanel { Source = source });
|
||||||
|
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Terminate()
|
||||||
|
{
|
||||||
|
if (!deferred) return;
|
||||||
|
ComponentManager.ItemInitialized -= OnItemInitialized;
|
||||||
|
deferred = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
namespace PRecAnnot.Plant3D2026;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Autodesk-facing composition-root lifecycle. The concrete IExtensionApplication
|
|
||||||
/// implementation is enabled only after the Plant 3D 2026 SDK reference contract
|
|
||||||
/// is verified on the target installation.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class EntryPointLifecycle : IDisposable
|
|
||||||
{
|
|
||||||
private int _initialized;
|
|
||||||
public bool IsInitialized => Volatile.Read(ref _initialized) == 1;
|
|
||||||
public bool Initialize() => Interlocked.Exchange(ref _initialized, 1) == 0;
|
|
||||||
public void Terminate() => Interlocked.Exchange(ref _initialized, 0);
|
|
||||||
public void Dispose() => Terminate();
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><PlatformTarget>x64</PlatformTarget><UseWindowsForms>true</UseWindowsForms></PropertyGroup>
|
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><PlatformTarget>x64</PlatformTarget><UseWindowsForms>true</UseWindowsForms><EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition></PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" />
|
<ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" />
|
||||||
<ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" />
|
<ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" />
|
||||||
<ProjectReference Include="..\PRecAnnot.Core\PRecAnnot.Core.csproj" />
|
<ProjectReference Include="..\PRecAnnot.Core\PRecAnnot.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Condition="Exists('$(Plant3D2026InstallDir)')">
|
<ItemGroup Condition="Exists('$(AutoCAD2026ManagedDir)') and Exists('$(Plant3D2026ManagedDir)')">
|
||||||
<Reference Include="AcMgd"><HintPath>$(Plant3D2026InstallDir)\AcMgd.dll</HintPath><Private>false</Private></Reference>
|
<Reference Include="AcMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="AcDbMgd"><HintPath>$(Plant3D2026InstallDir)\AcDbMgd.dll</HintPath><Private>false</Private></Reference>
|
<Reference Include="AcDbMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcDbMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="AcCoreMgd"><HintPath>$(Plant3D2026InstallDir)\AcCoreMgd.dll</HintPath><Private>false</Private></Reference>
|
<Reference Include="AcCoreMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcCoreMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026InstallDir)\PLNT3D\PnPProjectManagerMgd.dll</HintPath><Private>false</Private></Reference>
|
<Reference Include="AdWindows"><HintPath>$(AutoCAD2026ManagedDir)\AdWindows.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
<Reference Include="PnPDataLinks"><HintPath>$(Plant3D2026InstallDir)\PLNT3D\PnPDataLinks.dll</HintPath><Private>false</Private></Reference>
|
<Reference Include="PnPDataLinks"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataLinks.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<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="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>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDir
|
|||||||
{
|
{
|
||||||
public static Plant3D2026Availability Detect(string installDirectory)
|
public static Plant3D2026Availability Detect(string installDirectory)
|
||||||
{
|
{
|
||||||
var expected = new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", Path.Combine("PLNT3D", "PnPProjectManagerMgd.dll"), Path.Combine("PLNT3D", "PnPDataLinks.dll") };
|
var expected = new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", "PnPDataLinks.dll", Path.Combine("PLNT3D", "PnPProjectManagerMgd.dll"), Path.Combine("PLNT3D", "PnP3dObjectsMgd.dll") };
|
||||||
var missing = expected.Where(x => !File.Exists(Path.Combine(installDirectory, x))).ToArray();
|
var missing = expected.Where(x => !File.Exists(Path.Combine(installDirectory, x))).ToArray();
|
||||||
return new(missing.Length == 0, installDirectory, missing);
|
return new(missing.Length == 0, installDirectory, missing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
||||||
|
{
|
||||||
|
public PlantRuntimeContext ReadCurrentContext()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null)
|
||||||
|
{
|
||||||
|
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
|
||||||
|
}
|
||||||
|
|
||||||
|
PlantProject? project;
|
||||||
|
try { project = PlantApplication.CurrentProject; }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return Empty(true, false, "PLANT_PROJECT_READ_FAILED", exception.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project is null)
|
||||||
|
{
|
||||||
|
return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден.");
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthoProject? orthoProject;
|
||||||
|
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return Empty(true, true, "ORTHO_PROJECT_PART_READ_FAILED", exception.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orthoProject is null)
|
||||||
|
{
|
||||||
|
return Empty(true, true, "ORTHO_PROJECT_PART_UNAVAILABLE", "Ortho project part недоступен.", OrthoDetectionStatus.Unsupported, "ProjectPart(Ortho)");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string resolvedPath;
|
||||||
|
var isOrtho = OrthoDocUtils.IsValidOrthoDrawing(document.Name, orthoProject, out resolvedPath);
|
||||||
|
if (!isOrtho)
|
||||||
|
{
|
||||||
|
return Empty(true, true, "NOT_ORTHO_DRAWING", "OrthoDocUtils.IsValidOrthoDrawing вернул false.", OrthoDetectionStatus.NotOrtho, "Ortho project part", "OrthoDocUtils.IsValidOrthoDrawing=false");
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewports = new PlantOrthoViewportReader(orthoProject, document.Name).ReadViewports(CancellationToken.None);
|
||||||
|
return new PlantRuntimeContext
|
||||||
|
{
|
||||||
|
HasActiveDocument = true,
|
||||||
|
HasPlantProject = true,
|
||||||
|
IsOrthoDrawing = true,
|
||||||
|
DrawingName = Path.GetFileName(document.Name),
|
||||||
|
ProjectDisplayName = project.Name,
|
||||||
|
Viewports = viewports,
|
||||||
|
Diagnostics = Array.Empty<RuntimeDiagnostic>(),
|
||||||
|
Detection = new OrthoDetectionResult
|
||||||
|
{
|
||||||
|
Status = OrthoDetectionStatus.Confirmed,
|
||||||
|
Evidence = new[] { "ProjectPart(Ortho)", "OrthoDocUtils.IsValidOrthoDrawing=true", "DwgSheetDefinitions" },
|
||||||
|
Diagnostics = Array.Empty<RuntimeDiagnostic>()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
return Empty(true, true, "ORTHO_DETECTION_FAILED", exception.GetType().Name, OrthoDetectionStatus.Failed, "ProjectPart(Ortho)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlantRuntimeContext Empty(bool hasDocument, bool hasProject, string code, string message, OrthoDetectionStatus status = OrthoDetectionStatus.NotOrtho, params string[] evidence) => new()
|
||||||
|
{
|
||||||
|
HasActiveDocument = hasDocument,
|
||||||
|
HasPlantProject = hasProject,
|
||||||
|
IsOrthoDrawing = false,
|
||||||
|
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
|
||||||
|
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
||||||
|
{
|
||||||
|
private readonly OrthoProject project;
|
||||||
|
private readonly string drawingName;
|
||||||
|
|
||||||
|
public PlantOrthoViewportReader(OrthoProject project, string drawingName)
|
||||||
|
{
|
||||||
|
this.project = project;
|
||||||
|
this.drawingName = Path.GetFileName(drawingName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new List<OrthoViewportInfo>();
|
||||||
|
foreach (var sheet in project.DwgSheetDefinitions.AsArray())
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
if (!string.Equals(Path.GetFileName(sheet.DwgName), drawingName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
|
||||||
|
foreach (var view in sheet.DwgViewDefs.AsArray())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var id = StableId(sheet.DwgGuid.ToString(), view.ViewName);
|
||||||
|
result.Add(new OrthoViewportInfo
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
DisplayName = view.ViewName,
|
||||||
|
CenterX = view.CenterPoint.X,
|
||||||
|
CenterY = view.CenterPoint.Y,
|
||||||
|
Width = view.ViewWidth,
|
||||||
|
Height = view.ViewHeight,
|
||||||
|
Scale = view.ViewScale,
|
||||||
|
TwistAngle = 0,
|
||||||
|
IsOn = true,
|
||||||
|
IsLocked = false,
|
||||||
|
SourceDrawingName = sheet.DwgName,
|
||||||
|
ViewName = view.ViewName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// One malformed view must not abort the remaining read-only analysis.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.OrderByDescending(x => x.CenterY).ThenBy(x => x.CenterX).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StableId(string sheetId, string viewName)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
||||||
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class PlantObjectReader : IPlantObjectReader
|
||||||
|
{
|
||||||
|
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document 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>();
|
||||||
|
|
||||||
|
var snapshots = new List<PlantObjectSnapshot>();
|
||||||
|
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var className = dataLinks.GetObjectClassname(rowId);
|
||||||
|
var properties = dataLinks.GetAllProperties(rowId, true)
|
||||||
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
||||||
|
snapshots.Add(new PlantObjectSnapshot
|
||||||
|
{
|
||||||
|
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
|
||||||
|
ObjectClass = className ?? string.Empty,
|
||||||
|
Tag = Value("Tag", "TAG"),
|
||||||
|
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||||
|
SupportNumber = Value("Support Number", "SupportNumber"),
|
||||||
|
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
||||||
|
Geometry = new GeometrySnapshot(),
|
||||||
|
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
||||||
|
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep the read path best-effort; no write operation is attempted.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StableId(string drawingName, int rowId, string? className)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
||||||
|
return "obj-" + Convert.ToHexString(hash)[..24];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||||
|
{
|
||||||
|
public void EnsureWriteAllowed(string operation)
|
||||||
|
{
|
||||||
|
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
||||||
|
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class RuntimeDiagnostics
|
||||||
|
{
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static readonly string LogDirectory = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Promhimproekt", "PRecAnnot2026", "Logs");
|
||||||
|
|
||||||
|
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
||||||
|
|
||||||
|
public static void Write(
|
||||||
|
string eventType,
|
||||||
|
string level = "INFO",
|
||||||
|
string? operationId = null,
|
||||||
|
string? commandName = null,
|
||||||
|
string? resultStatus = null,
|
||||||
|
System.Exception? exception = null,
|
||||||
|
Document? document = null,
|
||||||
|
IReadOnlyDictionary<string, object?>? details = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["Timestamp"] = DateTimeOffset.UtcNow,
|
||||||
|
["Level"] = level,
|
||||||
|
["OperationId"] = operationId,
|
||||||
|
["EventType"] = eventType,
|
||||||
|
["AssemblyVersion"] = typeof(EntryPoint).Assembly.GetName().Version?.ToString(),
|
||||||
|
["AutoCADVersion"] = typeof(Document).Assembly.GetName().Version?.ToString(),
|
||||||
|
["PlantVersion"] = typeof(PlantApplication).Assembly.GetName().Version?.ToString(),
|
||||||
|
["ProcessId"] = Environment.ProcessId,
|
||||||
|
["ThreadId"] = Environment.CurrentManagedThreadId,
|
||||||
|
["ApartmentState"] = Thread.CurrentThread.GetApartmentState().ToString(),
|
||||||
|
["ActiveDocumentName"] = SafeDocumentName(document),
|
||||||
|
["CommandName"] = commandName,
|
||||||
|
["ResultStatus"] = resultStatus,
|
||||||
|
["ExceptionType"] = exception?.GetType().FullName,
|
||||||
|
["ExceptionMessage"] = exception?.Message,
|
||||||
|
["StackTrace"] = level.Equals("ERROR", StringComparison.OrdinalIgnoreCase) ? exception?.StackTrace : null,
|
||||||
|
["RuntimeMode"] = RuntimeMode
|
||||||
|
};
|
||||||
|
|
||||||
|
if (details is not null)
|
||||||
|
foreach (var item in details) payload[item.Key] = item.Value;
|
||||||
|
|
||||||
|
var line = JsonSerializer.Serialize(payload) + Environment.NewLine;
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(LogDirectory);
|
||||||
|
File.AppendAllText(Path.Combine(LogDirectory, "runtime.jsonl"), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Runtime diagnostics must never prevent AutoCAD command execution.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? SafeDocumentName(Document? document)
|
||||||
|
{
|
||||||
|
try { return document is null ? null : Path.GetFileName(document.Name); }
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SmokeWriteBlockedException() : InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||||
|
|
||||||
|
internal static class SmokeGuard
|
||||||
|
{
|
||||||
|
public static bool IsSmoke => string.Equals(RuntimeDiagnostics.RuntimeMode, "Smoke", StringComparison.OrdinalIgnoreCase);
|
||||||
|
public static void EnsureReadOnly() { if (IsSmoke) throw new SmokeWriteBlockedException(); }
|
||||||
|
}
|
||||||
@@ -7,6 +7,16 @@ namespace PRecAnnot.Core.Tests;
|
|||||||
|
|
||||||
public sealed class FoundationTests
|
public sealed class FoundationTests
|
||||||
{
|
{
|
||||||
|
private static string ProjectRoot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "PRecAnnot2026.sln"))) directory = directory.Parent;
|
||||||
|
return directory?.FullName ?? throw new DirectoryNotFoundException("PRecAnnot2026.sln was not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void Plan_IsDeterministic()
|
[Fact] public void Plan_IsDeterministic()
|
||||||
{
|
{
|
||||||
var engine = new AnnotationRuleEngine();
|
var engine = new AnnotationRuleEngine();
|
||||||
@@ -34,4 +44,103 @@ public sealed class FoundationTests
|
|||||||
var ribbon = new RibbonRegistration(); var registry = CommandRegistry.Foundation();
|
var ribbon = new RibbonRegistration(); var registry = CommandRegistry.Foundation();
|
||||||
Assert.True(ribbon.Register(registry)); Assert.False(ribbon.Register(registry)); Assert.Equal(3, ribbon.RegisteredCount);
|
Assert.True(ribbon.Register(registry)); Assert.False(ribbon.Register(registry)); Assert.Equal(3, ribbon.RegisteredCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void PlantAdapter_LoadsAgainst2026Assemblies()
|
||||||
|
{
|
||||||
|
var adapter = Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "bin", "Release", "net8.0-windows", "PRecAnnot.Plant3D2026.dll");
|
||||||
|
var assembly = System.Reflection.AssemblyName.GetAssemblyName(adapter);
|
||||||
|
var project = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PRecAnnot.Plant3D2026.csproj"));
|
||||||
|
Assert.NotNull(assembly);
|
||||||
|
Assert.Contains("AcMgd", project);
|
||||||
|
Assert.Contains("PnPProjectManagerMgd", project);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void CommandClass_HasCommandMethodAttributes()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs"));
|
||||||
|
Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", source);
|
||||||
|
Assert.Contains("[CommandMethod(\"PXP-AAV\")]", source);
|
||||||
|
Assert.Contains("[CommandMethod(\"PXP-OA\")]", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void PrimaryCommand_IsRegistered() => Assert.Contains("[CommandMethod(\"PHP_REC_ANNOT\")]", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
|
[Fact] public void LegacyAliases_AreRegistered() => Assert.True(File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-AAV") && File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")).Contains("PXP-OA"));
|
||||||
|
[Fact] public void RibbonCommand_ReferencesRegisteredCommand() => Assert.Contains("PHP_REC_ANNOT ", File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "EntryPoint.cs")));
|
||||||
|
|
||||||
|
[Fact] public void AutodeskReferences_AreNotCopyLocal()
|
||||||
|
{
|
||||||
|
var project = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PRecAnnot.Plant3D2026.csproj"));
|
||||||
|
Assert.DoesNotContain("<Private>true</Private>", project);
|
||||||
|
Assert.DoesNotContain("<CopyLocal>true</CopyLocal>", project);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void Bundle_DoesNotContainAutodeskAssemblies()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(ProjectRoot, "artifacts", "P_Rec_Annot.bundle");
|
||||||
|
if (!Directory.Exists(root)) return;
|
||||||
|
Assert.DoesNotContain(Directory.EnumerateFiles(root, "*.dll", SearchOption.AllDirectories), path => Path.GetFileName(path).StartsWith("PnP", StringComparison.OrdinalIgnoreCase) || new[] { "AcMgd.dll", "AcDbMgd.dll", "AcCoreMgd.dll", "AdWindows.dll" }.Contains(Path.GetFileName(path), StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void Core_HasNoAutodeskReferences()
|
||||||
|
{
|
||||||
|
var references = typeof(AnnotationRequest).Assembly.GetReferencedAssemblies();
|
||||||
|
Assert.DoesNotContain(references, x => x.Name?.StartsWith("Autodesk", StringComparison.OrdinalIgnoreCase) == true || x.Name?.StartsWith("Ac", StringComparison.OrdinalIgnoreCase) == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoDetection_NoPlantProject_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(false, false, false).Status);
|
||||||
|
[Fact] public void OrthoDetection_NormalDwg_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(true, true, false).Status);
|
||||||
|
[Fact] public void OrthoDetection_ConfirmedOrtho_ReturnsConfirmed() => Assert.Equal(OrthoDetectionStatus.Confirmed, OrthoDetectionClassifier.Classify(true, true, true).Status);
|
||||||
|
[Fact] public void OrthoDetection_AmbiguousMetadata_ReturnsAmbiguous() => Assert.Equal(OrthoDetectionStatus.Ambiguous, OrthoDetectionClassifier.Classify(true, true, false, true).Status);
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_InputOrderDoesNotChangeResult()
|
||||||
|
{
|
||||||
|
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
|
||||||
|
var objects = new[] { Snapshot("b", "Pipe", "L-2"), Snapshot("a", "Valve", "V-1") };
|
||||||
|
var builder = new AnnotationPlanBuilder();
|
||||||
|
var first = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects });
|
||||||
|
var second = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects.Reverse().ToArray() });
|
||||||
|
Assert.Equal(first.PlanId, second.PlanId);
|
||||||
|
Assert.Equal(first.Summary, second.Summary);
|
||||||
|
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||||
|
{
|
||||||
|
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||||
|
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_UnsupportedObject_IsMarkedUnsupported()
|
||||||
|
{
|
||||||
|
var plan = Build(Snapshot("a", "", "L-1"));
|
||||||
|
Assert.Equal(AnnotationOperationKind.Unsupported, Assert.Single(plan.Operations).Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ExistingAnnotation_BecomesUpdate()
|
||||||
|
{
|
||||||
|
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
|
||||||
|
var item = Snapshot("a", "Pipe", "L-1");
|
||||||
|
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = new[] { item }, Existing = new[] { new ExistingAnnotation("a", "old", new(0, 0)) } });
|
||||||
|
Assert.Equal(AnnotationOperationKind.Update, Assert.Single(plan.Operations).Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_DuplicateCandidate_BecomesConflict()
|
||||||
|
{
|
||||||
|
var plan = Build(Snapshot("a", "Pipe", "L-1"), Snapshot("a", "Pipe", "L-1"));
|
||||||
|
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
||||||
|
|
||||||
|
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("ForWrite", source);
|
||||||
|
Assert.DoesNotContain("AppendEntity", source);
|
||||||
|
Assert.DoesNotContain("AcceptChanges", source);
|
||||||
|
Assert.DoesNotContain("SetProperties", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
param([ValidateSet('Debug','Release')][string]$Configuration='Release',[string]$Plant3D2026InstallDir)
|
param([ValidateSet('Debug','Release')][string]$Configuration='Release',[Parameter(Mandatory=$true)][string]$AutoCAD2026ManagedDir,[Parameter(Mandatory=$true)][string]$Plant3D2026ManagedDir)
|
||||||
$ErrorActionPreference='Stop'
|
$ErrorActionPreference='Stop'
|
||||||
dotnet build (Join-Path $PSScriptRoot '..\PRecAnnot2026.sln') --configuration $Configuration /p:Plant3D2026InstallDir=$Plant3D2026InstallDir
|
dotnet build (Join-Path $PSScriptRoot '..\PRecAnnot2026.sln') --configuration $Configuration /p:AutoCAD2026ManagedDir=$AutoCAD2026ManagedDir /p:Plant3D2026ManagedDir=$Plant3D2026ManagedDir
|
||||||
|
if($LASTEXITCODE -ne 0){ throw "Build failed: $LASTEXITCODE" }
|
||||||
|
$bundleRoot=Join-Path $PSScriptRoot '..\artifacts\P_Rec_Annot.bundle'
|
||||||
|
$contents=Join-Path $bundleRoot 'Contents\Windows'
|
||||||
|
New-Item -ItemType Directory -Force -Path $contents | Out-Null
|
||||||
|
$output=Join-Path $PSScriptRoot "..\src\PRecAnnot.Plant3D2026\bin\$Configuration\net8.0-windows"
|
||||||
|
Get-ChildItem $output -File -Filter '*.dll' | Copy-Item -Destination $contents -Force
|
||||||
|
Copy-Item (Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\PackageContents.xml') $bundleRoot -Force
|
||||||
Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.'
|
Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.'
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
param([string]$BundleRoot=(Join-Path $PSScriptRoot '..\artifacts\P_Rec_Annot.bundle'))
|
||||||
$ErrorActionPreference='Stop'
|
$ErrorActionPreference='Stop'
|
||||||
$package=Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\PackageContents.xml'
|
$package=Join-Path $BundleRoot 'PackageContents.xml'
|
||||||
[xml]$xml=Get-Content -LiteralPath $package
|
[xml]$xml=Get-Content -LiteralPath $package
|
||||||
$entry=$xml.ApplicationPackage.Components.ComponentEntry
|
$entry=$xml.ApplicationPackage.Components.ComponentEntry
|
||||||
if(@($entry).Count -ne 1){throw 'Bundle must have exactly one entry assembly.'}
|
if(@($entry).Count -ne 1){throw 'Bundle must have exactly one entry assembly.'}
|
||||||
if($entry.ModuleName -match '_netload|OrthoLoader|Settings.xml'){throw 'Legacy loading chain is forbidden.'}
|
if($entry.ModuleName -match '_netload|OrthoLoader|Settings.xml'){throw 'Legacy loading chain is forbidden.'}
|
||||||
if($entry.ModuleName -notmatch 'PRecAnnot\.Plant3D2026\.dll'){throw 'Unexpected entry assembly.'}
|
if($entry.ModuleName -notmatch 'PRecAnnot\.Plant3D2026\.dll'){throw 'Unexpected entry assembly.'}
|
||||||
|
if(-not (Test-Path (Join-Path $BundleRoot 'Contents\Windows\PRecAnnot.Plant3D2026.dll'))){throw 'Entry assembly is missing.'}
|
||||||
|
$forbidden='^(AcMgd|AcDbMgd|AcCoreMgd|AdWindows|PnP.*)\.dll$'
|
||||||
|
$forbiddenFiles=Get-ChildItem $BundleRoot -Recurse -File | Where-Object { $_.Name -match $forbidden -or $_.Extension -in '.pdb','.cs' -or $_.FullName -match '\\(bin|obj)\\' }
|
||||||
|
if($forbiddenFiles){throw "Bundle contains forbidden files: $($forbiddenFiles.FullName -join ', ')"}
|
||||||
Write-Host 'Bundle manifest is structurally valid.'
|
Write-Host 'Bundle manifest is structurally valid.'
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
param(
|
||||||
|
[string]$DeployedRoot = '\\phpsrv03\NBIM\save\Apps\Plant3D\DriveA\P_Rec_Annot',
|
||||||
|
[string]$SourceRoot = '\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo',
|
||||||
|
[string]$OutputRoot = (Split-Path -Parent $PSCommandPath)
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-PeArchitecture([string]$Path) {
|
||||||
|
if ([IO.Path]::GetExtension($Path) -notin '.dll', '.exe') { return $null }
|
||||||
|
try {
|
||||||
|
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
||||||
|
try {
|
||||||
|
$reader = [IO.BinaryReader]::new($stream)
|
||||||
|
$stream.Position = 0x3c; $offset = $reader.ReadInt32(); $stream.Position = $offset + 4
|
||||||
|
$machine = $reader.ReadUInt16()
|
||||||
|
switch ($machine) { 0x14c { 'x86' } 0x8664 { 'x64' } 0x1c4 { 'ARM' } default { 'Unknown' } }
|
||||||
|
} finally { $stream.Dispose() }
|
||||||
|
} catch { 'Unreadable' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-AssemblyMetadata([string]$Path) {
|
||||||
|
$result = [ordered]@{ AssemblyName = $null; AssemblyVersion = $null; TargetFramework = $null }
|
||||||
|
if ([IO.Path]::GetExtension($Path) -ne '.dll') { return [pscustomobject]$result }
|
||||||
|
try {
|
||||||
|
$name = [Reflection.AssemblyName]::GetAssemblyName($Path)
|
||||||
|
$result.AssemblyName = $name.Name; $result.AssemblyVersion = $name.Version.ToString()
|
||||||
|
try {
|
||||||
|
$assembly = [Reflection.Assembly]::ReflectionOnlyLoadFrom($Path)
|
||||||
|
$attribute = $assembly.GetCustomAttributesData() | Where-Object { $_.AttributeType.FullName -eq 'System.Runtime.Versioning.TargetFrameworkAttribute' } | Select-Object -First 1
|
||||||
|
if ($attribute) { $result.TargetFramework = [string]$attribute.ConstructorArguments[0].Value }
|
||||||
|
} catch { }
|
||||||
|
} catch { }
|
||||||
|
[pscustomobject]$result
|
||||||
|
}
|
||||||
|
|
||||||
|
$inventory = foreach ($file in Get-ChildItem -LiteralPath $DeployedRoot -Recurse -File) {
|
||||||
|
$hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256
|
||||||
|
$version = try { [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName) } catch { $null }
|
||||||
|
$meta = Get-AssemblyMetadata $file.FullName
|
||||||
|
[pscustomobject]@{
|
||||||
|
RelativePath = $file.FullName.Substring($DeployedRoot.Length).TrimStart('\')
|
||||||
|
Name = $file.Name; Extension = $file.Extension; Length = $file.Length; LastWriteTimeUtc = $file.LastWriteTimeUtc.ToString('o')
|
||||||
|
Sha256 = $hash.Hash; FileVersion = $version.FileVersion; ProductVersion = $version.ProductVersion
|
||||||
|
AssemblyName = $meta.AssemblyName; AssemblyVersion = $meta.AssemblyVersion; TargetFramework = $meta.TargetFramework
|
||||||
|
PeArchitecture = Get-PeArchitecture $file.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$inventory | Sort-Object RelativePath | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-files.csv') -NoTypeInformation -Encoding utf8
|
||||||
|
|
||||||
|
$settings = [xml](Get-Content -LiteralPath (Join-Path $DeployedRoot 'Settings.xml'))
|
||||||
|
$sequence = 0
|
||||||
|
$configuration = foreach ($item in $settings.Main.LoadAssemblies.LoadAssembly) {
|
||||||
|
$sequence++
|
||||||
|
$configured = [string]$item.assemblyname; $resolved = Join-Path $DeployedRoot $configured
|
||||||
|
$entry = $inventory | Where-Object Name -eq $configured | Select-Object -First 1
|
||||||
|
[pscustomobject]@{
|
||||||
|
Sequence = $sequence; ConfigFile = 'Settings.xml'; ConfiguredPath = $configured; ResolvedPath = ('<deployed-root>\' + $configured)
|
||||||
|
Assembly = $entry.AssemblyName; Exists = Test-Path -LiteralPath $resolved; Sha256 = $entry.Sha256
|
||||||
|
LoadMethod = 'acDoc.Editor.Command("._netload", fullPath)'; Required = $true
|
||||||
|
FailureBehavior = 'Outer loader catch stops remaining sequence and shows MessageBox'; Notes = 'Path is combined with OrthoLoader assembly directory.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$configuration | Export-Csv -LiteralPath (Join-Path $OutputRoot 'configuration-map.csv') -NoTypeInformation -Encoding utf8
|
||||||
|
|
||||||
|
$projects = Get-ChildItem -LiteralPath $SourceRoot -Recurse -File -Filter *.csproj | ForEach-Object {
|
||||||
|
$content = Get-Content -LiteralPath $_.FullName -Raw
|
||||||
|
$assemblyName = if ($content -match '<AssemblyName>([^<]+)</AssemblyName>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($_.Name) }
|
||||||
|
$framework = if ($content -match '<TargetFrameworkVersion>([^<]+)</TargetFrameworkVersion>') { $Matches[1] } else { $null }
|
||||||
|
$platform = if ($content -match '<PlatformTarget>([^<]+)</PlatformTarget>') { $Matches[1] } else { $null }
|
||||||
|
[pscustomobject]@{ AssemblyName = $assemblyName; Csproj = $_.FullName.Substring($SourceRoot.Length).TrimStart('\'); TargetFramework = $framework; PlatformTarget = $platform }
|
||||||
|
}
|
||||||
|
$sourceMap = foreach ($dll in $inventory | Where-Object Extension -eq '.dll') {
|
||||||
|
$matches = @($projects | Where-Object { $_.AssemblyName -eq $dll.AssemblyName -or $_.AssemblyName -eq [IO.Path]::GetFileNameWithoutExtension($dll.Name) })
|
||||||
|
$releaseCandidate = Join-Path (Join-Path $SourceRoot 'FilesRelease') $dll.Name
|
||||||
|
$exactReleaseMatch = (Test-Path -LiteralPath $releaseCandidate) -and ((Get-FileHash -LiteralPath $releaseCandidate -Algorithm SHA256).Hash -eq $dll.Sha256)
|
||||||
|
$status = if ($exactReleaseMatch) { 'EXACT_SOURCE_MATCH' } elseif ($matches.Count -eq 1) { 'LIKELY_SOURCE_MATCH' } elseif ($matches.Count -gt 1) { 'MULTIPLE_CANDIDATES' } elseif ($dll.Name -match '^PnP|^Ac') { 'AUTODESK_DEPENDENCY' } else { 'NO_SOURCE_FOUND' }
|
||||||
|
$project = $matches | Select-Object -First 1
|
||||||
|
[pscustomobject]@{ DeployedDll=$dll.Name; DeployedSha256=$dll.Sha256; FileVersion=$dll.FileVersion; AssemblyVersion=$dll.AssemblyVersion; LegacyProject=if($project){[IO.Path]::GetFileNameWithoutExtension($project.Csproj)}else{$null}; LegacyCsproj=if($project){$project.Csproj}else{$null}; LegacyOutputName=if($project){$project.AssemblyName}else{$null}; TargetFramework=if($project){$project.TargetFramework}else{$null}; PlatformTarget=if($project){$project.PlatformTarget}else{$null}; CommandCount=$null; RibbonRole=if($dll.AssemblyName -eq 'OrthoLoader'){'Central ribbon/loader'}else{$null}; ConfigurationSource=if($configuration.ConfiguredPath -contains $dll.Name){'Settings.xml'}else{$null}; MatchStatus=$status }
|
||||||
|
}
|
||||||
|
$sourceMap | Sort-Object DeployedDll | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-to-source-map.csv') -NoTypeInformation -Encoding utf8
|
||||||
|
|
||||||
|
"GeneratedUtc=$([DateTime]::UtcNow.ToString('o'))`nDeployedRoot=<network root redacted to report context>`nSourceRoot=<network source root redacted to report context>`nFiles=$($inventory.Count)" | Set-Content -LiteralPath (Join-Path $OutputRoot 'evidence\generation.txt') -Encoding utf8
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Legacy P_Rec_Annot loading audit
|
||||||
|
|
||||||
|
Scope: read-only inspection of the deployed network directory and the v1.0.4 source checkout. No legacy DLL was executed, copied, or deployed.
|
||||||
|
|
||||||
|
The deployed root has 52 files, including 22 managed DLLs. `Settings.xml` enumerates 20 application modules. The inventory and its SHA-256 values are in `deployed-files.csv`; generated report paths use `<deployed-root>` rather than embedding a new runtime configuration path.
|
||||||
|
|
||||||
|
High-confidence conclusion: the central loader is `OrthoLoader.dll`. `OrthoLoader.Ribbon.Initialize()` invokes `OrthoLoader.Initialize()`. The latter reads adjacent `Settings.xml` and calls `acDoc.Editor.Command("._netload", fullPath)` sequentially for the configured modules. The bootstrap that initially loads `OrthoLoader.dll` is not present in either audited directory; it remains unknown. Therefore the audit proves same-directory loading, but cannot prove whether an external launcher first NETLOADed the UNC copy or a local copy.
|
||||||
|
|
||||||
|
Source-to-binary mapping is `EXACT_SOURCE_MATCH` for 15 deployed DLLs by SHA-256 against `FilesRelease`. Six newer deployed DLLs are `LIKELY_SOURCE_MATCH` by assembly/project identity and metadata but differ byte-for-byte from the checked-in release output. `InsertElevation.dll` is `MULTIPLE_CANDIDATES` because archival projects share the assembly identity.
|
||||||
|
|
||||||
|
## Potential test data
|
||||||
|
|
||||||
|
| RelativePath | Type | LikelyPurpose | ContainsProjectData | SafeForTest | Reason |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `Template.dwg` | DWG | legacy command template | Unknown | No | Located beside production deployment; no authorization to open or copy it was established. |
|
||||||
|
| `test.txt` | text | unknown/empty marker | No content | Not useful | Zero bytes. |
|
||||||
|
| `Справка*.pdf/docx` | documentation | user instructions | Unknown | Review only | Not a CAD fixture. |
|
||||||
|
|
||||||
|
No `Project.xml`, `.dcf`, or explicitly labelled sample/demo Ortho DWG was found in the deployed root.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"Sequence","ConfigFile","ConfiguredPath","ResolvedPath","Assembly","Exists","Sha256","LoadMethod","Required","FailureBehavior","Notes"
|
||||||
|
"1","Settings.xml","AnnotateLineNumbers.dll","<deployed-root>\AnnotateLineNumbers.dll","AnnotateLineNumbers","True","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"2","Settings.xml","AutoAnnotateView.dll","<deployed-root>\AutoAnnotateView.dll","AutoAnnotateView","True","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"3","Settings.xml","AutoLinenumberConnections.dll","<deployed-root>\AutoLinenumberConnections.dll","AutoLinenumberConnections","True","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"4","Settings.xml","CheckOrthoView.dll","<deployed-root>\CheckOrthoView.dll","CheckOrthoView","True","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"5","Settings.xml","EnablerLoader.dll","<deployed-root>\EnablerLoader.dll","EnablerLoader","True","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"6","Settings.xml","ExtentsIntersect.dll","<deployed-root>\ExtentsIntersect.dll","ExtentsIntersect","True","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"7","Settings.xml","GetComponentGeometry.dll","<deployed-root>\GetComponentGeometry.dll","GetComponentGeometry","True","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"8","Settings.xml","GetLinenumberAnnotationText.dll","<deployed-root>\GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText","True","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"9","Settings.xml","GetPlantScriptFolderPath.dll","<deployed-root>\GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath","True","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"10","Settings.xml","GetScriptParameters.dll","<deployed-root>\GetScriptParameters.dll","GetScriptParameters","True","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"11","Settings.xml","InsertElevation.dll","<deployed-root>\InsertElevation.dll","InsertElevation","True","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"12","Settings.xml","LoadedAssemblies.dll","<deployed-root>\LoadedAssemblies.dll","LoadedAssemblies","True","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"13","Settings.xml","OrthoAutoAnnotate.dll","<deployed-root>\OrthoAutoAnnotate.dll","OrthoAutoAnnotate","True","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"14","Settings.xml","P3DCopyMissingObjectsToOrthoDrawing.dll","<deployed-root>\P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing","True","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"15","Settings.xml","P3DNewMa.dll","<deployed-root>\P3DNewMa.dll","P3DNewMa","True","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"16","Settings.xml","P3DSetLayerForViewport.dll","<deployed-root>\P3DSetLayerForViewport.dll","P3DSetLayerForViewport","True","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"17","Settings.xml","PHPAnnotations.dll","<deployed-root>\PHPAnnotations.dll","PHPAnnotations","True","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"18","Settings.xml","SwapAttributesMenuItem.dll","<deployed-root>\SwapAttributesMenuItem.dll","SwapAttributesMenuItem","True","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"19","Settings.xml","UpdateOrthoDrawings.dll","<deployed-root>\UpdateOrthoDrawings.dll","UpdateOrthoDrawings","True","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
"20","Settings.xml","UpdateOrthoViewBlocks.dll","<deployed-root>\UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks","True","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
"RelativePath","Name","Extension","Length","LastWriteTimeUtc","Sha256","FileVersion","ProductVersion","AssemblyName","AssemblyVersion","TargetFramework","PeArchitecture"
|
||||||
|
"AnnotateLineNumbers.dll","AnnotateLineNumbers.dll",".dll","126976","2024-12-13T07:41:34.0000000Z","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","v1.0.4","AnnotateLineNumbers","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"AutoAnnotateView.dll","AutoAnnotateView.dll",".dll","49664","2025-02-10T07:47:43.4844455Z","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","v1.0.4","AutoAnnotateView","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"AutoElevation.dll","AutoElevation.dll",".dll","28160","2025-01-16T08:13:32.2554341Z","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","v1.0.4","AutoElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"AutoLinenumberConnections.dll","AutoLinenumberConnections.dll",".dll","41472","2024-12-13T07:41:34.0000000Z","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","v1.0.4","AutoLinenumberConnections","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"b455ee6eaed79d76f084846a60ce3284","b455ee6eaed79d76f084846a60ce3284","","0","2023-12-29T11:13:44.6409171Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
|
||||||
|
"CheckOrthoView.dll","CheckOrthoView.dll",".dll","14848","2024-12-13T07:41:34.0000000Z","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","v1.0.4","CheckOrthoView","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"EnablerLoader.dll","EnablerLoader.dll",".dll","6144","2024-12-13T07:41:34.0000000Z","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","v1.0.4","EnablerLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"ExtentsIntersect.dll","ExtentsIntersect.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","v1.0.4","ExtentsIntersect","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"GetComponentGeometry.dll","GetComponentGeometry.dll",".dll","10240","2024-12-13T07:41:34.0000000Z","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","v1.0.4","GetComponentGeometry","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"GetComponentGeometry.xml","GetComponentGeometry.xml",".xml","1239","2025-01-14T11:26:24.5054138Z","70169C3A3AAAFE5A2282E01D5B9B7C99F2F4FF9B7514553434302674ABE46C98",,,,,,
|
||||||
|
"GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText.dll",".dll","23040","2024-12-13T07:41:34.0000000Z","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","v1.0.4","GetLinenumberAnnotationText","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","v1.0.4","GetPlantScriptFolderPath","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"GetScriptParameters.dll","GetScriptParameters.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","v1.0.4","GetScriptParameters","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"ico\button_BJ.png","button_BJ.png",".png","1235","2024-12-13T07:41:34.0000000Z","414F0A23E917D41A82C697B0E9CDDDF9BFCACD1627310BD5717AEA172418AA51",,,,,,
|
||||||
|
"ico\button_CAC.png","button_CAC.png",".png","1943","2024-12-13T07:41:34.0000000Z","FA2B8877F8B41C70BA62C737C40374394D6840A53DFE2D0B0F83463C4817C138",,,,,,
|
||||||
|
"ico\button_PXP-AAV.png","button_PXP-AAV.png",".png","1905","2024-12-13T07:41:34.0000000Z","6AB171963B553B434B5AA4BFBE91993B1782606AAF9B486FA23BB27BC1F0E4AA",,,,,,
|
||||||
|
"ico\button_PXP-AC.png","button_PXP-AC.png",".png","1751","2024-12-13T07:41:34.0000000Z","60A6BD62EF3E50BDDCD00D9C281F2399E6C9D42E0C2DAA1742273C0DC48FE9C3",,,,,,
|
||||||
|
"ico\button_PXP-AE.png","button_PXP-AE.png",".png","2001","2024-12-13T07:41:34.0000000Z","F1DCF0E908F5AF38CCD13ABA58B824E5B186430414499C0F623C1D5EEAD8DAD7",,,,,,
|
||||||
|
"ico\button_PXP-ALC.png","button_PXP-ALC.png",".png","1740","2024-12-13T07:41:34.0000000Z","19DB2449B66B95965155E6E3BC313BD52000FE38C95B0FAD90EF07DF53EA9CC9",,,,,,
|
||||||
|
"ico\button_PXP-COV.png","button_PXP-COV.png",".png","2007","2024-12-13T07:41:34.0000000Z","529BDD045961C244FBD0DE177B899AF9E7CE0F4506B3463C56D2704B3BF207B5",,,,,,
|
||||||
|
"ico\button_PXP-DAV.png","button_PXP-DAV.png",".png","1249","2025-01-28T06:29:03.3724433Z","A7932F2ABDAC652EA3A901A221AB3BE2BC80FCB76FB203D44FC63AF3681F51E3",,,,,,
|
||||||
|
"ico\button_PXP-DJ.png","button_PXP-DJ.png",".png","1470","2024-12-13T07:41:34.0000000Z","127957752DC2B6573CD2FD9A2791914BA22802E07E9B7E23CD3AE1FFF9E4FFC4",,,,,,
|
||||||
|
"ico\button_PXP-ELN.png","button_PXP-ELN.png",".png","1927","2024-12-13T07:41:34.0000000Z","DF32E6A465D5C196A17F3F96473315590DF2D71995F63D2D241D7599B62C99BB",,,,,,
|
||||||
|
"ico\button_PXP-ESN.png","button_PXP-ESN.png",".png","1636","2024-12-13T07:41:34.0000000Z","0F089184CFD428DA3620F9C0AD2DCBFC21B31ED34FA301E8D5D45923783C0CC7",,,,,,
|
||||||
|
"ico\button_PXP-IE.png","button_PXP-IE.png",".png","1885","2024-12-13T07:41:34.0000000Z","F1C6373BFAF82E1908AF371D5F21133D27E0B84AFC706A703530855A2046FE43",,,,,,
|
||||||
|
"ico\button_PXP-IOD.png","button_PXP-IOD.png",".png","1942","2024-12-13T07:41:34.0000000Z","494E90C899A0BB5BB365B0E1452E92A68359610C5A38A37D09F520A39BDB3899",,,,,,
|
||||||
|
"ico\button_PXP-LN.png","button_PXP-LN.png",".png","2024","2024-12-13T07:41:34.0000000Z","F3305ED6D266E29FA475D4C8A7DF38D2D2A1776A4CE379B876B6CFA278B2FE5D",,,,,,
|
||||||
|
"ico\button_PXP-MA.png","button_PXP-MA.png",".png","1907","2024-12-13T07:41:34.0000000Z","9386249121644DCF4B9C70491F561E8B6E5BAAE87EBB2C76A84E46B10A60C8C3",,,,,,
|
||||||
|
"ico\button_PXP-OA.png","button_PXP-OA.png",".png","1999","2024-12-13T07:41:34.0000000Z","EAA2AD7EE5C6B7F3716AF1D7690C76AE59229AE9B6BA02009F5009CD9307F4E8",,,,,,
|
||||||
|
"ico\button_PXP-OSI.png","button_PXP-OSI.png",".png","1508","2024-12-13T07:41:34.0000000Z","422D10B6EBDD76085CF9C5985391CCA060464B200D6792D604658AAE47F12CEE",,,,,,
|
||||||
|
"ico\button_PXP-RA.png","button_PXP-RA.png",".png","1826","2024-12-13T07:41:34.0000000Z","1DB3F39C6CCA4EABE79D81DE6DCF9900649CDC9064F2EBF5E45F9AD849B8317F",,,,,,
|
||||||
|
"ico\button_PXP-RMP.png","button_PXP-RMP.png",".png","1759","2024-12-13T07:41:34.0000000Z","156026EC6DEB7BDBE720047409E9A25615D650D8634C2F0BAF49A71E27135258",,,,,,
|
||||||
|
"ico\button_PXP-ULN.png","button_PXP-ULN.png",".png","1928","2024-12-13T07:41:34.0000000Z","B37EBB460691EAA7BC3A91DF05C27FE7CC1F0E9F9D69BDA7703956EAA7AA785C",,,,,,
|
||||||
|
"ico\button_PXP-UOB.png","button_PXP-UOB.png",".png","1569","2024-12-13T07:41:34.0000000Z","3BC16B9923CCD34CF10F403CD65732B217E255E89539174841E7F30D9DFD2DCD",,,,,,
|
||||||
|
"InsertElevation.dll","InsertElevation.dll",".dll","24576","2025-01-16T08:13:32.2504470Z","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","v1.0.4","InsertElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"LoadedAssemblies.dll","LoadedAssemblies.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","v1.0.4","LoadedAssemblies","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"OrthoAutoAnnotate.dll","OrthoAutoAnnotate.dll",".dll","108544","2025-02-10T07:45:33.3280516Z","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","v1.0.4","OrthoAutoAnnotate","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"OrthoLoader.dll","OrthoLoader.dll",".dll","18944","2025-02-24T06:59:01.9094468Z","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","v1.0.4","OrthoLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing.dll",".dll","14336","2024-12-13T07:41:34.0000000Z","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","v1.0.4","P3DCopyMissingObjectsToOrthoDrawing","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"P3DNewMa.dll","P3DNewMa.dll",".dll","103936","2025-02-24T06:59:03.1350320Z","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","v1.0.4","P3DNewMa","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"P3DSetLayerForViewport.dll","P3DSetLayerForViewport.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","v1.0.4","P3DSetLayerForViewport","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"PHPAnnotations.dll","PHPAnnotations.dll",".dll","69120","2024-12-13T07:41:34.0000000Z","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","v1.0.4","PHPAnnotations","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"Settings.xml","Settings.xml",".xml","136576","2025-01-31T07:28:24.1891507Z","AFEDC7526923ACE1DC60374F98F171809B36992262A37E1F11828F2B8D9799CB",,,,,,
|
||||||
|
"SwapAttributesMenuItem.dll","SwapAttributesMenuItem.dll",".dll","11264","2024-12-13T07:41:34.0000000Z","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","v1.0.4","SwapAttributesMenuItem","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"Template.dwg","Template.dwg",".dwg","148438","2025-01-30T13:16:13.0000000Z","EA279A30F113660CF53585451083BAEB0A01CA58C8F9A767C4B7C2A5E66ECCA3",,,,,,
|
||||||
|
"test.txt","test.txt",".txt","0","2025-01-16T09:56:52.0597137Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
|
||||||
|
"UpdateOrthoDrawings.dll","UpdateOrthoDrawings.dll",".dll","45568","2025-02-24T06:59:02.4829428Z","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","v1.0.4","UpdateOrthoDrawings","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks.dll",".dll","15360","2024-12-13T07:41:34.0000000Z","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","v1.0.4","UpdateOrthoViewBlocks","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
||||||
|
"Справка по работе с палитрой.docx","Справка по работе с палитрой.docx",".docx","855301","2025-02-05T09:53:08.1186958Z","B6DC65BA17C4EA79DBCCC501909BEDBF56EB6A9F1E24BFAD1B01158B6EC639D0",,,,,,
|
||||||
|
"Справка по работе с палитрой.pdf","Справка по работе с палитрой.pdf",".pdf","744035","2025-02-05T09:53:21.3752687Z","332CA70D3264C28BC712CA790157D0CD03BE7E07F42603DAF9247600271B7B75",,,,,,
|
||||||
|
"Справка.docx","Справка.docx",".docx","87889","2025-02-06T05:13:36.4991443Z","14BA9BD252FB6DF37C4D151787E8799AB1703847C1DEC24F3EFE4D6805B0EB12",,,,,,
|
||||||
|
"Справка.pdf","Справка.pdf",".pdf","154074","2025-02-06T05:14:24.2809131Z","EB840514755A9F56ABC8351B8EC41233FE2512118DB2D4BA0ACD46E74D2C7DC9",,,,,,
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
"DeployedDll","DeployedSha256","FileVersion","AssemblyVersion","LegacyProject","LegacyCsproj","LegacyOutputName","TargetFramework","PlatformTarget","CommandCount","RibbonRole","ConfigurationSource","MatchStatus"
|
||||||
|
"AnnotateLineNumbers.dll","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","1.0.4.0","AnnotateLineNumbers","AnnotateLineNumbers\AnnotateLineNumbers.csproj","AnnotateLineNumbers","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"AutoAnnotateView.dll","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","1.0.4.0","AutoAnnotateView","AutoAnnotateView\AutoAnnotateView.csproj","AutoAnnotateView","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
||||||
|
"AutoElevation.dll","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","1.0.4.0","AutoElevation","AutoElevation\AutoElevation.csproj","AutoElevation","v4.7","x64",,,,"LIKELY_SOURCE_MATCH"
|
||||||
|
"AutoLinenumberConnections.dll","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","1.0.4.0","AutoLinenumberConnections","AutoLinenumberConnections\AutoLinenumberConnections.csproj","AutoLinenumberConnections","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"CheckOrthoView.dll","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","1.0.4.0","CheckOrthoView","CheckOrthoView\CheckOrthoView.csproj","CheckOrthoView","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"EnablerLoader.dll","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","1.0.4.0","EnablerLoader","EnablerLoader\EnablerLoader.csproj","EnablerLoader","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"ExtentsIntersect.dll","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","1.0.4.0","ExtentsIntersect","ExtentsIntersect\ExtentsIntersect.csproj","ExtentsIntersect","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"GetComponentGeometry.dll","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","1.0.4.0","GetComponentGeometry","GetComponentGeometry\GetComponentGeometry.csproj","GetComponentGeometry","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"GetLinenumberAnnotationText.dll","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","1.0.4.0","GetLinenumberAnnotationText","GetLinenumberAnnotationText\GetLinenumberAnnotationText.csproj","GetLinenumberAnnotationText","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"GetPlantScriptFolderPath.dll","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","1.0.4.0","GetPlantScriptFolderPath","GetPlantScriptFolderPath\GetPlantScriptFolderPath.csproj","GetPlantScriptFolderPath","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"GetScriptParameters.dll","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","1.0.4.0","GetScriptParameters","GetScriptParameters\GetScriptParameters.csproj","GetScriptParameters","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"InsertElevation.dll","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","1.0.4.0","InsertElevation","InsertElevation\InsertElevation.csproj","InsertElevation","v4.7","x64",,,"Settings.xml","MULTIPLE_CANDIDATES"
|
||||||
|
"LoadedAssemblies.dll","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","1.0.4.0","LoadedAssemblies","LoadedAssemblies\LoadedAssemblies.csproj","LoadedAssemblies","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"OrthoAutoAnnotate.dll","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","1.0.4.0","OrthoAutoAnnotate","OrthoAutoAnnotate\OrthoAutoAnnotate.csproj","OrthoAutoAnnotate","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
||||||
|
"OrthoLoader.dll","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","1.0.4.0","OrthoLoader","OrthoLoader\OrthoLoader.csproj","OrthoLoader","v4.7","x64",,"Central ribbon/loader",,"LIKELY_SOURCE_MATCH"
|
||||||
|
"P3DCopyMissingObjectsToOrthoDrawing.dll","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","1.0.4.0","P3DCopyMissingObjectsToOrthoDrawing","P3DCopyMissingObjectsToOrthoDrawing\P3DCopyMissingObjectsToOrthoDrawing.csproj","P3DCopyMissingObjectsToOrthoDrawing","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"P3DNewMa.dll","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","1.0.4.0","P3DNewMa","P3DNewMa\P3DNewMa.csproj","P3DNewMa","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
||||||
|
"P3DSetLayerForViewport.dll","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","1.0.4.0","P3DSetLayerForViewport","P3DSetLayerForViewport\P3DSetLayerForViewport.csproj","P3DSetLayerForViewport","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"PHPAnnotations.dll","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","1.0.4.0","PHPAnnotations","PHPAnnotations\PHPAnnotations.csproj","PHPAnnotations","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"SwapAttributesMenuItem.dll","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","1.0.4.0","SwapAttributesMenuItem","SwapAttributesMenuItem\SwapAttributesMenuItem.csproj","SwapAttributesMenuItem","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
"UpdateOrthoDrawings.dll","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","1.0.4.0","UpdateOrthoDrawings","UpdateOrthoDrawings\UpdateOrthoDrawings.csproj","UpdateOrthoDrawings","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
||||||
|
"UpdateOrthoViewBlocks.dll","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","1.0.4.0","UpdateOrthoViewBlocks","UpdateOrthoViewBlocks\UpdateOrthoViewBlocks.csproj","UpdateOrthoViewBlocks","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
GeneratedUtc=2026-07-31T10:39:04.9899963Z
|
||||||
|
DeployedRoot=<network root redacted to report context>
|
||||||
|
SourceRoot=<network source root redacted to report context>
|
||||||
|
Files=52
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Legacy versus new loading model
|
||||||
|
|
||||||
|
| Legacy behavior | Legacy evidence | New implementation | Compatibility requirement | Decision | Risk |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Unknown external bootstrap loads `OrthoLoader.dll` | `loading-chain.md` | Bundle bootstrap not selected yet | Identify the workstation/project startup owner first | Defer loading-model decision | High |
|
||||||
|
| 20 independent DLLs loaded in order | `Settings.xml`, `Loader.cs:63-81` | one entry assembly currently | Preserve required command availability, not the DLL count | Evaluate command/module migration individually | Medium |
|
||||||
|
| DLLs loaded from OrthoLoader's directory (possibly UNC) | `Loader.cs:49,65`; bootstrap source unknown | local smoke bundle only | Decide offline/update policy with operations owner | Do not hard-code UNC in new code | High |
|
||||||
|
| Settings.xml controls order | `Loader.cs:104-112` | MSBuild/bundle manifest currently | Preserve explicit dependency ordering where behavior requires it | Document, do not emulate blindly | Medium |
|
||||||
|
| central PluginsOrtho Ribbon | `Ribbon.cs` | new Promhimproekt tab | Preserve actual command mapping where adopted | Separate UI migration decision | Medium |
|
||||||
|
| project selection uses local app_netload.lsp | `Ribbon.cs:380-405` | no equivalent | Confirm security/project-selection policy | Do not reproduce hidden local control file | High |
|
||||||
|
| network icons and documents | `Ribbon.cs:330-357`, deployed files | bundle resources | Bundle only licensed, necessary resources | Replace network runtime dependency | Medium |
|
||||||
|
|
||||||
|
Recommendation: do not choose AppLoader or a manual-NETLOAD model yet. First identify the missing external bootstrap and confirm whether `app_netload.lsp` is the corporate launcher contract. Once confirmed, use a single versioned entry assembly with explicit manifest/dependency validation; avoid sequential programmatic NETLOAD and UNC execution because the legacy chain is non-atomic and fails the remaining modules after one error.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Proven legacy loading chain
|
||||||
|
|
||||||
|
```text
|
||||||
|
Unknown external bootstrap
|
||||||
|
-> NETLOAD of deployed OrthoLoader.dll (not evidenced in audited directories)
|
||||||
|
-> OrthoLoader.Ribbon.Initialize()
|
||||||
|
-> subscribe ComponentManager.ItemInitialized
|
||||||
|
-> OrthoLoader.Initialize()
|
||||||
|
-> adjacent Settings.xml / Main/LoadAssemblies/LoadAssembly
|
||||||
|
-> sequential acDoc.Editor.Command("._netload", fullPath) for 20 DLLs
|
||||||
|
-> each DLL exposes CommandMethod commands
|
||||||
|
-> Ribbon ItemInitialized creates PluginsOrtho tab and command buttons
|
||||||
|
```
|
||||||
|
|
||||||
|
| Claim | Evidence | Confidence |
|
||||||
|
|---|---|---|
|
||||||
|
| Central loader is `OrthoLoader.dll` | `OrthoLoader/Ribbon.cs:151-156`; deployed DLL metadata and `Settings.xml` | High |
|
||||||
|
| Module list is read from adjacent `Settings.xml` | `OrthoLoader/Loader.cs:104-112` | High |
|
||||||
|
| Module loading is programmatic `._netload`, in XML order | `OrthoLoader/Loader.cs:63-81`; `configuration-map.csv` | High |
|
||||||
|
| Full DLL path is loader directory + configured filename | `OrthoLoader/Loader.cs:49, 65` | High |
|
||||||
|
| Modules load from the same directory as OrthoLoader | `Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)` in `Loader.cs:49,65` | High |
|
||||||
|
| Modules loaded directly from UNC | true only if external bootstrap NETLOADed the deployed UNC copy; bootstrap is outside audit scope | Unknown |
|
||||||
|
| Missing/failed module stops later loading and shows MessageBox | single outer try/catch around the loop, `Loader.cs:63-99` | High |
|
||||||
|
| Ribbon owner is OrthoLoader | `OrthoLoader/Ribbon.cs:16-19,151-172,193-321` | High |
|
||||||
|
| Initial bootstrap mechanism | no LISP/CUI/ARG/REG/PackageContents file found in either audited directory | Unknown |
|
||||||
|
|
||||||
|
`Assembly.LoadFrom` occurs inside several legacy command implementations for secondary dependencies. It is not the mechanism used by `OrthoLoader` to load its configured modules. `EnablerLoader` separately uses `Assembly.LoadFile` for the Plant enabler from the host base directory.
|
||||||
|
|
||||||
|
No `PackageContents.xml`, `*.bundle`, registry demand-load registration, LISP, CUI, MNL, script, batch, or PowerShell bootstrap was found in the audited locations. This proves absence only within audit scope; it does not prove the workstation bootstrap did not exist elsewhere.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Dependency,Category,Evidence,Requiredness,Notes
|
||||||
|
Settings.xml,CONFIGURATION,OrthoLoader/Loader.cs:104-112,Required,Adjacent to each loaded assembly directory
|
||||||
|
ico/*.png,RIBBON_RESOURCE,OrthoLoader/Ribbon.cs:330-357,Optional,Missing file yields null image
|
||||||
|
app_netload.lsp,CONFIGURATION,OrthoLoader/Ribbon.cs:380-405,Implicit,Local workstation file gates project-specific Ribbon enablement
|
||||||
|
annotation_settings.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
|
||||||
|
users_indexes_list.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
|
||||||
|
Template.dwg,TEMPLATE,Deployed inventory and legacy source references,Command-dependent,Do not treat deployed copy as approved test fixture
|
||||||
|
Plant project XML/DWG,COMMAND_TIME,UpdateOrthoDrawings and annotation source,Required by relevant command,Out of deployed plugin root
|
||||||
|
Update report CSV,LOGGING,UpdateOrthoDrawings/Commands.cs:1004,Optional,Written beside executing assembly by legacy command
|
||||||
|
Help PDFs,RIBBON_RESOURCE,P3DNewMa/NewMaControl.cs:6214,Optional,Opened from assembly directory
|
||||||
|
@@ -0,0 +1,9 @@
|
|||||||
|
# Observed user workflow
|
||||||
|
|
||||||
|
After an external bootstrap loaded `OrthoLoader.dll`, the loader created a `PluginsOrtho` Ribbon tab only when `isProjectCorrect` accepted the current Plant project. That predicate reads the first line of the local file `C:\AutoCAD User Support\Applications\app_netload.lsp` and compares its encoded project root with the active Plant project (`OrthoLoader/Ribbon.cs:380-405`).
|
||||||
|
|
||||||
|
`PluginsOrtho` has eight panels. Button IDs are commands; clicks call `Document.SendStringToExecute(command + "\n", ...)` (`Ribbon.cs:193-321, 526-529`). Examples: `PXP-AAV` is registered by `AutoAnnotateView/Commands.cs:546`; `PXP-OA` by `OrthoAutoAnnotate/Commands.cs:6444`.
|
||||||
|
|
||||||
|
The deployed `ico/*.png` files are runtime Ribbon resources. They are resolved from the loader assembly directory; in this deployment that is the UNC directory. The code has a null-image fallback when an icon is absent.
|
||||||
|
|
||||||
|
The tab and panels use stable IDs and lookup before creation. Button collections are appended when a missing panel is created; repeated project-open events can enable existing UI or create absent UI. The event is detached after first Ribbon initialization, but `CurrentProjectOpenedEvent` is retained for the process lifetime.
|
||||||
Reference in New Issue
Block a user