Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@ public void Leader_segment_runs_from_nearest_target_boundary_to_tag_head()
Assert.Equal(1.5, leader.Length, 6);
}

[Fact]
public void Attached_leader_envelope_contains_the_tagged_element_and_hidden_attachment_leg()
{
var envelope = TagPlacementPlanner.BuildConservativeLeaderEnvelope(
Rect(-2, -1, 1, 3),
4,
-3);

Assert.Equal(-2, envelope.MinX, 6);
Assert.Equal(-3, envelope.MinY, 6);
Assert.Equal(4, envelope.MaxX, 6);
Assert.Equal(3, envelope.MaxY, 6);
Assert.True(new TagSegment2(-3, 0, 5, 0).CrossesInterior(envelope));
}

[Fact]
public void Rejects_otherwise_clear_head_when_its_leader_crosses_an_existing_leader()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,28 @@ public void GeometryAwareTagsRemainOnlyWithMeasuredCollisionFreeReadback(
{
Assert.Equal(expected, TagWorkPolicy.KeepCreatedTag(geometryAware, hasMeasurableGeometry, collisionFree));
}

[Theory]
[InlineData(false, 1, false, true, false)]
[InlineData(true, 0, false, true, false)]
[InlineData(true, 2, false, true, false)]
[InlineData(true, 1, false, false, false)]
[InlineData(true, 1, false, true, true)]
[InlineData(true, 1, true, false, true)]
public void MeasuredLeaderPreparationRequiresOneReferenceAndSupportedFreeEnd(
bool leaderApplied,
int taggedReferenceCount,
bool isAlreadyFree,
bool canAssignFree,
bool expected)
{
Assert.Equal(
expected,
TagWorkPolicy.CanPrepareMeasuredLeader(
leaderApplied,
taggedReferenceCount,
isAlreadyFree,
canAssignFree));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,19 @@ public static TagSegment2 BuildLeaderSegment(TagRect2 target, TagRect2 tagBounds
return new TagSegment2(startX, startY, tagBounds.CenterX, tagBounds.CenterY);
}

public static TagRect2 BuildConservativeLeaderEnvelope(TagRect2 target, double anchorX, double anchorY)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (double.IsNaN(anchorX) || double.IsInfinity(anchorX) ||
double.IsNaN(anchorY) || double.IsInfinity(anchorY))
throw new ArgumentOutOfRangeException(nameof(anchorX), "Leader anchor coordinates must be finite.");
return new TagRect2(
Math.Min(target.MinX, anchorX),
Math.Min(target.MinY, anchorY),
Math.Max(target.MaxX, anchorX),
Math.Max(target.MaxY, anchorY));
}

private static bool SameRect(TagRect2 left, TagRect2 right, double tolerance = 1e-9) =>
Math.Abs(left.MinX - right.MinX) <= tolerance &&
Math.Abs(left.MinY - right.MinY) <= tolerance &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,14 @@ public static bool KeepCreatedTag(bool geometryAware, bool hasMeasurableGeometry
{
return !geometryAware || (hasMeasurableGeometry && collisionFree);
}

public static bool CanPrepareMeasuredLeader(
bool leaderApplied,
int taggedReferenceCount,
bool isAlreadyFree,
bool canAssignFree)
{
return leaderApplied && taggedReferenceCount == 1 && (isAlreadyFree || canAssignFree);
}
}
}
152 changes: 132 additions & 20 deletions apps/revit-bridge-addin/RevitBridge.Logic/Handlers/TagElementsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ private sealed class GeometryPlacementOutcome
public bool CollisionFree { get; set; }
public bool LeaderApplied { get; set; }
public string LeaderGeometryStatus { get; set; } = "not_requested";
public string? LeaderEndConditionBefore { get; set; }
public string? LeaderEndConditionAfter { get; set; }
public bool LeaderEndpointAssigned { get; set; }
public string? LeaderGeometryError { get; set; }
public int LeaderCrossingCount { get; set; }
public int LeaderProtectedCrossingCount { get; set; }
public double? LeaderLength { get; set; }
Expand Down Expand Up @@ -666,7 +670,7 @@ private static GeometryObstacleSet CollectGeometryObstacles(Document doc, View v
var centerU = head.DotProduct(right);
var centerV = head.DotProduct(up);
rect = new TagRect2(centerU - tagWidth * 0.5, centerV - tagHeight * 0.5, centerU + tagWidth * 0.5, centerV + tagHeight * 0.5);
if (!AddExistingTagLeaderSegments(view, existingTag, obstacles.LeaderSegments))
if (!AddExistingTagLeaderGeometry(view, existingTag, obstacles))
throw new InvalidOperationException($"Existing leader geometry for tag {ElementIdCompat.GetValue(existingTag.Id)} could not be measured. Geometry-aware placement stopped to avoid an unverified leader crossing.");
}
else
Expand Down Expand Up @@ -809,6 +813,13 @@ private static void ValidateAppliedLeader(
return;
}

if (!TryPrepareCreatedLeaderForReadback(doc, view, tag, plan, outcome))
{
outcome.CollisionFree = false;
outcome.Status = "leader_geometry_unavailable";
return;
}

var actualSegments = ReadIndependentTagLeaderSegments(tag, view);
if (actualSegments.Count == 0)
{
Expand All @@ -829,19 +840,123 @@ private static void ValidateAppliedLeader(
if (!outcome.CollisionFree) outcome.Status = "actual_leader_collision";
}

private static bool AddExistingTagLeaderSegments(
private static bool TryPrepareCreatedLeaderForReadback(
Document doc,
View view,
IndependentTag tag,
ICollection<TagSegment2> destination)
GeometryPlacementPlan plan,
GeometryPlacementOutcome outcome)
{
if (!tag.HasLeader) return true;
var actual = ReadIndependentTagLeaderSegments(tag, view);
if (actual.Count > 0)
try
{
foreach (var segment in actual) destination.Add(segment);
var references = tag.GetTaggedReferences();
var isAlreadyFree = tag.LeaderEndCondition == LeaderEndCondition.Free;
var canAssignFree = isAlreadyFree || tag.CanLeaderEndConditionBeAssigned(LeaderEndCondition.Free);
outcome.LeaderEndConditionBefore = tag.LeaderEndCondition.ToString();

if (!TagWorkPolicy.CanPrepareMeasuredLeader(
outcome.LeaderApplied,
references.Count,
isAlreadyFree,
canAssignFree))
{
outcome.LeaderGeometryStatus = references.Count == 1
? "free_end_not_supported"
: "tagged_reference_count_unsupported";
return false;
}

if (!isAlreadyFree)
{
tag.LeaderEndCondition = LeaderEndCondition.Free;
doc.Regenerate();
}

var plannedSegment = outcome.LeaderSegment;
if (plannedSegment == null)
{
outcome.LeaderGeometryStatus = "planned_endpoint_unavailable";
return false;
}

var reference = references[0];
if (!tag.IsLeaderVisible(reference))
{
outcome.LeaderGeometryStatus = "leader_not_visible";
return false;
}

var endpoint = FromViewCoordinates(
view,
plannedSegment.StartX,
plannedSegment.StartY,
plan.PlaneW);
tag.SetLeaderEnd(reference, endpoint);
doc.Regenerate();

outcome.LeaderEndConditionAfter = tag.LeaderEndCondition.ToString();
outcome.LeaderEndpointAssigned = true;
return true;
}
return false;
catch (Exception ex)
{
outcome.LeaderGeometryStatus = "free_end_assignment_failed";
outcome.LeaderGeometryError = ex.Message;
return false;
}
}

private static bool AddExistingTagLeaderGeometry(
View view,
IndependentTag tag,
GeometryObstacleSet obstacles)
{
if (!tag.HasLeader) return true;
try
{
var references = tag.GetTaggedReferences();
if (references.Count == 0) return false;
var visibleLeaderCount = 0;
var head = tag.TagHeadPosition;
var right = view.RightDirection.Normalize();
var up = view.UpDirection.Normalize();

foreach (var reference in references)
{
if (!tag.IsLeaderVisible(reference)) continue;
visibleLeaderCount++;

if (tag.LeaderEndCondition == LeaderEndCondition.Free)
{
var end = tag.GetLeaderEnd(reference);
var elbow = tag.HasLeaderElbow(reference) ? tag.GetLeaderElbow(reference) : null;
var before = obstacles.LeaderSegments.Count;
AddLeaderPath(end, elbow, head, view, obstacles.LeaderSegments);
if (obstacles.LeaderSegments.Count == before) return false;
continue;
}

var attachedElbow = tag.HasLeaderElbow(reference) ? tag.GetLeaderElbow(reference) : null;
var hiddenLegAnchor = attachedElbow ?? head;
var taggedElement = tag.Document.GetElement(reference.ElementId);
var targetBounds = taggedElement == null ? null : ToViewRect(taggedElement.get_BoundingBox(view), view);
if (targetBounds == null) return false;

obstacles.LeaderProtectedObstacles.Add(
TagPlacementPlanner.BuildConservativeLeaderEnvelope(
targetBounds,
hiddenLegAnchor.DotProduct(right),
hiddenLegAnchor.DotProduct(up)));
if (attachedElbow != null)
AddLeaderSegment(attachedElbow, head, view, obstacles.LeaderSegments);
}

return visibleLeaderCount > 0;
}
catch
{
return false;
}
}

private static List<TagSegment2> ReadIndependentTagLeaderSegments(IndependentTag tag, View view)
Expand All @@ -850,26 +965,19 @@ private static List<TagSegment2> ReadIndependentTagLeaderSegments(IndependentTag
try
{
if (!tag.HasLeader) return segments;
if (tag.LeaderEndCondition != LeaderEndCondition.Free) return segments;
var head = tag.TagHeadPosition;
var references = TryReadTaggedReferences(tag);
var references = tag.GetTaggedReferences();
if (references.Count > 0)
{
foreach (var reference in references)
{
var end = TryInvokeXyzMethod(tag, "GetLeaderEnd", reference);
var elbow = TryInvokeXyzMethod(tag, "GetLeaderElbow", reference);
if (!tag.IsLeaderVisible(reference)) return new List<TagSegment2>();
var end = tag.GetLeaderEnd(reference);
var elbow = tag.HasLeaderElbow(reference) ? tag.GetLeaderElbow(reference) : null;
AddLeaderPath(end, elbow, head, view, segments);
}
}
else
{
AddLeaderPath(
TryReadXyzProperty(tag, "LeaderEnd"),
TryReadXyzProperty(tag, "LeaderElbow"),
head,
view,
segments);
}
}
catch
{
Expand Down Expand Up @@ -1690,6 +1798,10 @@ private static object BuildTagReadback(Document doc, Element tag, Element target
collisionFree = geometryOutcome.CollisionFree,
leaderApplied = geometryOutcome.LeaderApplied,
leaderGeometryStatus = geometryOutcome.LeaderGeometryStatus,
leaderEndConditionBefore = geometryOutcome.LeaderEndConditionBefore,
leaderEndConditionAfter = geometryOutcome.LeaderEndConditionAfter,
leaderEndpointAssigned = geometryOutcome.LeaderEndpointAssigned,
leaderGeometryError = geometryOutcome.LeaderGeometryError,
leaderLength = geometryOutcome.LeaderLength,
leaderCrossingCount = geometryOutcome.LeaderCrossingCount,
leaderProtectedCrossingCount = geometryOutcome.LeaderProtectedCrossingCount,
Expand Down
Loading