Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1563e2fae0 | ||
|
|
bd5f47d811 | ||
|
|
1bfb91bc4b | ||
|
|
209b8b89b8 | ||
|
|
31787fc05a | ||
|
|
11cf97c476 | ||
|
|
76d362d2c3 | ||
|
|
37d3cacc13 | ||
|
|
713e620485 | ||
|
|
6f32371418 | ||
|
|
243080a1aa | ||
|
|
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,89 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## Viewport membership and source DataLinks
|
||||||
|
|
||||||
|
Legacy P_Rec_Annot source for read-only analysis is
|
||||||
|
`\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo`.
|
||||||
|
|
||||||
|
A source object is not a member of an Ortho viewport without confirmed
|
||||||
|
evidence. A geometry-only filter is at most `PartiallyResolved`. Apply is
|
||||||
|
blocked by any `PLUGIN_ONLY_CHANGE` in a DWG, DCF, or project metadata found by
|
||||||
|
a paired CONTROL/DryRun comparison.
|
||||||
|
|
||||||
|
## Ortho source DataLinks and DCF control
|
||||||
|
|
||||||
|
DataLinks активного Ortho drawing не являются источником Piping rows. Piping
|
||||||
|
rows читаются из source model drawing и Piping project part, разрешённых через
|
||||||
|
Ortho sheet/view definitions.
|
||||||
|
|
||||||
|
Перед предположениями об Autodesk runtime API необходимо исследовать текущий
|
||||||
|
код, доступный legacy usage, assembly metadata и реальные runtime types.
|
||||||
|
Reflection/runtime inspection применяется только для подтверждения данных,
|
||||||
|
которых нельзя установить статически. Production resolver не угадывает
|
||||||
|
Autodesk properties по строковым именам.
|
||||||
|
|
||||||
|
Подтверждённый контракт Plant 3D 2026: `IDwgViewDef.DwgQuery` →
|
||||||
|
`IDwgQuery.ModelFiles` → `ModelFile[].ModelFileName`. При несовпадении типа
|
||||||
|
возвращать `ORTHO_MODEL_FILES_API_UNSUPPORTED`, без fallback-эвристики.
|
||||||
|
|
||||||
|
Проверка DCF выполняется на парных `control` и `DryRun` fixtures, созданных из
|
||||||
|
одного закрытого состояния проекта. Сравнивать нужно `control delta` с
|
||||||
|
`DryRun delta`: одинаковые изменения классифицируются как baseline behavior
|
||||||
|
Plant 3D, а plugin-only изменения блокируют дальнейшую работу.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Annotation-plan determinism
|
||||||
|
|
||||||
|
`AnnotationPlanBuilder` uses schema version `1`. Its canonical input is the normalized command, source drawing basename, view ID, source snapshots sorted by stable ID, properties sorted by key, membership state, and operations sorted by stable source key and type. Absolute paths, process data, timestamps and runtime ObjectIds are excluded.
|
||||||
|
|
||||||
|
Runtime DryRun confirmed equal PlanIds and summaries for two calls of each supported command. Changing a relevant normalized input changes the PlanId.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# CONTROL versus DryRun DCF comparison
|
||||||
|
|
||||||
|
DBMOD covers only the active drawing. This procedure separately compares
|
||||||
|
content-bearing project files after identical CONTROL and DryRun sessions.
|
||||||
|
|
||||||
|
1. With Plant 3D closed and the Launcher session `RolledBack`, copy the local
|
||||||
|
fixture to `<FROZEN_FIXTURE>`.
|
||||||
|
2. Create `<CONTROL_FIXTURE>` and `<DRYRUN_FIXTURE>` from that same frozen
|
||||||
|
state; calculate complete local SHA-256 inventories before each run.
|
||||||
|
3. Open model and Ortho drawings in CONTROL through the same Launcher DryRun
|
||||||
|
launch contract, but invoke no P_Rec_Annot command. Close without saving,
|
||||||
|
wait for Launcher rollback, then calculate the after inventory.
|
||||||
|
4. Repeat with DRYRUN and invoke each supported command twice. Close without
|
||||||
|
saving, wait for rollback, then calculate the after inventory.
|
||||||
|
5. Compare deltas by logical relative path. Classify each content-bearing file
|
||||||
|
as `UNCHANGED`, `AUTODESK_BASELINE_CHANGE`, `PLUGIN_ONLY_CHANGE`, or
|
||||||
|
`INCONCLUSIVE`.
|
||||||
|
|
||||||
|
Content-bearing groups include model DWG, Ortho DWG, `Piping.dcf`, remaining
|
||||||
|
DCF/database files, `Project.xml` or equivalent project metadata, and Ortho
|
||||||
|
metadata. Lock, cache, session, log, preview, and temporary files are reported
|
||||||
|
separately and cannot establish project invariance.
|
||||||
|
|
||||||
|
Any `PLUGIN_ONLY_CHANGE` blocks Apply. Full paths and hashes remain local;
|
||||||
|
repository evidence contains only logical fixture markers, groups, counts, and
|
||||||
|
classifications.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"sourceObjectsRead": 7,
|
||||||
|
"membershipResolved": 0,
|
||||||
|
"membershipPartial": 0,
|
||||||
|
"membershipUnresolved": 7,
|
||||||
|
"phpRecAnnotPlanIds": ["plan-C5D40DC32505D5D9CFE6D38B", "plan-C5D40DC32505D5D9CFE6D38B"],
|
||||||
|
"pxpAavPlanIds": ["plan-E4877CA13418D43A17F47AAA", "plan-E4877CA13418D43A17F47AAA"],
|
||||||
|
"pxpOaPlanIds": ["plan-6C794D25C292E73AA3BDDC31", "plan-6C794D25C292E73AA3BDDC31"],
|
||||||
|
"summariesDeterministic": true,
|
||||||
|
"dbmodBefore": 0,
|
||||||
|
"dbmodAfter": 0,
|
||||||
|
"launcherSession": "RolledBack",
|
||||||
|
"dcfControlComparison": null,
|
||||||
|
"applyModeUsed": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"fixture": {
|
||||||
|
"frozen": "<FROZEN_FIXTURE>",
|
||||||
|
"control": "<CONTROL_FIXTURE>",
|
||||||
|
"dryRun": "<DRYRUN_FIXTURE>",
|
||||||
|
"trackedFileCount": 210
|
||||||
|
},
|
||||||
|
"membership": {
|
||||||
|
"mechanism": "IDwgViewDef.DwgQuery.Execute -> MdObjectId.PrjObjectId -> DataLinksManager.FindAcPpRowId",
|
||||||
|
"orthoViews": 1,
|
||||||
|
"sourceObjects": 7,
|
||||||
|
"resolved": 5,
|
||||||
|
"partiallyResolved": 0,
|
||||||
|
"outsideView": 2,
|
||||||
|
"notResolved": 0,
|
||||||
|
"evidenceCode": "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"PHP_REC_ANNOT": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-AD701C7AA38D99E4C28D7D6D",
|
||||||
|
"run2PlanId": "plan-AD701C7AA38D99E4C28D7D6D",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
},
|
||||||
|
"PXP-AAV": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-C4FCD62F4EEBA1D1948FA22B",
|
||||||
|
"run2PlanId": "plan-C4FCD62F4EEBA1D1948FA22B",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
},
|
||||||
|
"PXP-OA": {
|
||||||
|
"planSchemaVersion": 1,
|
||||||
|
"run1PlanId": "plan-C30D133E2D5B4BF4C0AF2ED1",
|
||||||
|
"run2PlanId": "plan-C30D133E2D5B4BF4C0AF2ED1",
|
||||||
|
"planDeterministic": true,
|
||||||
|
"summaryDeterministic": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"projectFileComparison": {
|
||||||
|
"controlDeltaCount": 0,
|
||||||
|
"dryRunDeltaCount": 0,
|
||||||
|
"modelDwgPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"orthoDwgPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"pipingDcfPluginOnlyDelta": "UNCHANGED",
|
||||||
|
"projectMetadataPluginOnlyDelta": "UNCHANGED"
|
||||||
|
},
|
||||||
|
"dbmod": {
|
||||||
|
"pairedRun": null,
|
||||||
|
"reason": "Command-window output was not retained."
|
||||||
|
},
|
||||||
|
"launcher": {
|
||||||
|
"controlSession": "RolledBack",
|
||||||
|
"dryRunSession": "RolledBack",
|
||||||
|
"netloadCount": 1,
|
||||||
|
"profileArgumentPresent": false
|
||||||
|
},
|
||||||
|
"apply": {
|
||||||
|
"performed": false,
|
||||||
|
"authorised": false,
|
||||||
|
"reason": "All operations remain Skip; annotation rules are not yet implemented."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-08-03",
|
||||||
|
"executionMode": "DryRun",
|
||||||
|
"plantProjectDetected": true,
|
||||||
|
"projectDisplayNameStatus": "FoundWithName",
|
||||||
|
"orthoDetection": "Confirmed",
|
||||||
|
"viewportCount": 1,
|
||||||
|
"dataLinksAvailable": true,
|
||||||
|
"dataLinksSheetToSourceMapping": "resolved-through-piping-project-drawing-id",
|
||||||
|
"sourceDrawingsResolved": 1,
|
||||||
|
"sourceDataLinksAvailable": true,
|
||||||
|
"objectsRead": 7,
|
||||||
|
"pipeObjectsRead": 7,
|
||||||
|
"phpRecAnnotRuns": 1,
|
||||||
|
"pxpAavRuns": null,
|
||||||
|
"pxpOaRuns": null,
|
||||||
|
"planId": null,
|
||||||
|
"repeatedPlanIdEqual": null,
|
||||||
|
"dbmodBefore": null,
|
||||||
|
"dbmodAfter": null,
|
||||||
|
"dwgModified": null,
|
||||||
|
"dcfModified": null,
|
||||||
|
"changedProjectFiles": null,
|
||||||
|
"unhandledExceptions": 0,
|
||||||
|
"applyModeUsed": false,
|
||||||
|
"launcherRollback": "RolledBack",
|
||||||
|
"exactViewportMembership": "not-verified"
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Ortho DryRun runtime result
|
||||||
|
|
||||||
|
Date: 2026-08-03
|
||||||
|
|
||||||
|
The local test fixture's `TestPipeOrtho.dwg` was detected as Ortho with one
|
||||||
|
viewport. In the paired `DryRun`, `PHP_REC_ANNOT`, `PXP-AAV`, and `PXP-OA`
|
||||||
|
each ran twice. Plugin telemetry recorded all six preflight completions without
|
||||||
|
an unhandled exception. The command-window DBMOD output was not retained for
|
||||||
|
this paired run, so its DBMOD value is recorded as `NOT RECORDED` rather than
|
||||||
|
inferred from telemetry.
|
||||||
|
|
||||||
|
`PlantProject.Name` was available for this fixture and rendered as
|
||||||
|
`PRecAnnotTestProject`.
|
||||||
|
|
||||||
|
The active Ortho sheet does not expose direct Piping rows and must not be used
|
||||||
|
as a DataLinks source. Metadata inspection and a sanitized runtime probe
|
||||||
|
confirmed `IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName` as the source-model
|
||||||
|
reference. The adapter resolves that reference through
|
||||||
|
`PipingProject.GetPnPDrawingFiles()` and reads
|
||||||
|
`DataLinksManager.SelectAcPpRowIds(PnPProjectDrawing.Id)`.
|
||||||
|
|
||||||
|
On the paired real DryRun, one source drawing was resolved and seven source
|
||||||
|
Plant objects were read. The confirmed view query resolved five source rows to
|
||||||
|
the active viewport and classified two as outside the view. Each command
|
||||||
|
reported seven skipped operations, zero writes, zero conflicts, and zero
|
||||||
|
errors. The repeated plans were deterministic:
|
||||||
|
|
||||||
|
| Command | Run 1 / run 2 PlanId |
|
||||||
|
| --- | --- |
|
||||||
|
| `PHP_REC_ANNOT` | `plan-AD701C7AA38D99E4C28D7D6D` |
|
||||||
|
| `PXP-AAV` | `plan-C4FCD62F4EEBA1D1948FA22B` |
|
||||||
|
| `PXP-OA` | `plan-C30D133E2D5B4BF4C0AF2ED1` |
|
||||||
|
|
||||||
|
This verifies source-model DataLinks access, exact view membership through the
|
||||||
|
stored view query, and repeated-plan determinism. The remaining functional
|
||||||
|
limitation is annotation-rule coverage: all seven operations remain safe
|
||||||
|
`Skip` operations, so no first Apply is authorised.
|
||||||
|
|
||||||
|
The earlier single-fixture SHA baseline changed before the first command and
|
||||||
|
was therefore inconclusive. A fresh paired comparison used 210-file CONTROL
|
||||||
|
and DRYRUN copies created from the same closed frozen fixture. Both inventories
|
||||||
|
had zero SHA-256 delta after their respective sessions. The CONTROL session
|
||||||
|
invoked no plugin command; the DRYRUN session invoked all six commands. Thus
|
||||||
|
the model DWG, Ortho DWG, `Piping.dcf`, project metadata, and other tracked
|
||||||
|
content-bearing files have no plugin-only delta in this comparison. Both
|
||||||
|
Launcher sessions completed automatic rollback to `RolledBack`.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Ortho source-model mapping
|
||||||
|
|
||||||
|
## Confirmed Plant 3D 2026 read path
|
||||||
|
|
||||||
|
```text
|
||||||
|
Active Ortho document
|
||||||
|
→ OrthoProject.DwgSheetDefinitions
|
||||||
|
→ IDwgSheetDef.DwgViewDefs
|
||||||
|
→ IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName
|
||||||
|
→ PipingProject.GetPnPDrawingFiles()
|
||||||
|
→ exact normalized-path match to PnPProjectDrawing
|
||||||
|
→ PnPProjectDrawing.Id
|
||||||
|
→ DataLinksManager.SelectAcPpRowIds(int dwgid)
|
||||||
|
→ GetObjectClassname / GetAllProperties
|
||||||
|
→ independent PlantObjectSnapshot
|
||||||
|
→ AnnotationPlanBuilder
|
||||||
|
```
|
||||||
|
|
||||||
|
`PlantOrthoViewportReader.ReadViewports` reads the typed DwgQuery contract and
|
||||||
|
creates an Ortho viewport snapshot. `PlantObjectReader.ReadObjects` resolves
|
||||||
|
the resulting model-file reference through the Piping project drawing list and
|
||||||
|
reads DataLinks by project drawing ID. The active Ortho database is not a
|
||||||
|
Piping-row source.
|
||||||
|
|
||||||
|
All calls are read-only. The adapter opens no write transaction and does not
|
||||||
|
save a drawing, DCF, or project metadata.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
The source reader returns stable telemetry diagnostics rather than suppressing
|
||||||
|
the result of a failed query: `ORTHO_SOURCE_DRAWING_NOT_RESOLVED`,
|
||||||
|
`ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT`, `ORTHO_SOURCE_DRAWING_AMBIGUOUS`,
|
||||||
|
`PIPING_PROJECT_PART_NOT_FOUND`, `SOURCE_DATALINKS_UNAVAILABLE`,
|
||||||
|
`SOURCE_DATALINKS_NO_ROWS`, `SOURCE_DATABASE_READ_FAILED`,
|
||||||
|
`SOURCE_PIPE_ROWS_NOT_FOUND`, or `SOURCE_DATALINKS_READ`.
|
||||||
|
|
||||||
|
Exact membership of a source object in a particular Ortho viewport is not yet
|
||||||
|
implemented. A source-level DryRun plan must not be presented as proof of
|
||||||
|
viewport membership.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Ortho viewport membership
|
||||||
|
|
||||||
|
## Runtime path
|
||||||
|
|
||||||
|
`EntryPoint.RunDiagnostic` reads each `OrthoViewportInfo` and calls
|
||||||
|
`PlantObjectReader.ReadObjects`. The reader resolves source Piping rows, then
|
||||||
|
`PlantViewportMembershipReader.Read` evaluates the stored query for that exact
|
||||||
|
view. It maps `MdObjectId.PrjObjectId` to a source DataLinks row and assigns a
|
||||||
|
membership status to every independent `PlantObjectSnapshot`.
|
||||||
|
|
||||||
|
| Status | Meaning | Diagnostic evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Resolved` | The source row was returned by the exact view query. | `VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID` |
|
||||||
|
| `ObjectOutsideView` | The source row was not returned by that confirmed query. | `VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP` |
|
||||||
|
| `NotResolved` | The view contract is unavailable, ambiguous, or failed. | `VIEW_MEMBERSHIP_API_UNSUPPORTED` |
|
||||||
|
|
||||||
|
No source object is treated as visible solely because it belongs to the same
|
||||||
|
model drawing. No geometry-only result is upgraded to `Resolved`.
|
||||||
|
|
||||||
|
## Plan effect
|
||||||
|
|
||||||
|
`AnnotationPlanBuilder` includes membership status in its canonical plan input.
|
||||||
|
`ObjectOutsideView`, unsupported, and unresolved objects are `Skip` operations
|
||||||
|
with stable reason codes. `Resolved` membership can progress only through the
|
||||||
|
existing annotation rules; this DryRun slice never writes an annotation.
|
||||||
|
|
||||||
|
## Paired DryRun evidence
|
||||||
|
|
||||||
|
The paired local DryRun resolved five of seven source rows through the exact
|
||||||
|
view query and classified the remaining two as `ObjectOutsideView`. Each of
|
||||||
|
the three supported commands produced an identical PlanId on its repeated
|
||||||
|
invocation. Both Launcher sessions reached `RolledBack`.
|
||||||
|
|
||||||
|
The paired command-window DBMOD output was not retained. It is intentionally
|
||||||
|
reported as `NOT RECORDED`; project-file SHA-256 inventories nevertheless had
|
||||||
|
zero delta for both CONTROL and DRYRUN copies.
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# Plant 3D 2026 DwgQuery / ModelFiles API contract
|
||||||
|
|
||||||
|
## Scope and method
|
||||||
|
|
||||||
|
This report is based on metadata-only inspection of the installed Plant 3D 2026
|
||||||
|
assemblies. The assemblies were read from `<PLANT3D_2026_INSTALL>` and were not
|
||||||
|
copied into the repository. Metadata-only inspection was necessary because the
|
||||||
|
normal PowerShell host cannot load the product's .NET 8 dependencies safely.
|
||||||
|
|
||||||
|
Legacy source at the documented local path was not present on this machine, so
|
||||||
|
there is no legacy API claim in this report.
|
||||||
|
|
||||||
|
## Confirmed assemblies
|
||||||
|
|
||||||
|
| Assembly | Version | Relevant type |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `Autodesk.ProcessPower.Drawings2d.DwgQuery`, `IDwgQuery`, `DwgViewDef`, `IDwgViewDef`, `DwgSheetDef`, `IDwgSheetDef` |
|
||||||
|
| `PnPDwg2dUtil` | `0.0.0.0` | `Autodesk.ProcessPower.Drawings2d.ModelFile` |
|
||||||
|
| `PnP3dOrthoProjectPart` | `17.0.60.1` | Ortho project-part integration assembly; the metadata scan found no public DwgQuery declaration there |
|
||||||
|
|
||||||
|
`PnPDwg2dDef` and `PnPDwg2dUtil` use `0.0.0.0` assembly versions in the
|
||||||
|
installed metadata. The supported runtime contract is therefore identified by
|
||||||
|
the exact public type names below, not by an invented product-version number.
|
||||||
|
|
||||||
|
## Confirmed contract
|
||||||
|
|
||||||
|
```text
|
||||||
|
Autodesk.ProcessPower.Drawings2d.IDwgViewDef.DwgQuery
|
||||||
|
: Autodesk.ProcessPower.Drawings2d.IDwgQuery
|
||||||
|
|
||||||
|
Autodesk.ProcessPower.Drawings2d.IDwgQuery.ModelFiles
|
||||||
|
: Autodesk.ProcessPower.Drawings2d.ModelFile[]
|
||||||
|
|
||||||
|
Autodesk.ProcessPower.Drawings2d.ModelFile.ModelFileName
|
||||||
|
: System.String
|
||||||
|
|
||||||
|
Autodesk.ProcessPower.Drawings2d.ModelFile.ModelFileGuid
|
||||||
|
: System.String
|
||||||
|
```
|
||||||
|
|
||||||
|
`DwgViewDef` implements `IDwgViewDef`; `DwgQuery` implements `IDwgQuery` and
|
||||||
|
`IDisposable`; `ModelFile` implements `IComparable<ModelFile>`.
|
||||||
|
|
||||||
|
`ModelFiles` is an array, not an untyped enumerable. `ModelFileName` is the
|
||||||
|
only confirmed string field representing the model-file reference. `ModelFileGuid`
|
||||||
|
is a separate identity field. `IsReferencedDwg` and `Xform` describe reference
|
||||||
|
state and transform, not a substitute path.
|
||||||
|
|
||||||
|
## Public members relevant to source resolution
|
||||||
|
|
||||||
|
`IDwgViewDef` exposes `DwgQuery`, `ViewName`, layout/orientation properties,
|
||||||
|
and `SetClippingPlanes`, `SetLayout`, `SetOrientation`, `Erase`, and `New`.
|
||||||
|
|
||||||
|
`IDwgQuery` exposes `ModelFiles`, `Execute`, and `Erase`.
|
||||||
|
|
||||||
|
`ModelFile` exposes `ModelFileName`, `ModelFileGuid`, `IsReferencedDwg`, and
|
||||||
|
`Xform`; its constructors accept a file-name string and optional transform,
|
||||||
|
reference flag, and GUID.
|
||||||
|
|
||||||
|
`IDwgSheetDef.DwgViewDefs` exposes the view-definition collection. Sheet
|
||||||
|
`DwgName`/`DwgPath` identify the Ortho sheet and must not be used as the source
|
||||||
|
of Piping DataLinks.
|
||||||
|
|
||||||
|
## Current and legacy usage
|
||||||
|
|
||||||
|
The original adapter used reflection and tried several possible names
|
||||||
|
(`ModelFileName`, `Value`, and arbitrary string values). That was not a valid
|
||||||
|
Plant 3D 2026 contract.
|
||||||
|
|
||||||
|
The production read path now uses the confirmed typed chain only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IDwgViewDef.DwgQuery.ModelFiles[].ModelFileName
|
||||||
|
```
|
||||||
|
|
||||||
|
The legacy source path was unavailable locally, so no legacy source-model or
|
||||||
|
DataLinks workaround has been imported.
|
||||||
|
|
||||||
|
## Runtime evidence
|
||||||
|
|
||||||
|
Static metadata cannot prove whether `ModelFileName` is project-relative or
|
||||||
|
absolute in a concrete Ortho fixture, or whether a particular view has zero,
|
||||||
|
one, or multiple model files. In DryRun only, setting
|
||||||
|
`P_REC_ANNOT_DIAGNOSTICS=OrthoStructure` enables a sanitized structure probe.
|
||||||
|
It records type names, collection counts, null state, string length, and a
|
||||||
|
one-way value hash; it does not emit paths or engineering data.
|
||||||
|
|
||||||
|
On the local DryRun fixture, the runtime probe recorded one view with one
|
||||||
|
`ModelFile`. The concrete types were `DwgViewDef`, `DwgQueryByBox`, and
|
||||||
|
`ModelFile[]`; `ModelFileName` was non-null and an absolute fixture-model
|
||||||
|
reference. The path itself was not retained in repository evidence.
|
||||||
|
|
||||||
|
The resolver resolves that reference to exactly one
|
||||||
|
`PipingProject.GetPnPDrawingFiles()` entry by normalized full-path equality,
|
||||||
|
then uses the confirmed `DataLinksManager.SelectAcPpRowIds(int dwgid)` overload
|
||||||
|
with the resolved `PnPProjectDrawing.Id`. It does not open or query the active
|
||||||
|
Ortho database for Piping rows.
|
||||||
|
|
||||||
|
The production resolver has no fallback property-name search. If the exact
|
||||||
|
contract is unavailable at runtime it reports
|
||||||
|
`ORTHO_MODEL_FILES_API_UNSUPPORTED` rather than guessing.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Ortho viewport membership API
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This report records the Plant 3D 2026 contract used to map source-model
|
||||||
|
DataLinks objects to one Ortho view. Autodesk binaries were inspected in
|
||||||
|
`<PLANT3D_2026_INSTALL>` and are not part of this repository.
|
||||||
|
|
||||||
|
## Confirmed Plant 3D 2026 contract
|
||||||
|
|
||||||
|
| Assembly | Version | Type/member | Purpose |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `IDwgViewDef.DwgQuery` | Returns the query stored by an Ortho view. |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `DwgQueryByBox.Execute(StringCollection, StringCollection)` | Executes the stored view selection and returns `IDwgSet`. |
|
||||||
|
| `PnPDwg2dDef` | `0.0.0.0` | `IDwgSet.GetItem(int, ref MdObjectId)` | Enumerates selected source identities. |
|
||||||
|
| `PnPDwg2dUtil` | `0.0.0.0` | `MdObjectId.PrjObjectId` | Exposes the selected project object identity. |
|
||||||
|
| `PnPDataLinks` | `17.0.60.1` | `DataLinksManager.FindAcPpRowId(PpObjectId)` | Maps a selected project object to its DataLinks row. |
|
||||||
|
|
||||||
|
`DwgQueryByBox` also exposes `CoordinateSystem`, `XLength`, `YLength`,
|
||||||
|
`ZLength`, and `SelectionMask`. `DwgViewDef` exposes orientation, clipping,
|
||||||
|
and layout fields. They are not used as a heuristic in this slice because the
|
||||||
|
query result provides a stronger project-object identity.
|
||||||
|
|
||||||
|
## Selected implementation
|
||||||
|
|
||||||
|
`PlantViewportMembershipReader.Read` locates exactly one view by the stable
|
||||||
|
ID already generated from the sheet GUID and view name. It executes that
|
||||||
|
view's stored `DwgQuery`, converts every returned `MdObjectId.PrjObjectId`
|
||||||
|
through `DataLinksManager.FindAcPpRowId`, and intersects the result with the
|
||||||
|
source drawing's Piping rows.
|
||||||
|
|
||||||
|
Matching rows receive `Resolved` with
|
||||||
|
`VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID`. Source rows absent from the confirmed
|
||||||
|
query result receive `ObjectOutsideView` with
|
||||||
|
`VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP`. An unsupported or ambiguous runtime
|
||||||
|
contract stays `NotResolved` with `VIEW_MEMBERSHIP_API_UNSUPPORTED`; the code
|
||||||
|
does not guess alternate Autodesk properties or select an arbitrary view.
|
||||||
|
|
||||||
|
## Legacy analysis
|
||||||
|
|
||||||
|
The read-only legacy source at
|
||||||
|
`\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo` was available. Its
|
||||||
|
`UpdateOrthoDrawings/Commands.cs` configured `DwgQuery.ModelFiles` and called
|
||||||
|
`DwgQuery.Execute`; `OrthoAutoAnnotate/Commands.cs` and
|
||||||
|
`AutoAnnotateView/ViewPorts.cs` also used viewport and entity extents. Those
|
||||||
|
extents checks are a geometric filter, not an authoritative object identity,
|
||||||
|
so they were not ported as `Resolved` membership.
|
||||||
|
|
||||||
|
## Side effects and failure handling
|
||||||
|
|
||||||
|
The reader opens no Autodesk write transaction and calls no persistence API.
|
||||||
|
It does not modify DWG, DCF, or project metadata. `DLException` while mapping
|
||||||
|
an individual generated identity is ignored only for that non-Piping identity;
|
||||||
|
other failures yield the stable API diagnostic and preserve no false positive.
|
||||||
|
Runtime DryRun and paired control/DryRun hash comparison remain required to
|
||||||
|
verify actual no-write behavior.
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -57,6 +58,19 @@ public sealed record PlantObjectSnapshot
|
|||||||
public required GeometrySnapshot Geometry { get; init; }
|
public required GeometrySnapshot Geometry { get; init; }
|
||||||
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
public IReadOnlyDictionary<string, string?> Properties { get; init; } = new Dictionary<string, string?>();
|
||||||
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
|
public IReadOnlyList<string> Diagnostics { get; init; } = Array.Empty<string>();
|
||||||
|
public ViewMembershipStatus MembershipStatus { get; init; } = ViewMembershipStatus.NotResolved;
|
||||||
|
public IReadOnlyList<string> MembershipEvidenceCodes { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ViewMembershipStatus { NotResolved, Resolved, PartiallyResolved, ObjectOutsideView, Unsupported, Failed }
|
||||||
|
|
||||||
|
public sealed record SourceObjectViewportMembership
|
||||||
|
{
|
||||||
|
public required string SourceObjectStableKey { get; init; }
|
||||||
|
public required string ViewStableKey { get; init; }
|
||||||
|
public required ViewMembershipStatus Status { get; init; }
|
||||||
|
public IReadOnlyList<string> EvidenceCodes { get; init; } = Array.Empty<string>();
|
||||||
|
public string? ReasonCode { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record PlantRuntimeContext
|
public sealed record PlantRuntimeContext
|
||||||
@@ -66,6 +80,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 +89,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
|
||||||
@@ -106,12 +177,40 @@ public sealed record AnnotationPlanningRequest
|
|||||||
public required OrthoViewportInfo Viewport { get; init; }
|
public required OrthoViewportInfo Viewport { get; init; }
|
||||||
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
public IReadOnlyList<PlantObjectSnapshot> Objects { get; init; } = Array.Empty<PlantObjectSnapshot>();
|
||||||
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
public IReadOnlyList<ExistingAnnotation> Existing { get; init; } = Array.Empty<ExistingAnnotation>();
|
||||||
|
// Test-only limited Apply intent. It is part of the canonical plan and is
|
||||||
|
// deliberately explicit: the planner must never choose an arbitrary Pipe.
|
||||||
|
public string? LimitedApplyTargetSnapshotId { get; init; }
|
||||||
|
public string? LimitedApplyRuleId { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
public interface IAnnotationPlanBuilder { AnnotationPlan Build(AnnotationPlanningRequest request); }
|
||||||
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
public enum PRecAnnotExecutionMode { Smoke, DryRun, Apply }
|
||||||
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
public interface IRuntimeWriteGuard { void EnsureWriteAllowed(string operation); }
|
||||||
|
|
||||||
|
public sealed record CreatePipeAnnotationOperation
|
||||||
|
{
|
||||||
|
public required string OperationId { get; init; }
|
||||||
|
public required string SourceObjectStableKey { get; init; }
|
||||||
|
public required string ViewStableKey { get; init; }
|
||||||
|
public required string RuleId { get; init; }
|
||||||
|
public required AnnotationPoint Placement { get; init; }
|
||||||
|
public required string DuplicateKey { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record FirstLimitedApplyPermit
|
||||||
|
{
|
||||||
|
public required string FixtureMarker { get; init; }
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string TargetSnapshotId { get; init; }
|
||||||
|
public required string ExpectedPlanId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record FirstLimitedApplyPreflight
|
||||||
|
{
|
||||||
|
public required string PlanId { get; init; }
|
||||||
|
public required CreatePipeAnnotationOperation Operation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
public sealed record OrthoViewportRef(string DrawingId, string ViewportId);
|
||||||
|
|
||||||
public sealed record PlantObjectData
|
public sealed record PlantObjectData
|
||||||
@@ -148,6 +247,7 @@ public sealed record AnnotationIntent
|
|||||||
|
|
||||||
public sealed record AnnotationPlan
|
public sealed record AnnotationPlan
|
||||||
{
|
{
|
||||||
|
public int PlanSchemaVersion { get; init; }
|
||||||
public string PlanId { get; init; } = string.Empty;
|
public string PlanId { get; init; } = string.Empty;
|
||||||
public string CommandName { get; init; } = string.Empty;
|
public string CommandName { get; init; } = string.Empty;
|
||||||
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
public IReadOnlyList<AnnotationOperationPlan> Operations { get; init; } = Array.Empty<AnnotationOperationPlan>();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace PRecAnnot.Core;
|
|||||||
|
|
||||||
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
||||||
{
|
{
|
||||||
|
public const int CurrentPlanSchemaVersion = 2;
|
||||||
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
public AnnotationPlan Build(AnnotationPlanningRequest request)
|
||||||
{
|
{
|
||||||
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
var objects = request.Objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
@@ -30,6 +31,65 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.LimitedApplyTargetSnapshotId) &&
|
||||||
|
!string.Equals(item.SnapshotId, request.LimitedApplyTargetSnapshotId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "APPLY_NOT_SELECTED_TARGET" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.MembershipStatus is not ViewMembershipStatus.Resolved and not ViewMembershipStatus.PartiallyResolved)
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { MembershipReason(item.MembershipStatus) } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first Apply slice is intentionally narrower than the
|
||||||
|
// historic bulk rules: exactly one explicitly selected, resolved
|
||||||
|
// Pipe can become a PIPE block operation. All other objects keep
|
||||||
|
// their normal diagnostic treatment.
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.LimitedApplyTargetSnapshotId) &&
|
||||||
|
string.Equals(item.SnapshotId, request.LimitedApplyTargetSnapshotId, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (!string.Equals(item.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Unsupported, Reasons = new[] { "APPLY_UNSUPPORTED_OBJECT_CLASS" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.MembershipStatus != ViewMembershipStatus.Resolved)
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "APPLY_REQUIRES_RESOLVED_MEMBERSHIP" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.Geometry.PaperPosition is not { } pipePosition)
|
||||||
|
{
|
||||||
|
operations.Add(new AnnotationOperationPlan { SourceSnapshotId = item.SnapshotId, Kind = AnnotationOperationKind.Skip, Reasons = new[] { "ANNOTATION_PLACEMENT_NOT_RESOLVED" } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pipeIntent = new AnnotationIntent
|
||||||
|
{
|
||||||
|
StableKey = $"{request.Viewport.Id}:{item.SnapshotId}:{FirstLimitedApplyPlanner.RuleId}",
|
||||||
|
Text = FirstLimitedApplyPlanner.RuleId,
|
||||||
|
Position = new AnnotationPoint(pipePosition.X, pipePosition.Y),
|
||||||
|
Rotation = item.Geometry.Rotation ?? 0,
|
||||||
|
ClassName = item.ObjectClass,
|
||||||
|
Layer = "PXP_ANNOTATION"
|
||||||
|
};
|
||||||
|
operations.Add(new AnnotationOperationPlan
|
||||||
|
{
|
||||||
|
SourceSnapshotId = item.SnapshotId,
|
||||||
|
Kind = AnnotationOperationKind.Create,
|
||||||
|
AnnotationType = "PIPE",
|
||||||
|
Text = FirstLimitedApplyPlanner.RuleId,
|
||||||
|
Position = pipePosition,
|
||||||
|
Rotation = item.Geometry.Rotation,
|
||||||
|
Reasons = Array.Empty<string>()
|
||||||
|
});
|
||||||
|
creates.Add(pipeIntent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
var text = string.IsNullOrWhiteSpace(item.LineNumber) ? item.Tag?.Trim() : item.LineNumber!.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
{
|
{
|
||||||
@@ -84,7 +144,8 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
|
|
||||||
return new AnnotationPlan
|
return new AnnotationPlan
|
||||||
{
|
{
|
||||||
PlanId = BuildPlanId(request, objects),
|
PlanSchemaVersion = CurrentPlanSchemaVersion,
|
||||||
|
PlanId = BuildPlanId(request, objects, operations),
|
||||||
CommandName = request.CommandName,
|
CommandName = request.CommandName,
|
||||||
Operations = operations,
|
Operations = operations,
|
||||||
Diagnostics = diagnostics,
|
Diagnostics = diagnostics,
|
||||||
@@ -92,18 +153,41 @@ public sealed class AnnotationPlanBuilder : IAnnotationPlanBuilder
|
|||||||
Create = creates,
|
Create = creates,
|
||||||
Update = updates,
|
Update = updates,
|
||||||
Delete = Array.Empty<string>(),
|
Delete = Array.Empty<string>(),
|
||||||
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).ToArray(),
|
SkipReasons = operations.Where(x => x.Kind == AnnotationOperationKind.Skip).SelectMany(x => x.Reasons).OrderBy(x => x, StringComparer.Ordinal).ToArray(),
|
||||||
ExaminedCount = objects.Length,
|
ExaminedCount = objects.Length,
|
||||||
IsDeterministic = true
|
IsDeterministic = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects)
|
private static string BuildPlanId(AnnotationPlanningRequest request, IReadOnlyList<PlantObjectSnapshot> objects, IReadOnlyList<AnnotationOperationPlan> operations)
|
||||||
{
|
{
|
||||||
var canonical = new StringBuilder(request.CommandName).Append('|').Append(request.Viewport.SourceDrawingName).Append('|').Append(request.Viewport.Id);
|
var canonical = new StringBuilder("schema=").Append(CurrentPlanSchemaVersion)
|
||||||
foreach (var item in objects)
|
.Append("|command=").Append(Normalize(request.CommandName))
|
||||||
canonical.Append('|').Append(item.SnapshotId).Append('|').Append(item.ObjectClass).Append('|').Append(item.Tag).Append('|').Append(item.LineNumber).Append('|').Append(item.Geometry.PaperPosition);
|
.Append("|source=").Append(Normalize(Path.GetFileName(request.Viewport.SourceDrawingName)))
|
||||||
|
.Append("|view=").Append(Normalize(request.Viewport.Id))
|
||||||
|
.Append("|limited-target=").Append(Normalize(request.LimitedApplyTargetSnapshotId))
|
||||||
|
.Append("|limited-rule=").Append(Normalize(request.LimitedApplyRuleId));
|
||||||
|
foreach (var item in objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
canonical.Append("|object=").Append(Normalize(item.SnapshotId)).Append('|').Append(Normalize(item.ObjectClass))
|
||||||
|
.Append('|').Append(Normalize(item.Tag)).Append('|').Append(Normalize(item.LineNumber))
|
||||||
|
.Append('|').Append(item.MembershipStatus);
|
||||||
|
foreach (var property in item.Properties.OrderBy(x => x.Key, StringComparer.Ordinal))
|
||||||
|
canonical.Append("|property=").Append(Normalize(property.Key)).Append('=').Append(Normalize(property.Value));
|
||||||
|
}
|
||||||
|
foreach (var operation in operations.OrderBy(x => x.SourceSnapshotId, StringComparer.Ordinal).ThenBy(x => x.Kind))
|
||||||
|
canonical.Append("|operation=").Append(Normalize(operation.SourceSnapshotId)).Append(':').Append(operation.Kind).Append(':').Append(string.Join(',', operation.Reasons.OrderBy(x => x, StringComparer.Ordinal)));
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString()));
|
||||||
return "plan-" + Convert.ToHexString(hash)[..24];
|
return "plan-" + Convert.ToHexString(hash)[..24];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string? value) => (value ?? string.Empty).Trim().Normalize(NormalizationForm.FormC);
|
||||||
|
|
||||||
|
private static string MembershipReason(ViewMembershipStatus status) => status switch
|
||||||
|
{
|
||||||
|
ViewMembershipStatus.ObjectOutsideView => "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP",
|
||||||
|
ViewMembershipStatus.Unsupported => "VIEW_MEMBERSHIP_API_UNSUPPORTED",
|
||||||
|
ViewMembershipStatus.Failed => "VIEW_MEMBERSHIP_TRANSFORM_FAILED",
|
||||||
|
_ => "VIEW_MEMBERSHIP_NOT_RESOLVED"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public sealed class AnnotationRuleEngine : IAnnotationRuleEngine
|
|||||||
ObjectClass = item.ClassName,
|
ObjectClass = item.ClassName,
|
||||||
Tag = item.Tag,
|
Tag = item.Tag,
|
||||||
LineNumber = item.LineNumber,
|
LineNumber = item.LineNumber,
|
||||||
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation }
|
Geometry = new GeometrySnapshot { PaperPosition = new Point2Dto(item.Position.X, item.Position.Y), Rotation = item.Rotation },
|
||||||
|
MembershipStatus = ViewMembershipStatus.Resolved
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
return new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class FirstLimitedApplyPlanner
|
||||||
|
{
|
||||||
|
public const string FixtureMarker = "FIRST_LIMITED_APPLY";
|
||||||
|
public const string RuleId = "PXP-OA.PIPE.OPEN-END.V1";
|
||||||
|
|
||||||
|
public FirstLimitedApplyPreflight Prepare(
|
||||||
|
PRecAnnotExecutionMode mode,
|
||||||
|
AnnotationPlan plan,
|
||||||
|
OrthoViewportInfo viewport,
|
||||||
|
IReadOnlyList<PlantObjectSnapshot> objects,
|
||||||
|
FirstLimitedApplyPermit permit)
|
||||||
|
{
|
||||||
|
if (mode != PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_REQUIRED");
|
||||||
|
if (!permit.AllowApply || !string.Equals(permit.FixtureMarker, FixtureMarker, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
if (!string.Equals(permit.ExpectedPlanId, plan.PlanId, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("APPLY_PLAN_STALE");
|
||||||
|
|
||||||
|
var targets = objects.Where(x => string.Equals(x.SnapshotId, permit.TargetSnapshotId, StringComparison.Ordinal)).ToArray();
|
||||||
|
if (targets.Length != 1) throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_TARGET");
|
||||||
|
var target = targets[0];
|
||||||
|
if (target.MembershipStatus != ViewMembershipStatus.Resolved) throw new InvalidOperationException("APPLY_REQUIRES_RESOLVED_MEMBERSHIP");
|
||||||
|
if (!string.Equals(target.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("APPLY_UNSUPPORTED_OBJECT_CLASS");
|
||||||
|
if (target.Geometry.PaperPosition is not { } placement) throw new InvalidOperationException("ANNOTATION_PLACEMENT_NOT_RESOLVED");
|
||||||
|
|
||||||
|
var create = plan.Operations.Where(x => x.Kind == AnnotationOperationKind.Create && string.Equals(x.SourceSnapshotId, target.SnapshotId, StringComparison.Ordinal)).ToArray();
|
||||||
|
if (create.Length != 1) throw new InvalidOperationException("APPLY_TARGET_CREATE_NOT_PLANNED");
|
||||||
|
|
||||||
|
var duplicateKey = Stable("duplicate", RuleId, viewport.Id, target.SnapshotId);
|
||||||
|
return new FirstLimitedApplyPreflight
|
||||||
|
{
|
||||||
|
PlanId = plan.PlanId,
|
||||||
|
Operation = new CreatePipeAnnotationOperation
|
||||||
|
{
|
||||||
|
OperationId = Stable("operation", RuleId, viewport.Id, target.SnapshotId),
|
||||||
|
SourceObjectStableKey = target.SnapshotId,
|
||||||
|
ViewStableKey = viewport.Id,
|
||||||
|
RuleId = RuleId,
|
||||||
|
Placement = new AnnotationPoint(placement.X, placement.Y),
|
||||||
|
DuplicateKey = duplicateKey
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Stable(params string[] values) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("|", values))))[..24];
|
||||||
|
}
|
||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,6 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
{
|
{
|
||||||
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
||||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
|
||||||
|
|
||||||
var context = new PlantRuntimeContextReader().ReadCurrentContext();
|
var context = new PlantRuntimeContextReader().ReadCurrentContext();
|
||||||
var details = new Dictionary<string, object?>
|
var details = new Dictionary<string, object?>
|
||||||
@@ -75,12 +74,42 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mode == PRecAnnotExecutionMode.Apply && !string.Equals(commandName, "PXP-OA", StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("APPLY_RULE_NOT_SUPPORTED");
|
||||||
|
var hasLimitedApplyPermit = !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("P_REC_ANNOT_APPLY_PERMIT_PATH"));
|
||||||
|
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
||||||
|
? FirstLimitedApplyPermitReader.ReadFor(document)
|
||||||
|
: null;
|
||||||
|
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
||||||
|
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
||||||
|
|
||||||
var builder = new AnnotationPlanBuilder();
|
var builder = new AnnotationPlanBuilder();
|
||||||
var plans = new List<AnnotationPlan>();
|
var plans = new List<AnnotationPlan>();
|
||||||
|
var objectsByViewport = new List<IReadOnlyList<PlantObjectSnapshot>>();
|
||||||
foreach (var viewport in context.Viewports)
|
foreach (var viewport in context.Viewports)
|
||||||
{
|
{
|
||||||
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||||
plans.Add(builder.Build(new AnnotationPlanningRequest { CommandName = commandName, Viewport = viewport, Objects = objects }));
|
objectsByViewport.Add(objects);
|
||||||
|
plans.Add(builder.Build(new AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
CommandName = commandName,
|
||||||
|
Viewport = viewport,
|
||||||
|
Objects = objects,
|
||||||
|
LimitedApplyTargetSnapshotId = applyPermit?.TargetSnapshotId,
|
||||||
|
LimitedApplyRuleId = applyPermit is null ? null : FirstLimitedApplyPlanner.RuleId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == PRecAnnotExecutionMode.DryRun &&
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_DIAGNOSTICS"), "FirstLimitedApply", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var candidates = objectsByViewport.SelectMany(x => x)
|
||||||
|
.Where(x => string.Equals(x.ObjectClass, "Pipe", StringComparison.OrdinalIgnoreCase) && x.MembershipStatus == ViewMembershipStatus.Resolved && x.Geometry.PaperPosition is not null)
|
||||||
|
.Select(x => x.SnapshotId)
|
||||||
|
.OrderBy(x => x, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
WriteEditor(document, $"\nP_Rec_Annot Apply preview candidates: {string.Join(",", candidates)}");
|
||||||
|
details["ApplyPreviewCandidateCount"] = candidates.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
var summary = new
|
var summary = new
|
||||||
@@ -94,17 +123,50 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
WarningCount = plans.Sum(x => x.Summary.WarningCount),
|
||||||
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
ErrorCount = plans.Sum(x => x.Summary.ErrorCount)
|
||||||
};
|
};
|
||||||
|
var membershipResolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved);
|
||||||
|
var membershipPartial = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.PartiallyResolved);
|
||||||
|
var membershipUnresolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.NotResolved);
|
||||||
|
var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||||
|
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||||
|
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||||
|
details["PlanId"] = planId;
|
||||||
|
details["MembershipResolved"] = membershipResolved;
|
||||||
|
details["MembershipPartial"] = membershipPartial;
|
||||||
|
details["MembershipUnresolved"] = membershipUnresolved;
|
||||||
|
details["MembershipOutsideView"] = membershipOutside;
|
||||||
details["PlanCreateCount"] = summary.CreateCount;
|
details["PlanCreateCount"] = summary.CreateCount;
|
||||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||||
details["PlanSkipCount"] = summary.SkipCount;
|
details["PlanSkipCount"] = summary.SkipCount;
|
||||||
details["PlanConflictCount"] = summary.ConflictCount;
|
details["PlanConflictCount"] = summary.ConflictCount;
|
||||||
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
details["PlanUnsupportedCount"] = summary.UnsupportedCount;
|
||||||
details["ErrorCount"] = summary.ErrorCount;
|
details["ErrorCount"] = summary.ErrorCount;
|
||||||
|
if (mode == PRecAnnotExecutionMode.Apply)
|
||||||
|
{
|
||||||
|
var corePermit = new FirstLimitedApplyPermit
|
||||||
|
{
|
||||||
|
FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker,
|
||||||
|
AllowApply = applyPermit!.AllowApply,
|
||||||
|
TargetSnapshotId = applyPermit.TargetSnapshotId,
|
||||||
|
ExpectedPlanId = applyPermit.ExpectedPlanId
|
||||||
|
};
|
||||||
|
var preflight = new FirstLimitedApplyPlanner().Prepare(mode, plans.Single(), context.Viewports.Single(), objectsByViewport.Single(), corePermit);
|
||||||
|
new RuntimeWriteGuard(mode).EnsureWriteAllowed(preflight.Operation.OperationId);
|
||||||
|
RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document);
|
||||||
|
RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document);
|
||||||
|
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState);
|
||||||
|
details["ApplyOutcome"] = outcome.ToString();
|
||||||
|
details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0;
|
||||||
|
details["AnnotationsSkipped"] = outcome == PipeAnnotationWriteOutcome.AlreadyExists ? 1 : 0;
|
||||||
|
RuntimeDiagnostics.Write(outcome == PipeAnnotationWriteOutcome.Created ? "AnnotationCreated" : "AnnotationDuplicateDetected", operationId: operationId, commandName: commandName, resultStatus: outcome.ToString(), document: document);
|
||||||
|
RuntimeDiagnostics.Write("AnnotationTransactionCommitted", operationId: operationId, commandName: commandName, resultStatus: outcome.ToString(), document: document);
|
||||||
|
}
|
||||||
RuntimeDiagnostics.Write("CommandPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
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 не изменён.");
|
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);
|
WriteEditor(document, $"\nPlan schema: {AnnotationPlanBuilder.CurrentPlanSchemaVersion}; PlanId: {planId ?? "multiple"}; membership resolved/partial/outside/unresolved: {membershipResolved}/{membershipPartial}/{membershipOutside}/{membershipUnresolved}.");
|
||||||
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: "DryRunCompleted", document: document, details: details);
|
var completedStatus = mode == PRecAnnotExecutionMode.Apply ? "ApplyCompleted" : "DryRunCompleted";
|
||||||
|
WriteUsage(operationId, commandName, completedStatus, document, details, stopwatch.ElapsedMilliseconds);
|
||||||
|
RuntimeDiagnostics.Write("CommandCompleted", operationId: operationId, commandName: commandName, resultStatus: completedStatus, document: document, details: details);
|
||||||
}
|
}
|
||||||
catch (System.Exception exception)
|
catch (System.Exception exception)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// This file is deliberately runtime-only. Core receives only the anonymised
|
||||||
|
// permit and canonical operation; no AutoCAD handle, database or project path
|
||||||
|
// crosses the adapter boundary.
|
||||||
|
internal sealed record FirstLimitedApplyRuntimePermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
public required string TargetSnapshotId { get; init; }
|
||||||
|
public required string ExpectedPlanId { get; init; }
|
||||||
|
public required string VisibilityState { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class FirstLimitedApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=FIRST_LIMITED_APPLY";
|
||||||
|
|
||||||
|
public static FirstLimitedApplyRuntimePermit ReadFor(Document document)
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("APPLY_PERMIT_PATH_REQUIRED");
|
||||||
|
if (!File.Exists(path)) throw new InvalidOperationException("APPLY_PERMIT_MISSING");
|
||||||
|
|
||||||
|
var permit = JsonSerializer.Deserialize<FirstLimitedApplyRuntimePermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
if (permit is null || !permit.AllowApply || string.IsNullOrWhiteSpace(permit.TargetSnapshotId) || string.IsNullOrWhiteSpace(permit.ExpectedPlanId) || string.IsNullOrWhiteSpace(permit.VisibilityState))
|
||||||
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath))
|
||||||
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
if (!string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
|
||||||
|
var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))!;
|
||||||
|
var drawing = Path.GetFullPath(document.Name);
|
||||||
|
if (!drawing.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
return permit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum PipeAnnotationWriteOutcome { Created, AlreadyExists }
|
||||||
|
|
||||||
|
internal sealed class FirstLimitedPipeAnnotationWriter
|
||||||
|
{
|
||||||
|
private const string BlockName = "PIPE";
|
||||||
|
private const string XdataApplication = "PXP-OA";
|
||||||
|
|
||||||
|
public PipeAnnotationWriteOutcome Write(Document document, CreatePipeAnnotationOperation operation, string visibilityState)
|
||||||
|
{
|
||||||
|
var database = document.Database;
|
||||||
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
|
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
if (!table.Has(BlockName)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_BLOCK_MISSING");
|
||||||
|
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
if (HasOwnedAnnotation(transaction, paper, operation.DuplicateKey)) return PipeAnnotationWriteOutcome.AlreadyExists;
|
||||||
|
EnsureRegApp(transaction, database);
|
||||||
|
|
||||||
|
using var annotation = new BlockReference(
|
||||||
|
new Point3d(operation.Placement.X, operation.Placement.Y, operation.Placement.Z),
|
||||||
|
table[BlockName]);
|
||||||
|
paper.AppendEntity(annotation);
|
||||||
|
transaction.AddNewlyCreatedDBObject(annotation, true);
|
||||||
|
SetConfirmedVisibility(annotation, visibilityState);
|
||||||
|
annotation.XData = new ResultBuffer(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, XdataApplication),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, operation.DuplicateKey),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, operation.RuleId));
|
||||||
|
transaction.Commit();
|
||||||
|
return PipeAnnotationWriteOutcome.Created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasOwnedAnnotation(Transaction transaction, BlockTableRecord paper, string duplicateKey)
|
||||||
|
{
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue;
|
||||||
|
using var xdata = reference.GetXDataForApplication(XdataApplication);
|
||||||
|
if (xdata?.AsArray().Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, duplicateKey, StringComparison.Ordinal)) == true)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database)
|
||||||
|
{
|
||||||
|
var apps = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
|
||||||
|
if (apps.Has(XdataApplication)) return;
|
||||||
|
apps.UpgradeOpen();
|
||||||
|
using var record = new RegAppTableRecord { Name = XdataApplication };
|
||||||
|
apps.Add(record);
|
||||||
|
transaction.AddNewlyCreatedDBObject(record, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "V" property and allowed values are the confirmed legacy PXP-OA
|
||||||
|
// dynamic-block contract. Failure is pre-commit and leaves no entity.
|
||||||
|
private static void SetConfirmedVisibility(BlockReference annotation, string visibilityState)
|
||||||
|
{
|
||||||
|
var property = annotation.DynamicBlockReferencePropertyCollection
|
||||||
|
.Cast<DynamicBlockReferenceProperty>()
|
||||||
|
.SingleOrDefault(value => string.Equals(value.PropertyName, "V", StringComparison.Ordinal));
|
||||||
|
if (property is null || !property.GetAllowedValues().OfType<string>().Contains(visibilityState, StringComparer.Ordinal))
|
||||||
|
throw new InvalidOperationException("ANNOTATION_VISIBILITY_STATE_UNSUPPORTED");
|
||||||
|
property.Value = visibilityState;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Autodesk.ProcessPower.Drawings2d;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// This is a diagnostic-only probe. It is never part of source resolution and is
|
||||||
|
// called only by the explicit DryRun/OrthoStructure environment flag combination.
|
||||||
|
internal static class OrthoRuntimeStructureInspector
|
||||||
|
{
|
||||||
|
public static void Inspect(IDwgViewDef view)
|
||||||
|
{
|
||||||
|
if (!RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = view.DwgQuery;
|
||||||
|
var modelFiles = query?.ModelFiles;
|
||||||
|
var details = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ViewRuntimeType"] = TypeName(view.GetType()),
|
||||||
|
["DwgQueryRuntimeType"] = query is null ? null : TypeName(query.GetType()),
|
||||||
|
["ModelFilesDeclaredType"] = typeof(ModelFile[]).FullName,
|
||||||
|
["ModelFilesRuntimeType"] = modelFiles?.GetType().AssemblyQualifiedName,
|
||||||
|
["ModelFilesCount"] = modelFiles?.Length ?? 0,
|
||||||
|
["ModelFileElements"] = modelFiles?.Select(DescribeModelFile).ToArray() ?? Array.Empty<IReadOnlyDictionary<string, object?>>()
|
||||||
|
};
|
||||||
|
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspected", resultStatus: "Captured", details: details);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoRuntimeStructureInspectionFailed", "WARNING", resultStatus: "Failed", exception: exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, object?> DescribeModelFile(ModelFile? modelFile)
|
||||||
|
{
|
||||||
|
if (modelFile is null)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, object?> { ["IsNull"] = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
var properties = modelFile.GetType()
|
||||||
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(property => property.CanRead && property.GetIndexParameters().Length == 0)
|
||||||
|
.OrderBy(property => property.Name, StringComparer.Ordinal)
|
||||||
|
.Select(property => DescribeProperty(modelFile, property))
|
||||||
|
.ToArray();
|
||||||
|
return new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["RuntimeType"] = TypeName(modelFile.GetType()),
|
||||||
|
["Properties"] = properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, object?> DescribeProperty(ModelFile modelFile, PropertyInfo property)
|
||||||
|
{
|
||||||
|
object? value;
|
||||||
|
try { value = property.GetValue(modelFile); }
|
||||||
|
catch { value = null; }
|
||||||
|
return new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["PropertyName"] = property.Name,
|
||||||
|
["PropertyType"] = property.PropertyType.FullName,
|
||||||
|
["ValueKind"] = value?.GetType().FullName ?? "null",
|
||||||
|
["IsNull"] = value is null,
|
||||||
|
["StringLength"] = value is string text ? text.Length : null,
|
||||||
|
["PathKind"] = property.Name == "ModelFileName" && value is string path
|
||||||
|
? (Path.IsPathRooted(path) ? "Absolute" : "ProjectRelative")
|
||||||
|
: null,
|
||||||
|
["CollectionCount"] = value is ICollection collection ? collection.Count : null,
|
||||||
|
["StableHash"] = value is null ? null : Hash(value.ToString() ?? string.Empty)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TypeName(Type type) => $"{type.FullName}, {type.Assembly.GetName().Name}, {type.Assembly.GetName().Version}";
|
||||||
|
|
||||||
|
private static string Hash(string value) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..16];
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
<Reference Include="PnPDataObjects"><HintPath>$(AutoCAD2026ManagedDir)\PnPDataObjects.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="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="PnP3dObjectsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dObjectsMgd.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<Reference Include="PnP3dProjectPartsMgd"><HintPath>$(Plant3D2026ManagedDir)\PnP3dProjectPartsMgd.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="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>
|
<Reference Include="PnPDwg2dDef"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dDef.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Collections.Specialized;
|
||||||
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.Drawings2d;
|
||||||
|
using Autodesk.ProcessPower.PnP3dObjects;
|
||||||
using Autodesk.ProcessPower.P3dProjectParts;
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
using Autodesk.ProcessPower.ProjectManager;
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
@@ -32,6 +36,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 +67,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 +90,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 +134,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,22 +153,137 @@ 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(IDwgViewDef view)
|
||||||
|
{
|
||||||
|
// Confirmed Plant 3D 2026 contract (PnPDwg2dDef/PnPDwg2dUtil):
|
||||||
|
// IDwgViewDef.DwgQuery -> IDwgQuery.ModelFiles -> ModelFile.ModelFileName.
|
||||||
|
// The Ortho sheet is never used as a Piping DataLinks source.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoModelFilesContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RuntimeDiagnostics.IsOrthoStructureDiagnosticsEnabled)
|
||||||
|
{
|
||||||
|
OrthoRuntimeStructureInspector.Inspect(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.DwgQuery?.ModelFiles
|
||||||
|
.Select(file => file.ModelFileName)
|
||||||
|
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoModelFilesReadFailed", "WARNING", resultStatus: "ORTHO_MODEL_FILES_API_UNSUPPORTED", exception: exception);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class OrthoModelFilesContract
|
||||||
|
{
|
||||||
|
private const string ExpectedViewType = "Autodesk.ProcessPower.Drawings2d.DwgViewDef";
|
||||||
|
private const string ExpectedQueryType = "Autodesk.ProcessPower.Drawings2d.DwgQueryByBox";
|
||||||
|
private const string ExpectedModelFileType = "Autodesk.ProcessPower.Drawings2d.ModelFile";
|
||||||
|
private const string ExpectedDefAssembly = "PnPDwg2dDef";
|
||||||
|
private const string ExpectedUtilAssembly = "PnPDwg2dUtil";
|
||||||
|
private const string ExpectedVersion = "0.0.0.0";
|
||||||
|
|
||||||
|
public static bool IsSupported(IDwgViewDef view, out OrthoModelFilesContractDiagnostic diagnostic)
|
||||||
|
{
|
||||||
|
var query = view.DwgQuery;
|
||||||
|
var files = query?.ModelFiles;
|
||||||
|
var valid = HasExpectedType(view, ExpectedViewType, ExpectedDefAssembly) &&
|
||||||
|
query is not null && HasExpectedType(query, ExpectedQueryType, ExpectedDefAssembly) &&
|
||||||
|
files is not null && files.GetType() == typeof(ModelFile[]) &&
|
||||||
|
files.All(file => file is not null && HasExpectedType(file, ExpectedModelFileType, ExpectedUtilAssembly));
|
||||||
|
diagnostic = new OrthoModelFilesContractDiagnostic(
|
||||||
|
valid ? "ORTHO_MODEL_FILES_API_SUPPORTED" : "ORTHO_MODEL_FILES_API_UNSUPPORTED",
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ExpectedViewType"] = ExpectedViewType,
|
||||||
|
["ActualViewType"] = TypeName(view),
|
||||||
|
["ExpectedQueryType"] = ExpectedQueryType,
|
||||||
|
["ActualQueryType"] = query is null ? null : TypeName(query),
|
||||||
|
["ExpectedModelFileType"] = ExpectedModelFileType,
|
||||||
|
["ActualModelFilesType"] = files?.GetType().AssemblyQualifiedName,
|
||||||
|
["ExpectedAssemblyVersion"] = ExpectedVersion,
|
||||||
|
["ActualViewAssemblyVersion"] = view.GetType().Assembly.GetName().Version?.ToString(),
|
||||||
|
["ActualQueryAssemblyVersion"] = query?.GetType().Assembly.GetName().Version?.ToString(),
|
||||||
|
["ActualModelFilesAssemblyVersion"] = files?.GetType().Assembly.GetName().Version?.ToString()
|
||||||
|
});
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasExpectedType(object value, string expectedType, string expectedAssembly) =>
|
||||||
|
string.Equals(value.GetType().FullName, expectedType, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(value.GetType().Assembly.GetName().Name, expectedAssembly, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(value.GetType().Assembly.GetName().Version?.ToString(), ExpectedVersion, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static string TypeName(object value) => $"{value.GetType().FullName}, {value.GetType().Assembly.GetName().Name}, {value.GetType().Assembly.GetName().Version}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record OrthoModelFilesContractDiagnostic(string Code, IReadOnlyDictionary<string, object?> Details);
|
||||||
|
|
||||||
internal sealed class PlantObjectReader : IPlantObjectReader
|
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;
|
if (string.IsNullOrWhiteSpace(viewport.SourceDrawingName))
|
||||||
if (document is null) return Array.Empty<PlantObjectSnapshot>();
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
PipingProject? pipingProject;
|
||||||
|
try { pipingProject = PlantApplication.ProjectPart("Piping") as PipingProject; }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport, exception);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
if (pipingProject is null)
|
||||||
|
{
|
||||||
|
WriteOutcome("PIPING_PROJECT_PART_NOT_FOUND", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceDrawing = ResolveProjectDrawing(pipingProject, viewport.SourceDrawingName);
|
||||||
|
if (sourceDrawing is null) return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
|
||||||
DataLinksManager? dataLinks;
|
DataLinksManager? dataLinks;
|
||||||
try { dataLinks = PlantApplication.ProjectPart("Piping")?.DataLinksManager; }
|
try { dataLinks = pipingProject.DataLinksManager; }
|
||||||
catch { return Array.Empty<PlantObjectSnapshot>(); }
|
catch (Exception exception)
|
||||||
if (dataLinks is null) return Array.Empty<PlantObjectSnapshot>();
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
if (dataLinks is null)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport);
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
var snapshots = new List<PlantObjectSnapshot>();
|
var snapshots = new List<PlantObjectSnapshot>();
|
||||||
foreach (var rowId in dataLinks.SelectAcPpRowIds(document.Database))
|
try
|
||||||
|
{
|
||||||
|
// Confirmed Plant 3D 2026 contract: resolve the source model to the
|
||||||
|
// Piping project drawing record, then read its DataLinks by drawing Id.
|
||||||
|
// The active Ortho database is deliberately never passed to DataLinks.
|
||||||
|
var rowIds = dataLinks.SelectAcPpRowIds(sourceDrawing.Id);
|
||||||
|
if (rowIds.Count == 0)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_NO_ROWS", viewport, details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||||
|
return Array.Empty<PlantObjectSnapshot>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var membership = PlantViewportMembershipReader.Read(viewport, dataLinks, rowIds);
|
||||||
|
|
||||||
|
foreach (var rowId in rowIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -170,25 +295,91 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
string? Value(params string[] names) => properties.FirstOrDefault(x => names.Any(name => string.Equals(name, x.Key, StringComparison.OrdinalIgnoreCase))).Value;
|
||||||
snapshots.Add(new PlantObjectSnapshot
|
snapshots.Add(new PlantObjectSnapshot
|
||||||
{
|
{
|
||||||
SnapshotId = StableId(Path.GetFileName(document.Name), rowId, className),
|
SnapshotId = StableId(viewport.SourceDrawingName, rowId, className),
|
||||||
ObjectClass = className ?? string.Empty,
|
ObjectClass = className ?? string.Empty,
|
||||||
Tag = Value("Tag", "TAG"),
|
Tag = Value("Tag", "TAG"),
|
||||||
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
LineNumber = Value("Line Number", "LineNumber", "LINENUMBER"),
|
||||||
SupportNumber = Value("Support Number", "SupportNumber"),
|
SupportNumber = Value("Support Number", "SupportNumber"),
|
||||||
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
EquipmentNumber = Value("Equipment Number", "EquipmentNumber"),
|
||||||
Geometry = new GeometrySnapshot(),
|
Geometry = membership.GeometryFor(rowId),
|
||||||
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
Properties = properties.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase),
|
||||||
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" }
|
Diagnostics = new[] { "GEOMETRY_NOT_MAPPED" },
|
||||||
|
MembershipStatus = membership.StatusFor(rowId),
|
||||||
|
MembershipEvidenceCodes = membership.EvidenceFor(rowId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
// Keep the read path best-effort; no write operation is attempted.
|
RuntimeDiagnostics.Write("SourceDataLinksRowReadFailed", "WARNING", resultStatus: "SOURCE_DATABASE_READ_FAILED", exception: exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch (DLException exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATALINKS_UNAVAILABLE", viewport, exception);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("SOURCE_DATABASE_READ_FAILED", viewport, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteOutcome(snapshots.Count == 0 ? "SOURCE_PIPE_ROWS_NOT_FOUND" : "SOURCE_DATALINKS_READ", viewport,
|
||||||
|
details: new Dictionary<string, object?> { ["SourcePlantObjectsRead"] = snapshots.Count, ["PipeObjectsRead"] = snapshots.Count });
|
||||||
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
return snapshots.OrderBy(x => x.SnapshotId, StringComparer.Ordinal).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PnPProjectDrawing? ResolveProjectDrawing(PipingProject pipingProject, string sourceDrawingPath)
|
||||||
|
{
|
||||||
|
if (!Path.IsPathRooted(sourceDrawingPath))
|
||||||
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalizedSource;
|
||||||
|
try { normalizedSource = Path.GetFullPath(sourceDrawingPath); }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
WriteOutcome("ORTHO_SOURCE_DRAWING_NOT_RESOLVED", null, exception);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = pipingProject.GetPnPDrawingFiles()
|
||||||
|
.Where(drawing => MatchesSourcePath(drawing, normalizedSource))
|
||||||
|
.OrderBy(drawing => drawing.Id)
|
||||||
|
.ToArray();
|
||||||
|
if (matches.Length == 1)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("SourceProjectDrawingResolved", resultStatus: "SOURCE_FROM_PROJECT_DRAWING_ROW", details: new Dictionary<string, object?> { ["ProjectDrawingResolved"] = true });
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteOutcome(matches.Length == 0 ? "ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT" : "ORTHO_SOURCE_DRAWING_AMBIGUOUS", null,
|
||||||
|
details: new Dictionary<string, object?> { ["CandidateCount"] = matches.Length });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesSourcePath(PnPProjectDrawing drawing, string normalizedSource) =>
|
||||||
|
EquivalentPath(drawing.AbsoluteFileName, normalizedSource) ||
|
||||||
|
EquivalentPath(drawing.ResolvedFilePath, normalizedSource);
|
||||||
|
|
||||||
|
private static bool EquivalentPath(string? candidate, string normalizedSource)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate)) return false;
|
||||||
|
try { return string.Equals(Path.GetFullPath(candidate), normalizedSource, StringComparison.OrdinalIgnoreCase); }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteOutcome(string code, OrthoViewportInfo? viewport, Exception? exception = null, IReadOnlyDictionary<string, object?>? details = null)
|
||||||
|
{
|
||||||
|
var sanitizedDetails = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["SourceResolved"] = viewport?.SourceDrawingName is not null
|
||||||
|
};
|
||||||
|
if (details is not null) foreach (var detail in details) sanitizedDetails[detail.Key] = detail.Value;
|
||||||
|
RuntimeDiagnostics.Write("SourceDataLinksReadCompleted", code == "SOURCE_DATALINKS_READ" ? "INFO" : "WARNING", resultStatus: code, exception: exception, details: sanitizedDetails);
|
||||||
|
}
|
||||||
|
|
||||||
private static string StableId(string drawingName, int rowId, string? className)
|
private static string StableId(string drawingName, int rowId, string? className)
|
||||||
{
|
{
|
||||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes($"{drawingName}|{rowId}|{className}"));
|
||||||
@@ -196,11 +387,139 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class PlantViewportMembershipReader
|
||||||
|
{
|
||||||
|
internal readonly record struct MembershipReadResult(
|
||||||
|
IReadOnlySet<int> SelectedRows,
|
||||||
|
ViewMembershipStatus DefaultStatus,
|
||||||
|
string DefaultCode,
|
||||||
|
IReadOnlyDictionary<int, GeometrySnapshot>? Geometry = null)
|
||||||
|
{
|
||||||
|
public ViewMembershipStatus StatusFor(int rowId) =>
|
||||||
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||||
|
? ViewMembershipStatus.ObjectOutsideView
|
||||||
|
: DefaultStatus;
|
||||||
|
|
||||||
|
public IReadOnlyList<string> EvidenceFor(int rowId) =>
|
||||||
|
DefaultStatus == ViewMembershipStatus.Resolved && !SelectedRows.Contains(rowId)
|
||||||
|
? new[] { "VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP" }
|
||||||
|
: new[] { DefaultCode };
|
||||||
|
|
||||||
|
public GeometrySnapshot GeometryFor(int rowId) =>
|
||||||
|
Geometry is not null && Geometry.TryGetValue(rowId, out var geometry)
|
||||||
|
? geometry
|
||||||
|
: new GeometrySnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MembershipReadResult Read(OrthoViewportInfo viewport, DataLinksManager dataLinks, Autodesk.ProcessPower.DataObjects.PnPRowIdArray sourceRows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject;
|
||||||
|
if (project is null) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
|
||||||
|
var candidates = project.DwgSheetDefinitions.AsArray()
|
||||||
|
.SelectMany(sheet => sheet.DwgViewDefs.AsArray()
|
||||||
|
.Where(view => string.Equals(StableId(sheet.DwgGuid.ToString(), view.ViewName), viewport.Id, StringComparison.Ordinal))
|
||||||
|
.Select(view => (sheet, view)))
|
||||||
|
.ToArray();
|
||||||
|
if (candidates.Length != 1) return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
|
||||||
|
var view = candidates[0].view;
|
||||||
|
if (!OrthoModelFilesContract.IsSupported(view, out var diagnostic))
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipContractRejected", "WARNING", resultStatus: diagnostic.Code, details: diagnostic.Details);
|
||||||
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedRows = new HashSet<int>();
|
||||||
|
var geometry = new Dictionary<int, GeometrySnapshot>();
|
||||||
|
var selected = view.DwgQuery.Execute(new StringCollection(), new StringCollection());
|
||||||
|
for (var index = 0; index < selected.Count; index++)
|
||||||
|
{
|
||||||
|
Autodesk.ProcessPower.Drawings2d.MdObjectId sourceId = default;
|
||||||
|
if (!selected.GetItem(index, ref sourceId) || sourceId.IsNull || sourceId.PrjObjectId.IsNull) continue;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rowId = dataLinks.FindAcPpRowId(sourceId.PrjObjectId);
|
||||||
|
if (sourceRows.Contains(rowId))
|
||||||
|
{
|
||||||
|
selectedRows.Add(rowId);
|
||||||
|
if (TryReadPipePlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DLException)
|
||||||
|
{
|
||||||
|
// A generated entity without a Piping DataLinks row is outside this reader's source set.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "INFO", resultStatus: "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", details: new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["SelectedSourceRows"] = selectedRows.Count,
|
||||||
|
["SourceRows"] = sourceRows.Count
|
||||||
|
});
|
||||||
|
return new MembershipReadResult(selectedRows, ViewMembershipStatus.Resolved, "VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", geometry);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("ViewportMembershipReadCompleted", "WARNING", resultStatus: "VIEW_MEMBERSHIP_API_UNSUPPORTED", exception: exception);
|
||||||
|
return Unsupported("VIEW_MEMBERSHIP_API_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MembershipReadResult Unsupported(string code) => new(new HashSet<int>(), ViewMembershipStatus.NotResolved, code);
|
||||||
|
|
||||||
|
// Confirmed Plant 3D 2026 linkage: DwgQuery.Execute returns MdObjectId,
|
||||||
|
// whose PrjObjectId identifies the source entity and whose Xform projects
|
||||||
|
// that source entity into the particular Ortho view. This is not a sheet
|
||||||
|
// coordinate heuristic.
|
||||||
|
private static bool TryReadPipePlacement(MdObjectId sourceId, DataLinksManager dataLinks, out GeometrySnapshot geometry)
|
||||||
|
{
|
||||||
|
geometry = new GeometrySnapshot();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var openedByQuery = false;
|
||||||
|
var sourceDatabase = sourceId.GetDatabase(false, ref openedByQuery);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
||||||
|
if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false;
|
||||||
|
var point = pipe.StartPoint.TransformBy(sourceId.Xform);
|
||||||
|
geometry = new GeometrySnapshot
|
||||||
|
{
|
||||||
|
PaperPosition = new Point2Dto(point.X, point.Y),
|
||||||
|
Rotation = 0
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (openedByQuery) sourceDatabase.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ortho viewport IDs are generated by PlantOrthoViewportReader from exactly this pair.
|
||||||
|
// Only an exact regenerated ID is accepted; the reader never chooses an arbitrary view.
|
||||||
|
private static string StableId(string sheetId, string? viewName)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sheetId + "|" + (viewName ?? string.Empty)));
|
||||||
|
return "vp-" + Convert.ToHexString(hash)[..20];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
internal sealed class RuntimeWriteGuard(PRecAnnotExecutionMode mode) : IRuntimeWriteGuard
|
||||||
{
|
{
|
||||||
public void EnsureWriteAllowed(string operation)
|
public void EnsureWriteAllowed(string operation)
|
||||||
{
|
{
|
||||||
if (mode == PRecAnnotExecutionMode.Apply) throw new InvalidOperationException("APPLY_MODE_NOT_IMPLEMENTED");
|
if (mode == PRecAnnotExecutionMode.Apply) return;
|
||||||
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
throw new InvalidOperationException("SMOKE_WRITE_BLOCKED");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ internal static class RuntimeDiagnostics
|
|||||||
|
|
||||||
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
public static string RuntimeMode => Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE") ?? "Normal";
|
||||||
|
|
||||||
|
public static bool IsOrthoStructureDiagnosticsEnabled =>
|
||||||
|
string.Equals(RuntimeMode, "DryRun", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_DIAGNOSTICS"), "OrthoStructure", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public static void Write(
|
public static void Write(
|
||||||
string eventType,
|
string eventType,
|
||||||
string level = "INFO",
|
string level = "INFO",
|
||||||
|
|||||||
@@ -104,6 +104,62 @@ public sealed class FoundationTests
|
|||||||
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
Assert.Equal(first.Operations.Select(x => x.SourceSnapshotId), second.Operations.Select(x => x.SourceSnapshotId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ReversedObjectOrder_HasSamePlanId()
|
||||||
|
{
|
||||||
|
var first = Build(Snapshot("a", "Pipe", "L-1"), Snapshot("b", "Pipe", "L-2"));
|
||||||
|
var second = Build(Snapshot("b", "Pipe", "L-2"), Snapshot("a", "Pipe", "L-1"));
|
||||||
|
Assert.Equal(first.PlanId, second.PlanId);
|
||||||
|
Assert.Equal(first.Summary, second.Summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_AbsolutePath_DoesNotAffectPlanId()
|
||||||
|
{
|
||||||
|
var builder = new AnnotationPlanBuilder();
|
||||||
|
var item = Snapshot("a", "Pipe", "L-1");
|
||||||
|
var first = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "v", DisplayName = "v", SourceDrawingName = "C:\\one\\model.dwg" }, Objects = new[] { item } });
|
||||||
|
var second = builder.Build(new AnnotationPlanningRequest { CommandName = "PHP_REC_ANNOT", Viewport = new OrthoViewportInfo { Id = "v", DisplayName = "v", SourceDrawingName = "D:\\two\\model.dwg" }, Objects = new[] { item } });
|
||||||
|
Assert.Equal(first.PlanId, second.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ChangedRelevantProperty_ChangesPlanId()
|
||||||
|
{
|
||||||
|
var first = Build(Snapshot("a", "Pipe", "L-1"));
|
||||||
|
var second = Build(Snapshot("a", "Pipe", "L-2"));
|
||||||
|
Assert.NotEqual(first.PlanId, second.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_UnresolvedMembership_IsStableSkip()
|
||||||
|
{
|
||||||
|
var plan = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.NotResolved });
|
||||||
|
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||||
|
Assert.Contains("VIEW_MEMBERSHIP_NOT_RESOLVED", plan.SkipReasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ViewportMembership_OutsideClip_IsSkippedWithStableReason()
|
||||||
|
{
|
||||||
|
var plan = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
Assert.Equal(AnnotationOperationKind.Skip, Assert.Single(plan.Operations).Kind);
|
||||||
|
Assert.Contains("VIEW_MEMBERSHIP_OBJECT_OUTSIDE_CLIP", plan.SkipReasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ResolvedMembership_AffectsPlanId()
|
||||||
|
{
|
||||||
|
var resolved = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||||
|
var outside = Build(new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
Assert.NotEqual(resolved.PlanId, outside.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void AnnotationPlan_ReorderedMembership_HasSamePlanId()
|
||||||
|
{
|
||||||
|
var first = Build(
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved },
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView });
|
||||||
|
var second = Build(
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "b", ObjectClass = "Pipe", LineNumber = "L-2", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.ObjectOutsideView },
|
||||||
|
new PlantObjectSnapshot { SnapshotId = "a", ObjectClass = "Pipe", LineNumber = "L-1", Geometry = new GeometrySnapshot(), MembershipStatus = ViewMembershipStatus.Resolved });
|
||||||
|
Assert.Equal(first.PlanId, second.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
[Fact] public void AnnotationPlan_MissingLineNumber_IsSkipped()
|
||||||
{
|
{
|
||||||
var plan = Build(Snapshot("a", "Pipe", null, null));
|
var plan = Build(Snapshot("a", "Pipe", null, null));
|
||||||
@@ -130,8 +186,165 @@ 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 DwgQueryContract_UsesConfirmedModelFileContract()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("view.DwgQuery?.ModelFiles", source);
|
||||||
|
Assert.Contains("file.ModelFileName", source);
|
||||||
|
Assert.Contains("ORTHO_MODEL_FILES_API_UNSUPPORTED", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void DwgQueryContract_DoesNotGuessAlternativePropertyNames()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("GetProperty(\"ModelFileName\")", source);
|
||||||
|
Assert.DoesNotContain("GetProperty(\"Value\")", source);
|
||||||
|
Assert.DoesNotContain("GetProperty(\"FileName\")", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_UsesResolvedPipingProjectDrawingId()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("pipingProject.GetPnPDrawingFiles()", source);
|
||||||
|
Assert.Contains("dataLinks.SelectAcPpRowIds(sourceDrawing.Id)", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_DoesNotQueryTheActiveOrthoDatabase()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("SelectAcPpRowIds(sourceDatabase)", source);
|
||||||
|
Assert.DoesNotContain("ReadDwgFile(viewport.SourceDrawingName", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void OrthoSourceReader_ReportsDistinctSourceDiagnostics()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("ORTHO_SOURCE_DRAWING_NOT_IN_PROJECT", source);
|
||||||
|
Assert.Contains("SOURCE_DATALINKS_UNAVAILABLE", source);
|
||||||
|
Assert.Contains("SOURCE_DATALINKS_NO_ROWS", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ViewportMembership_UsesConfirmedPnpIdentityContract()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.Contains("view.DwgQuery.Execute", source);
|
||||||
|
Assert.Contains("sourceId.PrjObjectId", source);
|
||||||
|
Assert.Contains("dataLinks.FindAcPpRowId", source);
|
||||||
|
Assert.Contains("VIEW_MEMBERSHIP_FROM_CONFIRMED_PNP_ID", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void ViewportMembership_DoesNotSelectFirstViewport()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
|
Assert.DoesNotContain("DwgViewDefs.AsArray().First", source);
|
||||||
|
Assert.Contains("candidates.Length != 1", source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void RuntimeInspector_IsDisabledByDefault()
|
||||||
|
{
|
||||||
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "RuntimeDiagnostics.cs"));
|
||||||
|
Assert.Contains("P_REC_ANNOT_DIAGNOSTICS", source);
|
||||||
|
Assert.Contains("OrthoStructure", source);
|
||||||
|
}
|
||||||
|
|
||||||
[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 FirstLimitedApply_RequiresApplyMode()
|
||||||
|
{
|
||||||
|
var context = FirstApplyContext();
|
||||||
|
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.DryRun, context.Plan, context.Viewport, context.Objects, context.Permit));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApply_RequiresFreshPlan()
|
||||||
|
{
|
||||||
|
var context = FirstApplyContext();
|
||||||
|
var permit = context.Permit with { ExpectedPlanId = "stale" };
|
||||||
|
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, permit));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApply_RequiresResolvedMembership()
|
||||||
|
{
|
||||||
|
var context = FirstApplyContext();
|
||||||
|
var objects = new[] { context.Objects[0] with { MembershipStatus = ViewMembershipStatus.NotResolved } };
|
||||||
|
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, objects, context.Permit));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApply_RequiresExactlyOneTarget()
|
||||||
|
{
|
||||||
|
var context = FirstApplyContext();
|
||||||
|
var permit = context.Permit with { TargetSnapshotId = "missing" };
|
||||||
|
Assert.Throws<InvalidOperationException>(() => new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, permit));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApply_CreatesDeterministicOperation()
|
||||||
|
{
|
||||||
|
var context = FirstApplyContext();
|
||||||
|
var planner = new FirstLimitedApplyPlanner();
|
||||||
|
var first = planner.Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, context.Permit);
|
||||||
|
var second = planner.Prepare(PRecAnnotExecutionMode.Apply, context.Plan, context.Viewport, context.Objects, context.Permit);
|
||||||
|
Assert.Equal(first.Operation.OperationId, second.Operation.OperationId);
|
||||||
|
Assert.Equal(first.Operation.DuplicateKey, second.Operation.DuplicateKey);
|
||||||
|
Assert.DoesNotContain("C:\\", first.Operation.DuplicateKey, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApplyPlan_OneResolvedPipeCreatesOnePipeOperation()
|
||||||
|
{
|
||||||
|
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||||
|
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
CommandName = "PXP-OA", Viewport = viewport,
|
||||||
|
Objects = new[] { Snapshot("pipe", "Pipe", null), Snapshot("other", "Valve", "TEST") },
|
||||||
|
LimitedApplyTargetSnapshotId = "pipe", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId
|
||||||
|
});
|
||||||
|
Assert.Equal(1, plan.Summary.CreateCount);
|
||||||
|
Assert.Equal("PIPE", Assert.Single(plan.Operations.Where(x => x.Kind == AnnotationOperationKind.Create)).AnnotationType);
|
||||||
|
Assert.Contains("APPLY_NOT_SELECTED_TARGET", plan.Operations.Single(x => x.SourceSnapshotId == "other").Reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void FirstLimitedApplyPlan_SelectionChangesCanonicalPlanId()
|
||||||
|
{
|
||||||
|
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||||
|
var objects = new[] { Snapshot("one", "Pipe", null), Snapshot("two", "Pipe", null) };
|
||||||
|
var builder = new AnnotationPlanBuilder();
|
||||||
|
var one = builder.Build(new AnnotationPlanningRequest { CommandName = "PXP-OA", Viewport = viewport, Objects = objects, LimitedApplyTargetSnapshotId = "one", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId });
|
||||||
|
var two = builder.Build(new AnnotationPlanningRequest { CommandName = "PXP-OA", Viewport = viewport, Objects = objects, LimitedApplyTargetSnapshotId = "two", LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId });
|
||||||
|
Assert.NotEqual(one.PlanId, two.PlanId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
[Fact] public void DryRunPath_HasNoForWriteOperations()
|
||||||
{
|
{
|
||||||
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
var source = File.ReadAllText(Path.Combine(ProjectRoot, "src", "PRecAnnot.Plant3D2026", "PlantReadPath.cs"));
|
||||||
@@ -141,6 +354,21 @@ public sealed class FoundationTests
|
|||||||
Assert.DoesNotContain("SetProperties", source);
|
Assert.DoesNotContain("SetProperties", source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (AnnotationPlan Plan, OrthoViewportInfo Viewport, PlantObjectSnapshot[] Objects, FirstLimitedApplyPermit Permit) FirstApplyContext()
|
||||||
|
{
|
||||||
|
var viewport = new OrthoViewportInfo { Id = "vp", DisplayName = "view", SourceDrawingName = "model.dwg" };
|
||||||
|
var objects = new[] { Snapshot("pipe", "Pipe", "TEST") };
|
||||||
|
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
CommandName = "PXP-OA",
|
||||||
|
Viewport = viewport,
|
||||||
|
Objects = objects,
|
||||||
|
LimitedApplyTargetSnapshotId = "pipe",
|
||||||
|
LimitedApplyRuleId = FirstLimitedApplyPlanner.RuleId
|
||||||
|
});
|
||||||
|
return (plan, viewport, objects, new FirstLimitedApplyPermit { FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker, AllowApply = true, TargetSnapshotId = "pipe", ExpectedPlanId = plan.PlanId });
|
||||||
|
}
|
||||||
|
|
||||||
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 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) } };
|
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) }, MembershipStatus = ViewMembershipStatus.Resolved };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Launcher source architecture audit
|
|
||||||
|
|
||||||
Static, read-only audit of three independent nested Git repositories under `C:\dev\launcher\git` and the linked bootstrap repository `C:\dev\p_serv_starter`. No launcher executable, helper, build, installer, registry action, profile change, or AutoCAD process was run.
|
|
||||||
|
|
||||||
The three projects are all independent WinForms `WinExe` applications, not a three-project solution: `All_Serv_Install`, `All_Serv_InstallUpdater`, and `All_Serv_Load`. Each solution contains exactly one project and has Debug/Release AnyCPU `Build.0` enabled. There are no project references between them.
|
|
||||||
|
|
||||||
The current primary installer is `All_Serv_Install.exe` (v1.1.0.0). It presents a Form1 UI, reads server-side settings, copies selected corporate application directories to local targets, then starts a configured external executable. `All_Serv_InstallUpdater.exe` is a visible but short-lived self-updater: it updates the local installer with MD5 checks and starts `All_Serv_Install.exe`. `All_Serv_Load.exe` is an earlier separate WinForms loader (v1.0.0.0); source evidence does not establish that it is part of the current primary workflow.
|
|
||||||
|
|
||||||
No static source evidence shows a direct reference to `app_netload.lsp`, `OrthoLoader.dll`, or `P_Rec_Annot` in the updater. The installer configuration does establish `P_Rec_Annot` deployment: catalog `P_Rec_Annot`, install file `OrthoLoader.dll`, source DriveA, target `C:\AutoCAD User Support\Applications`.
|
|
||||||
|
|
||||||
`P_Serv_Starter.exe` is the confirmed external bootstrap selected by the Plant 3D installer pattern. Its source creates `_app_netload.lsp` from the installer `Settings.xml`, copies `launcher_starter.lsp` to the local Applications directory, and launches the configured local `start_plant_3d.bat`. The LISP renames the generated file to `app_netload.lsp`, adds the local Applications directory to SupportPath, and loads it. The source comment attributes LISP auto-start to `acaddoc.lsp`, but that file and the expected local installation were absent on this workstation during the audit; automatic startup remains `UNKNOWN`.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Application synchronization analysis
|
|
||||||
|
|
||||||
`All_Serv_Install` is a synchronizing installer, not merely a launcher. It reads server `Settings.xml`, selects products/plugins, and recursively copies application directories from configured network roots to local targets. Its copy path includes directory deletion and file deletion (`Form1.cs:396-645`), so it was not executed for this audit.
|
|
||||||
|
|
||||||
For Plant 3D_2019, the configuration specifies DriveA and DriveQ network sources and local target `C:\AutoCAD User Support\Applications`. The catalog `P_Rec_Annot` is selected with install entry `OrthoLoader.dll`; `download_list_plant.xml` maps the P_Rec_Annot plugin ID to a real area. This proves the intended local deployment source and target, but not the state of a particular workstation installation.
|
|
||||||
|
|
||||||
`All_Serv_InstallUpdater` is a separate local updater. Its `Form1_Load` computes MD5 checksums, compares a server control-sums manifest, deletes stale local files, copies changed files, writes a local control-sums file and starts `All_Serv_Install.exe`. It references `N:\BIM\save\All_Serv_Install` and `N:\BIM\data_write\All_Serv_Install`; it can write both local and server test/control files. It was not executed.
|
|
||||||
|
|
||||||
| Component | Source | Target | Version check | Destructive operations | Role | Confidence |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| All_Serv_Install | configured network DriveA/DriveQ | configured local target | configuration-driven selection; no content hash established | recursive copy, delete files/directories | application synchronizer | CONFIRMED |
|
|
||||||
| All_Serv_InstallUpdater | server installer directories | updater own local directory | MD5 control sums | deletes stale local files; writes control sums | installer updater | CONFIRMED |
|
|
||||||
| P_Serv_Starter | local installed executable + installer Settings.xml | local Applications | none | overwrites `_app_netload.lsp` and `launcher_starter.lsp` | per-launch bootstrap | CONFIRMED |
|
|
||||||
|
|
||||||
Offline behavior is `UNKNOWN`: both installer and updater require configured paths at the point they run, but source evidence does not show an explicit offline fallback.
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Architecture summary
|
|
||||||
|
|
||||||
## Project architecture
|
|
||||||
|
|
||||||
```text
|
|
||||||
All_Serv_InstallUpdater.exe (independent repository / updater)
|
|
||||||
-> starts local All_Serv_Install.exe
|
|
||||||
|
|
||||||
All_Serv_Install.exe (independent repository / visible installer and synchronizer)
|
|
||||||
-> starts configured P_Serv_Starter.exe
|
|
||||||
|
|
||||||
P_Serv_Starter.exe (separate repository / Plant bootstrap)
|
|
||||||
-> generates local _app_netload.lsp
|
|
||||||
-> copies launcher_starter.lsp
|
|
||||||
-> starts configured local start_plant_3d.bat
|
|
||||||
|
|
||||||
All_Serv_Load.exe (independent repository)
|
|
||||||
-> separate historical/alternate loader; no source dependency edge to the above
|
|
||||||
```
|
|
||||||
|
|
||||||
There are no ProjectReference edges between the three projects beneath `C:\dev\launcher\git`; each is its own one-project solution. `P_Serv_Starter` is a fourth independent repository selected by configuration, not a project reference. This explains why a user can experience one product despite multiple executables: the updater can hand off to the visible installer, which synchronizes selected applications and delegates Plant startup to a hidden bootstrap. Actual deployed shortcut/entrypoint evidence was not present, so the updater's use in a specific installation remains `INFERRED`.
|
|
||||||
|
|
||||||
## Roles and status
|
|
||||||
|
|
||||||
| Project | Output | Role | Status | Evidence |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| All_Serv_Install | WinExe | visible catalog UI, synchronizer and configured process launcher | ACTIVE_PRIMARY | Form1 startup and copy/Process.Start code |
|
|
||||||
| All_Serv_InstallUpdater | WinExe | installer self-updater then handoff | ACTIVE_UPDATER | Form1_Load MD5/copy/start code |
|
|
||||||
| All_Serv_Load | WinExe | independent alternate/historical loader | UNKNOWN | independent Form1 and 2019 batch script; no current deployment proof |
|
|
||||||
| P_Serv_Starter | Exe | Plant session bootstrap | ACTIVE_BOOTSTRAP | selected in installer XML; writes loader and starts batch |
|
|
||||||
|
|
||||||
## Runtime chain
|
|
||||||
|
|
||||||
The confirmed chain ends at execution of the local Plant start batch. The gap between that batch and `launcher_starter.lsp` automatic loading is `UNKNOWN`: no local batch, `acaddoc.lsp`, or Applications files were found. Once `launcher_starter.lsp` runs, all following load steps are source-confirmed.
|
|
||||||
|
|
||||||
## Answers to the requested questions
|
|
||||||
|
|
||||||
1. The three launcher projects are independent WinForms executables: installer, updater and alternate loader.
|
|
||||||
2. `All_Serv_Install` creates the confirmed visible catalog UI.
|
|
||||||
3. Three EXE artifacts are declared by the launcher repositories; P_Serv_Starter adds one external bootstrap EXE.
|
|
||||||
4. No launcher project is a class library; `launcher_starter.lsp` is a copied runtime content artifact.
|
|
||||||
5. Updater/helper/bootstrap behavior is confirmed in source; actual installed-process count is unknown.
|
|
||||||
6. Plant launch is delegated by `All_Serv_Install` to configured `P_Serv_Starter.exe`; the bootstrap starts a local batch. A current Plant executable/profile/argument string is not proven.
|
|
||||||
7. `app_netload.lsp` and `OrthoLoader.dll` are connected through P_Serv_Starter generated LISP and installer plugin metadata.
|
|
||||||
8. The installer synchronizes corporate application folders to a local Applications cache; it does not load P_Rec_Annot directly.
|
|
||||||
9. No source proof warrants modifying LAUNCHER for the new plugin yet. First validate one local entry assembly through the established catalog/bootstrap contract in a non-production pilot.
|
|
||||||
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# AutoCAD/Plant launch chain
|
|
||||||
|
|
||||||
## Confirmed static chain
|
|
||||||
|
|
||||||
```text
|
|
||||||
User
|
|
||||||
-> All_Serv_Install.exe Form1
|
|
||||||
-> copy selected Plant 3D application folders to local Applications target
|
|
||||||
-> Process.Start(P_Serv_Starter.exe, five configured arguments)
|
|
||||||
-> P_Serv_Starter creates local _app_netload.lsp and copies launcher_starter.lsp
|
|
||||||
-> P_Serv_Starter starts C:\AutoCAD User Support\p3d-support\start_plant_3d.bat
|
|
||||||
-> [UNKNOWN: installed batch contents and auto-load registration]
|
|
||||||
-> Plant 3D startup
|
|
||||||
-> launcher_starter.lsp (when loaded)
|
|
||||||
-> local app_netload.lsp
|
|
||||||
-> _netload local Applications\P_Rec_Annot\OrthoLoader.dll
|
|
||||||
-> OrthoLoader Settings.xml -> sequential ._netload configured modules
|
|
||||||
```
|
|
||||||
|
|
||||||
The final two steps are confirmed by the prior P_Rec_Annot legacy audit. The activation of `launcher_starter.lsp` is not directly implemented in `P_Serv_Starter`: `Program.cs:181` comments that `acaddoc.lsp` renames the generated file, while the copied LISP itself performs the rename and `load`. No matching installed `acaddoc.lsp`, batch file, or Applications directory existed on this workstation at audit time. Therefore automatic execution is `UNKNOWN`, not asserted.
|
|
||||||
|
|
||||||
## Source evidence
|
|
||||||
|
|
||||||
| Claim | Evidence | Confidence |
|
|
||||||
|---|---|---|
|
|
||||||
| User-visible installer starts configured object | `All_Serv_Install/Form1.cs:195-230` | CONFIRMED |
|
|
||||||
| Plant pattern object is `P_Serv_Starter.exe` | `All_Serv_Install/Settings.xml`, Plant 3D pattern | CONFIRMED |
|
|
||||||
| Five arguments include product, Applications target, settings, project and working directory | `Settings.xml` Plant pattern and `P_Serv_Starter/Program.cs:12-23` | CONFIRMED |
|
|
||||||
| Bootstrap generates `_app_netload.lsp` | `P_Serv_Starter/Program.cs:58-182` | CONFIRMED |
|
|
||||||
| Generated DLL modules use AutoLISP `_netload` | `P_Serv_Starter/Program.cs:146-151` | CONFIRMED |
|
|
||||||
| Bootstrap copies LISP and starts local batch file | `P_Serv_Starter/Program.cs:202-216,49-54` | CONFIRMED |
|
|
||||||
| Generated LISP is renamed, SupportPath updated and loaded | `P_Serv_Starter/launcher_starter.lsp:74-96` | CONFIRMED |
|
|
||||||
| Actual installed batch launches a specific Plant version | no installed batch was present | UNKNOWN |
|
|
||||||
|
|
||||||
## Historical source-only alternative
|
|
||||||
|
|
||||||
`All_Serv_Load/Addins/start_plant_3d.bat` contains a direct AutoCAD 2019 command using `/product PLNT3D /language "ru-RU"`, preceded by PlantCleaner and PnPPersistentCache cleanup. It is evidence only of that project's historical script; it is not evidence that current users start Plant 3D 2026 this way. No `/ld`, profile, LISP or script argument is present in that script.
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Artifact,SourceProject,Type,AssemblyName,AssemblyVersion,TargetFramework,Architecture,ReferencedBy,VisibleToUser,RequiredAtRuntime,LikelyRole,Confidence
|
|
||||||
All_Serv_Install.exe,All_Serv_Install,WinExe,All_Serv_Install,1.1.0.0,.NETFramework v4.7,AnyCPU,None,Yes,Yes,PRIMARY_INSTALLER,CONFIRMED
|
|
||||||
All_Serv_InstallUpdater.exe,All_Serv_InstallUpdater,WinExe,All_Serv_InstallUpdater,3.0.0.0,.NETFramework v4.7,AnyCPU,None,Briefly,Only when updater is launched,UPDATER_HELPER,CONFIRMED
|
|
||||||
All_Serv_Load.exe,All_Serv_Load,WinExe,All_Serv_Load,1.0.0.0,.NETFramework v4.8,AnyCPU,None,Possible,Unknown,SEPARATE_LEGACY_LOADER,INFERRED
|
|
||||||
P_Serv_Starter.exe,P_Serv_Starter,Exe,P_Serv_Starter,3.0.1.0,.NETFramework v4.7,AnyCPU,Configured external process from All_Serv_Install,No,Yes for Plant launch bootstrap,BOOTSTRAP,CONFIRMED
|
|
||||||
launcher_starter.lsp,P_Serv_Starter,Content,LISP script,N/A,N/A,N/A,Copied by P_Serv_Starter,No,Yes for generated loader activation,LISP_BOOTSTRAP,CONFIRMED
|
|
||||||
|
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
Solution,Configuration,Platform,Project,BuildEnabled,Notes
|
|
||||||
All_Serv_Install.sln,Debug,Any CPU,All_Serv_Install,yes,Only project in solution
|
|
||||||
All_Serv_Install.sln,Release,Any CPU,All_Serv_Install,yes,Only project in solution
|
|
||||||
All_Serv_InstallUpdater.sln,Debug,Any CPU,All_Serv_InstallUpdater,yes,Only project in solution
|
|
||||||
All_Serv_InstallUpdater.sln,Release,Any CPU,All_Serv_InstallUpdater,yes,Only project in solution
|
|
||||||
All_Serv_Load.sln,Debug,Any CPU,All_Serv_Load,yes,Only project in solution
|
|
||||||
All_Serv_Load.sln,Release,Any CPU,All_Serv_Load,yes,Only project in solution
|
|
||||||
|
@@ -1,10 +0,0 @@
|
|||||||
Setting,OwnerProject,Source,DefaultValue,RuntimeValueSource,Purpose,Sensitive,Required,Fallback,UsedBy,Confidence
|
|
||||||
InstallerSettings,All_Serv_Install,N:\BIM\save\All_Serv_Install\Settings.xml,None,hard-coded source path,product/plugin catalog and process pattern,No,Yes,None,Form1,CONFIRMED
|
|
||||||
PlantApplicationTarget,All_Serv_Install,Settings.xml Plant pattern,C:\AutoCAD User Support\Applications,XML target,local corporate applications directory,No,Yes,None,copy and P_Serv_Starter arguments,CONFIRMED
|
|
||||||
PlantBootstrapExecutable,All_Serv_Install,Settings.xml Plant pattern,C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe,XML object,external bootstrap process,No,Yes,None,Form1 Process.Start,CONFIRMED
|
|
||||||
PluginCatalog,All_Serv_Install,Settings.xml plugin entry,P_Rec_Annot,XML plugin metadata,subdirectory to deploy,No,Optional by selection,skip if unselected,copy and generated loader,CONFIRMED
|
|
||||||
PluginInstall,All_Serv_Install,Settings.xml plugin entry,OrthoLoader.dll,XML plugin metadata,first P_Rec_Annot assembly to load,No,Optional by selection,skip if catalog unavailable,P_Serv_Starter generated LISP,CONFIRMED
|
|
||||||
GeneratedLoader,P_Serv_Starter,arguments plus installer Settings.xml,_app_netload.lsp,computed at runtime,per-session module loading commands,Contains project path in first comment,Yes,abort on error,generated LISP,CONFIRMED
|
|
||||||
PlantStartScript,P_Serv_Starter,Program.cs,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,hard-coded source constant,launch Plant process,No,Yes,none,Process.Start,CONFIRMED
|
|
||||||
UpdaterServer,All_Serv_InstallUpdater,Form1.cs,N:\BIM\data_write\All_Serv_Install,hard-coded source constant,hash/copy source,No,Yes,none,Form1_Load,CONFIRMED
|
|
||||||
|
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
Artifact,ExpectedInstalledLocation,SourceProject,SourceAssemblyVersion,InstalledBinaryFound,InstalledMetadataMatch,MatchStatus,Notes
|
|
||||||
All_Serv_Install.exe,C:\CAD User Support\All_Serv_Install\All_Serv_Install.exe,All_Serv_Install,1.1.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,Expected location came from Settings.xml object directory
|
|
||||||
P_Serv_Starter.exe,C:\CAD User Support\All_Serv_Install\P_Serv_Starter.exe,P_Serv_Starter,3.0.1.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,Expected location came from Settings.xml object
|
|
||||||
All_Serv_InstallUpdater.exe,UNKNOWN,All_Serv_InstallUpdater,3.0.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,No installed location in inspected source
|
|
||||||
All_Serv_Load.exe,UNKNOWN,All_Serv_Load,1.0.0.0,False,N/A,NO_DEPLOYED_BINARY_FOUND,No installed location in inspected source
|
|
||||||
start_plant_3d.bat,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,P_Serv_Starter dependency,N/A,False,N/A,NO_DEPLOYED_FILE_FOUND,Not present at expected path during audit
|
|
||||||
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
EntryPoint,Project,File,Lines,ApplicationType,CommandLineArguments,CreatesVisibleWindow,CanRunHeadless,StartsOtherProcesses,SingleInstanceMechanism,ExitBehavior,Confidence
|
|
||||||
Program.Main,All_Serv_Install,All_Serv_Install/Program.cs,13-18,WinForms,none parsed,yes Form1,no,yes configured object,none,Application exits after launching configured object,CONFIRMED
|
|
||||||
Program.Main,All_Serv_InstallUpdater,All_Serv_InstallUpdater/Program.cs,13-18,WinForms,none parsed,yes Form1,no,yes All_Serv_Install.exe,none,exits after updater starts installer,CONFIRMED
|
|
||||||
Program.Main,All_Serv_Load,All_Serv_Load/All_Serv_Load/Program.cs,12-17,WinForms,none parsed,yes Form1,no,yes configured object,none,Application exits after launching configured object,CONFIRMED
|
|
||||||
|
@@ -1,11 +0,0 @@
|
|||||||
Generated: 2026-07-31
|
|
||||||
Method: static read-only source and metadata inspection; no build or executable launch.
|
|
||||||
|
|
||||||
Launcher sources:
|
|
||||||
- C:\dev\launcher\git\All_Serv_Install @ 29d163322a2dc01983e15bc26f62f787fb402a04 (main; clean)
|
|
||||||
- C:\dev\launcher\git\All_Serv_InstallUpdater @ 1e6f2e78a1ca0005ee704dbeb464e73a478c3390 (main; clean)
|
|
||||||
- C:\dev\launcher\git\All_Serv_Load @ bd38b87f903b02533dbb659d9776cd32dd9e2a3e (main; clean)
|
|
||||||
- C:\dev\p_serv_starter @ 8219520aad1a5ccc2cd96611f2a71fef61cbf9b1 (main; clean)
|
|
||||||
|
|
||||||
Local deployment probes were metadata-only. Expected launcher/bootstrap files and the relevant AutoCAD User Support scripts were absent at the configured locations at audit time.
|
|
||||||
No source, binaries, user configuration content, registry export, DWG, DCF, Project.xml or network files are included in this audit.
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# LAUNCHER to P_Rec_Annot
|
|
||||||
|
|
||||||
## Proven loading contract
|
|
||||||
|
|
||||||
| Stage | Evidence | Finding | Confidence |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Deployment selection | `All_Serv_Install/Settings.xml`; `download_lists/download_list_plant.xml` | `P_Rec_Annot` is an installer plugin catalog; its configured install file is `OrthoLoader.dll`. | CONFIRMED |
|
|
||||||
| Local target | Plant configuration target | P_Rec_Annot is intended to be copied under `C:\AutoCAD User Support\Applications\P_Rec_Annot`. | CONFIRMED |
|
|
||||||
| Bootstrap selection | installer Plant pattern object/arguments | `All_Serv_Install` invokes `P_Serv_Starter.exe` after install selection. | CONFIRMED |
|
|
||||||
| Generated first load | `P_Serv_Starter/Program.cs:140-174` | For each installed DLL catalog, generated LISP emits `(command "_netload" attribute)`. Therefore `OrthoLoader.dll` is loaded from the local Applications path when its catalog exists. | CONFIRMED |
|
|
||||||
| Generated LISP activation | `launcher_starter.lsp:74-96` | It renames `_app_netload.lsp`, adds Applications to SupportPath and `(load ...)`s `app_netload.lsp`. | CONFIRMED |
|
|
||||||
| Automatic LISP invocation | source comment in `Program.cs:181`; no installed startup file | `acaddoc.lsp` is named by a comment, but its installed content and registration were not available. | UNKNOWN |
|
|
||||||
| Secondary module load | legacy audit, `OrthoLoader.Ribbon.Initialize` / `OrthoLoader.Initialize` | OrthoLoader reads adjacent Settings.xml and sequentially calls `._netload` for 20 configured modules. | CONFIRMED |
|
|
||||||
|
|
||||||
## P_Rec_Annot-specific implication
|
|
||||||
|
|
||||||
The historic contract is not a direct network `NETLOAD`. It is a local synchronized application directory followed by a generated, per-session AutoLISP loader. A generated LISP includes the Plant project path in its first comment and conditionally emits loads only for existing local plugin catalogs. New code must not reproduce that behavior before deciding whether it is compatible, necessary and safe for Plant 3D 2026.
|
|
||||||
|
|
||||||
The prior legacy audit established that `OrthoLoader.dll` is the initial assembly for P_Rec_Annot. This launcher audit establishes how the installer/bootstrap is intended to make that assembly loadable. Neither audit proves the deployed local files existed or auto-loaded on this workstation.
|
|
||||||
|
|
||||||
## Integration recommendation
|
|
||||||
|
|
||||||
Use one versioned, locally synchronized entry artifact only after a compatibility decision. The minimal pilot should retain the existing installer catalog/target contract, make the bootstrap load exactly one new entry assembly, and use an explicitly reviewed startup mechanism. Do not rely on an unproven AppLoader bundle or add new global `TRUSTEDPATHS`/profile mutations. The generated loader and any project-path comment are legacy behavior, not requirements for the new P_Rec_Annot until runtime validation establishes a need.
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
ParentProject,ParentFile,ParentMethod,Executable,Arguments,WorkingDirectory,UseShellExecute,WaitForExit,Conditions,ErrorHandling,Confidence
|
|
||||||
All_Serv_Install,All_Serv_Install/Form1.cs,button launch handler around 195-230,configured Pattern.Object,Predictable quoted Pattern.Attrib values including project path,not set,default,not set,after successful plugin copy,none visible around Process.Start,CONFIRMED
|
|
||||||
All_Serv_InstallUpdater,All_Serv_InstallUpdater/Form1.cs,Form1_Load 28-48,local All_Serv_Install.exe,none,not set,default,not set,after successful MD5 update,closes on update failure,CONFIRMED
|
|
||||||
All_Serv_Load,All_Serv_Load/Form1.cs,launch handler around 180-205,configured Pattern.Object,configured attributes plus selected project path,not set,default,not set,after successful plugin copy,none visible around Process.Start,CONFIRMED
|
|
||||||
P_Serv_Starter,P_Serv_Starter/Program.cs,Main 15-54,C:\AutoCAD User Support\p3d-support\start_plant_3d.bat,none,changed to configured project directory before start,default,not set,after local loader generation and LISP copy,shows message and returns for loader/copy errors,CONFIRMED
|
|
||||||
start_plant_3d.bat,All_Serv_Load/Addins/start_plant_3d.bat,script,C:\Program Files\Autodesk\AutoCAD 2019\acad.exe,/product PLNT3D /language ru-RU,not set,START default,no,after PlantCleaner,script has no error checks,CONFIRMED
|
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
# Project dependency graph
|
|
||||||
|
|
||||||
```text
|
|
||||||
All_Serv_InstallUpdater.exe
|
|
||||||
-> local All_Serv_Install.exe (Process.Start)
|
|
||||||
|
|
||||||
All_Serv_Install.exe
|
|
||||||
-> configured external object: P_Serv_Starter.exe for Plant 3D 2019
|
|
||||||
-> copies selected application catalogs from configured network sources
|
|
||||||
|
|
||||||
All_Serv_Load.exe
|
|
||||||
-> configured external object (separate legacy workflow)
|
|
||||||
```
|
|
||||||
|
|
||||||
No `ProjectReference` exists in any of the three projects. Each solution has a single buildable project. `P_Serv_Starter.exe` is referenced only as a configured external object, not supplied by any audited launcher project.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# Repositories
|
|
||||||
|
|
||||||
| Repository | HEAD | Branch | Dirty | Remote | Submodules | Confidence |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| All_Serv_Install | `29d163322a2dc01983e15bc26f62f787fb402a04` | main | no | GitHub `pxpgitorg/All_Serv_Install` | none | CONFIRMED |
|
|
||||||
| All_Serv_InstallUpdater | `1e6f2e78a1ca0005ee704dbeb464e73a478c3390` | main | no | GitHub `pxpgitorg/All_Serv_InstallUpdater` | none | CONFIRMED |
|
|
||||||
| All_Serv_Load | `bd38b87f903b02533dbb659d9776cd32dd9e2a3e` | main | no | GitHub `pxpgitorg/All_Serv_Load` | none | CONFIRMED |
|
|
||||||
| P_Serv_Starter | `8219520aad1a5ccc2cd96611f2a71fef61cbf9b1` | main | no | GitHub `pxpgitorg/P_Serv_Starter` | none | CONFIRMED |
|
|
||||||
|
|
||||||
The launcher parent directory is not a Git repository; it contains three nested repositories. `P_Serv_Starter` is a fourth, separately supplied repository outside that parent and is referenced by installer configuration. No launcher or bootstrap source was modified.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Project,ProjectPath,ProjectGuid,ProjectType,Sdk,OutputType,TargetFramework,PlatformTarget,AssemblyName,RootNamespace,StartupObject,EntryPoint,ApplicationIcon,ApplicationManifest,ProjectReferences,PackageReferences,DirectDllReferences,BuildOutput,PublishOutput,IsStartupProject,HasMainMethod,HasAppXaml,HasFormsEntryPoint,HasPostBuildActions,LikelyRole,Status
|
|
||||||
All_Serv_Install,All_Serv_Install/All_Serv_Install.csproj,{FE649634-187B-410E-9A53-A8C41134F53D},C# classic MSBuild,,WinExe,v4.7,AnyCPU,All_Serv_Install,All_Serv_Install,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,primary corporate application installer/synchronizer,ACTIVE_PRIMARY
|
|
||||||
All_Serv_InstallUpdater,All_Serv_InstallUpdater/All_Serv_InstallUpdater.csproj,{6E075C69-20B5-4613-8BE1-5C41A615C70F},C# classic MSBuild,,WinExe,v4.7,AnyCPU,All_Serv_InstallUpdater,All_Serv_InstallUpdater,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,self-updater for All_Serv_Install,ACTIVE_UPDATER
|
|
||||||
All_Serv_Load,All_Serv_Load/All_Serv_Load/All_Serv_Load.csproj,{CCEF88B4-9157-4F50-AA7F-E0A6F3A665E3},C# classic MSBuild,,WinExe,v4.8,AnyCPU,All_Serv_Load,All_Serv_Load,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,yes,no,earlier standalone loader/install UI,UNKNOWN
|
|
||||||
P_Serv_Starter,P_Serv_Starter/P_Serv_Starter.csproj,{DBA449EE-5DEC-454B-9EF1-E71BF949D880},C# classic MSBuild,,Exe,v4.7,AnyCPU,P_Serv_Starter,P_Serv_Starter,,Program.Main,,,none,none,System assemblies,bin/Debug or bin/Release,none,only solution project,yes,no,no,no,external Plant bootstrap selected by installer configuration,ACTIVE_BOOTSTRAP
|
|
||||||
|
@@ -1,84 +0,0 @@
|
|||||||
param(
|
|
||||||
[string]$DeployedRoot = '\\phpsrv03\NBIM\save\Apps\Plant3D\DriveA\P_Rec_Annot',
|
|
||||||
[string]$SourceRoot = '\\phpsrv03\NBIM\code\CS\P_Rec_Annot\v1.0.4\repo',
|
|
||||||
[string]$OutputRoot = (Split-Path -Parent $PSCommandPath)
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
function Get-PeArchitecture([string]$Path) {
|
|
||||||
if ([IO.Path]::GetExtension($Path) -notin '.dll', '.exe') { return $null }
|
|
||||||
try {
|
|
||||||
$stream = [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
|
||||||
try {
|
|
||||||
$reader = [IO.BinaryReader]::new($stream)
|
|
||||||
$stream.Position = 0x3c; $offset = $reader.ReadInt32(); $stream.Position = $offset + 4
|
|
||||||
$machine = $reader.ReadUInt16()
|
|
||||||
switch ($machine) { 0x14c { 'x86' } 0x8664 { 'x64' } 0x1c4 { 'ARM' } default { 'Unknown' } }
|
|
||||||
} finally { $stream.Dispose() }
|
|
||||||
} catch { 'Unreadable' }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-AssemblyMetadata([string]$Path) {
|
|
||||||
$result = [ordered]@{ AssemblyName = $null; AssemblyVersion = $null; TargetFramework = $null }
|
|
||||||
if ([IO.Path]::GetExtension($Path) -ne '.dll') { return [pscustomobject]$result }
|
|
||||||
try {
|
|
||||||
$name = [Reflection.AssemblyName]::GetAssemblyName($Path)
|
|
||||||
$result.AssemblyName = $name.Name; $result.AssemblyVersion = $name.Version.ToString()
|
|
||||||
try {
|
|
||||||
$assembly = [Reflection.Assembly]::ReflectionOnlyLoadFrom($Path)
|
|
||||||
$attribute = $assembly.GetCustomAttributesData() | Where-Object { $_.AttributeType.FullName -eq 'System.Runtime.Versioning.TargetFrameworkAttribute' } | Select-Object -First 1
|
|
||||||
if ($attribute) { $result.TargetFramework = [string]$attribute.ConstructorArguments[0].Value }
|
|
||||||
} catch { }
|
|
||||||
} catch { }
|
|
||||||
[pscustomobject]$result
|
|
||||||
}
|
|
||||||
|
|
||||||
$inventory = foreach ($file in Get-ChildItem -LiteralPath $DeployedRoot -Recurse -File) {
|
|
||||||
$hash = Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256
|
|
||||||
$version = try { [Diagnostics.FileVersionInfo]::GetVersionInfo($file.FullName) } catch { $null }
|
|
||||||
$meta = Get-AssemblyMetadata $file.FullName
|
|
||||||
[pscustomobject]@{
|
|
||||||
RelativePath = $file.FullName.Substring($DeployedRoot.Length).TrimStart('\')
|
|
||||||
Name = $file.Name; Extension = $file.Extension; Length = $file.Length; LastWriteTimeUtc = $file.LastWriteTimeUtc.ToString('o')
|
|
||||||
Sha256 = $hash.Hash; FileVersion = $version.FileVersion; ProductVersion = $version.ProductVersion
|
|
||||||
AssemblyName = $meta.AssemblyName; AssemblyVersion = $meta.AssemblyVersion; TargetFramework = $meta.TargetFramework
|
|
||||||
PeArchitecture = Get-PeArchitecture $file.FullName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$inventory | Sort-Object RelativePath | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-files.csv') -NoTypeInformation -Encoding utf8
|
|
||||||
|
|
||||||
$settings = [xml](Get-Content -LiteralPath (Join-Path $DeployedRoot 'Settings.xml'))
|
|
||||||
$sequence = 0
|
|
||||||
$configuration = foreach ($item in $settings.Main.LoadAssemblies.LoadAssembly) {
|
|
||||||
$sequence++
|
|
||||||
$configured = [string]$item.assemblyname; $resolved = Join-Path $DeployedRoot $configured
|
|
||||||
$entry = $inventory | Where-Object Name -eq $configured | Select-Object -First 1
|
|
||||||
[pscustomobject]@{
|
|
||||||
Sequence = $sequence; ConfigFile = 'Settings.xml'; ConfiguredPath = $configured; ResolvedPath = ('<deployed-root>\' + $configured)
|
|
||||||
Assembly = $entry.AssemblyName; Exists = Test-Path -LiteralPath $resolved; Sha256 = $entry.Sha256
|
|
||||||
LoadMethod = 'acDoc.Editor.Command("._netload", fullPath)'; Required = $true
|
|
||||||
FailureBehavior = 'Outer loader catch stops remaining sequence and shows MessageBox'; Notes = 'Path is combined with OrthoLoader assembly directory.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$configuration | Export-Csv -LiteralPath (Join-Path $OutputRoot 'configuration-map.csv') -NoTypeInformation -Encoding utf8
|
|
||||||
|
|
||||||
$projects = Get-ChildItem -LiteralPath $SourceRoot -Recurse -File -Filter *.csproj | ForEach-Object {
|
|
||||||
$content = Get-Content -LiteralPath $_.FullName -Raw
|
|
||||||
$assemblyName = if ($content -match '<AssemblyName>([^<]+)</AssemblyName>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($_.Name) }
|
|
||||||
$framework = if ($content -match '<TargetFrameworkVersion>([^<]+)</TargetFrameworkVersion>') { $Matches[1] } else { $null }
|
|
||||||
$platform = if ($content -match '<PlatformTarget>([^<]+)</PlatformTarget>') { $Matches[1] } else { $null }
|
|
||||||
[pscustomobject]@{ AssemblyName = $assemblyName; Csproj = $_.FullName.Substring($SourceRoot.Length).TrimStart('\'); TargetFramework = $framework; PlatformTarget = $platform }
|
|
||||||
}
|
|
||||||
$sourceMap = foreach ($dll in $inventory | Where-Object Extension -eq '.dll') {
|
|
||||||
$matches = @($projects | Where-Object { $_.AssemblyName -eq $dll.AssemblyName -or $_.AssemblyName -eq [IO.Path]::GetFileNameWithoutExtension($dll.Name) })
|
|
||||||
$releaseCandidate = Join-Path (Join-Path $SourceRoot 'FilesRelease') $dll.Name
|
|
||||||
$exactReleaseMatch = (Test-Path -LiteralPath $releaseCandidate) -and ((Get-FileHash -LiteralPath $releaseCandidate -Algorithm SHA256).Hash -eq $dll.Sha256)
|
|
||||||
$status = if ($exactReleaseMatch) { 'EXACT_SOURCE_MATCH' } elseif ($matches.Count -eq 1) { 'LIKELY_SOURCE_MATCH' } elseif ($matches.Count -gt 1) { 'MULTIPLE_CANDIDATES' } elseif ($dll.Name -match '^PnP|^Ac') { 'AUTODESK_DEPENDENCY' } else { 'NO_SOURCE_FOUND' }
|
|
||||||
$project = $matches | Select-Object -First 1
|
|
||||||
[pscustomobject]@{ DeployedDll=$dll.Name; DeployedSha256=$dll.Sha256; FileVersion=$dll.FileVersion; AssemblyVersion=$dll.AssemblyVersion; LegacyProject=if($project){[IO.Path]::GetFileNameWithoutExtension($project.Csproj)}else{$null}; LegacyCsproj=if($project){$project.Csproj}else{$null}; LegacyOutputName=if($project){$project.AssemblyName}else{$null}; TargetFramework=if($project){$project.TargetFramework}else{$null}; PlatformTarget=if($project){$project.PlatformTarget}else{$null}; CommandCount=$null; RibbonRole=if($dll.AssemblyName -eq 'OrthoLoader'){'Central ribbon/loader'}else{$null}; ConfigurationSource=if($configuration.ConfiguredPath -contains $dll.Name){'Settings.xml'}else{$null}; MatchStatus=$status }
|
|
||||||
}
|
|
||||||
$sourceMap | Sort-Object DeployedDll | Export-Csv -LiteralPath (Join-Path $OutputRoot 'deployed-to-source-map.csv') -NoTypeInformation -Encoding utf8
|
|
||||||
|
|
||||||
"GeneratedUtc=$([DateTime]::UtcNow.ToString('o'))`nDeployedRoot=<network root redacted to report context>`nSourceRoot=<network source root redacted to report context>`nFiles=$($inventory.Count)" | Set-Content -LiteralPath (Join-Path $OutputRoot 'evidence\generation.txt') -Encoding utf8
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Legacy P_Rec_Annot loading audit
|
|
||||||
|
|
||||||
Scope: read-only inspection of the deployed network directory and the v1.0.4 source checkout. No legacy DLL was executed, copied, or deployed.
|
|
||||||
|
|
||||||
The deployed root has 52 files, including 22 managed DLLs. `Settings.xml` enumerates 20 application modules. The inventory and its SHA-256 values are in `deployed-files.csv`; generated report paths use `<deployed-root>` rather than embedding a new runtime configuration path.
|
|
||||||
|
|
||||||
High-confidence conclusion: the central loader is `OrthoLoader.dll`. `OrthoLoader.Ribbon.Initialize()` invokes `OrthoLoader.Initialize()`. The latter reads adjacent `Settings.xml` and calls `acDoc.Editor.Command("._netload", fullPath)` sequentially for the configured modules. The bootstrap that initially loads `OrthoLoader.dll` is not present in either audited directory; it remains unknown. Therefore the audit proves same-directory loading, but cannot prove whether an external launcher first NETLOADed the UNC copy or a local copy.
|
|
||||||
|
|
||||||
Source-to-binary mapping is `EXACT_SOURCE_MATCH` for 15 deployed DLLs by SHA-256 against `FilesRelease`. Six newer deployed DLLs are `LIKELY_SOURCE_MATCH` by assembly/project identity and metadata but differ byte-for-byte from the checked-in release output. `InsertElevation.dll` is `MULTIPLE_CANDIDATES` because archival projects share the assembly identity.
|
|
||||||
|
|
||||||
## Potential test data
|
|
||||||
|
|
||||||
| RelativePath | Type | LikelyPurpose | ContainsProjectData | SafeForTest | Reason |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `Template.dwg` | DWG | legacy command template | Unknown | No | Located beside production deployment; no authorization to open or copy it was established. |
|
|
||||||
| `test.txt` | text | unknown/empty marker | No content | Not useful | Zero bytes. |
|
|
||||||
| `Справка*.pdf/docx` | documentation | user instructions | Unknown | Review only | Not a CAD fixture. |
|
|
||||||
|
|
||||||
No `Project.xml`, `.dcf`, or explicitly labelled sample/demo Ortho DWG was found in the deployed root.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"Sequence","ConfigFile","ConfiguredPath","ResolvedPath","Assembly","Exists","Sha256","LoadMethod","Required","FailureBehavior","Notes"
|
|
||||||
"1","Settings.xml","AnnotateLineNumbers.dll","<deployed-root>\AnnotateLineNumbers.dll","AnnotateLineNumbers","True","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"2","Settings.xml","AutoAnnotateView.dll","<deployed-root>\AutoAnnotateView.dll","AutoAnnotateView","True","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"3","Settings.xml","AutoLinenumberConnections.dll","<deployed-root>\AutoLinenumberConnections.dll","AutoLinenumberConnections","True","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"4","Settings.xml","CheckOrthoView.dll","<deployed-root>\CheckOrthoView.dll","CheckOrthoView","True","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"5","Settings.xml","EnablerLoader.dll","<deployed-root>\EnablerLoader.dll","EnablerLoader","True","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"6","Settings.xml","ExtentsIntersect.dll","<deployed-root>\ExtentsIntersect.dll","ExtentsIntersect","True","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"7","Settings.xml","GetComponentGeometry.dll","<deployed-root>\GetComponentGeometry.dll","GetComponentGeometry","True","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"8","Settings.xml","GetLinenumberAnnotationText.dll","<deployed-root>\GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText","True","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"9","Settings.xml","GetPlantScriptFolderPath.dll","<deployed-root>\GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath","True","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"10","Settings.xml","GetScriptParameters.dll","<deployed-root>\GetScriptParameters.dll","GetScriptParameters","True","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"11","Settings.xml","InsertElevation.dll","<deployed-root>\InsertElevation.dll","InsertElevation","True","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"12","Settings.xml","LoadedAssemblies.dll","<deployed-root>\LoadedAssemblies.dll","LoadedAssemblies","True","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"13","Settings.xml","OrthoAutoAnnotate.dll","<deployed-root>\OrthoAutoAnnotate.dll","OrthoAutoAnnotate","True","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"14","Settings.xml","P3DCopyMissingObjectsToOrthoDrawing.dll","<deployed-root>\P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing","True","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"15","Settings.xml","P3DNewMa.dll","<deployed-root>\P3DNewMa.dll","P3DNewMa","True","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"16","Settings.xml","P3DSetLayerForViewport.dll","<deployed-root>\P3DSetLayerForViewport.dll","P3DSetLayerForViewport","True","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"17","Settings.xml","PHPAnnotations.dll","<deployed-root>\PHPAnnotations.dll","PHPAnnotations","True","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"18","Settings.xml","SwapAttributesMenuItem.dll","<deployed-root>\SwapAttributesMenuItem.dll","SwapAttributesMenuItem","True","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"19","Settings.xml","UpdateOrthoDrawings.dll","<deployed-root>\UpdateOrthoDrawings.dll","UpdateOrthoDrawings","True","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
"20","Settings.xml","UpdateOrthoViewBlocks.dll","<deployed-root>\UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks","True","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","acDoc.Editor.Command(""._netload"", fullPath)","True","Outer loader catch stops remaining sequence and shows MessageBox","Path is combined with OrthoLoader assembly directory."
|
|
||||||
|
@@ -1,53 +0,0 @@
|
|||||||
"RelativePath","Name","Extension","Length","LastWriteTimeUtc","Sha256","FileVersion","ProductVersion","AssemblyName","AssemblyVersion","TargetFramework","PeArchitecture"
|
|
||||||
"AnnotateLineNumbers.dll","AnnotateLineNumbers.dll",".dll","126976","2024-12-13T07:41:34.0000000Z","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","v1.0.4","AnnotateLineNumbers","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"AutoAnnotateView.dll","AutoAnnotateView.dll",".dll","49664","2025-02-10T07:47:43.4844455Z","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","v1.0.4","AutoAnnotateView","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"AutoElevation.dll","AutoElevation.dll",".dll","28160","2025-01-16T08:13:32.2554341Z","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","v1.0.4","AutoElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"AutoLinenumberConnections.dll","AutoLinenumberConnections.dll",".dll","41472","2024-12-13T07:41:34.0000000Z","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","v1.0.4","AutoLinenumberConnections","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"b455ee6eaed79d76f084846a60ce3284","b455ee6eaed79d76f084846a60ce3284","","0","2023-12-29T11:13:44.6409171Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
|
|
||||||
"CheckOrthoView.dll","CheckOrthoView.dll",".dll","14848","2024-12-13T07:41:34.0000000Z","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","v1.0.4","CheckOrthoView","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"EnablerLoader.dll","EnablerLoader.dll",".dll","6144","2024-12-13T07:41:34.0000000Z","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","v1.0.4","EnablerLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"ExtentsIntersect.dll","ExtentsIntersect.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","v1.0.4","ExtentsIntersect","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"GetComponentGeometry.dll","GetComponentGeometry.dll",".dll","10240","2024-12-13T07:41:34.0000000Z","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","v1.0.4","GetComponentGeometry","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"GetComponentGeometry.xml","GetComponentGeometry.xml",".xml","1239","2025-01-14T11:26:24.5054138Z","70169C3A3AAAFE5A2282E01D5B9B7C99F2F4FF9B7514553434302674ABE46C98",,,,,,
|
|
||||||
"GetLinenumberAnnotationText.dll","GetLinenumberAnnotationText.dll",".dll","23040","2024-12-13T07:41:34.0000000Z","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","v1.0.4","GetLinenumberAnnotationText","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"GetPlantScriptFolderPath.dll","GetPlantScriptFolderPath.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","v1.0.4","GetPlantScriptFolderPath","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"GetScriptParameters.dll","GetScriptParameters.dll",".dll","6656","2024-12-13T07:41:34.0000000Z","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","v1.0.4","GetScriptParameters","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"ico\button_BJ.png","button_BJ.png",".png","1235","2024-12-13T07:41:34.0000000Z","414F0A23E917D41A82C697B0E9CDDDF9BFCACD1627310BD5717AEA172418AA51",,,,,,
|
|
||||||
"ico\button_CAC.png","button_CAC.png",".png","1943","2024-12-13T07:41:34.0000000Z","FA2B8877F8B41C70BA62C737C40374394D6840A53DFE2D0B0F83463C4817C138",,,,,,
|
|
||||||
"ico\button_PXP-AAV.png","button_PXP-AAV.png",".png","1905","2024-12-13T07:41:34.0000000Z","6AB171963B553B434B5AA4BFBE91993B1782606AAF9B486FA23BB27BC1F0E4AA",,,,,,
|
|
||||||
"ico\button_PXP-AC.png","button_PXP-AC.png",".png","1751","2024-12-13T07:41:34.0000000Z","60A6BD62EF3E50BDDCD00D9C281F2399E6C9D42E0C2DAA1742273C0DC48FE9C3",,,,,,
|
|
||||||
"ico\button_PXP-AE.png","button_PXP-AE.png",".png","2001","2024-12-13T07:41:34.0000000Z","F1DCF0E908F5AF38CCD13ABA58B824E5B186430414499C0F623C1D5EEAD8DAD7",,,,,,
|
|
||||||
"ico\button_PXP-ALC.png","button_PXP-ALC.png",".png","1740","2024-12-13T07:41:34.0000000Z","19DB2449B66B95965155E6E3BC313BD52000FE38C95B0FAD90EF07DF53EA9CC9",,,,,,
|
|
||||||
"ico\button_PXP-COV.png","button_PXP-COV.png",".png","2007","2024-12-13T07:41:34.0000000Z","529BDD045961C244FBD0DE177B899AF9E7CE0F4506B3463C56D2704B3BF207B5",,,,,,
|
|
||||||
"ico\button_PXP-DAV.png","button_PXP-DAV.png",".png","1249","2025-01-28T06:29:03.3724433Z","A7932F2ABDAC652EA3A901A221AB3BE2BC80FCB76FB203D44FC63AF3681F51E3",,,,,,
|
|
||||||
"ico\button_PXP-DJ.png","button_PXP-DJ.png",".png","1470","2024-12-13T07:41:34.0000000Z","127957752DC2B6573CD2FD9A2791914BA22802E07E9B7E23CD3AE1FFF9E4FFC4",,,,,,
|
|
||||||
"ico\button_PXP-ELN.png","button_PXP-ELN.png",".png","1927","2024-12-13T07:41:34.0000000Z","DF32E6A465D5C196A17F3F96473315590DF2D71995F63D2D241D7599B62C99BB",,,,,,
|
|
||||||
"ico\button_PXP-ESN.png","button_PXP-ESN.png",".png","1636","2024-12-13T07:41:34.0000000Z","0F089184CFD428DA3620F9C0AD2DCBFC21B31ED34FA301E8D5D45923783C0CC7",,,,,,
|
|
||||||
"ico\button_PXP-IE.png","button_PXP-IE.png",".png","1885","2024-12-13T07:41:34.0000000Z","F1C6373BFAF82E1908AF371D5F21133D27E0B84AFC706A703530855A2046FE43",,,,,,
|
|
||||||
"ico\button_PXP-IOD.png","button_PXP-IOD.png",".png","1942","2024-12-13T07:41:34.0000000Z","494E90C899A0BB5BB365B0E1452E92A68359610C5A38A37D09F520A39BDB3899",,,,,,
|
|
||||||
"ico\button_PXP-LN.png","button_PXP-LN.png",".png","2024","2024-12-13T07:41:34.0000000Z","F3305ED6D266E29FA475D4C8A7DF38D2D2A1776A4CE379B876B6CFA278B2FE5D",,,,,,
|
|
||||||
"ico\button_PXP-MA.png","button_PXP-MA.png",".png","1907","2024-12-13T07:41:34.0000000Z","9386249121644DCF4B9C70491F561E8B6E5BAAE87EBB2C76A84E46B10A60C8C3",,,,,,
|
|
||||||
"ico\button_PXP-OA.png","button_PXP-OA.png",".png","1999","2024-12-13T07:41:34.0000000Z","EAA2AD7EE5C6B7F3716AF1D7690C76AE59229AE9B6BA02009F5009CD9307F4E8",,,,,,
|
|
||||||
"ico\button_PXP-OSI.png","button_PXP-OSI.png",".png","1508","2024-12-13T07:41:34.0000000Z","422D10B6EBDD76085CF9C5985391CCA060464B200D6792D604658AAE47F12CEE",,,,,,
|
|
||||||
"ico\button_PXP-RA.png","button_PXP-RA.png",".png","1826","2024-12-13T07:41:34.0000000Z","1DB3F39C6CCA4EABE79D81DE6DCF9900649CDC9064F2EBF5E45F9AD849B8317F",,,,,,
|
|
||||||
"ico\button_PXP-RMP.png","button_PXP-RMP.png",".png","1759","2024-12-13T07:41:34.0000000Z","156026EC6DEB7BDBE720047409E9A25615D650D8634C2F0BAF49A71E27135258",,,,,,
|
|
||||||
"ico\button_PXP-ULN.png","button_PXP-ULN.png",".png","1928","2024-12-13T07:41:34.0000000Z","B37EBB460691EAA7BC3A91DF05C27FE7CC1F0E9F9D69BDA7703956EAA7AA785C",,,,,,
|
|
||||||
"ico\button_PXP-UOB.png","button_PXP-UOB.png",".png","1569","2024-12-13T07:41:34.0000000Z","3BC16B9923CCD34CF10F403CD65732B217E255E89539174841E7F30D9DFD2DCD",,,,,,
|
|
||||||
"InsertElevation.dll","InsertElevation.dll",".dll","24576","2025-01-16T08:13:32.2504470Z","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","v1.0.4","InsertElevation","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"LoadedAssemblies.dll","LoadedAssemblies.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","v1.0.4","LoadedAssemblies","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"OrthoAutoAnnotate.dll","OrthoAutoAnnotate.dll",".dll","108544","2025-02-10T07:45:33.3280516Z","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","v1.0.4","OrthoAutoAnnotate","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"OrthoLoader.dll","OrthoLoader.dll",".dll","18944","2025-02-24T06:59:01.9094468Z","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","v1.0.4","OrthoLoader","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"P3DCopyMissingObjectsToOrthoDrawing.dll","P3DCopyMissingObjectsToOrthoDrawing.dll",".dll","14336","2024-12-13T07:41:34.0000000Z","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","v1.0.4","P3DCopyMissingObjectsToOrthoDrawing","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"P3DNewMa.dll","P3DNewMa.dll",".dll","103936","2025-02-24T06:59:03.1350320Z","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","v1.0.4","P3DNewMa","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"P3DSetLayerForViewport.dll","P3DSetLayerForViewport.dll",".dll","4608","2024-12-13T07:41:34.0000000Z","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","v1.0.4","P3DSetLayerForViewport","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"PHPAnnotations.dll","PHPAnnotations.dll",".dll","69120","2024-12-13T07:41:34.0000000Z","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","v1.0.4","PHPAnnotations","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"Settings.xml","Settings.xml",".xml","136576","2025-01-31T07:28:24.1891507Z","AFEDC7526923ACE1DC60374F98F171809B36992262A37E1F11828F2B8D9799CB",,,,,,
|
|
||||||
"SwapAttributesMenuItem.dll","SwapAttributesMenuItem.dll",".dll","11264","2024-12-13T07:41:34.0000000Z","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","v1.0.4","SwapAttributesMenuItem","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"Template.dwg","Template.dwg",".dwg","148438","2025-01-30T13:16:13.0000000Z","EA279A30F113660CF53585451083BAEB0A01CA58C8F9A767C4B7C2A5E66ECCA3",,,,,,
|
|
||||||
"test.txt","test.txt",".txt","0","2025-01-16T09:56:52.0597137Z","E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",,,,,,
|
|
||||||
"UpdateOrthoDrawings.dll","UpdateOrthoDrawings.dll",".dll","45568","2025-02-24T06:59:02.4829428Z","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","v1.0.4","UpdateOrthoDrawings","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"UpdateOrthoViewBlocks.dll","UpdateOrthoViewBlocks.dll",".dll","15360","2024-12-13T07:41:34.0000000Z","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","v1.0.4","UpdateOrthoViewBlocks","1.0.4.0",".NETFramework,Version=v4.7","x64"
|
|
||||||
"Справка по работе с палитрой.docx","Справка по работе с палитрой.docx",".docx","855301","2025-02-05T09:53:08.1186958Z","B6DC65BA17C4EA79DBCCC501909BEDBF56EB6A9F1E24BFAD1B01158B6EC639D0",,,,,,
|
|
||||||
"Справка по работе с палитрой.pdf","Справка по работе с палитрой.pdf",".pdf","744035","2025-02-05T09:53:21.3752687Z","332CA70D3264C28BC712CA790157D0CD03BE7E07F42603DAF9247600271B7B75",,,,,,
|
|
||||||
"Справка.docx","Справка.docx",".docx","87889","2025-02-06T05:13:36.4991443Z","14BA9BD252FB6DF37C4D151787E8799AB1703847C1DEC24F3EFE4D6805B0EB12",,,,,,
|
|
||||||
"Справка.pdf","Справка.pdf",".pdf","154074","2025-02-06T05:14:24.2809131Z","EB840514755A9F56ABC8351B8EC41233FE2512118DB2D4BA0ACD46E74D2C7DC9",,,,,,
|
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
"DeployedDll","DeployedSha256","FileVersion","AssemblyVersion","LegacyProject","LegacyCsproj","LegacyOutputName","TargetFramework","PlatformTarget","CommandCount","RibbonRole","ConfigurationSource","MatchStatus"
|
|
||||||
"AnnotateLineNumbers.dll","799BC277ED98BD870F01824180B5A945B24F016DFD9956A8CCC630AFE80FDFA4","1.0.4.1","1.0.4.0","AnnotateLineNumbers","AnnotateLineNumbers\AnnotateLineNumbers.csproj","AnnotateLineNumbers","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"AutoAnnotateView.dll","EAE55411EC84AC747A3816EE65CD095648282BE44235351E7C9ED1C188BCB611","1.0.4.6","1.0.4.0","AutoAnnotateView","AutoAnnotateView\AutoAnnotateView.csproj","AutoAnnotateView","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
|
||||||
"AutoElevation.dll","A8766B73B30F8F44213769EB7824F82F73ED418DE0EF49695E00A3EB7FC28885","1.0.4.5","1.0.4.0","AutoElevation","AutoElevation\AutoElevation.csproj","AutoElevation","v4.7","x64",,,,"LIKELY_SOURCE_MATCH"
|
|
||||||
"AutoLinenumberConnections.dll","0F6B2A759F2A63B9152B480B6968BDA20F42AD1D922F67CA6E6BFD31A0715F35","1.0.4.1","1.0.4.0","AutoLinenumberConnections","AutoLinenumberConnections\AutoLinenumberConnections.csproj","AutoLinenumberConnections","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"CheckOrthoView.dll","89DB5E537C2BAEC292CDD8A0A43819119850119D54958E8CC3C43E2136AB24A0","1.0.4.1","1.0.4.0","CheckOrthoView","CheckOrthoView\CheckOrthoView.csproj","CheckOrthoView","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"EnablerLoader.dll","745D625C27F92842F2EFF3C515DB48BB9FC0FE59B6E4559ACD696BCC93FF4C51","1.0.4.1","1.0.4.0","EnablerLoader","EnablerLoader\EnablerLoader.csproj","EnablerLoader","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"ExtentsIntersect.dll","59D2776E083D4BE2FBFBE4F43009EAA9CE0E81A1752815B45C510F80654169D9","1.0.4.1","1.0.4.0","ExtentsIntersect","ExtentsIntersect\ExtentsIntersect.csproj","ExtentsIntersect","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"GetComponentGeometry.dll","9EA8C48FEC4D36D32CC6AC88B78C434662C16D4484F86EFAA46ECC309E9F9142","1.0.4.1","1.0.4.0","GetComponentGeometry","GetComponentGeometry\GetComponentGeometry.csproj","GetComponentGeometry","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"GetLinenumberAnnotationText.dll","047E721DBE380061D9ABDD92BC5475440E3B7ADB2BD0F544D538BA24B121D30E","1.0.4.1","1.0.4.0","GetLinenumberAnnotationText","GetLinenumberAnnotationText\GetLinenumberAnnotationText.csproj","GetLinenumberAnnotationText","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"GetPlantScriptFolderPath.dll","5080A167A1DAF6C19BE08F6CAB678D490736B5E07E121A918A2915F0D9D8BF65","1.0.4.1","1.0.4.0","GetPlantScriptFolderPath","GetPlantScriptFolderPath\GetPlantScriptFolderPath.csproj","GetPlantScriptFolderPath","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"GetScriptParameters.dll","7FF7B0D26BE8D89FF4A5A4FA1C6EC17BDA1506BCCB78AFE0E650137CD7BA5E87","1.0.4.1","1.0.4.0","GetScriptParameters","GetScriptParameters\GetScriptParameters.csproj","GetScriptParameters","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"InsertElevation.dll","B2D8B98F353D3DDF3A75A848E6922DC30478A1E5F98B372F24A049690492CA82","1.0.4.5","1.0.4.0","InsertElevation","InsertElevation\InsertElevation.csproj","InsertElevation","v4.7","x64",,,"Settings.xml","MULTIPLE_CANDIDATES"
|
|
||||||
"LoadedAssemblies.dll","277EB83C587A984DC2F558B8A7C86AA98E195A77AFA6FADD7DB83F2B801C0E4E","1.0.4.1","1.0.4.0","LoadedAssemblies","LoadedAssemblies\LoadedAssemblies.csproj","LoadedAssemblies","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"OrthoAutoAnnotate.dll","2E6BB2EBA0A98AA3550DFA4E661832FC7C3EE3DFF49BFC147BD4DFAD7A8596C6","1.0.4.6","1.0.4.0","OrthoAutoAnnotate","OrthoAutoAnnotate\OrthoAutoAnnotate.csproj","OrthoAutoAnnotate","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
|
||||||
"OrthoLoader.dll","D418F2CBDD134C0156E2D332BD7F9ED471B9A1E0581FE42EDF98BBE01CB3345D","1.0.4.7","1.0.4.0","OrthoLoader","OrthoLoader\OrthoLoader.csproj","OrthoLoader","v4.7","x64",,"Central ribbon/loader",,"LIKELY_SOURCE_MATCH"
|
|
||||||
"P3DCopyMissingObjectsToOrthoDrawing.dll","6C879AFC7F35EC25E5E2FFF9FF0EE6EFEB155670DBFD5D0CDE9A0F629C5727B2","1.0.4.1","1.0.4.0","P3DCopyMissingObjectsToOrthoDrawing","P3DCopyMissingObjectsToOrthoDrawing\P3DCopyMissingObjectsToOrthoDrawing.csproj","P3DCopyMissingObjectsToOrthoDrawing","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"P3DNewMa.dll","97246EE9B24FD373551EF380C5AC0CA78CF7ED6A92581D5331B54445A2A948F8","1.0.4.7","1.0.4.0","P3DNewMa","P3DNewMa\P3DNewMa.csproj","P3DNewMa","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
|
||||||
"P3DSetLayerForViewport.dll","A28E58FDE4EBAA26450CA646C5A85F98C1A04AA42C0BFC5413D718243ACBF42B","1.0.4.1","1.0.4.0","P3DSetLayerForViewport","P3DSetLayerForViewport\P3DSetLayerForViewport.csproj","P3DSetLayerForViewport","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"PHPAnnotations.dll","816A693F1B775C5506043DD0461055F248D3897E96AC8285DAE29C5B71FFCA73","1.0.4.1","1.0.4.0","PHPAnnotations","PHPAnnotations\PHPAnnotations.csproj","PHPAnnotations","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"SwapAttributesMenuItem.dll","674C865E34F9C877561E03E2077F7F2C6852C3C8B6A0503ACAA7619F3E80A460","1.0.4.1","1.0.4.0","SwapAttributesMenuItem","SwapAttributesMenuItem\SwapAttributesMenuItem.csproj","SwapAttributesMenuItem","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
"UpdateOrthoDrawings.dll","1492FFF1F56853DF4A858CD3E926640C1E4920B5E3F5CFCC12F4389855E630BA","1.0.4.2","1.0.4.0","UpdateOrthoDrawings","UpdateOrthoDrawings\UpdateOrthoDrawings.csproj","UpdateOrthoDrawings","v4.7","x64",,,"Settings.xml","LIKELY_SOURCE_MATCH"
|
|
||||||
"UpdateOrthoViewBlocks.dll","5022D4D66D79E7E7D82F9FCB647DF8E1FE658A4694EB7794E363D5EA5517F5A9","1.0.4.1","1.0.4.0","UpdateOrthoViewBlocks","UpdateOrthoViewBlocks\UpdateOrthoViewBlocks.csproj","UpdateOrthoViewBlocks","v4.7","x64",,,"Settings.xml","EXACT_SOURCE_MATCH"
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
GeneratedUtc=2026-07-31T10:39:04.9899963Z
|
|
||||||
DeployedRoot=<network root redacted to report context>
|
|
||||||
SourceRoot=<network source root redacted to report context>
|
|
||||||
Files=52
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
# Legacy versus new loading model
|
|
||||||
|
|
||||||
| Legacy behavior | Legacy evidence | New implementation | Compatibility requirement | Decision | Risk |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| Unknown external bootstrap loads `OrthoLoader.dll` | `loading-chain.md` | Bundle bootstrap not selected yet | Identify the workstation/project startup owner first | Defer loading-model decision | High |
|
|
||||||
| 20 independent DLLs loaded in order | `Settings.xml`, `Loader.cs:63-81` | one entry assembly currently | Preserve required command availability, not the DLL count | Evaluate command/module migration individually | Medium |
|
|
||||||
| DLLs loaded from OrthoLoader's directory (possibly UNC) | `Loader.cs:49,65`; bootstrap source unknown | local smoke bundle only | Decide offline/update policy with operations owner | Do not hard-code UNC in new code | High |
|
|
||||||
| Settings.xml controls order | `Loader.cs:104-112` | MSBuild/bundle manifest currently | Preserve explicit dependency ordering where behavior requires it | Document, do not emulate blindly | Medium |
|
|
||||||
| central PluginsOrtho Ribbon | `Ribbon.cs` | new Promhimproekt tab | Preserve actual command mapping where adopted | Separate UI migration decision | Medium |
|
|
||||||
| project selection uses local app_netload.lsp | `Ribbon.cs:380-405` | no equivalent | Confirm security/project-selection policy | Do not reproduce hidden local control file | High |
|
|
||||||
| network icons and documents | `Ribbon.cs:330-357`, deployed files | bundle resources | Bundle only licensed, necessary resources | Replace network runtime dependency | Medium |
|
|
||||||
|
|
||||||
Recommendation: do not choose AppLoader or a manual-NETLOAD model yet. First identify the missing external bootstrap and confirm whether `app_netload.lsp` is the corporate launcher contract. Once confirmed, use a single versioned entry assembly with explicit manifest/dependency validation; avoid sequential programmatic NETLOAD and UNC execution because the legacy chain is non-atomic and fails the remaining modules after one error.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Proven legacy loading chain
|
|
||||||
|
|
||||||
```text
|
|
||||||
Unknown external bootstrap
|
|
||||||
-> NETLOAD of deployed OrthoLoader.dll (not evidenced in audited directories)
|
|
||||||
-> OrthoLoader.Ribbon.Initialize()
|
|
||||||
-> subscribe ComponentManager.ItemInitialized
|
|
||||||
-> OrthoLoader.Initialize()
|
|
||||||
-> adjacent Settings.xml / Main/LoadAssemblies/LoadAssembly
|
|
||||||
-> sequential acDoc.Editor.Command("._netload", fullPath) for 20 DLLs
|
|
||||||
-> each DLL exposes CommandMethod commands
|
|
||||||
-> Ribbon ItemInitialized creates PluginsOrtho tab and command buttons
|
|
||||||
```
|
|
||||||
|
|
||||||
| Claim | Evidence | Confidence |
|
|
||||||
|---|---|---|
|
|
||||||
| Central loader is `OrthoLoader.dll` | `OrthoLoader/Ribbon.cs:151-156`; deployed DLL metadata and `Settings.xml` | High |
|
|
||||||
| Module list is read from adjacent `Settings.xml` | `OrthoLoader/Loader.cs:104-112` | High |
|
|
||||||
| Module loading is programmatic `._netload`, in XML order | `OrthoLoader/Loader.cs:63-81`; `configuration-map.csv` | High |
|
|
||||||
| Full DLL path is loader directory + configured filename | `OrthoLoader/Loader.cs:49, 65` | High |
|
|
||||||
| Modules load from the same directory as OrthoLoader | `Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)` in `Loader.cs:49,65` | High |
|
|
||||||
| Modules loaded directly from UNC | true only if external bootstrap NETLOADed the deployed UNC copy; bootstrap is outside audit scope | Unknown |
|
|
||||||
| Missing/failed module stops later loading and shows MessageBox | single outer try/catch around the loop, `Loader.cs:63-99` | High |
|
|
||||||
| Ribbon owner is OrthoLoader | `OrthoLoader/Ribbon.cs:16-19,151-172,193-321` | High |
|
|
||||||
| Initial bootstrap mechanism | no LISP/CUI/ARG/REG/PackageContents file found in either audited directory | Unknown |
|
|
||||||
|
|
||||||
`Assembly.LoadFrom` occurs inside several legacy command implementations for secondary dependencies. It is not the mechanism used by `OrthoLoader` to load its configured modules. `EnablerLoader` separately uses `Assembly.LoadFile` for the Plant enabler from the host base directory.
|
|
||||||
|
|
||||||
No `PackageContents.xml`, `*.bundle`, registry demand-load registration, LISP, CUI, MNL, script, batch, or PowerShell bootstrap was found in the audited locations. This proves absence only within audit scope; it does not prove the workstation bootstrap did not exist elsewhere.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Dependency,Category,Evidence,Requiredness,Notes
|
|
||||||
Settings.xml,CONFIGURATION,OrthoLoader/Loader.cs:104-112,Required,Adjacent to each loaded assembly directory
|
|
||||||
ico/*.png,RIBBON_RESOURCE,OrthoLoader/Ribbon.cs:330-357,Optional,Missing file yields null image
|
|
||||||
app_netload.lsp,CONFIGURATION,OrthoLoader/Ribbon.cs:380-405,Implicit,Local workstation file gates project-specific Ribbon enablement
|
|
||||||
annotation_settings.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
|
|
||||||
users_indexes_list.xml,REFERENCE_DATA,README.md and annotation modules,Project-dependent,Located in Plant project data area according to README
|
|
||||||
Template.dwg,TEMPLATE,Deployed inventory and legacy source references,Command-dependent,Do not treat deployed copy as approved test fixture
|
|
||||||
Plant project XML/DWG,COMMAND_TIME,UpdateOrthoDrawings and annotation source,Required by relevant command,Out of deployed plugin root
|
|
||||||
Update report CSV,LOGGING,UpdateOrthoDrawings/Commands.cs:1004,Optional,Written beside executing assembly by legacy command
|
|
||||||
Help PDFs,RIBBON_RESOURCE,P3DNewMa/NewMaControl.cs:6214,Optional,Opened from assembly directory
|
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
# Observed user workflow
|
|
||||||
|
|
||||||
After an external bootstrap loaded `OrthoLoader.dll`, the loader created a `PluginsOrtho` Ribbon tab only when `isProjectCorrect` accepted the current Plant project. That predicate reads the first line of the local file `C:\AutoCAD User Support\Applications\app_netload.lsp` and compares its encoded project root with the active Plant project (`OrthoLoader/Ribbon.cs:380-405`).
|
|
||||||
|
|
||||||
`PluginsOrtho` has eight panels. Button IDs are commands; clicks call `Document.SendStringToExecute(command + "\n", ...)` (`Ribbon.cs:193-321, 526-529`). Examples: `PXP-AAV` is registered by `AutoAnnotateView/Commands.cs:546`; `PXP-OA` by `OrthoAutoAnnotate/Commands.cs:6444`.
|
|
||||||
|
|
||||||
The deployed `ico/*.png` files are runtime Ribbon resources. They are resolved from the loader assembly directory; in this deployment that is the UNC directory. The code has a null-image fallback when an icon is absent.
|
|
||||||
|
|
||||||
The tab and panels use stable IDs and lookup before creation. Button collections are appended when a missing panel is created; repeated project-open events can enable existing UI or create absent UI. The event is detached after first Ribbon initialization, but `CurrentProjectOpenedEvent` is retained for the process lifetime.
|
|
||||||
Reference in New Issue
Block a user