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
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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.

Expand All @@ -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**.

Expand Down
43 changes: 43 additions & 0 deletions ResPlan.Library/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,49 @@ public Coordinate GetEntrance()

return null;
}

public List<Geometry> GetVerticalAnchors()
{
var anchors = new List<Geometry>();
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]
Expand Down
123 changes: 123 additions & 0 deletions ResPlan.Tests/AnchorTests.cs
Original file line number Diff line number Diff line change
@@ -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<Geometry> { 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<Geometry> { 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<Geometry> { 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<Geometry> { stairs };
plan.Geometries["elevator"] = new List<Geometry> { 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<Geometry> { 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<Geometry> { 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<Geometry> { corridor1 };

var anchors = plan.GetVerticalAnchors();

Assert.Single(anchors);
Assert.Equal(corridor1, anchors[0]);
}
}
}
Loading