diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c4ed46a..4a9da88 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -11,7 +11,6 @@ graph TD subgraph DotNet ["ResPlan.Library (.NET)"] PL[PlanLoader] GG[GraphGenerator] - BG[BuildingGenerator] PR[PlanRenderer] Models[Data Models] PEM[PythonEnvManager] @@ -33,7 +32,6 @@ graph TD Wrapper -->|Streams JSON| PL PL -->|Deserializes to| Models GG -->|Consumes| Models - BG -->|Consumes & Produces| Models PR -->|Consumes| Models PL -.->|Manages| PEM PEM -.->|Configures| Venv @@ -86,12 +84,11 @@ classDiagram +Dictionary Geometries +Envelope Bounds +Graph ReferenceGraph + +Coordinate GetEntrance() } class PlanGenerationConstraints { +Polygon BoundingPolygon - +Vector2? FrontDoorFacing - +Vector2? GarageFacing } class Graph { @@ -121,25 +118,17 @@ classDiagram Graph "1" *-- "*" Node Graph "1" *-- "*" Edge Node "1" o-- "1" Geometry - Building "1" *-- "*" BuildingFloor - BuildingFloor "1" o-- "1" Plan ``` ### Type Definitions -* **`Building`**: Represents a multi-story structure. - * `Floors`: A list of `BuildingFloor` objects sorted by floor number. -* **`BuildingFloor`**: A single floor in a building. - * `Plan`: The floorplan associated with this level. - * `AdditionalGeometries`: Generated common areas (e.g., stairs, corridors) not present in the original plan. * **`Plan`**: The root object representing a single floorplan. * `Geometries`: A dictionary mapping category names (e.g., "living", "wall", "door") to lists of NetTopologySuite `Geometry` objects. * `Bounds`: The spatial bounding box of the plan. * `ReferenceGraph`: The ground-truth graph provided by the dataset (if available). -* **`PlanGenerationConstraints`**: Configuration for filtering and orienting loaded plans. + * `GetEntrance()`: Returns the coordinate of the plan's entrance (front door), or null if not found. +* **`PlanGenerationConstraints`**: Configuration for filtering loaded plans. * `BoundingPolygon`: A polygon that must fully contain the plan's bounds. - * `FrontDoorFacing`: A target vector for the front door's orientation relative to the plan's center. - * `GarageFacing`: *Unsupported in current dataset*. * **`Graph`**: Represents the connectivity graph of the floorplan. * Generated by `GraphGenerator` or loaded as `ReferenceGraph`. * **`Node`**: A node in the graph, typically representing a room or a portal (door/window). @@ -159,10 +148,8 @@ Handles the loading of plan data. * **pklPathOverride**: Optional path to a specific `.pkl` file. If null, defaults to the managed dataset path. * **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 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. + * **constraints**: Optional `PlanGenerationConstraints` to filter plans. + * **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. * **Returns**: A list of `Plan` objects. ### `ResPlan.Library.Data.PlanSerializer` @@ -190,35 +177,12 @@ Generates connectivity graphs from `Plan` geometries. * Connects rooms via "door" or "window" geometries. * **Returns**: A new `Graph` object. -## Building Generation - -### `ResPlan.Library.BuildingGenerator` - -Procedurally generates multi-story buildings from a set of available plans. - -* `Building GenerateBuilding(List availablePlans, int targetFloors)` - * Stacks plans to form a coherent building structure. - * **Algorithm**: - 1. **Normalization**: All candidate plans are translated so their "front door" is at (0,0). - 2. **Sorting**: Plans are sorted by bounding box area (largest first). - 3. **Stacking**: - * Floor 0 is the largest available plan. - * Subsequent floors are chosen from the remaining plans such that they "fit" within the previous floor's footprint (checking geometric containment > 95%). - * The generator attempts 4 cardinal rotations (0, 90, 180, 270 degrees) to find the best fit. - 4. **Stairs**: A "stair core" geometry (4x3m) is automatically generated adjacent to the front door for vertical circulation. - * **Returns**: A `Building` object containing the stacked floors. - ### `ResPlan.Library.PlanRenderer` Visualizes plans using SkiaSharp. * `static void Render(Plan plan, string outputPath, int width = 800, int height = 800)` * Renders a single `Plan`. -* `static void RenderFloor(BuildingFloor floor, string outputPath, int width = 800, int height = 800)` - * **floor**: The `BuildingFloor` to render. - * **Details**: - * Combines the `Plan` geometries with `AdditionalGeometries` (e.g., stairs). - * Renders "stairs" in a distinct color (DarkBlue). ## Testing diff --git a/README.md b/README.md index 5d04b61..bd7b75f 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Console.WriteLine($"Loaded {plans.Count} plans."); ### 2. Applying Constraints (Optional) -You can filter and orient plans using `PlanGenerationConstraints`. +You can filter plans using `PlanGenerationConstraints`. ```csharp using NetTopologySuite.Geometries; @@ -66,10 +66,7 @@ var boundingPoly = new GeometryFactory().CreatePolygon(coordinates); var constraints = new PlanGenerationConstraints { // Filter: Plan must fit inside this polygon - BoundingPolygon = boundingPoly, - - // Orientation: Rotate plan so the front door faces "Up" (positive Y) - FrontDoorFacing = new Vector2(0, 1) + BoundingPolygon = boundingPoly }; var constrainedPlans = await PlanLoader.LoadPlansAsync( @@ -121,32 +118,7 @@ foreach (var plan in plans) } ``` -### 5. Multi-Story Building Generation - -You can generate multi-story buildings by stacking compatible floor plans using `BuildingGenerator`. - -```csharp -using ResPlan.Library; - -// 1. Load a pool of plans (load enough to find good matches) -var plans = await PlanLoader.LoadPlansAsync(maxItems: 50, logger: logger); - -// 2. Generate a building -var generator = new BuildingGenerator(); -// Attempt to stack 3 floors using the available plans -Building building = generator.GenerateBuilding(plans, targetFloors: 3); - -Console.WriteLine($"Generated building with {building.Floors.Count} floors."); - -// 3. Render each floor (including generated stairs) -foreach (var floor in building.Floors) -{ - // RenderFloor visualizes the plan plus additional geometries (e.g., stairs) - PlanRenderer.RenderFloor(floor, $"building_floor_{floor.FloorNumber}.png"); -} -``` - -### 6. Serialization +### 5. 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.Demo/Program.cs b/ResPlan.Demo/Program.cs index b2c62ff..aade30d 100644 --- a/ResPlan.Demo/Program.cs +++ b/ResPlan.Demo/Program.cs @@ -12,32 +12,38 @@ static async Task Main(string[] args) try { Console.WriteLine("ResPlan Demo"); - Console.WriteLine("Generating Multi-Story Building..."); + Console.WriteLine("Loading Floor Plans..."); - var plans = await PlanLoader.LoadPlansAsync(maxItems: 50); + var plans = await PlanLoader.LoadPlansAsync(maxItems: 5); if (plans.Count == 0) { Console.WriteLine("No plans loaded."); return; } - var generator = new BuildingGenerator(); - var building = generator.GenerateBuilding(plans, 3); + Console.WriteLine($"Loaded {plans.Count} plans."); - Console.WriteLine($"Generated Building with {building.Floors.Count} floors."); - - // Render each floor string outputDir = "output"; if (!Directory.Exists(outputDir)) { Directory.CreateDirectory(outputDir); } - foreach (var floor in building.Floors) + foreach (var plan in plans) { - string filename = Path.Combine(outputDir, $"floor_{floor.FloorNumber}.png"); - PlanRenderer.RenderFloor(floor, filename); - Console.WriteLine($"Rendered {filename}"); + string filename = Path.Combine(outputDir, $"plan_{plan.Id}.png"); + PlanRenderer.Render(plan, filename); + Console.WriteLine($"Rendered Plan {plan.Id} to {filename}"); + + var entrance = plan.GetEntrance(); + if (entrance != null) + { + Console.WriteLine($" Plan {plan.Id} Entrance: {entrance}"); + } + else + { + Console.WriteLine($" Plan {plan.Id} has no entrance."); + } } Console.WriteLine("Done."); diff --git a/ResPlan.Library/BuildingGenerator.cs b/ResPlan.Library/BuildingGenerator.cs deleted file mode 100644 index f259ab5..0000000 --- a/ResPlan.Library/BuildingGenerator.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NetTopologySuite.Geometries; -using NetTopologySuite.Geometries.Utilities; - -namespace ResPlan.Library -{ - /// - /// Generates multi-story buildings by stacking compatible floor plans. - /// - public class BuildingGenerator - { - private readonly GeometryFactory _geometryFactory = new GeometryFactory(); - - /// - /// Generates a building with a specified number of floors using the provided plans. - /// - /// A list of candidate plans to stack. - /// The desired number of floors. - /// A object containing the stacked floors. - public Building GenerateBuilding(List availablePlans, int targetFloors) - { - var building = new Building(); - if (availablePlans == null || availablePlans.Count == 0) - return building; - - // Work on copies to avoid mutating the input list objects - var unusedPlans = availablePlans.Select(ClonePlan).ToList(); - - // Step 1: Normalize all plans (Center Front Door to 0,0) - foreach (var p in unusedPlans) - { - NormalizePlan(p); - } - - // Step 2: Sort by Area descending (simple heuristic) - unusedPlans = unusedPlans.OrderByDescending(p => p.Bounds.Area).ToList(); - - if (unusedPlans.Count == 0) return building; - - // Step 3: Pick Base Floor - var basePlan = unusedPlans[0]; - unusedPlans.RemoveAt(0); - - var floor0 = new BuildingFloor - { - FloorNumber = 0, - Plan = basePlan - }; - AddStairCore(floor0); - building.Floors.Add(floor0); - - // Step 4: Stack subsequent floors - var currentFloorPlan = basePlan; - - for (int i = 1; i < targetFloors; i++) - { - Plan bestFitPlan = null; - int bestFitIndex = -1; - double bestFitScore = -1; - - // Search for a plan that fits - for (int j = 0; j < unusedPlans.Count; j++) - { - var candidate = unusedPlans[j]; - - // We need to test rotations WITHOUT mutating 'candidate' permanently for the next iteration of 'j' loop - // But 'candidate' is already a clone from the input. - // However, if we rotate it for test 1, we must rotate it back or use a temp copy. - // Since geometry operations can be expensive, let's clone for the inner loop? - // Or just rotate, check, rotate back? Rotate back is cheaper. - - for (int r = 0; r < 4; r++) - { - // Rotate 90 degrees (relative to 0,0) - if (r > 0) - { - candidate.Rotate(Math.PI / 2.0, new Coordinate(0, 0)); - } - - // Check fit - var currentGeom = _geometryFactory.ToGeometry(currentFloorPlan.Bounds); - var candidateGeom = _geometryFactory.ToGeometry(candidate.Bounds); - - var intersection = currentGeom.Intersection(candidateGeom); - var coverage = intersection.Area / candidateGeom.Area; - - if (coverage > 0.95) // 95% contained - { - if (candidateGeom.Area > bestFitScore) - { - bestFitScore = candidateGeom.Area; - // We found a better fit. Snapshot the plan in its current state. - bestFitPlan = ClonePlan(candidate); - bestFitIndex = j; - } - } - } - - // Rotate back to original state for next outer loop usage - candidate.Rotate(Math.PI / 2.0, new Coordinate(0, 0)); - } - - if (bestFitPlan != null) - { - unusedPlans.RemoveAt(bestFitIndex); - var nextFloor = new BuildingFloor - { - FloorNumber = i, - Plan = bestFitPlan - }; - AddStairCore(nextFloor); - building.Floors.Add(nextFloor); - currentFloorPlan = bestFitPlan; - } - else - { - break; - } - } - - return building; - } - - private Plan ClonePlan(Plan source) - { - var newPlan = new Plan - { - Id = source.Id, - Geometries = new Dictionary>(), - // Bounds will be recalculated or copied - ReferenceGraph = null // Not deep cloning graph for now as we don't use it for generation - }; - - if (source.Bounds != null) - { - newPlan.Bounds = new Envelope(source.Bounds); - } - - foreach(var kvp in source.Geometries) - { - newPlan.Geometries[kvp.Key] = new List(); - foreach(var g in kvp.Value) - { - newPlan.Geometries[kvp.Key].Add(g.Copy()); - } - } - - return newPlan; - } - - private void NormalizePlan(Plan plan) - { - if (plan.Geometries.ContainsKey("front_door") && plan.Geometries["front_door"].Count > 0) - { - var fd = plan.Geometries["front_door"][0]; - var center = fd.Centroid.Coordinate; - - plan.Translate(-center.X, -center.Y); - } - } - - private void AddStairCore(BuildingFloor floor) - { - // Create a stair core rectangle 4x3m placed "south" of 0,0 - var coords = new Coordinate[] - { - new Coordinate(-2, -4), - new Coordinate(2, -4), - new Coordinate(2, 0), // Touches 0,0 (Front Door) - new Coordinate(-2, 0), - new Coordinate(-2, -4) - }; - var shell = _geometryFactory.CreateLinearRing(coords); - var poly = _geometryFactory.CreatePolygon(shell); - - floor.AdditionalGeometries["stairs"] = new List { poly }; - } - } -} diff --git a/ResPlan.Library/Models.cs b/ResPlan.Library/Models.cs index 09a69b7..58e770a 100644 --- a/ResPlan.Library/Models.cs +++ b/ResPlan.Library/Models.cs @@ -4,7 +4,6 @@ using System.Text.Json.Serialization; using MessagePack; using NetTopologySuite.Geometries; -using NetTopologySuite.Geometries.Utilities; using ResPlan.Library.Data; namespace ResPlan.Library @@ -54,59 +53,27 @@ public class Plan [Key(3)] public Graph ReferenceGraph { get; set; } - public void Rotate(double angleRadians, Coordinate center) + public Coordinate GetEntrance() { - var transform = new AffineTransformation(); - transform.Rotate(angleRadians, center.X, center.Y); + List candidates = null; - ApplyTransformation(transform); - } - - public void Translate(double dx, double dy) - { - var transform = new AffineTransformation(); - transform.Translate(dx, dy); - - ApplyTransformation(transform); - } - - private void ApplyTransformation(AffineTransformation transform) - { - // Transform geometries - foreach (var key in Geometries.Keys.ToList()) + if (Geometries.TryGetValue("front_door", out var doors) && doors.Any()) { - var originalList = Geometries[key]; - var newList = new List(); - foreach (var geom in originalList) - { - var newGeom = transform.Transform(geom); - newList.Add(newGeom); - } - Geometries[key] = newList; + candidates = doors; } - - // Update Bounds - var newEnvelope = new Envelope(); - foreach (var list in Geometries.Values) + else if (Geometries.TryGetValue("entrance", out var entrances) && entrances.Any()) { - foreach (var geom in list) - { - newEnvelope.ExpandToInclude(geom.EnvelopeInternal); - } + candidates = entrances; } - Bounds = newEnvelope; - // Transform Graph Nodes - if (ReferenceGraph != null && ReferenceGraph.Nodes != null) + if (candidates != null && candidates.Count > 0) { - foreach (var node in ReferenceGraph.Nodes.Values) - { - if (node.Geometry != null) - { - node.Geometry = transform.Transform(node.Geometry); - } - } + // Sort deterministically by Centroid X then Y + var sorted = candidates.OrderBy(g => g.Centroid.X).ThenBy(g => g.Centroid.Y).ToList(); + return sorted[0].Centroid.Coordinate; } + + return null; } } @@ -142,42 +109,4 @@ public class Edge [Key(2)] public string Type { get; set; } } - - /// - /// Represents a multi-story building composed of stacked floor plans. - /// - [MessagePackObject] - public class Building - { - /// - /// The list of floors in the building, typically ordered by floor number. - /// - [Key(0)] - public List Floors { get; set; } = new List(); - } - - /// - /// Represents a single floor within a building. - /// - [MessagePackObject] - public class BuildingFloor - { - /// - /// The 0-based index of the floor. - /// - [Key(0)] - public int FloorNumber { get; set; } - - /// - /// The floor plan associated with this level. - /// - [Key(1)] - public Plan Plan { get; set; } - - /// - /// Additional geometries generated for this floor (e.g., stairs, corridors) that are not part of the original plan. - /// - [Key(2)] - public Dictionary> AdditionalGeometries { get; set; } = new Dictionary>(); - } } diff --git a/ResPlan.Library/PlanGenerationConstraints.cs b/ResPlan.Library/PlanGenerationConstraints.cs index f3abc62..f6384c2 100644 --- a/ResPlan.Library/PlanGenerationConstraints.cs +++ b/ResPlan.Library/PlanGenerationConstraints.cs @@ -7,7 +7,5 @@ namespace ResPlan.Library public class PlanGenerationConstraints { public Polygon BoundingPolygon { get; set; } - public Vector2? FrontDoorFacing { get; set; } - public Vector2? GarageFacing { get; set; } } } diff --git a/ResPlan.Library/PlanLoader.cs b/ResPlan.Library/PlanLoader.cs index 6036012..5211b81 100644 --- a/ResPlan.Library/PlanLoader.cs +++ b/ResPlan.Library/PlanLoader.cs @@ -20,8 +20,6 @@ public static async Task> LoadPlansAsync(string jsonPath = null, stri { if (!string.IsNullOrEmpty(jsonPath) && File.Exists(jsonPath)) { - // In this path, we don't apply constraints inside LoadPlansFromJson for now, or we should? - // The prompt says "When using the API". It's better to be consistent. var loaded = LoadPlansFromJson(jsonPath); return ApplyConstraints(loaded, constraints, logger); } @@ -72,9 +70,6 @@ public static async Task> LoadPlansAsync(string jsonPath = null, stri UseShellExecute = false }; - // Set environment to use the venv? - // Invoking the python binary in venv/bin/python3 sets up sys.path correctly automatically. - using (var p = Process.Start(psi)) { var stdout = await p.StandardOutput.ReadToEndAsync(); @@ -87,10 +82,6 @@ public static async Task> LoadPlansAsync(string jsonPath = null, stri } // Parse stdout as JSON - // The script should output JSON. - // We might need to filter stdout if there are prints. - // The wrapper script should avoid prints. - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, @@ -125,55 +116,9 @@ private static List ApplyConstraints(List plans, PlanGenerationConst { if (constraints == null) return plans; - if (constraints.GarageFacing.HasValue) - { - logger?.Invoke("Warning: Garage/Driveway facing constraints are not supported by the current ResPlan dataset."); - } - var result = new List(); foreach (var plan in plans) { - // Apply Facing Constraint - if (constraints.FrontDoorFacing.HasValue) - { - // Find front door - Geometry frontDoor = null; - if (plan.Geometries.ContainsKey("front_door")) - { - var fds = plan.Geometries["front_door"]; - if (fds.Count > 0) - { - // If multiple, pick first? - frontDoor = fds[0]; - } - } - - if (frontDoor != null) - { - var centroid = frontDoor.Centroid.Coordinate; - var planCenter = plan.Bounds.Centre; // Envelope center - - // Vector from plan center to door center - var currentVec = new NetTopologySuite.Geometries.Coordinate(centroid.X - planCenter.X, centroid.Y - planCenter.Y); - - // If center and door are same (rare), skip - if (Math.Abs(currentVec.X) > 1e-6 || Math.Abs(currentVec.Y) > 1e-6) - { - var targetX = constraints.FrontDoorFacing.Value.X; - var targetY = constraints.FrontDoorFacing.Value.Y; - - // Angle of current vector - var currentAngle = Math.Atan2(currentVec.Y, currentVec.X); - // Angle of target - var targetAngle = Math.Atan2(targetY, targetX); - - var diff = targetAngle - currentAngle; - - plan.Rotate(diff, planCenter); - } - } - } - // Apply Bounding Constraint if (!IsPlanCompatible(plan, constraints.BoundingPolygon)) { diff --git a/ResPlan.Library/PlanRenderer.cs b/ResPlan.Library/PlanRenderer.cs index f85f458..e574b63 100644 --- a/ResPlan.Library/PlanRenderer.cs +++ b/ResPlan.Library/PlanRenderer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.IO; using NetTopologySuite.Geometries; using SkiaSharp; @@ -28,48 +29,6 @@ public class PlanRenderer "living", "bedroom", "bathroom", "kitchen", "door", "window", "wall", "front_door", "balcony", "stairs" }; - public static void RenderFloor(BuildingFloor floor, string outputPath, int width = 800, int height = 800) - { - // Create a composite plan for rendering - // We can shallow copy the plan logic - // But Render takes a Plan. - // We can create a temporary Plan object that merges Geometries + AdditionalGeometries - - // Create a composite plan for rendering - // We use deep copy of the lists to avoid mutating the original plan - var compositePlan = new Plan - { - Id = floor.Plan.Id, - Geometries = new Dictionary>(), - Bounds = new Envelope(floor.Plan.Bounds) - }; - - foreach(var kvp in floor.Plan.Geometries) - { - compositePlan.Geometries[kvp.Key] = new List(kvp.Value); - } - - foreach(var kvp in floor.AdditionalGeometries) - { - if(compositePlan.Geometries.ContainsKey(kvp.Key)) - { - compositePlan.Geometries[kvp.Key].AddRange(kvp.Value); - } - else - { - compositePlan.Geometries[kvp.Key] = new List(kvp.Value); - } - - // Expand bounds - foreach(var g in kvp.Value) - { - compositePlan.Bounds.ExpandToInclude(g.EnvelopeInternal); - } - } - - Render(compositePlan, outputPath, width, height); - } - public static void Render(Plan plan, string outputPath, int width = 800, int height = 800) { using var surface = SKSurface.Create(new SKImageInfo(width, height)); diff --git a/ResPlan.Tests/BuildingGeneratorTests.cs b/ResPlan.Tests/BuildingGeneratorTests.cs deleted file mode 100644 index 1ead246..0000000 --- a/ResPlan.Tests/BuildingGeneratorTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NetTopologySuite.Geometries; -using ResPlan.Library; -using Xunit; - -namespace ResPlan.Tests -{ - public class BuildingGeneratorTests - { - private readonly GeometryFactory _geometryFactory = new GeometryFactory(); - - [Fact] - public void TestGenerateBuilding() - { - // Create mock plans manually to avoid Python dependency in unit tests - var plans = new List - { - CreateMockPlan(1, 20, 20), // 400 area - CreateMockPlan(2, 15, 15), // 225 area - CreateMockPlan(3, 10, 10), // 100 area - CreateMockPlan(4, 30, 30) // 900 area (Largest) - }; - - var generator = new BuildingGenerator(); - var building = generator.GenerateBuilding(plans, targetFloors: 5); - - Assert.NotNull(building); - Assert.NotEmpty(building.Floors); - - // We provided 4 plans, target 5. Should use all 4 if they fit. - // Order should be 30x30 -> 20x20 -> 15x15 -> 10x10. - Assert.Equal(4, building.Floors.Count); - - // Verify Floor Sorting (Area Descending) - Assert.Equal(4, building.Floors[0].Plan.Id); // 30x30 - Assert.Equal(1, building.Floors[1].Plan.Id); // 20x20 - Assert.Equal(2, building.Floors[2].Plan.Id); // 15x15 - Assert.Equal(3, building.Floors[3].Plan.Id); // 10x10 - - // Check Stair Core - foreach(var floor in building.Floors) - { - Assert.True(floor.AdditionalGeometries.ContainsKey("stairs")); - Assert.NotEmpty(floor.AdditionalGeometries["stairs"]); - - // Verify Front Door is at 0,0 (Normalization) - if (floor.Plan.Geometries.ContainsKey("front_door")) - { - var fd = floor.Plan.Geometries["front_door"].FirstOrDefault(); - if (fd != null) - { - var c = fd.Centroid.Coordinate; - Assert.InRange(c.X, -0.001, 0.001); - Assert.InRange(c.Y, -0.001, 0.001); - } - } - } - - // Check containment - for(int i=0; i upper.Bounds.Area); - } - } - - private Plan CreateMockPlan(int id, double width, double height) - { - var plan = new Plan - { - Id = id, - Geometries = new Dictionary>() - }; - - // Define coordinates for a box - // Assume Front Door is at bottom-center relative to the box? - // Or we just place a box and a front door point. - // Let's place the box from (0,0) to (width, height) - // And Front Door at (width/2, 0). - - var box = _geometryFactory.CreatePolygon(new Coordinate[] - { - new Coordinate(0, 0), - new Coordinate(width, 0), - new Coordinate(width, height), - new Coordinate(0, height), - new Coordinate(0, 0) - }); - - // Create Front Door geometry (small line or polygon) - var fdCenterX = width / 2.0; - var fd = _geometryFactory.CreatePolygon(new Coordinate[] - { - new Coordinate(fdCenterX - 0.5, 0), - new Coordinate(fdCenterX + 0.5, 0), - new Coordinate(fdCenterX + 0.5, 0.2), // Slight thickness - new Coordinate(fdCenterX - 0.5, 0.2), - new Coordinate(fdCenterX - 0.5, 0) - }); - - plan.Geometries["living"] = new List { box }; - plan.Geometries["front_door"] = new List { fd }; - - plan.Bounds = box.EnvelopeInternal; - - return plan; - } - } -} diff --git a/ResPlan.Tests/ConstraintTests.cs b/ResPlan.Tests/ConstraintTests.cs deleted file mode 100644 index 72abd53..0000000 --- a/ResPlan.Tests/ConstraintTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -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> - { - { "room", new List { 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."); - } - } -}