feat: scaffold P_Rec_Annot .NET 8 architecture
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
artifacts/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
.vs/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<DebugType>portable</DebugType>
|
||||||
|
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == ''">$(ProgramFiles)\Autodesk\AutoCAD 2026</Plant3D2026InstallDir>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.1.0" Author="NBIM">
|
||||||
|
<Components>
|
||||||
|
<!-- SeriesMin/SeriesMax and platform token are pending official/runtime verification. -->
|
||||||
|
<RuntimeRequirements OS="Win64" />
|
||||||
|
<ComponentEntry AppName="PRecAnnot2026" ModuleName="Contents\Windows\PRecAnnot.Plant3D2026.dll" AppDescription="P_Rec_Annot entry assembly" LoadReasons="LoadOnAutoCADStartup" />
|
||||||
|
</Components>
|
||||||
|
</ApplicationPackage>
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
@@ -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.
|
||||||
@@ -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 |
|
||||||
@@ -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.
|
||||||
@@ -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<PlantObjectData> Objects { get; init; }
|
||||||
|
public required IReadOnlyList<ExistingAnnotation> 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<AnnotationIntent> Create { get; init; }
|
||||||
|
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
||||||
|
public required IReadOnlyList<string> Delete { get; init; }
|
||||||
|
public required IReadOnlyList<string> 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<OrthoViewportRef> GetViewports();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IPlantObjectReader
|
||||||
|
{
|
||||||
|
IReadOnlyList<PlantObjectData> ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAnnotationRepository
|
||||||
|
{
|
||||||
|
IReadOnlyList<ExistingAnnotation> 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -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<string, PRecAnnotCommandDescriptor> _commands;
|
||||||
|
|
||||||
|
public CommandRegistry(IEnumerable<PRecAnnotCommandDescriptor> 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<PRecAnnotCommandDescriptor> 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" }
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
|
||||||
|
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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<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))
|
||||||
|
{
|
||||||
|
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<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,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
|
||||||
|
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><PlatformTarget>x64</PlatformTarget><UseWindowsForms>true</UseWindowsForms></PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" />
|
||||||
|
<ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\PRecAnnot.Core\PRecAnnot.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup Condition="Exists('$(Plant3D2026InstallDir)')">
|
||||||
|
<Reference Include="AcMgd"><HintPath>$(Plant3D2026InstallDir)\AcMgd.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="AcDbMgd"><HintPath>$(Plant3D2026InstallDir)\AcDbMgd.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="AcCoreMgd"><HintPath>$(Plant3D2026InstallDir)\AcCoreMgd.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="PnPProjectManagerMgd"><HintPath>$(Plant3D2026InstallDir)\PLNT3D\PnPProjectManagerMgd.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
<Reference Include="PnPDataLinks"><HintPath>$(Plant3D2026InstallDir)\PLNT3D\PnPDataLinks.dll</HintPath><Private>false</Private></Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDirectory, IReadOnlyList<string> 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)}") { }
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0</TargetFramework></PropertyGroup>
|
||||||
|
<ItemGroup><ProjectReference Include="..\PRecAnnot.Abstractions\PRecAnnot.Abstractions.csproj" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><UseWindowsForms>true</UseWindowsForms></PropertyGroup>
|
||||||
|
<ItemGroup><ProjectReference Include="..\PRecAnnot.Application\PRecAnnot.Application.csproj" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using PRecAnnot.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.UI;
|
||||||
|
|
||||||
|
public sealed class RibbonRegistration
|
||||||
|
{
|
||||||
|
private readonly HashSet<string> _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;
|
||||||
|
}
|
||||||
@@ -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<ExistingAnnotation>() };
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup><TargetFramework>net8.0-windows</TargetFramework><IsPackable>false</IsPackable><IsTestProject>true</IsTestProject></PropertyGroup>
|
||||||
|
<ItemGroup><ProjectReference Include="..\..\src\PRecAnnot.Core\PRecAnnot.Core.csproj" /><ProjectReference Include="..\..\src\PRecAnnot.Application\PRecAnnot.Application.csproj" /><ProjectReference Include="..\..\src\PRecAnnot.UI\PRecAnnot.UI.csproj" /></ItemGroup>
|
||||||
|
<ItemGroup><PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /><PackageReference Include="xunit" Version="2.5.3" /><PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" /></ItemGroup>
|
||||||
|
<ItemGroup><Using Include="Xunit" /></ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -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.'
|
||||||
@@ -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.'
|
||||||
Reference in New Issue
Block a user