Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c27ee8e5 | ||
|
|
c83ff3d9b3 | ||
|
|
09c53be7f4 | ||
|
|
bdd2c9c888 | ||
|
|
95dba856c8 | ||
|
|
6fa3af6eda | ||
|
|
c616188336 | ||
|
|
bb44b1b1aa |
@@ -0,0 +1,129 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## Назначение проекта
|
||||||
|
|
||||||
|
Этот репозиторий содержит аудит, архитектуру, исходный код, тесты, runtime
|
||||||
|
evidence и документацию миграции корпоративной экосистемы Autodesk AutoCAD
|
||||||
|
Plant 3D 2019 на Autodesk AutoCAD Plant 3D 2026.
|
||||||
|
|
||||||
|
Конечная цель — функционально эквивалентная рабочая схема Plant 3D 2026:
|
||||||
|
корпоративный Launcher, локальная доставка и активация пакетов, managed
|
||||||
|
плагины, команды, Ribbon, Plant project integration, DataLinks, Ortho,
|
||||||
|
аннотации, обновление, диагностика и rollback. Миграция не считается
|
||||||
|
завершённой после успешной компиляции: необходима runtime-проверка реальных
|
||||||
|
сценариев в Plant 3D 2026.
|
||||||
|
|
||||||
|
## Связанные репозитории
|
||||||
|
|
||||||
|
- основной Autodesk-репозиторий: `C:\dev\autodesk`;
|
||||||
|
- Launcher Next: `C:\dev\launcher-next`;
|
||||||
|
- legacy launcher (read-only): `C:\dev\launcher\git`, `C:\dev\p_serv_starter`;
|
||||||
|
- legacy P_Rec_Annot (read-only): `C:\dev\autodesk\link\P_Rec_Annot\v1.0.4\repo`.
|
||||||
|
|
||||||
|
Legacy и deployed-файлы нельзя изменять без отдельного прямого задания.
|
||||||
|
|
||||||
|
## Целевая платформа и принцип миграции
|
||||||
|
|
||||||
|
- Autodesk AutoCAD Plant 3D 2026, series R25.1, Windows x64, .NET 8;
|
||||||
|
- использовать реальные Autodesk 2026 managed assemblies;
|
||||||
|
- managed package загружается локально, не напрямую с UNC;
|
||||||
|
- Autodesk DLL нельзя коммитить, помещать в package/fixture или публиковать.
|
||||||
|
|
||||||
|
Переносится функциональное поведение, а не историческая структура файлов. Не
|
||||||
|
воспроизводить без отдельного доказательства 20 последовательных `NETLOAD`,
|
||||||
|
legacy LISP bootstrap, `OrthoLoader.dll`, загрузку с сети или прежние
|
||||||
|
workaround. Предпочтительная цепочка:
|
||||||
|
|
||||||
|
`Launcher Next → versioned local package → один managed entry assembly → internal dependencies → commands and Ribbon`.
|
||||||
|
|
||||||
|
## Статус legacy-команд
|
||||||
|
|
||||||
|
Ни одна активная команда Plant 3D 2019 не исчезает молча. Для каждой команды
|
||||||
|
фиксируется ровно один статус: `IMPLEMENTED`, `RUNTIME_VERIFIED`,
|
||||||
|
`REPLACED_WITH_NATIVE`, `RETIRED_APPROVED` или `BLOCKED_WITH_REASON`.
|
||||||
|
Aliases сохраняются, пока их удаление не утверждено отдельно.
|
||||||
|
|
||||||
|
## Runtime уровни
|
||||||
|
|
||||||
|
`Smoke` проверяет assembly load, регистрацию команд, Plant context, Ribbon,
|
||||||
|
отсутствие необработанных исключений и отсутствие записи в DWG.
|
||||||
|
|
||||||
|
`DryRun` проверяет реальные Plant data, Ortho/sheet/view definitions, DataLinks,
|
||||||
|
Plant objects, детерминированный план и отсутствие записи в DWG/project DB.
|
||||||
|
|
||||||
|
`Apply` разрешается только отдельным явным заданием: write guard, ограниченная
|
||||||
|
DWG transaction, тестовая копия проекта, baseline, rollback/UNDO, duplicate
|
||||||
|
protection и runtime evidence обязательны. Apply нельзя включать только
|
||||||
|
локальной конфигурацией.
|
||||||
|
|
||||||
|
## Runtime важнее предположений
|
||||||
|
|
||||||
|
Build и unit tests не доказывают Autodesk integration. Для
|
||||||
|
`RUNTIME_VERIFIED` необходимы фактические результаты: команда, module path,
|
||||||
|
DBMOD, Ribbon, DataLinks, Ortho status, telemetry и состояние файлов после
|
||||||
|
закрытия. Не выдумывать UI evidence: использовать `null`, `NOT VERIFIED` или
|
||||||
|
`BLOCKED WITH REASON`.
|
||||||
|
|
||||||
|
На тестовой машине реальный Plant 3D запуск разрешён, если он входит в задачу
|
||||||
|
и есть test branch, локальный fixture, пройденные build/tests, rollback и нет
|
||||||
|
production deploy. Не создавать искусственный review-gate перед Process.Start.
|
||||||
|
Остановка допустима лишь после завершения доступной части работы и при
|
||||||
|
конкретном внешнем blocker: установка, ОС, права, Autodesk runtime, fixture или
|
||||||
|
необходимость production-данных.
|
||||||
|
|
||||||
|
## Test fixtures и отсутствие записи
|
||||||
|
|
||||||
|
Fixtures создаются вне Git, например `<TEST_FIXTURE_ROOT>`. Ortho fixture —
|
||||||
|
настоящий Plant project с Pipe, DataLinks row, line group, Ortho drawing,
|
||||||
|
Ortho viewport и source-model connection; обычный AutoCAD viewport не годится.
|
||||||
|
Production DWG/project использовать нельзя без прямого разрешения.
|
||||||
|
|
||||||
|
Нельзя коммитить DWG, DCF, Project.xml с runtime-data, project DB, Ortho
|
||||||
|
generated files или реальные engineering values.
|
||||||
|
|
||||||
|
Для Smoke/DryRun проверять DBMOD до/после, SHA-256 содержательных project
|
||||||
|
files, отсутствие непредусмотренных transactions/DCF/project-metadata writes.
|
||||||
|
`LastWriteTime` недостаточен. Отделять служебные Plant cache/session files от
|
||||||
|
содержательных данных.
|
||||||
|
|
||||||
|
## Ribbon, механика и evidence
|
||||||
|
|
||||||
|
Ribbon — часть продукта: проверять tab, panel, button, command binding, stable
|
||||||
|
IDs, отсутствие дубликатов при документах/workspace/reinitialization. Команда
|
||||||
|
может быть отдельно рабочей, но plugin slice не завершён без Ribbon evidence
|
||||||
|
или отдельного UI defect.
|
||||||
|
|
||||||
|
Документировать каждую runtime-цепочку: component/project/class/method,
|
||||||
|
input/output, Autodesk transaction mode, writes, telemetry, failure code и
|
||||||
|
rollback action. Документация соответствует фактическому коду.
|
||||||
|
|
||||||
|
Evidence содержит только проверенные значения. Не добавлять username, permit,
|
||||||
|
полный environment, real project paths/line numbers/ObjectId/RowId/properties,
|
||||||
|
полные logs, local manifest или startup scripts. Путь fixture заменять на
|
||||||
|
`<TEST_FIXTURE_ROOT>`.
|
||||||
|
|
||||||
|
## Warnings и Git
|
||||||
|
|
||||||
|
Известный `MSB3277` WindowsBase 4.0.0.0/8.0.0.0 допустим только при полном
|
||||||
|
совпадении с ранее зафиксированным warning. Новые warnings исследовать; не
|
||||||
|
подавлять `NoWarn` без причины.
|
||||||
|
|
||||||
|
Запрещены force push, amend/rebase опубликованной истории, push binaries,
|
||||||
|
изменение неизвестного remote и production deployment из test branch. Перед
|
||||||
|
commit выполнять:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
git diff --cached --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Не смешивать Launcher defect, plugin defect, runtime evidence и unrelated
|
||||||
|
formatting в одном commit.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
Срез завершён, когда реализованы код, build/tests, валидный package и требуемый
|
||||||
|
runtime-сценарий; записаны фактические результаты; выполнен rollback; fixture и
|
||||||
|
production изолированы; механика документирована; ветка опубликована обычным
|
||||||
|
push.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# AGENTS.md — P_Rec_Annot
|
||||||
|
|
||||||
|
## Назначение и package
|
||||||
|
|
||||||
|
`P_Rec_Annot` — приоритетный Plant 3D 2026 plugin для чтения, планирования и
|
||||||
|
последующего создания Ortho-аннотаций; это перенос активного legacy поведения
|
||||||
|
Plant 3D 2019 на .NET 8/Plant 3D 2026.
|
||||||
|
|
||||||
|
Единственная assembly для `NETLOAD` — `PRecAnnot.Plant3D2026.dll`.
|
||||||
|
`PRecAnnot.*.dll` — internal managed dependencies, их нельзя NETLOAD по одной.
|
||||||
|
Использовать Autodesk 2026 references; Autodesk DLL не включать в bundle.
|
||||||
|
|
||||||
|
## Commands и context
|
||||||
|
|
||||||
|
Поддерживаемые команды: `PHP_REC_ANNOT`, `PXP-AAV`, `PXP-OA`. Все
|
||||||
|
регистрируются реальным Autodesk API, работают в Smoke/DryRun, не изменяют DWG
|
||||||
|
в этих режимах и создают одно usage event на явный вызов.
|
||||||
|
|
||||||
|
Project status: `NotFound`, `FoundWithName`, `FoundWithoutDisplayName`,
|
||||||
|
`Ambiguous`, `Failed`. Никогда не выводить `проект '' найден`; для пустого
|
||||||
|
display name: `Plant 3D-проект открыт; отображаемое имя не определено.`
|
||||||
|
Diagnostic code: `PLANT_PROJECT_DISPLAY_NAME_EMPTY`.
|
||||||
|
|
||||||
|
Drawing states: `Ortho`, `NotOrtho`, `Ambiguous`, `Failed`. Обычный DWG не
|
||||||
|
классифицируется как Ortho; для реального Ortho применять Autodesk API и
|
||||||
|
`OrthoDocUtils.IsValidOrthoDrawing`.
|
||||||
|
|
||||||
|
## DryRun и determinism
|
||||||
|
|
||||||
|
Pipeline: `Plant context → Ortho detection → sheet/view definitions → viewport
|
||||||
|
snapshots → DataLinks → Plant properties → independent DTO → deterministic
|
||||||
|
AnnotationPlan → diagnostics → no write`.
|
||||||
|
|
||||||
|
Autodesk objects, transactions и database handles остаются в adapter layer;
|
||||||
|
Core/Application используют только независимые DTO. На неизменном fixture
|
||||||
|
повторный DryRun обязан давать тот же PlanId, counts, порядок операций и warning
|
||||||
|
codes, без side effects. PlanId не включает время, GUID, absolute path,
|
||||||
|
нестабильный ObjectId или пользовательские данные.
|
||||||
|
|
||||||
|
## Write guard, Ribbon, tests
|
||||||
|
|
||||||
|
Все writes проходят централизованный write guard; в Smoke/DryRun он всегда
|
||||||
|
запрещает изменения. Apply не обходится через command handler/adapter.
|
||||||
|
|
||||||
|
Ожидаемый Ribbon путь: `Промхимпроект → Аннотации → Аннотировать`. Требуются
|
||||||
|
stable IDs, отсутствие дублей, binding к `PHP_REC_ANNOT`, устойчивость к
|
||||||
|
документам/workspace и корректный Terminate.
|
||||||
|
|
||||||
|
Unit tests не требуют Autodesk runtime. Runtime evidence отдельно фиксирует
|
||||||
|
initialization, commands, context, Ortho, DataLinks, plan, Ribbon, DBMOD,
|
||||||
|
telemetry и shutdown.
|
||||||
|
|
||||||
|
Bundle содержит только фактический runtime dependency graph и никогда не
|
||||||
|
содержит Autodesk DLL, PDB, test assemblies, legacy 2019 assemblies, bin/obj
|
||||||
|
или production configuration.
|
||||||
|
|
||||||
|
Fixture: local Plant 3D 2026 project вне Git с настоящей Pipe, synthetic line
|
||||||
|
number, Ortho drawing/viewport и SHA baseline до DryRun.
|
||||||
|
|
||||||
|
## Ortho source DataLinks and DCF control
|
||||||
|
|
||||||
|
DataLinks активного Ortho drawing не являются источником Piping rows. Piping
|
||||||
|
rows читаются из source model drawing и Piping project part, разрешённых через
|
||||||
|
Ortho sheet/view definitions.
|
||||||
|
|
||||||
|
Проверка DCF выполняется на парных `control` и `DryRun` fixtures, созданных из
|
||||||
|
одного закрытого состояния проекта. Сравнивать нужно `control delta` с
|
||||||
|
`DryRun delta`: одинаковые изменения классифицируются как baseline behavior
|
||||||
|
Plant 3D, а plugin-only изменения блокируют дальнейшую работу.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"plantProjectDetected": true,
|
||||||
|
"projectDisplayNameStatus": "FoundWithName",
|
||||||
|
"orthoDetection": "Confirmed",
|
||||||
|
"viewportCount": 1,
|
||||||
|
"dataLinksAvailable": true,
|
||||||
|
"dataLinksSheetToSourceMapping": "blocked",
|
||||||
|
"objectsRead": 0,
|
||||||
|
"phpRecAnnotRuns": 2,
|
||||||
|
"pxpAavRuns": 2,
|
||||||
|
"pxpOaRuns": 1,
|
||||||
|
"dbmodBefore": 0,
|
||||||
|
"dbmodAfter": 0,
|
||||||
|
"dwgModified": null,
|
||||||
|
"dcfModified": true,
|
||||||
|
"changedProjectFiles": ["Piping.dcf"],
|
||||||
|
"unhandledExceptions": 0,
|
||||||
|
"applyModeUsed": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Ortho DryRun runtime result
|
||||||
|
|
||||||
|
Date: 2026-08-03
|
||||||
|
|
||||||
|
The local test fixture's `TestPipeOrtho.dwg` was detected as Ortho with one
|
||||||
|
viewport. In `DryRun`, `PHP_REC_ANNOT` ran twice, `PXP-AAV` ran twice, and
|
||||||
|
`PXP-OA` ran once. All completed without an unhandled exception; `DBMOD` was
|
||||||
|
`0` before and after.
|
||||||
|
|
||||||
|
`PlantProject.Name` was available for this fixture and rendered as
|
||||||
|
`PRecAnnotTestProject`.
|
||||||
|
|
||||||
|
The Ortho sheet did not expose direct Piping rows through
|
||||||
|
`DataLinksManager.SelectAcPpRowIds`; that API threw `DLException: NotFound`.
|
||||||
|
The adapter now converts this to an empty read-only result. Consequently the
|
||||||
|
run read zero source Plant objects and produced an empty plan. Mapping the
|
||||||
|
Ortho sheet to source-model objects remains the blocker for non-empty
|
||||||
|
AnnotationPlan validation.
|
||||||
|
|
||||||
|
The test fixture SHA baseline changed for `Piping.dcf` before the first DryRun
|
||||||
|
command. This run therefore does not prove DCF invariance, although no plugin
|
||||||
|
write operation was performed and DBMOD remained unchanged.
|
||||||
@@ -8,6 +8,7 @@ public sealed record RuntimeDiagnostic
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
|
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
|
||||||
|
public enum PlantProjectDetectionStatus { NotFound, FoundWithName, FoundWithoutDisplayName, Ambiguous, Failed }
|
||||||
|
|
||||||
public sealed record OrthoDetectionResult
|
public sealed record OrthoDetectionResult
|
||||||
{
|
{
|
||||||
@@ -66,6 +67,7 @@ public sealed record PlantRuntimeContext
|
|||||||
public bool IsOrthoDrawing { get; init; }
|
public bool IsOrthoDrawing { get; init; }
|
||||||
public string? DrawingName { get; init; }
|
public string? DrawingName { get; init; }
|
||||||
public string? ProjectDisplayName { get; init; }
|
public string? ProjectDisplayName { get; init; }
|
||||||
|
public PlantProjectDetectionStatus ProjectDetectionStatus { get; init; } = PlantProjectDetectionStatus.NotFound;
|
||||||
public IReadOnlyList<OrthoViewportInfo> Viewports { get; init; } = Array.Empty<OrthoViewportInfo>();
|
public IReadOnlyList<OrthoViewportInfo> Viewports { get; init; } = Array.Empty<OrthoViewportInfo>();
|
||||||
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty<string>(), Diagnostics = Array.Empty<RuntimeDiagnostic>() };
|
public OrthoDetectionResult Detection { get; init; } = new() { Status = OrthoDetectionStatus.Unsupported, Evidence = Array.Empty<string>(), Diagnostics = Array.Empty<RuntimeDiagnostic>() };
|
||||||
@@ -74,6 +76,62 @@ public sealed record PlantRuntimeContext
|
|||||||
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
public interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
||||||
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
public interface IOrthoViewportReader { IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken); }
|
||||||
|
|
||||||
|
public enum SourceDrawingResolutionStatus { Resolved, NotReferenced, Ambiguous, MissingFromProject, Unsupported, Failed }
|
||||||
|
|
||||||
|
public sealed record OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
public required string StableId { get; init; }
|
||||||
|
public required string DisplayName { get; init; }
|
||||||
|
public string? ProjectRelativePath { get; init; }
|
||||||
|
public long? ProjectDrawingRowId { get; init; }
|
||||||
|
public required SourceDrawingResolutionStatus Status { get; init; }
|
||||||
|
public IReadOnlyList<string> EvidenceCodes { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SourcePlantObjectSnapshot
|
||||||
|
{
|
||||||
|
public required string StableObjectKey { get; init; }
|
||||||
|
public required string ClassName { get; init; }
|
||||||
|
public string? SourceDrawingStableId { get; init; }
|
||||||
|
public string? LineGroupStableKey { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record SourceDrawingReadResult
|
||||||
|
{
|
||||||
|
public required OrthoSourceDrawingReference Source { get; init; }
|
||||||
|
public IReadOnlyList<SourcePlantObjectSnapshot> Objects { get; init; } = Array.Empty<SourcePlantObjectSnapshot>();
|
||||||
|
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IOrthoSourceDrawingResolver
|
||||||
|
{
|
||||||
|
IReadOnlyList<OrthoSourceDrawingReference> Resolve(OrthoDrawingSnapshot drawing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
public required string StableId { get; init; }
|
||||||
|
public IReadOnlyList<OrthoViewSourceSnapshot> Views { get; init; } = Array.Empty<OrthoViewSourceSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record OrthoViewSourceSnapshot
|
||||||
|
{
|
||||||
|
public required string ViewStableId { get; init; }
|
||||||
|
public IReadOnlyList<string> ReferencedDrawingPaths { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISourceDrawingDataLinksReader
|
||||||
|
{
|
||||||
|
SourceDrawingReadResult Read(PlantProjectContext project, OrthoSourceDrawingReference source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record PlantProjectContext
|
||||||
|
{
|
||||||
|
public required string StableProjectId { get; init; }
|
||||||
|
public string? ProjectDisplayName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
public enum AnnotationOperationKind { Create, Update, Skip, Conflict, Unsupported }
|
||||||
|
|
||||||
public sealed record AnnotationOperationPlan
|
public sealed record AnnotationOperationPlan
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class OrthoSourceDrawingResolver : IOrthoSourceDrawingResolver
|
||||||
|
{
|
||||||
|
public IReadOnlyList<OrthoSourceDrawingReference> Resolve(OrthoDrawingSnapshot drawing)
|
||||||
|
{
|
||||||
|
var candidates = drawing.Views
|
||||||
|
.OrderBy(x => x.ViewStableId, StringComparer.Ordinal)
|
||||||
|
.SelectMany(x => x.ReferencedDrawingPaths)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Select(x => x.Trim().Replace('\\', '/'))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(path => new OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
StableId = StableId(drawing.StableId, path),
|
||||||
|
DisplayName = Path.GetFileName(path),
|
||||||
|
ProjectRelativePath = path,
|
||||||
|
Status = SourceDrawingResolutionStatus.Resolved,
|
||||||
|
EvidenceCodes = new[] { "SOURCE_FROM_VIEW_DEFINITION", "SOURCE_PATH_MATCHED_PROJECT_RELATIVE" }
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return candidates.Length != 0
|
||||||
|
? candidates
|
||||||
|
: new[]
|
||||||
|
{
|
||||||
|
new OrthoSourceDrawingReference
|
||||||
|
{
|
||||||
|
StableId = StableId(drawing.StableId, "none"),
|
||||||
|
DisplayName = "source drawing unavailable",
|
||||||
|
Status = SourceDrawingResolutionStatus.NotReferenced,
|
||||||
|
EvidenceCodes = new[] { "ORTHO_SOURCE_DRAWING_NOT_RESOLVED" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StableId(string drawingId, string source)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingId}|{source}"));
|
||||||
|
return "src-" + Convert.ToHexString(hash)[..20];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
using Autodesk.ProcessPower.DataLinks;
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
using Autodesk.ProcessPower.P3dProjectParts;
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
@@ -32,6 +33,9 @@ internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
|||||||
return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден.");
|
return Empty(true, false, "NO_PLANT_PROJECT", "Активный Plant 3D-проект не найден.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(project.Name) ? null : project.Name.Trim();
|
||||||
|
var projectStatus = displayName is null ? PlantProjectDetectionStatus.FoundWithoutDisplayName : PlantProjectDetectionStatus.FoundWithName;
|
||||||
|
|
||||||
OrthoProject? orthoProject;
|
OrthoProject? orthoProject;
|
||||||
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
|
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -60,9 +64,10 @@ internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
|||||||
HasPlantProject = true,
|
HasPlantProject = true,
|
||||||
IsOrthoDrawing = true,
|
IsOrthoDrawing = true,
|
||||||
DrawingName = Path.GetFileName(document.Name),
|
DrawingName = Path.GetFileName(document.Name),
|
||||||
ProjectDisplayName = project.Name,
|
ProjectDisplayName = displayName ?? "display name unavailable",
|
||||||
|
ProjectDetectionStatus = projectStatus,
|
||||||
Viewports = viewports,
|
Viewports = viewports,
|
||||||
Diagnostics = Array.Empty<RuntimeDiagnostic>(),
|
Diagnostics = displayName is null ? new[] { new RuntimeDiagnostic { Code = "PLANT_PROJECT_DISPLAY_NAME_EMPTY", Message = "Plant project is open but no display name is available.", Severity = "Warning" } } : Array.Empty<RuntimeDiagnostic>(),
|
||||||
Detection = new OrthoDetectionResult
|
Detection = new OrthoDetectionResult
|
||||||
{
|
{
|
||||||
Status = OrthoDetectionStatus.Confirmed,
|
Status = OrthoDetectionStatus.Confirmed,
|
||||||
@@ -82,6 +87,8 @@ internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
|||||||
HasActiveDocument = hasDocument,
|
HasActiveDocument = hasDocument,
|
||||||
HasPlantProject = hasProject,
|
HasPlantProject = hasProject,
|
||||||
IsOrthoDrawing = false,
|
IsOrthoDrawing = false,
|
||||||
|
ProjectDisplayName = hasProject ? "display name unavailable" : null,
|
||||||
|
ProjectDetectionStatus = hasProject ? PlantProjectDetectionStatus.Ambiguous : PlantProjectDetectionStatus.NotFound,
|
||||||
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
|
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
|
||||||
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
|
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
|
||||||
};
|
};
|
||||||
@@ -124,7 +131,7 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
|||||||
TwistAngle = 0,
|
TwistAngle = 0,
|
||||||
IsOn = true,
|
IsOn = true,
|
||||||
IsLocked = false,
|
IsLocked = false,
|
||||||
SourceDrawingName = sheet.DwgName,
|
SourceDrawingName = ReadSourceDrawingPath(view),
|
||||||
ViewName = view.ViewName
|
ViewName = view.ViewName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -143,6 +150,28 @@ internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
|||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
||||||
return "vp-" + Convert.ToHexString(hash)[..20];
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? ReadSourceDrawingPath(object view)
|
||||||
|
{
|
||||||
|
// The Plant 3D 2026 view definition owns a DwgQuery. Its ModelFiles are
|
||||||
|
// the authoritative source-model references; sheet.DwgName is the Ortho
|
||||||
|
// sheet and must not be used for Piping DataLinks.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = view.GetType().GetProperty("DwgQuery")?.GetValue(view);
|
||||||
|
var files = query?.GetType().GetProperty("ModelFiles")?.GetValue(query) as System.Collections.IEnumerable;
|
||||||
|
if (files is null) return null;
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
var value = file as string
|
||||||
|
?? file?.GetType().GetProperty("ModelFileName")?.GetValue(file)?.ToString()
|
||||||
|
?? file?.GetType().GetProperty("Value")?.GetValue(file)?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class PlantObjectReader : IPlantObjectReader
|
internal sealed class PlantObjectReader : IPlantObjectReader
|
||||||
@@ -150,42 +179,55 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
if (document is null) return Array.Empty<PlantObjectSnapshot>();
|
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
DataLinksManager? dataLinks;
|
DataLinksManager? dataLinks;
|
||||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
||||||
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
||||||
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
|
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
|
if (!Path.IsPathRooted(viewport.SourceDrawingName) || !File.Exists(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
var snapshots = new List<PlantObjectSnapshot>();
|
var snapshots = new List<PlantObjectSnapshot>();
|
||||||
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
|
try
|
||||||
{
|
{
|
||||||
try
|
using var sourceDatabase = new Database(false, true);
|
||||||
|
sourceDatabase.ReadDwgFile(viewport.SourceDrawingName, FileShare.ReadWrite, true, null);
|
||||||
|
sourceDatabase.CloseInput(true);
|
||||||
|
foreach (var rowId in dataLinks.SelectAcPpRowIds(sourceDatabase))
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
try
|
||||||
var className = dataLinks.GetObjectClassname(rowId);
|
|
||||||
var properties = dataLinks.GetAllProperties(rowId, true)
|
|
||||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
|
||||||
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
|
||||||
snapshots.Add(new PlantObjectSnapshot
|
|
||||||
{
|
{
|
||||||
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
ObjectClass = className ?? string.Empty,
|
var className = dataLinks.GetObjectClassname(rowId);
|
||||||
Tag = Value("Tag", "TAG"),
|
var properties = dataLinks.GetAllProperties(rowId, true)
|
||||||
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
SupportNumber = Value("Support Number", "SupportNumber"),
|
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
||||||
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
||||||
Geometry = new GeometrySnapshot(),
|
snapshots.Add(new PlantObjectSnapshot
|
||||||
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
{
|
||||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
SnapshotId = StableId(viewport.SourceDrawingName, rowId, className),
|
||||||
});
|
ObjectClass = className ?? string.Empty,
|
||||||
}
|
Tag = Value("Tag", "TAG"),
|
||||||
catch
|
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||||
{
|
SupportNumber = Value("Support Number", "SupportNumber"),
|
||||||
// Keep the read path best-effort; no write operation is attempted.
|
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
||||||
|
Geometry = new GeometrySnapshot(),
|
||||||
|
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
||||||
|
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep the read path best-effort; no write operation is attempted.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (DLException)
|
||||||
|
{
|
||||||
|
// An Ortho sheet can have no direct Piping DataLinks rows. The caller
|
||||||
|
// receives an empty read-only result rather than a command failure.
|
||||||
|
}
|
||||||
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,40 @@ public sealed class FoundationTests
|
|||||||
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
|
Assert.Equal(AnnotationOperationKind.Conflict, plan.Operations.Last().Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_OneView_ResolvesOneModelDrawing()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
StableId = "sheet",
|
||||||
|
Views = new[] { new OrthoViewSourceSnapshot { ViewStableId = "view", ReferencedDrawingPaths = new[] { "Plant 3D Models/TestPipeModel.dwg" } } }
|
||||||
|
});
|
||||||
|
var source = Assert.Single(result);
|
||||||
|
Assert.Equal(SourceDrawingResolutionStatus.Resolved, source.Status);
|
||||||
|
Assert.Equal("TestPipeModel.dwg", source.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_DuplicateViews_DeduplicatesSource()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot
|
||||||
|
{
|
||||||
|
StableId = "sheet",
|
||||||
|
Views = new[]
|
||||||
|
{
|
||||||
|
new OrthoViewSourceSnapshot { ViewStableId = "a", ReferencedDrawingPaths = new[] { "models/A.dwg" } },
|
||||||
|
new OrthoViewSourceSnapshot { ViewStableId = "b", ReferencedDrawingPaths = new[] { "models/A.dwg" } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Assert.Single(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceResolver_MissingSource_ReturnsDiagnostic()
|
||||||
|
{
|
||||||
|
var result = new OrthoSourceDrawingResolver().Resolve(new OrthoDrawingSnapshot { StableId = "sheet" });
|
||||||
|
var source = Assert.Single(result);
|
||||||
|
Assert.Equal(SourceDrawingResolutionStatus.NotReferenced, source.Status);
|
||||||
|
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", source.EvidenceCodes);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
[Fact] public void ApplyMode_IsBlocked() => Assert.Throws<InvalidOperationException>(() => new ExecutionModeWriteGuard(PRecAnnotExecutionMode.Apply).EnsureWriteAllowed("test"));
|
||||||
|
|
||||||
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||||
|
|||||||
Reference in New Issue
Block a user