feat: scaffold P_Rec_Annot .NET 8 architecture
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user