feat: port p-rec-annot elevation subsystem
This commit is contained in:
@@ -68,6 +68,8 @@ public sealed record ElevationCandidate(string StableId, double ModelElevation,
|
|||||||
public sealed record ExistingElevationAnnotation(string StableId, string SourceStableId, string Text, AnnotationPoint Position);
|
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 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 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 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);
|
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*
|
// A legacy PXP-ALC annotation belongs to a connector between two *different*
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@ public static class LegacyElevationFormatter
|
|||||||
public static int RoundModelElevation(double modelZ, double datumOffset)
|
public static int RoundModelElevation(double modelZ, double datumOffset)
|
||||||
{
|
{
|
||||||
// InsertElevation.BlockJig divides the model-space ordinate by five,
|
// InsertElevation.BlockJig divides the model-space ordinate by five,
|
||||||
// rounds that quotient away from zero, then formats the resulting
|
// rounds that quotient away from zero and restores the five-unit grid
|
||||||
// integer as a signed metres/millimetres value. It does not multiply
|
// before formatting the signed metres/millimetres value.
|
||||||
// the rounded quotient back by five.
|
var grid = Math.Round((modelZ + datumOffset) / 5d, MidpointRounding.AwayFromZero) * 5d;
|
||||||
return checked((int)Math.Round((modelZ + datumOffset) / 5d, MidpointRounding.AwayFromZero));
|
return checked((int)Math.Round(grid, MidpointRounding.AwayFromZero));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string FormatMillimetres(int millimetres)
|
public static string FormatMillimetres(int millimetres)
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,8 @@ public sealed class EntryPoint : IExtensionApplication
|
|||||||
[CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText();
|
[CommandMethod("PXP-ELN")] public void EditLineNumbersCommand() => LineNumberEditCommands.EditText();
|
||||||
[CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition();
|
[CommandMethod("PXP-RMP")] public void ResetMarkerPositionCommand() => LineNumberEditCommands.ResetMarkerPosition();
|
||||||
[CommandMethod("PXP-ALC")] public void AutoLineNumberConnectionsCommand() => LineConnectionCommand.Run();
|
[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("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
|
[CommandMethod("PHP_REC_AUTOMATION_START", CommandFlags.Session)]
|
||||||
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
|
public void AutomationStartCommand() => FirstLimitedApplyAutomationBootstrap.Arm();
|
||||||
|
|||||||
@@ -511,7 +511,9 @@ internal sealed class PlantViewportMembershipReader
|
|||||||
var direction = modelDirection?.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),
|
||||||
|
Elevation = modelPoint.Z,
|
||||||
Rotation = 0,
|
Rotation = 0,
|
||||||
PaperDirection = direction is null ? null : new Vector2Dto(direction.Value.X, direction.Value.Y)
|
PaperDirection = direction is null ? null : new Vector2Dto(direction.Value.X, direction.Value.Y)
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -10,7 +10,7 @@ public sealed class ElevationAnnotationPlannerTests
|
|||||||
{
|
{
|
||||||
var a = new ElevationCandidate("a", 5, 0, new(0, 0), 0, "PXP", "GOSTC");
|
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 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,001", new(0, 0)), new("old-b1", "b", "+0,002", new()), new("old-b2", "b", "+0,002", new())]);
|
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,
|
Assert.Collection(plan.Operations,
|
||||||
value => Assert.Equal(AnnotationOperationKind.Skip, value.Kind),
|
value => Assert.Equal(AnnotationOperationKind.Skip, value.Kind),
|
||||||
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind),
|
value => Assert.Equal(AnnotationOperationKind.Conflict, value.Kind),
|
||||||
@@ -23,6 +23,6 @@ public sealed class ElevationAnnotationPlannerTests
|
|||||||
{
|
{
|
||||||
var plan = new ElevationAnnotationPlanner().Build([new ElevationCandidate("a", 13, 0, new(), 0, "PXP", "GOSTC")], [new("old", "a", "+0,010", new())]);
|
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(AnnotationOperationKind.Update, plan.Operations.Single().Kind);
|
||||||
Assert.Equal("+0,003", plan.Operations.Single().Text);
|
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));
|
||||||
|
}
|
||||||
@@ -7,10 +7,10 @@ public sealed class LegacyElevationFormatterTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0, 0, "0,000")]
|
[InlineData(0, 0, "0,000")]
|
||||||
[InlineData(2.49, 0, "0,000")]
|
[InlineData(2.49, 0, "0,000")]
|
||||||
[InlineData(2.5, 0, "+0,001")]
|
[InlineData(2.5, 0, "+0,005")]
|
||||||
[InlineData(-2.5, 0, "-0,001")]
|
[InlineData(-2.5, 0, "-0,005")]
|
||||||
[InlineData(1233, 2, "+0,247")]
|
[InlineData(1233, 2, "+1,235")]
|
||||||
[InlineData(-1233, -2, "-0,247")]
|
[InlineData(-1233, -2, "-1,235")]
|
||||||
public void PreservesFiveMillimetreAwayFromZeroLegacyRule(double modelZ, double datum, string expected) =>
|
public void PreservesFiveMillimetreAwayFromZeroLegacyRule(double modelZ, double datum, string expected) =>
|
||||||
Assert.Equal(expected, LegacyElevationFormatter.Calculate(modelZ, datum));
|
Assert.Equal(expected, LegacyElevationFormatter.Calculate(modelZ, datum));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user