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
2 changes: 1 addition & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Handles the loading of plan data.
* **maxItems**: Optional limit on the number of plans to load.
* **logger**: Optional callback to receive real-time progress updates (e.g., dependency installation logs, download progress).
* **constraints**: Optional `PlanGenerationConstraints` to filter or orient plans.
* **BoundingPolygon**: If set, only plans whose bounds are completely contained within this polygon are returned.
* **BoundingPolygon**: If set, only plans whose geometry is completely contained within this polygon are returned. The check first validates the plan's Axis-Aligned Bounding Box (AABB) for performance, and if that fails, performs a precise geometry-by-geometry containment check to support rotated plans.
* **FrontDoorFacing**: If set, plans are rotated so their front door (vector from center to door) aligns with this vector.
* **GarageFacing**: Currently unsupported; will log a warning if set.
* **Returns**: A list of `Plan` objects.
Expand Down
3 changes: 3 additions & 0 deletions ResPlan.Library/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("ResPlan.Tests")]
47 changes: 37 additions & 10 deletions ResPlan.Library/PlanLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
{
private static readonly WKTReader _wktReader = new WKTReader();

public static async Task<List<Plan>> LoadPlansAsync(string jsonPath = null, string pklPathOverride = null, int? maxItems = null, Action<string> logger = null, PlanGenerationConstraints constraints = null)

Check warning on line 19 in ResPlan.Library/PlanLoader.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 19 in ResPlan.Library/PlanLoader.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 19 in ResPlan.Library/PlanLoader.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.

Check warning on line 19 in ResPlan.Library/PlanLoader.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
if (!string.IsNullOrEmpty(jsonPath) && File.Exists(jsonPath))
{
Expand Down Expand Up @@ -175,24 +175,51 @@
}

// Apply Bounding Constraint
if (constraints.BoundingPolygon != null)
if (!IsPlanCompatible(plan, constraints.BoundingPolygon))
{
// Check if plan bounds are within polygon
// Since plan.Bounds is an Envelope, we convert to geometry
var geometryFactory = new GeometryFactory();
var planGeom = geometryFactory.ToGeometry(plan.Bounds);

if (!constraints.BoundingPolygon.Contains(planGeom))
{
continue; // Filter out
}
continue;
}

result.Add(plan);
}
return result;
}

internal static bool IsPlanCompatible(Plan plan, Polygon boundingPolygon)
{
if (boundingPolygon == null) return true;

// Check if plan bounds are within polygon
// Since plan.Bounds is an Envelope, we convert to geometry
var geometryFactory = new GeometryFactory();
var planGeom = geometryFactory.ToGeometry(plan.Bounds);

// Fast check: if AABB is contained, we are good.
if (boundingPolygon.Contains(planGeom))
{
return true;
}

// Fallback: Precise check using actual geometries
// If the AABB doesn't fit (e.g. rotated plan), check if all individual geometries fit
bool allInside = true;
foreach (var kvp in plan.Geometries)
{
foreach (var geom in kvp.Value)
{
// Use Covers instead of Contains to allow boundary contact
if (!boundingPolygon.Covers(geom))
{
allInside = false;
break;
}
}
if (!allInside) break;
}

return allInside;
}

private static Plan ConvertDataToPlan(ResPlanData data)
{
var plan = new Plan
Expand Down
64 changes: 64 additions & 0 deletions ResPlan.Tests/ConstraintTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using NetTopologySuite.Geometries;
using ResPlan.Library;
using Xunit;

namespace ResPlan.Tests
{
public class ConstraintTests
{
[Fact]
public void IsPlanCompatible_RotatedPlanInRotatedLot_ReturnsTrue()
{
// Arrange
var geometryFactory = new GeometryFactory();

// Create a Plan with a 10x10 square at (0,0)
// Centered at 5,5
var coords = new Coordinate[]
{
new Coordinate(0, 0),
new Coordinate(10, 0),
new Coordinate(10, 10),
new Coordinate(0, 10),
new Coordinate(0, 0)
};
var shell = geometryFactory.CreateLinearRing(coords);
var poly = geometryFactory.CreatePolygon(shell);

var plan = new Plan
{
Id = 1,
Geometries = new Dictionary<string, List<Geometry>>
{
{ "room", new List<Geometry> { poly } }
}
};
// Initial bounds (0,0) to (10,10)
plan.Bounds = new Envelope(0, 10, 0, 10);

// Rotate the plan by 45 degrees around its center (5,5)
plan.Rotate(System.Math.PI / 4.0, new Coordinate(5, 5));

// Now create a bounding polygon that matches the rotated plan geometry exactly
// We can cheat by using the plan's geometry itself (the rotated poly)
// But to be rigorous, let's use the rotated poly from the plan
var rotatedGeom = plan.Geometries["room"][0];
Assert.True(rotatedGeom is Polygon);
var boundingPolygon = (Polygon)rotatedGeom.Copy();

// The bounding polygon now matches the plan's geometry.
// However, plan.Bounds (AABB) will be larger than the rotated geometry.
// AABB of a 45-degree rotated 10x10 square is roughly 14.14x14.14.
// This AABB will NOT be contained in the 10x10 rotated square (which is the boundingPolygon).

// Act
bool isCompatible = PlanLoader.IsPlanCompatible(plan, boundingPolygon);

// Assert
// This should be true if we are checking geometry containment correctly.
// It will be false if we are only checking AABB containment.
Assert.True(isCompatible, "Plan should be compatible with a bounding polygon that exactly matches its geometry.");
}
}
}
Loading