From 60b3934cd25eed5d1e566dbf7c65d4fa70e298c8 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 31 Jul 2026 09:57:34 +0300 Subject: [PATCH] feat: scaffold P_Rec_Annot .NET 8 architecture --- Plant3D2026/P_Rec_Annot/.gitignore | 4 + Plant3D2026/P_Rec_Annot/Directory.Build.props | 11 +++ Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln | 54 +++++++++++ Plant3D2026/P_Rec_Annot/README.md | 24 +++++ .../P_Rec_Annot.bundle/PackageContents.xml | 8 ++ Plant3D2026/P_Rec_Annot/docs/architecture.md | 3 + Plant3D2026/P_Rec_Annot/docs/build.md | 3 + .../docs/command-migration-matrix.md | 18 ++++ .../P_Rec_Annot/docs/golden-test-plan.md | 3 + .../docs/legacy-rule-provenance.csv | 5 + .../P_Rec_Annot/docs/migration-dashboard.md | 3 + .../docs/module-migration-matrix.md | 15 +++ .../P_Rec_Annot/docs/runtime-test-plan.md | 3 + .../src/PRecAnnot.Abstractions/Contracts.cs | 95 +++++++++++++++++++ .../PRecAnnot.Abstractions.csproj | 3 + .../PRecAnnot.Application/CommandRegistry.cs | 36 +++++++ .../PRecAnnot.Application.csproj | 4 + .../PRecAnnot.Core/AnnotationRuleEngine.cs | 48 ++++++++++ .../src/PRecAnnot.Core/PRecAnnot.Core.csproj | 4 + .../EntryPointLifecycle.cs | 15 +++ .../PRecAnnot.Plant3D2026.csproj | 15 +++ .../Plant3D2026Availability.cs | 14 +++ .../src/PRecAnnot.Telemetry/JsonlUsageSink.cs | 14 +++ .../PRecAnnot.Telemetry.csproj | 4 + .../src/PRecAnnot.UI/PRecAnnot.UI.csproj | 4 + .../src/PRecAnnot.UI/RibbonRegistration.cs | 17 ++++ .../PRecAnnot.Core.Tests/FoundationTests.cs | 37 ++++++++ .../PRecAnnot.Core.Tests.csproj | 6 ++ .../P_Rec_Annot/tools/Build-Bundle.ps1 | 4 + .../P_Rec_Annot/tools/Validate-Bundle.ps1 | 8 ++ 30 files changed, 482 insertions(+) create mode 100644 Plant3D2026/P_Rec_Annot/.gitignore create mode 100644 Plant3D2026/P_Rec_Annot/Directory.Build.props create mode 100644 Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln create mode 100644 Plant3D2026/P_Rec_Annot/README.md create mode 100644 Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml create mode 100644 Plant3D2026/P_Rec_Annot/docs/architecture.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/build.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv create mode 100644 Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md create mode 100644 Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj create mode 100644 Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs create mode 100644 Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj create mode 100644 Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 create mode 100644 Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 diff --git a/Plant3D2026/P_Rec_Annot/.gitignore b/Plant3D2026/P_Rec_Annot/.gitignore new file mode 100644 index 0000000..42fcf36 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/.gitignore @@ -0,0 +1,4 @@ +artifacts/ +bin/ +obj/ +.vs/ diff --git a/Plant3D2026/P_Rec_Annot/Directory.Build.props b/Plant3D2026/P_Rec_Annot/Directory.Build.props new file mode 100644 index 0000000..fa78c7b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/Directory.Build.props @@ -0,0 +1,11 @@ + + + enable + enable + latest + true + true + portable + $(ProgramFiles)\Autodesk\AutoCAD 2026 + + diff --git a/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln b/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln new file mode 100644 index 0000000..a958ea3 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/PRecAnnot2026.sln @@ -0,0 +1,54 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Abstractions", "src\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj", "{10000000-0000-0000-0000-000000000001}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Core", "src\PRecAnnot.Core\PRecAnnot.Core.csproj", "{10000000-0000-0000-0000-000000000002}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Application", "src\PRecAnnot.Application\PRecAnnot.Application.csproj", "{10000000-0000-0000-0000-000000000003}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Plant3D2026", "src\PRecAnnot.Plant3D2026\PRecAnnot.Plant3D2026.csproj", "{10000000-0000-0000-0000-000000000004}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.UI", "src\PRecAnnot.UI\PRecAnnot.UI.csproj", "{10000000-0000-0000-0000-000000000005}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Telemetry", "src\PRecAnnot.Telemetry\PRecAnnot.Telemetry.csproj", "{10000000-0000-0000-0000-000000000006}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PRecAnnot.Core.Tests", "tests\PRecAnnot.Core.Tests\PRecAnnot.Core.Tests.csproj", "{10000000-0000-0000-0000-000000000007}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {10000000-0000-0000-0000-000000000001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000001}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000001}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000001}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000002}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000004}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000004}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000004}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000005}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000005}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000005}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000005}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000006}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000006}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000006}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000006}.Release|Any CPU.Build.0 = Release|Any CPU + {10000000-0000-0000-0000-000000000007}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {10000000-0000-0000-0000-000000000007}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10000000-0000-0000-0000-000000000007}.Release|Any CPU.ActiveCfg = Release|Any CPU + {10000000-0000-0000-0000-000000000007}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Plant3D2026/P_Rec_Annot/README.md b/Plant3D2026/P_Rec_Annot/README.md new file mode 100644 index 0000000..11fb38b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/README.md @@ -0,0 +1,24 @@ +# P_Rec_Annot Plant 3D 2026 foundation + +Foundation for migration from the legacy P_Rec_Annot v1.0.4. Legacy sources remain read-only and are not copied here. + +Current slice: + +* `PRecAnnot.Abstractions` — Autodesk-free immutable contracts and DTOs; +* `PRecAnnot.Core` — deterministic annotation planning and duplicate filtering; +* `PRecAnnot.Application` — command registry and use-case orchestration; +* `PRecAnnot.Plant3D2026` — guarded adapter boundary; compilation against Plant 3D 2026 is blocked until the SDK/runtime is installed; +* `PRecAnnot.UI` — idempotent Ribbon registration model, without legacy `_netload`; +* `PRecAnnot.Telemetry` — local JSONL command-scope events, without project path/number; +* `PRecAnnot.Bundle.Tests` and `PRecAnnot.Core.Tests` — architecture/core/packaging checks. + +The first vertical slice is intentionally a dry-run planning slice. It does not mutate DWG until the Plant 3D 2026 adapter is compiled and runtime-tested on a controlled project. + +Build: + +```powershell +dotnet build .\PRecAnnot2026.sln --configuration Release +dotnet test .\PRecAnnot2026.sln --configuration Release --no-build +``` + +When Plant 3D 2026 is installed, pass `/p:Plant3D2026InstallDir=...`. Autodesk assemblies are `Private=false`, `CopyLocal=false` and are never committed. diff --git a/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml new file mode 100644 index 0000000..2bd6de8 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/bundle/P_Rec_Annot.bundle/PackageContents.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Plant3D2026/P_Rec_Annot/docs/architecture.md b/Plant3D2026/P_Rec_Annot/docs/architecture.md new file mode 100644 index 0000000..9c2acb4 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/architecture.md @@ -0,0 +1,3 @@ +# Target architecture + +Core and Abstractions have no Autodesk, WinForms or WPF references. Only `PRecAnnot.Plant3D2026` may reference installed Autodesk assemblies. DWG mutation remains synchronous on AutoCAD's thread; no `Task.Run` is used for database operations. Ribbon registration is guarded and unloadable. The bundle has one entry assembly and no legacy `_netload` chain. diff --git a/Plant3D2026/P_Rec_Annot/docs/build.md b/Plant3D2026/P_Rec_Annot/docs/build.md new file mode 100644 index 0000000..d7b0829 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/build.md @@ -0,0 +1,3 @@ +# 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. diff --git a/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md b/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md new file mode 100644 index 0000000..8596f76 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/command-migration-matrix.md @@ -0,0 +1,18 @@ +# Command migration matrix + +Initial active set is based on non-archive command registrations and Ribbon definitions in audit evidence. This matrix is deliberately explicit; commands remain blocked until Plant 3D 2026 runtime evidence exists. + +| Legacy command | Legacy module | Target use case | Decision | Status | Blocker/test | +|---|---|---|---|---|---| +| PXP-AAV | AutoAnnotateView | automatic annotations in viewport | port after isolation | BLOCKED_WITH_REASON | 2026 assemblies/runtime | +| PXP-OA | OrthoAutoAnnotate | axis/support graphical annotations | port after isolation | BLOCKED_WITH_REASON | exact legacy rules/golden DWG | +| PXP-ALC | AutoLinenumberConnections | line-number connection annotations | reimplement | BLOCKED_WITH_REASON | DataLinks adapter | +| PXP-LN, PXP-ELN, PXP-ULN, PXP-ESN, PXP-RMP | AnnotateLineNumbers | line-number list/updates | reimplement | BLOCKED_WITH_REASON | golden cases | +| PXP-AE, PXP-IE | AutoElevation/InsertElevation | elevations | reimplement | BLOCKED_WITH_REASON | geometry/runtime | +| PXP-IOD | UpdateOrthoDrawings | batch update | defer to Wave 5 | BLOCKED_WITH_REASON | multi-DWG transaction | +| PXP-UOB | UpdateOrthoViewBlocks | viewport layers | adapter | BLOCKED_WITH_REASON | layer API | +| PXP-OSI, PXP-COV | OrthoAutoAnnotate/CheckOrthoView | locate/check objects | compare native | BLOCKED_WITH_REASON | runtime comparison | +| PXP-MA, CAC, BJ, mto-ma | PHPAnnotations/P3DNewMa | manual annotation blocks | port/review | BLOCKED_WITH_REASON | UI and block contract | +| PXP-AC | SwapAttributesMenuItem | swap annotation attributes | port after rules | BLOCKED_WITH_REASON | block schema | + +Archive-only registrations are not published. No command is silently discarded. diff --git a/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md b/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md new file mode 100644 index 0000000..2d721ae --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/golden-test-plan.md @@ -0,0 +1,3 @@ +# Golden test plan + +Capture a minimal 2019 Ortho fixture with input objects, output blocks/attributes, text, layer, style, coordinates, rotation, scale, DataLinks, before/after usage event and repeat-run behavior. Recreate the same fixture in 2026 only after the adapter is available. Do not commit production DWG/DCF. diff --git a/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv b/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv new file mode 100644 index 0000000..592e568 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/legacy-rule-provenance.csv @@ -0,0 +1,5 @@ +RuleId,TargetClass,TargetMethod,LegacyFile,LegacyLines,Behavior,MigrationDecision,TestName +RULE-001,AnnotationRuleEngine,BuildPlan,AutoAnnotateView/Commands.cs,476-500,Remove prior annotations then insert calculated annotations,REIMPLEMENT_SAME_BEHAVIOR,ExistingAnnotation_IsUpdatedNotDuplicated +RULE-002,AnnotationRuleEngine,BuildPlan,OrthoAutoAnnotate/Commands.cs,6444-6488,Require Plant project and registered Ortho drawing,KEEP_AFTER_ISOLATION,PlantProjectAndOrthoDrawingRequired +RULE-003,PlantObjectReader,ReadObjects,README.md,96-101,Read EngineeringItems and line-group relationships through PnP API,REIMPLEMENT_SAME_BEHAVIOR,PlantAdapter_ReadsGoldenObjects +RULE-004,CommandRegistry,Foundation,OrthoLoader/Ribbon.cs,109-129,Expose annotation commands in annotation panel,REIMPLEMENT_SAME_BEHAVIOR,AllActiveCommands_AreMapped diff --git a/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md b/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md new file mode 100644 index 0000000..48bbdca --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/migration-dashboard.md @@ -0,0 +1,3 @@ +# 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. diff --git a/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md b/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md new file mode 100644 index 0000000..62e2a63 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/module-migration-matrix.md @@ -0,0 +1,15 @@ +# Module migration matrix + +| Legacy module | Target project | Classification | Wave | +|---|---|---|---| +| AnnotateLineNumbers | PRecAnnot.Core + Plant3D2026 | KEEP_AFTER_ISOLATION | 3 | +| AutoAnnotateView | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 1 | +| OrthoAutoAnnotate | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 1 | +| AutoLinenumberConnections | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 2 | +| AutoElevation/InsertElevation | Core + Plant3D2026 | REIMPLEMENT_SAME_BEHAVIOR | 3 | +| UpdateOrthoDrawings | Plant3D2026/Application | DEFER_AND_RETEST | 5 | +| OrthoLoader | bundle/entry composition root | DO_NOT_REUSE | 1 | +| Ribbon | PRecAnnot.UI | REIMPLEMENT | 1 | +| GetLinenumberAnnotationText | Plant adapter + Core mapper | KEEP_AFTER_ISOLATION | 2 | +| PHPAnnotations/P3DNewMa | UI + block adapter | REVIEW_USAGE | 4 | +| telemetry | PRecAnnot.Telemetry | REIMPLEMENT | 1 | diff --git a/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md b/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md new file mode 100644 index 0000000..b8a19f5 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/docs/runtime-test-plan.md @@ -0,0 +1,3 @@ +# 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. diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs new file mode 100644 index 0000000..8a194e3 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -0,0 +1,95 @@ +namespace PRecAnnot.Abstractions; + +public sealed record OrthoViewportRef(string DrawingId, string ViewportId); + +public sealed record PlantObjectData +{ + public required string ObjectId { get; init; } + public required string ClassName { get; init; } + public string? LineNumber { get; init; } + public string? Tag { get; init; } + public string? DisplayName { get; init; } + public required AnnotationPoint Position { get; init; } + public double Rotation { get; init; } +} + +public readonly record struct AnnotationPoint(double X, double Y, double Z = 0); + +public sealed record ExistingAnnotation(string StableKey, string? Text, AnnotationPoint Position); + +public sealed record AnnotationRequest +{ + public required OrthoViewportRef Viewport { get; init; } + public required IReadOnlyList Objects { get; init; } + public required IReadOnlyList Existing { get; init; } +} + +public sealed record AnnotationIntent +{ + public required string StableKey { get; init; } + public required string Text { get; init; } + public required AnnotationPoint Position { get; init; } + public required double Rotation { get; init; } + public required string ClassName { get; init; } + public required string Layer { get; init; } +} + +public sealed record AnnotationPlan +{ + public required IReadOnlyList Create { get; init; } + public required IReadOnlyList Update { get; init; } + public required IReadOnlyList Delete { get; init; } + public required IReadOnlyList SkipReasons { get; init; } + public int ExaminedCount { get; init; } + public bool IsDeterministic { get; init; } = true; +} + +public interface IPlantProjectContext +{ + bool IsPlantProjectOpen { get; } + string? ProjectName { get; } +} + +public interface IOrthoDrawingContext +{ + bool IsOrthoDrawing { get; } + IReadOnlyList GetViewports(); +} + +public interface IPlantObjectReader +{ + IReadOnlyList ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken); +} + +public interface IAnnotationRepository +{ + IReadOnlyList FindExisting(OrthoViewportRef viewport); + AnnotationWriteResult Apply(AnnotationPlan plan, CancellationToken cancellationToken); +} + +public sealed record AnnotationWriteResult(int Created, int Updated, int Deleted, int Skipped); + +public interface IAnnotationRuleEngine +{ + AnnotationPlan BuildPlan(AnnotationRequest request); +} + +public sealed record CommandUsageEvent +{ + public required string CommandName { get; init; } + public required string ResultStatus { get; init; } + public int ObjectsExamined { get; init; } + public int AnnotationsCreated { get; init; } + public int AnnotationsUpdated { get; init; } + public int AnnotationsDeleted { get; init; } + public int AnnotationsSkipped { get; init; } + public int WarningCount { get; init; } + public int ErrorCount { get; init; } + public long DurationMilliseconds { get; init; } + public DateTimeOffset TimestampUtc { get; init; } = DateTimeOffset.UtcNow; +} + +public interface ICommandUsageSink +{ + void Record(CommandUsageEvent usageEvent); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj new file mode 100644 index 0000000..c3c3554 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/PRecAnnot.Abstractions.csproj @@ -0,0 +1,3 @@ + + net8.0 + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs new file mode 100644 index 0000000..9581daa --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/CommandRegistry.cs @@ -0,0 +1,36 @@ +namespace PRecAnnot.Application; + +public sealed record PRecAnnotCommandDescriptor +{ + public required string CommandName { get; init; } + public required string DisplayName { get; init; } + public required string UseCaseId { get; init; } + public required string RibbonPanel { get; init; } + public bool RequiresPlantProject { get; init; } + public bool RequiresOrthoDrawing { get; init; } + public bool RequiresSelection { get; init; } + public string Status { get; init; } = "BLOCKED_WITH_REASON"; +} + +public sealed class CommandRegistry +{ + private readonly IReadOnlyDictionary _commands; + + public CommandRegistry(IEnumerable commands) + { + var list = commands.ToList(); + if (list.GroupBy(x => x.CommandName, StringComparer.OrdinalIgnoreCase).Any(x => x.Count() > 1)) + throw new ArgumentException("Command names must be unique.", nameof(commands)); + _commands = list.ToDictionary(x => x.CommandName, StringComparer.OrdinalIgnoreCase); + } + + public IReadOnlyCollection Commands => _commands.Values.ToArray(); + public bool TryGet(string name, out PRecAnnotCommandDescriptor? descriptor) => _commands.TryGetValue(name, out descriptor); + + public static CommandRegistry Foundation() => new(new[] + { + new PRecAnnotCommandDescriptor { CommandName = "PHP_REC_ANNOT", DisplayName = "Аннотировать", UseCaseId = "ortho-annotation", RibbonPanel = "Аннотации", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" }, + new PRecAnnotCommandDescriptor { CommandName = "PXP-AAV", DisplayName = "Разместить аннотации автоматически", UseCaseId = "auto-annotate-view", RibbonPanel = "Аннотации", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" }, + new PRecAnnotCommandDescriptor { CommandName = "PXP-OA", DisplayName = "Разместить осевые линии и опоры", UseCaseId = "ortho-axes-supports", RibbonPanel = "Осевые линии и опоры", RequiresPlantProject = true, RequiresOrthoDrawing = true, Status = "BLOCKED_WITH_REASON" } + }); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj new file mode 100644 index 0000000..c156ee7 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Application/PRecAnnot.Application.csproj @@ -0,0 +1,4 @@ + + net8.0 + + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs new file mode 100644 index 0000000..0c935e8 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/AnnotationRuleEngine.cs @@ -0,0 +1,48 @@ +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Core; + +public sealed class AnnotationRuleEngine : IAnnotationRuleEngine +{ + public AnnotationPlan BuildPlan(AnnotationRequest request) + { + var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase); + var create = new List(); + var update = new List(); + var skipped = new List(); + + foreach (var item in request.Objects.OrderBy(x => x.ObjectId, StringComparer.Ordinal)) + { + if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag)) + { + skipped.Add($"{item.ObjectId}:missing-line-number-or-tag"); + continue; + } + + 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(), + SkipReasons = skipped, + ExaminedCount = request.Objects.Count + }; + } + + private static string BuildText(PlantObjectData item) => + string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim(); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj new file mode 100644 index 0000000..c156ee7 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/PRecAnnot.Core.csproj @@ -0,0 +1,4 @@ + + net8.0 + + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs new file mode 100644 index 0000000..3f3b2ca --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/EntryPointLifecycle.cs @@ -0,0 +1,15 @@ +namespace PRecAnnot.Plant3D2026; + +/// +/// 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. +/// +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(); +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj new file mode 100644 index 0000000..c5d408b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/PRecAnnot.Plant3D2026.csproj @@ -0,0 +1,15 @@ + + net8.0-windowsx64true + + + + + + + $(Plant3D2026InstallDir)\AcMgd.dllfalse + $(Plant3D2026InstallDir)\AcDbMgd.dllfalse + $(Plant3D2026InstallDir)\AcCoreMgd.dllfalse + $(Plant3D2026InstallDir)\PLNT3D\PnPProjectManagerMgd.dllfalse + $(Plant3D2026InstallDir)\PLNT3D\PnPDataLinks.dllfalse + + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs new file mode 100644 index 0000000..541907c --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Plant3D2026/Plant3D2026Availability.cs @@ -0,0 +1,14 @@ +namespace PRecAnnot.Plant3D2026; + +public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDirectory, IReadOnlyList MissingAssemblies) +{ + 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 missing = expected.Where(x => !File.Exists(Path.Combine(installDirectory, x))).ToArray(); + return new(missing.Length == 0, installDirectory, missing); + } +} + +public sealed class Plant3D2026BlockedException(Plant3D2026Availability availability) + : InvalidOperationException($"Plant 3D 2026 assemblies are unavailable at '{availability.InstallDirectory}'. Missing: {string.Join(", ", availability.MissingAssemblies)}") { } diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs new file mode 100644 index 0000000..bfcaea9 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/JsonlUsageSink.cs @@ -0,0 +1,14 @@ +using System.Text.Json; +using PRecAnnot.Abstractions; + +namespace PRecAnnot.Telemetry; + +public sealed class JsonlUsageSink(string path) : ICommandUsageSink +{ + private readonly object _gate = new(); + public void Record(CommandUsageEvent usageEvent) + { + var json = JsonSerializer.Serialize(usageEvent); + lock (_gate) File.AppendAllText(path, json + Environment.NewLine); + } +} diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj new file mode 100644 index 0000000..c156ee7 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Telemetry/PRecAnnot.Telemetry.csproj @@ -0,0 +1,4 @@ + + net8.0 + + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj new file mode 100644 index 0000000..960f619 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/PRecAnnot.UI.csproj @@ -0,0 +1,4 @@ + + net8.0-windowstrue + + diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs new file mode 100644 index 0000000..1149009 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.UI/RibbonRegistration.cs @@ -0,0 +1,17 @@ +using PRecAnnot.Application; + +namespace PRecAnnot.UI; + +public sealed class RibbonRegistration +{ + private readonly HashSet _registered = new(StringComparer.OrdinalIgnoreCase); + public bool Register(CommandRegistry registry) + { + var changed = false; + foreach (var item in registry.Commands) + changed |= _registered.Add($"{item.RibbonPanel}:{item.CommandName}"); + return changed; + } + public void Unregister(CommandRegistry registry) { foreach (var item in registry.Commands) _registered.Remove($"{item.RibbonPanel}:{item.CommandName}"); } + public int RegisteredCount => _registered.Count; +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs new file mode 100644 index 0000000..2a2106b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/FoundationTests.cs @@ -0,0 +1,37 @@ +using PRecAnnot.Abstractions; +using PRecAnnot.Application; +using PRecAnnot.Core; +using PRecAnnot.UI; + +namespace PRecAnnot.Core.Tests; + +public sealed class FoundationTests +{ + [Fact] public void Plan_IsDeterministic() + { + var engine = new AnnotationRuleEngine(); + var request = new AnnotationRequest { Viewport = new("d", "v"), Objects = new[] { new PlantObjectData { ObjectId = "2", ClassName = "Pipe", LineNumber = "L-2", Position = new(2, 2) }, new PlantObjectData { ObjectId = "1", ClassName = "Pipe", LineNumber = "L-1", Position = new(1, 1) } }, Existing = Array.Empty() }; + var plan = engine.BuildPlan(request); + Assert.Equal(new[] { "1", "2" }, plan.Create.Select(x => x.StableKey.Split(':').Last())); + } + + [Fact] public void ExistingAnnotation_IsUpdatedNotDuplicated() + { + var item = new PlantObjectData { ObjectId = "1", ClassName = "Valve", Tag = "V-1", Position = new(0, 0) }; + var plan = new AnnotationRuleEngine().BuildPlan(new AnnotationRequest { Viewport = new("d", "v"), Objects = new[] { item }, Existing = new[] { new ExistingAnnotation("d:v:1", "old", new(0, 0)) } }); + Assert.Empty(plan.Create); Assert.Single(plan.Update); + } + + [Fact] public void CoreContracts_DoNotExposeAutodeskTypes() + { + var names = typeof(AnnotationRequest).Assembly.GetReferencedAssemblies().Select(x => x.Name).ToArray(); + Assert.DoesNotContain(names, x => x is not null && x.StartsWith("Ac", StringComparison.OrdinalIgnoreCase)); + Assert.DoesNotContain(names, x => x is not null && x.StartsWith("Autodesk", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] public void RibbonRegistration_IsIdempotent() + { + var ribbon = new RibbonRegistration(); var registry = CommandRegistry.Foundation(); + Assert.True(ribbon.Register(registry)); Assert.False(ribbon.Register(registry)); Assert.Equal(3, ribbon.RegisteredCount); + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj new file mode 100644 index 0000000..8f61c3b --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/PRecAnnot.Core.Tests.csproj @@ -0,0 +1,6 @@ + + net8.0-windowsfalsetrue + + + + diff --git a/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 new file mode 100644 index 0000000..68d002a --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tools/Build-Bundle.ps1 @@ -0,0 +1,4 @@ +param([ValidateSet('Debug','Release')][string]$Configuration='Release',[string]$Plant3D2026InstallDir) +$ErrorActionPreference='Stop' +dotnet build (Join-Path $PSScriptRoot '..\PRecAnnot2026.sln') --configuration $Configuration /p:Plant3D2026InstallDir=$Plant3D2026InstallDir +Write-Host 'Bundle assembly build completed; deployment is intentionally not performed.' diff --git a/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 new file mode 100644 index 0000000..8eb2f25 --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tools/Validate-Bundle.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference='Stop' +$package=Join-Path $PSScriptRoot '..\bundle\P_Rec_Annot.bundle\PackageContents.xml' +[xml]$xml=Get-Content -LiteralPath $package +$entry=$xml.ApplicationPackage.Components.ComponentEntry +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 -notmatch 'PRecAnnot\.Plant3D2026\.dll'){throw 'Unexpected entry assembly.'} +Write-Host 'Bundle manifest is structurally valid.'