diff --git a/README.md b/README.md index bd7b75f..1dbbd14 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,26 @@ var constrainedPlans = await PlanLoader.LoadPlansAsync( ); ``` -### 3. Generating Connectivity Graphs +### 3. Vertical Anchors (Semantic Data) + +Plans now expose semantic data to assist with vertical stacking and alignment in 3D generation contexts. The `GetVerticalAnchors()` method returns geometries that serve as optimal connection points between floors (e.g., stairs, elevators, or central corridors). + +```csharp +using ResPlan.Library; + +foreach (var plan in plans) +{ + var anchors = plan.GetVerticalAnchors(); + Console.WriteLine($"Plan {plan.Id} has {anchors.Count} vertical anchors."); + + foreach (var anchor in anchors) + { + Console.WriteLine($" - Anchor at {anchor.Centroid}"); + } +} +``` + +### 4. Generating Connectivity Graphs Once a `Plan` is loaded, you can generate a graph representing the connectivity between rooms, doors, and windows using `GraphGenerator`. @@ -100,7 +119,7 @@ foreach (var plan in plans) } ``` -### 4. Rendering Floorplans +### 5. Rendering Floorplans You can visualize the floorplan using `PlanRenderer`. This uses SkiaSharp to produce an image file. @@ -118,7 +137,7 @@ foreach (var plan in plans) } ``` -### 5. Serialization +### 6. Serialization To support binary serialization and handle special floating-point values (like `NaN` or `Infinity`) which are not standard in JSON, the library provides a helper class `PlanSerializer` using **MessagePack**. diff --git a/ResPlan.Library/Models.cs b/ResPlan.Library/Models.cs index 58e770a..9a0f843 100644 --- a/ResPlan.Library/Models.cs +++ b/ResPlan.Library/Models.cs @@ -75,6 +75,49 @@ public Coordinate GetEntrance() return null; } + + public List GetVerticalAnchors() + { + var anchors = new List(); + var keys = new[] { "stairs", "elevator", "foyer" }; + + foreach (var key in keys) + { + if (Geometries.TryGetValue(key, out var geoms) && geoms != null) + { + anchors.AddRange(geoms); + } + } + + if (anchors.Any()) + { + return anchors; + } + + if (Geometries.TryGetValue("corridor", out var corridors) && corridors != null && corridors.Any()) + { + if (Bounds != null) + { + var center = Bounds.Centre; + // Use the factory of the first corridor geometry to ensure compatibility + var factory = corridors.First().Factory; + var centerPoint = factory.CreatePoint(center); + + var closest = corridors.OrderBy(g => g.Distance(centerPoint)).FirstOrDefault(); + if (closest != null) + { + anchors.Add(closest); + } + } + else + { + // If no bounds, just take the first corridor as fallback + anchors.Add(corridors.First()); + } + } + + return anchors; + } } [MessagePackObject] diff --git a/ResPlan.Tests/AnchorTests.cs b/ResPlan.Tests/AnchorTests.cs new file mode 100644 index 0000000..d70fbad --- /dev/null +++ b/ResPlan.Tests/AnchorTests.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Linq; +using NetTopologySuite.Geometries; +using ResPlan.Library; +using Xunit; + +namespace ResPlan.Tests +{ + public class AnchorTests + { + private GeometryFactory _factory = new GeometryFactory(); + + private Polygon CreateBox(double x, double y, double size) + { + return _factory.CreatePolygon(new[] + { + new Coordinate(x, y), + new Coordinate(x + size, y), + new Coordinate(x + size, y + size), + new Coordinate(x, y + size), + new Coordinate(x, y) + }); + } + + [Fact] + public void GetVerticalAnchors_ReturnsStairs_WhenPresent() + { + var plan = new Plan(); + var stairs = CreateBox(0, 0, 10); + plan.Geometries["stairs"] = new List { stairs }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Single(anchors); + Assert.Equal(stairs, anchors[0]); + } + + [Fact] + public void GetVerticalAnchors_ReturnsElevator_WhenPresent() + { + var plan = new Plan(); + var elevator = CreateBox(10, 10, 5); + plan.Geometries["elevator"] = new List { elevator }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Single(anchors); + Assert.Equal(elevator, anchors[0]); + } + + [Fact] + public void GetVerticalAnchors_ReturnsFoyer_WhenPresent() + { + var plan = new Plan(); + var foyer = CreateBox(20, 20, 15); + plan.Geometries["foyer"] = new List { foyer }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Single(anchors); + Assert.Equal(foyer, anchors[0]); + } + + [Fact] + public void GetVerticalAnchors_ReturnsAllPriorityAnchors() + { + var plan = new Plan(); + var stairs = CreateBox(0, 0, 10); + var elevator = CreateBox(20, 0, 5); + plan.Geometries["stairs"] = new List { stairs }; + plan.Geometries["elevator"] = new List { elevator }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Equal(2, anchors.Count); + Assert.Contains(stairs, anchors); + Assert.Contains(elevator, anchors); + } + + [Fact] + public void GetVerticalAnchors_ReturnsCentralCorridor_WhenNoPriorityAnchors() + { + var plan = new Plan(); + // Bounds covering 0,0 to 100,100. Center is 50,50. + plan.Bounds = new Envelope(0, 100, 0, 100); + + var corridorFar = CreateBox(0, 0, 10); // Center at 5,5. Dist to 50,50 ~ 63.6 + var corridorNear = CreateBox(45, 45, 10); // Center at 50,50. Dist to 50,50 = 0 + + plan.Geometries["corridor"] = new List { corridorFar, corridorNear }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Single(anchors); + Assert.Equal(corridorNear, anchors[0]); + } + + [Fact] + public void GetVerticalAnchors_ReturnsEmpty_WhenNoAnchorsOrCorridors() + { + var plan = new Plan(); + plan.Geometries["bedroom"] = new List { CreateBox(0, 0, 10) }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Empty(anchors); + } + + [Fact] + public void GetVerticalAnchors_FallbackCorridor_WhenBoundsNull() + { + var plan = new Plan(); + // Bounds is null + var corridor1 = CreateBox(0, 0, 10); + plan.Geometries["corridor"] = new List { corridor1 }; + + var anchors = plan.GetVerticalAnchors(); + + Assert.Single(anchors); + Assert.Equal(corridor1, anchors[0]); + } + } +}