Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c27ee8e5 | ||
|
|
c83ff3d9b3 | ||
|
|
09c53be7f4 | ||
|
|
bdd2c9c888 | ||
|
|
95dba856c8 | ||
|
|
6fa3af6eda | ||
|
|
c616188336 | ||
|
|
bb44b1b1aa | ||
|
|
ea3a9e157e | ||
|
|
837ac1d994 | ||
|
|
142b5f8b93 | ||
|
|
dcf95d7e3a | ||
|
|
c733a27cbb | ||
|
|
9a7a6cd678 |
@@ -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.
|
||||
@@ -1,4 +1,6 @@
|
||||
artifacts/
|
||||
.local-dotnet-sdk/
|
||||
.local-tools/
|
||||
bin/
|
||||
obj/
|
||||
.vs/
|
||||
|
||||
@@ -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 изменения блокируют дальнейшую работу.
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"executionMode": "DryRun",
|
||||
"runtimeExecuted": false,
|
||||
"plantProjectDetected": null,
|
||||
"orthoDetection": "NotExecuted",
|
||||
"viewportCount": null,
|
||||
"dataLinksAvailable": null,
|
||||
"objectsRead": null,
|
||||
"plan": {
|
||||
"create": null,
|
||||
"update": null,
|
||||
"skip": null,
|
||||
"conflict": null,
|
||||
"unsupported": null
|
||||
},
|
||||
"planDeterministic": true,
|
||||
"dbmodBefore": null,
|
||||
"dbmodAfter": null,
|
||||
"dwgModified": null,
|
||||
"dcfModified": null,
|
||||
"projectXmlModified": null,
|
||||
"unhandledExceptions": null,
|
||||
"blocker": "No controlled test Ortho DWG/project path was supplied or discovered; runtime DryRun was not claimed."
|
||||
}
|
||||
@@ -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,26 @@
|
||||
{
|
||||
"product": "Autodesk AutoCAD Plant 3D 2026",
|
||||
"manualNetload": true,
|
||||
"bundleLoaded": true,
|
||||
"automaticAppLoaderChecked": false,
|
||||
"smokeModeConfirmedByOperator": true,
|
||||
"observedCommandLogRuntimeMode": "Normal",
|
||||
"assemblyInitializeCountObserved": 1,
|
||||
"commands": {
|
||||
"PHP_REC_ANNOT": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
|
||||
"PXP-AAV": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" },
|
||||
"PXP-OA": { "registered": true, "executed": true, "usageEvents": 1, "result": "PreflightAccepted" }
|
||||
},
|
||||
"ribbon": { "created": true, "duplicateCheckedExhaustively": false },
|
||||
"plantContext": { "projectDetected": true, "projectDisplayName": "DefaultProject", "observedOrthoDetected": false, "dataLinksAvailable": true },
|
||||
"writesBlocked": true,
|
||||
"dbmodChecked": false,
|
||||
"dwgModified": false,
|
||||
"dcfModified": false,
|
||||
"projectXmlModified": false,
|
||||
"telemetryEventsExpected": 3,
|
||||
"telemetryEventsActual": 3,
|
||||
"unhandledExceptions": 0,
|
||||
"temporaryInstallationRemoved": true,
|
||||
"productionDeploy": false
|
||||
}
|
||||
@@ -1,3 +1,18 @@
|
||||
# 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 smoke | Manual NETLOAD confirmed on Plant 3D 2026; automatic AppLoader and Ortho DryRun remain unverified |
|
||||
| Ortho read path | Compiled against `OrthoDocUtils`, `OrthoProject`, `DwgSheetDefinitions`, `DwgViewDefs`, and `DataLinksManager`; runtime fixture pending |
|
||||
| Dry-run plan | Deterministic Core builder with DTO boundary and Apply guard; no annotation writes |
|
||||
| Production deploy | NOT PERFORMED |
|
||||
|
||||
Fixed API assumptions: foundation-only lifecycle became `IExtensionApplication`; command descriptors became actual `CommandMethod` registrations; Ortho detection uses `OrthoDocUtils.IsValidOrthoDrawing` plus the Ortho project part; viewport metadata uses `DwgSheetDefinitions`/`DwgViewDefs`; object properties use read-only `DataLinksManager.SelectAcPpRowIds` and `GetAllProperties`; Core receives only immutable DTOs. Remaining blockers are a controlled Ortho fixture, geometry/object-to-viewport mapping, legacy source provenance, and future Apply implementation.
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,9 @@
|
||||
# PXP-AAV read-path analysis
|
||||
|
||||
Status: traced to the existing command registration and the shared dry-run plan path; annotation writes are not implemented in this slice.
|
||||
|
||||
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| PXP-AAV-001 | PXP-AAV | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is recorded, but the requested legacy checkout is absent on this workstation | read objects visible to an Ortho view and build a dry-run plan | adapter DTO -> `AnnotationPlan` | `AnnotationPlan_InputOrderDoesNotChangeResult` | partial |
|
||||
|
||||
The implementation deliberately does not claim behavioral equivalence. The next required input is a read-only copy of the legacy source or a golden test Ortho drawing with expected diagnostics.
|
||||
@@ -0,0 +1,9 @@
|
||||
# PXP-OA read-path analysis
|
||||
|
||||
Status: command is a real `CommandMethod` and uses the same safe context/read-plan pipeline. No Ortho annotation is created, updated, or deleted.
|
||||
|
||||
| RuleId | LegacyCommand | LegacyProject | LegacyFile | ObservedBehavior | TargetUseCase | TargetClass | Test | Status |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| PXP-OA-001 | PXP-OA | legacy audit | `docs/legacy-rule-provenance.csv` | legacy provenance is available, but source checkout was not found locally | Ortho context and DataLinks read diagnostics | adapter DTO -> `AnnotationPlan` | `ApplyMode_IsBlocked` | partial |
|
||||
|
||||
Exact object filters, annotation text, placement rules, and existing-annotation detection remain unconfirmed until the legacy source or a controlled golden fixture is supplied.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Plant 3D 2026 runtime smoke results
|
||||
|
||||
## Scope
|
||||
|
||||
The smoke test used a temporary per-user bundle installation only. Production deploy was not performed. The temporary installation was removed after AutoCAD closed.
|
||||
|
||||
## Confirmed
|
||||
|
||||
- Manual `NETLOAD` loaded the P_Rec_Annot entry assembly in AutoCAD Plant 3D 2026.
|
||||
- `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA` were accepted by the command line.
|
||||
- The active Plant project was detected as `DefaultProject`.
|
||||
- The user confirmed Smoke mode behavior and no DWG mutation during the manual smoke interaction.
|
||||
- The runtime log contains one assembly initialization and one normal termination for the observed process.
|
||||
- The runtime log records Ribbon creation with status `Created`.
|
||||
- The runtime log records exactly one `CommandUsageEvent` for each of the three commands.
|
||||
- The observed Plant context reported `DataLinksAvailable=true` and `IsOrthoDrawing=false` for `Floor Plan Sample.dwg`.
|
||||
- No unhandled exception was reported in the captured runtime log.
|
||||
|
||||
## Not confirmed
|
||||
|
||||
- Automatic AppLoader loading without `NETLOAD` was not tested.
|
||||
- DBMOD and byte-level DWG/DCF/Project.xml comparisons were not captured by the smoke evidence.
|
||||
- Ribbon duplicate behavior across workspace/document lifecycle was not exhaustively tested.
|
||||
- A command invocation logged as `RuntimeMode=Smoke` is not present in the captured JSONL; the three detailed command records have `RuntimeMode=Normal`.
|
||||
|
||||
## Evidence handling
|
||||
|
||||
The detailed runtime JSONL remains local under `%LOCALAPPDATA%\Promhimproekt\PRecAnnot2026\Logs\` and is not committed. No DWG, DCF, Project.xml, Autodesk DLL, user identity, project path, or full local runtime path is included in the repository.
|
||||
|
||||
Temporary installation: PERFORMED AND REMOVED.
|
||||
Production deploy: NOT PERFORMED.
|
||||
@@ -1,3 +1,19 @@
|
||||
# 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.
|
||||
# Current slice: Ortho read / dry-run
|
||||
|
||||
`P_REC_ANNOT_RUNTIME_MODE=Smoke` reads context only. `DryRun` reads Ortho view metadata and DataLinks properties, builds an immutable deterministic `AnnotationPlan`, and never applies it. `Apply` is blocked with `APPLY_MODE_NOT_IMPLEMENTED`; unknown values resolve to `DryRun`.
|
||||
|
||||
The read adapter uses `OrthoDocUtils.IsValidOrthoDrawing`, `OrthoProject.DwgSheetDefinitions`, `IDwgViewDef`, and read-only `DataLinksManager` methods. The adapter does not expose AutoCAD or Plant objects to Core and contains no write calls. A runtime DryRun result is not published until a controlled test Ortho DWG is available; see `docs/evidence/ortho-dry-run-summary.json`.
|
||||
|
||||
@@ -1,5 +1,175 @@
|
||||
namespace PRecAnnot.Abstractions;
|
||||
|
||||
public sealed record RuntimeDiagnostic
|
||||
{
|
||||
public required string Code { get; init; }
|
||||
public required string Message { get; init; }
|
||||
public string Severity { get; init; } = "Warning";
|
||||
}
|
||||
|
||||
public enum OrthoDetectionStatus { Confirmed, NotOrtho, Ambiguous, Unsupported, Failed }
|
||||
public enum PlantProjectDetectionStatus { NotFound, FoundWithName, FoundWithoutDisplayName, Ambiguous, Failed }
|
||||
|
||||
public sealed record OrthoDetectionResult
|
||||
{
|
||||
public required OrthoDetectionStatus Status { get; init; }
|
||||
public required IReadOnlyList<string> Evidence { get; init; }
|
||||
public required IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; }
|
||||
}
|
||||
|
||||
public readonly record struct Point2Dto(double X, double Y);
|
||||
public readonly record struct Point3Dto(double X, double Y, double Z);
|
||||
public readonly record struct Vector2Dto(double X, double Y);
|
||||
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
|
||||
|
||||
public sealed record GeometrySnapshot
|
||||
{
|
||||
public Point3Dto? WorldPosition { get; init; }
|
||||
public Point2Dto? PaperPosition { get; init; }
|
||||
public Bounds2Dto? ProjectedBounds { get; init; }
|
||||
public double? Rotation { get; init; }
|
||||
public double? Elevation { get; init; }
|
||||
}
|
||||
|
||||
public sealed record OrthoViewportInfo
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string DisplayName { get; init; }
|
||||
public double CenterX { get; init; }
|
||||
public double CenterY { get; init; }
|
||||
public double Width { get; init; }
|
||||
public double Height { get; init; }
|
||||
public double Scale { get; init; }
|
||||
public double TwistAngle { get; init; }
|
||||
public bool IsOn { get; init; }
|
||||
public bool IsLocked { get; init; }
|
||||
public string? SourceDrawingName { get; init; }
|
||||
public string? ViewName { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PlantObjectSnapshot
|
||||
{
|
||||
public required string SnapshotId { get; init; }
|
||||
public required string ObjectClass { get; init; }
|
||||
public string? Tag { get; init; }
|
||||
public string? LineNumber { get; init; }
|
||||
public string? SupportNumber { get; init; }
|
||||
public string? EquipmentNumber { get; init; }
|
||||
public required GeometrySnapshot Geometry { get; init; }
|
||||
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed record PlantRuntimeContext
|
||||
{
|
||||
public bool HasActiveDocument { get; init; }
|
||||
public bool HasPlantProject { get; init; }
|
||||
public bool IsOrthoDrawing { get; init; }
|
||||
public string? DrawingName { 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<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 interface IPlantRuntimeContextReader { PlantRuntimeContext ReadCurrentContext(); }
|
||||
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 sealed record AnnotationOperationPlan
|
||||
{
|
||||
public required string SourceSnapshotId { get; init; }
|
||||
public required AnnotationOperationKind Kind { get; init; }
|
||||
public string? AnnotationType { get; init; }
|
||||
public string? Text { get; init; }
|
||||
public Point2Dto? Position { get; init; }
|
||||
public double? Rotation { get; init; }
|
||||
public string? ExistingAnnotationId { get; init; }
|
||||
public required IReadOnlyList<string> Reasons { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AnnotationPlanSummary
|
||||
{
|
||||
public int ObjectsExamined { get; init; }
|
||||
public int CreateCount { get; init; }
|
||||
public int UpdateCount { get; init; }
|
||||
public int SkipCount { get; init; }
|
||||
public int ConflictCount { get; init; }
|
||||
public int UnsupportedCount { get; init; }
|
||||
public int WarningCount { get; init; }
|
||||
public int ErrorCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed record AnnotationPlanningRequest
|
||||
{
|
||||
public required string CommandName { get; init; }
|
||||
public required OrthoViewportInfo Viewport { get; init; }
|
||||
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
||||
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
||||
}
|
||||
|
||||
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
||||
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
||||
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
||||
|
||||
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
||||
|
||||
public sealed record PlantObjectData
|
||||
@@ -36,6 +206,11 @@ public sealed record AnnotationIntent
|
||||
|
||||
public sealed record AnnotationPlan
|
||||
{
|
||||
public string PlanId { get; init; } = string.Empty;
|
||||
public string CommandName { get; init; } = string.Empty;
|
||||
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||
public IReadOnlyList<RuntimeDiagnostic> Diagnostics { get; init; } = Array.Empty<RuntimeDiagnostic>();
|
||||
public AnnotationPlanSummary Summary { get; init; } = new();
|
||||
public required IReadOnlyList<AnnotationIntent> Create { get; init; }
|
||||
public required IReadOnlyList<AnnotationIntent> Update { get; init; }
|
||||
public required IReadOnlyList<string> Delete { get; init; }
|
||||
@@ -58,7 +233,7 @@ public interface IOrthoDrawingContext
|
||||
|
||||
public interface IPlantObjectReader
|
||||
{
|
||||
IReadOnlyList<PlantObjectData> ReadObjects(OrthoViewportRef viewport, CancellationToken cancellationToken);
|
||||
IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface IAnnotationRepository
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||
{
|
||||
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||
{
|
||||
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||
var existing = request.Existing.ToDictionary(x => x.StableKey, StringComparer.OrdinalIgnoreCase);
|
||||
var operations = new List<AnnotationOperationPlan>(objects.Length);
|
||||
var creates = new List<AnnotationIntent>();
|
||||
var updates = new List<AnnotationIntent>();
|
||||
var diagnostics = new List<RuntimeDiagnostic>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in objects)
|
||||
{
|
||||
if (!seen.Add(item.SnapshotId))
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Conflict, Reasons = new[] { "duplicate-snapshot-id" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.ObjectClass))
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "missing-object-class" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "missing-line-number-or-tag" } });
|
||||
continue;
|
||||
}
|
||||
|
||||
var position = item.Geometry.PaperPosition;
|
||||
if (position is null)
|
||||
{
|
||||
diagnostics.Add(new RuntimeDiagnostic { Code = "MISSING_GEOMETRY", Message = $"No paper position for {item.SnapshotId}." });
|
||||
}
|
||||
|
||||
var stableKey = $"{request.Viewport.SourceDrawingName ?? string.Empty}:{request.Viewport.Id}:{item.SnapshotId}";
|
||||
var existingAnnotation = existing.TryGetValue(stableKey, out var match) ? match : existing.TryGetValue(item.SnapshotId, out var directMatch) ? directMatch : null;
|
||||
var kind = existingAnnotation is null ? AnnotationOperationKind.Create : AnnotationOperationKind.Update;
|
||||
operations.Add(new AnnotationOperationPlan
|
||||
{
|
||||
SourceSnapshotId = item.SnapshotId,
|
||||
Kind = kind,
|
||||
AnnotationType = "PlantObject",
|
||||
Text = text,
|
||||
Position = position,
|
||||
Rotation = item.Geometry.Rotation,
|
||||
ExistingAnnotationId = existingAnnotation?.StableKey,
|
||||
Reasons = Array.Empty<string>()
|
||||
});
|
||||
|
||||
var intent = new AnnotationIntent
|
||||
{
|
||||
StableKey = stableKey,
|
||||
Text = text,
|
||||
Position = position is { } point ? new AnnotationPoint(point.X, point.Y) : new AnnotationPoint(0, 0),
|
||||
Rotation = item.Geometry.Rotation ?? 0,
|
||||
ClassName = item.ObjectClass,
|
||||
Layer = "PXP_ANNOTATION"
|
||||
};
|
||||
if (kind == AnnotationOperationKind.Create) creates.Add(intent); else updates.Add(intent);
|
||||
}
|
||||
|
||||
var summary = new AnnotationPlanSummary
|
||||
{
|
||||
ObjectsExamined = objects.Length,
|
||||
CreateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Create),
|
||||
UpdateCount = operations.Count(x => x.Kind == AnnotationOperationKind.Update),
|
||||
SkipCount = operations.Count(x => x.Kind == AnnotationOperationKind.Skip),
|
||||
ConflictCount = operations.Count(x => x.Kind == AnnotationOperationKind.Conflict),
|
||||
UnsupportedCount = operations.Count(x => x.Kind == AnnotationOperationKind.Unsupported),
|
||||
WarningCount = diagnostics.Count(x => !string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase)),
|
||||
ErrorCount = diagnostics.Count(x => string.Equals(x.Severity, "Error", StringComparison.OrdinalIgnoreCase))
|
||||
};
|
||||
|
||||
return new AnnotationPlan
|
||||
{
|
||||
PlanId = BuildPlanId(request, objects),
|
||||
CommandName = request.CommandName,
|
||||
Operations = operations,
|
||||
Diagnostics = diagnostics,
|
||||
Summary = summary,
|
||||
Create = creates,
|
||||
Update = updates,
|
||||
Delete = Array.Empty<string>(),
|
||||
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
|
||||
ExaminedCount = objects.Length,
|
||||
IsDeterministic = true
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
|
||||
{
|
||||
var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id);
|
||||
foreach (var item in objects)
|
||||
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||
}
|
||||
}
|
||||
@@ -6,43 +6,21 @@ 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))
|
||||
var objects = request.Objects.Select(item => new PlantObjectSnapshot
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.LineNumber) && string.IsNullOrWhiteSpace(item.Tag))
|
||||
{
|
||||
skipped.Add($"{item.ObjectId}:missing-line-number-or-tag");
|
||||
continue;
|
||||
}
|
||||
SnapshotId = item.ObjectId,
|
||||
ObjectClass = item.ClassName,
|
||||
Tag = item.Tag,
|
||||
LineNumber = item.LineNumber,
|
||||
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
|
||||
}).ToArray();
|
||||
|
||||
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
|
||||
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||
{
|
||||
Create = create,
|
||||
Update = update,
|
||||
Delete = Array.Empty<string>(),
|
||||
SkipReasons = skipped,
|
||||
ExaminedCount = request.Objects.Count
|
||||
};
|
||||
CommandName = "PHP_REC_ANNOT",
|
||||
Viewport = new OrthoViewportInfo { Id = request.Viewport.ViewportId, DisplayName = request.Viewport.ViewportId, SourceDrawingName = request.Viewport.DrawingId },
|
||||
Objects = objects,
|
||||
Existing = request.Existing
|
||||
});
|
||||
}
|
||||
|
||||
private static string BuildText(PlantObjectData item) =>
|
||||
string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag!.Trim() : item.LineNumber.Trim();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public sealed class ExecutionModeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||
{
|
||||
public void EnsureWriteAllowed(string operation) => throw new InvalidOperationException(mode == PRecAnnotExecutionMode.Apply ? "APPLY_MODE_NOT_IMPLEMENTED" : "SMOKE_WRITE_BLOCKED");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using PRecAnnot.Abstractions;
|
||||
|
||||
namespace PRecAnnot.Core;
|
||||
|
||||
public static class OrthoDetectionClassifier
|
||||
{
|
||||
public static OrthoDetectionResult Classify(bool hasPlantProject, bool hasOrthoPart, bool apiConfirmed, bool metadataAmbiguous = false)
|
||||
{
|
||||
if (!hasPlantProject) return Result(OrthoDetectionStatus.NotOrtho, "NO_PLANT_PROJECT", "Active Plant project is absent.");
|
||||
if (metadataAmbiguous) return Result(OrthoDetectionStatus.Ambiguous, "AMBIGUOUS_ORTHO_METADATA", "Plant metadata is insufficient to confirm Ortho.");
|
||||
if (!hasOrthoPart) return Result(OrthoDetectionStatus.Unsupported, "ORTHO_PART_UNAVAILABLE", "Ortho project part is unavailable.");
|
||||
return apiConfirmed
|
||||
? Result(OrthoDetectionStatus.Confirmed, "ORTHO_API_CONFIRMED", "OrthoDocUtils confirmed the active drawing.")
|
||||
: Result(OrthoDetectionStatus.NotOrtho, "ORTHO_API_NEGATIVE", "OrthoDocUtils did not confirm the active drawing.");
|
||||
}
|
||||
|
||||
private static OrthoDetectionResult Result(OrthoDetectionStatus status, string code, string message) => new()
|
||||
{
|
||||
Status = status,
|
||||
Evidence = new[] { code },
|
||||
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } }
|
||||
};
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Diagnostics;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.Runtime;
|
||||
using Autodesk.Windows;
|
||||
using PRecAnnot.Abstractions;
|
||||
using PRecAnnot.Core;
|
||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
public sealed class EntryPoint : IExtensionApplication
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
||||
RibbonIntegration.Register();
|
||||
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
RuntimeDiagnostics.Write("AssemblyTerminateStarted");
|
||||
RibbonIntegration.Terminate();
|
||||
RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
var operationId = Guid.NewGuid().ToString("N");
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
Document? document = null;
|
||||
RuntimeDiagnostics.Write("CommandStarted", operationId: operationId, commandName: commandName);
|
||||
try
|
||||
{
|
||||
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
||||
|
||||
var context = new PlantRuntimeContextReader().ReadCurrentContext();
|
||||
var details = new Dictionary<string, object?>
|
||||
{
|
||||
["ExecutionMode"] = mode.ToString(),
|
||||
["IsPlantProjectDetected"] = context.HasPlantProject,
|
||||
["IsPlantProjectOpen"] = context.HasPlantProject,
|
||||
["IsOrthoDrawing"] = context.IsOrthoDrawing,
|
||||
["ViewportCount"] = context.Viewports.Count,
|
||||
["DataLinksAvailable"] = DataLinksAvailable(context.HasPlantProject)
|
||||
};
|
||||
|
||||
if (document is null || !context.HasPlantProject)
|
||||
{
|
||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
|
||||
WriteEditor(document, "\nP_Rec_Annot: Активный Plant 3D-проект не найден; DWG не изменён.");
|
||||
WriteUsage(operationId, commandName, "PreflightRejected", document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "PreflightRejected", document: document, details: details);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == PRecAnnotExecutionMode.Smoke || !context.IsOrthoDrawing)
|
||||
{
|
||||
var status = context.IsOrthoDrawing ? "PreflightAccepted" : "NotOrtho";
|
||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
|
||||
WriteEditor(document, $"\nP_Rec_Annot {commandName}: проект '{context.ProjectDisplayName}' найден; тип чертежа: {(context.IsOrthoDrawing ? "Ortho" : "NotOrtho")}; DWG не изменён.");
|
||||
WriteUsage(operationId, commandName, status, document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: status, document: document, details: details);
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var plans = new List<AnnotationPlan>();
|
||||
foreach (var viewport in context.Viewports)
|
||||
{
|
||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
|
||||
}
|
||||
|
||||
var summary = new
|
||||
{
|
||||
ObjectsExamined = plans.Sum(x => x.Summary.ObjectsExamined),
|
||||
CreateCount = plans.Sum(x => x.Summary.CreateCount),
|
||||
UpdateCount = plans.Sum(x => x.Summary.UpdateCount),
|
||||
SkipCount = plans.Sum(x => x.Summary.SkipCount),
|
||||
ConflictCount = plans.Sum(x => x.Summary.ConflictCount),
|
||||
UnsupportedCount = plans.Sum(x => x.Summary.UnsupportedCount),
|
||||
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
||||
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
||||
};
|
||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||
details["PlanCreateCount"] = summary.CreateCount;
|
||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||
details["PlanSkipCount"] = summary.SkipCount;
|
||||
details["PlanConflictCount"] = summary.ConflictCount;
|
||||
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
||||
details["ErrorCount"] = summary.ErrorCount;
|
||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
WriteEditor(document, $"\nP_Rec_Annot — диагностический план\nПроект: {context.ProjectDisplayName}\nТип чертежа: Ortho\nViewport: {context.Viewports.Count}\nОбъектов найдено: {summary.ObjectsExamined}\nБудет создано: {summary.CreateCount}\nБудет обновлено: {summary.UpdateCount}\nБудет пропущено: {summary.SkipCount}\nКонфликтов: {summary.ConflictCount}\nОшибок: {summary.ErrorCount}\nDWG не изменён.");
|
||||
WriteUsage(operationId, commandName, "DryRunCompleted", document, details, stopwatch.ElapsedMilliseconds);
|
||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
||||
}
|
||||
catch (System.Exception exception)
|
||||
{
|
||||
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
|
||||
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
|
||||
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static PRecAnnotExecutionMode ParseMode(string value) =>
|
||||
value.Equals("Smoke", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Smoke :
|
||||
value.Equals("Apply", StringComparison.OrdinalIgnoreCase) ? PRecAnnotExecutionMode.Apply : PRecAnnotExecutionMode.DryRun;
|
||||
|
||||
private static bool DataLinksAvailable(bool hasPlantProject)
|
||||
{
|
||||
try { return hasPlantProject && Autodesk.ProcessPower.PlantInstance.PlantApplication.ProjectPart("Piping")?.DataLinksManager is not null; }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static void WriteUsage(string operationId, string commandName, string status, Document? document, IReadOnlyDictionary<string, object?>? details, long duration, System.Exception? exception = null)
|
||||
{
|
||||
var eventDetails = new Dictionary<string, object?>
|
||||
{
|
||||
["ObjectsExamined"] = 0, ["AnnotationsCreated"] = 0, ["AnnotationsUpdated"] = 0,
|
||||
["AnnotationsDeleted"] = 0, ["AnnotationsSkipped"] = 0, ["WarningCount"] = 0,
|
||||
["ErrorCount"] = exception is null ? 0 : 1, ["DurationMilliseconds"] = duration
|
||||
};
|
||||
if (details is not null) foreach (var item in details) eventDetails[item.Key] = item.Value;
|
||||
RuntimeDiagnostics.Write("CommandUsageEvent", exception is null ? "INFO" : "ERROR", operationId, commandName, status, exception, document, eventDetails);
|
||||
}
|
||||
|
||||
private static void WriteEditor(Document? document, string message)
|
||||
{
|
||||
try { document?.Editor.WriteMessage(message); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RibbonIntegration
|
||||
{
|
||||
private const string TabId = "PRecAnnot.Plant3D2026";
|
||||
private const string PanelId = "PRecAnnot.Plant3D2026.Annotations";
|
||||
private const string ButtonId = "PRecAnnot.Plant3D2026.PHP_REC_ANNOT";
|
||||
private static bool deferred;
|
||||
|
||||
public static void Register()
|
||||
{
|
||||
RuntimeDiagnostics.Write("RibbonInitializationStarted");
|
||||
try
|
||||
{
|
||||
if (ComponentManager.Ribbon is null)
|
||||
{
|
||||
if (!deferred) { ComponentManager.ItemInitialized += OnItemInitialized; deferred = true; }
|
||||
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Deferred");
|
||||
return;
|
||||
}
|
||||
RegisterNow();
|
||||
}
|
||||
catch (System.Exception exception) { RuntimeDiagnostics.Write("RibbonInitializationCompleted", "ERROR", exception: exception, resultStatus: "Failed"); }
|
||||
}
|
||||
|
||||
private static void OnItemInitialized(object? sender, EventArgs args)
|
||||
{
|
||||
if (ComponentManager.Ribbon is null) return;
|
||||
ComponentManager.ItemInitialized -= OnItemInitialized;
|
||||
deferred = false;
|
||||
Register();
|
||||
}
|
||||
|
||||
private static void RegisterNow()
|
||||
{
|
||||
var ribbon = ComponentManager.Ribbon!;
|
||||
var tab = ribbon.Tabs.FirstOrDefault(x => string.Equals(x.Id, TabId, StringComparison.OrdinalIgnoreCase));
|
||||
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
|
||||
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
|
||||
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
|
||||
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
|
||||
tab.Panels.Add(new RibbonPanel { Source = source });
|
||||
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
|
||||
}
|
||||
|
||||
public static void Terminate()
|
||||
{
|
||||
if (!deferred) return;
|
||||
ComponentManager.ItemInitialized -= OnItemInitialized;
|
||||
deferred = false;
|
||||
}
|
||||
}
|
||||
@@ -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,20 @@
|
||||
<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="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.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>
|
||||
<Reference Include="PnP3dOrthoProjectPart"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoProjectPart.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.AutoCAD.DatabaseServices;
|
||||
using Autodesk.ProcessPower.DataLinks;
|
||||
using Autodesk.ProcessPower.P3dProjectParts;
|
||||
using Autodesk.ProcessPower.PlantInstance;
|
||||
using Autodesk.ProcessPower.ProjectManager;
|
||||
using PRecAnnot.Abstractions;
|
||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
internal sealed class PlantRuntimeContextReader : IPlantRuntimeContextReader
|
||||
{
|
||||
public PlantRuntimeContext ReadCurrentContext()
|
||||
{
|
||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
if (document is null)
|
||||
{
|
||||
return Empty(false, false, "NO_ACTIVE_DOCUMENT", "Активный документ отсутствует.");
|
||||
}
|
||||
|
||||
PlantProject? project;
|
||||
try { project = PlantApplication.CurrentProject; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Empty(true, false, "PLANT_PROJECT_READ_FAILED", exception.GetType().Name);
|
||||
}
|
||||
|
||||
if (project is null)
|
||||
{
|
||||
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;
|
||||
try { orthoProject = PlantApplication.ProjectPart("Ortho") as OrthoProject; }
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Empty(true, true, "ORTHO_PROJECT_PART_READ_FAILED", exception.GetType().Name);
|
||||
}
|
||||
|
||||
if (orthoProject is null)
|
||||
{
|
||||
return Empty(true, true, "ORTHO_PROJECT_PART_UNAVAILABLE", "Ortho project part недоступен.", OrthoDetectionStatus.Unsupported, "ProjectPart(Ortho)");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string resolvedPath;
|
||||
var isOrtho = OrthoDocUtils.IsValidOrthoDrawing(document.Name, orthoProject, out resolvedPath);
|
||||
if (!isOrtho)
|
||||
{
|
||||
return Empty(true, true, "NOT_ORTHO_DRAWING", "OrthoDocUtils.IsValidOrthoDrawing вернул false.", OrthoDetectionStatus.NotOrtho, "Ortho project part", "OrthoDocUtils.IsValidOrthoDrawing=false");
|
||||
}
|
||||
|
||||
var viewports = new PlantOrthoViewportReader(orthoProject, document.Name).ReadViewports(CancellationToken.None);
|
||||
return new PlantRuntimeContext
|
||||
{
|
||||
HasActiveDocument = true,
|
||||
HasPlantProject = true,
|
||||
IsOrthoDrawing = true,
|
||||
DrawingName = Path.GetFileName(document.Name),
|
||||
ProjectDisplayName = displayName ?? "display name unavailable",
|
||||
ProjectDetectionStatus = projectStatus,
|
||||
Viewports = viewports,
|
||||
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
|
||||
{
|
||||
Status = OrthoDetectionStatus.Confirmed,
|
||||
Evidence = new[] { "ProjectPart(Ortho)", "OrthoDocUtils.IsValidOrthoDrawing=true", "DwgSheetDefinitions" },
|
||||
Diagnostics = Array.Empty<RuntimeDiagnostic>()
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
return Empty(true, true, "ORTHO_DETECTION_FAILED", exception.GetType().Name, OrthoDetectionStatus.Failed, "ProjectPart(Ortho)");
|
||||
}
|
||||
}
|
||||
|
||||
private static PlantRuntimeContext Empty(bool hasDocument, bool hasProject, string code, string message, OrthoDetectionStatus status = OrthoDetectionStatus.NotOrtho, params string[] evidence) => new()
|
||||
{
|
||||
HasActiveDocument = hasDocument,
|
||||
HasPlantProject = hasProject,
|
||||
IsOrthoDrawing = false,
|
||||
ProjectDisplayName = hasProject ? "display name unavailable" : null,
|
||||
ProjectDetectionStatus = hasProject ? PlantProjectDetectionStatus.Ambiguous : PlantProjectDetectionStatus.NotFound,
|
||||
Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } },
|
||||
Detection = new OrthoDetectionResult { Status = status, Evidence = evidence, Diagnostics = new[] { new RuntimeDiagnostic { Code = code, Message = message } } }
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed class PlantOrthoViewportReader : IOrthoViewportReader
|
||||
{
|
||||
private readonly OrthoProject project;
|
||||
private readonly string drawingName;
|
||||
|
||||
public PlantOrthoViewportReader(OrthoProject project, string drawingName)
|
||||
{
|
||||
this.project = project;
|
||||
this.drawingName = Path.GetFileName(drawingName);
|
||||
}
|
||||
|
||||
public IReadOnlyList<OrthoViewportInfo> ReadViewports(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<OrthoViewportInfo>();
|
||||
foreach (var sheet in project.DwgSheetDefinitions.AsArray())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (!string.Equals(Path.GetFileName(sheet.DwgName), drawingName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
foreach (var view in sheet.DwgViewDefs.AsArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var id = StableId(sheet.DwgGuid.ToString(), view.ViewName);
|
||||
result.Add(new OrthoViewportInfo
|
||||
{
|
||||
Id = id,
|
||||
DisplayName = view.ViewName,
|
||||
CenterX = view.CenterPoint.X,
|
||||
CenterY = view.CenterPoint.Y,
|
||||
Width = view.ViewWidth,
|
||||
Height = view.ViewHeight,
|
||||
Scale = view.ViewScale,
|
||||
TwistAngle = 0,
|
||||
IsOn = true,
|
||||
IsLocked = false,
|
||||
SourceDrawingName = ReadSourceDrawingPath(view),
|
||||
ViewName = view.ViewName
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// One malformed view must not abort the remaining read-only analysis.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.OrderByDescending(x => x.CenterY).ThenBy(x => x.CenterX).ThenBy(x => x.Id, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
private static string StableId(string sheetId, string viewName)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + viewName));
|
||||
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
|
||||
{
|
||||
public IReadOnlyList<PlantObjectSnapshot> ReadObjects(OrthoViewportInfo viewport, CancellationToken cancellationToken)
|
||||
{
|
||||
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||
if (document is null || string.IsNullOrWhiteSpace(viewport.SourceDrawingName)) return Array.Empty<PlantObjectSnapshot>();
|
||||
|
||||
DataLinksManager? dataLinks;
|
||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
||||
catch { 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>();
|
||||
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))
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
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(viewport.SourceDrawingName, rowId, className),
|
||||
ObjectClass = className ?? string.Empty,
|
||||
Tag = Value("Tag", "TAG"),
|
||||
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||
SupportNumber = Value("Support Number", "SupportNumber"),
|
||||
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();
|
||||
}
|
||||
|
||||
private static string StableId(string drawingName, int rowId, string? className)
|
||||
{
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
||||
return "obj-" + Convert.ToHexString(hash)[..24];
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||
{
|
||||
public void EnsureWriteAllowed(string operation)
|
||||
{
|
||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
||||
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Autodesk.AutoCAD.ApplicationServices;
|
||||
using Autodesk.ProcessPower.PlantInstance;
|
||||
|
||||
namespace PRecAnnot.Plant3D2026;
|
||||
|
||||
internal static class RuntimeDiagnostics
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static readonly string LogDirectory = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Promhimproekt", "PRecAnnot2026", "Logs");
|
||||
|
||||
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
||||
|
||||
public static void Write(
|
||||
string eventType,
|
||||
string level = "INFO",
|
||||
string? operationId = null,
|
||||
string? commandName = null,
|
||||
string? resultStatus = null,
|
||||
System.Exception? exception = null,
|
||||
Document? document = null,
|
||||
IReadOnlyDictionary<string, object?>? details = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = new Dictionary<string, object?>(StringComparer.Ordinal)
|
||||
{
|
||||
["Timestamp"] = DateTimeOffset.UtcNow,
|
||||
["Level"] = level,
|
||||
["OperationId"] = operationId,
|
||||
["EventType"] = eventType,
|
||||
["AssemblyVersion"] = typeof(EntryPoint).Assembly.GetName().Version?.ToString(),
|
||||
["AutoCADVersion"] = typeof(Document).Assembly.GetName().Version?.ToString(),
|
||||
["PlantVersion"] = typeof(PlantApplication).Assembly.GetName().Version?.ToString(),
|
||||
["ProcessId"] = Environment.ProcessId,
|
||||
["ThreadId"] = Environment.CurrentManagedThreadId,
|
||||
["ApartmentState"] = Thread.CurrentThread.GetApartmentState().ToString(),
|
||||
["ActiveDocumentName"] = SafeDocumentName(document),
|
||||
["CommandName"] = commandName,
|
||||
["ResultStatus"] = resultStatus,
|
||||
["ExceptionType"] = exception?.GetType().FullName,
|
||||
["ExceptionMessage"] = exception?.Message,
|
||||
["StackTrace"] = level.Equals("ERROR", StringComparison.OrdinalIgnoreCase) ? exception?.StackTrace : null,
|
||||
["RuntimeMode"] = RuntimeMode
|
||||
};
|
||||
|
||||
if (details is not null)
|
||||
foreach (var item in details) payload[item.Key] = item.Value;
|
||||
|
||||
var line = JsonSerializer.Serialize(payload) + Environment.NewLine;
|
||||
lock (Gate)
|
||||
{
|
||||
Directory.CreateDirectory(LogDirectory);
|
||||
File.AppendAllText(Path.Combine(LogDirectory, "runtime.jsonl"), line);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Runtime diagnostics must never prevent AutoCAD command execution.
|
||||
}
|
||||
}
|
||||
|
||||
private static string? SafeDocumentName(Document? document)
|
||||
{
|
||||
try { return document is null ? null : Path.GetFileName(document.Name); }
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SmokeWriteBlockedException() : InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||
|
||||
internal static class SmokeGuard
|
||||
{
|
||||
public static bool IsSmoke => string.Equals(RuntimeDiagnostics.RuntimeMode, "Smoke", StringComparison.OrdinalIgnoreCase);
|
||||
public static void EnsureReadOnly() { if (IsSmoke) throw new SmokeWriteBlockedException(); }
|
||||
}
|
||||
@@ -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,137 @@ 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);
|
||||
}
|
||||
|
||||
[Fact] public void OrthoDetection_NoPlantProject_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(false, false, false).Status);
|
||||
[Fact] public void OrthoDetection_NormalDwg_ReturnsNotOrtho() => Assert.Equal(OrthoDetectionStatus.NotOrtho, OrthoDetectionClassifier.Classify(true, true, false).Status);
|
||||
[Fact] public void OrthoDetection_ConfirmedOrtho_ReturnsConfirmed() => Assert.Equal(OrthoDetectionStatus.Confirmed, OrthoDetectionClassifier.Classify(true, true, true).Status);
|
||||
[Fact] public void OrthoDetection_AmbiguousMetadata_ReturnsAmbiguous() => Assert.Equal(OrthoDetectionStatus.Ambiguous, OrthoDetectionClassifier.Classify(true, true, false, true).Status);
|
||||
|
||||
[Fact] public void AnnotationPlan_InputOrderDoesNotChangeResult()
|
||||
{
|
||||
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
|
||||
var objects = new[] { Snapshot("b", "Pipe", "L-2"), Snapshot("a", "Valve", "V-1") };
|
||||
var builder = new AnnotationPlanBuilder();
|
||||
var first = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects });
|
||||
var second = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = objects.Reverse().ToArray() });
|
||||
Assert.Equal(first.PlanId, second.PlanId);
|
||||
Assert.Equal(first.Summary, second.Summary);
|
||||
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||
{
|
||||
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_UnsupportedObject_IsMarkedUnsupported()
|
||||
{
|
||||
var plan = Build(Snapshot("a", "", "L-1"));
|
||||
Assert.Equal(AnnotationOperationKind.Unsupported, Assert.Single(plan.Operations).Kind);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_ExistingAnnotation_BecomesUpdate()
|
||||
{
|
||||
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" };
|
||||
var item = Snapshot("a", "Pipe", "L-1");
|
||||
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = viewport, Objects = new[] { item }, Existing = new[] { new ExistingAnnotation("a", "old", new(0, 0)) } });
|
||||
Assert.Equal(AnnotationOperationKind.Update, Assert.Single(plan.Operations).Kind);
|
||||
}
|
||||
|
||||
[Fact] public void AnnotationPlan_DuplicateCandidate_BecomesConflict()
|
||||
{
|
||||
var plan = Build(Snapshot("a", "Pipe", "L-1"), Snapshot("a", "Pipe", "L-1"));
|
||||
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 DryRunPath_HasNoForWriteOperations()
|
||||
{
|
||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||
Assert.DoesNotContain("ForWrite", source);
|
||||
Assert.DoesNotContain("AppendEntity", source);
|
||||
Assert.DoesNotContain("AcceptChanges", source);
|
||||
Assert.DoesNotContain("SetProperties", source);
|
||||
}
|
||||
|
||||
private static AnnotationPlan Build(params PlantObjectSnapshot[] objects) => new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "A", SourceDrawingName = "test.dwg" }, Objects = objects });
|
||||
private static PlantObjectSnapshot Snapshot(string id, string type, string? line, string? tag = "TAG") => new() { SnapshotId = id, ObjectClass = type, LineNumber = line, Tag = tag, Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(1, 2) } };
|
||||
}
|
||||
|
||||
@@ -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.'
|
||||
|
||||
Reference in New Issue
Block a user