diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs index 84c508e..62e7bab 100644 --- a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Abstractions/Contracts.cs @@ -122,6 +122,7 @@ 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); diff --git a/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ConnectorViewportEligibilityEvaluator.cs b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ConnectorViewportEligibilityEvaluator.cs new file mode 100644 index 0000000..d1eb8fc --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/src/PRecAnnot.Core/ConnectorViewportEligibilityEvaluator.cs @@ -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; + } +} diff --git a/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ConnectorViewportEligibilityEvaluatorTests.cs b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ConnectorViewportEligibilityEvaluatorTests.cs new file mode 100644 index 0000000..fd792aa --- /dev/null +++ b/Plant3D2026/P_Rec_Annot/tests/PRecAnnot.Core.Tests/ConnectorViewportEligibilityEvaluatorTests.cs @@ -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); +}