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 |
@@ -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/
|
||||||
|
|||||||
@@ -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,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();
|
||||||
|
|
||||||
@@ -62,6 +92,55 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
|
[CommandMethod("PHP_REC_AUTOMATION_FINALIZE", CommandFlags.Session)]
|
||||||
public void AutomationFinalizeCommand() => ScriptDrivenFirstApplyAutomation.FinalizeAutomation();
|
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)
|
||||||
{
|
{
|
||||||
var operationId = Guid.NewGuid().ToString("N");
|
var operationId = Guid.NewGuid().ToString("N");
|
||||||
@@ -165,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;
|
||||||
@@ -173,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;
|
||||||
@@ -292,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; }
|
||||||
@@ -85,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; }
|
||||||
|
|||||||
@@ -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,7 +477,7 @@ 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";
|
var stage = "GetDatabase";
|
||||||
@@ -492,14 +493,29 @@ internal sealed class PlantViewportMembershipReader
|
|||||||
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
using var transaction = sourceDatabase.TransactionManager.StartOpenCloseTransaction();
|
||||||
stage = "MakeAcDbObjectId";
|
stage = "MakeAcDbObjectId";
|
||||||
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
var objectId = dataLinks.MakeAcDbObjectId(sourceId.PrjObjectId, sourceDatabase);
|
||||||
stage = "OpenPipe";
|
stage = "OpenPlantEntity";
|
||||||
if (transaction.GetObject(objectId, OpenMode.ForRead) is not Pipe pipe) return false;
|
var entity = transaction.GetObject(objectId, OpenMode.ForRead);
|
||||||
stage = "TransformStartPoint";
|
Autodesk.AutoCAD.Geometry.Point3d modelPoint;
|
||||||
var point = pipe.StartPoint.TransformBy(sourceId.Xform);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-20
@@ -14,7 +14,7 @@ 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, string ExpectedEntryAssemblySha256, bool Diagnostic);
|
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, 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 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;
|
||||||
@@ -43,8 +43,8 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
if (terminal) return;
|
if (terminal) return;
|
||||||
config = ReadConfig();
|
config = ReadConfig();
|
||||||
RequireGate(config);
|
RequireGate(config);
|
||||||
discovery = DiscoverPxpOa(config);
|
discovery = DiscoverTargetCommand(config);
|
||||||
RequireDiscovery(discovery);
|
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;
|
||||||
@@ -93,6 +93,8 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
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, PermitAccepted = permitAccepted, 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)
|
internal static void RecordApplyOutcome(PipeAnnotationWriteOutcome outcome)
|
||||||
{
|
{
|
||||||
@@ -126,9 +128,9 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
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))) throw new InvalidOperationException("PXP_OA_PLAN_RESULT_NOT_CAPTURED");
|
if (runs.Any(x => string.IsNullOrWhiteSpace(x.PlanId))) throw new InvalidOperationException("AUTOMATION_TARGET_PLAN_RESULT_NOT_CAPTURED");
|
||||||
if (!string.Equals(runs[0].PlanId, runs[1].PlanId, StringComparison.Ordinal)) throw new InvalidOperationException("AUTOMATION_PLAN_IDS_DIFFER");
|
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);
|
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 ((!apply && runs.Any(x => x.Create != 1)) || (apply && (runs[0].Create != 1 || runs[1].Create != 0))) throw new InvalidOperationException("AUTOMATION_CREATE_COUNTS_INVALID");
|
||||||
@@ -159,7 +161,7 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
correlations.Add(correlation);
|
correlations.Add(correlation);
|
||||||
if (correlation.RejectionReason is not null) return;
|
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));
|
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("PxpOaWillStart:" + runs.Count);
|
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);
|
||||||
@@ -177,14 +179,14 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
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 AutomationCommandCorrelationEvidence Correlate(string kind, object sender, string rawName, bool sequenceAccepted)
|
private static AutomationCommandCorrelationEvidence Correlate(string kind, object sender, string rawName, bool sequenceAccepted)
|
||||||
{
|
{
|
||||||
var normalized = rawName.Trim();
|
var normalized = rawName.Trim();
|
||||||
var command = string.Equals(normalized, "PXP-OA", StringComparison.OrdinalIgnoreCase);
|
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 state = prepared && !terminal && config is not null && fixture is not null && database is not null;
|
||||||
var senderDocument = sender as Document;
|
var senderDocument = sender as Document;
|
||||||
var senderPath = senderDocument?.Name;
|
var senderPath = senderDocument?.Name;
|
||||||
@@ -218,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"), root.GetProperty("entryAssemblySha256").GetString() ?? throw new InvalidOperationException("AUTOMATION_ENTRY_ASSEMBLY_HASH_REQUIRED"), root.TryGetProperty("diagnostic", out var diagnostic) && diagnostic.ValueKind == JsonValueKind.True);
|
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)
|
||||||
{
|
{
|
||||||
@@ -231,18 +235,18 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
var first = runs.ElementAtOrDefault(0); var second = runs.ElementAtOrDefault(1);
|
||||||
var apply = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
|
var apply = string.Equals(config.Phase, "Apply", StringComparison.OrdinalIgnoreCase);
|
||||||
var afterUndo = apply && terminal ? Counts(database!) : null;
|
var afterUndo = apply && terminal ? Counts(database!) : null;
|
||||||
var evidence = new FirstApplyAutomationEvidence { Phase = config.Phase, 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 => "PXP-OA").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 };
|
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 DiscoverPxpOa(Config value)
|
private static PxpOaDiscoveryEvidence DiscoverTargetCommand(Config value)
|
||||||
{
|
{
|
||||||
var entry = typeof(EntryPoint).Assembly;
|
var entry = typeof(EntryPoint).Assembly;
|
||||||
var loaded = AppDomain.CurrentDomain.GetAssemblies().Any(x => ReferenceEquals(x, entry));
|
var loaded = AppDomain.CurrentDomain.GetAssemblies().Any(x => ReferenceEquals(x, entry));
|
||||||
var location = entry.Location;
|
var location = entry.Location;
|
||||||
var hash = File.Exists(location) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(location))) : null;
|
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, "PXP-OA", StringComparison.OrdinalIgnoreCase)).ToArray();
|
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 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))));
|
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);
|
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);
|
||||||
@@ -260,13 +264,13 @@ internal static class ScriptDrivenFirstApplyAutomation
|
|||||||
yield return new(name, group, flags, type, assembly);
|
yield return new(name, group, flags, type, assembly);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static void RequireDiscovery(PxpOaDiscoveryEvidence value)
|
private static void RequireDiscovery(PxpOaDiscoveryEvidence value, string targetCommand)
|
||||||
{
|
{
|
||||||
if (!value.AssemblyLoaded) throw new InvalidOperationException("PXP_OA_ASSEMBLY_NOT_LOADED");
|
if (!value.AssemblyLoaded) throw new InvalidOperationException("AUTOMATION_TARGET_ASSEMBLY_NOT_LOADED");
|
||||||
if (!value.BundleHashMatched) throw new InvalidOperationException("PXP_OA_BUNDLE_HASH_MISMATCH");
|
if (!value.BundleHashMatched) throw new InvalidOperationException("AUTOMATION_TARGET_BUNDLE_HASH_MISMATCH");
|
||||||
if (!value.CommandClassIncluded) throw new InvalidOperationException("PXP_OA_COMMANDCLASS_NOT_REGISTERED");
|
if (!value.CommandClassIncluded) throw new InvalidOperationException("AUTOMATION_TARGET_COMMANDCLASS_NOT_REGISTERED");
|
||||||
if (value.DeclarationCount > 1) throw new InvalidOperationException("PXP_OA_COMMAND_COLLISION");
|
if (value.DeclarationCount > 1) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_COLLISION");
|
||||||
if (value.DeclarationCount == 0 || !value.Found) throw new InvalidOperationException("PXP_OA_COMMAND_NOT_FOUND");
|
if (value.DeclarationCount == 0 || !value.Found) throw new InvalidOperationException("AUTOMATION_TARGET_COMMAND_NOT_FOUND");
|
||||||
if (!string.Equals(value.GlobalName, "PXP-OA", StringComparison.Ordinal)) throw new InvalidOperationException("PXP_OA_GLOBAL_NAME_MISMATCH");
|
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 };
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class OrthoAnnotationSettingsLoaderTests
|
||||||
|
{
|
||||||
|
[Fact] public void ReadsLegacyAnnotationAssets() { var value = new OrthoAnnotationSettingsLoader().Load("<Main><Annotations layer='A' textstyle='T' multileaderstyle='M'/></Main>"); Assert.Equal("A", value.Layer); Assert.Equal("M", value.MultiLeaderStyle); }
|
||||||
|
[Fact] public void RejectsMissingStyle() => Assert.Throws<InvalidDataException>(() => new OrthoAnnotationSettingsLoader().Load("<Main><Annotations layer='A' textstyle='T'/></Main>"));
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class OrthoBatchPlannerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void KeepsSameNameDrawingsDistinctByRelativePath()
|
||||||
|
{
|
||||||
|
var plan = new OrthoBatchPlanner().Build([
|
||||||
|
new("g2", "A.dwg", "Ortho\\Area2\\A.dwg", ["V2"]),
|
||||||
|
new("g1", "A.dwg", "Ortho\\Area1\\A.dwg", ["V1"])],
|
||||||
|
[new("Ortho/Area2/A.dwg", ["V2"]), new("Ortho\\Area1\\A.dwg", ["V1"])]);
|
||||||
|
Assert.Collection(plan.Operations, x => Assert.Equal("g1", x.DrawingGuid), x => Assert.Equal("g2", x.DrawingGuid));
|
||||||
|
Assert.Empty(plan.Diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsolatesMissingDrawingAndViewFailures()
|
||||||
|
{
|
||||||
|
var plan = new OrthoBatchPlanner().Build([new("g", "A", "Ortho\\A.dwg", ["V1"])], [new("Ortho\\A.dwg", ["missing", "V1"]), new("Ortho\\B.dwg", ["V2"])]);
|
||||||
|
Assert.Single(plan.Operations); Assert.Equal(2, plan.Diagnostics.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectsEscapingAndAbsolutePaths()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => OrthoBatchPlanner.Normalize("..\\x.dwg"));
|
||||||
|
Assert.Throws<ArgumentException>(() => OrthoBatchPlanner.Normalize("C:\\x.dwg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
using PRecAnnot.Core;
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
public sealed class OrthoLayerSettingsLoaderTests
|
||||||
|
{
|
||||||
|
[Fact] public void ParsesStatusLayerAndRgbRules()
|
||||||
|
{
|
||||||
|
var rules = new OrthoLayerSettingsLoader().Load("<Main><LayersSetup><LayerSetup filtertype='Status' filtervalue='Reserved' lineweight='30' linetype='Continuous' color='1'/><LayerSetup filtertype='Layer' filtervalue='Axis' lineweight='bad' linetype='' color='1,2,3'/></LayersSetup></Main>");
|
||||||
|
Assert.True(rules[0].MatchesStatus("reserved")); Assert.Equal(30, rules[0].LineWeight); Assert.Equal((byte)2, rules[1].Green); Assert.True(rules[1].MatchesLayer("V1", "v1-axis"));
|
||||||
|
}
|
||||||
|
[Fact] public void RejectsOldOrEmptyShape() => Assert.Throws<InvalidDataException>(() => new OrthoLayerSettingsLoader().Load("<Main><LayersSetup/></Main>"));
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
using PRecAnnot.Abstractions;
|
||||||
|
|
||||||
|
namespace PRecAnnot.Core.Tests;
|
||||||
|
|
||||||
|
public sealed class PlantConnectorRelationshipContractTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Snapshot_SeparatesConnectorPartsAndLineGroups()
|
||||||
|
{
|
||||||
|
var value = new PlantConnectorRelationshipSnapshot("pnp-connector:7", 7, 10, 11, 21, 22, []);
|
||||||
|
Assert.NotEqual(value.FirstPartRowId, value.SecondPartRowId);
|
||||||
|
Assert.NotEqual(value.FirstLineGroupRowId, value.SecondLineGroupRowId);
|
||||||
|
Assert.Equal("pnp-connector:7", value.StableId);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user