feat: plan p-rec-annot connector annotations

This commit is contained in:
codex
2026-08-07 09:45:07 +03:00
parent 20ac5e4808
commit 53d6378cfa
4 changed files with 173 additions and 1 deletions
@@ -80,8 +80,23 @@ public sealed record LineConnectionBoundaryCandidate(
Point3Dto SecondEndpoint,
Bounds3Dto Bounds,
AnnotationPoint Position,
double Rotation);
double Rotation,
string? FirstLineGroupStableId = null,
string? SecondLineGroupStableId = null);
public sealed record LineConnectionBoundaryPlan(string StableId, string Text, AnnotationPoint Position, double Rotation, IReadOnlyList<string> Reasons);
// 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,
@@ -20,6 +20,16 @@ public sealed class LineConnectionBoundaryPlanner
}
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"]));
continue;
}
if (first.Length == 0 || second.Length == 0)
{
result.Add(new(candidate.StableId, string.Empty, candidate.Position, candidate.Rotation, ["LINE_GROUP_NOT_RESOLVED"]));
@@ -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");
}
}