Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e0c13ee00 | ||
|
|
07abdc013b | ||
|
|
990d057ae3 | ||
|
|
8ad3d97edb | ||
|
|
6624c4ffd0 | ||
|
|
a80ff5220d | ||
|
|
beadf751b0 | ||
|
|
5047b7d41b | ||
|
|
f706840d5d | ||
|
|
eb70c9d1a1 | ||
|
|
f127fb1ed2 | ||
|
|
0e53ffb810 | ||
|
|
9242d2bb08 | ||
|
|
9aad3662ed | ||
|
|
eaa6044682 | ||
|
|
87887ffc76 | ||
|
|
9443babaf1 | ||
|
|
bb45add91d | ||
|
|
5bd39d56c4 | ||
|
|
4a351d9b44 | ||
|
|
4a84c268f8 | ||
|
|
289b262053 | ||
|
|
64ace55338 | ||
|
|
8510974d75 | ||
|
|
14c244657d | ||
|
|
79f6124885 | ||
|
|
11a8130494 | ||
|
|
1986d15c2b | ||
|
|
3eaa659947 | ||
|
|
652515c2ba | ||
|
|
9a41d831b4 | ||
|
|
700922b832 | ||
|
|
064cfdd803 | ||
|
|
fcdec4d227 | ||
|
|
461cc8f71b | ||
|
|
b381f83bbd | ||
|
|
d4016237d8 | ||
|
|
aaec3f93f2 | ||
|
|
548c73cb27 | ||
|
|
c57c844f78 | ||
|
|
7d8cc570df | ||
|
|
ba39a649be | ||
|
|
b69a858f8e | ||
|
|
532c9dc1f2 | ||
|
|
01a0b711d0 | ||
|
|
acfa6723c9 | ||
|
|
53d6378cfa | ||
|
|
20ac5e4808 | ||
|
|
f9cd0a43bf | ||
|
|
a8749ffa69 | ||
|
|
43c9e42b5b | ||
|
|
95ae9dfd18 | ||
|
|
34cdfa225c | ||
|
|
2626b64e63 | ||
|
|
6e909f89bc | ||
|
|
91f316d79b | ||
|
|
7090321167 | ||
|
|
6f39bb204d | ||
|
|
3128568965 | ||
|
|
58f71267a2 | ||
|
|
066a9f3d94 | ||
|
|
0a7f05989a | ||
|
|
315d36a36a | ||
|
|
431366c27c | ||
|
|
172d0fa0f1 | ||
|
|
d3da634894 | ||
|
|
a17ad87fc2 | ||
|
|
ed7218983b | ||
|
|
10af8c8f12 | ||
|
|
b6a2cd06af | ||
|
|
8e197cdcde | ||
|
|
7070e4dffe | ||
|
|
cce3ff71c5 | ||
|
|
70ae18981a | ||
|
|
20960ae91f | ||
|
|
ca0f49ee5a | ||
|
|
c3ac183fff | ||
|
|
68f500692c | ||
|
|
86cabb584b | ||
|
|
aefd3aa487 | ||
|
|
07ab3c436f | ||
|
|
c97f02c550 | ||
|
|
62f470c7ba | ||
|
|
2fe1ca71df | ||
|
|
e707456ab6 | ||
|
|
3cecee4777 | ||
|
|
8a9f95a605 | ||
|
|
c270a182ce | ||
|
|
25311e89f0 | ||
|
|
f5a70df68a | ||
|
|
5725bc8737 | ||
|
|
6e4cc74e82 | ||
|
|
1127a8376f | ||
|
|
36178374db | ||
|
|
b33351117b | ||
|
|
56dcce42f6 |
@@ -1,6 +1,10 @@
|
|||||||
/link/
|
/link/
|
||||||
/link
|
/link
|
||||||
|
|
||||||
|
# Generated build output is never source or package input.
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
|
||||||
# DocumentAutomation generated files and local source data
|
# DocumentAutomation generated files and local source data
|
||||||
DocumentAutomation/**/bin/
|
DocumentAutomation/**/bin/
|
||||||
DocumentAutomation/**/obj/
|
DocumentAutomation/**/obj/
|
||||||
|
|||||||
@@ -127,3 +127,37 @@ formatting в одном commit.
|
|||||||
runtime-сценарий; записаны фактические результаты; выполнен rollback; fixture и
|
runtime-сценарий; записаны фактические результаты; выполнен rollback; fixture и
|
||||||
production изолированы; механика документирована; ветка опубликована обычным
|
production изолированы; механика документирована; ветка опубликована обычным
|
||||||
push.
|
push.
|
||||||
|
|
||||||
|
## Общая карта проекта
|
||||||
|
|
||||||
|
Каноническая общая карта проекта находится в Google Drive:
|
||||||
|
`https://drive.google.com/drive/folders/1avnR0V8q7y5pfWRYwB6LPBEEyfpN7PpV`.
|
||||||
|
Локальная синхронизированная копия, если она доступна, определяется только
|
||||||
|
переменной среды `PHIM_PLANT3D_PROJECT_MAP_ROOT`; её путь не хардкодить.
|
||||||
|
|
||||||
|
Перед работой над архитектурой, миграцией или runtime-сценарием обязательно
|
||||||
|
прочитать доступные индекс/обзор карты, целевую архитектуру и migration status,
|
||||||
|
индекс runtime evidence, а также записи карты для затрагиваемых Launcher и
|
||||||
|
P_Rec_Annot. Затем сверить их с локальными AGENTS.md, релевантными tracked
|
||||||
|
документами, текущим кодом и тестами.
|
||||||
|
|
||||||
|
Источники истины имеют следующий приоритет: фактическое runtime evidence;
|
||||||
|
текущий код, tests и tracked configuration на проверенном Git HEAD; tracked
|
||||||
|
документация репозитория; общая карта проекта. Внешняя карта описывает и
|
||||||
|
связывает работу, но не является build/runtime dependency и не переопределяет
|
||||||
|
текущий код или runtime evidence.
|
||||||
|
|
||||||
|
Проверять свежесть каждой используемой записи карты по фактическим `git
|
||||||
|
rev-parse HEAD`, ветке и привязанному runtime evidence. Если карта или её
|
||||||
|
локальная копия недоступна, фиксировать `PROJECT_MAP_UNAVAILABLE`; если
|
||||||
|
зафиксированные commit/runtime status не совпадают с фактическими —
|
||||||
|
`PROJECT_MAP_STALE`. Эти статусы не разрешают выдумывать evidence: продолжать
|
||||||
|
по доступным локальным источникам и явно сообщать расхождение.
|
||||||
|
|
||||||
|
Системные инварианты сохраняются: Launcher доставляет versioned local package
|
||||||
|
с одним managed entry assembly и без Autodesk DLL; P_Rec_Annot подтверждает
|
||||||
|
реальное Plant/Ortho/DataLinks поведение runtime evidence, а не только build
|
||||||
|
или tests. После изменения архитектуры либо runtime-статуса в отчёте указывать
|
||||||
|
конкретные документы общей карты, которые требуют обновления (архитектура,
|
||||||
|
статус миграции, evidence index и запись затронутой подсистемы); не заявлять,
|
||||||
|
что они уже обновлены, без фактического изменения.
|
||||||
|
|||||||
@@ -87,3 +87,33 @@ Autodesk properties по строковым именам.
|
|||||||
одного закрытого состояния проекта. Сравнивать нужно `control delta` с
|
одного закрытого состояния проекта. Сравнивать нужно `control delta` с
|
||||||
`DryRun delta`: одинаковые изменения классифицируются как baseline behavior
|
`DryRun delta`: одинаковые изменения классифицируются как baseline behavior
|
||||||
Plant 3D, а plugin-only изменения блокируют дальнейшую работу.
|
Plant 3D, а plugin-only изменения блокируют дальнейшую работу.
|
||||||
|
|
||||||
|
## Общая карта проекта
|
||||||
|
|
||||||
|
Каноническая общая карта проекта:
|
||||||
|
`https://drive.google.com/drive/folders/1avnR0V8q7y5pfWRYwB6LPBEEyfpN7PpV`.
|
||||||
|
Необязательная локальная копия доступна только через
|
||||||
|
`PHIM_PLANT3D_PROJECT_MAP_ROOT`; локальный путь синхронизации не хардкодить.
|
||||||
|
|
||||||
|
До изменения P_Rec_Annot читать доступные индекс карты, запись архитектуры и
|
||||||
|
статуса P_Rec_Annot, DryRun/Apply contract, карту Ortho/DataLinks и связанный
|
||||||
|
runtime evidence index. Сверять их с фактическим Git HEAD, текущими plugin
|
||||||
|
code/tests/configuration и runtime evidence конкретной fixture.
|
||||||
|
|
||||||
|
Приоритет источников: runtime evidence, затем code/tests/configuration на
|
||||||
|
проверенном HEAD, затем tracked repository documentation, затем общая карта.
|
||||||
|
Карта не является build/runtime dependency и не заменяет фактический код либо
|
||||||
|
runtime evidence. Недоступную карту отмечать `PROJECT_MAP_UNAVAILABLE`;
|
||||||
|
несовпадение её commit/runtime status с фактическими данными —
|
||||||
|
`PROJECT_MAP_STALE`.
|
||||||
|
|
||||||
|
Инварианты P_Rec_Annot: остаётся единственная NETLOAD entry assembly, aliases
|
||||||
|
и статусы команд не исчезают молча, Smoke/DryRun не пишут DWG, а Apply требует
|
||||||
|
явного разрешения, локальной fixture, ограниченной transaction, evidence и
|
||||||
|
проверяемого rollback/UNDO. Не ослаблять подтверждённый Ortho source/DataLinks
|
||||||
|
контракт и DCF control comparison ради сведений из карты.
|
||||||
|
|
||||||
|
После изменения plugin architecture или runtime-status в отчёте перечислять
|
||||||
|
документы карты, требующие обновления: запись P_Rec_Annot, command/status,
|
||||||
|
Ortho/DataLinks contract, DryRun/Apply/UNDO evidence и migration status. Не
|
||||||
|
утверждать обновление внешней документации без фактического изменения.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
<Import Project="Local.Build.props" Condition="Exists('Local.Build.props')" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -9,5 +10,8 @@
|
|||||||
<AutoCAD2026ManagedDir Condition="'$(AutoCAD2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)</AutoCAD2026ManagedDir>
|
<AutoCAD2026ManagedDir Condition="'$(AutoCAD2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)</AutoCAD2026ManagedDir>
|
||||||
<Plant3D2026ManagedDir Condition="'$(Plant3D2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)\PLNT3D</Plant3D2026ManagedDir>
|
<Plant3D2026ManagedDir Condition="'$(Plant3D2026ManagedDir)' == '' and '$(Plant3D2026InstallDir)' != ''">$(Plant3D2026InstallDir)\PLNT3D</Plant3D2026ManagedDir>
|
||||||
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == '' and '$(AutoCAD2026ManagedDir)' != ''">$(AutoCAD2026ManagedDir)</Plant3D2026InstallDir>
|
<Plant3D2026InstallDir Condition="'$(Plant3D2026InstallDir)' == '' and '$(AutoCAD2026ManagedDir)' != ''">$(AutoCAD2026ManagedDir)</Plant3D2026InstallDir>
|
||||||
|
<Plant3D2026StandardInstallDir Condition="'$(Plant3D2026StandardInstallDir)' == ''">$(ProgramW6432)\Autodesk\AutoCAD 2026</Plant3D2026StandardInstallDir>
|
||||||
|
<AutoCAD2026ManagedDir Condition="'$(AutoCAD2026ManagedDir)' == '' and Exists('$(Plant3D2026StandardInstallDir)\AcMgd.dll')">$(Plant3D2026StandardInstallDir)</AutoCAD2026ManagedDir>
|
||||||
|
<Plant3D2026ManagedDir Condition="'$(Plant3D2026ManagedDir)' == '' and Exists('$(Plant3D2026StandardInstallDir)\PLNT3D\PnPProjectManagerMgd.dll')">$(Plant3D2026StandardInstallDir)\PLNT3D</Plant3D2026ManagedDir>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.1.0" Author="NBIM">
|
<ApplicationPackage SchemaVersion="1.0" Name="P_Rec_Annot" Description="P_Rec_Annot Plant 3D 2026" AppVersion="0.2.19" ProductCode="b455ee6eaed79d76f084846a60ce3284" Author="NBIM">
|
||||||
<Components>
|
<Components>
|
||||||
<!-- SeriesMin/SeriesMax and platform token are pending official/runtime verification. -->
|
<!-- SeriesMin/SeriesMax and platform token are pending official/runtime verification. -->
|
||||||
<RuntimeRequirements OS="Win64" />
|
<RuntimeRequirements OS="Win64" />
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Main>
|
||||||
|
<CopyMissingObjects>
|
||||||
|
<Object type="Block" name="BLOCK_ANNO_POSITION" />
|
||||||
|
<Object type="Block" name="BLOCK_ELEVATION" />
|
||||||
|
<Object type="Block" name="BLOCK_LINENUMBER_CONNECTION" />
|
||||||
|
<Object type="Layer" name="PXP_ANNOTATION" />
|
||||||
|
<Object type="Layer" name="PXP_AXIS" />
|
||||||
|
<Object type="TextStyle" name="GOSTC" />
|
||||||
|
</CopyMissingObjects>
|
||||||
|
<LayersSetup>
|
||||||
|
<LayerSetup filtertype="Status" filtervalue="New" color="" lineweight="60" linetype="Continuous" />
|
||||||
|
<LayerSetup filtertype="Status" filtervalue="Existing" color="" lineweight="-3" linetype="Continuous" />
|
||||||
|
<LayerSetup filtertype="Status" filtervalue="Reserved" color="" lineweight="-3" linetype="PXP_DASH" />
|
||||||
|
<LayerSetup filtertype="Status" filtervalue="Demolition" color="" lineweight="-3" linetype="PXP_DASH" />
|
||||||
|
<LayerSetup filtertype="Layer" filtervalue="CENTER" color="4" lineweight="-3" linetype="PXP_AXIS" />
|
||||||
|
</LayersSetup>
|
||||||
|
<Annotations layer="PXP_ANNOTATION" multileaderstyle="PXP_MLEADER" textstyle="GOSTC" />
|
||||||
|
<LinenumberConnections polylinelayer="PXP_AXIS" textlayer="PXP_TEXT" textstyle="GOSTC" />
|
||||||
|
<Elevations layer="PXP_THIN" textstyle="GOSTC" />
|
||||||
|
<SetLayer name="PXP_AXIS" />
|
||||||
|
</Main>
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# Build
|
# Build
|
||||||
|
|
||||||
|
References resolve in this order: explicit `AutoCAD2026ManagedDir` and
|
||||||
|
`Plant3D2026ManagedDir`, an untracked `Local.Build.props`, then the validated
|
||||||
|
standard Plant 3D 2026 install directory. No Autodesk binary is copied into the
|
||||||
|
repository or bundle. If discovery does not find all required assemblies, pass
|
||||||
|
the explicit properties shown below.
|
||||||
|
|
||||||
The workstation has .NET SDK 8.0.423 and Autodesk AutoCAD Plant 3D 2026 installed. The paths are deliberately supplied at build time; no machine-specific Autodesk path is stored in project files.
|
The workstation has .NET SDK 8.0.423 and Autodesk AutoCAD Plant 3D 2026 installed. The paths are deliberately supplied at build time; no machine-specific Autodesk path is stored in project files.
|
||||||
|
|
||||||
Detected assembly inventory:
|
Detected assembly inventory:
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"candidate": "P_Rec_Annot 0.2.19",
|
||||||
|
"root": "<TEST_FIXTURE_ROOT>/controlled-fixtures-0.2.19",
|
||||||
|
"sourcePolicy": "copied only from approved controlled PRecAnnotOrthoDryRun/FilesRelease fixtures; no production data",
|
||||||
|
"families": [
|
||||||
|
{"id":"line-number","artifacts":["line-number/TestPipeOrtho.dwg","line-number/TestPipeModel.dwg","line-number/Project.xml","line-number/Piping.dcf"],"status":"MATERIALIZED; OWNED_HANDLE_DISCOVERY_REQUIRED","workflows":["PXP-LN","PXP-ULN","PXP-ESN","PXP-ELN","PXP-RMP"]},
|
||||||
|
{"id":"elevation","artifacts":["elevation/TestPipeOrtho.dwg","elevation/TestPipeModel.dwg","elevation/Project.xml","elevation/Piping.dcf"],"status":"MATERIALIZED; SOURCE_ELEVATION_CASES_REQUIRED","workflows":["PXP-AE","PXP-IE"]},
|
||||||
|
{"id":"ortho","artifacts":["ortho/TestPipeOrtho.dwg","ortho/TestPipeModel.dwg","ortho/Project.xml","ortho/Piping.dcf"],"status":"MATERIALIZED; EXACT_VIEWPORT_MAPPING_REQUIRED","workflows":["PXP-AAV","PXP-COV","PXP-UOB","PXP-DJ"]},
|
||||||
|
{"id":"manual","artifacts":["manual/Template.dwg"],"status":"MATERIALIZED; MANUAL_STATE_AND_OWNED_BLOCK_REQUIRED","workflows":["mto-ma","PXP-MA","CAC","CAC1","BJ","PXP-MAB","PXP-AC"]},
|
||||||
|
{"id":"batch","artifacts":["batch/FolderA/SameName.dwg","batch/FolderB/SameName.dwg"],"status":"MATERIALIZED; CONTROLLED_PROJECT_RELATIVE_IDENTITY_REQUIRED","workflows":["PXP-IOD"]}
|
||||||
|
],
|
||||||
|
"hashes": {
|
||||||
|
"TestPipeOrtho.dwg": "B372BB94B039AB70E0721F2403D36065588C099B101360BE916C74F55B4B7A39",
|
||||||
|
"TestPipeModel.dwg": "D702E6E1BF100F6BB2D5623128ECB8E8685398FEFD162A989A1574404C1EA8BF",
|
||||||
|
"Project.xml": "EC622CF25E31FDCFEA009581880DA74DC5213EC8427767AB1310E715E2C4F231",
|
||||||
|
"Piping.dcf": "F28A09832CDF647722E81F6CC38A632EC576E8B921E68B01D5335421FF20CE36",
|
||||||
|
"Template.dwg": "22CAAD35CE9C55891CF91042ED9B2C23BA4D5A0745AA40D69F41C1F609DAADB3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"policy": "controlled-local-only; fail-closed on ambiguous identity; no production DWG/project writes",
|
||||||
|
"fixtures": [
|
||||||
|
{
|
||||||
|
"id": "LINE_NUMBER_ORTHO_OWNED",
|
||||||
|
"requiredDocument": "controlled Plant Ortho DWG copy",
|
||||||
|
"requiredObjects": ["Pipe with DataLinks line number", "owned line-number block", "legacy layers/text styles"],
|
||||||
|
"selection": "exact owned block handle discovered from drawing",
|
||||||
|
"preState": "owned annotation baseline and unchanged Plant source",
|
||||||
|
"mutation": "create/update/delete line annotation",
|
||||||
|
"repeat": "update or AlreadyExists according to command",
|
||||||
|
"undo": "native AutoCAD UNDO restores entity and ownership baseline",
|
||||||
|
"workflows": ["PXP-LN", "PXP-ULN", "PXP-ESN", "PXP-ELN", "PXP-RMP"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "CONNECTOR_TOPOLOGY",
|
||||||
|
"requiredDocument": "controlled Plant project and Ortho copy",
|
||||||
|
"requiredObjects": ["two connected Plant parts", "connector/port snapshot", "P3dPartConnection", "line-group relationships"],
|
||||||
|
"selection": "exact connector source identity; no arbitrary entity fallback",
|
||||||
|
"preState": "no owned connection annotation",
|
||||||
|
"mutation": "orthogonal connection leader and ownership XData",
|
||||||
|
"repeat": "recreate/update without duplicate ownership",
|
||||||
|
"undo": "native AutoCAD UNDO restores pre-annotation state",
|
||||||
|
"workflows": ["PXP-ALC"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ELEVATION_GOLDEN",
|
||||||
|
"requiredDocument": "controlled Plant model plus paper/model context",
|
||||||
|
"requiredObjects": ["positive, negative and zero elevations", "BLOCK_ELEVATION definition", "attribute definitions"],
|
||||||
|
"selection": "exact Plant source object handle",
|
||||||
|
"preState": "source object and annotation baseline",
|
||||||
|
"mutation": "legacy /5, AwayFromZero, *5 elevation annotation",
|
||||||
|
"repeat": "update existing owned annotation",
|
||||||
|
"undo": "native AutoCAD UNDO restores baseline",
|
||||||
|
"workflows": ["PXP-AE", "PXP-IE"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ORTHO_VIEWPORT_EXACT",
|
||||||
|
"requiredDocument": "controlled Plant Ortho DWG with one-to-one source view mapping",
|
||||||
|
"requiredObjects": ["exact viewport handle/ObjectId", "layout membership", "Ortho view identity", "visible Plant candidates"],
|
||||||
|
"selection": "validated AcDbViewport handle and expected Ortho mapping",
|
||||||
|
"preState": "viewport and annotation baseline",
|
||||||
|
"mutation": "view annotation/viewport maintenance operation",
|
||||||
|
"repeat": "deterministic update/no duplicate",
|
||||||
|
"undo": "native AutoCAD UNDO restores viewport/entity baseline",
|
||||||
|
"workflows": ["PXP-AAV", "PXP-COV", "PXP-UOB", "PXP-DJ"],
|
||||||
|
"status": "EXACT_VIEWPORT_FIXTURE_BLOCKED"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "MANUAL_TEMPLATE_EDIT",
|
||||||
|
"requiredDocument": "controlled local Template-derived drawing",
|
||||||
|
"requiredObjects": ["manual annotation blocks", "attributes", "dynamic properties", "required layers/styles", "owned P_REC_ANNOT_MANUAL XData"],
|
||||||
|
"selection": "exact owned annotation handle for edit/reopen",
|
||||||
|
"preState": "palette state and owned annotation baseline",
|
||||||
|
"mutation": "manual insert/update/attribute persistence",
|
||||||
|
"repeat": "restore and update existing owned annotation",
|
||||||
|
"undo": "native AutoCAD UNDO restores entity and manual-state baseline",
|
||||||
|
"workflows": ["mto-ma", "PXP-MA", "CAC", "CAC1", "BJ", "PXP-MAB", "PXP-AC"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ORTHO_BATCH_SAME_NAME",
|
||||||
|
"requiredDocument": "controlled project copy with two relative drawing paths",
|
||||||
|
"requiredObjects": ["same filename in different project-relative directories", "one failing controlled drawing"],
|
||||||
|
"selection": "project-relative identity, never filename-only",
|
||||||
|
"preState": "unchanged controlled copies",
|
||||||
|
"mutation": "per-drawing update with continue-on-error",
|
||||||
|
"repeat": "deterministic ordering and idempotent result",
|
||||||
|
"undo": "per-drawing recovery/cleanup",
|
||||||
|
"workflows": ["PXP-IOD"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Release asset mapping
|
||||||
|
|
||||||
|
This is the release-to-composite contract for the owner-provided `P_Rec_Annot
|
||||||
|
v1.0.4.2` payload. It deliberately does not copy legacy binaries or a DWG into
|
||||||
|
source control. Autodesk assemblies, legacy plugin DLLs, Plant project data,
|
||||||
|
and the controlled `Template.dwg` fixture are excluded by repository policy.
|
||||||
|
|
||||||
|
## Executable payload
|
||||||
|
|
||||||
|
| Legacy release artifact | Modern functional replacement | Package disposition |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AnnotateLineNumbers.dll`, `GetLinenumberAnnotationText.dll` | `PRecAnnot.Core`, `PRecAnnot.Application`, `PRecAnnot.Plant3D2026` line-number services | Replaced by composite assemblies |
|
||||||
|
| `AutoLinenumberConnections.dll` | connector snapshots, connection planner and Plant adapter | Replaced by composite assemblies |
|
||||||
|
| `AutoElevation.dll`, `InsertElevation.dll` | elevation formatter, planner and Plant mutation adapters | Replaced by composite assemblies |
|
||||||
|
| `AutoAnnotateView.dll`, `OrthoAutoAnnotate.dll` | modern Ortho annotation commands and adapters | Replaced by composite assemblies |
|
||||||
|
| `CheckOrthoView.dll`, `UpdateOrthoViewBlocks.dll`, `P3DCopyMissingObjectsToOrthoDrawing.dll`, `P3DSetLayerForViewport.dll`, `UpdateOrthoDrawings.dll` | modern Ortho maintenance commands and adapters | Replaced by composite assemblies |
|
||||||
|
| `GetComponentGeometry.dll`, `ExtentsIntersect.dll` | `PRecAnnot.Core` geometry contracts and Plant geometry reader | Replaced by composite assemblies |
|
||||||
|
| `P3DNewMa.dll`, `PHPAnnotations.dll`, `SwapAttributesMenuItem.dll` | `PRecAnnot.UI` palette and manual annotation commands | Replaced by composite assemblies |
|
||||||
|
| `GetPlantScriptFolderPath.dll`, `GetScriptParameters.dll` | typed portable settings/path contract | Replaced by composite assemblies |
|
||||||
|
| `OrthoLoader.dll`, `LoadedAssemblies.dll`, `EnablerLoader.dll` | composite bootstrap dependency order and capability checks | Replaced by the single entry assembly; legacy loaders never ship or execute |
|
||||||
|
|
||||||
|
The package file manifest generated by `tools/Build-Bundle.ps1` records SHA-256
|
||||||
|
for every package file. `tools/Validate-Bundle.ps1` verifies that manifest
|
||||||
|
before materialization.
|
||||||
|
|
||||||
|
## Configuration and named-object contract
|
||||||
|
|
||||||
|
| Artifact | Modern consumer / role | Missing-resource behavior |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Settings.xml` | `bundle/P_Rec_Annot.bundle/Settings.xml`, parsed by `ProductionSettingsContract` and feature-specific settings readers | Command preflight reports configuration failure; no mutation occurs |
|
||||||
|
| `Template.dwg` | Controlled local fixture/template source for line-number, connection, elevation, Ortho and manual block/style contracts | Never silently depend on an arbitrary drawing. Commands report the required block/style as unavailable and make no mutation |
|
||||||
|
| block definitions and attribute tags | Resolved by the respective annotation/manual command before transaction | Preflight failure; no partial creation |
|
||||||
|
| layers, text styles, MLeader styles, dimension styles | Resolved by feature settings and the controlled template contract | Preflight failure; no partial mutation |
|
||||||
|
| dynamic block properties | Read and updated only by the manual annotation workflow when the selected block exposes them | Preserve existing values when absent; do not synthesize properties |
|
||||||
|
|
||||||
|
`Template.dwg` has release SHA-256
|
||||||
|
`22CAAD35CE9C55891CF91042ED9B2C23BA4D5A0745AA40D69F41C1F609DAADB3`.
|
||||||
|
It must be supplied through the controlled fixture provisioner, not committed or
|
||||||
|
bundled as a production drawing.
|
||||||
|
|
||||||
|
## User-facing resources
|
||||||
|
|
||||||
|
| Release artifacts | Consumer | Mapping state |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ico/button_*.png` (20 files) | Legacy Ribbon/palette imagery | Source inventory retained; modern Ribbon currently uses command bindings without those binary images. Image packaging requires an approved non-binary resource import and explicit UI runtime evidence. |
|
||||||
|
| `Справка по работе с палитрой.docx` and `.pdf` | Manual annotation help surface | Documentation assets are retained in the owner release. The modern palette exposes no fabricated local copy; help links must resolve from a package-approved documentation asset before UI runtime verification. |
|
||||||
|
| `GetComponentGeometry.xml` | Legacy assembly XML documentation | Functionally replaced by modern source/API documentation; it is not a runtime dependency. |
|
||||||
|
|
||||||
|
The two rows marked as requiring approved resource import are intentionally not
|
||||||
|
classified as packaged. They remain explicit release-asset and UI-runtime gaps,
|
||||||
|
rather than being hidden by copying legacy content or claiming unverified
|
||||||
|
resource behavior.
|
||||||
@@ -21,6 +21,119 @@ public readonly record struct Point2Dto(double X, double Y);
|
|||||||
public readonly record struct Point3Dto(double X, double Y, double Z);
|
public readonly record struct Point3Dto(double X, double Y, double Z);
|
||||||
public readonly record struct Vector2Dto(double X, double Y);
|
public readonly record struct Vector2Dto(double X, double Y);
|
||||||
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
|
public sealed record Bounds2Dto(Point2Dto Minimum, Point2Dto Maximum);
|
||||||
|
public sealed record Bounds3Dto(Point3Dto Minimum, Point3Dto Maximum);
|
||||||
|
public enum ExtentsIntersectionType { DoNotIntersect, Intersect, ExtentsMatch, FirstExtentsInsideSecondExtents, SecondExtentsInsideFirstExtents }
|
||||||
|
|
||||||
|
public sealed record LineNumberAnnotationSettings
|
||||||
|
{
|
||||||
|
public required string NullOrEmpty { get; init; }
|
||||||
|
public required IReadOnlyList<LineNumberAnnotationDefinition> Annotations { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record LineNumberAnnotationDefinition
|
||||||
|
{
|
||||||
|
public required string Nickname { get; init; }
|
||||||
|
public IReadOnlyDictionary<string, string> Variables { get; init; } = new Dictionary<string, string>();
|
||||||
|
public required IReadOnlyList<LineNumberAnnotationTag> Tags { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record LineNumberAnnotationTag
|
||||||
|
{
|
||||||
|
public required string Field { get; init; }
|
||||||
|
public string Divider { get; init; } = string.Empty;
|
||||||
|
public bool SaveWhenEmpty { get; init; } = true;
|
||||||
|
public string IgnoreValue { get; init; } = string.Empty;
|
||||||
|
public string ProcessingXml { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record LineNumberFormattingResult(bool Success, string Text, string? ErrorCode = null);
|
||||||
|
public interface ILineNumberAnnotationFormatter
|
||||||
|
{
|
||||||
|
LineNumberFormattingResult Format(LineNumberAnnotationDefinition definition, IReadOnlyDictionary<string, string?> fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DataLinksPropertyReadResult(bool Success, IReadOnlyDictionary<string, string?> Properties, string? FailureCode = null);
|
||||||
|
public interface IDataLinksPropertyReader { DataLinksPropertyReadResult ReadProperties(long rowId); }
|
||||||
|
public sealed record LineNumberFieldResolution(bool Success, IReadOnlyDictionary<string, string?> Values, string? FailureCode = null);
|
||||||
|
public sealed record LineNumberCandidate(string StableId, long RowId, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyDictionary<string, string?> Properties);
|
||||||
|
public sealed record ExistingLineNumberAnnotation(string StableId, string SourceStableId, string Text);
|
||||||
|
public sealed record LineNumberCommandOperation(string StableId, AnnotationOperationKind Kind, string? Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyList<string> Reasons);
|
||||||
|
public sealed record LineNumberCommandPlan(IReadOnlyList<LineNumberCommandOperation> Operations);
|
||||||
|
public sealed record LineNumberApplyPreflight(string StableId, int MarkerNumber, string Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle);
|
||||||
|
public sealed record LineNumberApplyResult(int Created, int Updated, int Unchanged);
|
||||||
|
public sealed record LineNumberListApplyPreflight(string StableId, AnnotationPoint Position, double Rotation, IReadOnlyList<LineNumberApplyPreflight> Rows);
|
||||||
|
public sealed record LineNumberPreviewResult(bool Success, LineNumberCommandPlan? Plan, IReadOnlyList<LineNumberApplyPreflight> Apply, string? FailureCode = null);
|
||||||
|
public sealed record LineNumberCandidateReadResult(bool Success, LineNumberCandidate? Candidate, string? FailureCode = null);
|
||||||
|
public sealed record ElevationCandidate(string StableId, double ModelElevation, double DatumOffset, AnnotationPoint Position, double Rotation, string Layer, string TextStyle);
|
||||||
|
public sealed record ExistingElevationAnnotation(string StableId, string SourceStableId, string Text, AnnotationPoint Position);
|
||||||
|
public sealed record ElevationAnnotationOperation(string StableId, AnnotationOperationKind Kind, string? Text, AnnotationPoint Position, double Rotation, string Layer, string TextStyle, IReadOnlyList<string> Reasons);
|
||||||
|
public sealed record ElevationAnnotationPlan(IReadOnlyList<ElevationAnnotationOperation> Operations);
|
||||||
|
public sealed record ElevationAssets(string Layer, string TextStyle, string BlockName = "BLOCK_ELEVATION");
|
||||||
|
public sealed record ElevationApplyResult(int Created, int Updated, int Unchanged);
|
||||||
|
public sealed record LineConnectionCandidate(string StableId, string LineNumber, Point3Dto FirstEndpoint, Point3Dto SecondEndpoint, Bounds3Dto Bounds, AnnotationPoint Position, double Rotation);
|
||||||
|
public sealed record LineConnectionGroup(string LineNumber, IReadOnlyList<LineConnectionCandidate> Members, AnnotationPoint Placement, double Rotation);
|
||||||
|
// A legacy PXP-ALC annotation belongs to a connector between two *different*
|
||||||
|
// line groups; it is not a duplicate marker for one line group.
|
||||||
|
public sealed record LineConnectionBoundaryCandidate(
|
||||||
|
string StableId,
|
||||||
|
string FirstLineNumber,
|
||||||
|
string SecondLineNumber,
|
||||||
|
Point3Dto FirstEndpoint,
|
||||||
|
Point3Dto SecondEndpoint,
|
||||||
|
Bounds3Dto Bounds,
|
||||||
|
AnnotationPoint Position,
|
||||||
|
double Rotation,
|
||||||
|
string? FirstLineGroupStableId = null,
|
||||||
|
string? SecondLineGroupStableId = null);
|
||||||
|
public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons, string? FirstText = null, string? SecondText = null, Point3Dto? ConnectorPosition = null);
|
||||||
|
public sealed record LineConnectionAssets(string PolylineLayer, string TextLayer, string TextStyle, string BlockName = "BLOCK_LINENUMBER_CONNECTION");
|
||||||
|
public sealed record LineConnectionApplyResult(int Deleted, int Created);
|
||||||
|
// The geometry adapter supplies the paper-space connector point and orientation.
|
||||||
|
// Keeping it separate from the PnP relation snapshot prevents Autodesk objects
|
||||||
|
// from crossing into the deterministic PXP-ALC planning layer.
|
||||||
|
public sealed record LineConnectionPlacement(
|
||||||
|
Point3Dto FirstEndpoint,
|
||||||
|
Point3Dto SecondEndpoint,
|
||||||
|
Bounds3Dto Bounds,
|
||||||
|
AnnotationPoint Position,
|
||||||
|
double Rotation);
|
||||||
|
public sealed record LineConnectionPreviewResult(
|
||||||
|
bool Success,
|
||||||
|
IReadOnlyList<LineConnectionBoundaryPlan> Plans,
|
||||||
|
string? FailureCode = null);
|
||||||
|
public sealed record PlantConnectorRelationshipSnapshot(
|
||||||
|
string StableId,
|
||||||
|
long ConnectorRowId,
|
||||||
|
long FirstPartRowId,
|
||||||
|
long SecondPartRowId,
|
||||||
|
long FirstLineGroupRowId,
|
||||||
|
long SecondLineGroupRowId,
|
||||||
|
IReadOnlyList<string> Diagnostics,
|
||||||
|
string? ConnectorEntityStableId = null);
|
||||||
|
public sealed record PlantConnectorRelationshipReadResult(bool Success, IReadOnlyList<PlantConnectorRelationshipSnapshot> Connectors, string? FailureCode = null);
|
||||||
|
public interface IPlantConnectorRelationshipReader
|
||||||
|
{
|
||||||
|
PlantConnectorRelationshipReadResult Read();
|
||||||
|
}
|
||||||
|
public sealed record PlantConnectorPortSnapshot(
|
||||||
|
string ConnectorStableId,
|
||||||
|
Point3Dto FirstPosition,
|
||||||
|
Point3Dto SecondPosition,
|
||||||
|
Point3Dto FirstDirection,
|
||||||
|
Point3Dto SecondDirection);
|
||||||
|
public sealed record PlantConnectorPortReadResult(bool Success, PlantConnectorPortSnapshot? Ports, string? FailureCode = null);
|
||||||
|
public interface IPlantConnectorPortReader
|
||||||
|
{
|
||||||
|
PlantConnectorPortReadResult Read(PlantConnectorRelationshipSnapshot connector);
|
||||||
|
}
|
||||||
|
public sealed record ConnectorViewportEligibility(bool Eligible, Point3Dto? PlacementPoint, string? FailureCode = null);
|
||||||
|
public sealed record LineNumberOwnedRow(string SourceStableId, int MarkerNumber, string Text, AnnotationPoint MarkerAttributePosition);
|
||||||
|
public sealed record LineNumberRowMutation(string SourceStableId, int PreviousMarkerNumber, int MarkerNumber, string Text, AnnotationPoint? MarkerAttributePosition);
|
||||||
|
public sealed record LineNumberTextAsset(string Layer, string TextStyle, double Size, string TextValue, Point2Dto Position);
|
||||||
|
public sealed record LineNumberLineAsset(string Layer, Point2Dto Start, Point2Dto End);
|
||||||
|
public sealed record LineNumberMarkerAsset(string Layer, double MaskScaleFactor, IReadOnlyList<LineNumberTextAsset> Texts, IReadOnlyList<LineNumberLineAsset> Lines);
|
||||||
|
public sealed record LineNumberListAsset(string Layer, double MaskScaleFactor, double LineHeight, double LineSpacingDistance, LineNumberTextAsset MarkerText, LineNumberTextAsset LineNumberText, LineNumberLineAsset Line);
|
||||||
|
public sealed record AnnotateLineNumbersAssets(LineNumberMarkerAsset Marker, LineNumberListAsset List);
|
||||||
|
|
||||||
public sealed record GeometrySnapshot
|
public sealed record GeometrySnapshot
|
||||||
{
|
{
|
||||||
@@ -29,6 +142,36 @@ public sealed record GeometrySnapshot
|
|||||||
public Bounds2Dto? ProjectedBounds { get; init; }
|
public Bounds2Dto? ProjectedBounds { get; init; }
|
||||||
public double? Rotation { get; init; }
|
public double? Rotation { get; init; }
|
||||||
public double? Elevation { get; init; }
|
public double? Elevation { get; init; }
|
||||||
|
public Vector2Dto? PaperDirection { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform-neutral result of the legacy GetComponentGeometry contract. The
|
||||||
|
// Plant adapter is responsible for reading Parts/Pipes and normalising units;
|
||||||
|
// consumers do not take a dependency on Autodesk managed assemblies.
|
||||||
|
public sealed record ComponentGeometryDefinition(
|
||||||
|
string Kind,
|
||||||
|
IReadOnlyDictionary<string, string> Parameters,
|
||||||
|
Bounds3Dto Bounds,
|
||||||
|
bool ImperialDimensions);
|
||||||
|
public sealed record ComponentGeometryReadResult(bool Success, ComponentGeometryDefinition? Geometry, string? FailureCode = null);
|
||||||
|
public sealed record ViewportLayerPlan(bool RequiresChange, string TargetLayer, string? FailureCode = null);
|
||||||
|
public enum OrthoAnnotationRepresentation { None, MultiLeader, BubbleBlock }
|
||||||
|
public sealed record OrthoAnnotationOptions(bool Supports, bool Valves, bool Instruments, bool Plugs, bool Boards);
|
||||||
|
public sealed record OrthoAnnotationClassification(string StableId, OrthoAnnotationRepresentation Representation, string Text, IReadOnlyList<string> Reasons);
|
||||||
|
public sealed record OrthoAnnotationAssets(string Layer, string TextStyle, string MultiLeaderStyle, string BubbleBlock = "BLOCK_ANNO_POSITION");
|
||||||
|
public sealed record OrthoBatchDrawing(string DrawingGuid, string DisplayName, string RelativePath, IReadOnlyList<string> Views);
|
||||||
|
public sealed record OrthoBatchSelection(string RelativePath, IReadOnlyList<string> Views);
|
||||||
|
public sealed record OrthoBatchOperation(string DrawingGuid, string DisplayName, string RelativePath, string ViewName);
|
||||||
|
public sealed record OrthoBatchPlan(IReadOnlyList<OrthoBatchOperation> Operations, IReadOnlyList<string> Diagnostics);
|
||||||
|
public enum OrthoLayerFilterKind { Status, Layer, Table }
|
||||||
|
public sealed record OrthoLayerRule(OrthoLayerFilterKind Kind, string Filter, int? LineWeight, string LineType, short? ColorIndex, byte? Red, byte? Green, byte? Blue)
|
||||||
|
{
|
||||||
|
public bool MatchesStatus(string status) => Kind == OrthoLayerFilterKind.Status && string.Equals(Filter, status, StringComparison.OrdinalIgnoreCase);
|
||||||
|
public bool MatchesLayer(string viewName, string layer) => Kind == OrthoLayerFilterKind.Layer && string.Equals(viewName + "-" + Filter, layer, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
public interface IPlantComponentGeometryReader
|
||||||
|
{
|
||||||
|
ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record OrthoViewportInfo
|
public sealed record OrthoViewportInfo
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class AnnotateLineNumbersSettingsLoader
|
||||||
|
{
|
||||||
|
public AnnotateLineNumbersAssets Load(string xml)
|
||||||
|
{
|
||||||
|
var lineNumbers = XElement.Parse(xml).Element("LineNumbers") ?? throw Invalid("LineNumbers");
|
||||||
|
var marker = lineNumbers.Element("Marker") ?? throw Invalid("Marker");
|
||||||
|
var list = lineNumbers.Element("List") ?? throw Invalid("List");
|
||||||
|
var texts = marker.Elements("Text").Select(x => Text(x)).ToArray();
|
||||||
|
var lines = marker.Elements("Line").Select(Line).ToArray();
|
||||||
|
if (texts.Length == 0 || lines.Length == 0) throw Invalid("MarkerContent");
|
||||||
|
return new(
|
||||||
|
new(Required(marker, "layer"), Number(marker, "maskscalefactor"), texts, lines),
|
||||||
|
new(Required(list, "layer"), Number(list, "maskscalefactor"), Number(list, "lineheight"), Number(list, "linespacingdistance"), Text(RequiredElement(list, "MarkerText"), false), Text(RequiredElement(list, "LineNumberText"), false), Line(RequiredElement(list, "Line"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberTextAsset Text(XElement value, bool readValue = true) => new(Required(value, "layer"), Required(value, "textstyle"), Number(value, "size"), readValue ? Required(value, "textvalue", true) : string.Empty, new(Number(value, "positionx"), Number(value, "positiony")));
|
||||||
|
private static LineNumberLineAsset Line(XElement value) => new(Required(value, "layer"), new(Number(value, "startx"), Number(value, "starty")), new(OptionalNumber(value, "endx"), OptionalNumber(value, "endy")));
|
||||||
|
private static XElement RequiredElement(XElement parent, string name) => parent.Element(name) ?? throw Invalid(name);
|
||||||
|
private static string Required(XElement value, string name, bool emptyAllowed = false)
|
||||||
|
{
|
||||||
|
var attribute = value.Attribute(name) ?? throw Invalid(name);
|
||||||
|
if (!emptyAllowed && string.IsNullOrWhiteSpace(attribute.Value)) throw Invalid(name);
|
||||||
|
return attribute.Value;
|
||||||
|
}
|
||||||
|
private static double Number(XElement value, string name) => Parse(Required(value, name));
|
||||||
|
private static double OptionalNumber(XElement value, string name) => value.Attribute(name) is { } attribute ? Parse(attribute.Value) : 0;
|
||||||
|
private static double Parse(string value) => double.TryParse(value.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) ? result : throw Invalid("number");
|
||||||
|
private static InvalidDataException Invalid(string field) => new("LINE_NUMBER_SETTINGS_INVALID:" + field);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class AttributeSwapPlanner
|
||||||
|
{
|
||||||
|
public static double ComputeShelfLength(double firstWidth, double firstOffset, double secondWidth, double secondOffset) =>
|
||||||
|
Math.Abs(Math.Max(firstWidth + firstOffset, secondWidth + secondOffset) + .9);
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<string, string> Swap(IReadOnlyDictionary<string, string> metadata, string first, string second)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(metadata, StringComparer.Ordinal);
|
||||||
|
if (result.TryGetValue(first, out var firstValue) && result.TryGetValue(second, out var secondValue))
|
||||||
|
{ result[first] = secondValue; result[second] = firstValue; }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
// Port of AutoLinenumberConnections.IsOKPrepareAnnotation's geometry gate.
|
||||||
|
// The caller performs model-to-paper transformation only after the connector
|
||||||
|
// is eligible, preserving legacy model-space containment semantics.
|
||||||
|
public sealed class ConnectorViewportEligibilityEvaluator
|
||||||
|
{
|
||||||
|
public ConnectorViewportEligibility Evaluate(PlantConnectorPortSnapshot ports, Point3Dto viewDirection, Bounds3Dto viewExtents, double parallelTolerance = 0.1)
|
||||||
|
{
|
||||||
|
if (parallelTolerance < 0) throw new ArgumentOutOfRangeException(nameof(parallelTolerance));
|
||||||
|
if (Parallel(ports.FirstDirection, viewDirection, parallelTolerance) || Parallel(ports.SecondDirection, viewDirection, parallelTolerance))
|
||||||
|
return new(false, null, "CONNECTOR_PORT_PARALLEL_TO_VIEW");
|
||||||
|
if (!Inside(ports.FirstPosition, viewExtents) || !Inside(ports.SecondPosition, viewExtents))
|
||||||
|
return new(false, null, "CONNECTOR_OUTSIDE_VIEW_EXTENTS");
|
||||||
|
// Legacy stores ports[1] as the insertion/model anchor.
|
||||||
|
return new(true, ports.SecondPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Inside(Point3Dto point, Bounds3Dto bounds) =>
|
||||||
|
point.X >= bounds.Minimum.X && point.X <= bounds.Maximum.X &&
|
||||||
|
point.Y >= bounds.Minimum.Y && point.Y <= bounds.Maximum.Y &&
|
||||||
|
point.Z >= bounds.Minimum.Z && point.Z <= bounds.Maximum.Z;
|
||||||
|
|
||||||
|
private static bool Parallel(Point3Dto first, Point3Dto second, double tolerance)
|
||||||
|
{
|
||||||
|
var firstLength = Math.Sqrt(first.X * first.X + first.Y * first.Y + first.Z * first.Z);
|
||||||
|
var secondLength = Math.Sqrt(second.X * second.X + second.Y * second.Y + second.Z * second.Z);
|
||||||
|
if (firstLength == 0 || secondLength == 0) return true;
|
||||||
|
var cosine = Math.Abs((first.X * second.X + first.Y * second.Y + first.Z * second.Z) / (firstLength * secondLength));
|
||||||
|
return 1d - cosine <= tolerance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class ElevationAnnotationPlanner
|
||||||
|
{
|
||||||
|
public ElevationAnnotationPlan Build(IEnumerable<ElevationCandidate> candidates, IEnumerable<ExistingElevationAnnotation> existing)
|
||||||
|
{
|
||||||
|
var known = existing.GroupBy(value => value.SourceStableId, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(group => group.Key, group => group.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var operations = new List<ElevationAnnotationOperation>();
|
||||||
|
foreach (var candidate in candidates.OrderBy(value => value.StableId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!seen.Add(candidate.StableId))
|
||||||
|
{
|
||||||
|
operations.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_CANDIDATE"]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate.Layer) || string.IsNullOrWhiteSpace(candidate.TextStyle) || double.IsNaN(candidate.ModelElevation) || double.IsInfinity(candidate.ModelElevation))
|
||||||
|
{
|
||||||
|
operations.Add(new(candidate.StableId, AnnotationOperationKind.Skip, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["ELEVATION_CANDIDATE_INVALID"]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var text = LegacyElevationFormatter.Calculate(candidate.ModelElevation, candidate.DatumOffset);
|
||||||
|
var matches = known.GetValueOrDefault(candidate.StableId, []);
|
||||||
|
if (matches.Length > 1)
|
||||||
|
{
|
||||||
|
operations.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_EXISTING_ANNOTATION"]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (matches.Length == 1 && string.Equals(matches[0].Text, text, StringComparison.Ordinal) && matches[0].Position == candidate.Position)
|
||||||
|
operations.Add(new(candidate.StableId, AnnotationOperationKind.Skip, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["ALREADY_CURRENT"]));
|
||||||
|
else
|
||||||
|
operations.Add(new(candidate.StableId, matches.Length == 1 ? AnnotationOperationKind.Update : AnnotationOperationKind.Create, text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
return new(operations);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class ElevationSettingsLoader
|
||||||
|
{
|
||||||
|
public ElevationAssets Load(string xml)
|
||||||
|
{
|
||||||
|
var element = XElement.Parse(xml).Element("Elevations")
|
||||||
|
?? throw new InvalidDataException("ELEVATION_SETTINGS_INVALID:Elevations");
|
||||||
|
return new ElevationAssets(Required(element, "layer"), Required(element, "textstyle"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Required(XElement element, string name) =>
|
||||||
|
element.Attribute(name) is { Value: var value } && !string.IsNullOrWhiteSpace(value)
|
||||||
|
? value
|
||||||
|
: throw new InvalidDataException("ELEVATION_SETTINGS_INVALID:" + name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class ExtentsIntersectionClassifier
|
||||||
|
{
|
||||||
|
// Source-backed port of ExtentsIntersect.Commands without Autodesk types.
|
||||||
|
public static ExtentsIntersectionType Classify(Bounds3Dto first, Bounds3Dto second)
|
||||||
|
{
|
||||||
|
if (Same(first, second)) return ExtentsIntersectionType.ExtentsMatch;
|
||||||
|
if (Contains(first, second)) return ExtentsIntersectionType.SecondExtentsInsideFirstExtents;
|
||||||
|
if (Contains(second, first)) return ExtentsIntersectionType.FirstExtentsInsideSecondExtents;
|
||||||
|
return Overlaps(first, second) ? ExtentsIntersectionType.Intersect : ExtentsIntersectionType.DoNotIntersect;
|
||||||
|
}
|
||||||
|
private static bool Contains(Bounds3Dto outer, Bounds3Dto inner) => outer.Minimum.X <= inner.Minimum.X && outer.Minimum.Y <= inner.Minimum.Y && outer.Minimum.Z <= inner.Minimum.Z && outer.Maximum.X >= inner.Maximum.X && outer.Maximum.Y >= inner.Maximum.Y && outer.Maximum.Z >= inner.Maximum.Z;
|
||||||
|
private static bool Overlaps(Bounds3Dto a, Bounds3Dto b) => a.Minimum.X <= b.Maximum.X && a.Maximum.X >= b.Minimum.X && a.Minimum.Y <= b.Maximum.Y && a.Maximum.Y >= b.Minimum.Y && a.Minimum.Z <= b.Maximum.Z && a.Maximum.Z >= b.Minimum.Z;
|
||||||
|
private static bool Same(Bounds3Dto a, Bounds3Dto b) => a == b;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class LegacyElevationFormatter
|
||||||
|
{
|
||||||
|
public static int RoundModelElevation(double modelZ, double datumOffset)
|
||||||
|
{
|
||||||
|
// InsertElevation.BlockJig divides the model-space ordinate by five,
|
||||||
|
// rounds that quotient away from zero and restores the five-unit grid
|
||||||
|
// before formatting the signed metres/millimetres value.
|
||||||
|
var grid = Math.Round((modelZ + datumOffset) / 5d, MidpointRounding.AwayFromZero) * 5d;
|
||||||
|
return checked((int)Math.Round(grid, MidpointRounding.AwayFromZero));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatMillimetres(int millimetres)
|
||||||
|
{
|
||||||
|
if (millimetres == 0) return "0,000";
|
||||||
|
var magnitude = Math.Abs((long)millimetres).ToString(CultureInfo.InvariantCulture).PadLeft(4, '0');
|
||||||
|
var metres = magnitude[..^3];
|
||||||
|
var fraction = magnitude[^3..];
|
||||||
|
return $"{(millimetres > 0 ? "+" : "-")}{metres},{fraction}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Calculate(double modelZ, double datumOffset) => FormatMillimetres(RoundModelElevation(modelZ, datumOffset));
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
// Source-backed PXP-ALC decision logic: only a connector whose related parts
|
||||||
|
// resolve to different line groups produces an annotation. The connector is
|
||||||
|
// the stable ownership identity, preserving repeat/update behaviour.
|
||||||
|
public sealed class LineConnectionBoundaryPlanner
|
||||||
|
{
|
||||||
|
public IReadOnlyList<LineConnectionBoundaryPlan> Build(IEnumerable<LineConnectionBoundaryCandidate> candidates)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var result = new List<LineConnectionBoundaryPlan>();
|
||||||
|
foreach (var candidate in candidates.OrderBy(value => value.StableId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!seen.Add(candidate.StableId))
|
||||||
|
{
|
||||||
|
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["DUPLICATE_CONNECTOR_CANDIDATE"], ConnectorPosition: candidate.FirstEndpoint));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var first = candidate.FirstLineNumber?.Trim() ?? string.Empty;
|
||||||
|
var second = candidate.SecondLineNumber?.Trim() ?? string.Empty;
|
||||||
|
// The legacy command gates insertion on the two P3dLineGroup
|
||||||
|
// relationship IDs, not just on their rendered text. Equal text in
|
||||||
|
// distinct groups remains a real connector boundary; equal group IDs
|
||||||
|
// never does.
|
||||||
|
if (!string.IsNullOrWhiteSpace(candidate.FirstLineGroupStableId) &&
|
||||||
|
string.Equals(candidate.FirstLineGroupStableId, candidate.SecondLineGroupStableId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (first.Length == 0 || second.Length == 0)
|
||||||
|
{
|
||||||
|
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"], ConnectorPosition: candidate.FirstEndpoint));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (string.Equals(first, second, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["SAME_LINE_GROUP"], ConnectorPosition: candidate.FirstEndpoint));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var ordered = new[] { first, second }.OrderBy(value => value, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
result.Add(new(candidate.StableId, string.Join(" / ", ordered), candidate.Position, candidate.Rotation, Array.Empty<string>(), first, second, candidate.FirstEndpoint));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
// Source-backed bridge for AutoLinenumberConnections: each endpoint uses the
|
||||||
|
// same GetLinenumberAnnotationText formatting contract as PXP-LN, while the
|
||||||
|
// connector itself remains the stable annotation ownership key.
|
||||||
|
public sealed class LineConnectionPreviewService(
|
||||||
|
LineNumberFieldResolver fieldResolver,
|
||||||
|
ILineNumberAnnotationFormatter formatter,
|
||||||
|
LineConnectionBoundaryPlanner boundaryPlanner)
|
||||||
|
{
|
||||||
|
public LineConnectionPreviewResult Preview(
|
||||||
|
LineNumberAnnotationDefinition definition,
|
||||||
|
IEnumerable<PlantConnectorRelationshipSnapshot> connectors,
|
||||||
|
IReadOnlyDictionary<string, LineConnectionPlacement> placements)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
ArgumentNullException.ThrowIfNull(connectors);
|
||||||
|
ArgumentNullException.ThrowIfNull(placements);
|
||||||
|
|
||||||
|
var candidates = new List<LineConnectionBoundaryCandidate>();
|
||||||
|
foreach (var connector in connectors.OrderBy(value => value.StableId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!placements.TryGetValue(connector.StableId, out var placement))
|
||||||
|
{
|
||||||
|
candidates.Add(new(connector.StableId, string.Empty, string.Empty, new(), new(), new(new(), new()), new(), 0,
|
||||||
|
connector.FirstLineGroupRowId.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
connector.SecondLineGroupRowId.ToString(System.Globalization.CultureInfo.InvariantCulture)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fields = definition.Tags.Select(tag => tag.Field).Where(field => !string.IsNullOrWhiteSpace(field));
|
||||||
|
var first = ResolveText(definition, connector.FirstPartRowId, fields);
|
||||||
|
var second = ResolveText(definition, connector.SecondPartRowId, fields);
|
||||||
|
candidates.Add(new(
|
||||||
|
connector.StableId,
|
||||||
|
first.Text,
|
||||||
|
second.Text,
|
||||||
|
placement.FirstEndpoint,
|
||||||
|
placement.SecondEndpoint,
|
||||||
|
placement.Bounds,
|
||||||
|
placement.Position,
|
||||||
|
placement.Rotation,
|
||||||
|
connector.FirstLineGroupRowId.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
connector.SecondLineGroupRowId.ToString(System.Globalization.CultureInfo.InvariantCulture)));
|
||||||
|
}
|
||||||
|
|
||||||
|
var plans = boundaryPlanner.Build(candidates);
|
||||||
|
// Per-connector property failures are represented as non-insertable
|
||||||
|
// plans, matching legacy's continue-on-one-bad-connector behaviour.
|
||||||
|
var repaired = plans.Select(plan =>
|
||||||
|
{
|
||||||
|
var connector = connectors.First(value => string.Equals(value.StableId, plan.StableId, StringComparison.Ordinal));
|
||||||
|
if (!placements.ContainsKey(plan.StableId)) return plan with { Reasons = plan.Reasons.Append("CONNECTOR_GEOMETRY_UNAVAILABLE").Distinct(StringComparer.Ordinal).ToArray() };
|
||||||
|
var fields = definition.Tags.Select(tag => tag.Field).Where(field => !string.IsNullOrWhiteSpace(field));
|
||||||
|
var first = ResolveText(definition, connector.FirstPartRowId, fields);
|
||||||
|
var second = ResolveText(definition, connector.SecondPartRowId, fields);
|
||||||
|
var reasons = plan.Reasons.Concat(first.Error is null ? [] : [first.Error]).Concat(second.Error is null ? [] : [second.Error]).Distinct(StringComparer.Ordinal).ToArray();
|
||||||
|
return reasons.Length == plan.Reasons.Count ? plan : plan with { Text = reasons.Length == 0 ? plan.Text : string.Empty, Reasons = reasons };
|
||||||
|
}).ToArray();
|
||||||
|
return new(true, repaired);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string Text, string? Error) ResolveText(LineNumberAnnotationDefinition definition, long rowId, IEnumerable<string> fields)
|
||||||
|
{
|
||||||
|
var resolved = fieldResolver.Resolve(rowId, fields);
|
||||||
|
if (!resolved.Success) return (string.Empty, resolved.FailureCode ?? "DATALINKS_PROPERTY_READ_FAILED");
|
||||||
|
var formatted = formatter.Format(definition, resolved.Values);
|
||||||
|
return formatted.Success ? (formatted.Text, null) : (string.Empty, formatted.ErrorCode ?? "LINE_NUMBER_FORMAT_FAILED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineConnectionSettingsLoader
|
||||||
|
{
|
||||||
|
public LineConnectionAssets Load(string xml)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var element = XDocument.Parse(xml).Root?.Element("LinenumberConnections") ?? throw new InvalidOperationException("LINE_CONNECTION_SETTINGS_MISSING");
|
||||||
|
string Required(string name) => string.IsNullOrWhiteSpace((string?)element.Attribute(name))
|
||||||
|
? throw new InvalidOperationException("LINE_CONNECTION_SETTING_MISSING:" + name)
|
||||||
|
: (string)element.Attribute(name)!;
|
||||||
|
return new(Required("polylinelayer"), Required("textlayer"), Required("textstyle"));
|
||||||
|
}
|
||||||
|
catch (System.Xml.XmlException exception) { throw new InvalidOperationException("LINE_CONNECTION_SETTINGS_XML_INVALID", exception); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
// Source-backed port of GetLinenumberAnnotationText. Plant/DataLinks access
|
||||||
|
// stays at the adapter boundary; this service is deterministic and testable.
|
||||||
|
public sealed class LineNumberAnnotationFormatter : ILineNumberAnnotationFormatter
|
||||||
|
{
|
||||||
|
public LineNumberFormattingResult Format(LineNumberAnnotationDefinition definition, IReadOnlyDictionary<string, string?> fields)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
ArgumentNullException.ThrowIfNull(fields);
|
||||||
|
var text = new System.Text.StringBuilder();
|
||||||
|
foreach (var tag in definition.Tags)
|
||||||
|
{
|
||||||
|
if (!fields.TryGetValue(tag.Field, out var value)) return new(false, string.Empty, "FIELD_NOT_FOUND:" + tag.Field);
|
||||||
|
value ??= string.Empty;
|
||||||
|
if (!string.IsNullOrWhiteSpace(tag.ProcessingXml))
|
||||||
|
{
|
||||||
|
var processed = Process(value, tag.ProcessingXml, definition.Variables, fields, tag.Field);
|
||||||
|
if (!processed.Success) return processed;
|
||||||
|
value = processed.Text;
|
||||||
|
}
|
||||||
|
if (string.Equals(value, tag.IgnoreValue, StringComparison.Ordinal)) value = string.Empty;
|
||||||
|
if (string.IsNullOrEmpty(value) && !tag.SaveWhenEmpty) continue;
|
||||||
|
if (text.Length > 0) text.Append(tag.Divider == "space" ? " " : tag.Divider);
|
||||||
|
text.Append(string.IsNullOrEmpty(value) ? definition.Nickname : value);
|
||||||
|
}
|
||||||
|
return new(true, text.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberFormattingResult Process(string value, string processingXml, IReadOnlyDictionary<string, string> variables, IReadOnlyDictionary<string, string?> fields, string field)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var root = XElement.Parse(processingXml);
|
||||||
|
if (root.Name.LocalName != "processing" || !root.Elements("function").Any()) return new(false, string.Empty, "PROCESSING_INVALID");
|
||||||
|
foreach (var function in root.Elements("function"))
|
||||||
|
{
|
||||||
|
var result = Apply(function, value, variables, fields, field);
|
||||||
|
if (!result.Success) return result;
|
||||||
|
value = result.Text;
|
||||||
|
}
|
||||||
|
return new(true, value);
|
||||||
|
}
|
||||||
|
catch (System.Xml.XmlException) { return new(false, string.Empty, "PROCESSING_INVALID"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberFormattingResult Apply(XElement function, string value, IReadOnlyDictionary<string, string> variables, IReadOnlyDictionary<string, string?> fields, string field)
|
||||||
|
{
|
||||||
|
var name = function.Element("name")?.Value;
|
||||||
|
var mode = function.Element("mod")?.Value;
|
||||||
|
var aliases = function.Elements("aliases").ToDictionary(x => x.Element("key")?.Value ?? string.Empty, x => x.Element("value")?.Value ?? string.Empty, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var attrs = function.Elements("attr").Select(a => Resolve(a.Element("value")?.Value, variables, fields)).ToArray();
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(mode) || attrs.Any(x => x is null)) return new(false, string.Empty, "PROCESSING_INVALID");
|
||||||
|
var found = false;
|
||||||
|
switch (name)
|
||||||
|
{
|
||||||
|
case "ToUpper": found = aliases.TryGetValue(attrs[0]!.ToUpperInvariant(), out value!); break;
|
||||||
|
case "NotContains":
|
||||||
|
if (attrs.Length < 2) return new(false, string.Empty, "PROCESSING_INVALID");
|
||||||
|
if (attrs[1]!.Contains(attrs[0]!, StringComparison.Ordinal)) { value = string.Empty; found = true; }
|
||||||
|
else found = aliases.TryGetValue(value, out value!);
|
||||||
|
break;
|
||||||
|
case "Empty": if (string.IsNullOrEmpty(attrs[0])) value = string.Empty; found = true; break;
|
||||||
|
default: return new(false, string.Empty, "PROCESSING_UNKNOWN_FUNCTION:" + name);
|
||||||
|
}
|
||||||
|
return mode == "interrupt" && !found ? new(false, string.Empty, "PROCESSING_ALIAS_NOT_FOUND:" + field) : new(true, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Resolve(string? value, IReadOnlyDictionary<string, string> variables, IReadOnlyDictionary<string, string?> fields)
|
||||||
|
{
|
||||||
|
if (value is null) return null;
|
||||||
|
if (value.Length >= 2 && value[0] == '%' && value[^1] == '%') return variables.TryGetValue(value[1..^1], out var resolved) ? resolved : null;
|
||||||
|
return fields.TryGetValue(value, out var field) ? field ?? string.Empty : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineNumberAnnotationSettingsLoader
|
||||||
|
{
|
||||||
|
public LineNumberAnnotationSettings Load(string xml)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(xml);
|
||||||
|
var root = XElement.Parse(xml);
|
||||||
|
if (root.Name.LocalName != "root") throw new InvalidDataException("SETTINGS_ROOT_INVALID");
|
||||||
|
var nullOrEmpty = root.Element("nullOrEmpty")?.Value;
|
||||||
|
if (nullOrEmpty is null) throw new InvalidDataException("SETTINGS_INVALID:10");
|
||||||
|
var annotations = root.Elements("annotation").Select(ParseAnnotation).ToArray();
|
||||||
|
if (annotations.Length == 0) throw new InvalidDataException("SETTINGS_INVALID:11");
|
||||||
|
return new LineNumberAnnotationSettings { NullOrEmpty = nullOrEmpty, Annotations = annotations };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberAnnotationDefinition ParseAnnotation(XElement annotation)
|
||||||
|
{
|
||||||
|
var nickname = annotation.Element("nickname")?.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(nickname)) throw new InvalidDataException("SETTINGS_INVALID:20");
|
||||||
|
var tags = annotation.Elements("tag").Select(ParseTag).ToArray();
|
||||||
|
if (tags.Length == 0) throw new InvalidDataException("SETTINGS_INVALID:21");
|
||||||
|
var variables = annotation.Elements("set").Select(x => new KeyValuePair<string, string>(x.Element("key")?.Value ?? string.Empty, x.Element("value")?.Value ?? string.Empty)).ToArray();
|
||||||
|
if (variables.Length == 0) throw new InvalidDataException("SETTINGS_INVALID:22");
|
||||||
|
if (variables.Any(x => string.IsNullOrWhiteSpace(x.Key))) throw new InvalidDataException("SETTINGS_INVALID:40");
|
||||||
|
return new LineNumberAnnotationDefinition { Nickname = nickname, Variables = variables.ToDictionary(x => x.Key, x => x.Value, StringComparer.Ordinal), Tags = tags };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberAnnotationTag ParseTag(XElement tag)
|
||||||
|
{
|
||||||
|
var divider = tag.Element("devider")?.Value;
|
||||||
|
var saver = tag.Element("saver")?.Value;
|
||||||
|
var field = tag.Element("field")?.Value;
|
||||||
|
var ignore = tag.Element("ignore")?.Value;
|
||||||
|
var processing = tag.Element("processing")?.Value;
|
||||||
|
if (divider is null) throw new InvalidDataException("SETTINGS_INVALID:30");
|
||||||
|
if (saver is null) throw new InvalidDataException("SETTINGS_INVALID:31");
|
||||||
|
if (string.IsNullOrWhiteSpace(field)) throw new InvalidDataException("SETTINGS_INVALID:32");
|
||||||
|
if (ignore is null) throw new InvalidDataException("SETTINGS_INVALID:33");
|
||||||
|
if (processing is null) throw new InvalidDataException("SETTINGS_INVALID:34");
|
||||||
|
return new LineNumberAnnotationTag { Field = field, Divider = divider, SaveWhenEmpty = string.IsNullOrEmpty(saver), IgnoreValue = ignore, ProcessingXml = processing };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineNumberCommandPlanner(ILineNumberAnnotationFormatter formatter)
|
||||||
|
{
|
||||||
|
public LineNumberCommandPlan Build(LineNumberAnnotationDefinition definition, IEnumerable<LineNumberCandidate> candidates, IEnumerable<ExistingLineNumberAnnotation> existing)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var indexed = existing.GroupBy(x => x.SourceStableId, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var result = new List<LineNumberCommandOperation>();
|
||||||
|
foreach (var candidate in candidates.OrderBy(x => x.StableId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
if (!seen.Add(candidate.StableId)) { result.Add(new(candidate.StableId, AnnotationOperationKind.Conflict, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, ["DUPLICATE_CANDIDATE"])); continue; }
|
||||||
|
var text = formatter.Format(definition, candidate.Properties);
|
||||||
|
if (!text.Success) { result.Add(new(candidate.StableId, AnnotationOperationKind.Skip, null, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, [text.ErrorCode ?? "FORMAT_FAILED"])); continue; }
|
||||||
|
result.Add(new(candidate.StableId, indexed.ContainsKey(candidate.StableId) ? AnnotationOperationKind.Update : AnnotationOperationKind.Create, text.Text, candidate.Position, candidate.Rotation, candidate.Layer, candidate.TextStyle, Array.Empty<string>()));
|
||||||
|
}
|
||||||
|
return new(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<LineNumberApplyPreflight> PrepareApply(LineNumberCommandPlan plan) => plan.Operations
|
||||||
|
.Where(x => x.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update)
|
||||||
|
.OrderBy(x => x.StableId, StringComparer.Ordinal)
|
||||||
|
.Select((x, index) => new LineNumberApplyPreflight(x.StableId, index + 1, x.Text!, x.Position, x.Rotation, x.Layer, x.TextStyle)).ToArray();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineNumberCommandPreviewService(LineNumberCommandPlanner planner, LineNumberFieldResolver fieldResolver)
|
||||||
|
{
|
||||||
|
public LineNumberPreviewResult Preview(LineNumberAnnotationDefinition definition, IEnumerable<LineNumberCandidate> candidates, IEnumerable<ExistingLineNumberAnnotation> existing)
|
||||||
|
{
|
||||||
|
var resolved = new List<LineNumberCandidate>();
|
||||||
|
foreach (var candidate in candidates.OrderBy(item => item.StableId, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var fields = definition.Tags.Select(tag => tag.Field).Where(field => !string.IsNullOrWhiteSpace(field));
|
||||||
|
var properties = fieldResolver.Resolve(candidate.RowId, fields);
|
||||||
|
if (!properties.Success) return new(false, null, Array.Empty<LineNumberApplyPreflight>(), properties.FailureCode);
|
||||||
|
resolved.Add(candidate with { Properties = properties.Values });
|
||||||
|
}
|
||||||
|
var plan = planner.Build(definition, resolved, existing);
|
||||||
|
return new(true, plan, LineNumberCommandPlanner.PrepareApply(plan));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineNumberConnectionPlanner
|
||||||
|
{
|
||||||
|
public IReadOnlyList<LineConnectionGroup> Build(IEnumerable<LineConnectionCandidate> candidates, double tolerance)
|
||||||
|
{
|
||||||
|
if (tolerance < 0 || double.IsNaN(tolerance) || double.IsInfinity(tolerance)) throw new ArgumentOutOfRangeException(nameof(tolerance));
|
||||||
|
return candidates
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value.LineNumber))
|
||||||
|
.OrderBy(value => value.StableId, StringComparer.Ordinal)
|
||||||
|
.GroupBy(value => value.LineNumber, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => BuildGroup(group.Key, group.ToArray(), tolerance))
|
||||||
|
.Where(group => group is not null)
|
||||||
|
.Cast<LineConnectionGroup>()
|
||||||
|
.OrderBy(group => group.LineNumber, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineConnectionGroup? BuildGroup(string number, LineConnectionCandidate[] members, double tolerance)
|
||||||
|
{
|
||||||
|
if (members.Length < 2) return null;
|
||||||
|
var connected = members.Where(first => members.Any(second => !ReferenceEquals(first, second) && Connected(first, second, tolerance))).OrderBy(value => value.StableId, StringComparer.Ordinal).ToArray();
|
||||||
|
if (connected.Length < 2) return null;
|
||||||
|
var anchor = connected[0];
|
||||||
|
return new(number, connected, anchor.Position, anchor.Rotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Connected(LineConnectionCandidate first, LineConnectionCandidate second, double tolerance)
|
||||||
|
{
|
||||||
|
if (Distance(first.FirstEndpoint, second.FirstEndpoint) <= tolerance || Distance(first.FirstEndpoint, second.SecondEndpoint) <= tolerance || Distance(first.SecondEndpoint, second.FirstEndpoint) <= tolerance || Distance(first.SecondEndpoint, second.SecondEndpoint) <= tolerance)
|
||||||
|
return true;
|
||||||
|
return ExtentsIntersectionClassifier.Classify(first.Bounds, second.Bounds) is not ExtentsIntersectionType.DoNotIntersect;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Distance(Point3Dto first, Point3Dto second)
|
||||||
|
{
|
||||||
|
var x = first.X - second.X; var y = first.Y - second.Y; var z = first.Z - second.Z;
|
||||||
|
return Math.Sqrt(x * x + y * y + z * z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces the canonical, fixture-bound payload for the legacy line-number
|
||||||
|
/// editing commands. The command-specific Apply permit signs this value so a
|
||||||
|
/// controlled session cannot turn an arbitrary selected block or prompted
|
||||||
|
/// value into a write merely by switching runtime mode to Apply.
|
||||||
|
/// </summary>
|
||||||
|
public static class LineNumberEditApplyPlan
|
||||||
|
{
|
||||||
|
public static string ComputeMutationHash(string commandName, string selectedObjectHandle, IReadOnlyList<LineNumberRowMutation> mutations)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(commandName)) throw new ArgumentException("LINE_NUMBER_EDIT_COMMAND_REQUIRED", nameof(commandName));
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedObjectHandle)) throw new ArgumentException("LINE_NUMBER_EDIT_SELECTION_REQUIRED", nameof(selectedObjectHandle));
|
||||||
|
ArgumentNullException.ThrowIfNull(mutations);
|
||||||
|
if (mutations.Count == 0) throw new ArgumentException("LINE_NUMBER_EDIT_MUTATIONS_REQUIRED", nameof(mutations));
|
||||||
|
|
||||||
|
var canonical = new StringBuilder()
|
||||||
|
.Append(commandName).Append('|')
|
||||||
|
.Append(selectedObjectHandle).Append('\n');
|
||||||
|
foreach (var mutation in mutations.OrderBy(value => value.SourceStableId, StringComparer.Ordinal)
|
||||||
|
.ThenBy(value => value.PreviousMarkerNumber)
|
||||||
|
.ThenBy(value => value.MarkerNumber))
|
||||||
|
{
|
||||||
|
canonical.Append(mutation.SourceStableId).Append('|')
|
||||||
|
.Append(mutation.PreviousMarkerNumber).Append('|')
|
||||||
|
.Append(mutation.MarkerNumber).Append('|')
|
||||||
|
.Append(mutation.Text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace("\r", "\n", StringComparison.Ordinal)).Append('|');
|
||||||
|
if (mutation.MarkerAttributePosition is { } position)
|
||||||
|
canonical.Append(position.X.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append(',')
|
||||||
|
.Append(position.Y.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append(',')
|
||||||
|
.Append(position.Z.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
canonical.Append('\n');
|
||||||
|
}
|
||||||
|
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(canonical.ToString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class LineNumberEditPlanner
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<LineNumberRowMutation> Renumber(IEnumerable<LineNumberOwnedRow> rows, int newStart)
|
||||||
|
{
|
||||||
|
if (newStart <= 0) throw new ArgumentOutOfRangeException(nameof(newStart), "LINE_NUMBER_START_MUST_BE_POSITIVE");
|
||||||
|
var ordered = rows.OrderBy(value => value.MarkerNumber).ThenBy(value => value.SourceStableId, StringComparer.Ordinal).ToArray();
|
||||||
|
if (ordered.Length == 0) throw new ArgumentException("LINE_NUMBER_LIST_EMPTY", nameof(rows));
|
||||||
|
if (ordered.Select(value => value.MarkerNumber).Distinct().Count() != ordered.Length) throw new InvalidOperationException("LINE_NUMBER_LIST_DUPLICATE_MARKER_NUMBER");
|
||||||
|
return ordered.Select((value, index) => new LineNumberRowMutation(value.SourceStableId, value.MarkerNumber, checked(newStart + index), value.Text, null)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LineNumberRowMutation EditText(LineNumberOwnedRow row, string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text)) throw new ArgumentException("LINE_NUMBER_TEXT_EMPTY", nameof(text));
|
||||||
|
return new(row.SourceStableId, row.MarkerNumber, row.MarkerNumber, text, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LineNumberRowMutation ResetMarkerAttributePosition(LineNumberOwnedRow row, Point2Dto configuredPosition) =>
|
||||||
|
new(row.SourceStableId, row.MarkerNumber, row.MarkerNumber, row.Text, new(configuredPosition.X, configuredPosition.Y));
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class LineNumberFieldResolver(IDataLinksPropertyReader reader)
|
||||||
|
{
|
||||||
|
// Source-backed Convert_field_to_value port. The adapter supplies a
|
||||||
|
// deterministic, read-only flattened key set: Table.Column and Column.
|
||||||
|
public LineNumberFieldResolution Resolve(long rowId, IEnumerable<string> fields)
|
||||||
|
{
|
||||||
|
var read = reader.ReadProperties(rowId);
|
||||||
|
if (!read.Success) return new(false, new Dictionary<string, string?>(), read.FailureCode ?? "DATALINKS_READ_FAILED");
|
||||||
|
var values = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var field in fields.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(field)) return new(false, values, "LINE_NUMBER_FIELD_EMPTY");
|
||||||
|
var dot = field.IndexOf('.', StringComparison.Ordinal);
|
||||||
|
if (dot <= 0 || dot == field.Length - 1) return new(false, values, "LINE_NUMBER_FIELD_INVALID:" + field);
|
||||||
|
if (!read.Properties.TryGetValue(field, out var value) && !read.Properties.TryGetValue(field[(dot + 1)..], out value)) return new(false, values, "LINE_NUMBER_COLUMN_MISSING:" + field);
|
||||||
|
values[field] = value ?? string.Empty;
|
||||||
|
}
|
||||||
|
return new(true, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public static class LineNumberListPlanner
|
||||||
|
{
|
||||||
|
public static LineNumberListApplyPreflight Build(AnnotationPoint position, double rotation, IEnumerable<LineNumberApplyPreflight> rows)
|
||||||
|
{
|
||||||
|
var ordered = rows.OrderBy(row => row.MarkerNumber).ToArray();
|
||||||
|
if (ordered.Length == 0) throw new ArgumentException("LINE_NUMBER_LIST_EMPTY", nameof(rows));
|
||||||
|
if (ordered.Any(row => string.IsNullOrWhiteSpace(row.StableId) || string.IsNullOrWhiteSpace(row.Text))) throw new ArgumentException("LINE_NUMBER_LIST_ROW_INVALID", nameof(rows));
|
||||||
|
if (ordered.Select(row => row.MarkerNumber).Distinct().Count() != ordered.Length) throw new ArgumentException("LINE_NUMBER_LIST_DUPLICATE_MARKER_NUMBER", nameof(rows));
|
||||||
|
var key = string.Join("|", ordered.Select(row => $"{row.MarkerNumber}:{row.StableId}"));
|
||||||
|
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(key))).ToLowerInvariant();
|
||||||
|
return new($"ln-list:{digest}", position, rotation, ordered);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public enum ManualAnnotationKind
|
||||||
|
{
|
||||||
|
LineNumber,
|
||||||
|
ValveOrNozzle,
|
||||||
|
FlowDirection,
|
||||||
|
InstrumentPosition,
|
||||||
|
SectionElevation,
|
||||||
|
InstrumentConnection,
|
||||||
|
AutoCadBlock,
|
||||||
|
PlatformElevation,
|
||||||
|
OtherElevation
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ManualAnnotationCommandRouter
|
||||||
|
{
|
||||||
|
public static string Resolve(ManualAnnotationKind kind) => kind switch
|
||||||
|
{
|
||||||
|
ManualAnnotationKind.LineNumber => "PXP-LN",
|
||||||
|
ManualAnnotationKind.FlowDirection => "PXP-OA",
|
||||||
|
ManualAnnotationKind.SectionElevation or ManualAnnotationKind.PlatformElevation or ManualAnnotationKind.OtherElevation => "PXP-IE",
|
||||||
|
ManualAnnotationKind.AutoCadBlock => "PXP-MAB",
|
||||||
|
ManualAnnotationKind.ValveOrNozzle or ManualAnnotationKind.InstrumentPosition or ManualAnnotationKind.InstrumentConnection => "PXP-AAV",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown manual annotation kind.")
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed record ManualAnnotationState(string Kind, ManualDatumMode DatumMode, double DatumOffset)
|
||||||
|
{
|
||||||
|
public string Serialize() => string.Join("|", Kind, DatumMode, DatumOffset.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
|
public static ManualAnnotationState? Deserialize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||||
|
var fields = value.Split('|');
|
||||||
|
if (fields.Length != 3 || !Enum.TryParse<ManualDatumMode>(fields[1], true, out var mode) ||
|
||||||
|
!double.TryParse(fields[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var offset)) return null;
|
||||||
|
return new ManualAnnotationState(fields[0], mode, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public enum ManualDatumMode { Absolute, Relative }
|
||||||
|
|
||||||
|
public static class ManualDatumResolver
|
||||||
|
{
|
||||||
|
public static double Resolve(double sourceElevation, ManualDatumMode mode, double offset) =>
|
||||||
|
mode == ManualDatumMode.Absolute ? sourceElevation : sourceElevation + offset;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class OrthoAnnotationClassifier
|
||||||
|
{
|
||||||
|
public OrthoAnnotationClassification Classify(PlantObjectSnapshot source, OrthoAnnotationOptions options)
|
||||||
|
{
|
||||||
|
string Property(params string[] names) => source.Properties.FirstOrDefault(x => names.Any(n => string.Equals(n, x.Key, StringComparison.OrdinalIgnoreCase))).Value?.Trim() ?? string.Empty;
|
||||||
|
var className = source.ObjectClass.Trim();
|
||||||
|
if (className.Equals("Support", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var title = Property("Title"); var text = Normalize(Property("Annotation"));
|
||||||
|
return options.Supports && title.StartsWith("НЕСТАНДАРТНЫЕ", StringComparison.OrdinalIgnoreCase) && text.Length > 0
|
||||||
|
? Ready(source, OrthoAnnotationRepresentation.MultiLeader, text)
|
||||||
|
: Skip(source, "SUPPORT_NOT_NKR_OR_DISABLED");
|
||||||
|
}
|
||||||
|
if (className.Equals("Valve", StringComparison.OrdinalIgnoreCase) || className.Equals("InlineInstrument", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var value = Normalize(className.Equals("Valve", StringComparison.OrdinalIgnoreCase) ? Property("Annotation") : Property("Tag"));
|
||||||
|
if (value.Contains('$')) return options.Instruments ? Ready(source, OrthoAnnotationRepresentation.BubbleBlock, value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "INSTRUMENT_ANNOTATION_DISABLED");
|
||||||
|
return HasAlphaNumeric(value) && options.Valves ? Ready(source, OrthoAnnotationRepresentation.MultiLeader, value) : Skip(source, "VALVE_ANNOTATION_EMPTY_OR_DISABLED");
|
||||||
|
}
|
||||||
|
if (className.Equals("Plug", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var value = Normalize(Property("TieInNumber"));
|
||||||
|
if (!options.Plugs || !HasAlphaNumeric(value)) return Skip(source, "PLUG_ANNOTATION_EMPTY_OR_DISABLED");
|
||||||
|
var representation = value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader;
|
||||||
|
return Ready(source, representation, "Для подключения\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
if (className.Equals("Board", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var value = Normalize(Property("Tag", "Annotation"));
|
||||||
|
return options.Boards && value.Length > 0 ? Ready(source, value.Contains('$') ? OrthoAnnotationRepresentation.BubbleBlock : OrthoAnnotationRepresentation.MultiLeader, "Шкаф КИП для прибора\\Pпоз. " + value.Replace("$", "\\P", StringComparison.Ordinal)) : Skip(source, "BOARD_ANNOTATION_EMPTY_OR_DISABLED");
|
||||||
|
}
|
||||||
|
return Skip(source, "ORTHO_ANNOTATION_CLASS_UNSUPPORTED");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasAlphaNumeric(string value) => Regex.IsMatch(value, "[a-zA-Z0-9]");
|
||||||
|
private static string Normalize(string value) => value.Replace("{nl}", "\\P", StringComparison.Ordinal);
|
||||||
|
private static OrthoAnnotationClassification Ready(PlantObjectSnapshot source, OrthoAnnotationRepresentation representation, string text) => new(source.SnapshotId, representation, text, Array.Empty<string>());
|
||||||
|
private static OrthoAnnotationClassification Skip(PlantObjectSnapshot source, string reason) => new(source.SnapshotId, OrthoAnnotationRepresentation.None, string.Empty, [reason]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class OrthoAnnotationSettingsLoader
|
||||||
|
{
|
||||||
|
public OrthoAnnotationAssets Load(string xml)
|
||||||
|
{
|
||||||
|
var value = XElement.Parse(xml).Element("Annotations") ?? throw Invalid("Annotations");
|
||||||
|
return new(Required(value, "layer"), Required(value, "textstyle"), Required(value, "multileaderstyle"));
|
||||||
|
}
|
||||||
|
private static string Required(XElement value, string name) => value.Attribute(name) is { Value: var result } && !string.IsNullOrWhiteSpace(result) ? result : throw Invalid(name);
|
||||||
|
private static InvalidDataException Invalid(string value) => new("ORTHO_ANNOTATION_SETTINGS_INVALID:" + value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
// v1.0.4.2 identity rule: a selected drawing is matched by its normalized
|
||||||
|
// project-relative path, never by the display/file name alone.
|
||||||
|
public sealed class OrthoBatchPlanner
|
||||||
|
{
|
||||||
|
public OrthoBatchPlan Build(IEnumerable<OrthoBatchDrawing> drawings, IEnumerable<OrthoBatchSelection> selections)
|
||||||
|
{
|
||||||
|
var diagnostics = new List<string>(); var operations = new List<OrthoBatchOperation>();
|
||||||
|
var source = drawings.OrderBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase).ThenBy(x => x.DrawingGuid, StringComparer.Ordinal).ToArray();
|
||||||
|
var duplicateIds = source.GroupBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase).Where(x => x.Count() > 1).Select(x => x.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var selection in selections.OrderBy(x => Normalize(x.RelativePath), StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var relative = Normalize(selection.RelativePath);
|
||||||
|
if (duplicateIds.Contains(relative)) { diagnostics.Add("ORTHO_DRAWING_RELATIVE_PATH_AMBIGUOUS:" + relative); continue; }
|
||||||
|
var drawing = source.SingleOrDefault(x => string.Equals(Normalize(x.RelativePath), relative, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (drawing is null) { diagnostics.Add("ORTHO_DRAWING_NOT_FOUND:" + relative); continue; }
|
||||||
|
foreach (var view in selection.Views.Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
|
||||||
|
if (drawing.Views.Contains(view, StringComparer.OrdinalIgnoreCase)) operations.Add(new(drawing.DrawingGuid, drawing.DisplayName, relative, view));
|
||||||
|
else diagnostics.Add($"ORTHO_VIEW_NOT_FOUND:{relative}:{view}");
|
||||||
|
}
|
||||||
|
return new(operations.OrderBy(x => x.RelativePath, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.ViewName, StringComparer.OrdinalIgnoreCase).ThenBy(x => x.DrawingGuid, StringComparer.Ordinal).ToArray(), diagnostics.OrderBy(x => x, StringComparer.Ordinal).ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string relativePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(relativePath) || Path.IsPathRooted(relativePath)) throw new ArgumentException("ORTHO_RELATIVE_PATH_REQUIRED", nameof(relativePath));
|
||||||
|
var parts = relativePath.Replace('/', '\\').Split('\\', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Any(x => x == "..")) throw new ArgumentException("ORTHO_RELATIVE_PATH_ESCAPES_PROJECT", nameof(relativePath));
|
||||||
|
return string.Join('\\', parts.Where(x => x != "."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class OrthoLayerSettingsLoader
|
||||||
|
{
|
||||||
|
public IReadOnlyList<OrthoLayerRule> Load(string xml)
|
||||||
|
{
|
||||||
|
var root = XElement.Parse(xml).Element("LayersSetup") ?? throw Invalid("LayersSetup");
|
||||||
|
var result = root.Elements("LayerSetup").Select(Parse).ToArray();
|
||||||
|
return result.Length > 0 ? result : throw Invalid("LayerSetup");
|
||||||
|
}
|
||||||
|
private static OrthoLayerRule Parse(XElement value)
|
||||||
|
{
|
||||||
|
var kind = Required(value, "filtertype").ToUpperInvariant() switch { "STATUS" => OrthoLayerFilterKind.Status, "LAYER" => OrthoLayerFilterKind.Layer, _ => OrthoLayerFilterKind.Table };
|
||||||
|
var filter = Required(value, "filtervalue", true); if (filter.Length == 0) filter = "(Undefined)";
|
||||||
|
int? weight = int.TryParse(Required(value, "lineweight", true), out var parsedWeight) ? parsedWeight : null;
|
||||||
|
var color = Required(value, "color", true); short? index = short.TryParse(color, out var parsedIndex) ? parsedIndex : null; byte? red = null, green = null, blue = null;
|
||||||
|
var rgb = color.Split(','); if (rgb.Length == 3 && byte.TryParse(rgb[0], out var r) && byte.TryParse(rgb[1], out var g) && byte.TryParse(rgb[2], out var b)) { red = r; green = g; blue = b; index = null; }
|
||||||
|
return new(kind, filter, weight, Required(value, "linetype", true), index, red, green, blue);
|
||||||
|
}
|
||||||
|
private static string Required(XElement value, string name, bool empty = false) => value.Attribute(name) is { Value: var result } && (empty || !string.IsNullOrWhiteSpace(result)) ? result : throw Invalid(name);
|
||||||
|
private static InvalidDataException Invalid(string field) => new("ORTHO_LAYER_SETTINGS_INVALID:" + field);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
/// <summary>Typed projection of the release Settings.xml sections consumed by production workflows.</summary>
|
||||||
|
public sealed record ProductionSettingsContract(
|
||||||
|
IReadOnlyList<NamedDrawingObject> CopyMissingObjects,
|
||||||
|
IReadOnlyList<LayerSetupRule> LayerSetup,
|
||||||
|
string AnnotationLayer,
|
||||||
|
string AnnotationTextStyle,
|
||||||
|
string AnnotationLeaderStyle,
|
||||||
|
string ConnectionPolylineLayer,
|
||||||
|
string ConnectionTextLayer,
|
||||||
|
string ConnectionTextStyle,
|
||||||
|
string ElevationLayer,
|
||||||
|
string ElevationTextStyle,
|
||||||
|
string SetLayer,
|
||||||
|
double? FixedSupportPlusLength,
|
||||||
|
string? ScriptFolder,
|
||||||
|
LineNumberMarkerSettings? LineNumberMarker,
|
||||||
|
SupportTopViewSettings? SupportTopView)
|
||||||
|
{
|
||||||
|
public LineNumberMarkerAsset? ToLineNumberMarkerAsset()
|
||||||
|
{
|
||||||
|
if (LineNumberMarker is null) return null;
|
||||||
|
var texts = LineNumberMarker.Text is { } text
|
||||||
|
? new[] { new LineNumberTextAsset(text.Layer ?? string.Empty, text.TextStyle ?? string.Empty, text.Size ?? 0, text.TextValue ?? string.Empty, new(text.PositionX ?? 0, text.PositionY ?? 0)) }
|
||||||
|
: Array.Empty<LineNumberTextAsset>();
|
||||||
|
var lines = LineNumberMarker.Line is { } line
|
||||||
|
? new[] { new LineNumberLineAsset(line.Layer ?? string.Empty, new(line.StartX ?? 0, line.StartY ?? 0), new(line.EndX ?? 0, line.EndY ?? 0)) }
|
||||||
|
: Array.Empty<LineNumberLineAsset>();
|
||||||
|
return new(LineNumberMarker.Layer, LineNumberMarker.MaskScaleFactor ?? 0, texts, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ProductionSettingsContract Parse(string xml)
|
||||||
|
{
|
||||||
|
var root = XElement.Parse(xml, LoadOptions.PreserveWhitespace);
|
||||||
|
var copy = root.Element("CopyMissingObjects")?.Elements("Object")
|
||||||
|
.Select(value => new NamedDrawingObject(Required(value, "type"), Required(value, "name")))
|
||||||
|
.ToArray() ?? Array.Empty<NamedDrawingObject>();
|
||||||
|
var layers = root.Element("LayersSetup")?.Elements("LayerSetup")
|
||||||
|
.Select(value => new LayerSetupRule(
|
||||||
|
Required(value, "filtertype"), Required(value, "filtervalue", allowEmpty: true),
|
||||||
|
ParseInt(value, "lineweight"), Required(value, "linetype", allowEmpty: true),
|
||||||
|
Required(value, "color", allowEmpty: true)))
|
||||||
|
.ToArray() ?? Array.Empty<LayerSetupRule>();
|
||||||
|
var annotations = root.Element("Annotations");
|
||||||
|
var connections = root.Element("LinenumberConnections");
|
||||||
|
var elevations = root.Element("Elevations");
|
||||||
|
var setLayer = root.Element("SetLayer")?.Attribute("name")?.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(setLayer)) throw Invalid("SetLayer.name");
|
||||||
|
var plusLength = root.Element("PlusLengthNO")?.Attribute("value")?.Value;
|
||||||
|
double? parsedPlusLength = TryParseLegacyNumber(plusLength, out var plus) ? plus : null;
|
||||||
|
var marker = root.Element("LineNumbers")?.Element("Marker");
|
||||||
|
LineNumberMarkerSettings? parsedMarker = marker is null ? null : new(
|
||||||
|
Required(marker, "layer"),
|
||||||
|
TryParseLegacyNumber(marker.Attribute("maskscalefactor")?.Value, out var mask) ? mask : null,
|
||||||
|
ParseLine(marker.Element("Line")),
|
||||||
|
ParseText(marker.Element("Text")));
|
||||||
|
var supports = root.Element("SupportsInTopView");
|
||||||
|
SupportTopViewSettings? parsedSupports = supports is null ? null : new(
|
||||||
|
supports.Attribute("undefinedsize")?.Value,
|
||||||
|
supports.Elements("Support").Select(value => new SupportRule(
|
||||||
|
Required(value, "scriptname"),
|
||||||
|
TryParseLegacyNumber(value.Attribute("nominaldiameter")?.Value, out var diameter) ? diameter : null,
|
||||||
|
value.Attribute("nominalumit")?.Value,
|
||||||
|
int.TryParse(value.Attribute("lastdigit")?.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var lastDigit) ? lastDigit : null,
|
||||||
|
TryParseLegacyNumber(value.Attribute("scale")?.Value, out var scale) ? scale : null,
|
||||||
|
value.Attribute("selectionvalue")?.Value)).ToArray());
|
||||||
|
return new(
|
||||||
|
copy,
|
||||||
|
layers,
|
||||||
|
Required(annotations, "layer"),
|
||||||
|
Required(annotations, "textstyle"),
|
||||||
|
Required(annotations, "multileaderstyle"),
|
||||||
|
Required(connections, "polylinelayer"),
|
||||||
|
Required(connections, "textlayer"),
|
||||||
|
Required(connections, "textstyle"),
|
||||||
|
Required(elevations, "layer"),
|
||||||
|
Required(elevations, "textstyle"),
|
||||||
|
setLayer,
|
||||||
|
parsedPlusLength,
|
||||||
|
root.Element("ScriptFolder")?.Attribute("path")?.Value,
|
||||||
|
parsedMarker,
|
||||||
|
parsedSupports);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineGeometrySettings? ParseLine(XElement? value) => value is null ? null : new(
|
||||||
|
TryParseLegacyNumber(value.Attribute("startx")?.Value, out var startX) ? startX : null,
|
||||||
|
TryParseLegacyNumber(value.Attribute("starty")?.Value, out var startY) ? startY : null,
|
||||||
|
TryParseLegacyNumber(value.Attribute("endx")?.Value, out var endX) ? endX : null,
|
||||||
|
TryParseLegacyNumber(value.Attribute("endy")?.Value, out var endY) ? endY : null,
|
||||||
|
value.Attribute("layer")?.Value);
|
||||||
|
|
||||||
|
private static TextAnnotationSettings? ParseText(XElement? value) => value is null ? null : new(
|
||||||
|
value.Attribute("layer")?.Value,
|
||||||
|
value.Attribute("textstyle")?.Value,
|
||||||
|
TryParseLegacyNumber(value.Attribute("size")?.Value, out var size) ? size : null,
|
||||||
|
value.Attribute("textvalue")?.Value,
|
||||||
|
TryParseLegacyNumber(value.Attribute("positionx")?.Value, out var positionX) ? positionX : null,
|
||||||
|
TryParseLegacyNumber(value.Attribute("positiony")?.Value, out var positionY) ? positionY : null);
|
||||||
|
|
||||||
|
private static int? ParseInt(XElement value, string name) => int.TryParse(value.Attribute(name)?.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : null;
|
||||||
|
private static bool TryParseLegacyNumber(string? value, out double parsed)
|
||||||
|
{
|
||||||
|
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out parsed)) return true;
|
||||||
|
return double.TryParse(value?.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out parsed);
|
||||||
|
}
|
||||||
|
private static string Required(XElement? value, string name, bool allowEmpty = false) => value?.Attribute(name) is { Value: var result } && (allowEmpty || !string.IsNullOrWhiteSpace(result)) ? result : throw Invalid(name);
|
||||||
|
private static InvalidDataException Invalid(string field) => new("PRODUCTION_SETTINGS_INVALID:" + field);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record NamedDrawingObject(string Type, string Name);
|
||||||
|
public sealed record LayerSetupRule(string FilterType, string FilterValue, int? LineWeight, string LineType, string Color);
|
||||||
|
public sealed record LineNumberMarkerSettings(string Layer, double? MaskScaleFactor, LineGeometrySettings? Line, TextAnnotationSettings? Text);
|
||||||
|
public sealed record LineGeometrySettings(double? StartX, double? StartY, double? EndX, double? EndY, string? Layer);
|
||||||
|
public sealed record TextAnnotationSettings(string? Layer, string? TextStyle, double? Size, string? TextValue, double? PositionX, double? PositionY);
|
||||||
|
public sealed record SupportTopViewSettings(string? UndefinedSize, IReadOnlyList<SupportRule> Rules)
|
||||||
|
{
|
||||||
|
public SupportRule? Resolve(double nominalDiameter, string nominalUnit, string? selectionValue = null) => Rules
|
||||||
|
.Where(rule => rule.NominalDiameter is not null && Math.Abs(rule.NominalDiameter.Value - nominalDiameter) < 0.0001)
|
||||||
|
.Where(rule => string.Equals(rule.NominalUnit, nominalUnit, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(rule => selectionValue is null || string.Equals(rule.SelectionValue, selectionValue, StringComparison.Ordinal))
|
||||||
|
.OrderBy(rule => rule.LastDigit ?? int.MaxValue)
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
public sealed record SupportRule(string ScriptName, double? NominalDiameter, string? NominalUnit, int? LastDigit, double? Scale, string? SelectionValue);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core;
|
||||||
|
|
||||||
|
public sealed class ViewportLayerPlanner
|
||||||
|
{
|
||||||
|
public ViewportLayerPlan Build(string? currentLayer, bool targetLayerExists, string targetLayer = "Defpoints")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(targetLayer)) return new(false, string.Empty, "VIEWPORT_TARGET_LAYER_INVALID");
|
||||||
|
if (!targetLayerExists) return new(false, targetLayer, "VIEWPORT_TARGET_LAYER_MISSING");
|
||||||
|
return new(!string.Equals(currentLayer, targetLayer, StringComparison.OrdinalIgnoreCase), targetLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class AutoAnnotateViewCommand
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new PromptEntityOptions("\nВыберите видовой экран для автоматической аннотации: ");
|
||||||
|
options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
var selected = document.Editor.GetEntity(options); if (selected.Status != PromptStatus.OK) return;
|
||||||
|
var context = new PlantRuntimeContextReader().ReadCurrentContext(document);
|
||||||
|
if (!context.HasPlantProject || !context.IsOrthoDrawing) throw new InvalidOperationException("AUTO_ANNOTATE_VIEW_ORTHO_CONTEXT_REQUIRED");
|
||||||
|
OrthoViewportInfo viewport;
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
{
|
||||||
|
var entity = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
|
||||||
|
var matches = context.Viewports.Where(x => Math.Abs(x.CenterX - entity.CenterPoint.X) < .001 && Math.Abs(x.CenterY - entity.CenterPoint.Y) < .001 && Math.Abs(x.Width - entity.Width) < .001 && Math.Abs(x.Height - entity.Height) < .001).ToArray();
|
||||||
|
viewport = matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "AUTO_ANNOTATE_VIEW_NOT_FOUND" : "AUTO_ANNOTATE_VIEW_AMBIGUOUS");
|
||||||
|
}
|
||||||
|
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||||
|
var classifications = objects.OrderBy(x => x.SnapshotId, StringComparer.Ordinal)
|
||||||
|
.Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray();
|
||||||
|
var permit = string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? FirstLimitedApplyPermitReader.ReadFor(document) : null;
|
||||||
|
var plan = new AnnotationPlanBuilder().Build(new AnnotationPlanningRequest
|
||||||
|
{
|
||||||
|
CommandName = "PXP-AAV", Viewport = viewport, Objects = objects,
|
||||||
|
LimitedApplyTargetSnapshotId = permit?.TargetSnapshotId,
|
||||||
|
LimitedApplyRuleId = permit is null ? null : FirstLimitedApplyPlanner.RuleId
|
||||||
|
});
|
||||||
|
var leaders = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader);
|
||||||
|
var bubbles = classifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock);
|
||||||
|
document.Editor.WriteMessage($"\nPXP-AAV preview: examined={plan.Summary.ObjectsExamined}; create={plan.Summary.CreateCount}; update={plan.Summary.UpdateCount}; leaders={leaders}; bubbles={bubbles}; skip={plan.Summary.SkipCount}; conflicts={plan.Summary.ConflictCount}; PlanId={plan.PlanId}. DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("AutoAnnotateViewPreviewCompleted", commandName: "PXP-AAV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId, ["ObjectsExamined"] = plan.Summary.ObjectsExamined, ["MultiLeaderCandidates"] = leaders, ["BubbleCandidates"] = bubbles });
|
||||||
|
ScriptDrivenFirstApplyAutomation.RecordPxpAav(plan.PlanId, objects.Select(value => value.SnapshotId).OrderBy(value => value, StringComparer.Ordinal).ToArray(), viewport.Id, plan.Summary.CreateCount, plan.Summary.SkipCount, permit is not null, plan.Summary.ObjectsExamined, objects.Count(value => value.MembershipStatus == ViewMembershipStatus.Resolved));
|
||||||
|
if (permit is null) return;
|
||||||
|
if (!string.Equals(permit.ExpectedPlanId, plan.PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("APPLY_PLAN_ID_MISMATCH");
|
||||||
|
var selectedSource = objects.SingleOrDefault(x => string.Equals(x.SnapshotId, permit.TargetSnapshotId, StringComparison.Ordinal)) ?? throw new InvalidOperationException("APPLY_TARGET_NOT_FOUND");
|
||||||
|
var selectedClassification = classifications.Single(x => string.Equals(x.StableId, permit.TargetSnapshotId, StringComparison.Ordinal));
|
||||||
|
if (selectedClassification.Representation != OrthoAnnotationRepresentation.None)
|
||||||
|
{
|
||||||
|
var settingsPath = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(settingsPath) || !File.Exists(settingsPath)) throw new InvalidOperationException("ORTHO_ANNOTATION_CONFIGURATION_REQUIRED");
|
||||||
|
var assets = new OrthoAnnotationSettingsLoader().Load(File.ReadAllText(settingsPath));
|
||||||
|
var result = new OrthoAnnotationWriter().Replace(document, viewport, selectedSource, selectedClassification, assets);
|
||||||
|
document.Editor.WriteMessage("\nPXP-AAV Apply: " + result + "; AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("AutoAnnotateViewApplyCompleted", commandName: "PXP-AAV", resultStatus: result, document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId, ["Representation"] = selectedClassification.Representation.ToString() });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var corePermit = new FirstLimitedApplyPermit { FixtureMarker = FirstLimitedApplyPlanner.FixtureMarker, AllowApply = permit.AllowApply, TargetSnapshotId = permit.TargetSnapshotId, ExpectedPlanId = permit.ExpectedPlanId };
|
||||||
|
var preflight = new FirstLimitedApplyPlanner().Prepare(PRecAnnotExecutionMode.Apply, plan, viewport, objects, corePermit);
|
||||||
|
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document, preflight.Operation, permit.VisibilityState, permit.AnnotationTemplateDwgPath);
|
||||||
|
document.Editor.WriteMessage("\nPXP-AAV Apply: " + outcome + "; AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("AutoAnnotateViewApplyCompleted", commandName: "PXP-AAV", resultStatus: outcome.ToString(), document: document, details: new Dictionary<string, object?> { ["PlanId"] = plan.PlanId });
|
||||||
|
ScriptDrivenFirstApplyAutomation.RecordApplyOutcome(outcome);
|
||||||
|
}
|
||||||
|
catch (Exception exception) { RuntimeDiagnostics.Write("AutoAnnotateViewFailed", "ERROR", commandName: "PXP-AAV", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-AAV: " + exception.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
using Autodesk.AutoCAD.Runtime;
|
||||||
|
|
||||||
|
[assembly: CommandClass(typeof(PRecAnnot.Plant3D2026.EntryPoint))]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed record CompositeAssemblyManifest(string Name, string Sha256, bool Required);
|
||||||
|
internal sealed record CompositeManifest(int SchemaVersion, string Package, string Version, IReadOnlyList<string> Modules, IReadOnlyList<CompositeAssemblyManifest> Assemblies);
|
||||||
|
|
||||||
|
internal static class CompositeModuleManifest
|
||||||
|
{
|
||||||
|
public static CompositeManifest Validate()
|
||||||
|
{
|
||||||
|
var assemblyPath = typeof(EntryPoint).Assembly.Location;
|
||||||
|
var path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(assemblyPath)!, "..", "module-manifest.json"));
|
||||||
|
if (!File.Exists(path)) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_MISSING");
|
||||||
|
var manifest = JsonSerializer.Deserialize<CompositeManifest>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID");
|
||||||
|
// Schema 2 adds the package-wide file manifest while preserving the
|
||||||
|
// required composite module/assembly contract consumed here.
|
||||||
|
if (manifest.SchemaVersion is not (1 or 2) || !string.Equals(manifest.Package, "P_Rec_Annot", StringComparison.Ordinal) || manifest.Modules.Count == 0) throw new InvalidOperationException("COMPOSITE_MODULE_MANIFEST_INVALID");
|
||||||
|
foreach (var item in manifest.Assemblies.Where(value => value.Required))
|
||||||
|
{
|
||||||
|
var file = Path.Combine(Path.GetDirectoryName(assemblyPath)!, item.Name);
|
||||||
|
if (!File.Exists(file)) throw new InvalidOperationException($"COMPOSITE_MODULE_ASSEMBLY_MISSING:{item.Name}");
|
||||||
|
var hash = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(file))).ToLowerInvariant();
|
||||||
|
if (!string.Equals(hash, item.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException($"COMPOSITE_MODULE_HASH_MISMATCH:{item.Name}");
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class ElevationAnnotationWriter
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-AE";
|
||||||
|
|
||||||
|
public IReadOnlyList<ExistingElevationAnnotation> ReadExisting(Document document)
|
||||||
|
{
|
||||||
|
var result = new List<ExistingElevationAnnotation>();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue;
|
||||||
|
var data = reference.GetXDataForApplication(ApplicationName)?.AsArray();
|
||||||
|
if (data is null) continue;
|
||||||
|
var strings = data.Where(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(x => x.Value?.ToString() ?? string.Empty).ToArray();
|
||||||
|
var source = Value(strings, "SOURCE"); var text = Value(strings, "ANNOTATIONTEXT");
|
||||||
|
if (source is null || text is null) continue;
|
||||||
|
result.Add(new ExistingElevationAnnotation(reference.Handle.ToString(), source, text, new AnnotationPoint(reference.Position.X, reference.Position.Y, reference.Position.Z)));
|
||||||
|
}
|
||||||
|
return result.OrderBy(x => x.SourceStableId, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElevationApplyResult Apply(Document document, IReadOnlyList<ElevationAnnotationOperation> operations, ElevationAssets assets, bool manual = false)
|
||||||
|
{
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(document.Database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
if (!blocks.Has(assets.BlockName)) throw new InvalidOperationException("ELEVATION_BLOCK_MISSING:" + assets.BlockName);
|
||||||
|
if (!layers.Has(assets.Layer)) throw new InvalidOperationException("ELEVATION_LAYER_MISSING:" + assets.Layer);
|
||||||
|
if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("ELEVATION_TEXT_STYLE_MISSING:" + assets.TextStyle);
|
||||||
|
EnsureRegApp(transaction, document.Database);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BlockName], OpenMode.ForRead);
|
||||||
|
var owned = paper.Cast<ObjectId>().Select(id => transaction.GetObject(id, OpenMode.ForRead)).OfType<BlockReference>()
|
||||||
|
.Select(reference => (reference, source: Source(reference))).Where(x => x.source is not null)
|
||||||
|
.GroupBy(x => x.source!, StringComparer.OrdinalIgnoreCase).ToDictionary(x => x.Key, x => x.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var created = 0; var updated = 0; var unchanged = 0;
|
||||||
|
foreach (var operation in operations.Where(x => x.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update))
|
||||||
|
{
|
||||||
|
if (operation.Text is null) continue;
|
||||||
|
var matches = owned.GetValueOrDefault(operation.StableId, []);
|
||||||
|
BlockReference reference;
|
||||||
|
if (matches.Length == 0)
|
||||||
|
{
|
||||||
|
reference = new BlockReference(ToPoint(operation.Position), definition.ObjectId) { LayerId = layers[assets.Layer], Rotation = operation.Rotation };
|
||||||
|
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true); created++;
|
||||||
|
AddAttributes(transaction, definition, reference, operation.Text, styles[assets.TextStyle]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reference = matches[0].reference; reference.UpgradeOpen();
|
||||||
|
reference.Position = ToPoint(operation.Position); reference.Rotation = operation.Rotation; reference.LayerId = layers[assets.Layer]; updated++;
|
||||||
|
foreach (ObjectId attributeId in reference.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(attributeId, OpenMode.ForWrite);
|
||||||
|
attribute.TextString = operation.Text + (attribute.Tag.Contains('*') ? "*" : string.Empty); attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reference.XData = Ownership(operation.StableId, operation.Text, manual);
|
||||||
|
ApplyLegacyDynamicProperties(reference);
|
||||||
|
}
|
||||||
|
unchanged += operations.Count(x => x.Kind == AnnotationOperationKind.Skip && x.Reasons.Contains("ALREADY_CURRENT"));
|
||||||
|
transaction.Commit();
|
||||||
|
return new(created, updated, unchanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddAttributes(Transaction transaction, BlockTableRecord definition, BlockReference reference, string text, ObjectId style)
|
||||||
|
{
|
||||||
|
foreach (ObjectId id in definition)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue;
|
||||||
|
using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(source, reference.BlockTransform);
|
||||||
|
attribute.TextString = text + (attribute.Tag.Contains('*') ? "*" : string.Empty); attribute.TextStyleId = style;
|
||||||
|
attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyLegacyDynamicProperties(BlockReference reference)
|
||||||
|
{
|
||||||
|
if (!reference.IsDynamicBlock) return;
|
||||||
|
foreach (DynamicBlockReferenceProperty property in reference.DynamicBlockReferencePropertyCollection)
|
||||||
|
if (string.Equals(property.PropertyName, "V1", StringComparison.OrdinalIgnoreCase) && !property.ReadOnly) property.Value = "От объекта";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Source(BlockReference reference)
|
||||||
|
{
|
||||||
|
var strings = reference.GetXDataForApplication(ApplicationName)?.AsArray().Where(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(x => x.Value?.ToString() ?? string.Empty).ToArray();
|
||||||
|
return strings is null ? null : Value(strings, "SOURCE");
|
||||||
|
}
|
||||||
|
private static string? Value(string[] values, string key) { var index = Array.FindIndex(values, x => string.Equals(x, key, StringComparison.Ordinal)); return index >= 0 && index + 1 < values.Length ? values[index + 1] : null; }
|
||||||
|
private static Point3d ToPoint(AnnotationPoint point) => new(point.X, point.Y, point.Z);
|
||||||
|
private static ResultBuffer Ownership(string source, string text, bool manual) => new(new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source), new TypedValue((int)DxfCode.ExtendedDataAsciiString, "ANNOTATIONTEXT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, text), new TypedValue((int)DxfCode.ExtendedDataAsciiString, manual ? "MANUAL" : "AUTO"));
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database) { var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead); if (table.Has(ApplicationName)) return; table.UpgradeOpen(); using var record = new RegAppTableRecord { Name = ApplicationName }; table.Add(record); transaction.AddNewlyCreatedDBObject(record, true); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed record ElevationApplyPermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ElevationApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string Marker = "P_REC_ANNOT_TEST_FIXTURE=ELEVATION_APPLY";
|
||||||
|
|
||||||
|
public static ElevationApplyPermit ReadAndValidate(Document document)
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_ELEVATION_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("ELEVATION_APPLY_PERMIT_REQUIRED");
|
||||||
|
var permit = JsonSerializer.Deserialize<ElevationApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||||
|
?? throw new InvalidOperationException("ELEVATION_APPLY_PERMIT_INVALID");
|
||||||
|
if (!permit.AllowApply || !Path.IsPathFullyQualified(permit.FixtureMarkerPath) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), Marker, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("ELEVATION_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ELEVATION_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
return permit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class ElevationCommands
|
||||||
|
{
|
||||||
|
public static void RunAutomatic() => Run("PXP-AE", manual: false);
|
||||||
|
public static void RunManual() => Run("PXP-IE", manual: true);
|
||||||
|
|
||||||
|
private static void Run(string command, bool manual)
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assets = ElevationRuntimeConfiguration.Read();
|
||||||
|
var datum = PromptDatum(document.Editor);
|
||||||
|
if (datum is null) return;
|
||||||
|
if (manual) RunManual(document, assets, datum.Value);
|
||||||
|
else RunAutomatic(document, assets, datum.Value);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("ElevationCommandFailed", "ERROR", commandName: command, resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
document.Editor.WriteMessage($"\n{command}: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunAutomatic(Document document, ElevationAssets assets, double datum)
|
||||||
|
{
|
||||||
|
var selected = SelectViewport(document.Editor);
|
||||||
|
if (selected.Status != PromptStatus.OK) return;
|
||||||
|
OrthoViewportInfo viewport;
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
viewport = ResolveViewport(document, (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead));
|
||||||
|
var candidates = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None)
|
||||||
|
.Where(value => value.MembershipStatus == ViewMembershipStatus.Resolved && value.Geometry.PaperPosition is not null && value.Geometry.Elevation.HasValue)
|
||||||
|
.Select(value => new ElevationCandidate(value.SnapshotId, value.Geometry.Elevation!.Value, datum,
|
||||||
|
new AnnotationPoint(value.Geometry.PaperPosition!.Value.X, value.Geometry.PaperPosition.Value.Y), value.Geometry.Rotation ?? 0, assets.Layer, assets.TextStyle))
|
||||||
|
.ToArray();
|
||||||
|
var writer = new ElevationAnnotationWriter();
|
||||||
|
var plan = new ElevationAnnotationPlanner().Build(candidates, writer.ReadExisting(document));
|
||||||
|
WritePreview(document, "PXP-AE", plan);
|
||||||
|
if (!IsApply()) return;
|
||||||
|
ElevationApplyPermitReader.ReadAndValidate(document);
|
||||||
|
var result = writer.Apply(document, plan.Operations, assets);
|
||||||
|
WriteApply(document, "PXP-AE", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunManual(Document document, ElevationAssets assets, double datum)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var point = document.Editor.GetPoint("\nУкажите положение высотной отметки: ");
|
||||||
|
if (point.Status != PromptStatus.OK) return;
|
||||||
|
Viewport? viewport = null;
|
||||||
|
Point3d modelPoint = default;
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
{
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
viewport = paper.Cast<ObjectId>().Select(id => transaction.GetObject(id, OpenMode.ForRead)).OfType<Viewport>()
|
||||||
|
.Where(value => value.Number > 1 && !value.PerspectiveOn && Contains(value, point.Value))
|
||||||
|
.OrderBy(value => value.Number).FirstOrDefault();
|
||||||
|
if (viewport is not null) modelPoint = point.Value.TransformBy(ModelToPaper(viewport).Inverse());
|
||||||
|
}
|
||||||
|
if (viewport is null) { document.Editor.WriteMessage("\nТочка должна находиться внутри ортографического видового экрана."); continue; }
|
||||||
|
var stableId = $"manual:{viewport.Handle}:{point.Value.X:R}:{point.Value.Y:R}";
|
||||||
|
var candidate = new ElevationCandidate(stableId, modelPoint.Z, datum, new AnnotationPoint(point.Value.X, point.Value.Y), 0, assets.Layer, assets.TextStyle);
|
||||||
|
var plan = new ElevationAnnotationPlanner().Build([candidate], Array.Empty<ExistingElevationAnnotation>());
|
||||||
|
WritePreview(document, "PXP-IE", plan);
|
||||||
|
if (!IsApply()) return;
|
||||||
|
ElevationApplyPermitReader.ReadAndValidate(document);
|
||||||
|
var result = new ElevationAnnotationWriter().Apply(document, plan.Operations, assets, manual: true);
|
||||||
|
WriteApply(document, "PXP-IE", result);
|
||||||
|
// Legacy PXP-IE repeats until Escape; every committed iteration is
|
||||||
|
// a native AutoCAD transaction and therefore participates in UNDO.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double? PromptDatum(Editor editor)
|
||||||
|
{
|
||||||
|
var options = new PromptDoubleOptions("\nСмещение нуля МСК (в мм) <0>: ") { AllowNone = true, DefaultValue = 0, UseDefaultValue = true };
|
||||||
|
var result = editor.GetDouble(options);
|
||||||
|
return result.Status == PromptStatus.OK ? result.Value : result.Status == PromptStatus.None ? 0 : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PromptEntityResult SelectViewport(Editor editor)
|
||||||
|
{
|
||||||
|
var options = new PromptEntityOptions("\nВыберите видовой экран: "); options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
return editor.GetEntity(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoViewportInfo ResolveViewport(Document document, Viewport selected)
|
||||||
|
{
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ELEVATION_ORTHO_PROJECT_UNAVAILABLE");
|
||||||
|
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
|
||||||
|
.Where(value => Math.Abs(value.CenterX - selected.CenterPoint.X) < 0.001 && Math.Abs(value.CenterY - selected.CenterPoint.Y) < 0.001 && Math.Abs(value.Width - selected.Width) < 0.001 && Math.Abs(value.Height - selected.Height) < 0.001).ToArray();
|
||||||
|
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ELEVATION_ORTHO_VIEW_NOT_FOUND" : "ELEVATION_ORTHO_VIEW_AMBIGUOUS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Matrix3d ModelToPaper(Viewport viewport)
|
||||||
|
{
|
||||||
|
var center = new Point3d(viewport.ViewCenter.X, viewport.ViewCenter.Y, 0);
|
||||||
|
return Matrix3d.Displacement(new Vector3d(viewport.CenterPoint.X - center.X, viewport.CenterPoint.Y - center.Y, 0))
|
||||||
|
* Matrix3d.Scaling(viewport.CustomScale, center)
|
||||||
|
* Matrix3d.Rotation(viewport.TwistAngle, Vector3d.ZAxis, Point3d.Origin)
|
||||||
|
* Matrix3d.WorldToPlane(new Plane(viewport.ViewTarget, viewport.ViewDirection));
|
||||||
|
}
|
||||||
|
private static bool Contains(Viewport viewport, Point3d point) => viewport.GeometricExtents.MinPoint.X <= point.X && viewport.GeometricExtents.MaxPoint.X >= point.X && viewport.GeometricExtents.MinPoint.Y <= point.Y && viewport.GeometricExtents.MaxPoint.Y >= point.Y;
|
||||||
|
private static bool IsApply() => string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
|
private static void WritePreview(Document document, string command, ElevationAnnotationPlan plan) { document.Editor.WriteMessage($"\n{command} preview: create={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Create)}; update={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Update)}; skip={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Skip)}; conflict={plan.Operations.Count(x => x.Kind == AnnotationOperationKind.Conflict)}. DWG не изменён."); RuntimeDiagnostics.Write("ElevationPreviewCompleted", commandName: command, resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["OperationCount"] = plan.Operations.Count }); }
|
||||||
|
private static void WriteApply(Document document, string command, ElevationApplyResult result) { document.Editor.WriteMessage($"\n{command} Apply: created={result.Created}; updated={result.Updated}; already-current={result.Unchanged}."); RuntimeDiagnostics.Write("ElevationApplyCompleted", commandName: command, resultStatus: "Applied", document: document, details: new Dictionary<string, object?> { ["Created"] = result.Created, ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged }); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class ElevationRuntimeConfiguration
|
||||||
|
{
|
||||||
|
public static ElevationAssets Read()
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("ELEVATION_CONFIGURATION_REQUIRED:P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||||
|
return new ElevationSettingsLoader().Load(File.ReadAllText(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
using Autodesk.AutoCAD.Runtime;
|
using Autodesk.AutoCAD.Runtime;
|
||||||
using Autodesk.Windows;
|
using Autodesk.Windows;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
using PRecAnnot.Abstractions;
|
using PRecAnnot.Abstractions;
|
||||||
using PRecAnnot.Core;
|
using PRecAnnot.Core;
|
||||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
@@ -13,7 +15,11 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
public void Initialize()
|
public void Initialize()
|
||||||
{
|
{
|
||||||
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
RuntimeDiagnostics.Write("AssemblyInitializeStarted");
|
||||||
|
var manifest = CompositeModuleManifest.Validate();
|
||||||
|
RuntimeDiagnostics.Write("CompositeModulesValidated", resultStatus: "Validated", details: new Dictionary<string, object?> { ["PackageVersion"] = manifest.Version, ["ModuleCount"] = manifest.Modules.Count });
|
||||||
RibbonIntegration.Register();
|
RibbonIntegration.Register();
|
||||||
|
SwapAttributesContextMenu.Attach();
|
||||||
|
ManualAnnotationPalette.Initialize();
|
||||||
// Test automation is driven only by the explicitly generated /b script.
|
// Test automation is driven only by the explicitly generated /b script.
|
||||||
// Initialize remains production-equivalent and must not orchestrate commands.
|
// Initialize remains production-equivalent and must not orchestrate commands.
|
||||||
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
RuntimeDiagnostics.Write("AssemblyInitializeCompleted");
|
||||||
@@ -23,6 +29,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
{
|
{
|
||||||
RuntimeDiagnostics.Write("AssemblyTerminateStarted");
|
RuntimeDiagnostics.Write("AssemblyTerminateStarted");
|
||||||
RibbonIntegration.Terminate();
|
RibbonIntegration.Terminate();
|
||||||
|
SwapAttributesContextMenu.Detach();
|
||||||
|
ManualAnnotationPalette.Terminate();
|
||||||
RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
|
RuntimeDiagnostics.Write("AssemblyTerminateCompleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,11 +38,33 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT");
|
public void PrimaryCommand() => RunDiagnostic("PHP_REC_ANNOT");
|
||||||
|
|
||||||
[CommandMethod("PXP-AAV")]
|
[CommandMethod("PXP-AAV")]
|
||||||
public void AutoAnnotateViewCommand() => RunDiagnostic("PXP-AAV");
|
public void AutoAnnotateViewCommand() => PRecAnnot.Plant3D2026.AutoAnnotateViewCommand.Run();
|
||||||
|
|
||||||
[CommandMethod("PXP-OA")]
|
[CommandMethod("PXP-OA")]
|
||||||
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
|
public void OrthoAnnotateCommand() => RunDiagnostic("PXP-OA");
|
||||||
|
|
||||||
|
[CommandMethod("PXP-LN")] public void AnnotateLineNumbersCommand() => RunLineNumberPreview();
|
||||||
|
[CommandMethod("PXP-ULN")] public void UpdateLineNumbersCommand() => LineNumberRefreshCommand.Run();
|
||||||
|
[CommandMethod("PXP-ESN")] public void EditLineNumberStartCommand() => LineNumberEditCommands.Renumber();
|
||||||
|
[CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText();
|
||||||
|
[CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition();
|
||||||
|
[CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run();
|
||||||
|
[CommandMethod("PXP-AE")] public void AutoElevationCommand() => ElevationCommands.RunAutomatic();
|
||||||
|
[CommandMethod("PXP-IE")] public void InsertElevationCommand() => ElevationCommands.RunManual();
|
||||||
|
[CommandMethod("PXP-COV")] public void CheckOrthoViewportCommand() => OrthoMaintenanceCommands.CheckViewport();
|
||||||
|
[CommandMethod("PXP-UOB")] public void UpdateOrthoViewBlocksCommand() => PRecAnnot.Plant3D2026.UpdateOrthoViewBlocksCommand.Run();
|
||||||
|
[CommandMethod("PXP-RA", CommandFlags.Session)] public void RemoveOrthoAnnotationsCommand() => OrthoAnnotationRemovalCommand.Run();
|
||||||
|
[CommandMethod("PXP-OSI", CommandFlags.Session)] public void ShowOrthoSourceInModelCommand() => OrthoSourceNavigationCommand.Run();
|
||||||
|
[CommandMethod("PXP-DJ")] public void DimensionOrthoAnnotationsCommand() => OrthoDimensionCommand.Run();
|
||||||
|
[CommandMethod("PXP-AC", CommandFlags.UsePickSet)] public void SwapAttributesCommand() => PRecAnnot.Plant3D2026.SwapAttributesCommand.Run();
|
||||||
|
[CommandMethod("PXP-IOD", CommandFlags.Session)] public void UpdateOrthoDrawingsCommand() => PRecAnnot.Plant3D2026.UpdateOrthoDrawingsCommand.Run();
|
||||||
|
[CommandMethod("mto-ma")] public void ShowManualAnnotationPaletteCommand() => ManualAnnotationPalette.Show();
|
||||||
|
[CommandMethod("PXP-MA")] public void ShowManualAnnotationPaletteAliasCommand() => ManualAnnotationPalette.Show();
|
||||||
|
[CommandMethod("CAC1")] public void PlaceManualAnnotationCommand() => ManualAnnotationEditCommand.Run();
|
||||||
|
[CommandMethod("PXP-MAB")] public void InsertManualBlockCommand() => ManualBlockInsertionCommand.Run(ManualAnnotationPalette.CurrentState());
|
||||||
|
[CommandMethod("CAC")] public void CreateManualAnnotationCommand() => ManualBlockInsertionCommand.Run(ManualAnnotationPalette.CurrentState());
|
||||||
|
[CommandMethod("BJ")] public void ManualBlockJigCommand() => ManualBlockInsertionCommand.Run(ManualAnnotationPalette.CurrentState());
|
||||||
|
|
||||||
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
|
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
|
||||||
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
|
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
|
||||||
|
|
||||||
@@ -47,8 +77,69 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
[CommandMethod("PHP_REC_AUTOMATION_PREPARE", CommandFlags.Session)]
|
[CommandMethod("PHP_REC_AUTOMATION_PREPARE", CommandFlags.Session)]
|
||||||
public void AutomationPrepareCommand() => ScriptDrivenFirstApplyAutomation.Prepare();
|
public void AutomationPrepareCommand() => ScriptDrivenFirstApplyAutomation.Prepare();
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_AUTOMATION_BEFORE_PXP_1", CommandFlags.Session)]
|
||||||
|
public void AutomationBeforePxp1Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("BeforePxp1");
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_AUTOMATION_AFTER_PXP_1", CommandFlags.Session)]
|
||||||
|
public void AutomationAfterPxp1Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("AfterPxp1");
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_AUTOMATION_BEFORE_PXP_2", CommandFlags.Session)]
|
||||||
|
public void AutomationBeforePxp2Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("BeforePxp2");
|
||||||
|
|
||||||
|
[CommandMethod("PHP_REC_AUTOMATION_AFTER_PXP_2", CommandFlags.Session)]
|
||||||
|
public void AutomationAfterPxp2Command() => ScriptDrivenFirstApplyAutomation.RecordSentinel("AfterPxp2");
|
||||||
|
|
||||||
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
|
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
|
||||||
public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizePreview();
|
public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizeAutomation();
|
||||||
|
|
||||||
|
private static void RunLineNumberPreview()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
var editor = document.Editor;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configuration = LineNumberRuntimeConfigurationReader.Read();
|
||||||
|
var profileDefault = configuration.Annotations.Annotations.OrderBy(value => value.Nickname, StringComparer.Ordinal).First();
|
||||||
|
var profilePrompt = new PromptStringOptions($"\nПрофиль номера линии <{profileDefault.Nickname}>: ") { AllowSpaces = false, DefaultValue = profileDefault.Nickname, UseDefaultValue = true };
|
||||||
|
var profileInput = editor.GetString(profilePrompt);
|
||||||
|
if (profileInput.Status != PromptStatus.OK) return;
|
||||||
|
var profile = configuration.Annotations.Annotations.SingleOrDefault(value => string.Equals(value.Nickname, profileInput.StringResult, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (profile is null) throw new InvalidOperationException("LINE_NUMBER_PROFILE_NOT_FOUND");
|
||||||
|
|
||||||
|
var selection = editor.GetEntity("\nВыберите компонент Plant 3D для аннотации номера линии: ");
|
||||||
|
if (selection.Status != PromptStatus.OK) return;
|
||||||
|
var piping = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("LINE_NUMBER_PIPING_DATALINKS_UNAVAILABLE");
|
||||||
|
var candidate = new PlantLineNumberCandidateReader(piping).Read(selection.ObjectId, configuration.Assets.Marker.Layer, configuration.Assets.Marker.Texts[0].TextStyle);
|
||||||
|
if (!candidate.Success) throw new InvalidOperationException(candidate.FailureCode);
|
||||||
|
var preview = new LineNumberCommandPreviewService(
|
||||||
|
new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()),
|
||||||
|
new LineNumberFieldResolver(new PlantDataLinksPropertyReader(piping)))
|
||||||
|
.Preview(profile, [candidate.Candidate!], Array.Empty<ExistingLineNumberAnnotation>());
|
||||||
|
if (!preview.Success) throw new InvalidOperationException(preview.FailureCode);
|
||||||
|
var operations = string.Join(", ", preview.Plan!.Operations.Select(value => $"{value.Kind}:{value.Text}"));
|
||||||
|
editor.WriteMessage($"\nPXP-LN preview: {operations}. DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("LineNumberPreviewCompleted", commandName: "PXP-LN", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["OperationCount"] = preview.Apply.Count });
|
||||||
|
if (ParseMode(RuntimeDiagnostics.RuntimeMode) != PRecAnnotExecutionMode.Apply) return;
|
||||||
|
|
||||||
|
var operation = preview.Apply.SingleOrDefault() ?? throw new InvalidOperationException("LINE_NUMBER_APPLY_REQUIRES_EXACTLY_ONE_OPERATION");
|
||||||
|
var placement = editor.GetPoint("\nУкажите положение маркера номера линии: ");
|
||||||
|
if (placement.Status != PromptStatus.OK) return;
|
||||||
|
operation = operation with { Position = new AnnotationPoint(placement.Value.X, placement.Value.Y, placement.Value.Z) };
|
||||||
|
var permit = LineNumberApplyPermitReader.Read();
|
||||||
|
LineNumberApplyPermitReader.Validate(document, permit, operation);
|
||||||
|
// AutoCAD wraps a command's committed transaction in its native
|
||||||
|
// UNDO record. No compensating-delete path is used here.
|
||||||
|
var result = new LineNumberAnnotationWriter().Apply(document, [operation], configuration.Assets);
|
||||||
|
RuntimeDiagnostics.Write("LineNumberApplyCompleted", commandName: "PXP-LN", resultStatus: result.Created > 0 ? "Created" : result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document, details: new Dictionary<string, object?> { ["Created"] = result.Created, ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged });
|
||||||
|
editor.WriteMessage($"\nPXP-LN Apply: created={result.Created}; updated={result.Updated}; already-current={result.Unchanged}.");
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("LineNumberPreviewFailed", "ERROR", commandName: "PXP-LN", resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
WriteEditor(document, $"\nPXP-LN: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void RunDiagnostic(string commandName)
|
private static void RunDiagnostic(string commandName)
|
||||||
{
|
{
|
||||||
@@ -61,7 +152,13 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
var mode = ParseMode(RuntimeDiagnostics.RuntimeMode);
|
||||||
|
|
||||||
var context = new PlantRuntimeContextReader().ReadCurrentContext();
|
// Script-driven commands can run while Plant's implicit current
|
||||||
|
// document lags behind MDI activation. Bind the planner to the
|
||||||
|
// command document captured above, which is also the observer's
|
||||||
|
// fixture identity.
|
||||||
|
var context = ScriptDrivenFirstApplyAutomation.TryGetPreparedContext(document, out var scriptContext)
|
||||||
|
? scriptContext
|
||||||
|
: new PlantRuntimeContextReader().ReadCurrentContext(document);
|
||||||
var details = new Dictionary<string, object?>
|
var details = new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["ExecutionMode"] = mode.ToString(),
|
["ExecutionMode"] = mode.ToString(),
|
||||||
@@ -97,7 +194,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
var applyPermit = (mode == PRecAnnotExecutionMode.Apply || hasLimitedApplyPermit) && document is not null
|
||||||
? FirstLimitedApplyPermitReader.ReadFor(document)
|
? FirstLimitedApplyPermitReader.ReadFor(document)
|
||||||
: null;
|
: null;
|
||||||
var automationTarget = FirstLimitedApplyAutomationCoordinator.SelectedTargetSnapshotId;
|
var automationTarget = ScriptDrivenFirstApplyAutomation.SelectedTargetSnapshotId
|
||||||
|
?? FirstLimitedApplyAutomationCoordinator.SelectedTargetSnapshotId;
|
||||||
var limitedTarget = applyPermit?.TargetSnapshotId ?? automationTarget;
|
var limitedTarget = applyPermit?.TargetSnapshotId ?? automationTarget;
|
||||||
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
if (mode == PRecAnnotExecutionMode.Apply && context.Viewports.Count != 1)
|
||||||
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
throw new InvalidOperationException("APPLY_REQUIRES_EXACTLY_ONE_VIEWPORT");
|
||||||
@@ -146,6 +244,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
var membershipPartial = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.PartiallyResolved);
|
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 membershipUnresolved = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.NotResolved);
|
||||||
var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
var membershipOutside = objectsByViewport.SelectMany(x => x).Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||||
|
var orthoClassifications = objectsByViewport.SelectMany(x => x).OrderBy(x => x.SnapshotId, StringComparer.Ordinal)
|
||||||
|
.Select(x => new OrthoAnnotationClassifier().Classify(x, new(true, true, true, true, true))).ToArray();
|
||||||
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
var planId = plans.Count == 1 ? plans[0].PlanId : null;
|
||||||
details["ObjectsExamined"] = summary.ObjectsExamined;
|
details["ObjectsExamined"] = summary.ObjectsExamined;
|
||||||
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
details["PlanSchemaVersion"] = AnnotationPlanBuilder.CurrentPlanSchemaVersion;
|
||||||
@@ -154,6 +254,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
details["MembershipPartial"] = membershipPartial;
|
details["MembershipPartial"] = membershipPartial;
|
||||||
details["MembershipUnresolved"] = membershipUnresolved;
|
details["MembershipUnresolved"] = membershipUnresolved;
|
||||||
details["MembershipOutsideView"] = membershipOutside;
|
details["MembershipOutsideView"] = membershipOutside;
|
||||||
|
details["MultiLeaderCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.MultiLeader);
|
||||||
|
details["BubbleCandidates"] = orthoClassifications.Count(x => x.Representation == OrthoAnnotationRepresentation.BubbleBlock);
|
||||||
details["PlanCreateCount"] = summary.CreateCount;
|
details["PlanCreateCount"] = summary.CreateCount;
|
||||||
details["PlanUpdateCount"] = summary.UpdateCount;
|
details["PlanUpdateCount"] = summary.UpdateCount;
|
||||||
details["PlanSkipCount"] = summary.SkipCount;
|
details["PlanSkipCount"] = summary.SkipCount;
|
||||||
@@ -181,7 +283,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
new RuntimeWriteGuard(mode).EnsureWriteAllowed(preflight.Operation.OperationId);
|
new RuntimeWriteGuard(mode).EnsureWriteAllowed(preflight.Operation.OperationId);
|
||||||
RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document);
|
RuntimeDiagnostics.Write("ApplyPreflightCompleted", operationId: operationId, commandName: commandName, resultStatus: "Accepted", document: document);
|
||||||
RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document);
|
RuntimeDiagnostics.Write("AnnotationTransactionStarted", operationId: operationId, commandName: commandName, document: document);
|
||||||
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState);
|
var outcome = new FirstLimitedPipeAnnotationWriter().Write(document!, preflight.Operation, applyPermit.VisibilityState, applyPermit.AnnotationTemplateDwgPath);
|
||||||
|
ScriptDrivenFirstApplyAutomation.RecordApplyOutcome(outcome);
|
||||||
FirstLimitedApplyAutomationCoordinator.RecordApplyOutcome(outcome);
|
FirstLimitedApplyAutomationCoordinator.RecordApplyOutcome(outcome);
|
||||||
details["ApplyOutcome"] = outcome.ToString();
|
details["ApplyOutcome"] = outcome.ToString();
|
||||||
details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0;
|
details["AnnotationsCreated"] = outcome == PipeAnnotationWriteOutcome.Created ? 1 : 0;
|
||||||
@@ -198,6 +301,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
}
|
}
|
||||||
catch (System.Exception exception)
|
catch (System.Exception exception)
|
||||||
{
|
{
|
||||||
|
if (string.Equals(commandName, "PXP-OA", StringComparison.OrdinalIgnoreCase) && string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase))
|
||||||
|
ScriptDrivenFirstApplyAutomation.RecordApplyFailure(exception);
|
||||||
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
|
RuntimeDiagnostics.Write("CommandFailed", "ERROR", operationId, commandName, exception.Message, exception, document);
|
||||||
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
|
WriteUsage(operationId, commandName, exception.Message, document, null, stopwatch.ElapsedMilliseconds, exception);
|
||||||
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
|
WriteEditor(document, $"\nP_Rec_Annot: безопасная диагностика завершилась ошибкой: {exception.Message}");
|
||||||
@@ -270,12 +375,35 @@ internal static class RibbonIntegration
|
|||||||
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
|
if (tab is null) { tab = new RibbonTab { Id = TabId, Title = "Промхимпроект" }; ribbon.Tabs.Add(tab); }
|
||||||
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
|
var panel = tab.Panels.FirstOrDefault(x => string.Equals(x.Source?.Id, PanelId, StringComparison.OrdinalIgnoreCase));
|
||||||
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
|
if (panel is not null) { RuntimeDiagnostics.Write("RibbonAlreadyExists", resultStatus: "AlreadyExists"); RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "AlreadyExists"); return; }
|
||||||
|
// Keep the existing legacy-compatible PHP_REC_ANNOT action and expose the
|
||||||
|
// production command surface from the same composite panel.
|
||||||
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
|
var source = new RibbonPanelSource { Id = PanelId, Title = "Аннотации" };
|
||||||
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
|
source.Items.Add(new RibbonButton { Id = ButtonId, Text = "Аннотировать", ShowText = true, CommandParameter = "PHP_REC_ANNOT " });
|
||||||
|
AddButton(source, "LineNumber", "Line number", "PXP-LN");
|
||||||
|
AddButton(source, "LineConnections", "Line connections", "PXP-ALC");
|
||||||
|
AddButton(source, "AutoElevation", "Auto elevation", "PXP-AE");
|
||||||
|
AddButton(source, "InsertElevation", "Insert elevation", "PXP-IE");
|
||||||
|
AddButton(source, "OrthoAnnotate", "Ortho annotate", "PXP-OA");
|
||||||
|
AddButton(source, "AnnotateView", "Annotate view", "PXP-AAV");
|
||||||
|
AddButton(source, "ShowOrthoSource", "Show source in model", "PXP-OSI");
|
||||||
|
AddButton(source, "RemoveOrthoAnnotations", "Remove Ortho annotations", "PXP-RA");
|
||||||
|
AddButton(source, "DimensionOrthoAnnotations", "Dimension Ortho annotations", "PXP-DJ");
|
||||||
|
AddButton(source, "UpdateOrthoViewBlocks", "Update Ortho view blocks", "PXP-UOB");
|
||||||
|
AddButton(source, "UpdateOrthoDrawings", "Update Ortho drawings", "PXP-IOD");
|
||||||
|
AddButton(source, "SwapAttributes", "Swap attributes", "PXP-AC");
|
||||||
|
AddButton(source, "CreateManualAnnotation", "Create manual annotation", "CAC");
|
||||||
|
AddButton(source, "ManualBlockJig", "Manual block jig", "BJ");
|
||||||
|
AddButton(source, "ManualPalette", "Manual annotations", "mto-ma");
|
||||||
tab.Panels.Add(new RibbonPanel { Source = source });
|
tab.Panels.Add(new RibbonPanel { Source = source });
|
||||||
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
|
RuntimeDiagnostics.Write("RibbonInitializationCompleted", resultStatus: "Created");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddButton(RibbonPanelSource? source, string suffix, string text, string command)
|
||||||
|
{
|
||||||
|
if (source is null) return;
|
||||||
|
source.Items.Add(new RibbonButton { Id = $"{PanelId}.{suffix}", Text = text, ShowText = true, CommandParameter = command + " " });
|
||||||
|
}
|
||||||
|
|
||||||
public static void Terminate()
|
public static void Terminate()
|
||||||
{
|
{
|
||||||
if (!deferred) return;
|
if (!deferred) return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace PRecAnnot.Plant3D2026;
|
|||||||
internal sealed record FirstApplyAutomationConfiguration
|
internal sealed record FirstApplyAutomationConfiguration
|
||||||
{
|
{
|
||||||
public required string Phase { get; init; }
|
public required string Phase { get; init; }
|
||||||
|
public string? TargetCommand { get; init; }
|
||||||
public string? OrthoDrawingPath { get; init; }
|
public string? OrthoDrawingPath { get; init; }
|
||||||
public string? ModelDrawingPath { get; init; }
|
public string? ModelDrawingPath { get; init; }
|
||||||
public required string EvidencePath { get; init; }
|
public required string EvidencePath { get; init; }
|
||||||
@@ -21,6 +22,9 @@ internal sealed record FirstApplyAutomationConfiguration
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal sealed record AutomationEntityCounts(int Total, int PipeBlocks, int PxpOaXdata, int Owned);
|
internal sealed record AutomationEntityCounts(int Total, int PipeBlocks, int PxpOaXdata, int Owned);
|
||||||
|
internal sealed record PxpOaDiscoveryEvidence(bool Found, string? GlobalName, string? GroupName, string? Flags, string? DeclaringType, string? DeclaringAssembly, string? AssemblyLocation, string? AssemblySha256, bool AssemblyLoaded, bool CommandClassIncluded, bool BundleHashMatched, int DeclarationCount);
|
||||||
|
internal sealed record AutomationSentinelEvidence(string Stage, string? DocumentIdentity, string? DatabaseIdentity, DateTimeOffset TimestampUtc);
|
||||||
|
internal sealed record AutomationCommandCorrelationEvidence(string EventKind, string RawCommandName, string NormalizedCommandName, bool CommandNameMatched, bool PreparedStateMatched, bool DocumentMatched, bool DatabaseMatched, bool FixturePathMatched, bool SequenceAccepted, string? RejectionReason, string? SenderType, string? SenderDocumentPath, string? ExpectedDocumentPath, string? ActiveDocumentPath, int RunCount, DateTimeOffset TimestampUtc);
|
||||||
|
|
||||||
internal static class FirstLimitedApplyAutomationBootstrap
|
internal static class FirstLimitedApplyAutomationBootstrap
|
||||||
{
|
{
|
||||||
@@ -82,6 +86,7 @@ internal sealed record FirstApplyAutomationEvidence
|
|||||||
{
|
{
|
||||||
public int SchemaVersion { get; init; } = 1;
|
public int SchemaVersion { get; init; } = 1;
|
||||||
public required string Phase { get; init; }
|
public required string Phase { get; init; }
|
||||||
|
public string? TargetCommand { get; init; }
|
||||||
public bool Completed { get; init; }
|
public bool Completed { get; init; }
|
||||||
public bool? Prepared { get; init; }
|
public bool? Prepared { get; init; }
|
||||||
public bool? FunctionalEvidenceCompleted { get; init; }
|
public bool? FunctionalEvidenceCompleted { get; init; }
|
||||||
@@ -122,6 +127,10 @@ internal sealed record FirstApplyAutomationEvidence
|
|||||||
public string? PreviousState { get; init; }
|
public string? PreviousState { get; init; }
|
||||||
public string? CurrentState { get; init; }
|
public string? CurrentState { get; init; }
|
||||||
public IReadOnlyList<string> StateTransitions { get; init; } = Array.Empty<string>();
|
public IReadOnlyList<string> StateTransitions { get; init; } = Array.Empty<string>();
|
||||||
|
public PxpOaDiscoveryEvidence? PxpOaDiscovery { get; init; }
|
||||||
|
public IReadOnlyList<AutomationSentinelEvidence> Sentinels { get; init; } = Array.Empty<AutomationSentinelEvidence>();
|
||||||
|
public IReadOnlyList<string> CommandEvents { get; init; } = Array.Empty<string>();
|
||||||
|
public IReadOnlyList<AutomationCommandCorrelationEvidence> CommandCorrelations { get; init; } = Array.Empty<AutomationCommandCorrelationEvidence>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class FirstLimitedApplyAutomationCoordinator
|
internal sealed class FirstLimitedApplyAutomationCoordinator
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ internal sealed record FirstLimitedApplyRuntimePermit
|
|||||||
public required string TargetSnapshotId { get; init; }
|
public required string TargetSnapshotId { get; init; }
|
||||||
public required string ExpectedPlanId { get; init; }
|
public required string ExpectedPlanId { get; init; }
|
||||||
public required string VisibilityState { get; init; }
|
public required string VisibilityState { get; init; }
|
||||||
|
// Test-only local copy of the legacy v1.0.4 Template.dwg. The package
|
||||||
|
// deliberately carries no DWG assets; the per-run permit binds this source.
|
||||||
|
public required string AnnotationTemplateDwgPath { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class FirstLimitedApplyPermitReader
|
internal static class FirstLimitedApplyPermitReader
|
||||||
@@ -41,6 +44,8 @@ internal static class FirstLimitedApplyPermitReader
|
|||||||
var drawing = Path.GetFullPath(document.Name);
|
var drawing = Path.GetFullPath(document.Name);
|
||||||
if (!drawing.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
if (!drawing.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
|
||||||
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
throw new InvalidOperationException("APPLY_TEST_FIXTURE_OR_PERMIT_REQUIRED");
|
||||||
|
if (string.IsNullOrWhiteSpace(permit.AnnotationTemplateDwgPath) || !Path.IsPathFullyQualified(permit.AnnotationTemplateDwgPath) || permit.AnnotationTemplateDwgPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.AnnotationTemplateDwgPath))
|
||||||
|
throw new InvalidOperationException("ANNOTATION_TEMPLATE_ASSET_REQUIRED");
|
||||||
return permit;
|
return permit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,12 +57,12 @@ internal sealed class FirstLimitedPipeAnnotationWriter
|
|||||||
private const string BlockName = "PIPE";
|
private const string BlockName = "PIPE";
|
||||||
private const string XdataApplication = "PXP-OA";
|
private const string XdataApplication = "PXP-OA";
|
||||||
|
|
||||||
public PipeAnnotationWriteOutcome Write(Document document, CreatePipeAnnotationOperation operation, string visibilityState)
|
public PipeAnnotationWriteOutcome Write(Document document, CreatePipeAnnotationOperation operation, string visibilityState, string annotationTemplateDwgPath)
|
||||||
{
|
{
|
||||||
var database = document.Database;
|
var database = document.Database;
|
||||||
using var transaction = database.TransactionManager.StartTransaction();
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
if (!table.Has(BlockName)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_BLOCK_MISSING");
|
var blockId = EnsureLegacyPipeBlock(transaction, database, table, annotationTemplateDwgPath);
|
||||||
|
|
||||||
var paper = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
var paper = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
if (HasOwnedAnnotation(transaction, paper, operation.DuplicateKey)) return PipeAnnotationWriteOutcome.AlreadyExists;
|
if (HasOwnedAnnotation(transaction, paper, operation.DuplicateKey)) return PipeAnnotationWriteOutcome.AlreadyExists;
|
||||||
@@ -65,7 +70,7 @@ internal sealed class FirstLimitedPipeAnnotationWriter
|
|||||||
|
|
||||||
using var annotation = new BlockReference(
|
using var annotation = new BlockReference(
|
||||||
new Point3d(operation.Placement.X, operation.Placement.Y, operation.Placement.Z),
|
new Point3d(operation.Placement.X, operation.Placement.Y, operation.Placement.Z),
|
||||||
table[BlockName]);
|
blockId);
|
||||||
paper.AppendEntity(annotation);
|
paper.AppendEntity(annotation);
|
||||||
transaction.AddNewlyCreatedDBObject(annotation, true);
|
transaction.AddNewlyCreatedDBObject(annotation, true);
|
||||||
SetConfirmedVisibility(annotation, visibilityState);
|
SetConfirmedVisibility(annotation, visibilityState);
|
||||||
@@ -77,6 +82,27 @@ internal sealed class FirstLimitedPipeAnnotationWriter
|
|||||||
return PipeAnnotationWriteOutcome.Created;
|
return PipeAnnotationWriteOutcome.Created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ObjectId EnsureLegacyPipeBlock(Transaction destinationTransaction, Database destination, BlockTable destinationTable, string templatePath)
|
||||||
|
{
|
||||||
|
if (destinationTable.Has(BlockName)) return destinationTable[BlockName];
|
||||||
|
if (!File.Exists(templatePath)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_ASSET_MISSING");
|
||||||
|
|
||||||
|
using var source = new Database(false, true);
|
||||||
|
source.ReadDwgFile(templatePath, FileShare.Read, true, string.Empty);
|
||||||
|
source.CloseInput(true);
|
||||||
|
using var sourceTransaction = source.TransactionManager.StartTransaction();
|
||||||
|
var sourceTable = (BlockTable)sourceTransaction.GetObject(source.BlockTableId, OpenMode.ForRead);
|
||||||
|
if (!sourceTable.Has(BlockName)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_PIPE_BLOCK_MISSING");
|
||||||
|
using var sourceIds = new ObjectIdCollection { sourceTable[BlockName] };
|
||||||
|
using var mapping = new IdMapping();
|
||||||
|
destination.WblockCloneObjects(sourceIds, destination.BlockTableId, mapping, DuplicateRecordCloning.Ignore, false);
|
||||||
|
sourceTransaction.Commit();
|
||||||
|
|
||||||
|
var refreshed = (BlockTable)destinationTransaction.GetObject(destination.BlockTableId, OpenMode.ForRead);
|
||||||
|
if (!refreshed.Has(BlockName)) throw new InvalidOperationException("ANNOTATION_TEMPLATE_BLOCK_IMPORT_FAILED");
|
||||||
|
return refreshed[BlockName];
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HasOwnedAnnotation(Transaction transaction, BlockTableRecord paper, string duplicateKey)
|
private static bool HasOwnedAnnotation(Transaction transaction, BlockTableRecord paper, string duplicateKey)
|
||||||
{
|
{
|
||||||
foreach (ObjectId id in paper)
|
foreach (ObjectId id in paper)
|
||||||
@@ -106,8 +132,9 @@ internal sealed class FirstLimitedPipeAnnotationWriter
|
|||||||
var property = annotation.DynamicBlockReferencePropertyCollection
|
var property = annotation.DynamicBlockReferencePropertyCollection
|
||||||
.Cast<DynamicBlockReferenceProperty>()
|
.Cast<DynamicBlockReferenceProperty>()
|
||||||
.SingleOrDefault(value => string.Equals(value.PropertyName, "V", StringComparison.Ordinal));
|
.SingleOrDefault(value => string.Equals(value.PropertyName, "V", StringComparison.Ordinal));
|
||||||
if (property is null || !property.GetAllowedValues().OfType<string>().Contains(visibilityState, StringComparer.Ordinal))
|
var allowed = property?.GetAllowedValues().OfType<string>().ToArray() ?? [];
|
||||||
throw new InvalidOperationException("ANNOTATION_VISIBILITY_STATE_UNSUPPORTED");
|
if (property is null || !allowed.Contains(visibilityState, StringComparer.Ordinal))
|
||||||
|
throw new InvalidOperationException($"ANNOTATION_VISIBILITY_STATE_UNSUPPORTED:Allowed={string.Join('|', allowed)}");
|
||||||
property.Value = visibilityState;
|
property.Value = visibilityState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class LineConnectionAnnotationWriter
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-ALC";
|
||||||
|
|
||||||
|
public LineConnectionApplyResult Replace(Document document, string viewportStableId, IReadOnlyList<LineConnectionBoundaryPlan> plans, LineConnectionAssets assets)
|
||||||
|
{
|
||||||
|
var insertable = plans.Where(plan => plan.Reasons.Count == 0).OrderBy(plan => plan.Position.X).ThenByDescending(plan => plan.Position.Y).ToArray();
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
var database = document.Database;
|
||||||
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
if (!blocks.Has(assets.BlockName)) throw new InvalidOperationException("LINE_CONNECTION_BLOCK_MISSING:" + assets.BlockName);
|
||||||
|
if (!layers.Has(assets.PolylineLayer)) throw new InvalidOperationException("LINE_CONNECTION_POLYLINE_LAYER_MISSING:" + assets.PolylineLayer);
|
||||||
|
if (!layers.Has(assets.TextLayer)) throw new InvalidOperationException("LINE_CONNECTION_TEXT_LAYER_MISSING:" + assets.TextLayer);
|
||||||
|
if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("LINE_CONNECTION_TEXT_STYLE_MISSING:" + assets.TextStyle);
|
||||||
|
EnsureRegApp(transaction, database);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
var deleted = 0;
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
var value = transaction.GetObject(id, OpenMode.ForRead);
|
||||||
|
var data = value.GetXDataForApplication(ApplicationName)?.AsArray();
|
||||||
|
if (data is null || !data.Any(item => item.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(item.Value?.ToString(), viewportStableId, StringComparison.Ordinal))) continue;
|
||||||
|
value.UpgradeOpen(); value.Erase(); deleted++;
|
||||||
|
}
|
||||||
|
var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BlockName], OpenMode.ForRead);
|
||||||
|
foreach (var plan in insertable)
|
||||||
|
{
|
||||||
|
using var reference = new BlockReference(new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z), definition.ObjectId) { LayerId = layers[assets.PolylineLayer], Rotation = plan.Rotation };
|
||||||
|
reference.XData = Ownership(viewportStableId, plan);
|
||||||
|
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
|
||||||
|
var values = new[] { plan.FirstText ?? string.Empty, plan.SecondText ?? string.Empty };
|
||||||
|
var index = 0;
|
||||||
|
foreach (ObjectId entityId in definition)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(entityId, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue;
|
||||||
|
using var attribute = new AttributeReference();
|
||||||
|
attribute.SetAttributeFromBlock(source, reference.BlockTransform);
|
||||||
|
attribute.TextString = values[Math.Min(index++, 1)].Replace("\n", "\\P", StringComparison.Ordinal);
|
||||||
|
attribute.LayerId = layers[assets.TextLayer]; attribute.TextStyleId = styles[assets.TextStyle];
|
||||||
|
attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
if (plan.ConnectorPosition is { } connector)
|
||||||
|
{
|
||||||
|
using var leader = new Polyline(3) { LayerId = layers[assets.PolylineLayer], XData = Ownership(viewportStableId, plan) };
|
||||||
|
leader.AddVertexAt(0, new Point2d(connector.X, connector.Y), 0, 0, 0);
|
||||||
|
// Legacy PXP-ALC uses an orthogonal two-segment leader. Its
|
||||||
|
// elbow shares the annotation Y coordinate for a stable route.
|
||||||
|
leader.AddVertexAt(1, new Point2d(connector.X, plan.Position.Y), 0, 0, 0);
|
||||||
|
leader.AddVertexAt(2, new Point2d(plan.Position.X, plan.Position.Y), 0, 0, 0);
|
||||||
|
paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transaction.Commit();
|
||||||
|
return new(deleted, insertable.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResultBuffer Ownership(string viewport, LineConnectionBoundaryPlan plan) => new(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.StableId),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LINENUMBER1"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.FirstText ?? string.Empty),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LINENUMBER2"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, plan.SecondText ?? string.Empty));
|
||||||
|
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database)
|
||||||
|
{
|
||||||
|
var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
|
||||||
|
if (table.Has(ApplicationName)) return;
|
||||||
|
table.UpgradeOpen(); using var record = new RegAppTableRecord { Name = ApplicationName }; table.Add(record); transaction.AddNewlyCreatedDBObject(record, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed record LineConnectionApplyPermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
public required string ExpectedViewportStableId { get; init; }
|
||||||
|
public required IReadOnlyList<string> ExpectedConnectorStableIds { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class LineConnectionApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_CONNECTION_APPLY";
|
||||||
|
|
||||||
|
public static LineConnectionApplyPermit Read()
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_CONNECTION_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_REQUIRED");
|
||||||
|
return JsonSerializer.Deserialize<LineConnectionApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||||
|
?? throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Validate(Document document, LineConnectionApplyPermit permit, string viewportId, IReadOnlyList<LineConnectionBoundaryPlan> plans)
|
||||||
|
{
|
||||||
|
var actual = plans.Where(plan => plan.Reasons.Count == 0).Select(plan => plan.StableId).OrderBy(value => value, StringComparer.Ordinal).ToArray();
|
||||||
|
var expected = permit.ExpectedConnectorStableIds.OrderBy(value => value, StringComparer.Ordinal).ToArray();
|
||||||
|
if (!permit.AllowApply || !string.Equals(permit.ExpectedViewportStableId, viewportId, StringComparison.Ordinal) || !actual.SequenceEqual(expected, StringComparer.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_CONNECTION_APPLY_PERMIT_PLAN_MISMATCH");
|
||||||
|
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_CONNECTION_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
var root = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_CONNECTION_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class LineConnectionCommand
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new PromptEntityOptions("\nВыберите видовой экран для аннотаций стыков номеров линий: ");
|
||||||
|
options.SetRejectMessage("\nТребуется видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
var selected = document.Editor.GetEntity(options);
|
||||||
|
if (selected.Status != PromptStatus.OK) return;
|
||||||
|
OrthoViewportInfo viewport;
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
{
|
||||||
|
var entity = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
|
||||||
|
viewport = ResolveViewport(document, entity);
|
||||||
|
}
|
||||||
|
var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("LINE_CONNECTION_PIPING_DATALINKS_UNAVAILABLE");
|
||||||
|
var relationships = new PlantConnectorRelationshipReader(manager).Read();
|
||||||
|
if (!relationships.Success) throw new InvalidOperationException(relationships.FailureCode);
|
||||||
|
var objects = new PlantObjectReader().ReadObjects(viewport, CancellationToken.None);
|
||||||
|
var byRow = objects.Where(value => value.MembershipStatus == ViewMembershipStatus.Resolved && value.Geometry.PaperPosition is not null)
|
||||||
|
.Select(value => (Object: value, Row: ReadRowId(value)))
|
||||||
|
.Where(value => value.Row.HasValue).ToDictionary(value => value.Row!.Value, value => value.Object);
|
||||||
|
var placements = new Dictionary<string, LineConnectionPlacement>(StringComparer.Ordinal);
|
||||||
|
foreach (var relationship in relationships.Connectors)
|
||||||
|
{
|
||||||
|
if (!byRow.TryGetValue(relationship.ConnectorRowId, out var connector) || connector.Geometry.PaperPosition is not { } point) continue;
|
||||||
|
var direction = connector.Geometry.PaperDirection ?? new Vector2Dto(1, 0);
|
||||||
|
var horizontalLeader = Math.Abs(direction.Y) >= Math.Abs(direction.X);
|
||||||
|
var block = horizontalLeader ? new AnnotationPoint(viewport.CenterX, point.Y) : new AnnotationPoint(point.X, viewport.CenterY);
|
||||||
|
var endpoint = new Point3Dto(point.X, point.Y, 0);
|
||||||
|
placements[relationship.StableId] = new(endpoint, endpoint, new(endpoint, endpoint), block, 0);
|
||||||
|
}
|
||||||
|
var configuration = LineNumberRuntimeConfigurationReader.Read();
|
||||||
|
var definition = configuration.Annotations.Annotations.OrderBy(value => value.Nickname, StringComparer.Ordinal).First();
|
||||||
|
var preview = new LineConnectionPreviewService(new LineNumberFieldResolver(new PlantDataLinksPropertyReader(manager)), new LineNumberAnnotationFormatter(), new LineConnectionBoundaryPlanner())
|
||||||
|
.Preview(definition, relationships.Connectors, placements);
|
||||||
|
var ready = preview.Plans.Count(plan => plan.Reasons.Count == 0);
|
||||||
|
document.Editor.WriteMessage($"\nPXP-ALC preview: connectors={relationships.Connectors.Count}; eligible={ready}; skipped={preview.Plans.Count - ready}. DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("LineConnectionPreviewCompleted", commandName: "PXP-ALC", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["ConnectorCount"] = relationships.Connectors.Count, ["EligibleCount"] = ready });
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
var permit = LineConnectionApplyPermitReader.Read();
|
||||||
|
LineConnectionApplyPermitReader.Validate(document, permit, viewport.Id, preview.Plans);
|
||||||
|
var result = new LineConnectionAnnotationWriter().Replace(document, viewport.Id, preview.Plans, configuration.Connections);
|
||||||
|
document.Editor.WriteMessage($"\nPXP-ALC Apply: deleted={result.Deleted}; created={result.Created}.");
|
||||||
|
RuntimeDiagnostics.Write("LineConnectionApplyCompleted", commandName: "PXP-ALC", resultStatus: "Applied", document: document, details: new Dictionary<string, object?> { ["Deleted"] = result.Deleted, ["Created"] = result.Created });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("LineConnectionCommandFailed", "ERROR", commandName: "PXP-ALC", resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
document.Editor.WriteMessage("\nPXP-ALC: " + exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoViewportInfo ResolveViewport(Document document, Viewport selected)
|
||||||
|
{
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("LINE_CONNECTION_ORTHO_PROJECT_UNAVAILABLE");
|
||||||
|
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
|
||||||
|
.Where(value => Math.Abs(value.CenterX - selected.CenterPoint.X) < 0.001 && Math.Abs(value.CenterY - selected.CenterPoint.Y) < 0.001 && Math.Abs(value.Width - selected.Width) < 0.001 && Math.Abs(value.Height - selected.Height) < 0.001)
|
||||||
|
.ToArray();
|
||||||
|
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "LINE_CONNECTION_ORTHO_VIEW_NOT_FOUND" : "LINE_CONNECTION_ORTHO_VIEW_AMBIGUOUS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long? ReadRowId(PlantObjectSnapshot value) => value.Properties.TryGetValue("PRecAnnot.RowId", out var text) && long.TryParse(text, out var row) ? row : null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class LineNumberAnnotationWriter
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-LN";
|
||||||
|
private const string MarkerBlockName = "LN_MARKER";
|
||||||
|
private const string MarkerAttributeTag = "NUMBER";
|
||||||
|
|
||||||
|
public LineNumberApplyResult Apply(Document document, IReadOnlyList<LineNumberApplyPreflight> plan, AnnotateLineNumbersAssets assets)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(document);
|
||||||
|
ArgumentNullException.ThrowIfNull(plan);
|
||||||
|
ArgumentNullException.ThrowIfNull(assets);
|
||||||
|
if (plan.Select(item => item.StableId).Distinct(StringComparer.Ordinal).Count() != plan.Count)
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_APPLY_DUPLICATE_STABLE_ID");
|
||||||
|
|
||||||
|
var created = 0;
|
||||||
|
var updated = 0;
|
||||||
|
var unchanged = 0;
|
||||||
|
var database = document.Database;
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
ValidateAssets(transaction, database, assets.Marker);
|
||||||
|
EnsureRegApp(transaction, database);
|
||||||
|
var markerDefinition = EnsureMarkerDefinition(transaction, database, blocks, assets.Marker);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
|
||||||
|
foreach (var item in plan.OrderBy(value => value.MarkerNumber))
|
||||||
|
{
|
||||||
|
var existing = FindOwnedMarker(transaction, paper, item.StableId);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
CreateMarker(transaction, paper, markerDefinition, item, assets.Marker);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
else if (UpdateMarker(transaction, existing, item, assets.Marker.Layer)) updated++;
|
||||||
|
else unchanged++;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
return new(created, updated, unchanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy PXP-LN materialises its selected marker/text pairs as a paper-space
|
||||||
|
// LN_LIST block. The list ownership key makes repeat Apply update that
|
||||||
|
// aggregate rather than append another list.
|
||||||
|
public LineNumberApplyResult ApplyList(Document document, LineNumberListApplyPreflight plan, AnnotateLineNumbersAssets assets)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(document);
|
||||||
|
ArgumentNullException.ThrowIfNull(plan);
|
||||||
|
if (plan.Rows.Count == 0) return new(0, 0, 1);
|
||||||
|
if (plan.Rows.Select(row => row.MarkerNumber).Distinct().Count() != plan.Rows.Count)
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_LIST_DUPLICATE_MARKER_NUMBER");
|
||||||
|
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
var database = document.Database;
|
||||||
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
ValidateListAssets(transaction, database, assets.List);
|
||||||
|
EnsureRegApp(transaction, database);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
var existing = FindOwnedMarker(transaction, paper, plan.StableId);
|
||||||
|
if (existing is not null)
|
||||||
|
{
|
||||||
|
var changed = UpdateList(transaction, existing, plan);
|
||||||
|
transaction.Commit();
|
||||||
|
return new(0, changed ? 1 : 0, changed ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var definition = CreateListDefinition(transaction, database, blocks, plan, assets.List);
|
||||||
|
using var reference = new BlockReference(new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z), definition)
|
||||||
|
{
|
||||||
|
Rotation = plan.Rotation,
|
||||||
|
Layer = assets.List.Layer,
|
||||||
|
XData = ListOwnership(plan.StableId)
|
||||||
|
};
|
||||||
|
paper.AppendEntity(reference);
|
||||||
|
transaction.AddNewlyCreatedDBObject(reference, true);
|
||||||
|
AppendListAttributes(transaction, definition, reference, plan.Rows);
|
||||||
|
transaction.Commit();
|
||||||
|
return new(1, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineNumberApplyResult ApplyEdits(Document document, IReadOnlyList<LineNumberRowMutation> mutations)
|
||||||
|
{
|
||||||
|
if (mutations.Count == 0) return new(0, 0, 1);
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
var database = document.Database;
|
||||||
|
using var transaction = database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
var updated = 0;
|
||||||
|
foreach (var mutation in mutations)
|
||||||
|
{
|
||||||
|
var marker = FindOwnedMarker(transaction, paper, mutation.SourceStableId) ?? throw new InvalidOperationException("LINE_NUMBER_OWNED_MARKER_NOT_FOUND");
|
||||||
|
foreach (ObjectId id in marker.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite);
|
||||||
|
if (!string.Equals(attribute.Tag, MarkerAttributeTag, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
attribute.TextString = mutation.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
if (mutation.MarkerAttributePosition is { } reset) attribute.Position = new Point3d(reset.X, reset.Y, reset.Z).TransformBy(marker.BlockTransform);
|
||||||
|
}
|
||||||
|
foreach (ObjectId entityId in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(entityId, OpenMode.ForRead) is not BlockReference list || !list.Name.StartsWith("LN_LIST", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
foreach (ObjectId attributeId in list.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(attributeId, OpenMode.ForWrite);
|
||||||
|
if (string.Equals(attribute.Tag, $"NUMBER{mutation.PreviousMarkerNumber}", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
attribute.Tag = $"NUMBER{mutation.MarkerNumber}";
|
||||||
|
attribute.TextString = mutation.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
else if (string.Equals(attribute.Tag, $"LINENUMBER{mutation.PreviousMarkerNumber}", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
attribute.Tag = $"LINENUMBER{mutation.MarkerNumber}";
|
||||||
|
attribute.TextString = mutation.Text.Replace("\n", "\\P", StringComparison.Ordinal);
|
||||||
|
attribute.UpdateMTextAttribute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updated++;
|
||||||
|
}
|
||||||
|
transaction.Commit();
|
||||||
|
return new(0, updated, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateAssets(Transaction transaction, Database database, LineNumberMarkerAsset marker)
|
||||||
|
{
|
||||||
|
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
foreach (var layer in marker.Texts.Select(value => value.Layer).Append(marker.Layer).Concat(marker.Lines.Select(value => value.Layer)).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
if (!layers.Has(layer)) throw new InvalidOperationException($"LINE_NUMBER_LAYER_MISSING:{layer}");
|
||||||
|
foreach (var style in marker.Texts.Select(value => value.TextStyle).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
if (!styles.Has(style)) throw new InvalidOperationException($"LINE_NUMBER_TEXT_STYLE_MISSING:{style}");
|
||||||
|
if (marker.Texts.Count == 0) throw new InvalidOperationException("LINE_NUMBER_MARKER_TEXT_ASSET_MISSING");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateListAssets(Transaction transaction, Database database, LineNumberListAsset list)
|
||||||
|
{
|
||||||
|
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
foreach (var layer in new[] { list.Layer, list.MarkerText.Layer, list.LineNumberText.Layer, list.Line.Layer }.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
if (!layers.Has(layer)) throw new InvalidOperationException($"LINE_NUMBER_LAYER_MISSING:{layer}");
|
||||||
|
foreach (var style in new[] { list.MarkerText.TextStyle, list.LineNumberText.TextStyle }.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||||
|
if (!styles.Has(style)) throw new InvalidOperationException($"LINE_NUMBER_TEXT_STYLE_MISSING:{style}");
|
||||||
|
if (list.LineHeight <= 0) throw new InvalidOperationException("LINE_NUMBER_LIST_HEIGHT_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectId CreateListDefinition(Transaction transaction, Database database, BlockTable blocks, LineNumberListApplyPreflight plan, LineNumberListAsset list)
|
||||||
|
{
|
||||||
|
blocks.UpgradeOpen();
|
||||||
|
var baseName = $"LN_LIST{plan.Rows.Count}";
|
||||||
|
var name = baseName;
|
||||||
|
for (var suffix = 1; blocks.Has(name); suffix++) name = $"{baseName}_{suffix}";
|
||||||
|
using var definition = new BlockTableRecord { Name = name, Origin = Point3d.Origin };
|
||||||
|
var id = blocks.Add(definition);
|
||||||
|
transaction.AddNewlyCreatedDBObject(definition, true);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
for (var index = 0; index < plan.Rows.Count; index++)
|
||||||
|
{
|
||||||
|
var row = plan.Rows[index];
|
||||||
|
AppendListAttribute(transaction, definition, row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture), $"NUMBER{row.MarkerNumber}", "Номер маркера:", list.MarkerText, index, list.LineHeight, list.MaskScaleFactor, layers, styles);
|
||||||
|
AppendListAttribute(transaction, definition, row.Text.Replace("\n", "(|)", StringComparison.Ordinal), $"LINENUMBER{row.MarkerNumber}", "Номер линии:", list.LineNumberText, index, list.LineHeight, list.MaskScaleFactor, layers, styles);
|
||||||
|
using var horizontal = new Line(
|
||||||
|
Shift(list.Line.Start, index, list.LineHeight),
|
||||||
|
Shift(list.Line.End, index, list.LineHeight)) { LayerId = layers[list.Line.Layer] };
|
||||||
|
definition.AppendEntity(horizontal);
|
||||||
|
transaction.AddNewlyCreatedDBObject(horizontal, true);
|
||||||
|
}
|
||||||
|
using var vertical = new Line(
|
||||||
|
new Point3d(list.Line.Start.X, list.Line.Start.Y + list.LineHeight, 0),
|
||||||
|
Shift(list.Line.Start, plan.Rows.Count - 1, list.LineHeight)) { LayerId = layers[list.Line.Layer] };
|
||||||
|
definition.AppendEntity(vertical);
|
||||||
|
transaction.AddNewlyCreatedDBObject(vertical, true);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendListAttribute(Transaction transaction, BlockTableRecord definition, string text, string tag, string prompt, LineNumberTextAsset asset, int row, double lineHeight, double maskScale, LayerTable layers, TextStyleTable styles)
|
||||||
|
{
|
||||||
|
var position = new Point3d(asset.Position.X, asset.Position.Y - lineHeight * row, 0);
|
||||||
|
using var attribute = new AttributeDefinition(position, text, tag, prompt, styles[asset.TextStyle]) { Height = asset.Size, IsMTextAttributeDefinition = true, LayerId = layers[asset.Layer] };
|
||||||
|
var mtext = attribute.MTextAttributeDefinition;
|
||||||
|
mtext.BackgroundFill = true;
|
||||||
|
mtext.BackgroundScaleFactor = maskScale;
|
||||||
|
mtext.UseBackgroundColor = true;
|
||||||
|
attribute.MTextAttributeDefinition = mtext;
|
||||||
|
definition.AppendEntity(attribute);
|
||||||
|
transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Point3d Shift(Point2Dto source, int row, double height) => new(source.X, source.Y - height * row, 0);
|
||||||
|
|
||||||
|
private static void AppendListAttributes(Transaction transaction, ObjectId definitionId, BlockReference reference, IReadOnlyList<LineNumberApplyPreflight> rows)
|
||||||
|
{
|
||||||
|
var values = rows.SelectMany(row => new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>($"NUMBER{row.MarkerNumber}", row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
new KeyValuePair<string, string>($"LINENUMBER{row.MarkerNumber}", row.Text.Replace("\n", "\\P", StringComparison.Ordinal))
|
||||||
|
}).ToDictionary(value => value.Key, value => value.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var definition = (BlockTableRecord)transaction.GetObject(definitionId, OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in definition)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue;
|
||||||
|
using var attribute = new AttributeReference();
|
||||||
|
attribute.SetAttributeFromBlock(source, reference.BlockTransform);
|
||||||
|
attribute.TextString = values[source.Tag];
|
||||||
|
attribute.UpdateMTextAttribute();
|
||||||
|
var row = rows.Single(value => source.Tag.EndsWith(value.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal));
|
||||||
|
attribute.XData = new ResultBuffer(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, row.StableId),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, source.Tag.StartsWith("NUMBER", StringComparison.OrdinalIgnoreCase) ? "MARKER" : "LINENUMBER"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataInteger32, row.MarkerNumber));
|
||||||
|
reference.AttributeCollection.AppendAttribute(attribute);
|
||||||
|
transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UpdateList(Transaction transaction, BlockReference reference, LineNumberListApplyPreflight plan)
|
||||||
|
{
|
||||||
|
var position = new Point3d(plan.Position.X, plan.Position.Y, plan.Position.Z);
|
||||||
|
var changed = reference.Position != position || reference.Rotation != plan.Rotation;
|
||||||
|
reference.UpgradeOpen();
|
||||||
|
reference.Position = position;
|
||||||
|
reference.Rotation = plan.Rotation;
|
||||||
|
var values = plan.Rows.SelectMany(row => new[]
|
||||||
|
{
|
||||||
|
new KeyValuePair<string, string>($"NUMBER{row.MarkerNumber}", row.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||||
|
new KeyValuePair<string, string>($"LINENUMBER{row.MarkerNumber}", row.Text.Replace("\n", "\\P", StringComparison.Ordinal))
|
||||||
|
}).ToDictionary(value => value.Key, value => value.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (ObjectId id in reference.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite);
|
||||||
|
if (!values.TryGetValue(attribute.Tag, out var value)) continue;
|
||||||
|
changed |= !string.Equals(attribute.TextString, value, StringComparison.Ordinal);
|
||||||
|
attribute.TextString = value;
|
||||||
|
attribute.UpdateMTextAttribute();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectId EnsureMarkerDefinition(Transaction transaction, Database database, BlockTable blocks, LineNumberMarkerAsset marker)
|
||||||
|
{
|
||||||
|
if (blocks.Has(MarkerBlockName)) return blocks[MarkerBlockName];
|
||||||
|
blocks.UpgradeOpen();
|
||||||
|
using var definition = new BlockTableRecord { Name = MarkerBlockName, Origin = Point3d.Origin };
|
||||||
|
var definitionId = blocks.Add(definition);
|
||||||
|
transaction.AddNewlyCreatedDBObject(definition, true);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
|
||||||
|
foreach (var text in marker.Texts)
|
||||||
|
{
|
||||||
|
using var attribute = new AttributeDefinition(
|
||||||
|
new Point3d(text.Position.X, text.Position.Y, 0), text.TextValue, MarkerAttributeTag, "Номер маркера:", styles[text.TextStyle])
|
||||||
|
{
|
||||||
|
Height = text.Size,
|
||||||
|
IsMTextAttributeDefinition = true,
|
||||||
|
LayerId = layers[text.Layer]
|
||||||
|
};
|
||||||
|
definition.AppendEntity(attribute);
|
||||||
|
transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
foreach (var source in marker.Lines)
|
||||||
|
{
|
||||||
|
using var line = new Line(new Point3d(source.Start.X, source.Start.Y, 0), new Point3d(source.End.X, source.End.Y, 0)) { LayerId = layers[source.Layer] };
|
||||||
|
definition.AppendEntity(line);
|
||||||
|
transaction.AddNewlyCreatedDBObject(line, true);
|
||||||
|
}
|
||||||
|
return definitionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateMarker(Transaction transaction, BlockTableRecord paper, ObjectId definitionId, LineNumberApplyPreflight item, LineNumberMarkerAsset marker)
|
||||||
|
{
|
||||||
|
using var reference = new BlockReference(new Point3d(item.Position.X, item.Position.Y, item.Position.Z), definitionId)
|
||||||
|
{
|
||||||
|
Rotation = item.Rotation,
|
||||||
|
Layer = marker.Layer
|
||||||
|
};
|
||||||
|
paper.AppendEntity(reference);
|
||||||
|
transaction.AddNewlyCreatedDBObject(reference, true);
|
||||||
|
reference.XData = Ownership(item);
|
||||||
|
|
||||||
|
var definition = (BlockTableRecord)transaction.GetObject(definitionId, OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in definition)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition source || source.Constant) continue;
|
||||||
|
using var attribute = new AttributeReference();
|
||||||
|
attribute.SetAttributeFromBlock(source, reference.BlockTransform);
|
||||||
|
attribute.TextString = item.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
attribute.XData = new ResultBuffer(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "MARKER"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataInteger32, item.MarkerNumber));
|
||||||
|
reference.AttributeCollection.AppendAttribute(attribute);
|
||||||
|
transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockReference? FindOwnedMarker(Transaction transaction, BlockTableRecord paper, string stableId)
|
||||||
|
{
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue;
|
||||||
|
using var data = reference.GetXDataForApplication(ApplicationName);
|
||||||
|
var values = data?.AsArray();
|
||||||
|
if (values?.Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, stableId, StringComparison.Ordinal)) == true)
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResultBuffer ListOwnership(string stableId) => new(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, stableId),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "LIST"));
|
||||||
|
|
||||||
|
private static bool UpdateMarker(Transaction transaction, BlockReference marker, LineNumberApplyPreflight item, string layer)
|
||||||
|
{
|
||||||
|
var position = new Point3d(item.Position.X, item.Position.Y, item.Position.Z);
|
||||||
|
using var existingOwnership = marker.GetXDataForApplication(ApplicationName);
|
||||||
|
var ownership = existingOwnership?.AsArray() ?? [];
|
||||||
|
var changed = marker.Position != position || marker.Rotation != item.Rotation ||
|
||||||
|
!string.Equals(marker.Layer, layer, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!ownership.Any(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value as string, item.Text, StringComparison.Ordinal));
|
||||||
|
marker.UpgradeOpen();
|
||||||
|
marker.Position = position;
|
||||||
|
marker.Rotation = item.Rotation;
|
||||||
|
marker.Layer = layer;
|
||||||
|
marker.XData = Ownership(item);
|
||||||
|
foreach (ObjectId id in marker.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForWrite);
|
||||||
|
if (!string.Equals(attribute.Tag, MarkerAttributeTag, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
var value = item.MarkerNumber.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
changed |= !string.Equals(attribute.TextString, value, StringComparison.Ordinal);
|
||||||
|
attribute.TextString = value;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResultBuffer Ownership(LineNumberApplyPreflight item) => new(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, item.StableId),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "MARKER"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataInteger32, item.MarkerNumber),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, item.Text));
|
||||||
|
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database)
|
||||||
|
{
|
||||||
|
var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
|
||||||
|
if (table.Has(ApplicationName)) return;
|
||||||
|
table.UpgradeOpen();
|
||||||
|
using var record = new RegAppTableRecord { Name = ApplicationName };
|
||||||
|
table.Add(record);
|
||||||
|
transaction.AddNewlyCreatedDBObject(record, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed record LineNumberApplyPermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
public required string ExpectedStableId { get; init; }
|
||||||
|
public required string ExpectedText { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record LineNumberEditApplyPermit
|
||||||
|
{
|
||||||
|
public required bool AllowApply { get; init; }
|
||||||
|
public required string FixtureMarkerPath { get; init; }
|
||||||
|
public required string ExpectedCommandName { get; init; }
|
||||||
|
public required string ExpectedSelectedObjectHandle { get; init; }
|
||||||
|
public required string ExpectedMutationHash { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class LineNumberApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_NUMBER_APPLY";
|
||||||
|
|
||||||
|
public static void Validate(Document document, LineNumberApplyPermit permit, LineNumberApplyPreflight operation)
|
||||||
|
{
|
||||||
|
if (!permit.AllowApply || !string.Equals(permit.ExpectedStableId, operation.StableId, StringComparison.Ordinal) || !string.Equals(permit.ExpectedText, operation.Text, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_APPLY_PERMIT_PLAN_MISMATCH");
|
||||||
|
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
var fixtureRoot = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(fixtureRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_NUMBER_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LineNumberApplyPermit Read()
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_APPLY_PERMIT_REQUIRED");
|
||||||
|
return JsonSerializer.Deserialize<LineNumberApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||||
|
?? throw new InvalidOperationException("LINE_NUMBER_APPLY_PERMIT_INVALID");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class LineNumberEditApplyPermitReader
|
||||||
|
{
|
||||||
|
private const string MarkerContent = "P_REC_ANNOT_TEST_FIXTURE=LINE_NUMBER_EDIT_APPLY";
|
||||||
|
|
||||||
|
public static LineNumberEditApplyPermit Read()
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_EDIT_APPLY_PERMIT_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_REQUIRED");
|
||||||
|
return JsonSerializer.Deserialize<LineNumberEditApplyPermit>(File.ReadAllText(path), new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
|
||||||
|
?? throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_INVALID");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Validate(Document document, LineNumberEditApplyPermit permit, string commandName, string selectedObjectHandle, IReadOnlyList<LineNumberRowMutation> mutations)
|
||||||
|
{
|
||||||
|
var mutationHash = LineNumberEditApplyPlan.ComputeMutationHash(commandName, selectedObjectHandle, mutations);
|
||||||
|
if (!permit.AllowApply ||
|
||||||
|
!string.Equals(permit.ExpectedCommandName, commandName, StringComparison.Ordinal) ||
|
||||||
|
!string.Equals(permit.ExpectedSelectedObjectHandle, selectedObjectHandle, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!string.Equals(permit.ExpectedMutationHash, mutationHash, StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_PERMIT_PLAN_MISMATCH");
|
||||||
|
if (!Path.IsPathFullyQualified(permit.FixtureMarkerPath) || permit.FixtureMarkerPath.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(permit.FixtureMarkerPath) || !string.Equals(File.ReadAllText(permit.FixtureMarkerPath).Trim(), MarkerContent, StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
var fixtureRoot = Path.GetDirectoryName(Path.GetFullPath(permit.FixtureMarkerPath))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(fixtureRoot, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_NUMBER_EDIT_APPLY_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class LineNumberEditCommands
|
||||||
|
{
|
||||||
|
public static void Renumber()
|
||||||
|
{
|
||||||
|
Run("PXP-ESN", (document, editor, selected) =>
|
||||||
|
{
|
||||||
|
var start = editor.GetInteger(new PromptIntegerOptions("\nВведите новое начальное значение маркера: ") { LowerLimit = 1 });
|
||||||
|
if (start.Status != PromptStatus.OK) return null;
|
||||||
|
var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected);
|
||||||
|
return LineNumberEditPlanner.Renumber(rows, start.Value);
|
||||||
|
}, "Выберите блок списка номеров линий: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EditText()
|
||||||
|
{
|
||||||
|
Run("PXP-ELN", (document, editor, selected) =>
|
||||||
|
{
|
||||||
|
var rows = new LineNumberOwnedAnnotationReader().ReadList(document, selected);
|
||||||
|
var marker = editor.GetInteger(new PromptIntegerOptions("\nВведите номер редактируемого маркера: ") { LowerLimit = 1 });
|
||||||
|
if (marker.Status != PromptStatus.OK) return null;
|
||||||
|
var row = rows.SingleOrDefault(value => value.MarkerNumber == marker.Value) ?? throw new InvalidOperationException("LINE_NUMBER_ROW_NOT_FOUND");
|
||||||
|
var text = editor.GetString(new PromptStringOptions("\nВведите новый текст номера линии: ") { AllowSpaces = true });
|
||||||
|
if (text.Status != PromptStatus.OK) return null;
|
||||||
|
return [LineNumberEditPlanner.EditText(row, text.StringResult)];
|
||||||
|
}, "Выберите блок списка номеров линий: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ResetMarkerPosition()
|
||||||
|
{
|
||||||
|
Run("PXP-RMP", (document, _, selected) =>
|
||||||
|
{
|
||||||
|
var row = new LineNumberOwnedAnnotationReader().ReadMarker(document, selected);
|
||||||
|
var configured = LineNumberRuntimeConfigurationReader.Read().Assets.Marker.Texts[0].Position;
|
||||||
|
return [LineNumberEditPlanner.ResetMarkerAttributePosition(row, configured)];
|
||||||
|
}, "Выберите маркер номера линии: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Run(string command, Func<Document, Editor, ObjectId, IReadOnlyList<LineNumberRowMutation>?> operation, string prompt)
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// These legacy editing paths are mutating commands. Until each
|
||||||
|
// command has a permit that binds its selected owned block and
|
||||||
|
// requested edit, DryRun must remain read-only rather than letting
|
||||||
|
// an editor prompt accidentally commit a transaction.
|
||||||
|
if (!IsApply())
|
||||||
|
{
|
||||||
|
document.Editor.WriteMessage($"\n{command} preview boundary: Apply permit is required; DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("LineNumberEditPreviewCompleted", commandName: command, resultStatus: "ApplyPermitRequired", document: document);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var options = new PromptEntityOptions($"\n{prompt}");
|
||||||
|
options.SetRejectMessage("\nТребуется вхождение блока PXP-LN.");
|
||||||
|
options.AddAllowedClass(typeof(BlockReference), true);
|
||||||
|
var selected = document.Editor.GetEntity(options);
|
||||||
|
if (selected.Status != PromptStatus.OK) return;
|
||||||
|
var mutations = operation(document, document.Editor, selected.ObjectId);
|
||||||
|
if (mutations is null) return;
|
||||||
|
var selectedHandle = selected.ObjectId.Handle.ToString();
|
||||||
|
LineNumberEditApplyPermitReader.Validate(document, LineNumberEditApplyPermitReader.Read(), command, selectedHandle, mutations);
|
||||||
|
var result = new LineNumberAnnotationWriter().ApplyEdits(document, mutations);
|
||||||
|
document.Editor.WriteMessage($"\n{command}: updated={result.Updated}; unchanged={result.Unchanged}.");
|
||||||
|
RuntimeDiagnostics.Write("LineNumberEditCompleted", commandName: command, resultStatus: result.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document,
|
||||||
|
details: new Dictionary<string, object?> { ["Updated"] = result.Updated, ["AlreadyCurrent"] = result.Unchanged });
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("LineNumberEditFailed", "ERROR", commandName: command, resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
document.Editor.WriteMessage($"\n{command}: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsApply() => string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class LineNumberOwnedAnnotationReader
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-LN";
|
||||||
|
|
||||||
|
public IReadOnlyList<LineNumberOwnedRow> ReadList(Document document, ObjectId listId)
|
||||||
|
{
|
||||||
|
using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(listId, OpenMode.ForRead) is not BlockReference list || !HasRole(list, "LIST"))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_LIST_NOT_SELECTED");
|
||||||
|
|
||||||
|
var rows = new Dictionary<int, (string? StableId, string? Text)>();
|
||||||
|
foreach (ObjectId id in list.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForRead);
|
||||||
|
var ownership = ReadOwnership(attribute);
|
||||||
|
if (ownership is null) continue;
|
||||||
|
rows.TryGetValue(ownership.Value.MarkerNumber, out var row);
|
||||||
|
rows[ownership.Value.MarkerNumber] = ownership.Value.Role == "LINENUMBER"
|
||||||
|
? (ownership.Value.StableId, attribute.TextString.Replace("\\P", "\n", StringComparison.Ordinal))
|
||||||
|
: (ownership.Value.StableId ?? row.StableId, row.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = rows.OrderBy(value => value.Key)
|
||||||
|
.Select(value => new LineNumberOwnedRow(
|
||||||
|
value.Value.StableId ?? throw new InvalidOperationException("LINE_NUMBER_ROW_STABLE_ID_MISSING"),
|
||||||
|
value.Key,
|
||||||
|
value.Value.Text ?? throw new InvalidOperationException("LINE_NUMBER_ROW_TEXT_MISSING"),
|
||||||
|
default))
|
||||||
|
.ToArray();
|
||||||
|
if (result.Length == 0) throw new InvalidOperationException("LINE_NUMBER_LIST_EMPTY");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LineNumberOwnedRow ReadMarker(Document document, ObjectId markerId)
|
||||||
|
{
|
||||||
|
using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(markerId, OpenMode.ForRead) is not BlockReference marker)
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_MARKER_NOT_SELECTED");
|
||||||
|
var values = marker.GetXDataForApplication(ApplicationName)?.AsArray() ?? [];
|
||||||
|
var stableId = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string)
|
||||||
|
.FirstOrDefault(value => !string.Equals(value, "MARKER", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var markerNumber = values.Where(value => value.TypeCode == 1071).Select(value => Convert.ToInt32(value.Value)).FirstOrDefault();
|
||||||
|
var text = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).LastOrDefault() ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(stableId) || markerNumber <= 0) throw new InvalidOperationException("LINE_NUMBER_MARKER_OWNERSHIP_MISSING");
|
||||||
|
var localPosition = Point3d.Origin;
|
||||||
|
foreach (ObjectId id in marker.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = (AttributeReference)transaction.GetObject(id, OpenMode.ForRead);
|
||||||
|
if (!string.Equals(attribute.Tag, "NUMBER", StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
localPosition = attribute.Position.TransformBy(marker.BlockTransform.Inverse());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return new(stableId, markerNumber, text, new(localPosition.X, localPosition.Y, localPosition.Z));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<LineNumberOwnedRow> ReadMarkers(Document document)
|
||||||
|
{
|
||||||
|
using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
var rows = new List<LineNumberOwnedRow>();
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not BlockReference marker || !HasRole(marker, "MARKER")) continue;
|
||||||
|
var values = marker.GetXDataForApplication(ApplicationName)?.AsArray() ?? [];
|
||||||
|
var strings = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).ToArray();
|
||||||
|
var stableId = strings.FirstOrDefault(value => !string.Equals(value, "MARKER", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var markerNumber = values.Where(value => value.TypeCode == 1071).Select(value => Convert.ToInt32(value.Value)).FirstOrDefault();
|
||||||
|
var text = strings.LastOrDefault() ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(stableId) || markerNumber <= 0) continue;
|
||||||
|
rows.Add(new(stableId, markerNumber, text, default));
|
||||||
|
}
|
||||||
|
return rows.OrderBy(value => value.MarkerNumber).ThenBy(value => value.SourceStableId, StringComparer.Ordinal).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasRole(BlockReference reference, string role) =>
|
||||||
|
reference.GetXDataForApplication(ApplicationName)?.AsArray().Any(value =>
|
||||||
|
value.TypeCode == 1000 && string.Equals(value.Value as string, role, StringComparison.OrdinalIgnoreCase)) == true;
|
||||||
|
|
||||||
|
private static (string? StableId, string Role, int MarkerNumber)? ReadOwnership(AttributeReference attribute)
|
||||||
|
{
|
||||||
|
var values = attribute.GetXDataForApplication(ApplicationName)?.AsArray();
|
||||||
|
if (values is null) return null;
|
||||||
|
var strings = values.Where(value => value.TypeCode == 1000).Select(value => value.Value as string).ToArray();
|
||||||
|
var role = strings.FirstOrDefault(value => value is "MARKER" or "LINENUMBER");
|
||||||
|
var number = values.Where(value => value.TypeCode == 1071).Select(value => Convert.ToInt32(value.Value)).FirstOrDefault();
|
||||||
|
return role is null || number <= 0 ? null : (strings.FirstOrDefault(value => value != role), role, number);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// PXP-ULN refreshes the source-backed text of already-owned annotations. The
|
||||||
|
// legacy command drives the same operation across selected Ortho drawings; the
|
||||||
|
// modern command keeps the mutation pipeline scoped to the active controlled
|
||||||
|
// drawing until the Ortho drawing enumerator is ported.
|
||||||
|
internal static class LineNumberRefreshCommand
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var configuration = LineNumberRuntimeConfigurationReader.Read();
|
||||||
|
var definition = configuration.Annotations.Annotations.OrderBy(value => value.Nickname, StringComparer.Ordinal).First();
|
||||||
|
var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("LINE_NUMBER_PIPING_DATALINKS_UNAVAILABLE");
|
||||||
|
var owned = new LineNumberOwnedAnnotationReader().ReadMarkers(document);
|
||||||
|
if (owned.Count == 0) throw new InvalidOperationException("LINE_NUMBER_OWNED_MARKERS_NOT_FOUND");
|
||||||
|
var candidates = owned.Select(row => ReadCandidate(document.Database, row, manager, configuration.Assets)).ToArray();
|
||||||
|
var preview = new LineNumberCommandPreviewService(
|
||||||
|
new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()),
|
||||||
|
new LineNumberFieldResolver(new PlantDataLinksPropertyReader(manager)))
|
||||||
|
.Preview(definition, candidates, owned.Select(row => new ExistingLineNumberAnnotation(row.SourceStableId, row.SourceStableId, row.Text)));
|
||||||
|
if (!preview.Success) throw new InvalidOperationException(preview.FailureCode);
|
||||||
|
|
||||||
|
var rows = owned.ToDictionary(row => row.SourceStableId, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var apply = preview.Plan!.Operations.Where(operation => operation.Kind is AnnotationOperationKind.Create or AnnotationOperationKind.Update)
|
||||||
|
.Select(operation => new LineNumberApplyPreflight(operation.StableId, rows[operation.StableId].MarkerNumber, operation.Text!, operation.Position, operation.Rotation, operation.Layer, operation.TextStyle))
|
||||||
|
.OrderBy(operation => operation.MarkerNumber).ToArray();
|
||||||
|
if (!IsApply())
|
||||||
|
{
|
||||||
|
document.Editor.WriteMessage($"\nPXP-ULN preview: changes={apply.Length}; DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("LineNumberRefreshPreviewCompleted", commandName: "PXP-ULN", resultStatus: "PreviewCompleted", document: document,
|
||||||
|
details: new Dictionary<string, object?> { ["OperationCount"] = apply.Length, ["Scope"] = "ActiveDrawing" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The controlled Apply contract is deliberately narrow: a permit
|
||||||
|
// binds one resolved source/text pair, preventing a refresh from
|
||||||
|
// widening into an unreviewed drawing-wide write.
|
||||||
|
if (apply.Length != 1) throw new InvalidOperationException("LINE_NUMBER_REFRESH_APPLY_REQUIRES_EXACTLY_ONE_OPERATION");
|
||||||
|
LineNumberApplyPermitReader.Validate(document, LineNumberApplyPermitReader.Read(), apply[0]);
|
||||||
|
var writer = new LineNumberAnnotationWriter();
|
||||||
|
var markerResult = writer.Apply(document, apply, configuration.Assets);
|
||||||
|
var mutations = apply.Select(item => new LineNumberRowMutation(item.StableId, rows[item.StableId].MarkerNumber, item.MarkerNumber, item.Text, null)).ToArray();
|
||||||
|
var listResult = writer.ApplyEdits(document, mutations);
|
||||||
|
document.Editor.WriteMessage($"\nPXP-ULN: updated markers={markerResult.Updated}; lists={listResult.Updated}; already-current={markerResult.Unchanged}.");
|
||||||
|
RuntimeDiagnostics.Write("LineNumberRefreshCompleted", commandName: "PXP-ULN", resultStatus: markerResult.Updated > 0 ? "Updated" : "AlreadyCurrent", document: document,
|
||||||
|
details: new Dictionary<string, object?> { ["UpdatedMarkers"] = markerResult.Updated, ["UpdatedLists"] = listResult.Updated, ["AlreadyCurrent"] = markerResult.Unchanged, ["Scope"] = "ActiveDrawing" });
|
||||||
|
}
|
||||||
|
catch (System.Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("LineNumberRefreshFailed", "ERROR", commandName: "PXP-ULN", resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
document.Editor.WriteMessage($"\nPXP-ULN: {exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsApply() => string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static LineNumberCandidate ReadCandidate(Database database, LineNumberOwnedRow row, Autodesk.ProcessPower.DataLinks.DataLinksManager manager, AnnotateLineNumbersAssets assets)
|
||||||
|
{
|
||||||
|
const string prefix = "acad:";
|
||||||
|
if (!row.SourceStableId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("LINE_NUMBER_SOURCE_ID_UNSUPPORTED");
|
||||||
|
if (!long.TryParse(row.SourceStableId[prefix.Length..], System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var handle))
|
||||||
|
throw new InvalidOperationException("LINE_NUMBER_SOURCE_HANDLE_INVALID");
|
||||||
|
var id = database.GetObjectId(false, new Handle(handle), 0);
|
||||||
|
var read = new PlantLineNumberCandidateReader(manager).Read(id, assets.Marker.Layer, assets.Marker.Texts[0].TextStyle);
|
||||||
|
if (!read.Success) throw new InvalidOperationException(read.FailureCode);
|
||||||
|
return read.Candidate!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed record LineNumberRuntimeConfiguration(LineNumberAnnotationSettings Annotations, AnnotateLineNumbersAssets Assets, LineConnectionAssets Connections);
|
||||||
|
|
||||||
|
internal static class LineNumberRuntimeConfigurationReader
|
||||||
|
{
|
||||||
|
public static LineNumberRuntimeConfiguration Read()
|
||||||
|
{
|
||||||
|
var annotationPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_ANNOTATION_SETTINGS_PATH");
|
||||||
|
var drawingPath = RequiredLocalFile("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||||
|
var drawingXml = File.ReadAllText(drawingPath);
|
||||||
|
return new(new LineNumberAnnotationSettingsLoader().Load(File.ReadAllText(annotationPath)), new AnnotateLineNumbersSettingsLoader().Load(drawingXml), new LineConnectionSettingsLoader().Load(drawingXml));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RequiredLocalFile(string variable)
|
||||||
|
{
|
||||||
|
var path = Environment.GetEnvironmentVariable(variable);
|
||||||
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path) || path.StartsWith("\\\\", StringComparison.Ordinal) || !File.Exists(path))
|
||||||
|
throw new InvalidOperationException($"LINE_NUMBER_CONFIGURATION_REQUIRED:{variable}");
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class ManualAnnotationEditCommand
|
||||||
|
{
|
||||||
|
private const string AppName = "P_REC_ANNOT_MANUAL";
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
var options = new PromptEntityOptions("\nSelect owned manual annotation to edit: ");
|
||||||
|
options.SetRejectMessage("\nSelect a block reference.");
|
||||||
|
options.AddAllowedClass(typeof(BlockReference), true);
|
||||||
|
var selection = document.Editor.GetEntity(options);
|
||||||
|
if (selection.Status != PromptStatus.OK) return;
|
||||||
|
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var reference = transaction.GetObject(selection.ObjectId, OpenMode.ForWrite) as BlockReference;
|
||||||
|
if (reference is null) return;
|
||||||
|
var state = ReadState(reference);
|
||||||
|
if (state is null)
|
||||||
|
{
|
||||||
|
document.Editor.WriteMessage("\nManual annotation state is missing or invalid; no migration was performed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.Editor.WriteMessage($"\nRestored {state.Kind}; datum={state.DatumMode}; offset={state.DatumOffset:0.###}.");
|
||||||
|
foreach (ObjectId attributeId in reference.AttributeCollection)
|
||||||
|
{
|
||||||
|
var attribute = transaction.GetObject(attributeId, OpenMode.ForWrite) as AttributeReference;
|
||||||
|
if (attribute is null) continue;
|
||||||
|
var prompt = new PromptStringOptions($"\n{attribute.Tag} <{attribute.TextString}>: ") { AllowSpaces = true, UseDefaultValue = true, DefaultValue = attribute.TextString };
|
||||||
|
var value = document.Editor.GetString(prompt);
|
||||||
|
if (value.Status == PromptStatus.OK) attribute.TextString = value.StringResult;
|
||||||
|
else if (value.Status is PromptStatus.Cancel or PromptStatus.Error) return;
|
||||||
|
}
|
||||||
|
reference.XData = new ResultBuffer(new TypedValue((int)DxfCode.ExtendedDataRegAppName, AppName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, state.Serialize()));
|
||||||
|
transaction.Commit();
|
||||||
|
document.Editor.WriteMessage("\nManual annotation updated; native AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("ManualAnnotationEditCompleted", commandName: "CAC1", resultStatus: "Updated", document: document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ManualAnnotationState? ReadState(BlockReference reference)
|
||||||
|
{
|
||||||
|
using var data = reference.GetXDataForApplication(AppName);
|
||||||
|
var serialized = data?.AsArray().FirstOrDefault(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Value as string;
|
||||||
|
return ManualAnnotationState.Deserialize(serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.AutoCAD.Windows;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using WinForms = System.Windows.Forms;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class ManualAnnotationPalette
|
||||||
|
{
|
||||||
|
private static readonly Guid PaletteId = new("dc74a715-60cc-6f6b-8dc7-ccf6a3e52d60");
|
||||||
|
private static PaletteSet? palette;
|
||||||
|
private static ManualAnnotationControl? control;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated -= DocumentActivated;
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated += DocumentActivated;
|
||||||
|
}
|
||||||
|
public static void Terminate()
|
||||||
|
{
|
||||||
|
AcadApplication.DocumentManager.DocumentActivated -= DocumentActivated;
|
||||||
|
if (palette is not null) palette.Visible = false;
|
||||||
|
}
|
||||||
|
public static void Show()
|
||||||
|
{
|
||||||
|
if (palette is null)
|
||||||
|
{
|
||||||
|
control = new ManualAnnotationControl();
|
||||||
|
palette = new PaletteSet("Ortho drawing annotations", "P_REC_ANNOT_MANUAL", PaletteId)
|
||||||
|
{ Style = PaletteSetStyles.NameEditable | PaletteSetStyles.ShowPropertiesMenu | PaletteSetStyles.ShowAutoHideButton | PaletteSetStyles.ShowCloseButton };
|
||||||
|
palette.Add("Annotations", control);
|
||||||
|
}
|
||||||
|
control!.RefreshAvailability(); palette.Visible = true;
|
||||||
|
RuntimeDiagnostics.Write("ManualAnnotationPaletteShown", resultStatus: "Visible");
|
||||||
|
}
|
||||||
|
public static void PlaceSelected() => control?.Place();
|
||||||
|
public static ManualAnnotationState CurrentState() => control?.CurrentState() ?? new ManualAnnotationState("AutoCadBlock", ManualDatumMode.Absolute, 0);
|
||||||
|
private static void DocumentActivated(object sender, Autodesk.AutoCAD.ApplicationServices.DocumentCollectionEventArgs args) => control?.RefreshAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ManualAnnotationControl : WinForms.UserControl
|
||||||
|
{
|
||||||
|
private readonly WinForms.FlowLayoutPanel choices = new() { Dock = WinForms.DockStyle.Top, AutoSize = true, FlowDirection = WinForms.FlowDirection.TopDown, WrapContents = false };
|
||||||
|
private readonly WinForms.Button place = new() { Text = "Place", AutoSize = true };
|
||||||
|
private readonly WinForms.Button reconnect = new() { Text = "Reconnect / refresh", AutoSize = true };
|
||||||
|
private readonly WinForms.Button help = new() { Text = "Help", AutoSize = true };
|
||||||
|
private readonly WinForms.RadioButton absoluteDatum = new() { Text = "Absolute datum", AutoSize = true, Checked = true };
|
||||||
|
private readonly WinForms.RadioButton relativeDatum = new() { Text = "Relative datum", AutoSize = true };
|
||||||
|
private readonly WinForms.NumericUpDown datumOffset = new() { DecimalPlaces = 3, Minimum = -100000, Maximum = 100000, Increment = 0.001M, Enabled = false };
|
||||||
|
private readonly Dictionary<WinForms.RadioButton, ManualAnnotationKind> kinds = new();
|
||||||
|
public ManualAnnotationControl()
|
||||||
|
{
|
||||||
|
Dock = WinForms.DockStyle.Fill; AutoScroll = true;
|
||||||
|
Add("Line number", ManualAnnotationKind.LineNumber, true); Add("Valve or nozzle", ManualAnnotationKind.ValveOrNozzle); Add("Flow direction", ManualAnnotationKind.FlowDirection);
|
||||||
|
Add("Instrument position", ManualAnnotationKind.InstrumentPosition); Add("Section elevation", ManualAnnotationKind.SectionElevation); Add("Instrument connection point", ManualAnnotationKind.InstrumentConnection);
|
||||||
|
Add("AutoCAD block", ManualAnnotationKind.AutoCadBlock); Add("Platform elevation", ManualAnnotationKind.PlatformElevation); Add("Other elevation", ManualAnnotationKind.OtherElevation);
|
||||||
|
place.Click += (_, _) => Place(); reconnect.Click += (_, _) => Reconnect(); help.Click += (_, _) => OpenHelp();
|
||||||
|
var datum = new WinForms.FlowLayoutPanel { Dock = WinForms.DockStyle.Top, AutoSize = true }; datum.Controls.Add(absoluteDatum); datum.Controls.Add(relativeDatum); datum.Controls.Add(datumOffset);
|
||||||
|
absoluteDatum.CheckedChanged += (_, _) => datumOffset.Enabled = relativeDatum.Checked;
|
||||||
|
relativeDatum.CheckedChanged += (_, _) => datumOffset.Enabled = relativeDatum.Checked;
|
||||||
|
var actions = new WinForms.FlowLayoutPanel { Dock = WinForms.DockStyle.Top, AutoSize = true }; actions.Controls.Add(place); actions.Controls.Add(reconnect); actions.Controls.Add(help);
|
||||||
|
Controls.Add(datum); Controls.Add(actions); Controls.Add(choices); RefreshTheme();
|
||||||
|
}
|
||||||
|
private void Add(string text, ManualAnnotationKind kind, bool selected = false) { var button = new WinForms.RadioButton { Text = text, AutoSize = true, Checked = selected }; kinds.Add(button, kind); choices.Controls.Add(button); }
|
||||||
|
public void RefreshAvailability() { var available = AcadApplication.DocumentManager.MdiActiveDocument is not null && PlantApplication.CurrentProject is not null; place.Enabled = available; reconnect.Enabled = available; RefreshTheme(); }
|
||||||
|
public void Place()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument; if (document is null) return;
|
||||||
|
var kind = kinds.First(value => value.Key.Checked).Value;
|
||||||
|
var command = ManualAnnotationCommandRouter.Resolve(kind);
|
||||||
|
document.SendStringToExecute(command + " ", true, false, false);
|
||||||
|
RuntimeDiagnostics.Write("ManualAnnotationActionInvoked", commandName: command, resultStatus: kind.ToString(), document: document);
|
||||||
|
}
|
||||||
|
public ManualAnnotationState CurrentState() => new(kinds.First(value => value.Key.Checked).Value.ToString(), relativeDatum.Checked ? ManualDatumMode.Relative : ManualDatumMode.Absolute, (double)datumOffset.Value);
|
||||||
|
private static void Reconnect() => AcadApplication.DocumentManager.MdiActiveDocument?.SendStringToExecute("PXP-ULN ", true, false, false);
|
||||||
|
private static void OpenHelp()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetDirectoryName(typeof(ManualAnnotationControl).Assembly.Location)!, "ManualAnnotationHelp.pdf");
|
||||||
|
if (File.Exists(path)) System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
private void RefreshTheme()
|
||||||
|
{
|
||||||
|
var dark = Convert.ToInt16(AcadApplication.GetSystemVariable("COLORTHEME")) == 0;
|
||||||
|
BackColor = dark ? System.Drawing.Color.FromArgb(92, 92, 92) : System.Drawing.Color.FromArgb(245, 245, 245); ForeColor = dark ? System.Drawing.Color.White : System.Drawing.Color.Black;
|
||||||
|
foreach (WinForms.Control child in choices.Controls) { child.BackColor = BackColor; child.ForeColor = ForeColor; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ManualBlockInsertionCommand
|
||||||
|
{
|
||||||
|
public static void Run(ManualAnnotationState state)
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument; if (document is null) return;
|
||||||
|
var name = document.Editor.GetString(new PromptStringOptions("\nBlock name: ") { AllowSpaces = true }); if (name.Status != PromptStatus.OK || string.IsNullOrWhiteSpace(name.StringResult)) return;
|
||||||
|
var point = document.Editor.GetPoint("\nInsertion point: "); if (point.Status != PromptStatus.OK) return;
|
||||||
|
var angle = document.Editor.GetAngle(new PromptAngleOptions("\nRotation <0>: ") { AllowNone = true, BasePoint = point.Value, UseBasePoint = true }); if (angle.Status is not (PromptStatus.OK or PromptStatus.None)) return;
|
||||||
|
using var documentLock = document.LockDocument(); using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead); if (!blocks.Has(name.StringResult)) { document.Editor.WriteMessage("\nBlock definition not found."); return; }
|
||||||
|
var space = (BlockTableRecord)transaction.GetObject(document.Database.CurrentSpaceId, OpenMode.ForWrite); var definition = (BlockTableRecord)transaction.GetObject(blocks[name.StringResult], OpenMode.ForRead);
|
||||||
|
using var reference = new BlockReference(point.Value, definition.ObjectId) { Rotation = angle.Status == PromptStatus.OK ? angle.Value : 0 };
|
||||||
|
space.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
|
||||||
|
var regApps = (RegAppTable)transaction.GetObject(document.Database.RegAppTableId, OpenMode.ForRead);
|
||||||
|
const string appName = "P_REC_ANNOT_MANUAL";
|
||||||
|
if (!regApps.Has(appName)) { regApps.UpgradeOpen(); using var app = new RegAppTableRecord { Name = appName }; regApps.Add(app); transaction.AddNewlyCreatedDBObject(app, true); }
|
||||||
|
reference.XData = new ResultBuffer(new TypedValue((int)DxfCode.ExtendedDataRegAppName, appName), new TypedValue((int)DxfCode.ExtendedDataAsciiString, state.Serialize()));
|
||||||
|
foreach (ObjectId id in definition)
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is AttributeDefinition source && !source.Constant)
|
||||||
|
{ var value = document.Editor.GetString(new PromptStringOptions($"\n{source.Tag} <{source.TextString}>: ") { AllowSpaces = true }); using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(source, reference.BlockTransform); attribute.TextString = value.Status == PromptStatus.OK ? value.StringResult : source.TextString; reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true); }
|
||||||
|
transaction.Commit(); document.Editor.WriteMessage("\nManual block placed; AutoCAD UNDO is available.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Source port of OrthoAutoAnnotate.Commands.RemoveAnnotations (PXP-RA).
|
||||||
|
// The modern writer records the viewport identity in PXP-OA XData, allowing
|
||||||
|
// deterministic removal without depending on the historical named-dictionary scan.
|
||||||
|
internal static class OrthoAnnotationRemovalCommand
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-OA";
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new PromptEntityOptions("\nSelect Ortho viewport: ");
|
||||||
|
options.SetRejectMessage("\nA viewport is required.");
|
||||||
|
options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
var selected = document.Editor.GetEntity(options);
|
||||||
|
if (selected.Status != PromptStatus.OK) return;
|
||||||
|
var viewport = Resolve(document, selected.ObjectId);
|
||||||
|
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForRead);
|
||||||
|
var removed = 0;
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
var entity = transaction.GetObject(id, OpenMode.ForRead) as Entity;
|
||||||
|
if (entity is null || !OwnedByViewport(entity, viewport.Id)) continue;
|
||||||
|
entity.UpgradeOpen();
|
||||||
|
entity.Erase();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
transaction.Commit();
|
||||||
|
document.Editor.WriteMessage($"\nPXP-RA: removed={removed}; native AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("OrthoAnnotationRemovalCompleted", commandName: "PXP-RA", resultStatus: "Completed", document: document, details: new Dictionary<string, object?> { ["Removed"] = removed, ["Viewport"] = viewport.Id });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoAnnotationRemovalFailed", "ERROR", commandName: "PXP-RA", resultStatus: exception.Message, exception: exception, document: document);
|
||||||
|
document.Editor.WriteMessage("\nPXP-RA: " + exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoViewportInfo Resolve(Document document, ObjectId selectedId)
|
||||||
|
{
|
||||||
|
using var transaction = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var selected = (Viewport)transaction.GetObject(selectedId, OpenMode.ForRead);
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_PROJECT_UNAVAILABLE");
|
||||||
|
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
|
||||||
|
.Where(x => Math.Abs(x.CenterX - selected.CenterPoint.X) < .001 && Math.Abs(x.CenterY - selected.CenterPoint.Y) < .001 && Math.Abs(x.Width - selected.Width) < .001 && Math.Abs(x.Height - selected.Height) < .001)
|
||||||
|
.ToArray();
|
||||||
|
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ORTHO_VIEW_NOT_FOUND" : "ORTHO_VIEW_AMBIGUOUS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OwnedByViewport(Entity entity, string viewportId)
|
||||||
|
{
|
||||||
|
using var data = entity.GetXDataForApplication(ApplicationName);
|
||||||
|
var values = data?.AsArray().Where(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(value => value.Value?.ToString()).ToArray();
|
||||||
|
return values is { Length: >= 2 } && string.Equals(values[0], "VIEWPORT", StringComparison.Ordinal) && string.Equals(values[1], viewportId, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class OrthoAnnotationWriter
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-OA";
|
||||||
|
public string Replace(Document document, OrthoViewportInfo viewport, PlantObjectSnapshot source, OrthoAnnotationClassification classification, OrthoAnnotationAssets assets)
|
||||||
|
{
|
||||||
|
if (source.Geometry.PaperPosition is not { } point || classification.Representation == OrthoAnnotationRepresentation.None) throw new InvalidOperationException("ORTHO_ANNOTATION_OPERATION_INVALID");
|
||||||
|
using var documentLock = document.LockDocument(); using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var blocks = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||||
|
var styles = (TextStyleTable)transaction.GetObject(document.Database.TextStyleTableId, OpenMode.ForRead);
|
||||||
|
var leaders = (DBDictionary)transaction.GetObject(document.Database.MLeaderStyleDictionaryId, OpenMode.ForRead);
|
||||||
|
if (!layers.Has(assets.Layer)) throw new InvalidOperationException("ORTHO_ANNOTATION_LAYER_MISSING:" + assets.Layer);
|
||||||
|
if (!styles.Has(assets.TextStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_TEXT_STYLE_MISSING:" + assets.TextStyle);
|
||||||
|
if (!leaders.Contains(assets.MultiLeaderStyle)) throw new InvalidOperationException("ORTHO_ANNOTATION_MLEADER_STYLE_MISSING:" + assets.MultiLeaderStyle);
|
||||||
|
if (classification.Representation == OrthoAnnotationRepresentation.BubbleBlock && !blocks.Has(assets.BubbleBlock)) throw new InvalidOperationException("ORTHO_ANNOTATION_BUBBLE_BLOCK_MISSING:" + assets.BubbleBlock);
|
||||||
|
EnsureRegApp(transaction, document.Database);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(blocks[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
var deleted = 0;
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
var entity = transaction.GetObject(id, OpenMode.ForRead) as Entity; if (entity is null || !OwnedBy(entity, source.SnapshotId)) continue;
|
||||||
|
entity.UpgradeOpen(); entity.Erase(); deleted++;
|
||||||
|
}
|
||||||
|
var start = new Point3d(point.X, point.Y, 0); var right = point.X >= viewport.CenterX;
|
||||||
|
var land = new Point3d(viewport.CenterX + (right ? viewport.Width / 2d + 5d : -viewport.Width / 2d - 5d), point.Y, 0);
|
||||||
|
if (classification.Representation == OrthoAnnotationRepresentation.MultiLeader)
|
||||||
|
{
|
||||||
|
using var leader = new MLeader { LayerId = layers[assets.Layer], MLeaderStyle = leaders.GetAt(assets.MultiLeaderStyle), ContentType = ContentType.MTextContent, LeaderLineType = LeaderType.StraightLeader };
|
||||||
|
var leaderId = leader.AddLeader(); var lineId = leader.AddLeaderLine(leaderId); leader.AddFirstVertex(lineId, start); leader.AddLastVertex(lineId, land);
|
||||||
|
using var text = new MText { Contents = classification.Text, TextStyleId = styles[assets.TextStyle], Location = land, BackgroundFill = true, UseBackgroundColor = true, BackgroundScaleFactor = 1.1 };
|
||||||
|
leader.MText = text; leader.TextLocation = land; leader.XData = Ownership(viewport, source, classification);
|
||||||
|
paper.AppendEntity(leader); transaction.AddNewlyCreatedDBObject(leader, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var definition = (BlockTableRecord)transaction.GetObject(blocks[assets.BubbleBlock], OpenMode.ForRead);
|
||||||
|
using var reference = new BlockReference(start, definition.ObjectId) { LayerId = layers[assets.Layer], XData = Ownership(viewport, source, classification) };
|
||||||
|
paper.AppendEntity(reference); transaction.AddNewlyCreatedDBObject(reference, true);
|
||||||
|
foreach (ObjectId id in definition)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not AttributeDefinition definitionAttribute || definitionAttribute.Constant) continue;
|
||||||
|
using var attribute = new AttributeReference(); attribute.SetAttributeFromBlock(definitionAttribute, reference.BlockTransform); attribute.TextString = classification.Text; attribute.TextStyleId = styles[assets.TextStyle]; attribute.UpdateMTextAttribute(); reference.AttributeCollection.AppendAttribute(attribute); transaction.AddNewlyCreatedDBObject(attribute, true);
|
||||||
|
}
|
||||||
|
if (reference.IsDynamicBlock) foreach (DynamicBlockReferenceProperty property in reference.DynamicBlockReferencePropertyCollection)
|
||||||
|
{
|
||||||
|
if (property.ReadOnly) continue;
|
||||||
|
if (string.Equals(property.PropertyName, "P1 X", StringComparison.OrdinalIgnoreCase)) property.Value = land.X - start.X;
|
||||||
|
else if (string.Equals(property.PropertyName, "P1 Y", StringComparison.OrdinalIgnoreCase)) property.Value = land.Y - start.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transaction.Commit(); return deleted == 0 ? "Created" : "Updated";
|
||||||
|
}
|
||||||
|
private static bool OwnedBy(Entity entity, string source) => entity.GetXDataForApplication(ApplicationName)?.AsArray().Any(x => x.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(x.Value?.ToString(), source, StringComparison.Ordinal)) == true;
|
||||||
|
private static ResultBuffer Ownership(OrthoViewportInfo viewport, PlantObjectSnapshot source, OrthoAnnotationClassification value)
|
||||||
|
{
|
||||||
|
source.Properties.TryGetValue("PRecAnnot.RowId", out var rowId);
|
||||||
|
return new ResultBuffer(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, ApplicationName),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "VIEWPORT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport.Id),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, source.SnapshotId),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE_DRAWING"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport.SourceDrawingName ?? string.Empty),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCE_ROW"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, rowId ?? string.Empty),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "ANNOTATIONTEXT"), new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Text),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, value.Representation.ToString()));
|
||||||
|
}
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database) { var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead); if (table.Has(ApplicationName)) return; table.UpgradeOpen(); using var record = new RegAppTableRecord { Name = ApplicationName }; table.Add(record); transaction.AddNewlyCreatedDBObject(record, true); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Source-backed PXP-DJ mutation boundary. The legacy command dimensions the
|
||||||
|
// selected PXP-OA annotation blocks, rejects mixed viewport scales and keeps
|
||||||
|
// all dimension creation in one transaction so native UNDO restores the prior
|
||||||
|
// state. The Plant-specific candidate discovery remains deliberately typed
|
||||||
|
// through the PXP-OA ownership contract.
|
||||||
|
internal static class OrthoDimensionCommand
|
||||||
|
{
|
||||||
|
private const string AnnotationApp = "PXP-OA";
|
||||||
|
private const string DimensionApp = "PXP-DJ";
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
var selection = document.Editor.GetSelection(new SelectionFilter(new[] { new TypedValue((int)DxfCode.ExtendedDataRegAppName, AnnotationApp) }));
|
||||||
|
if (selection.Status != PromptStatus.OK || selection.Value.Count < 2)
|
||||||
|
{
|
||||||
|
document.Editor.WriteMessage("\nSelect at least two PXP-OA annotations.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var read = document.Database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var points = new List<(ObjectId Id, Point3d Position, string Viewport)>();
|
||||||
|
foreach (var id in selection.Value.GetObjectIds())
|
||||||
|
{
|
||||||
|
if (read.GetObject(id, OpenMode.ForRead) is not BlockReference reference) continue;
|
||||||
|
if (!TryReadViewport(reference, out var viewport)) continue;
|
||||||
|
points.Add((id, reference.Position, viewport));
|
||||||
|
}
|
||||||
|
read.Commit();
|
||||||
|
if (points.Count < 2 || points.Select(point => point.Viewport).Distinct(StringComparer.Ordinal).Count() != 1)
|
||||||
|
{
|
||||||
|
document.Editor.WriteMessage("\nSelected annotations must belong to one Ortho viewport.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
points = points.OrderBy(point => point.Position.X).ThenBy(point => point.Position.Y).ThenBy(point => point.Id.Handle.Value).ToList();
|
||||||
|
var defaultAngle = Math.Abs(points[^1].Position.X - points[0].Position.X) >= Math.Abs(points[^1].Position.Y - points[0].Position.Y) ? 0d : Math.PI / 2d;
|
||||||
|
var angle = document.Editor.GetAngle(new PromptAngleOptions("\nDimension rotation <automatic>: ") { AllowNone = true });
|
||||||
|
if (angle.Status is not (PromptStatus.OK or PromptStatus.None)) return;
|
||||||
|
var rotation = angle.Status == PromptStatus.OK ? angle.Value : defaultAngle;
|
||||||
|
var offset = document.Editor.GetPoint("\nDimension line location: ");
|
||||||
|
if (offset.Status != PromptStatus.OK) return;
|
||||||
|
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var table = (BlockTable)transaction.GetObject(document.Database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.PaperSpace], OpenMode.ForWrite);
|
||||||
|
EnsureRegApp(transaction, document.Database);
|
||||||
|
EraseOwnedDimensions(transaction, paper, points[0].Viewport, points.Select(point => point.Id.Handle.ToString()).ToHashSet(StringComparer.OrdinalIgnoreCase));
|
||||||
|
for (var index = 1; index < points.Count; index++)
|
||||||
|
{
|
||||||
|
var first = points[index - 1].Position;
|
||||||
|
var second = points[index].Position;
|
||||||
|
var dimension = new RotatedDimension(rotation, first, second, offset.Value, string.Empty, document.Database.Dimstyle);
|
||||||
|
dimension.LayerId = document.Database.Clayer;
|
||||||
|
dimension.XData = Ownership(points[0].Viewport, points.Select(point => point.Id.Handle.ToString()));
|
||||||
|
paper.AppendEntity(dimension);
|
||||||
|
transaction.AddNewlyCreatedDBObject(dimension, true);
|
||||||
|
}
|
||||||
|
transaction.Commit();
|
||||||
|
document.Editor.WriteMessage($"\nCreated {points.Count - 1} Ortho dimensions; native UNDO is available.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadViewport(BlockReference reference, out string viewport)
|
||||||
|
{
|
||||||
|
viewport = string.Empty;
|
||||||
|
using var data = reference.GetXDataForApplication(AnnotationApp);
|
||||||
|
var values = data?.AsArray().Where(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(value => value.Value?.ToString() ?? string.Empty).ToArray();
|
||||||
|
if (values is null) return false;
|
||||||
|
for (var index = 0; index + 1 < values.Length; index++) if (string.Equals(values[index], "VIEWPORT", StringComparison.Ordinal)) { viewport = values[index + 1]; break; }
|
||||||
|
return !string.IsNullOrWhiteSpace(viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EraseOwnedDimensions(Transaction transaction, BlockTableRecord paper, string viewport, HashSet<string> handles)
|
||||||
|
{
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) continue;
|
||||||
|
using var data = entity.GetXDataForApplication(DimensionApp);
|
||||||
|
var values = data?.AsArray().Where(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(value => value.Value?.ToString() ?? string.Empty).ToArray();
|
||||||
|
if (values is null || !values.Contains(viewport, StringComparer.Ordinal)) continue;
|
||||||
|
if (!handles.Any(handle => values.Contains(handle, StringComparer.OrdinalIgnoreCase))) continue;
|
||||||
|
entity.UpgradeOpen();
|
||||||
|
entity.Erase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResultBuffer Ownership(string viewport, IEnumerable<string> handles) => new(
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataRegAppName, DimensionApp),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, viewport),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, "SOURCES"),
|
||||||
|
new TypedValue((int)DxfCode.ExtendedDataAsciiString, string.Join(",", handles)));
|
||||||
|
|
||||||
|
private static void EnsureRegApp(Transaction transaction, Database database)
|
||||||
|
{
|
||||||
|
var table = (RegAppTable)transaction.GetObject(database.RegAppTableId, OpenMode.ForRead);
|
||||||
|
if (table.Has(DimensionApp)) return;
|
||||||
|
table.UpgradeOpen();
|
||||||
|
using var record = new RegAppTableRecord { Name = DimensionApp };
|
||||||
|
table.Add(record);
|
||||||
|
transaction.AddNewlyCreatedDBObject(record, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class OrthoMaintenanceCommands
|
||||||
|
{
|
||||||
|
public static void CheckViewport()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new PromptEntityOptions("\nВыберите видовой экран для проверки: ");
|
||||||
|
options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
var selected = document.Editor.GetEntity(options); if (selected.Status != PromptStatus.OK) return;
|
||||||
|
OrthoViewportInfo view;
|
||||||
|
ViewportLayerPlan layerPlan;
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
{
|
||||||
|
var viewport = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
|
||||||
|
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||||
|
layerPlan = new ViewportLayerPlanner().Build(viewport.Layer, layers.Has("Defpoints"));
|
||||||
|
view = Resolve(document, viewport);
|
||||||
|
}
|
||||||
|
if (layerPlan.FailureCode is not null) throw new InvalidOperationException(layerPlan.FailureCode);
|
||||||
|
var objects = new PlantObjectReader().ReadObjects(view, CancellationToken.None);
|
||||||
|
var resolved = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.Resolved);
|
||||||
|
var outside = objects.Count(x => x.MembershipStatus == ViewMembershipStatus.ObjectOutsideView);
|
||||||
|
var unresolved = objects.Count - resolved - outside;
|
||||||
|
document.Editor.WriteMessage($"\nPXP-COV preview: resolved={resolved}; outside={outside}; unresolved={unresolved}; viewport-layer-change={layerPlan.RequiresChange}. DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("OrthoViewportCheckCompleted", commandName: "PXP-COV", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["Resolved"] = resolved, ["Outside"] = outside, ["Unresolved"] = unresolved, ["LayerChange"] = layerPlan.RequiresChange });
|
||||||
|
if (!layerPlan.RequiresChange || !string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
ValidateFixture(document);
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var write = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var target = (Viewport)write.GetObject(selected.ObjectId, OpenMode.ForWrite);
|
||||||
|
var layerTable = (LayerTable)write.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||||
|
target.LayerId = layerTable[layerPlan.TargetLayer];
|
||||||
|
write.Commit();
|
||||||
|
document.Editor.WriteMessage("\nPXP-COV Apply: viewport moved to Defpoints; AutoCAD UNDO is available.");
|
||||||
|
}
|
||||||
|
catch (Exception exception) { RuntimeDiagnostics.Write("OrthoViewportCheckFailed", "ERROR", commandName: "PXP-COV", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-COV: " + exception.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrthoViewportInfo Resolve(Document document, Viewport selected)
|
||||||
|
{
|
||||||
|
var project = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_PROJECT_UNAVAILABLE");
|
||||||
|
var matches = new PlantOrthoViewportReader(project, document.Name).ReadViewports(CancellationToken.None)
|
||||||
|
.Where(x => Math.Abs(x.CenterX - selected.CenterPoint.X) < .001 && Math.Abs(x.CenterY - selected.CenterPoint.Y) < .001 && Math.Abs(x.Width - selected.Width) < .001 && Math.Abs(x.Height - selected.Height) < .001).ToArray();
|
||||||
|
return matches.Length == 1 ? matches[0] : throw new InvalidOperationException(matches.Length == 0 ? "ORTHO_VIEW_NOT_FOUND" : "ORTHO_VIEW_AMBIGUOUS");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateFixture(Document document)
|
||||||
|
{
|
||||||
|
var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER");
|
||||||
|
if (string.IsNullOrWhiteSpace(marker) || !Path.IsPathFullyQualified(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_MAINTENANCE_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
|
||||||
|
var root = Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar;
|
||||||
|
if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Source-backed replacement for OrthoAutoAnnotate.HighlightAndZoomPnPId.
|
||||||
|
// New PXP-OA annotations persist their project source drawing and DataLinks row
|
||||||
|
// in XData. Legacy annotations without that information remain untouched and
|
||||||
|
// fail safely rather than being migrated destructively.
|
||||||
|
internal static class OrthoSourceNavigationCommand
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-OA";
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var ortho = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (ortho is null || PlantApplication.CurrentProject is null) return;
|
||||||
|
var pick = ortho.Editor.GetEntity(new PromptEntityOptions("\nSelect an Ortho annotation to show its model source: ") { AllowNone = false });
|
||||||
|
if (pick.Status != PromptStatus.OK) return;
|
||||||
|
if (!TryReadSource(ortho.Database, pick.ObjectId, out var sourceDrawing, out var rowId))
|
||||||
|
{
|
||||||
|
ortho.Editor.WriteMessage("\nThe selected annotation has no modern source identity; no drawing was changed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = AcadApplication.DocumentManager.Cast<Document>()
|
||||||
|
.FirstOrDefault(document => SamePath(document.Name, sourceDrawing));
|
||||||
|
if (target is null)
|
||||||
|
{
|
||||||
|
var answer = ortho.Editor.GetKeywords(new PromptKeywordOptions($"\nOpen model drawing '{Path.GetFileName(sourceDrawing)}'? [Open/Cancel] <Open>: ") { Keywords = { "Open", "Cancel" }, AllowNone = true });
|
||||||
|
if (answer.Status == PromptStatus.Cancel || string.Equals(answer.StringResult, "Cancel", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
if (!File.Exists(sourceDrawing)) { ortho.Editor.WriteMessage("\nThe source drawing is unavailable in the active project."); return; }
|
||||||
|
try { target = AcadApplication.DocumentManager.Open(sourceDrawing, false); }
|
||||||
|
catch (Exception exception) { RuntimeDiagnostics.Write("OrthoSourceNavigation", "ERROR", resultStatus: "SOURCE_DRAWING_OPEN_FAILED", exception: exception); ortho.Editor.WriteMessage("\nUnable to open the source drawing."); return; }
|
||||||
|
}
|
||||||
|
|
||||||
|
AcadApplication.DocumentManager.MdiActiveDocument = target;
|
||||||
|
var links = PlantApplication.ProjectPart("Piping")?.DataLinksManager;
|
||||||
|
if (links is null) { target.Editor.WriteMessage("\nPiping DataLinks are unavailable."); return; }
|
||||||
|
var id = FindEntityForRow(target.Database, links, rowId);
|
||||||
|
if (id.IsNull) { target.Editor.WriteMessage("\nThe source component no longer exists."); return; }
|
||||||
|
HighlightAndZoom(target, id);
|
||||||
|
RuntimeDiagnostics.Write("OrthoSourceNavigation", resultStatus: "SOURCE_OBJECT_HIGHLIGHTED", document: target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSource(Database database, ObjectId id, out string drawing, out int rowId)
|
||||||
|
{
|
||||||
|
drawing = string.Empty; rowId = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) return false;
|
||||||
|
using var data = entity.GetXDataForApplication(ApplicationName);
|
||||||
|
var values = data?.AsArray().Where(value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString).Select(value => value.Value?.ToString() ?? string.Empty).ToArray();
|
||||||
|
if (values is null) return false;
|
||||||
|
for (var index = 0; index + 1 < values.Length; index++)
|
||||||
|
{
|
||||||
|
if (string.Equals(values[index], "SOURCE_DRAWING", StringComparison.Ordinal)) drawing = values[index + 1];
|
||||||
|
if (string.Equals(values[index], "SOURCE_ROW", StringComparison.Ordinal)) int.TryParse(values[index + 1], out rowId);
|
||||||
|
}
|
||||||
|
return Path.IsPathRooted(drawing) && rowId > 0;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectId FindEntityForRow(Database database, DataLinksManager links, int rowId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var table = (BlockTable)transaction.GetObject(database.BlockTableId, OpenMode.ForRead);
|
||||||
|
var model = (BlockTableRecord)transaction.GetObject(table[BlockTableRecord.ModelSpace], OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in model)
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity) continue;
|
||||||
|
try { if (links.FindAcPpRowId(id) == rowId) return id; }
|
||||||
|
catch (DLException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return ObjectId.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void HighlightAndZoom(Document document, ObjectId id)
|
||||||
|
{
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) return;
|
||||||
|
entity.Highlight();
|
||||||
|
var extents = entity.GeometricExtents;
|
||||||
|
using var view = document.Editor.GetCurrentView();
|
||||||
|
var transform = Matrix3d.PlaneToWorld(view.ViewDirection);
|
||||||
|
transform = Matrix3d.Displacement(view.Target - Point3d.Origin) * transform;
|
||||||
|
transform = Matrix3d.Rotation(-view.ViewTwist, view.ViewDirection, view.Target) * transform;
|
||||||
|
extents.TransformBy(transform.Inverse());
|
||||||
|
view.Width = Math.Max(extents.MaxPoint.X - extents.MinPoint.X, 1d);
|
||||||
|
view.Height = Math.Max(extents.MaxPoint.Y - extents.MinPoint.Y, 1d);
|
||||||
|
view.CenterPoint = new Point2d((extents.MaxPoint.X + extents.MinPoint.X) / 2d, (extents.MaxPoint.Y + extents.MinPoint.Y) / 2d);
|
||||||
|
document.Editor.SetCurrentView(view);
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SamePath(string left, string right) => string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -14,9 +14,12 @@
|
|||||||
<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="PnP3dPartsRepository"><HintPath>$(AutoCAD2026ManagedDir)\PnP3dPartsRepository.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="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="PnP3dOrthoDrawingsUI"><HintPath>$(Plant3D2026ManagedDir)\PnP3dOrthoDrawingsUI.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>
|
<Reference Include="PnPDwg2dUtil"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dUtil.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
|
<Reference Include="PnPDwg2dGen"><HintPath>$(Plant3D2026ManagedDir)\PnPDwg2dGen.dll</HintPath><Private>false</Private><CopyLocal>false</CopyLocal></Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.ProcessPower.PnP3dObjects;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Modern Plant 2026 adapter for the production-useful part of legacy
|
||||||
|
// GetComponentGeometry.IsGeometryRead: Pipe dimensions and Part extents are
|
||||||
|
// read from the selected source object with explicit unit normalisation.
|
||||||
|
internal sealed class PlantComponentGeometryReader(Database database) : IPlantComponentGeometryReader
|
||||||
|
{
|
||||||
|
public ComponentGeometryReadResult Read(string stableEntityId, bool convertToMetric)
|
||||||
|
{
|
||||||
|
if (!TryObjectId(stableEntityId, out var id)) return new(false, null, "COMPONENT_GEOMETRY_STABLE_ID_INVALID");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Entity entity) return new(false, null, "COMPONENT_GEOMETRY_ENTITY_NOT_FOUND");
|
||||||
|
var extents = entity.GeometricExtents;
|
||||||
|
var bounds = new Bounds3Dto(new(extents.MinPoint.X, extents.MinPoint.Y, extents.MinPoint.Z), new(extents.MaxPoint.X, extents.MaxPoint.Y, extents.MaxPoint.Z));
|
||||||
|
if (entity is Pipe pipe)
|
||||||
|
{
|
||||||
|
var factor = Factor(convertToMetric, pipe.PartSizeProperties.LinearUnit);
|
||||||
|
var parameters = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["D"] = Format(pipe.OuterDiameter * factor),
|
||||||
|
["L"] = Format(pipe.Length * factor),
|
||||||
|
["StartX"] = Format(pipe.StartPoint.X), ["StartY"] = Format(pipe.StartPoint.Y), ["StartZ"] = Format(pipe.StartPoint.Z),
|
||||||
|
["EndX"] = Format(pipe.EndPoint.X), ["EndY"] = Format(pipe.EndPoint.Y), ["EndZ"] = Format(pipe.EndPoint.Z)
|
||||||
|
};
|
||||||
|
AddDerivedGeometry(parameters, pipe.StartPoint, pipe.EndPoint);
|
||||||
|
AddPorts(parameters, pipe.GetPorts(PortType.Both));
|
||||||
|
return new(true, new("CPP", parameters, bounds, factor != 1));
|
||||||
|
}
|
||||||
|
if (entity is Part part)
|
||||||
|
{
|
||||||
|
var factor = Factor(convertToMetric, part.PartSizeProperties.LinearUnit);
|
||||||
|
var center = extents.MinPoint + (extents.MaxPoint - extents.MinPoint) / 2d;
|
||||||
|
var parameters = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["BoundsX"] = Format((extents.MaxPoint.X - extents.MinPoint.X) * factor),
|
||||||
|
["BoundsY"] = Format((extents.MaxPoint.Y - extents.MinPoint.Y) * factor),
|
||||||
|
["BoundsZ"] = Format((extents.MaxPoint.Z - extents.MinPoint.Z) * factor),
|
||||||
|
["CenterX"] = Format(center.X), ["CenterY"] = Format(center.Y), ["CenterZ"] = Format(center.Z)
|
||||||
|
};
|
||||||
|
var ports = part.GetPorts(PortType.Both);
|
||||||
|
AddPorts(parameters, ports);
|
||||||
|
if (ports.Count >= 2) AddDerivedGeometry(parameters, ports[0].Position, ports[1].Position);
|
||||||
|
return new(true, new("PART", parameters, bounds, factor != 1));
|
||||||
|
}
|
||||||
|
return new(false, null, "COMPONENT_GEOMETRY_UNSUPPORTED_ENTITY");
|
||||||
|
}
|
||||||
|
catch (Autodesk.AutoCAD.Runtime.Exception) { return new(false, null, "COMPONENT_GEOMETRY_EXTENTS_UNAVAILABLE"); }
|
||||||
|
catch (Exception) { return new(false, null, "COMPONENT_GEOMETRY_READ_FAILED"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryObjectId(string stableEntityId, out ObjectId id)
|
||||||
|
{
|
||||||
|
id = ObjectId.Null;
|
||||||
|
const string prefix = "acad:";
|
||||||
|
if (string.IsNullOrWhiteSpace(stableEntityId) || !stableEntityId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) return false;
|
||||||
|
if (!long.TryParse(stableEntityId[prefix.Length..], System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var handle)) return false;
|
||||||
|
try { id = database.GetObjectId(false, new Handle(handle), 0); return !id.IsNull; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double Factor(bool convertToMetric, object? unit) => convertToMetric && string.Equals(unit?.ToString(), "Inch", StringComparison.OrdinalIgnoreCase) ? 25.4 : 1d;
|
||||||
|
private static string Format(double value) => value.ToString("0.###############", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
private static void AddPorts(Dictionary<string, string> parameters, PortCollection ports)
|
||||||
|
{
|
||||||
|
parameters["PortCount"] = ports.Count.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
for (var index = 0; index < ports.Count; index++)
|
||||||
|
{
|
||||||
|
var port = ports[index]; var prefix = "Port" + index;
|
||||||
|
parameters[prefix + "Name"] = port.Name ?? string.Empty;
|
||||||
|
parameters[prefix + "X"] = Format(port.Position.X); parameters[prefix + "Y"] = Format(port.Position.Y); parameters[prefix + "Z"] = Format(port.Position.Z);
|
||||||
|
parameters[prefix + "DirectionX"] = Format(port.Direction.X); parameters[prefix + "DirectionY"] = Format(port.Direction.Y); parameters[prefix + "DirectionZ"] = Format(port.Direction.Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddDerivedGeometry(Dictionary<string, string> parameters, Autodesk.AutoCAD.Geometry.Point3d start, Autodesk.AutoCAD.Geometry.Point3d end)
|
||||||
|
{
|
||||||
|
var axis = start.GetVectorTo(end); var center = start + axis / 2d;
|
||||||
|
parameters["CenterX"] = Format(center.X); parameters["CenterY"] = Format(center.Y); parameters["CenterZ"] = Format(center.Z);
|
||||||
|
if (axis.Length <= Autodesk.AutoCAD.Geometry.Tolerance.Global.EqualPoint) return;
|
||||||
|
var direction = axis.GetNormal();
|
||||||
|
parameters["AxisX"] = Format(direction.X); parameters["AxisY"] = Format(direction.Y); parameters["AxisZ"] = Format(direction.Z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.ProcessPower.PnP3dObjects;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Reads the actual two Plant Connector ports from the source model database.
|
||||||
|
// Paper-space projection and viewport filtering deliberately stay in the Ortho
|
||||||
|
// adapter; this boundary only exposes stable, Autodesk-free model geometry.
|
||||||
|
internal sealed class PlantConnectorPortReader(Database sourceModelDatabase) : IPlantConnectorPortReader
|
||||||
|
{
|
||||||
|
public PlantConnectorPortReadResult Read(PlantConnectorRelationshipSnapshot connector)
|
||||||
|
{
|
||||||
|
if (!TryObjectId(connector.ConnectorEntityStableId, out var id)) return new(false, null, "CONNECTOR_ENTITY_HANDLE_UNAVAILABLE");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var transaction = sourceModelDatabase.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is not Connector entity) return new(false, null, "CONNECTOR_ENTITY_NOT_FOUND");
|
||||||
|
var ports = entity.GetPorts(PortType.Both);
|
||||||
|
if (ports.Count != 2) return new(false, null, "CONNECTOR_PORT_COUNT_INVALID");
|
||||||
|
var first = ports[0];
|
||||||
|
var second = ports[1];
|
||||||
|
return new(true, new(
|
||||||
|
connector.StableId,
|
||||||
|
Point(first.Position),
|
||||||
|
Point(second.Position),
|
||||||
|
Vector(first.Direction),
|
||||||
|
Vector(second.Direction)));
|
||||||
|
}
|
||||||
|
catch (Autodesk.AutoCAD.Runtime.Exception) { return new(false, null, "CONNECTOR_PORT_READ_FAILED"); }
|
||||||
|
catch (Exception) { return new(false, null, "CONNECTOR_PORT_READ_FAILED"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryObjectId(string? stableId, out ObjectId id)
|
||||||
|
{
|
||||||
|
id = ObjectId.Null;
|
||||||
|
const string prefix = "acad:";
|
||||||
|
if (string.IsNullOrWhiteSpace(stableId) || !stableId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
!long.TryParse(stableId[prefix.Length..], System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var handle)) return false;
|
||||||
|
try { id = sourceModelDatabase.GetObjectId(false, new Handle(handle), 0); return !id.IsNull; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Point3Dto Point(Autodesk.AutoCAD.Geometry.Point3d point) => new(point.X, point.Y, point.Z);
|
||||||
|
private static Point3Dto Vector(Autodesk.AutoCAD.Geometry.Vector3d vector) => new(vector.X, vector.Y, vector.Z);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Reads the two P3dPartConnection edges and their line-group relationships.
|
||||||
|
// This is the Plant 2026 equivalent of the legacy PXP-ALC connector discovery
|
||||||
|
// pass; it deliberately returns row identities only, never PnP/AutoCAD
|
||||||
|
// objects, so Core remains Autodesk-free.
|
||||||
|
internal sealed class PlantConnectorRelationshipReader(DataLinksManager dataLinks) : IPlantConnectorRelationshipReader
|
||||||
|
{
|
||||||
|
public PlantConnectorRelationshipReadResult Read()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var database = dataLinks.GetPnPDatabase();
|
||||||
|
var rows = database.Tables["PnPDataLinks"].Select("RowClassName='P3dConnector'")
|
||||||
|
.OrderBy(row => Convert.ToInt64(row["RowId"]))
|
||||||
|
.ToArray();
|
||||||
|
var result = new List<PlantConnectorRelationshipSnapshot>();
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var connector = Convert.ToInt64(row["RowId"]);
|
||||||
|
var first = dataLinks.GetRelatedRowIds("P3dPartConnection", "Part1", (int)connector, "Part2");
|
||||||
|
var second = dataLinks.GetRelatedRowIds("P3dPartConnection", "Part2", (int)connector, "Part1");
|
||||||
|
if (first.Count != 1 || second.Count != 1) continue;
|
||||||
|
var firstPart = first.First();
|
||||||
|
var secondPart = second.First();
|
||||||
|
var firstGroups = dataLinks.GetRelatedRowIds("P3dLineGroupPartRelationship", "Part", firstPart, "LineGroup");
|
||||||
|
var secondGroups = dataLinks.GetRelatedRowIds("P3dLineGroupPartRelationship", "Part", secondPart, "LineGroup");
|
||||||
|
if (firstGroups.Count != 1 || secondGroups.Count != 1) continue;
|
||||||
|
result.Add(new($"pnp-connector:{connector}", connector, firstPart, secondPart, firstGroups.First(), secondGroups.First(), Array.Empty<string>(), TryEntityStableId(row)));
|
||||||
|
}
|
||||||
|
return new(true, result.OrderBy(value => value.StableId, StringComparer.Ordinal).ToArray());
|
||||||
|
}
|
||||||
|
catch (DLException) { return new(false, Array.Empty<PlantConnectorRelationshipSnapshot>(), "CONNECTOR_DATALINKS_READ_FAILED"); }
|
||||||
|
catch (Exception) { return new(false, Array.Empty<PlantConnectorRelationshipSnapshot>(), "CONNECTOR_RELATIONSHIP_READ_FAILED"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryEntityStableId(dynamic row)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handle = Convert.ToInt64(row["DwgHandleLow"], System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
return handle > 0 ? $"acad:{handle:X}" : null;
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal sealed class PlantDataLinksPropertyReader(DataLinksManager dataLinks) : IDataLinksPropertyReader
|
||||||
|
{
|
||||||
|
public DataLinksPropertyReadResult ReadProperties(long rowId)
|
||||||
|
{
|
||||||
|
if (rowId <= 0) return new(false, new Dictionary<string, string?>(), "DATALINKS_ROW_ID_INVALID");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var values = dataLinks.GetAllProperties((int)rowId, true)
|
||||||
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(x => x.Key, x => (string?)x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
return new(true, values);
|
||||||
|
}
|
||||||
|
catch (DLException) { return new(false, new Dictionary<string, string?>(), "DATALINKS_PROPERTY_READ_FAILED"); }
|
||||||
|
catch (Exception) { return new(false, new Dictionary<string, string?>(), "DATALINKS_PROPERTY_READ_FAILED"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.ProcessPower.DataLinks;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
// Converts an explicit AutoCAD selection to the small, AutoCAD-free candidate
|
||||||
|
// used by the line-number preview. It deliberately performs no DataLinks or
|
||||||
|
// DWG write and reports unavailable Plant mappings as typed failures.
|
||||||
|
internal sealed class PlantLineNumberCandidateReader(DataLinksManager dataLinks)
|
||||||
|
{
|
||||||
|
public LineNumberCandidateReadResult Read(ObjectId selectedId, string layer, string textStyle)
|
||||||
|
{
|
||||||
|
if (selectedId.IsNull || !selectedId.IsValid) return new(false, null, "LINE_NUMBER_SELECTION_INVALID");
|
||||||
|
if (string.IsNullOrWhiteSpace(layer) || string.IsNullOrWhiteSpace(textStyle)) return new(false, null, "LINE_NUMBER_ASSET_CONTRACT_INVALID");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rowId = dataLinks.FindAcPpRowId(selectedId);
|
||||||
|
if (rowId <= 0) return new(false, null, "LINE_NUMBER_DATALINKS_ROW_NOT_FOUND");
|
||||||
|
var database = selectedId.Database;
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
if (transaction.GetObject(selectedId, OpenMode.ForRead) is not Entity entity) return new(false, null, "LINE_NUMBER_SELECTION_NOT_ENTITY");
|
||||||
|
var extents = entity.GeometricExtents;
|
||||||
|
var center = new Point3d(
|
||||||
|
(extents.MinPoint.X + extents.MaxPoint.X) / 2d,
|
||||||
|
(extents.MinPoint.Y + extents.MaxPoint.Y) / 2d,
|
||||||
|
(extents.MinPoint.Z + extents.MaxPoint.Z) / 2d);
|
||||||
|
transaction.Commit();
|
||||||
|
return new(true, new($"acad:{selectedId.Handle}", rowId, new(center.X, center.Y, center.Z), 0, layer, textStyle, new Dictionary<string, string?>()));
|
||||||
|
}
|
||||||
|
catch (Autodesk.AutoCAD.Runtime.Exception) { return new(false, null, "LINE_NUMBER_SELECTION_GEOMETRY_UNAVAILABLE"); }
|
||||||
|
catch (DLException) { return new(false, null, "LINE_NUMBER_DATALINKS_ROW_NOT_FOUND"); }
|
||||||
|
catch (Exception) { return new(false, null, "LINE_NUMBER_CANDIDATE_READ_FAILED"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -294,6 +294,7 @@ internal sealed class PlantObjectReader : IPlantObjectReader
|
|||||||
var properties = dataLinks.GetAllProperties(rowId, true)
|
var properties = dataLinks.GetAllProperties(rowId, true)
|
||||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(x => x.Key, x => x.Last().Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
properties["PRecAnnot.RowId"] = rowId.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -447,7 +448,7 @@ internal sealed class PlantViewportMembershipReader
|
|||||||
if (sourceRows.Contains(rowId))
|
if (sourceRows.Contains(rowId))
|
||||||
{
|
{
|
||||||
selectedRows.Add(rowId);
|
selectedRows.Add(rowId);
|
||||||
if (TryReadPipePlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement;
|
if (TryReadPlacement(sourceId, dataLinks, out var placement)) geometry[rowId] = placement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (DLException)
|
catch (DLException)
|
||||||
@@ -476,23 +477,45 @@ internal sealed class PlantViewportMembershipReader
|
|||||||
// whose PrjObjectId identifies the source entity and whose Xform projects
|
// whose PrjObjectId identifies the source entity and whose Xform projects
|
||||||
// that source entity into the particular Ortho view. This is not a sheet
|
// that source entity into the particular Ortho view. This is not a sheet
|
||||||
// coordinate heuristic.
|
// coordinate heuristic.
|
||||||
private static bool TryReadPipePlacement(MdObjectId sourceId, DataLinksManager dataLinks, out GeometrySnapshot geometry)
|
private static bool TryReadPlacement(MdObjectId sourceId, DataLinksManager dataLinks, out GeometrySnapshot geometry)
|
||||||
{
|
{
|
||||||
geometry = new GeometrySnapshot();
|
geometry = new GeometrySnapshot();
|
||||||
|
var stage = "GetDatabase";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var openedByQuery = false;
|
var openedByQuery = false;
|
||||||
var sourceDatabase = sourceId.GetDatabase(false, ref openedByQuery);
|
// The source model is normally not an active document in an Ortho /b session.
|
||||||
|
// Ask MdObjectId to open its source database read-only and dispose it below when it did.
|
||||||
|
var sourceDatabase = sourceId.GetDatabase(true, ref openedByQuery);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
stage = "StartTransaction";
|
||||||
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
stage = "MakeAcDbObjectId";
|
||||||
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
||||||
if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false;
|
stage = "OpenPlantEntity";
|
||||||
var point = pipe.StartPoint.TransformBy(sourceId.Xform);
|
var entity = transaction.GetObject(objectId, OpenMode.ForRead);
|
||||||
|
Autodesk.AutoCAD.Geometry.Point3d modelPoint;
|
||||||
|
Autodesk.AutoCAD.Geometry.Vector3d? modelDirection = null;
|
||||||
|
if (entity is Pipe pipe) modelPoint = pipe.StartPoint;
|
||||||
|
else if (entity is Connector connector)
|
||||||
|
{
|
||||||
|
var ports = connector.GetPorts(PortType.Both);
|
||||||
|
if (ports.Count != 2) return false;
|
||||||
|
modelPoint = ports[1].Position;
|
||||||
|
modelDirection = ports[0].Direction;
|
||||||
|
}
|
||||||
|
else return false;
|
||||||
|
stage = "TransformPlacement";
|
||||||
|
var point = modelPoint.TransformBy(sourceId.Xform);
|
||||||
|
var direction = modelDirection?.TransformBy(sourceId.Xform);
|
||||||
geometry = new GeometrySnapshot
|
geometry = new GeometrySnapshot
|
||||||
{
|
{
|
||||||
|
WorldPosition = new Point3Dto(modelPoint.X, modelPoint.Y, modelPoint.Z),
|
||||||
PaperPosition = new Point2Dto(point.X, point.Y),
|
PaperPosition = new Point2Dto(point.X, point.Y),
|
||||||
Rotation = 0
|
Elevation = modelPoint.Z,
|
||||||
|
Rotation = 0,
|
||||||
|
PaperDirection = direction is null ? null : new Vector2Dto(direction.Value.X, direction.Value.Y)
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -501,8 +524,9 @@ internal sealed class PlantViewportMembershipReader
|
|||||||
if (openedByQuery) sourceDatabase.Dispose();
|
if (openedByQuery) sourceDatabase.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
|
RuntimeDiagnostics.Write("OrthoPipePlacementReadFailed", "ERROR", resultStatus: "ORTHO_PIPE_PLACEMENT_UNAVAILABLE", exception: exception, details: new Dictionary<string, object?> { ["PlacementStage"] = stage });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-20
@@ -1,9 +1,12 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using Autodesk.AutoCAD.ApplicationServices;
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
using Autodesk.AutoCAD.DatabaseServices;
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
using Autodesk.AutoCAD.Runtime;
|
using Autodesk.AutoCAD.Runtime;
|
||||||
using Autodesk.ProcessPower.PlantInstance;
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
using Autodesk.ProcessPower.ProjectManager;
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
namespace PRecAnnot.Plant3D2026;
|
namespace PRecAnnot.Plant3D2026;
|
||||||
@@ -11,8 +14,8 @@ namespace PRecAnnot.Plant3D2026;
|
|||||||
// Test-only observer. The AutoCAD /b processor, never this type, drives PXP-OA.
|
// Test-only observer. The AutoCAD /b processor, never this type, drives PXP-OA.
|
||||||
internal static class ScriptDrivenFirstApplyAutomation
|
internal static class ScriptDrivenFirstApplyAutomation
|
||||||
{
|
{
|
||||||
private sealed record Config(string Phase, string OrthoDrawingPath, string EvidencePath, string FixtureMarkerPath);
|
private sealed record Config(string Phase, string OrthoDrawingPath, string EvidencePath, string FixtureMarkerPath, string ExpectedEntryAssemblySha256, string TargetCommand, bool Diagnostic);
|
||||||
private sealed record Run(int Sequence, string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, int? DbmodBefore, int? DbmodAfter, AutomationEntityCounts? Before, AutomationEntityCounts? After, bool WillStart, bool Ended, bool Failed, bool Cancelled, int? SourceRows, int? MembershipResolved);
|
private sealed record Run(int Sequence, string? PlanId, IReadOnlyList<string> Candidates, string? View, int Create, int Skip, bool? PermitAccepted, string? ApplyOutcome, int? DbmodBefore, int? DbmodAfter, AutomationEntityCounts? Before, AutomationEntityCounts? After, bool WillStart, bool Ended, bool Failed, bool Cancelled, int? SourceRows, int? MembershipResolved);
|
||||||
private static Config? config;
|
private static Config? config;
|
||||||
private static Document? fixture;
|
private static Document? fixture;
|
||||||
private static Database? database;
|
private static Database? database;
|
||||||
@@ -20,8 +23,18 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
private static int? dbmodBaseline;
|
private static int? dbmodBaseline;
|
||||||
private static readonly List<Run> runs = [];
|
private static readonly List<Run> runs = [];
|
||||||
private static readonly List<string> transitions = [];
|
private static readonly List<string> transitions = [];
|
||||||
|
private static readonly List<string> commandEvents = [];
|
||||||
|
private static readonly List<AutomationSentinelEvidence> sentinels = [];
|
||||||
|
private static readonly List<AutomationCommandCorrelationEvidence> correlations = [];
|
||||||
|
private static PxpOaDiscoveryEvidence? discovery;
|
||||||
|
// Plant's implicit current-document lookup can lag between script commands.
|
||||||
|
// PREPARE has already proved this immutable context belongs to the live fixture;
|
||||||
|
// retain it for the two immediately following public PXP-OA commands only.
|
||||||
|
private static PlantRuntimeContext? preparedContext;
|
||||||
|
private static string? selectedTargetSnapshotId;
|
||||||
private static bool prepared;
|
private static bool prepared;
|
||||||
private static bool terminal;
|
private static bool terminal;
|
||||||
|
private static string? applyExceptionText;
|
||||||
|
|
||||||
internal static void Prepare()
|
internal static void Prepare()
|
||||||
{
|
{
|
||||||
@@ -30,6 +43,8 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
if (terminal) return;
|
if (terminal) return;
|
||||||
config = ReadConfig();
|
config = ReadConfig();
|
||||||
RequireGate(config);
|
RequireGate(config);
|
||||||
|
discovery = DiscoverTargetCommand(config);
|
||||||
|
RequireDiscovery(discovery, config.TargetCommand);
|
||||||
var active = AcadApplication.DocumentManager.MdiActiveDocument ?? throw new InvalidOperationException("AUTOMATION_ACTIVE_DOCUMENT_MISSING");
|
var active = AcadApplication.DocumentManager.MdiActiveDocument ?? throw new InvalidOperationException("AUTOMATION_ACTIVE_DOCUMENT_MISSING");
|
||||||
if (!SamePath(active.Name, config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_UNEXPECTED_ACTIVE_FIXTURE");
|
if (!SamePath(active.Name, config.OrthoDrawingPath)) throw new InvalidOperationException("AUTOMATION_UNEXPECTED_ACTIVE_FIXTURE");
|
||||||
fixture = active;
|
fixture = active;
|
||||||
@@ -45,6 +60,12 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
var context = new PlantRuntimeContextReader().ReadCurrentContext(fixture);
|
var context = new PlantRuntimeContextReader().ReadCurrentContext(fixture);
|
||||||
if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1)
|
if (!context.HasPlantProject || !context.IsOrthoDrawing || context.Viewports.Count != 1)
|
||||||
throw new InvalidOperationException("AUTOMATION_ORTHO_CONTEXT_INVALID");
|
throw new InvalidOperationException("AUTOMATION_ORTHO_CONTEXT_INVALID");
|
||||||
|
var candidates = new PlantObjectReader().ReadObjects(context.Viewports.Single(), CancellationToken.None)
|
||||||
|
.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();
|
||||||
|
if (candidates.Length == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE");
|
||||||
|
preparedContext = context;
|
||||||
|
selectedTargetSnapshotId = candidates[0];
|
||||||
dbmodBaseline = Dbmod();
|
dbmodBaseline = Dbmod();
|
||||||
baseline = Counts(database);
|
baseline = Counts(database);
|
||||||
prepared = true;
|
prepared = true;
|
||||||
@@ -54,27 +75,77 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
catch (System.Exception exception) { Fail("AUTOMATION_PREPARE_FAILED", exception); }
|
catch (System.Exception exception) { Fail("AUTOMATION_PREPARE_FAILED", exception); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static void RecordSentinel(string stage)
|
||||||
|
{
|
||||||
|
Guard(() =>
|
||||||
|
{
|
||||||
|
if (!prepared || terminal || fixture is null || database is null) return;
|
||||||
|
sentinels.Add(new(stage, fixture.Name, database.Filename, DateTimeOffset.UtcNow));
|
||||||
|
transitions.Add("Sentinel:" + stage);
|
||||||
|
Write(false, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
internal static void RecordPxpOa(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted, int sourceRows, int membershipResolved)
|
internal static void RecordPxpOa(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted, int sourceRows, int membershipResolved)
|
||||||
{
|
{
|
||||||
if (!prepared || terminal || runs.Count == 0) return;
|
if (!prepared || terminal || runs.Count == 0) return;
|
||||||
var last = runs[^1];
|
var last = runs[^1];
|
||||||
if (!last.WillStart || last.PlanId is not null) return;
|
if (!last.WillStart || last.PlanId is not null) return;
|
||||||
runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, SourceRows = sourceRows, MembershipResolved = membershipResolved };
|
runs[^1] = last with { PlanId = planId, Candidates = candidates, View = view, Create = create, Skip = skip, PermitAccepted = permitAccepted, SourceRows = sourceRows, MembershipResolved = membershipResolved };
|
||||||
|
}
|
||||||
|
internal static void RecordPxpAav(string? planId, IReadOnlyList<string> candidates, string? view, int create, int skip, bool? permitAccepted, int sourceRows, int membershipResolved) =>
|
||||||
|
RecordPxpOa(planId, candidates, view, create, skip, permitAccepted, sourceRows, membershipResolved);
|
||||||
|
|
||||||
|
internal static void RecordApplyOutcome(PipeAnnotationWriteOutcome outcome)
|
||||||
|
{
|
||||||
|
if (!prepared || terminal || runs.Count == 0) return;
|
||||||
|
var last = runs[^1];
|
||||||
|
runs[^1] = outcome == PipeAnnotationWriteOutcome.AlreadyExists
|
||||||
|
? last with { ApplyOutcome = outcome.ToString(), Create = 0, Skip = 1 }
|
||||||
|
: last with { ApplyOutcome = outcome.ToString() };
|
||||||
|
}
|
||||||
|
internal static void RecordApplyFailure(System.Exception exception)
|
||||||
|
{
|
||||||
|
if (!prepared || terminal) return;
|
||||||
|
applyExceptionText = exception.ToString();
|
||||||
|
Write(false, "AUTOMATION_APPLY_COMMAND_FAILED");
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void FinalizePreview()
|
internal static bool TryGetPreparedContext(Document? commandDocument, out PlantRuntimeContext context)
|
||||||
|
{
|
||||||
|
context = null!;
|
||||||
|
if (!prepared || terminal || fixture is null || preparedContext is null || commandDocument is null || config is null) return false;
|
||||||
|
if (!SamePath(commandDocument.Name, config.OrthoDrawingPath) || !SamePath(fixture.Name, config.OrthoDrawingPath)) return false;
|
||||||
|
if (commandDocument.Database is null || database is null || !SamePath(commandDocument.Database.Filename, database.Filename)) return false;
|
||||||
|
context = preparedContext;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string? SelectedTargetSnapshotId => prepared && !terminal ? selectedTargetSnapshotId : null;
|
||||||
|
|
||||||
|
internal static void FinalizeAutomation()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!prepared) throw new InvalidOperationException("AUTOMATION_NOT_PREPARED");
|
if (!prepared) throw new InvalidOperationException("AUTOMATION_NOT_PREPARED");
|
||||||
if (runs.Count != 2) throw new InvalidOperationException("AUTOMATION_EXPECTS_EXACTLY_TWO_PXP_OA");
|
if (runs.Count != 2) throw new InvalidOperationException("AUTOMATION_EXPECTS_EXACTLY_TWO_TARGET_COMMAND_RUNS");
|
||||||
if (runs.Any(x => !x.WillStart || !x.Ended || x.Failed || x.Cancelled)) throw new InvalidOperationException("AUTOMATION_PXP_OA_NOT_ENDED");
|
if (runs.Any(x => !x.WillStart || !x.Ended || x.Failed || x.Cancelled)) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_NOT_ENDED");
|
||||||
if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId)) || !string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER");
|
if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId))) throw new InvalidOperationException("AUTOMATION_TARGET_PLAN_RESULT_NOT_CAPTURED");
|
||||||
if (runs.Any(x => x.Create != 1)) throw new InvalidOperationException("AUTOMATION_PREVIEW_CREATE_NOT_ONE");
|
if (!string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER");
|
||||||
|
var apply = string.Equals(config!.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if ((!apply && runs.Any(x => x.Create != 1)) || (apply && (runs[0].Create != 1 || runs[1].Create != 0))) throw new InvalidOperationException("AUTOMATION_CREATE_COUNTS_INVALID");
|
||||||
if (runs[0].Candidates.Count == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE");
|
if (runs[0].Candidates.Count == 0) throw new InvalidOperationException("AUTOMATION_NO_ELIGIBLE_CANDIDATE");
|
||||||
if (Dbmod() != dbmodBaseline || !Equals(Counts(database!), baseline)) throw new InvalidOperationException("AUTOMATION_INSPECTOR_INVARIANCE_FAILED");
|
if (apply)
|
||||||
|
{
|
||||||
|
if (runs[0].PermitAccepted != true) throw new InvalidOperationException("AUTOMATION_APPLY_PERMIT_NOT_ACCEPTED");
|
||||||
|
if (!string.Equals(runs[0].ApplyOutcome, PipeAnnotationWriteOutcome.Created.ToString(), StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_APPLY_CREATE_NOT_CONFIRMED");
|
||||||
|
if (!string.Equals(runs[1].ApplyOutcome, PipeAnnotationWriteOutcome.AlreadyExists.ToString(), StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_APPLY_DUPLICATE_NOT_CONFIRMED");
|
||||||
|
var undoCounts = Counts(database!);
|
||||||
|
if (!Equals(undoCounts, baseline)) throw new InvalidOperationException("AUTOMATION_UNDO_DID_NOT_RESTORE_BASELINE");
|
||||||
|
}
|
||||||
|
else if (Dbmod() != dbmodBaseline || !Equals(Counts(database!), baseline)) throw new InvalidOperationException("AUTOMATION_INSPECTOR_INVARIANCE_FAILED");
|
||||||
terminal = true;
|
terminal = true;
|
||||||
transitions.Add("PreviewCompleted");
|
transitions.Add(apply ? "ApplyCompleted" : "PreviewCompleted");
|
||||||
Write(true, null);
|
Write(true, null);
|
||||||
Unsubscribe();
|
Unsubscribe();
|
||||||
}
|
}
|
||||||
@@ -85,9 +156,12 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
{
|
{
|
||||||
Guard(() =>
|
Guard(() =>
|
||||||
{
|
{
|
||||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count >= 2) return;
|
commandEvents.Add("WillStart:" + args.GlobalCommandName);
|
||||||
runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null));
|
var correlation = Correlate("WillStart", sender, args.GlobalCommandName, runs.Count < 2);
|
||||||
transitions.Add("PxpOaWillStart:" + runs.Count);
|
correlations.Add(correlation);
|
||||||
|
if (correlation.RejectionReason is not null) return;
|
||||||
|
runs.Add(new Run(runs.Count + 1, null, [], null, 0, 0, null, null, Dbmod(), null, Counts(database!), null, true, false, false, false, null, null));
|
||||||
|
transitions.Add("TargetCommandWillStart:" + runs.Count);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
private static void OnEnded(object sender, CommandEventArgs args) => Finish(sender, args, false, false);
|
private static void OnEnded(object sender, CommandEventArgs args) => Finish(sender, args, false, false);
|
||||||
@@ -97,19 +171,42 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
{
|
{
|
||||||
Guard(() =>
|
Guard(() =>
|
||||||
{
|
{
|
||||||
if (!IsFixture(sender) || !IsPxp(args.GlobalCommandName) || runs.Count == 0) return;
|
var kind = failed ? "Failed" : cancelled ? "Cancelled" : "Ended";
|
||||||
|
commandEvents.Add(kind + ":" + args.GlobalCommandName);
|
||||||
|
var correlation = Correlate(kind, sender, args.GlobalCommandName, runs.Count > 0);
|
||||||
|
correlations.Add(correlation);
|
||||||
|
if (correlation.RejectionReason is not null) return;
|
||||||
var last = runs[^1];
|
var last = runs[^1];
|
||||||
if (last.Ended || last.Failed || last.Cancelled) return;
|
if (last.Ended || last.Failed || last.Cancelled) return;
|
||||||
runs[^1] = last with { Ended = !failed && !cancelled, Failed = failed, Cancelled = cancelled, DbmodAfter = Dbmod(), After = Counts(database!) };
|
runs[^1] = last with { Ended = !failed && !cancelled, Failed = failed, Cancelled = cancelled, DbmodAfter = Dbmod(), After = Counts(database!) };
|
||||||
transitions.Add(failed ? "PxpOaFailed" : cancelled ? "PxpOaCancelled" : "PxpOaEnded:" + last.Sequence);
|
transitions.Add(failed ? "TargetCommandFailed" : cancelled ? "TargetCommandCancelled" : "TargetCommandEnded:" + last.Sequence);
|
||||||
Write(false, null);
|
Write(false, null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
private static bool IsFixture(object sender) => !terminal && fixture is not null && ReferenceEquals(sender, fixture) && database is not null && ReferenceEquals(fixture.Database, database);
|
private static AutomationCommandCorrelationEvidence Correlate(string kind, object sender, string rawName, bool sequenceAccepted)
|
||||||
private static bool IsPxp(string name) => string.Equals(name.TrimStart('.', '_'), "PXP-OA", StringComparison.OrdinalIgnoreCase);
|
{
|
||||||
|
var normalized = rawName.Trim();
|
||||||
|
var command = config is not null && string.Equals(normalized, config.TargetCommand, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var state = prepared && !terminal && config is not null && fixture is not null && database is not null;
|
||||||
|
var senderDocument = sender as Document;
|
||||||
|
var senderPath = senderDocument?.Name;
|
||||||
|
var expectedPath = config?.OrthoDrawingPath;
|
||||||
|
var activePath = AcadApplication.DocumentManager.MdiActiveDocument?.Name;
|
||||||
|
var fixturePath = senderPath is not null && expectedPath is not null && SamePath(senderPath, expectedPath);
|
||||||
|
var activeFixture = activePath is not null && expectedPath is not null && SamePath(activePath, expectedPath);
|
||||||
|
var document = fixturePath && activeFixture;
|
||||||
|
var databaseMatch = senderDocument?.Database is not null && database is not null && SamePath(senderDocument.Database.Filename, database.Filename);
|
||||||
|
var rejection = !command ? "COMMAND_NAME_MISMATCH" : !state ? "NOT_PREPARED" : !document ? "DOCUMENT_IDENTITY_MISMATCH" : !databaseMatch ? "DATABASE_IDENTITY_MISMATCH" : !sequenceAccepted ? "UNEXPECTED_SEQUENCE" : null;
|
||||||
|
return new(kind, rawName, normalized, command, state, document, databaseMatch, fixturePath, sequenceAccepted, rejection, sender.GetType().FullName, senderPath, expectedPath, activePath, runs.Count, DateTimeOffset.UtcNow);
|
||||||
|
}
|
||||||
private static void Guard(Action action) { try { if (!terminal) action(); } catch (System.Exception ex) { Fail("AUTOMATION_EVENT_HANDLER_FAILED", ex); } }
|
private static void Guard(Action action) { try { if (!terminal) action(); } catch (System.Exception ex) { Fail("AUTOMATION_EVENT_HANDLER_FAILED", ex); } }
|
||||||
private static void Fail(string code, System.Exception exception) { if (terminal) return; terminal = true; transitions.Add("Failed:" + code); try { Write(false, code + ":" + exception); } finally { Unsubscribe(); } }
|
private static void Fail(string code, System.Exception exception) { if (terminal) return; terminal = true; transitions.Add("Failed:" + code); try { Write(false, code + ":" + exception); } finally { Unsubscribe(); } }
|
||||||
private static void Unsubscribe() { if (fixture is null) return; try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { } }
|
private static void Unsubscribe()
|
||||||
|
{
|
||||||
|
if (fixture is not null) try { fixture.CommandWillStart -= OnWillStart; fixture.CommandEnded -= OnEnded; fixture.CommandFailed -= OnFailed; fixture.CommandCancelled -= OnCancelled; } catch { }
|
||||||
|
preparedContext = null;
|
||||||
|
selectedTargetSnapshotId = null;
|
||||||
|
}
|
||||||
private static int? Dbmod() { try { return Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD")); } catch { return null; } }
|
private static int? Dbmod() { try { return Convert.ToInt32(AcadApplication.GetSystemVariable("DBMOD")); } catch { return null; } }
|
||||||
private static AutomationEntityCounts Counts(Database db)
|
private static AutomationEntityCounts Counts(Database db)
|
||||||
{
|
{
|
||||||
@@ -123,7 +220,9 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
{
|
{
|
||||||
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION_CONFIG") ?? throw new InvalidOperationException("AUTOMATION_CONFIG_MISSING");
|
var path = Environment.GetEnvironmentVariable("P_REC_ANNOT_AUTOMATION_CONFIG") ?? throw new InvalidOperationException("AUTOMATION_CONFIG_MISSING");
|
||||||
using var json = JsonDocument.Parse(File.ReadAllText(path)); var root = json.RootElement;
|
using var json = JsonDocument.Parse(File.ReadAllText(path)); var root = json.RootElement;
|
||||||
return new(root.GetProperty("phase").GetString() ?? "Preview", root.GetProperty("orthoDrawingPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED"), root.GetProperty("evidencePath").GetString() ?? throw new InvalidOperationException("AUTOMATION_EVIDENCE_PATH_REQUIRED"), root.GetProperty("fixtureMarkerPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_MARKER_PATH_REQUIRED"));
|
var target = root.TryGetProperty("targetCommand", out var command) ? command.GetString() : "PXP-OA";
|
||||||
|
if (string.IsNullOrWhiteSpace(target) || !target.All(value => char.IsAsciiLetterOrDigit(value) || value is '-' or '_')) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_INVALID");
|
||||||
|
return new(root.GetProperty("phase").GetString() ?? "Preview", root.GetProperty("orthoDrawingPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_ORTHO_PATH_REQUIRED"), root.GetProperty("evidencePath").GetString() ?? throw new InvalidOperationException("AUTOMATION_EVIDENCE_PATH_REQUIRED"), root.GetProperty("fixtureMarkerPath").GetString() ?? throw new InvalidOperationException("AUTOMATION_MARKER_PATH_REQUIRED"), root.GetProperty("entryAssemblySha256").GetString() ?? throw new InvalidOperationException("AUTOMATION_ENTRY_ASSEMBLY_HASH_REQUIRED"), target, root.TryGetProperty("diagnostic", out var diagnostic) && diagnostic.ValueKind == JsonValueKind.True);
|
||||||
}
|
}
|
||||||
private static void RequireGate(Config value)
|
private static void RequireGate(Config value)
|
||||||
{
|
{
|
||||||
@@ -134,8 +233,44 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
{
|
{
|
||||||
if (config is null) return;
|
if (config is null) return;
|
||||||
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
||||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, CompletedCommands = runs.Where(x => x.Ended).Select(x => "PXP-OA").ToArray(), PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null, EvidenceFlushed = true, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions };
|
var apply = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var afterUndo = apply && terminal ? Counts(database!) : null;
|
||||||
|
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, TargetCommand = config.TargetCommand, Completed = completed, Prepared = prepared, FunctionalEvidenceCompleted = completed, ErrorCode = error, DbmodBefore = dbmodBaseline, DbmodAfterCommand1 = first?.DbmodAfter, DbmodAfterCommand2 = second?.DbmodAfter, DbmodAfterUndo = apply && terminal ? Dbmod() : null, Baseline = baseline, AfterCommand1 = first?.After, AfterCommand2 = second?.After, AfterUndo = afterUndo, CompletedCommands = runs.Where(x => x.Ended).Select(x => config.TargetCommand).Concat(apply && commandEvents.Any(x => string.Equals(x, "Ended:UNDO", StringComparison.OrdinalIgnoreCase)) ? ["UNDO"] : []).ToArray(), PlanIdRun1 = first?.PlanId, PlanIdRun2 = second?.PlanId, PlanIdsEqual = first is not null && second is not null ? string.Equals(first.PlanId, second.PlanId, StringComparison.Ordinal) : null, EligibleCandidateCount = first?.Candidates.Count, SelectedCandidateHash = first?.Candidates.OrderBy(x => x, StringComparer.Ordinal).FirstOrDefault(), ViewHash = first?.View, CreateRun1 = first?.Create, CreateRun2 = second?.Create, SkipRun2 = second?.Skip, ApplyOutcomeRun1 = first?.ApplyOutcome, ApplyOutcomeRun2 = second?.ApplyOutcome, PermitAccepted = first?.PermitAccepted, CommandsCompleted = runs.Count == 2 && runs.All(x => x.Ended) && (!apply || commandEvents.Any(x => string.Equals(x, "Ended:UNDO", StringComparison.OrdinalIgnoreCase))), InspectorsCompleted = baseline is not null && first?.After is not null && second?.After is not null && (!apply || afterUndo is not null), EvidenceFlushed = true, ExceptionText = applyExceptionText, CurrentState = terminal ? (completed ? "Completed" : "Failed") : "Prepared", StateTransitions = transitions, PxpOaDiscovery = discovery, Sentinels = sentinels, CommandEvents = commandEvents, CommandCorrelations = correlations };
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(config.EvidencePath)!);
|
||||||
var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true);
|
var temp = config.EvidencePath + ".tmp"; File.WriteAllText(temp, JsonSerializer.Serialize(evidence)); File.Move(temp, config.EvidencePath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PxpOaDiscoveryEvidence DiscoverTargetCommand(Config value)
|
||||||
|
{
|
||||||
|
var entry = typeof(EntryPoint).Assembly;
|
||||||
|
var loaded = AppDomain.CurrentDomain.GetAssemblies().Any(x => ReferenceEquals(x, entry));
|
||||||
|
var location = entry.Location;
|
||||||
|
var hash = File.Exists(location) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(location))) : null;
|
||||||
|
var declarations = AppDomain.CurrentDomain.GetAssemblies().SelectMany(Declarations).Where(x => string.Equals(x.Name, value.TargetCommand, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||||
|
var owner = declarations.SingleOrDefault(x => x.Assembly == entry);
|
||||||
|
var commandClass = entry.GetCustomAttributesData().Any(x => string.Equals(x.AttributeType.FullName, "Autodesk.AutoCAD.Runtime.CommandClassAttribute", StringComparison.Ordinal) && x.ConstructorArguments.Any(a => string.Equals(a.Value as Type, typeof(EntryPoint))));
|
||||||
|
return new(owner is not null, owner?.Name, owner?.Group, owner?.Flags, owner?.Type.FullName, owner?.Assembly.GetName().Name, location, hash, loaded, commandClass, hash is not null && string.Equals(hash, value.ExpectedEntryAssemblySha256, StringComparison.OrdinalIgnoreCase), declarations.Length);
|
||||||
|
}
|
||||||
|
private sealed record Declaration(string Name, string? Group, string? Flags, Type Type, Assembly Assembly);
|
||||||
|
private static IEnumerable<Declaration> Declarations(Assembly assembly)
|
||||||
|
{
|
||||||
|
Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException exception) { types = exception.Types.Where(x => x is not null).Cast<Type>().ToArray(); }
|
||||||
|
foreach (var type in types) foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) foreach (var attribute in method.GetCustomAttributesData().Where(x => string.Equals(x.AttributeType.FullName, "Autodesk.AutoCAD.Runtime.CommandMethodAttribute", StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
var name = attribute.ConstructorArguments.FirstOrDefault().Value as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||||
|
var group = attribute.NamedArguments.FirstOrDefault(x => x.MemberName == "GroupName").TypedValue.Value as string;
|
||||||
|
var flags = attribute.NamedArguments.FirstOrDefault(x => x.MemberName == "Flags").TypedValue.Value?.ToString();
|
||||||
|
yield return new(name, group, flags, type, assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void RequireDiscovery(PxpOaDiscoveryEvidence value, string targetCommand)
|
||||||
|
{
|
||||||
|
if (!value.AssemblyLoaded) throw new InvalidOperationException("AUTOMATION_TARGET_ASSEMBLY_NOT_LOADED");
|
||||||
|
if (!value.BundleHashMatched) throw new InvalidOperationException("AUTOMATION_TARGET_BUNDLE_HASH_MISMATCH");
|
||||||
|
if (!value.CommandClassIncluded) throw new InvalidOperationException("AUTOMATION_TARGET_COMMANDCLASS_NOT_REGISTERED");
|
||||||
|
if (value.DeclarationCount > 1) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_COLLISION");
|
||||||
|
if (value.DeclarationCount == 0 || !value.Found) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_NOT_FOUND");
|
||||||
|
if (!string.Equals(value.GlobalName, targetCommand, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("AUTOMATION_TARGET_GLOBAL_NAME_MISMATCH");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.AutoCAD.Runtime;
|
||||||
|
using Autodesk.AutoCAD.Windows;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class SwapAttributesCommand
|
||||||
|
{
|
||||||
|
private const string ApplicationName = "PXP-ALC";
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (document is null || PlantApplication.CurrentProject is null) return;
|
||||||
|
var selected = document.Editor.SelectImplied();
|
||||||
|
if (selected.Status != PromptStatus.OK || selected.Value.Count == 0) selected = document.Editor.GetSelection();
|
||||||
|
if (selected.Status != PromptStatus.OK || selected.Value.Count == 0) return;
|
||||||
|
using var documentLock = document.LockDocument();
|
||||||
|
using var transaction = document.Database.TransactionManager.StartTransaction();
|
||||||
|
if (transaction.GetObject(selected.Value.GetObjectIds()[0], OpenMode.ForWrite, false) is not BlockReference block || block.AttributeCollection.Count != 2)
|
||||||
|
{ document.Editor.WriteMessage("\nPXP-AC requires a block with exactly two attributes."); return; }
|
||||||
|
var first = transaction.GetObject(block.AttributeCollection[0], OpenMode.ForWrite, false) as AttributeReference;
|
||||||
|
var second = transaction.GetObject(block.AttributeCollection[1], OpenMode.ForWrite, false) as AttributeReference;
|
||||||
|
if (first is null || second is null || first.IsConstant || second.IsConstant || !first.IsMTextAttribute || !second.IsMTextAttribute)
|
||||||
|
{ document.Editor.WriteMessage("\nPXP-AC requires two editable multiline attributes."); return; }
|
||||||
|
var data = block.GetXDataForApplication(ApplicationName)?.AsArray();
|
||||||
|
if (data is null) { document.Editor.WriteMessage("\nRun PXP-ALC before swapping connection attributes."); return; }
|
||||||
|
var firstText = first.MTextAttribute.Contents;
|
||||||
|
var secondText = second.MTextAttribute.Contents;
|
||||||
|
first.TextString = secondText; second.TextString = firstText;
|
||||||
|
first.UpdateMTextAttribute(); second.UpdateMTextAttribute();
|
||||||
|
ResizeShelf(block, first, second);
|
||||||
|
SwapXData(data, "LINENUMBER1", "LINENUMBER2");
|
||||||
|
SwapXData(data, "ANNOTATIONTEXT1", "ANNOTATIONTEXT2");
|
||||||
|
block.XData = new ResultBuffer(data);
|
||||||
|
transaction.Commit();
|
||||||
|
document.Editor.WriteMessage("\nPXP-AC: attributes swapped and shelf resized; AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("SwapAttributesCompleted", commandName: "PXP-AC", resultStatus: "Applied", document: document);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResizeShelf(BlockReference block, AttributeReference first, AttributeReference second)
|
||||||
|
{
|
||||||
|
var rotation = block.Rotation;
|
||||||
|
var firstOffset = new Point3d(block.Position.GetVectorTo(first.AlignmentPoint).ToArray()).RotateBy(-rotation, Vector3d.ZAxis, Point3d.Origin);
|
||||||
|
var secondOffset = new Point3d(block.Position.GetVectorTo(second.AlignmentPoint).ToArray()).RotateBy(-rotation, Vector3d.ZAxis, Point3d.Origin);
|
||||||
|
first.Rotation = 0; second.Rotation = 0; first.UpdateMTextAttribute(); second.UpdateMTextAttribute();
|
||||||
|
var length = AttributeSwapPlanner.ComputeShelfLength(
|
||||||
|
first.MTextAttribute.GeometricExtents.MaxPoint.X - first.MTextAttribute.GeometricExtents.MinPoint.X, firstOffset.X,
|
||||||
|
second.MTextAttribute.GeometricExtents.MaxPoint.X - second.MTextAttribute.GeometricExtents.MinPoint.X, secondOffset.X);
|
||||||
|
foreach (DynamicBlockReferenceProperty property in block.DynamicBlockReferencePropertyCollection)
|
||||||
|
if (string.Equals(property.PropertyName, "L", StringComparison.OrdinalIgnoreCase) && !property.ReadOnly) { property.Value = Math.Abs(length); break; }
|
||||||
|
first.Rotation = rotation; second.Rotation = rotation; first.UpdateMTextAttribute(); second.UpdateMTextAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SwapXData(TypedValue[] values, string firstMarker, string secondMarker)
|
||||||
|
{
|
||||||
|
var first = Array.FindIndex(values, value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value?.ToString(), firstMarker, StringComparison.Ordinal));
|
||||||
|
var second = Array.FindIndex(values, value => value.TypeCode == (int)DxfCode.ExtendedDataAsciiString && string.Equals(value.Value?.ToString(), secondMarker, StringComparison.Ordinal));
|
||||||
|
if (first < 0 || second < 0 || first + 1 >= values.Length || second + 1 >= values.Length) return;
|
||||||
|
(values[first + 1], values[second + 1]) = (values[second + 1], values[first + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class SwapAttributesContextMenu
|
||||||
|
{
|
||||||
|
private static ContextMenuExtension? menu;
|
||||||
|
public static void Attach()
|
||||||
|
{
|
||||||
|
if (menu is not null) return;
|
||||||
|
menu = new ContextMenuExtension();
|
||||||
|
var item = new MenuItem("Swap attributes and resize shelf");
|
||||||
|
item.Click += OnClick; menu.MenuItems.Add(item);
|
||||||
|
AcadApplication.AddObjectContextMenuExtension(RXObject.GetClass(typeof(BlockReference)), menu);
|
||||||
|
}
|
||||||
|
public static void Detach()
|
||||||
|
{
|
||||||
|
if (menu is null) return;
|
||||||
|
AcadApplication.RemoveObjectContextMenuExtension(RXObject.GetClass(typeof(BlockReference)), menu); menu = null;
|
||||||
|
}
|
||||||
|
private static void OnClick(object? sender, EventArgs args) => AcadApplication.DocumentManager.MdiActiveDocument?.SendStringToExecute("_.PXP-AC ", true, false, false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using Autodesk.AutoCAD.ApplicationServices;
|
||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.AutoCAD.Geometry;
|
||||||
|
using Autodesk.ProcessPower.Drawings2d;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.P3dProjectParts;
|
||||||
|
using Autodesk.ProcessPower.PnP3dOrthoDrawingsUI;
|
||||||
|
using Autodesk.ProcessPower.ProjectManager;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class UpdateOrthoDrawingsCommand
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var owner = AcadApplication.DocumentManager.MdiActiveDocument;
|
||||||
|
if (owner is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var project = PlantApplication.CurrentProject ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_REQUIRED");
|
||||||
|
var ortho = PlantApplication.ProjectPart("Ortho") as OrthoProject ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_PART_REQUIRED");
|
||||||
|
var drawings = ReadDrawings(project, ortho);
|
||||||
|
var selections = PromptSelections(owner.Editor, drawings);
|
||||||
|
if (selections.Count == 0) return;
|
||||||
|
var plan = new OrthoBatchPlanner().Build(drawings, selections);
|
||||||
|
foreach (var diagnostic in plan.Diagnostics) owner.Editor.WriteMessage("\nPXP-IOD: " + diagnostic);
|
||||||
|
owner.Editor.WriteMessage($"\nPXP-IOD preview: drawings={plan.Operations.Select(value => value.RelativePath).Distinct(StringComparer.OrdinalIgnoreCase).Count()}; views={plan.Operations.Count}; no drawings modified.");
|
||||||
|
RuntimeDiagnostics.Write("UpdateOrthoDrawingsPreviewCompleted", commandName: "PXP-IOD", resultStatus: "PreviewCompleted", document: owner, details: new Dictionary<string, object?> { ["DrawingCount"] = plan.Operations.Select(value => value.RelativePath).Distinct(StringComparer.OrdinalIgnoreCase).Count(), ["ViewCount"] = plan.Operations.Count });
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
var fixtureRoot = ValidateFixture();
|
||||||
|
var failures = Apply(project, ortho, plan, fixtureRoot);
|
||||||
|
owner.Editor.WriteMessage($"\nPXP-IOD Apply completed: views={plan.Operations.Count}; failures={failures.Count}.");
|
||||||
|
RuntimeDiagnostics.Write("UpdateOrthoDrawingsApplyCompleted", commandName: "PXP-IOD", resultStatus: failures.Count == 0 ? "Applied" : "PartiallyApplied", document: owner, details: new Dictionary<string, object?> { ["FailureCount"] = failures.Count, ["Failures"] = failures.ToArray() });
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
RuntimeDiagnostics.Write("UpdateOrthoDrawingsFailed", "ERROR", commandName: "PXP-IOD", resultStatus: exception.Message, exception: exception, document: owner);
|
||||||
|
owner.Editor.WriteMessage("\nPXP-IOD: " + exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<OrthoBatchDrawing> ReadDrawings(PlantProject project, OrthoProject ortho)
|
||||||
|
{
|
||||||
|
var root = Path.GetDirectoryName(project.FileName) ?? throw new InvalidOperationException("ORTHO_BATCH_PROJECT_ROOT_UNAVAILABLE");
|
||||||
|
return ortho.DwgSheetDefinitions.AsArray().Select(sheet =>
|
||||||
|
{
|
||||||
|
var path = UIUtils.GetDwgSheetDefResolvedPath(sheet);
|
||||||
|
var relative = Path.GetRelativePath(root, path);
|
||||||
|
return new OrthoBatchDrawing(sheet.DwgGuid.ToString(), Path.GetFileName(path), relative, sheet.DwgViewDefs.AsArray().Select(view => view.ViewName).ToArray());
|
||||||
|
}).OrderBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<OrthoBatchSelection> PromptSelections(Editor editor, IReadOnlyList<OrthoBatchDrawing> drawings)
|
||||||
|
{
|
||||||
|
var result = new List<OrthoBatchSelection>();
|
||||||
|
foreach (var drawing in drawings)
|
||||||
|
{
|
||||||
|
var prompt = new PromptKeywordOptions($"\nUpdate Ortho drawing {drawing.RelativePath}?") { AllowNone = true };
|
||||||
|
prompt.Keywords.Add("Yes"); prompt.Keywords.Add("No"); prompt.Keywords.Default = "No";
|
||||||
|
var answer = editor.GetKeywords(prompt);
|
||||||
|
if (answer.Status == PromptStatus.Cancel) return Array.Empty<OrthoBatchSelection>();
|
||||||
|
if (string.Equals(answer.StringResult, "Yes", StringComparison.OrdinalIgnoreCase)) result.Add(new(drawing.RelativePath, drawing.Views));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> Apply(PlantProject project, OrthoProject ortho, OrthoBatchPlan plan, string fixtureRoot)
|
||||||
|
{
|
||||||
|
var projectRoot = Path.GetDirectoryName(project.FileName)!;
|
||||||
|
var failures = new List<string>();
|
||||||
|
foreach (var group in plan.Operations.GroupBy(value => value.RelativePath, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var path = Path.GetFullPath(Path.Combine(projectRoot, group.Key));
|
||||||
|
if (!path.StartsWith(fixtureRoot, StringComparison.OrdinalIgnoreCase)) { failures.Add(group.Key + ":OUTSIDE_FIXTURE"); continue; }
|
||||||
|
Document? document = AcadApplication.DocumentManager.Cast<Document>().FirstOrDefault(value => string.Equals(Path.GetFullPath(value.Name), path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
var wasOpen = document is not null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (document is null) { UIUtils.OpenDocument(path, out document, false); document.Database.CloseInput(true); }
|
||||||
|
AcadApplication.DocumentManager.MdiActiveDocument = document;
|
||||||
|
using (document.LockDocument()) foreach (var operation in group) UpdateView(document, ortho, operation.ViewName);
|
||||||
|
if (wasOpen) document.Database.SaveAs(document.Name, true, DwgVersion.Current, document.Database.SecurityParameters); else document.CloseAndSave(path);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
failures.Add(group.Key + ":" + exception.GetType().Name);
|
||||||
|
if (!wasOpen && document is not null) try { document.CloseAndDiscard(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateView(Document document, OrthoProject ortho, string viewName)
|
||||||
|
{
|
||||||
|
if (!UIUtils.GetDwgSheetFromOrthoDrawingName(document.Database.Filename, out IDwgSheetDef sheet) || !UIUtils.GetViewDefByViewName(viewName, sheet, out IDwgViewDef definition))
|
||||||
|
throw new InvalidOperationException("ORTHO_BATCH_VIEW_NOT_FOUND:" + viewName);
|
||||||
|
var viewportId = FindViewport(document.Database, definition);
|
||||||
|
var frozen = ReadFrozenLayers(document.Database, viewportId); var off = new StringCollection();
|
||||||
|
if (!UIUtils.IsValidViewpport(viewportId, out double width, out double height, out double scale, out Point3d center) || !definition.SetLayout(width, height, ref center))
|
||||||
|
throw new InvalidOperationException("ORTHO_BATCH_VIEW_LAYOUT_INVALID:" + viewName);
|
||||||
|
definition.ViewScale = scale;
|
||||||
|
var resolved = new StringCollection(); resolved.AddRange(UIUtils.GetResolvedRefModels(definition, false).Select(value => value.ModelFileName).ToArray());
|
||||||
|
var models = new ArrayList(); UIUtils.UpdateFileCollectionWithXrefs(resolved, ref models, out Extents3d extents);
|
||||||
|
definition.DwgQuery.ModelFiles = models.ToArray(typeof(ModelFile)) as ModelFile[];
|
||||||
|
definition.DwgQuery.Execute(frozen, off);
|
||||||
|
UIUtils.CreateOrUpdateOrthoView(null!, sheet, viewName, ortho, false, true, Dwg2dGen.NewDwgSetClipper(definition, 0), 0, frozen, off, new Vector3d(), 0, new Vector3d(), null!);
|
||||||
|
definition.ViewCreationTime = DateTime.UtcNow.ToString("O");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ObjectId FindViewport(Database database, IDwgViewDef definition)
|
||||||
|
{
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var layouts = (DBDictionary)transaction.GetObject(database.LayoutDictionaryId, OpenMode.ForRead);
|
||||||
|
foreach (DBDictionaryEntry entry in layouts)
|
||||||
|
{
|
||||||
|
var layout = (Layout)transaction.GetObject(entry.Value, OpenMode.ForRead);
|
||||||
|
if (layout.ModelType) continue;
|
||||||
|
var paper = (BlockTableRecord)transaction.GetObject(layout.BlockTableRecordId, OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in paper)
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForRead) is Viewport viewport && viewport.Number > 1 &&
|
||||||
|
Math.Abs(viewport.CenterPoint.X - definition.CenterPoint.X) < .001 && Math.Abs(viewport.CenterPoint.Y - definition.CenterPoint.Y) < .001 &&
|
||||||
|
Math.Abs(viewport.Width - definition.ViewWidth) < .001 && Math.Abs(viewport.Height - definition.ViewHeight) < .001) return id;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("ORTHO_BATCH_VIEWPORT_NOT_FOUND:" + definition.ViewName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StringCollection ReadFrozenLayers(Database database, ObjectId viewportId)
|
||||||
|
{
|
||||||
|
var result = new StringCollection();
|
||||||
|
using var transaction = database.TransactionManager.StartOpenCloseTransaction();
|
||||||
|
var viewport = (Viewport)transaction.GetObject(viewportId, OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in viewport.GetFrozenLayers()) if (transaction.GetObject(id, OpenMode.ForRead) is LayerTableRecord layer) result.Add(layer.Name);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ValidateFixture()
|
||||||
|
{
|
||||||
|
var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_BATCH_FIXTURE_MARKER");
|
||||||
|
if (string.IsNullOrWhiteSpace(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_BATCH_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_BATCH_TEST_FIXTURE_REQUIRED");
|
||||||
|
return Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
using Autodesk.AutoCAD.DatabaseServices;
|
||||||
|
using Autodesk.AutoCAD.EditorInput;
|
||||||
|
using Autodesk.ProcessPower.DataObjects;
|
||||||
|
using Autodesk.ProcessPower.Drawings2d;
|
||||||
|
using Autodesk.ProcessPower.PlantInstance;
|
||||||
|
using Autodesk.ProcessPower.PnP3dOrthoDrawingsUI;
|
||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using AcadApplication = Autodesk.AutoCAD.ApplicationServices.Core.Application;
|
||||||
|
using AcadColor = Autodesk.AutoCAD.Colors.Color;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Plant3D2026;
|
||||||
|
|
||||||
|
internal static class UpdateOrthoViewBlocksCommand
|
||||||
|
{
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
var document = AcadApplication.DocumentManager.MdiActiveDocument; if (document is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = Environment.GetEnvironmentVariable("P_REC_ANNOT_LINE_NUMBER_DRAWING_SETTINGS_PATH");
|
||||||
|
if (string.IsNullOrWhiteSpace(settings) || !File.Exists(settings)) throw new InvalidOperationException("ORTHO_LAYER_CONFIGURATION_REQUIRED");
|
||||||
|
var rules = new OrthoLayerSettingsLoader().Load(File.ReadAllText(settings));
|
||||||
|
var options = new PromptEntityOptions("\nВыберите видовой экран для обновления слоев блоков: "); options.SetRejectMessage("\nДолжен быть видовой экран."); options.AddAllowedClass(typeof(Viewport), true);
|
||||||
|
var selected = document.Editor.GetEntity(options); if (selected.Status != PromptStatus.OK) return;
|
||||||
|
string viewName; var changes = new List<(string Layer, OrthoLayerRule Rule)>();
|
||||||
|
using (var transaction = document.Database.TransactionManager.StartOpenCloseTransaction())
|
||||||
|
{
|
||||||
|
var viewport = (Viewport)transaction.GetObject(selected.ObjectId, OpenMode.ForRead);
|
||||||
|
var context = new PlantRuntimeContextReader().ReadCurrentContext(document);
|
||||||
|
var matched = context.Viewports.Where(x => Math.Abs(x.CenterX - viewport.CenterPoint.X) < .001 && Math.Abs(x.CenterY - viewport.CenterPoint.Y) < .001 && Math.Abs(x.Width - viewport.Width) < .001 && Math.Abs(x.Height - viewport.Height) < .001).ToArray();
|
||||||
|
viewName = matched.Length == 1 ? matched[0].ViewName ?? matched[0].DisplayName : throw new InvalidOperationException(matched.Length == 0 ? "ORTHO_VIEW_NOT_FOUND" : "ORTHO_VIEW_AMBIGUOUS");
|
||||||
|
var layers = (LayerTable)transaction.GetObject(document.Database.LayerTableId, OpenMode.ForRead);
|
||||||
|
foreach (ObjectId id in layers)
|
||||||
|
{
|
||||||
|
var layer = (LayerTableRecord)transaction.GetObject(id, OpenMode.ForRead); var rule = rules.FirstOrDefault(x => x.MatchesLayer(viewName, layer.Name));
|
||||||
|
if (rule is not null) changes.Add((layer.Name, rule));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.Editor.WriteMessage($"\nPXP-UOB preview: view={viewName}; matching-layers={changes.Count}. DWG не изменён.");
|
||||||
|
RuntimeDiagnostics.Write("UpdateOrthoViewBlocksPreviewCompleted", commandName: "PXP-UOB", resultStatus: "PreviewCompleted", document: document, details: new Dictionary<string, object?> { ["ViewName"] = viewName, ["LayerCount"] = changes.Count });
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("P_REC_ANNOT_RUNTIME_MODE"), "Apply", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
ValidateFixture(document);
|
||||||
|
using var documentLock = document.LockDocument(); using var write = document.Database.TransactionManager.StartTransaction();
|
||||||
|
var layerTable = (LayerTable)write.GetObject(document.Database.LayerTableId, OpenMode.ForRead); var linetypes = (LinetypeTable)write.GetObject(document.Database.LinetypeTableId, OpenMode.ForRead);
|
||||||
|
var representationCount = ApplyRepresentationRules(document, selected.ObjectId, viewName, rules, write, linetypes);
|
||||||
|
foreach (var change in changes)
|
||||||
|
{
|
||||||
|
var layer = (LayerTableRecord)write.GetObject(layerTable[change.Layer], OpenMode.ForWrite); Apply(layer, change.Rule, linetypes);
|
||||||
|
}
|
||||||
|
var viewportToUpdate = (Viewport)write.GetObject(selected.ObjectId, OpenMode.ForWrite); if (layerTable.Has("Defpoints")) viewportToUpdate.LayerId = layerTable["Defpoints"];
|
||||||
|
write.Commit(); document.Editor.WriteMessage($"\nPXP-UOB Apply: updated-layers={changes.Count}; updated-representations={representationCount}; AutoCAD UNDO is available.");
|
||||||
|
RuntimeDiagnostics.Write("UpdateOrthoViewBlocksApplyCompleted", commandName: "PXP-UOB", resultStatus: "Applied", document: document, details: new Dictionary<string, object?> { ["LayerCount"] = changes.Count, ["RepresentationCount"] = representationCount });
|
||||||
|
}
|
||||||
|
catch (Exception exception) { RuntimeDiagnostics.Write("UpdateOrthoViewBlocksFailed", "ERROR", commandName: "PXP-UOB", resultStatus: exception.Message, exception: exception, document: document); document.Editor.WriteMessage("\nPXP-UOB: " + exception.Message); }
|
||||||
|
}
|
||||||
|
private static void Apply(LayerTableRecord layer, OrthoLayerRule rule, LinetypeTable linetypes)
|
||||||
|
{
|
||||||
|
if (rule.LineWeight.HasValue) layer.LineWeight = (LineWeight)rule.LineWeight.Value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(rule.LineType) && linetypes.Has(rule.LineType)) layer.LinetypeObjectId = linetypes[rule.LineType];
|
||||||
|
if (rule.Red.HasValue) layer.Color = AcadColor.FromRgb(rule.Red.Value, rule.Green!.Value, rule.Blue!.Value);
|
||||||
|
else if (rule.ColorIndex.HasValue) layer.Color = AcadColor.FromColorIndex(Autodesk.AutoCAD.Colors.ColorMethod.ByColor, rule.ColorIndex.Value);
|
||||||
|
}
|
||||||
|
private static int ApplyRepresentationRules(Autodesk.AutoCAD.ApplicationServices.Document document, ObjectId viewportId, string viewName, IReadOnlyList<OrthoLayerRule> rules, Transaction transaction, LinetypeTable linetypes)
|
||||||
|
{
|
||||||
|
var manager = PlantApplication.ProjectPart("Piping")?.DataLinksManager ?? throw new InvalidOperationException("ORTHO_PIPING_DATALINKS_UNAVAILABLE");
|
||||||
|
var database = manager.GetPnPDatabase();
|
||||||
|
IDwgSheetDef sheet;
|
||||||
|
IDwgViewDef view;
|
||||||
|
List<ObjectId> representations;
|
||||||
|
if (!UIUtils.GetDwgSheetFromOrthoDrawingName(document.Database.Filename, out sheet) ||
|
||||||
|
!UIUtils.GetViewDefByViewName(viewName, sheet, out view) ||
|
||||||
|
!UIUtils.GetOrthoEntityListInView(UIUtils.GetDwgSheetDefResolvedPath(sheet), viewName, out representations))
|
||||||
|
throw new InvalidOperationException("ORTHO_VIEW_REPRESENTATIONS_UNAVAILABLE");
|
||||||
|
var changed = 0;
|
||||||
|
foreach (var id in representations.Where(value => !value.IsNull).OrderBy(value => value.Handle.Value))
|
||||||
|
{
|
||||||
|
if (transaction.GetObject(id, OpenMode.ForWrite, false) is not BlockReference block) continue;
|
||||||
|
var modelId = PnPDwg2dUtil.GetModelObjectId(id, document.Database);
|
||||||
|
var rowId = PnPDwg2dUtil.PipingRowId(modelId);
|
||||||
|
var engineering = database.Tables["EngineeringItems"].Select("PnPID=" + rowId);
|
||||||
|
var equipment = database.Tables["Equipment"].Select("PnPID=" + rowId);
|
||||||
|
if (engineering.Length == 1 && equipment.Length == 0)
|
||||||
|
{
|
||||||
|
var status = Convert.ToString(engineering[0]["Status"]) ?? string.Empty;
|
||||||
|
var statusRule = rules.FirstOrDefault(rule => rule.MatchesStatus(status));
|
||||||
|
if (statusRule is not null) Apply(block, statusRule, linetypes);
|
||||||
|
}
|
||||||
|
else if (engineering.Length == 1 && equipment.Length == 1) block.LineWeight = LineWeight.ByLineWeightDefault;
|
||||||
|
if (transaction.GetObject(block.BlockTableRecord, OpenMode.ForRead) is BlockTableRecord definition)
|
||||||
|
foreach (ObjectId nestedId in definition)
|
||||||
|
if (transaction.GetObject(nestedId, OpenMode.ForWrite, false) is Entity nested && rules.FirstOrDefault(rule => rule.MatchesLayer(viewName, nested.Layer)) is { } nestedRule)
|
||||||
|
Apply(nested, nestedRule, linetypes);
|
||||||
|
changed++;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
private static void Apply(Entity entity, OrthoLayerRule rule, LinetypeTable linetypes)
|
||||||
|
{
|
||||||
|
if (rule.LineWeight.HasValue) entity.LineWeight = (LineWeight)rule.LineWeight.Value;
|
||||||
|
if (!string.IsNullOrWhiteSpace(rule.LineType) && linetypes.Has(rule.LineType)) entity.LinetypeId = linetypes[rule.LineType];
|
||||||
|
if (rule.Red.HasValue) entity.Color = AcadColor.FromRgb(rule.Red.Value, rule.Green!.Value, rule.Blue!.Value);
|
||||||
|
else if (rule.ColorIndex.HasValue) entity.Color = AcadColor.FromColorIndex(Autodesk.AutoCAD.Colors.ColorMethod.ByColor, rule.ColorIndex.Value);
|
||||||
|
}
|
||||||
|
private static void ValidateFixture(Autodesk.AutoCAD.ApplicationServices.Document document)
|
||||||
|
{
|
||||||
|
var marker = Environment.GetEnvironmentVariable("P_REC_ANNOT_ORTHO_MAINTENANCE_FIXTURE_MARKER");
|
||||||
|
if (string.IsNullOrWhiteSpace(marker) || !File.Exists(marker) || !string.Equals(File.ReadAllText(marker).Trim(), "P_REC_ANNOT_TEST_FIXTURE=ORTHO_MAINTENANCE_APPLY", StringComparison.Ordinal)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
|
||||||
|
var root = Path.GetDirectoryName(Path.GetFullPath(marker))! + Path.DirectorySeparatorChar; if (!Path.GetFullPath(document.Name).StartsWith(root, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("ORTHO_MAINTENANCE_TEST_FIXTURE_REQUIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class AnnotateLineNumbersSettingsLoaderTests
|
||||||
|
{
|
||||||
|
private const string Xml = "<Main><LineNumbers><Marker layer='M' maskscalefactor='1,5'><Text layer='T' textstyle='S' size='2,5' textvalue='' positionx='1' positiony='2'/><Line layer='L' startx='0' starty='0' endx='3' endy='4'/></Marker><List layer='LIST' maskscalefactor='1.2' lineheight='5' linespacingdistance='6'><MarkerText layer='T' textstyle='S' size='2' textvalue='' positionx='0' positiony='0'/><LineNumberText layer='T' textstyle='S' size='2' textvalue='' positionx='4' positiony='0'/><Line layer='L' startx='0' starty='0'/></List></LineNumbers></Main>";
|
||||||
|
[Fact] public void LoadsLegacyMarkerAndListContract() { var x = new AnnotateLineNumbersSettingsLoader().Load(Xml); Assert.Equal(1.5, x.Marker.MaskScaleFactor); Assert.Equal("M", x.Marker.Layer); Assert.Equal(6, x.List.LineSpacingDistance); Assert.Equal(0, x.List.Line.End.X); }
|
||||||
|
[Fact] public void RejectsMissingRequiredLayer() => Assert.Equal("LINE_NUMBER_SETTINGS_INVALID:layer", Assert.Throws<InvalidDataException>(() => new AnnotateLineNumbersSettingsLoader().Load(Xml.Replace(" layer='M'", ""))).Message);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class AttributeSwapPlannerTests
|
||||||
|
{
|
||||||
|
[Fact] public void Shelf_length_preserves_legacy_longer_attribute_rule() => Assert.Equal(6.9, AttributeSwapPlanner.ComputeShelfLength(4, 1, 5, 1));
|
||||||
|
[Fact] public void Metadata_values_are_swapped_without_losing_other_ownership_fields()
|
||||||
|
{
|
||||||
|
var result = AttributeSwapPlanner.Swap(new Dictionary<string, string> { ["LINENUMBER1"] = "10", ["LINENUMBER2"] = "20", ["VIEW"] = "A" }, "LINENUMBER1", "LINENUMBER2");
|
||||||
|
Assert.Equal("20", result["LINENUMBER1"]); Assert.Equal("10", result["LINENUMBER2"]); Assert.Equal("A", result["VIEW"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ComponentGeometryContractTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GeometryDefinition_PreservesStableParametersAndBounds()
|
||||||
|
{
|
||||||
|
var geometry = new ComponentGeometryDefinition("CPP", new Dictionary<string, string> { ["D"] = "25.4", ["L"] = "508" }, new(new(0, 0, 0), new(1, 2, 3)), true);
|
||||||
|
Assert.Equal("25.4", geometry.Parameters["D"]);
|
||||||
|
Assert.Equal(new Point3Dto(1, 2, 3), geometry.Bounds.Maximum);
|
||||||
|
Assert.True(geometry.ImperialDimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ConnectorViewportEligibilityEvaluatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UsesSecondPortAsLegacyPlacementAnchorWhenBothPortsAreVisible()
|
||||||
|
{
|
||||||
|
var result = new ConnectorViewportEligibilityEvaluator().Evaluate(Ports(new(1, 2, 3), new(4, 5, 6), new(1, 0, 0), new(0, 1, 0)), new(0, 0, 1), Bounds());
|
||||||
|
Assert.True(result.Eligible);
|
||||||
|
Assert.Equal(new Point3Dto(4, 5, 6), result.PlacementPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsPortParallelToViewDirection()
|
||||||
|
{
|
||||||
|
var result = new ConnectorViewportEligibilityEvaluator().Evaluate(Ports(new(), new(), new(0, 0, 1), new(1, 0, 0)), new(0, 0, 1), Bounds());
|
||||||
|
Assert.Equal("CONNECTOR_PORT_PARALLEL_TO_VIEW", result.FailureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsConnectorWhenAnyPortLiesOutsideModelViewExtents()
|
||||||
|
{
|
||||||
|
var result = new ConnectorViewportEligibilityEvaluator().Evaluate(Ports(new(0, 0, 0), new(11, 0, 0), new(1, 0, 0), new(0, 1, 0)), new(0, 0, 1), Bounds());
|
||||||
|
Assert.Equal("CONNECTOR_OUTSIDE_VIEW_EXTENTS", result.FailureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bounds3Dto Bounds() => new(new(-10, -10, -10), new(10, 10, 10));
|
||||||
|
private static PlantConnectorPortSnapshot Ports(Point3Dto firstPosition, Point3Dto secondPosition, Point3Dto firstDirection, Point3Dto secondDirection) => new("c", firstPosition, secondPosition, firstDirection, secondDirection);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ElevationAnnotationPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PlansCreateUpdateAlreadyCurrentAndConflictsDeterministically()
|
||||||
|
{
|
||||||
|
var a = new ElevationCandidate("a", 5, 0, new(0, 0), 0, "PXP", "GOSTC");
|
||||||
|
var b = new ElevationCandidate("b", 10, 0, new(1, 0), 0, "PXP", "GOSTC");
|
||||||
|
var plan = new ElevationAnnotationPlanner().Build([b, a, a], [new("old-a", "a", "+0,005", new(0, 0)), new("old-b1", "b", "+0,010", new()), new("old-b2", "b", "+0,010", new())]);
|
||||||
|
Assert.Collection(plan.Operations,
|
||||||
|
value => Assert.Equal(AnnotationOperationKind.Skip, value.Kind),
|
||||||
|
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind),
|
||||||
|
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind));
|
||||||
|
Assert.Equal("ALREADY_CURRENT", plan.Operations[0].Reasons.Single());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlansUpdateWhenTheFormattedElevationChanged()
|
||||||
|
{
|
||||||
|
var plan = new ElevationAnnotationPlanner().Build([new ElevationCandidate("a", 13, 0, new(), 0, "PXP", "GOSTC")], [new("old", "a", "+0,010", new())]);
|
||||||
|
Assert.Equal(AnnotationOperationKind.Update, plan.Operations.Single().Kind);
|
||||||
|
Assert.Equal("+0,015", plan.Operations.Single().Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ElevationSettingsLoaderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ReadsLegacyElevationAssets()
|
||||||
|
{
|
||||||
|
var result = new ElevationSettingsLoader().Load("<Main><Elevations layer='ELEV' textstyle='GOST'/></Main>");
|
||||||
|
Assert.Equal("ELEV", result.Layer);
|
||||||
|
Assert.Equal("GOST", result.TextStyle);
|
||||||
|
Assert.Equal("BLOCK_ELEVATION", result.BlockName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("<Main/>")]
|
||||||
|
[InlineData("<Main><Elevations layer='' textstyle='GOST'/></Main>")]
|
||||||
|
[InlineData("<Main><Elevations layer='ELEV'/></Main>")]
|
||||||
|
public void RejectsIncompleteLegacyAssets(string xml) =>
|
||||||
|
Assert.Throws<InvalidDataException>(() => new ElevationSettingsLoader().Load(xml));
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class ExtentsIntersectionClassifierTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0, 2, 2, ExtentsIntersectionType.ExtentsMatch)]
|
||||||
|
[InlineData(0, 0, 4, 4, ExtentsIntersectionType.FirstExtentsInsideSecondExtents)]
|
||||||
|
[InlineData(3, 3, 5, 5, ExtentsIntersectionType.DoNotIntersect)]
|
||||||
|
public void ClassifiesPlanarExtents(double min, double minY, double max, double maxY, ExtentsIntersectionType expected) => Assert.Equal(expected, ExtentsIntersectionClassifier.Classify(Box(0, 0, 2, 2), Box(min, minY, max, maxY)));
|
||||||
|
[Fact] public void TouchingExtents_Intersect() => Assert.Equal(ExtentsIntersectionType.Intersect, ExtentsIntersectionClassifier.Classify(Box(0, 0, 2, 2), Box(2, 1, 3, 2)));
|
||||||
|
private static Bounds3Dto Box(double x1, double y1, double x2, double y2) => new(new(x1, y1, 0), new(x2, y2, 1));
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LegacyElevationFormatterTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0, "0,000")]
|
||||||
|
[InlineData(2.49, 0, "0,000")]
|
||||||
|
[InlineData(2.5, 0, "+0,005")]
|
||||||
|
[InlineData(-2.5, 0, "-0,005")]
|
||||||
|
[InlineData(1233, 2, "+1,235")]
|
||||||
|
[InlineData(-1233, -2, "-1,235")]
|
||||||
|
public void PreservesFiveMillimetreAwayFromZeroLegacyRule(double modelZ, double datum, string expected) =>
|
||||||
|
Assert.Equal(expected, LegacyElevationFormatter.Calculate(modelZ, datum));
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(5, "+0,005")]
|
||||||
|
[InlineData(50, "+0,050")]
|
||||||
|
[InlineData(500, "+0,500")]
|
||||||
|
[InlineData(1000, "+1,000")]
|
||||||
|
[InlineData(-5, "-0,005")]
|
||||||
|
public void FormatsSignedMetresFromLegacyMillimetreInteger(int value, string expected) =>
|
||||||
|
Assert.Equal(expected, LegacyElevationFormatter.FormatMillimetres(value));
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineConnectionBoundaryPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EmitsSortedTextOnlyForDifferentLineGroups()
|
||||||
|
{
|
||||||
|
var result = new LineConnectionBoundaryPlanner().Build([
|
||||||
|
Candidate("b", "L-200", "L-100"), Candidate("a", "L-100", "L-100")]);
|
||||||
|
Assert.Equal("a", result[0].StableId);
|
||||||
|
Assert.Contains("SAME_LINE_GROUP", result[0].Reasons);
|
||||||
|
Assert.Equal("L-100 / L-200", result[1].Text);
|
||||||
|
Assert.Equal(new Point3Dto(), result[1].ConnectorPosition);
|
||||||
|
Assert.Empty(result[1].Reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsDuplicateAndMissingLineGroupsDeterministically()
|
||||||
|
{
|
||||||
|
var result = new LineConnectionBoundaryPlanner().Build([Candidate("a", "", "L-2"), Candidate("a", "L-1", "L-2")]);
|
||||||
|
Assert.Contains("LINE_GROUP_NOT_RESOLVED", result[0].Reasons);
|
||||||
|
Assert.Contains("DUPLICATE_CONNECTOR_CANDIDATE", result[1].Reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineConnectionBoundaryCandidate Candidate(string id, string first, string second) => new(id, first, second, new(), new(1, 1, 1), new(new(), new(1, 1, 1)), new(2, 3), 0);
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineConnectionPreviewServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void FormatsBothRelatedPartsAndKeepsConnectorOrderDeterministic()
|
||||||
|
{
|
||||||
|
var service = CreateService(new Dictionary<long, IReadOnlyDictionary<string, string?>>
|
||||||
|
{
|
||||||
|
[10] = Fields("L-200"), [20] = Fields("L-100"), [30] = Fields("L-300"), [40] = Fields("L-400")
|
||||||
|
});
|
||||||
|
var result = service.Preview(Definition(), [Connector("b", 10, 20, 1, 2), Connector("a", 30, 40, 3, 4)], Placements("a", "b"));
|
||||||
|
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Equal(["a", "b"], result.Plans.Select(plan => plan.StableId));
|
||||||
|
Assert.Equal("L-100 / L-200", result.Plans[1].Text);
|
||||||
|
Assert.Equal("L-200", result.Plans[1].FirstText);
|
||||||
|
Assert.Equal("L-100", result.Plans[1].SecondText);
|
||||||
|
Assert.Empty(result.Plans[1].Reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsSameLineGroupEvenWhenFormattedValuesDiffer()
|
||||||
|
{
|
||||||
|
var service = CreateService(new Dictionary<long, IReadOnlyDictionary<string, string?>> { [10] = Fields("L-100"), [20] = Fields("L-200") });
|
||||||
|
var result = service.Preview(Definition(), [Connector("connector", 10, 20, 7, 7)], Placements("connector"));
|
||||||
|
|
||||||
|
Assert.Equal(string.Empty, result.Plans.Single().Text);
|
||||||
|
Assert.Contains("SAME_LINE_GROUP", result.Plans.Single().Reasons);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PreservesPropertyFailureOnOnlyTheAffectedConnector()
|
||||||
|
{
|
||||||
|
var service = CreateService(new Dictionary<long, IReadOnlyDictionary<string, string?>> { [10] = Fields("L-100"), [30] = Fields("L-300"), [40] = Fields("L-400") });
|
||||||
|
var result = service.Preview(Definition(), [Connector("bad", 10, 20, 1, 2), Connector("good", 30, 40, 3, 4)], Placements("bad", "good"));
|
||||||
|
|
||||||
|
Assert.Contains("LINE_NUMBER_COLUMN_MISSING:EngineeringItems.LineNumber", result.Plans.Single(plan => plan.StableId == "bad").Reasons);
|
||||||
|
Assert.Equal("L-300 / L-400", result.Plans.Single(plan => plan.StableId == "good").Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecordsMissingGeometryWithoutInventingPlacement()
|
||||||
|
{
|
||||||
|
var service = CreateService(new Dictionary<long, IReadOnlyDictionary<string, string?>> { [10] = Fields("L-100"), [20] = Fields("L-200") });
|
||||||
|
var result = service.Preview(Definition(), [Connector("connector", 10, 20, 1, 2)], new Dictionary<string, LineConnectionPlacement>());
|
||||||
|
|
||||||
|
Assert.Contains("CONNECTOR_GEOMETRY_UNAVAILABLE", result.Plans.Single().Reasons);
|
||||||
|
Assert.Equal(string.Empty, result.Plans.Single().Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineConnectionPreviewService CreateService(IReadOnlyDictionary<long, IReadOnlyDictionary<string, string?>> rows) =>
|
||||||
|
new(new LineNumberFieldResolver(new Reader(rows)), new LineNumberAnnotationFormatter(), new LineConnectionBoundaryPlanner());
|
||||||
|
|
||||||
|
private static LineNumberAnnotationDefinition Definition() => new()
|
||||||
|
{
|
||||||
|
Nickname = "-", Tags = [new() { Field = "EngineeringItems.LineNumber", SaveWhenEmpty = true }]
|
||||||
|
};
|
||||||
|
|
||||||
|
private static PlantConnectorRelationshipSnapshot Connector(string id, long first, long second, long firstGroup, long secondGroup) =>
|
||||||
|
new(id, 99, first, second, firstGroup, secondGroup, Array.Empty<string>());
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, LineConnectionPlacement> Placements(params string[] ids) => ids.ToDictionary(id => id, id =>
|
||||||
|
new LineConnectionPlacement(new(1, 2, 0), new(3, 4, 0), new(new(1, 2, 0), new(3, 4, 0)), new(2, 3), 0), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, string?> Fields(string lineNumber) => new Dictionary<string, string?> { ["EngineeringItems.LineNumber"] = lineNumber };
|
||||||
|
|
||||||
|
private sealed class Reader(IReadOnlyDictionary<long, IReadOnlyDictionary<string, string?>> rows) : IDataLinksPropertyReader
|
||||||
|
{
|
||||||
|
public DataLinksPropertyReadResult ReadProperties(long rowId) => rows.TryGetValue(rowId, out var values)
|
||||||
|
? new(true, values)
|
||||||
|
: new(true, new Dictionary<string, string?>());
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineConnectionSettingsLoaderTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void LoadsLegacyLineConnectionAssetContract()
|
||||||
|
{
|
||||||
|
var assets = new LineConnectionSettingsLoader().Load("<Main><LinenumberConnections polylinelayer='Осевые' textlayer='ТЕКСТ' textstyle='GOSTC'/></Main>");
|
||||||
|
Assert.Equal("Осевые", assets.PolylineLayer);
|
||||||
|
Assert.Equal("ТЕКСТ", assets.TextLayer);
|
||||||
|
Assert.Equal("GOSTC", assets.TextStyle);
|
||||||
|
Assert.Equal("BLOCK_LINENUMBER_CONNECTION", assets.BlockName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsMissingProductionSetting()
|
||||||
|
{
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => new LineConnectionSettingsLoader().Load("<Main><LinenumberConnections polylinelayer='Осевые'/></Main>"));
|
||||||
|
Assert.Equal("LINE_CONNECTION_SETTING_MISSING:textlayer", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberAnnotationFormatterTests
|
||||||
|
{
|
||||||
|
private readonly LineNumberAnnotationFormatter _formatter = new();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatsTags_UsingLegacyDividerAndFieldValues()
|
||||||
|
{
|
||||||
|
var result = _formatter.Format(Definition(new LineNumberAnnotationTag { Field = "P3dLineGroup.Service", Divider = "space" }, new LineNumberAnnotationTag { Field = "P3dLineGroup.Number", Divider = "-" }), Fields(("P3dLineGroup.Service", "W"), ("P3dLineGroup.Number", "101")));
|
||||||
|
Assert.Equal(new(true, "W-101"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToUpper_UsesLegacyAliasMap()
|
||||||
|
{
|
||||||
|
var result = _formatter.Format(Definition(new LineNumberAnnotationTag { Field = "EngineeringItems.Service", ProcessingXml = "<processing><function><name>ToUpper</name><attr><value>EngineeringItems.Service</value></attr><mod>interrupt</mod><aliases><key>WATER</key><value>W</value></aliases></function></processing>" }), Fields(("EngineeringItems.Service", "water")));
|
||||||
|
Assert.Equal(new(true, "W"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingField_ReturnsPreciseError()
|
||||||
|
{
|
||||||
|
var result = _formatter.Format(Definition(new LineNumberAnnotationTag { Field = "P3dLineGroup.Number" }), Fields());
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Equal("FIELD_NOT_FOUND:P3dLineGroup.Number", result.ErrorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SettingsLoader_PreservesLegacyTagDefaults()
|
||||||
|
{
|
||||||
|
var settings = new LineNumberAnnotationSettingsLoader().Load("<root><nullOrEmpty>-</nullOrEmpty><annotation><nickname>LN</nickname><set><key>electric_heating</key><value>EH</value></set><tag><devider>space</devider><saver></saver><field>P3dLineGroup.Number</field><ignore></ignore><processing></processing></tag></annotation></root>");
|
||||||
|
Assert.Equal("-", settings.NullOrEmpty);
|
||||||
|
Assert.True(settings.Annotations.Single().Tags.Single().SaveWhenEmpty);
|
||||||
|
Assert.Equal("space", settings.Annotations.Single().Tags.Single().Divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SettingsLoader_RejectsLegacyInvalidAnnotation()
|
||||||
|
{
|
||||||
|
var error = Assert.Throws<InvalidDataException>(() => new LineNumberAnnotationSettingsLoader().Load("<root><nullOrEmpty>-</nullOrEmpty><annotation><nickname>LN</nickname></annotation></root>"));
|
||||||
|
Assert.Equal("SETTINGS_INVALID:21", error.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineNumberAnnotationDefinition Definition(params LineNumberAnnotationTag[] tags) => new() { Nickname = "LN", Variables = new Dictionary<string, string> { ["electric_heating"] = "EH" }, Tags = tags };
|
||||||
|
private static IReadOnlyDictionary<string, string?> Fields(params (string Key, string Value)[] values) => values.ToDictionary(x => x.Key, x => (string?)x.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class LineNumberCommandPlannerTests
|
||||||
|
{
|
||||||
|
[Fact] public void ProducesDeterministicCreateUpdateAndDuplicateDecisions()
|
||||||
|
{
|
||||||
|
var def = new LineNumberAnnotationDefinition { Nickname="LN", Tags=[new() { Field="EngineeringItems.LineNumber" }] };
|
||||||
|
var c = new LineNumberCandidate("b", 1, new(0,0), 0, "PXP", "Standard", new Dictionary<string,string?> { ["EngineeringItems.LineNumber"]="L-2" });
|
||||||
|
var plan = new LineNumberCommandPlanner(new LineNumberAnnotationFormatter()).Build(def, [c, c, c with { StableId="a" }], [new("x", "b", "old")]);
|
||||||
|
Assert.Collection(plan.Operations, x => Assert.Equal(AnnotationOperationKind.Create,x.Kind), x => Assert.Equal(AnnotationOperationKind.Update,x.Kind), x => Assert.Equal(AnnotationOperationKind.Conflict,x.Kind));
|
||||||
|
var apply = LineNumberCommandPlanner.PrepareApply(plan);
|
||||||
|
Assert.Equal(new[] { "a", "b" }, apply.Select(x => x.StableId));
|
||||||
|
Assert.Equal(new[] { 1, 2 }, apply.Select(x => x.MarkerNumber));
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberCommandPreviewServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UsesTheSameResolvedPropertiesForPreviewAndApply()
|
||||||
|
{
|
||||||
|
var reader = new FakeReader(new(true, new Dictionary<string, string?> { ["EngineeringItems.LineNumber"] = "L-42" }));
|
||||||
|
var service = new LineNumberCommandPreviewService(new(new LineNumberAnnotationFormatter()), new(reader));
|
||||||
|
var definition = new LineNumberAnnotationDefinition { Nickname = "LN", Tags = [new() { Field = "EngineeringItems.LineNumber" }] };
|
||||||
|
var candidate = new LineNumberCandidate("component", 42, new(1, 2), 0, "PXP", "Standard", new Dictionary<string, string?>());
|
||||||
|
var result = service.Preview(definition, [candidate], Array.Empty<ExistingLineNumberAnnotation>());
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Equal("L-42", Assert.Single(result.Apply).Text);
|
||||||
|
Assert.Equal(42, reader.LastRowId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropagatesTypedDataLinksFailureWithoutPlanning()
|
||||||
|
{
|
||||||
|
var service = new LineNumberCommandPreviewService(new(new LineNumberAnnotationFormatter()), new(new FakeReader(new(false, new Dictionary<string, string?>(), "DATALINKS_PROPERTY_READ_FAILED"))));
|
||||||
|
var definition = new LineNumberAnnotationDefinition { Nickname = "LN", Tags = [new() { Field = "EngineeringItems.LineNumber" }] };
|
||||||
|
var candidate = new LineNumberCandidate("component", 42, new(), 0, "PXP", "Standard", new Dictionary<string, string?>());
|
||||||
|
Assert.Equal("DATALINKS_PROPERTY_READ_FAILED", service.Preview(definition, [candidate], Array.Empty<ExistingLineNumberAnnotation>()).FailureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeReader(DataLinksPropertyReadResult result) : IDataLinksPropertyReader
|
||||||
|
{
|
||||||
|
public long LastRowId { get; private set; }
|
||||||
|
public DataLinksPropertyReadResult ReadProperties(long rowId) { LastRowId = rowId; return result; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberConnectionPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GroupsConnectedSameLineCandidatesInStableOrder()
|
||||||
|
{
|
||||||
|
var a = Candidate("a", "L-1", new(0, 0, 0), new(1, 0, 0));
|
||||||
|
var b = Candidate("b", "l-1", new(1.05, 0, 0), new(2, 0, 0));
|
||||||
|
var c = Candidate("c", "L-2", new(0, 0, 0), new(1, 0, 0));
|
||||||
|
var result = new LineNumberConnectionPlanner().Build([b, c, a], .1);
|
||||||
|
var group = Assert.Single(result);
|
||||||
|
Assert.Equal("L-1", group.LineNumber);
|
||||||
|
Assert.Equal(new[] { "a", "b" }, group.Members.Select(value => value.StableId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoesNotGroupDisjointOrSingleLineCandidates()
|
||||||
|
{
|
||||||
|
var a = Candidate("a", "L-1", new(0, 0, 0), new(1, 0, 0));
|
||||||
|
var b = Candidate("b", "L-1", new(10, 0, 0), new(11, 0, 0));
|
||||||
|
Assert.Empty(new LineNumberConnectionPlanner().Build([a, b], .1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LineConnectionCandidate Candidate(string id, string line, Point3Dto first, Point3Dto second) => new(id, line, first, second, new(new(Math.Min(first.X, second.X), Math.Min(first.Y, second.Y), Math.Min(first.Z, second.Z)), new(Math.Max(first.X, second.X), Math.Max(first.Y, second.Y), Math.Max(first.Z, second.Z))), new(first.X, first.Y), 0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberEditApplyPlanTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void MutationHash_IsStableAcrossInputOrder()
|
||||||
|
{
|
||||||
|
var first = new LineNumberRowMutation("pipe-a", 1, 10, "A", new(1, 2, 3));
|
||||||
|
var second = new LineNumberRowMutation("pipe-b", 2, 11, "B", null);
|
||||||
|
|
||||||
|
var expected = LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [first, second]);
|
||||||
|
|
||||||
|
Assert.Equal(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [second, first]));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MutationHash_BindsCommandSelectionAndRequestedEdit()
|
||||||
|
{
|
||||||
|
var mutation = new LineNumberRowMutation("pipe-a", 1, 10, "A", null);
|
||||||
|
var expected = LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [mutation]);
|
||||||
|
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ELN", "1A2B", [mutation]));
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "2B3C", [mutation]));
|
||||||
|
Assert.NotEqual(expected, LineNumberEditApplyPlan.ComputeMutationHash("PXP-ESN", "1A2B", [mutation with { Text = "Changed" }]));
|
||||||
|
}
|
||||||
|
}
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberEditCommandRegistrationTests
|
||||||
|
{
|
||||||
|
private static string ProjectRoot
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "PRecAnnot2026.sln"))) directory = directory.Parent;
|
||||||
|
return Path.Combine(directory!.FullName, "src", "PRecAnnot.Plant3D2026");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("PXP-ESN", "LineNumberEditCommands.Renumber")]
|
||||||
|
[InlineData("PXP-ELN", "LineNumberEditCommands.EditText")]
|
||||||
|
[InlineData("PXP-RMP", "LineNumberEditCommands.ResetMarkerPosition")]
|
||||||
|
public void EditCommand_UsesMutationPipeline(string command, string handler)
|
||||||
|
{
|
||||||
|
var entry = File.ReadAllText(Path.Combine(ProjectRoot, "EntryPoint.cs"));
|
||||||
|
var declaration = entry.Split('\n').Single(value => value.Contains($"CommandMethod(\"{command}\")", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(handler, declaration);
|
||||||
|
Assert.DoesNotContain("RunDiagnostic", declaration);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OwnedReader_RequiresPxpLnIdentityForListsAndMarkers()
|
||||||
|
{
|
||||||
|
var reader = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberOwnedAnnotationReader.cs"));
|
||||||
|
Assert.Contains("GetXDataForApplication(ApplicationName)", reader);
|
||||||
|
Assert.Contains("LINE_NUMBER_LIST_NOT_SELECTED", reader);
|
||||||
|
Assert.Contains("LINE_NUMBER_MARKER_OWNERSHIP_MISSING", reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MutationCommands_UseOneTransactionalWriter()
|
||||||
|
{
|
||||||
|
var commands = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs"));
|
||||||
|
Assert.Single(System.Text.RegularExpressions.Regex.Matches(commands, "ApplyEdits\\(document").Cast<System.Text.RegularExpressions.Match>());
|
||||||
|
Assert.Contains("LineNumberEditApplyPermitReader.Validate", commands);
|
||||||
|
Assert.Contains("ExpectedSelectedObjectHandle", File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberApplyPermit.cs")));
|
||||||
|
Assert.Contains("ExpectedMutationHash", File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberApplyPermit.cs")));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateCommand_UsesOwnedSourceRefreshInsteadOfDiagnosticOnlyHandler()
|
||||||
|
{
|
||||||
|
var entry = File.ReadAllText(Path.Combine(ProjectRoot, "EntryPoint.cs"));
|
||||||
|
var declaration = entry.Split('\n').Single(value => value.Contains("CommandMethod(\"PXP-ULN\")", StringComparison.Ordinal));
|
||||||
|
Assert.Contains("LineNumberRefreshCommand.Run", declaration);
|
||||||
|
Assert.DoesNotContain("RunDiagnostic", declaration);
|
||||||
|
var refresh = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberRefreshCommand.cs"));
|
||||||
|
Assert.Contains("if (!IsApply())", refresh);
|
||||||
|
Assert.Contains("LINE_NUMBER_REFRESH_APPLY_REQUIRES_EXACTLY_ONE_OPERATION", refresh);
|
||||||
|
Assert.Contains("LineNumberApplyPermitReader.Validate", refresh);
|
||||||
|
|
||||||
|
var edits = File.ReadAllText(Path.Combine(ProjectRoot, "LineNumberEditCommands.cs"));
|
||||||
|
Assert.Contains("if (!IsApply())", edits);
|
||||||
|
Assert.Contains("Apply permit is required", edits);
|
||||||
|
Assert.Contains("PlantLineNumberCandidateReader", refresh);
|
||||||
|
Assert.Contains("LineNumberCommandPreviewService", refresh);
|
||||||
|
Assert.Contains("ApplyEdits", refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberEditPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RenumbersLegacyListContiguouslyFromPositiveStart()
|
||||||
|
{
|
||||||
|
var rows = new[] { new LineNumberOwnedRow("b", 7, "L-2", new()), new LineNumberOwnedRow("a", 5, "L-1", new()) };
|
||||||
|
var result = LineNumberEditPlanner.Renumber(rows, 10);
|
||||||
|
Assert.Equal(new[] { 10, 11 }, result.Select(value => value.MarkerNumber));
|
||||||
|
Assert.Equal(new[] { 5, 7 }, result.Select(value => value.PreviousMarkerNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact] public void RejectsNonPositiveStart() => Assert.Throws<ArgumentOutOfRangeException>(() => LineNumberEditPlanner.Renumber([new("a", 1, "L", new())], 0));
|
||||||
|
[Fact] public void RejectsDuplicateExistingMarkerNumbers() => Assert.Throws<InvalidOperationException>(() => LineNumberEditPlanner.Renumber([new("a", 1, "L", new()), new("b", 1, "L2", new())], 2));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlansTextEditAndConfiguredMarkerPositionReset()
|
||||||
|
{
|
||||||
|
var row = new LineNumberOwnedRow("a", 3, "old", new(9, 9));
|
||||||
|
Assert.Equal("new", LineNumberEditPlanner.EditText(row, "new").Text);
|
||||||
|
Assert.Equal(new AnnotationPoint(-4, 4.4), LineNumberEditPlanner.ResetMarkerAttributePosition(row, new(-4, 4.4)).MarkerAttributePosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class LineNumberFieldResolverTests
|
||||||
|
{
|
||||||
|
[Fact] public void ResolvesNullAsLegacyEmptyString() => Assert.Equal(string.Empty, new LineNumberFieldResolver(new Reader(true, new Dictionary<string,string?> { ["EngineeringItems.LineNumber"] = null })).Resolve(5, ["EngineeringItems.LineNumber"]).Values["EngineeringItems.LineNumber"]);
|
||||||
|
[Fact] public void RejectsInvalidField() => Assert.Equal("LINE_NUMBER_FIELD_INVALID:LineNumber", new LineNumberFieldResolver(new Reader(true, new Dictionary<string, string?>())).Resolve(5, ["LineNumber"]).FailureCode);
|
||||||
|
[Fact] public void PropagatesAdapterFailure() => Assert.Equal("DATALINKS_PROPERTY_READ_FAILED", new LineNumberFieldResolver(new Reader(false, new Dictionary<string, string?>())).Resolve(5, ["EngineeringItems.LineNumber"]).FailureCode);
|
||||||
|
private sealed record Reader(bool Ok, IReadOnlyDictionary<string,string?> Values) : IDataLinksPropertyReader { public DataLinksPropertyReadResult ReadProperties(long _) => new(Ok, Values, Ok ? null : "DATALINKS_PROPERTY_READ_FAILED"); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class LineNumberListPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void ProducesStableOwnershipAndMarkerOrder()
|
||||||
|
{
|
||||||
|
var second = new LineNumberApplyPreflight("b", 2, "L-2", new(1, 1), 0, "L", "S");
|
||||||
|
var first = new LineNumberApplyPreflight("a", 1, "L-1", new(0, 0), 0, "L", "S");
|
||||||
|
var result = LineNumberListPlanner.Build(new(5, 6), 1.5, [second, first]);
|
||||||
|
Assert.Equal(new[] { 1, 2 }, result.Rows.Select(row => row.MarkerNumber));
|
||||||
|
Assert.Equal(result.StableId, LineNumberListPlanner.Build(new(99, 0), 0, [first, second]).StableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsDuplicateMarkerNumbers()
|
||||||
|
{
|
||||||
|
var one = new LineNumberApplyPreflight("a", 1, "L-1", new(), 0, "L", "S");
|
||||||
|
var duplicate = one with { StableId = "b" };
|
||||||
|
Assert.Throws<ArgumentException>(() => LineNumberListPlanner.Build(new(), 0, [one, duplicate]));
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ManualAnnotationCommandRouterTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(ManualAnnotationKind.LineNumber, "PXP-LN")]
|
||||||
|
[InlineData(ManualAnnotationKind.ValveOrNozzle, "PXP-AAV")]
|
||||||
|
[InlineData(ManualAnnotationKind.FlowDirection, "PXP-OA")]
|
||||||
|
[InlineData(ManualAnnotationKind.InstrumentPosition, "PXP-AAV")]
|
||||||
|
[InlineData(ManualAnnotationKind.SectionElevation, "PXP-IE")]
|
||||||
|
[InlineData(ManualAnnotationKind.InstrumentConnection, "PXP-AAV")]
|
||||||
|
[InlineData(ManualAnnotationKind.AutoCadBlock, "PXP-MAB")]
|
||||||
|
[InlineData(ManualAnnotationKind.PlatformElevation, "PXP-IE")]
|
||||||
|
[InlineData(ManualAnnotationKind.OtherElevation, "PXP-IE")]
|
||||||
|
public void Resolve_routes_every_legacy_palette_choice(ManualAnnotationKind kind, string expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ManualAnnotationCommandRouter.Resolve(kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ManualAnnotationPersistenceTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("AutoCadBlock", ManualDatumMode.Absolute, 0)]
|
||||||
|
[InlineData("SectionElevation", ManualDatumMode.Relative, -12.5)]
|
||||||
|
[InlineData("InstrumentPosition", ManualDatumMode.Relative, 4.25)]
|
||||||
|
public void State_round_trips_without_losing_legacy_datum_values(string kind, ManualDatumMode mode, double offset)
|
||||||
|
{
|
||||||
|
var state = new ManualAnnotationState(kind, mode, offset);
|
||||||
|
var restored = ManualAnnotationState.Deserialize(state.Serialize());
|
||||||
|
Assert.Equal(state, restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Invalid_state_is_not_accepted() => Assert.Null(ManualAnnotationState.Deserialize("bad|payload"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class ManualDatumResolverTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(12.5, ManualDatumMode.Absolute, 99, 12.5)]
|
||||||
|
[InlineData(12.5, ManualDatumMode.Relative, 3.5, 16)]
|
||||||
|
[InlineData(12.5, ManualDatumMode.Relative, -3.5, 9)]
|
||||||
|
[InlineData(0, ManualDatumMode.Relative, 0, 0)]
|
||||||
|
[InlineData(-8, ManualDatumMode.Relative, 2, -6)]
|
||||||
|
public void Resolve_preserves_legacy_absolute_and_relative_datum(double source, ManualDatumMode mode, double offset, double expected)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected, ManualDatumResolver.Resolve(source, mode, offset), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class OrthoAnnotationClassifierTests
|
||||||
|
{
|
||||||
|
private static readonly OrthoAnnotationOptions All = new(true, true, true, true, true);
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Valve", "Annotation", "V-1", OrthoAnnotationRepresentation.MultiLeader, "V-1")]
|
||||||
|
[InlineData("Valve", "Annotation", "PIC$101", OrthoAnnotationRepresentation.BubbleBlock, "PIC\\P101")]
|
||||||
|
[InlineData("InlineInstrument", "Tag", "TI$12", OrthoAnnotationRepresentation.BubbleBlock, "TI\\P12")]
|
||||||
|
[InlineData("Plug", "TieInNumber", "T-4", OrthoAnnotationRepresentation.MultiLeader, "Для подключения\\Pпоз. T-4")]
|
||||||
|
public void PreservesLegacyClassAndDelimiterRules(string type, string field, string value, OrthoAnnotationRepresentation representation, string text)
|
||||||
|
{
|
||||||
|
var result = new OrthoAnnotationClassifier().Classify(Source(type, new Dictionary<string, string?> { [field] = value }), All);
|
||||||
|
Assert.Equal(representation, result.Representation); Assert.Equal(text, result.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RequiresNonstandardSupportTitle() => Assert.Equal(OrthoAnnotationRepresentation.None, new OrthoAnnotationClassifier().Classify(Source("Support", new Dictionary<string, string?> { ["Title"] = "STANDARD", ["Annotation"] = "S1" }), All).Representation);
|
||||||
|
private static PlantObjectSnapshot Source(string type, IReadOnlyDictionary<string, string?> properties) => new() { SnapshotId = "x", ObjectClass = type, Geometry = new(), Properties = properties };
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user