Compare commits

..
3 Commits
12 changed files with 215 additions and 30 deletions
+2
View File
@@ -1,4 +1,6 @@
artifacts/
.local-dotnet-sdk/
.local-tools/
bin/
obj/
.vs/
@@ -6,6 +6,8 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Deterministic>true</Deterministic>
<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>
</Project>
+27 -1
View File
@@ -1,3 +1,29 @@
# 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.
@@ -1,3 +1,16 @@
# 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 | Not executed in AutoCAD/Plant process |
| Production deploy | NOT PERFORMED |
Fixed API assumptions: foundation-only lifecycle became `IExtensionApplication`; command descriptors became actual `CommandMethod` registrations; Plant project detection uses `PlantApplication.CurrentProject`; Ribbon uses `ComponentManager.Ribbon`, `RibbonTab`, `RibbonPanelSource`, and `RibbonButton`. Remaining blockers are runtime loading, Ortho API/data-link mapping, safe DWG transaction implementation, and golden fixture validation.
@@ -1,3 +1,14 @@
# 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.
@@ -0,0 +1,80 @@
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.ProcessPower.PlantInstance;
using Autodesk.Windows;
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
namespace PRecAnnot.Plant3D2026;
public sealed class EntryPoint : IExtensionApplication
{
public void Initialize() => RibbonIntegration.Register();
public void Terminate() { }
[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)
{
try
{
Document? document = AcadApplication.DocumentManager.MdiActiveDocument;
if (document is null)
return;
Editor editor = document.Editor;
if (PlantApplication.CurrentProject is null)
{
editor.WriteMessage("\nP_Rec_Annot: активный Plant 3D project не найден; DWG не изменён.");
return;
}
editor.WriteMessage($"\nP_Rec_Annot {commandName}: Plant 3D project '{PlantApplication.CurrentProject.Name}' найден; diagnostic-only, DWG не изменён.");
}
catch (System.Exception exception)
{
try { AcadApplication.DocumentManager.MdiActiveDocument?.Editor.WriteMessage($"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}"); }
catch { }
}
}
}
internal static class RibbonIntegration
{
private const string TabId = "PRecAnnot.Plant3D2026";
private const string Command = "PHP_REC_ANNOT ";
public static void Register()
{
try
{
RibbonControl? ribbon = ComponentManager.Ribbon;
if (ribbon is null) return;
RibbonTab? tab = ribbon.Tabs.FirstOrDefault(item => string.Equals(item.Id, TabId, StringComparison.OrdinalIgnoreCase));
if (tab is null)
{
tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" };
ribbon.Tabs.Add(tab);
}
if (tab.Panels.Any(panel => string.Equals(panel.Source?.Title, "Аннотации", StringComparison.OrdinalIgnoreCase))) return;
RibbonPanelSource source = new() { Title = "Аннотации" };
source.Items.Add(new RibbonButton { Text = "Аннотировать", ShowText = true, CommandParameter = Command });
tab.Panels.Add(new RibbonPanel { Source = source });
}
catch
{
// Ribbon is optional; command-line registration remains available.
}
}
}
@@ -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,17 @@
<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>
<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 Condition="Exists('$(AutoCAD2026ManagedDir)') and Exists('$(Plant3D2026ManagedDir)')">
<Reference Include="AcMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AcDbMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcDbMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AcCoreMgd"><HintPath>$(AutoCAD2026ManagedDir)\AcCoreMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="AdWindows"><HintPath>$(AutoCAD2026ManagedDir)\AdWindows.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
<Reference Include="PnPDataLinks"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataLinks.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>
</ItemGroup>
</Project>
@@ -4,7 +4,7 @@ public sealed record Plant3D2026Availability(bool IsAvailable, string InstallDir
{
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();
return new(missing.Length == 0, installDirectory, missing);
}
@@ -7,6 +7,16 @@ namespace PRecAnnot.Core.Tests;
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()
{
var engine = new AnnotationRuleEngine();
@@ -34,4 +44,46 @@ public sealed class FoundationTests
var ribbon = new RibbonRegistration(); var registry = CommandRegistry.Foundation();
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);
}
}
@@ -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'
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.'
@@ -1,8 +1,13 @@
param([string]$BundleRoot=(Join-Path $PSScriptRoot '..\artifacts\P_Rec_Annot.bundle'))
$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
$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.'}
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.'