roomrubikspack 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- roomrubikspack-0.1.0/LICENSE +21 -0
- roomrubikspack-0.1.0/PKG-INFO +154 -0
- roomrubikspack-0.1.0/README.md +133 -0
- roomrubikspack-0.1.0/pyproject.toml +37 -0
- roomrubikspack-0.1.0/setup.cfg +4 -0
- roomrubikspack-0.1.0/src/roomrubikspack/__init__.py +379 -0
- roomrubikspack-0.1.0/src/roomrubikspack/types.py +138 -0
- roomrubikspack-0.1.0/src/roomrubikspack/utils/__init__.py +1 -0
- roomrubikspack-0.1.0/src/roomrubikspack/utils/constraints.py +19 -0
- roomrubikspack-0.1.0/src/roomrubikspack/utils/graph_utils.py +143 -0
- roomrubikspack-0.1.0/src/roomrubikspack.egg-info/PKG-INFO +154 -0
- roomrubikspack-0.1.0/src/roomrubikspack.egg-info/SOURCES.txt +13 -0
- roomrubikspack-0.1.0/src/roomrubikspack.egg-info/dependency_links.txt +1 -0
- roomrubikspack-0.1.0/src/roomrubikspack.egg-info/requires.txt +8 -0
- roomrubikspack-0.1.0/src/roomrubikspack.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vijesh Kumar V
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: roomrubikspack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client wrapper for procedural architectural floorplan layout generator
|
|
5
|
+
Author-email: Vijesh Kumar V <your.email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/roomrubikspack
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/roomrubikspack/issues
|
|
9
|
+
Keywords: architecture,floorplan,layout,genetic-algorithm,CAD,procedural
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
15
|
+
Requires-Dist: networkx>=2.6.0
|
|
16
|
+
Requires-Dist: ezdxf>=1.0.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# RoomRubiksPack (Client Library)
|
|
23
|
+
|
|
24
|
+
RoomRubiksPack is a lightweight Python package for generating architectural floorplan layouts using procedural generation and an Elitist Genetic Algorithm.
|
|
25
|
+
|
|
26
|
+
This client library maintains a local, stateful API for creating room models, local graphing/connectivity check, local plotting/visualisation via `matplotlib`, and local CAD exports via `ezdxf`. The computationally heavy layout generation (GA) and auto-dimensioning are offloaded to a RoomRubiks API Server.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### Option 1: Install from PyPI
|
|
33
|
+
Once published, install the package via:
|
|
34
|
+
```bash
|
|
35
|
+
pip install roomrubikspack
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Option 2: Install from GitHub
|
|
39
|
+
```bash
|
|
40
|
+
pip install git+https://github.com/yourusername/roomrubikspack.git
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Option 3: Local Installation (After Downloading)
|
|
44
|
+
Navigate to the directory containing `pyproject.toml` and run:
|
|
45
|
+
```bash
|
|
46
|
+
pip install .
|
|
47
|
+
```
|
|
48
|
+
For local developers who want to modify the source code, run in editable mode:
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Configuring the Server Connection
|
|
56
|
+
|
|
57
|
+
By default, the client library will look for your live API server running at `https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app`.
|
|
58
|
+
|
|
59
|
+
You can point to a different server endpoint in two ways:
|
|
60
|
+
|
|
61
|
+
### 1. In Python Code (Recommended)
|
|
62
|
+
Set the URL dynamically via `rr.settings()`:
|
|
63
|
+
```python
|
|
64
|
+
import roomrubikspack as rr
|
|
65
|
+
|
|
66
|
+
rr.init()
|
|
67
|
+
rr.settings(server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Via Environment Variable
|
|
71
|
+
Before running your script, set the `ROOMRUBIKSPACK_SERVER_URL` environment variable:
|
|
72
|
+
```bash
|
|
73
|
+
# Windows PowerShell
|
|
74
|
+
$env:ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
75
|
+
|
|
76
|
+
# Windows Command Prompt
|
|
77
|
+
set ROOMRUBIKSPACK_SERVER_URL=https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app
|
|
78
|
+
|
|
79
|
+
# Linux/macOS
|
|
80
|
+
export ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
Here is a complete example. Make sure your local or remote RoomRubiks server is running before executing this script.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import roomrubikspack as rr
|
|
91
|
+
|
|
92
|
+
# Initialize session
|
|
93
|
+
rr.init()
|
|
94
|
+
|
|
95
|
+
# Configure the server (defaults to your live Cloud Run URL if omitted)
|
|
96
|
+
rr.settings(unit="m", server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
97
|
+
|
|
98
|
+
# Define Rooms
|
|
99
|
+
rr.room("living", "Living Room", area=20.0, startSpace=True)
|
|
100
|
+
rr.room("kitchen", "Kitchen", w=3.0, h=3.0)
|
|
101
|
+
rr.room("bed1", "Master Bed", area=16.0)
|
|
102
|
+
rr.room("bath1", "Attached Bath", area=4.0, attachedSpace=True)
|
|
103
|
+
|
|
104
|
+
# Define site boundary
|
|
105
|
+
rr.site([{"x": 0, "y": 0}, {"x": 20, "y": 0}, {"x": 20, "y": 20}, {"x": 0, "y": 20}])
|
|
106
|
+
|
|
107
|
+
# Define connectivity
|
|
108
|
+
rr.connectivity(
|
|
109
|
+
("living", "kitchen"),
|
|
110
|
+
("living", "bed1"),
|
|
111
|
+
("bed1", "bath1")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Optional: Draw connectivity graph locally
|
|
115
|
+
rr.connectivityshow()
|
|
116
|
+
|
|
117
|
+
# Add constraints to guide layout generation
|
|
118
|
+
rr.constraint("position", "bed1", "N")
|
|
119
|
+
rr.constraint("area", None, 120)
|
|
120
|
+
rr.constraint("perimeter", None, "minimize")
|
|
121
|
+
|
|
122
|
+
# Generate sizes for rooms missing width/height
|
|
123
|
+
rr.dimensiongen()
|
|
124
|
+
|
|
125
|
+
# Generate layout variations (sent to the server engine)
|
|
126
|
+
rr.generatelayout()
|
|
127
|
+
|
|
128
|
+
# View first variation locally
|
|
129
|
+
rr.showlayout(n=1, label=["name", "dim", "area"])
|
|
130
|
+
|
|
131
|
+
# Export layout to DXF locally
|
|
132
|
+
rr.exportlayout(n=1, filepath="output_layout.dxf")
|
|
133
|
+
|
|
134
|
+
# Blocks execution until plots are closed
|
|
135
|
+
rr.wait_for_plots()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## API Reference
|
|
141
|
+
|
|
142
|
+
- `rr.init()`: Clears all current session state.
|
|
143
|
+
- `rr.settings(unit, server_url)`: Set global measurement units (`'m'` or `'f'`) and configure the solver backend API endpoint.
|
|
144
|
+
- `rr.constructiongrid(add, remove, reset)`: View or manipulate the base construction grid sizes locally.
|
|
145
|
+
- `rr.room(id, name, w, h, area, startSpace, attachedSpace, ...)`: Register a room.
|
|
146
|
+
- `rr.site(points)`: Set an optional site boundary polygon.
|
|
147
|
+
- `rr.connectivity(*pairs)`: Define room connections. Planarity check runs instantly on the client.
|
|
148
|
+
- `rr.connectivityshow()`: Opens a Matplotlib window showing the adjacency graph.
|
|
149
|
+
- `rr.constraint(type, room_id, value)`: Registers a layout constraint.
|
|
150
|
+
- `rr.dimensiongen(avar, mar)`: Requests standard room dimensions from the server.
|
|
151
|
+
- `rr.generatelayout(lvar, sgap, max_variations)`: Sends session state to the server to run the GA layout engine.
|
|
152
|
+
- `rr.showlayout(n, label)`: Plots the `n`-th generated variation using Matplotlib.
|
|
153
|
+
- `rr.exportlayout(n, filepath)`: Saves the `n`-th layout to JSON or DXF.
|
|
154
|
+
- `rr.wait_for_plots()`: Helper to keep visual plots open.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# RoomRubiksPack (Client Library)
|
|
2
|
+
|
|
3
|
+
RoomRubiksPack is a lightweight Python package for generating architectural floorplan layouts using procedural generation and an Elitist Genetic Algorithm.
|
|
4
|
+
|
|
5
|
+
This client library maintains a local, stateful API for creating room models, local graphing/connectivity check, local plotting/visualisation via `matplotlib`, and local CAD exports via `ezdxf`. The computationally heavy layout generation (GA) and auto-dimensioning are offloaded to a RoomRubiks API Server.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Option 1: Install from PyPI
|
|
12
|
+
Once published, install the package via:
|
|
13
|
+
```bash
|
|
14
|
+
pip install roomrubikspack
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Option 2: Install from GitHub
|
|
18
|
+
```bash
|
|
19
|
+
pip install git+https://github.com/yourusername/roomrubikspack.git
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Option 3: Local Installation (After Downloading)
|
|
23
|
+
Navigate to the directory containing `pyproject.toml` and run:
|
|
24
|
+
```bash
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
For local developers who want to modify the source code, run in editable mode:
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Configuring the Server Connection
|
|
35
|
+
|
|
36
|
+
By default, the client library will look for your live API server running at `https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app`.
|
|
37
|
+
|
|
38
|
+
You can point to a different server endpoint in two ways:
|
|
39
|
+
|
|
40
|
+
### 1. In Python Code (Recommended)
|
|
41
|
+
Set the URL dynamically via `rr.settings()`:
|
|
42
|
+
```python
|
|
43
|
+
import roomrubikspack as rr
|
|
44
|
+
|
|
45
|
+
rr.init()
|
|
46
|
+
rr.settings(server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Via Environment Variable
|
|
50
|
+
Before running your script, set the `ROOMRUBIKSPACK_SERVER_URL` environment variable:
|
|
51
|
+
```bash
|
|
52
|
+
# Windows PowerShell
|
|
53
|
+
$env:ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
54
|
+
|
|
55
|
+
# Windows Command Prompt
|
|
56
|
+
set ROOMRUBIKSPACK_SERVER_URL=https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app
|
|
57
|
+
|
|
58
|
+
# Linux/macOS
|
|
59
|
+
export ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
Here is a complete example. Make sure your local or remote RoomRubiks server is running before executing this script.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import roomrubikspack as rr
|
|
70
|
+
|
|
71
|
+
# Initialize session
|
|
72
|
+
rr.init()
|
|
73
|
+
|
|
74
|
+
# Configure the server (defaults to your live Cloud Run URL if omitted)
|
|
75
|
+
rr.settings(unit="m", server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
76
|
+
|
|
77
|
+
# Define Rooms
|
|
78
|
+
rr.room("living", "Living Room", area=20.0, startSpace=True)
|
|
79
|
+
rr.room("kitchen", "Kitchen", w=3.0, h=3.0)
|
|
80
|
+
rr.room("bed1", "Master Bed", area=16.0)
|
|
81
|
+
rr.room("bath1", "Attached Bath", area=4.0, attachedSpace=True)
|
|
82
|
+
|
|
83
|
+
# Define site boundary
|
|
84
|
+
rr.site([{"x": 0, "y": 0}, {"x": 20, "y": 0}, {"x": 20, "y": 20}, {"x": 0, "y": 20}])
|
|
85
|
+
|
|
86
|
+
# Define connectivity
|
|
87
|
+
rr.connectivity(
|
|
88
|
+
("living", "kitchen"),
|
|
89
|
+
("living", "bed1"),
|
|
90
|
+
("bed1", "bath1")
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Optional: Draw connectivity graph locally
|
|
94
|
+
rr.connectivityshow()
|
|
95
|
+
|
|
96
|
+
# Add constraints to guide layout generation
|
|
97
|
+
rr.constraint("position", "bed1", "N")
|
|
98
|
+
rr.constraint("area", None, 120)
|
|
99
|
+
rr.constraint("perimeter", None, "minimize")
|
|
100
|
+
|
|
101
|
+
# Generate sizes for rooms missing width/height
|
|
102
|
+
rr.dimensiongen()
|
|
103
|
+
|
|
104
|
+
# Generate layout variations (sent to the server engine)
|
|
105
|
+
rr.generatelayout()
|
|
106
|
+
|
|
107
|
+
# View first variation locally
|
|
108
|
+
rr.showlayout(n=1, label=["name", "dim", "area"])
|
|
109
|
+
|
|
110
|
+
# Export layout to DXF locally
|
|
111
|
+
rr.exportlayout(n=1, filepath="output_layout.dxf")
|
|
112
|
+
|
|
113
|
+
# Blocks execution until plots are closed
|
|
114
|
+
rr.wait_for_plots()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## API Reference
|
|
120
|
+
|
|
121
|
+
- `rr.init()`: Clears all current session state.
|
|
122
|
+
- `rr.settings(unit, server_url)`: Set global measurement units (`'m'` or `'f'`) and configure the solver backend API endpoint.
|
|
123
|
+
- `rr.constructiongrid(add, remove, reset)`: View or manipulate the base construction grid sizes locally.
|
|
124
|
+
- `rr.room(id, name, w, h, area, startSpace, attachedSpace, ...)`: Register a room.
|
|
125
|
+
- `rr.site(points)`: Set an optional site boundary polygon.
|
|
126
|
+
- `rr.connectivity(*pairs)`: Define room connections. Planarity check runs instantly on the client.
|
|
127
|
+
- `rr.connectivityshow()`: Opens a Matplotlib window showing the adjacency graph.
|
|
128
|
+
- `rr.constraint(type, room_id, value)`: Registers a layout constraint.
|
|
129
|
+
- `rr.dimensiongen(avar, mar)`: Requests standard room dimensions from the server.
|
|
130
|
+
- `rr.generatelayout(lvar, sgap, max_variations)`: Sends session state to the server to run the GA layout engine.
|
|
131
|
+
- `rr.showlayout(n, label)`: Plots the `n`-th generated variation using Matplotlib.
|
|
132
|
+
- `rr.exportlayout(n, filepath)`: Saves the `n`-th layout to JSON or DXF.
|
|
133
|
+
- `rr.wait_for_plots()`: Helper to keep visual plots open.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "roomrubikspack"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Vijesh Kumar V", email = "your.email@example.com" }
|
|
10
|
+
]
|
|
11
|
+
description = "Client wrapper for procedural architectural floorplan layout generator"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
keywords = ["architecture", "floorplan", "layout", "genetic-algorithm", "CAD", "procedural"]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"requests>=2.25.0", # Server REST API communication
|
|
18
|
+
"matplotlib>=3.5.0", # showlayout() and connectivityshow() visualisation
|
|
19
|
+
"networkx>=2.6.0", # connectivityshow() network graph drawing
|
|
20
|
+
"ezdxf>=1.0.0" # exportlayout() DXF export
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/yourusername/roomrubikspack"
|
|
25
|
+
"Bug Tracker" = "https://github.com/yourusername/roomrubikspack/issues"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=7.0", # Test runner
|
|
30
|
+
"pytest-cov>=4.0", # Coverage reporting
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"] # src layout: package root is src/
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from typing import List, Dict, Tuple, Optional, Any
|
|
2
|
+
import json
|
|
3
|
+
import dataclasses
|
|
4
|
+
import requests
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .types import Room, Connection, Site, Door, Window, Furniture
|
|
8
|
+
from .utils.graph_utils import check_planarity
|
|
9
|
+
from .utils.constraints import add_constraint, clear_constraints
|
|
10
|
+
|
|
11
|
+
# Session state kept on the client side
|
|
12
|
+
_rooms: List[Room] = []
|
|
13
|
+
_connections: List[Connection] = []
|
|
14
|
+
_site: Optional[Site] = None
|
|
15
|
+
_layout_variations: List[List[Room]] = []
|
|
16
|
+
_DEFAULT_GRID_SIZES: List[float] = [1.2, 1.5, 1.8, 2.1, 2.4, 3.0, 4.5, 6.0, 7.5, 9.0]
|
|
17
|
+
_base_grid_sizes: List[float] = _DEFAULT_GRID_SIZES.copy()
|
|
18
|
+
_settings: Dict[str, str] = {"unit": "m"}
|
|
19
|
+
|
|
20
|
+
# Configurable server URL (falls back to live Cloud Run or environment variable)
|
|
21
|
+
_server_url: str = os.getenv("ROOMRUBIKSPACK_SERVER_URL", "https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app").rstrip('/')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def deserialize_door(d: Dict[str, Any]) -> Door:
|
|
25
|
+
valid_fields = {f.name for f in dataclasses.fields(Door)}
|
|
26
|
+
filtered_data = {k: v for k, v in d.items() if k in valid_fields}
|
|
27
|
+
return Door(**filtered_data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def deserialize_window(w: Dict[str, Any]) -> Window:
|
|
31
|
+
valid_fields = {f.name for f in dataclasses.fields(Window)}
|
|
32
|
+
filtered_data = {k: v for k, v in w.items() if k in valid_fields}
|
|
33
|
+
return Window(**filtered_data)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def deserialize_furniture(f: Dict[str, Any]) -> Furniture:
|
|
37
|
+
valid_fields = {f.name for f in dataclasses.fields(Furniture)}
|
|
38
|
+
filtered_data = {k: v for k, v in f.items() if k in valid_fields}
|
|
39
|
+
return Furniture(**filtered_data)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def deserialize_room(r: Dict[str, Any]) -> Room:
|
|
43
|
+
# Separate nested complex structures
|
|
44
|
+
doors_data = r.pop("doors", []) or []
|
|
45
|
+
windows_data = r.pop("windows", []) or []
|
|
46
|
+
furniture_data = r.pop("furniture", []) or []
|
|
47
|
+
|
|
48
|
+
doors = [deserialize_door(d) for d in doors_data if isinstance(d, dict)]
|
|
49
|
+
windows = [deserialize_window(w) for w in windows_data if isinstance(w, dict)]
|
|
50
|
+
furniture = [deserialize_furniture(f) for f in furniture_data if isinstance(f, dict)]
|
|
51
|
+
|
|
52
|
+
# Filter flat dictionary to match dataclass fields
|
|
53
|
+
valid_fields = {f.name for f in dataclasses.fields(Room)}
|
|
54
|
+
filtered_data = {k: v for k, v in r.items() if k in valid_fields}
|
|
55
|
+
|
|
56
|
+
room_obj = Room(**filtered_data)
|
|
57
|
+
room_obj.doors = doors
|
|
58
|
+
room_obj.windows = windows
|
|
59
|
+
room_obj.furniture = furniture
|
|
60
|
+
return room_obj
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def init():
|
|
64
|
+
"""Initializes a new session/project (clears any existing state)."""
|
|
65
|
+
global _rooms, _connections, _site, _layout_variations, _base_grid_sizes
|
|
66
|
+
_rooms = []
|
|
67
|
+
_connections = []
|
|
68
|
+
_site = None
|
|
69
|
+
_layout_variations = []
|
|
70
|
+
_base_grid_sizes = _DEFAULT_GRID_SIZES.copy()
|
|
71
|
+
clear_constraints()
|
|
72
|
+
print("RoomRubiks session initialized successfully.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def room(id: str, name: str = "", **kwargs):
|
|
76
|
+
"""Adds a room to the session and prints a success message with details."""
|
|
77
|
+
global _rooms
|
|
78
|
+
new_room = Room(id=id, name=name, **kwargs)
|
|
79
|
+
_rooms.append(new_room)
|
|
80
|
+
details = f"Area: {new_room.area} " if new_room.area else f"Size: {new_room.w}x{new_room.h} "
|
|
81
|
+
start_space_msg = " (Start Space)" if new_room.startSpace else ""
|
|
82
|
+
print(f"Added room successfully: {new_room.name} ({new_room.id}) - {details}{start_space_msg}")
|
|
83
|
+
return new_room
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def connectivity(*conn_pairs: Tuple[str, str]):
|
|
87
|
+
"""Registers the connections, runs a planarity check, prints a message."""
|
|
88
|
+
global _connections, _rooms
|
|
89
|
+
|
|
90
|
+
for pair in conn_pairs:
|
|
91
|
+
_connections.append(Connection(roomA=pair[0], roomB=pair[1]))
|
|
92
|
+
|
|
93
|
+
start_spaces = [r for r in _rooms if r.startSpace]
|
|
94
|
+
|
|
95
|
+
print(f"Added {len(conn_pairs)} connections.")
|
|
96
|
+
|
|
97
|
+
if len(start_spaces) != 1:
|
|
98
|
+
print(f"WARNING: There must be exactly one start space. Found {len(start_spaces)}.")
|
|
99
|
+
else:
|
|
100
|
+
print(f"Start space verified: {start_spaces[0].name}")
|
|
101
|
+
|
|
102
|
+
room_ids = [r.id for r in _rooms]
|
|
103
|
+
is_planar = check_planarity(room_ids, _connections)
|
|
104
|
+
if is_planar:
|
|
105
|
+
print("Planarity check passed: The connectivity graph satisfies Euler's formula.")
|
|
106
|
+
else:
|
|
107
|
+
print("WARNING: The provided connectivity graph is non-planar.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def connectivityshow():
|
|
111
|
+
"""Shows a matplotlib network diagram of the registered connectivity."""
|
|
112
|
+
global _connections, _rooms
|
|
113
|
+
if not _rooms or not _connections:
|
|
114
|
+
print("No rooms or connections registered to show.")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
import matplotlib.pyplot as plt
|
|
119
|
+
import networkx as nx
|
|
120
|
+
except ImportError:
|
|
121
|
+
print("Please install matplotlib and networkx to use connectivityshow(): pip install matplotlib networkx")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
G = nx.Graph()
|
|
125
|
+
|
|
126
|
+
for r in _rooms:
|
|
127
|
+
G.add_node(r.id, label=r.name or r.id, is_start=getattr(r, 'startSpace', False))
|
|
128
|
+
|
|
129
|
+
for c in _connections:
|
|
130
|
+
G.add_edge(c.roomA, c.roomB)
|
|
131
|
+
|
|
132
|
+
pos = nx.spring_layout(G, seed=42)
|
|
133
|
+
labels = nx.get_node_attributes(G, 'label')
|
|
134
|
+
colors = ['lightgreen' if nx.get_node_attributes(G, 'is_start').get(n) else 'lightblue' for n in G.nodes()]
|
|
135
|
+
|
|
136
|
+
plt.figure(figsize=(8, 6))
|
|
137
|
+
nx.draw(G, pos, labels=labels, node_color=colors, with_labels=True, node_size=2000, font_size=10, font_weight="bold", edge_color="gray")
|
|
138
|
+
plt.title("Connectivity Network Diagram")
|
|
139
|
+
plt.show(block=False)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def site(points: List[Dict[str, float]]):
|
|
143
|
+
"""Defines the optional site boundary."""
|
|
144
|
+
global _site
|
|
145
|
+
_site = Site(points=points)
|
|
146
|
+
print("Site boundary added successfully.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def constraint(constraint_type: str, room_id: Optional[str] = None, value: Any = None):
|
|
150
|
+
"""Adds a constraint (e.g. position, area, perimeter) for the layout generator."""
|
|
151
|
+
add_constraint(constraint_type, room_id, value)
|
|
152
|
+
print(f"Added constraint: {constraint_type} for room {room_id} with value {value}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def settings(unit: Optional[str] = None, server_url: Optional[str] = None):
|
|
156
|
+
"""Sets global project settings and server endpoint."""
|
|
157
|
+
global _settings, _server_url
|
|
158
|
+
if unit is not None:
|
|
159
|
+
unit = unit.lower()
|
|
160
|
+
if unit not in ['m', 'f']:
|
|
161
|
+
print("Warning: Unsupported unit. Use 'm' for meters or 'f' for feet.")
|
|
162
|
+
else:
|
|
163
|
+
_settings["unit"] = unit
|
|
164
|
+
print(f"Settings updated: unit set to '{unit}'")
|
|
165
|
+
|
|
166
|
+
if server_url is not None:
|
|
167
|
+
_server_url = server_url.rstrip('/')
|
|
168
|
+
print(f"Settings updated: server URL set to '{_server_url}'")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def constructiongrid(add: Optional[float] = None, remove: Optional[float] = None, reset: bool = False):
|
|
172
|
+
"""Shows or modifies the base construction grid sizes used by dimensiongen."""
|
|
173
|
+
global _base_grid_sizes, _DEFAULT_GRID_SIZES
|
|
174
|
+
|
|
175
|
+
if reset:
|
|
176
|
+
_base_grid_sizes = _DEFAULT_GRID_SIZES.copy()
|
|
177
|
+
print("Construction grid reset to defaults.")
|
|
178
|
+
return _base_grid_sizes
|
|
179
|
+
|
|
180
|
+
if add is not None:
|
|
181
|
+
if add not in _base_grid_sizes:
|
|
182
|
+
_base_grid_sizes.append(float(add))
|
|
183
|
+
_base_grid_sizes.sort()
|
|
184
|
+
print(f"Added {add} to construction grid.")
|
|
185
|
+
else:
|
|
186
|
+
print(f"{add} is already in the construction grid.")
|
|
187
|
+
if remove is not None:
|
|
188
|
+
if remove in _base_grid_sizes:
|
|
189
|
+
_base_grid_sizes.remove(float(remove))
|
|
190
|
+
print(f"Removed {remove} from construction grid.")
|
|
191
|
+
else:
|
|
192
|
+
print(f"{remove} is not in the construction grid.")
|
|
193
|
+
|
|
194
|
+
print(f"Current base construction grid: {_base_grid_sizes}")
|
|
195
|
+
return _base_grid_sizes
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def dimensiongen(avar: float = 0.10, mar: float = 1.5):
|
|
199
|
+
"""Calculates optimal dimensions for rooms missing them using the server."""
|
|
200
|
+
global _rooms, _base_grid_sizes, _server_url
|
|
201
|
+
|
|
202
|
+
payload = {
|
|
203
|
+
"rooms": [dataclasses.asdict(r) for r in _rooms],
|
|
204
|
+
"base_grid_sizes": _base_grid_sizes,
|
|
205
|
+
"area_variation": avar,
|
|
206
|
+
"max_aspect_ratio": mar
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
print(f"Requesting dimensions from server at {_server_url}...")
|
|
210
|
+
try:
|
|
211
|
+
response = requests.post(f"{_server_url}/dimensiongen", json=payload, timeout=30)
|
|
212
|
+
response.raise_for_status()
|
|
213
|
+
data = response.json()
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"Error connecting to RoomRubiks server: {e}")
|
|
216
|
+
print("Please check if the server is running and configured correctly.")
|
|
217
|
+
return {}
|
|
218
|
+
|
|
219
|
+
# Reconstruct rooms and update _rooms
|
|
220
|
+
updated_rooms = []
|
|
221
|
+
for rm_dict in data["rooms"]:
|
|
222
|
+
updated_rooms.append(deserialize_room(rm_dict))
|
|
223
|
+
_rooms = updated_rooms
|
|
224
|
+
|
|
225
|
+
saved_dimensions = data["saved_dimensions"]
|
|
226
|
+
print(f"Generated dimensions for {len(saved_dimensions)} rooms.")
|
|
227
|
+
print(f"Dimensions details: {saved_dimensions}")
|
|
228
|
+
return saved_dimensions
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def generatelayout(lvar: float = 0.5, sgap: float = 1.0, max_variations: int = 10, liked_layouts: Optional[List[Any]] = None):
|
|
232
|
+
"""Generates the architectural layouts using the server-side Elitist Genetic Algorithm."""
|
|
233
|
+
global _rooms, _connections, _site, _settings, _layout_variations, _server_url
|
|
234
|
+
print(f"Generating layout on server ({_server_url}) with location_variation={lvar}, allowed_space_gap={sgap}...")
|
|
235
|
+
|
|
236
|
+
from .utils.constraints import _global_constraints
|
|
237
|
+
|
|
238
|
+
payload = {
|
|
239
|
+
"rooms": [dataclasses.asdict(r) for r in _rooms],
|
|
240
|
+
"connections": [dataclasses.asdict(c) for c in _connections],
|
|
241
|
+
"constraints": _global_constraints,
|
|
242
|
+
"site": dataclasses.asdict(_site) if _site is not None else None,
|
|
243
|
+
"settings": _settings,
|
|
244
|
+
"location_variation": lvar,
|
|
245
|
+
"allowed_space_gap": sgap,
|
|
246
|
+
"max_variations": max_variations
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
response = requests.post(f"{_server_url}/generatelayout", json=payload, timeout=60)
|
|
251
|
+
response.raise_for_status()
|
|
252
|
+
data = response.json()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(f"Error connecting to RoomRubiks server: {e}")
|
|
255
|
+
print("Please check if the server is running and configured correctly.")
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
variations = []
|
|
259
|
+
for var_dict in data["variations"]:
|
|
260
|
+
var_rooms = [deserialize_room(rm_dict) for rm_dict in var_dict]
|
|
261
|
+
variations.append(var_rooms)
|
|
262
|
+
|
|
263
|
+
_layout_variations = variations
|
|
264
|
+
if not variations:
|
|
265
|
+
print("Failed to generate layouts.")
|
|
266
|
+
else:
|
|
267
|
+
print(f"Successfully generated {len(variations)} unique variations.")
|
|
268
|
+
return variations
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def showlayout(n: int = 1, label: Optional[List[str]] = None):
|
|
272
|
+
"""Shows a matplotlib plot of the n-th layout variation."""
|
|
273
|
+
global _layout_variations
|
|
274
|
+
if not _layout_variations or n < 1 or n > len(_layout_variations):
|
|
275
|
+
print(f"Variation {n} does not exist. Available variations: {len(_layout_variations)}")
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
layout = _layout_variations[n - 1]
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
import matplotlib.pyplot as plt
|
|
282
|
+
import matplotlib.patches as patches
|
|
283
|
+
except ImportError:
|
|
284
|
+
print("Please install matplotlib to use showlayout(): pip install matplotlib")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
fig, ax = plt.subplots()
|
|
288
|
+
|
|
289
|
+
global _site
|
|
290
|
+
if _site is not None and getattr(_site, 'points', None):
|
|
291
|
+
site_pts = [(pt['x'], pt['y']) for pt in _site.points]
|
|
292
|
+
poly = patches.Polygon(site_pts, closed=True, fill=False, edgecolor='red', linestyle='--', linewidth=2)
|
|
293
|
+
ax.add_patch(poly)
|
|
294
|
+
|
|
295
|
+
if label is None:
|
|
296
|
+
label = ["name"]
|
|
297
|
+
|
|
298
|
+
for r in layout:
|
|
299
|
+
is_corr = getattr(r, 'isCorridor', False)
|
|
300
|
+
color = '#e2e8f0' if is_corr else getattr(r, 'color', '#ffffff')
|
|
301
|
+
rect = patches.Rectangle((r.x, r.y), r.w, r.h, linewidth=1, edgecolor='black', facecolor=color, alpha=0.8)
|
|
302
|
+
ax.add_patch(rect)
|
|
303
|
+
if not is_corr:
|
|
304
|
+
label_parts = []
|
|
305
|
+
unit_str = "m" if _settings["unit"] == "m" else "ft"
|
|
306
|
+
sq_unit_str = "sq.m" if _settings["unit"] == "m" else "sq.ft"
|
|
307
|
+
|
|
308
|
+
if "name" in label:
|
|
309
|
+
label_parts.append(r.name or r.id)
|
|
310
|
+
if "id" in label:
|
|
311
|
+
label_parts.append(r.id)
|
|
312
|
+
if "dim" in label:
|
|
313
|
+
label_parts.append(f"{r.w}x{r.h}{unit_str}")
|
|
314
|
+
if "area" in label:
|
|
315
|
+
calc_area = round(r.w * r.h, 1)
|
|
316
|
+
label_parts.append(f"{calc_area} {sq_unit_str}")
|
|
317
|
+
|
|
318
|
+
text_str = "\n".join(label_parts)
|
|
319
|
+
ax.text(r.x + r.w/2, r.y + r.h/2, text_str, ha='center', va='center', fontsize=8)
|
|
320
|
+
|
|
321
|
+
ax.autoscale()
|
|
322
|
+
plt.gca().set_aspect('equal', adjustable='box')
|
|
323
|
+
plt.title(f"Layout Variation {n}")
|
|
324
|
+
plt.show(block=False)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def exportlayout(n: int = 1, filepath: str = "layout.json"):
|
|
328
|
+
"""Exports the n-th layout variation to JSON or DXF."""
|
|
329
|
+
global _layout_variations
|
|
330
|
+
if not _layout_variations or n < 1 or n > len(_layout_variations):
|
|
331
|
+
print(f"Variation {n} does not exist.")
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
layout = _layout_variations[n - 1]
|
|
335
|
+
|
|
336
|
+
if filepath.lower().endswith(".json"):
|
|
337
|
+
data = []
|
|
338
|
+
for r in layout:
|
|
339
|
+
data.append({
|
|
340
|
+
"id": r.id,
|
|
341
|
+
"name": getattr(r, 'name', ''),
|
|
342
|
+
"isCorridor": getattr(r, 'isCorridor', False),
|
|
343
|
+
"x": r.x,
|
|
344
|
+
"y": r.y,
|
|
345
|
+
"w": r.w,
|
|
346
|
+
"h": r.h
|
|
347
|
+
})
|
|
348
|
+
with open(filepath, 'w') as f:
|
|
349
|
+
json.dump(data, f, indent=2)
|
|
350
|
+
print(f"Exported variation {n} to {os.path.abspath(filepath)}")
|
|
351
|
+
elif filepath.lower().endswith(".dxf"):
|
|
352
|
+
try:
|
|
353
|
+
import ezdxf
|
|
354
|
+
except ImportError:
|
|
355
|
+
print("DXF export skipped (ezdxf not installed).")
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
doc = ezdxf.new('R2010')
|
|
359
|
+
msp = doc.modelspace()
|
|
360
|
+
|
|
361
|
+
for r in layout:
|
|
362
|
+
pts = [(r.x, r.y), (r.x + r.w, r.y), (r.x + r.w, r.y + r.h), (r.x, r.y + r.h)]
|
|
363
|
+
msp.add_lwpolyline(pts, close=True)
|
|
364
|
+
if not getattr(r, 'isCorridor', False):
|
|
365
|
+
msp.add_text(r.name or r.id, dxfattribs={'height': 0.2}).set_placement((r.x + r.w/2, r.y + r.h/2))
|
|
366
|
+
|
|
367
|
+
doc.saveas(filepath)
|
|
368
|
+
print(f"Exported variation {n} to {os.path.abspath(filepath)}")
|
|
369
|
+
else:
|
|
370
|
+
print("Unsupported file format. Please use .json or .dxf")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def wait_for_plots():
|
|
374
|
+
"""Blocks execution until all matplotlib windows are closed by the user."""
|
|
375
|
+
try:
|
|
376
|
+
import matplotlib.pyplot as plt
|
|
377
|
+
plt.show(block=True)
|
|
378
|
+
except ImportError:
|
|
379
|
+
pass
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
types.py
|
|
3
|
+
|
|
4
|
+
Defines all shared dataclasses (data models) used throughout the package.
|
|
5
|
+
These act as plain data containers — no methods or business logic lives here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Optional, Dict, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Window:
|
|
14
|
+
"""Represents a window element placed inside a room."""
|
|
15
|
+
id: str # Unique window identifier
|
|
16
|
+
worldX: float # X position in world coordinates (metres)
|
|
17
|
+
worldY: float # Y position in world coordinates (metres)
|
|
18
|
+
widthM: float # Width of the window opening (metres)
|
|
19
|
+
lengthM: float # Depth/thickness of the window (metres)
|
|
20
|
+
isVertical: bool # True = window sits on a vertical (N/S) wall
|
|
21
|
+
sillHeight: Optional[float] = None # Height of the window sill from floor level
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Door:
|
|
26
|
+
"""Represents a door element placed inside a room."""
|
|
27
|
+
id: str # Unique door identifier
|
|
28
|
+
worldX: float # X position in world coordinates (metres)
|
|
29
|
+
worldY: float # Y position in world coordinates (metres)
|
|
30
|
+
widthM: float # Clear opening width (metres)
|
|
31
|
+
lengthM: float # Door panel depth (metres)
|
|
32
|
+
isVertical: bool # True = door sits on a vertical (N/S) wall
|
|
33
|
+
isMain: Optional[bool] = False # True = main entrance door
|
|
34
|
+
isOpening: Optional[bool] = False # True = opening only (no panel drawn)
|
|
35
|
+
hingeX: Optional[float] = None # X coordinate of the hinge pivot
|
|
36
|
+
hingeY: Optional[float] = None # Y coordinate of the hinge pivot
|
|
37
|
+
swingDirX: Optional[float] = None # X component of the swing direction vector
|
|
38
|
+
swingDirY: Optional[float] = None # Y component of the swing direction vector
|
|
39
|
+
_candidates: Optional[List[Any]] = None # Internal: candidate placement positions
|
|
40
|
+
_candidateIdx: Optional[int] = None # Internal: chosen candidate index
|
|
41
|
+
doorCount: Optional[int] = None # Number of doors in a multi-leaf set
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Furniture:
|
|
46
|
+
"""Represents a furniture item placed inside a room."""
|
|
47
|
+
id: str # Unique furniture identifier
|
|
48
|
+
type: str # Category string e.g. "bed", "desk", "sofa"
|
|
49
|
+
worldX: float # X position in world coordinates (metres)
|
|
50
|
+
worldY: float # Y position in world coordinates (metres)
|
|
51
|
+
widthM: float # Width of the furniture piece (metres)
|
|
52
|
+
lengthM: float # Length of the furniture piece (metres)
|
|
53
|
+
rotation: float # Rotation angle in degrees (0 = facing right)
|
|
54
|
+
color: Optional[str] = None # Hex fill colour for rendering
|
|
55
|
+
mirrored: Optional[bool] = False # True = horizontally flipped
|
|
56
|
+
isResponsiveSet: Optional[bool] = False # True = part of a responsive furniture set
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Room:
|
|
61
|
+
"""
|
|
62
|
+
Core room model.
|
|
63
|
+
A room can either have explicit dimensions (w, h) or just an area —
|
|
64
|
+
dimensiongen() will resolve the latter into w/h automatically.
|
|
65
|
+
"""
|
|
66
|
+
id: str # Unique room identifier (user-defined)
|
|
67
|
+
name: str = "" # Human-readable room label
|
|
68
|
+
x: float = 0.0 # Initial X position hint (metres); refined by GA
|
|
69
|
+
y: float = 0.0 # Initial Y position hint (metres); refined by GA
|
|
70
|
+
w: Optional[float] = None # Width (metres); set by user or dimensiongen()
|
|
71
|
+
h: Optional[float] = None # Height (metres); set by user or dimensiongen()
|
|
72
|
+
area: Optional[float] = None # Target floor area in m²; triggers auto-dimensioning
|
|
73
|
+
color: str = "#ffffff" # Display fill colour
|
|
74
|
+
attached: bool = False # True = placed inside/onto a parent room
|
|
75
|
+
startSpace: bool = False # True = entry point; exactly one room must be True
|
|
76
|
+
attachedSpace: bool = False # True = user-declared dependent room (e.g. en-suite)
|
|
77
|
+
isCorridor: Optional[bool] = False # True = corridor strip (auto-created by layout engine)
|
|
78
|
+
wallPresent: Optional[bool] = True # False = room boundary is open (no wall drawn)
|
|
79
|
+
doors: Optional[List[Door]] = field(default_factory=list) # Doors on this room's walls
|
|
80
|
+
windows: Optional[List[Window]] = field(default_factory=list) # Windows on this room's walls
|
|
81
|
+
furniture: Optional[List[Furniture]] = field(default_factory=list) # Furniture inside the room
|
|
82
|
+
labelOffsetX: Optional[float] = None # Fine-tune label X offset for rendering
|
|
83
|
+
labelOffsetY: Optional[float] = None # Fine-tune label Y offset for rendering
|
|
84
|
+
levelId: Optional[str] = None # Floor/level this room belongs to
|
|
85
|
+
layerId: Optional[str] = None # Drawing layer identifier
|
|
86
|
+
nameFontSize: Optional[float] = None # Override name label font size
|
|
87
|
+
dimFontSize: Optional[float] = None # Override dimension label font size
|
|
88
|
+
areaFontSize: Optional[float] = None # Override area label font size
|
|
89
|
+
wfr: Optional[float] = None # Window-to-floor ratio (used in daylighting calc)
|
|
90
|
+
activityDbA: Optional[float] = None # Acoustic activity level in dBA
|
|
91
|
+
lightingTargetLux: Optional[float] = None # Daylighting target illuminance (lux)
|
|
92
|
+
lightingTargetHours: Optional[float] = None # Required daylighting hours per day
|
|
93
|
+
unconnectedHeight: Optional[float] = None # Ceiling height if disconnected from standard storey
|
|
94
|
+
topLevelId: Optional[str] = None # Top-most level if the room spans multiple levels
|
|
95
|
+
windowRegenCount: Optional[int] = None # Number of times windows have been regenerated
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class Connection:
|
|
100
|
+
"""
|
|
101
|
+
Represents an adjacency requirement between two rooms.
|
|
102
|
+
The layout engine tries to place roomA and roomB with a shared wall.
|
|
103
|
+
"""
|
|
104
|
+
roomA: str # ID of the first room
|
|
105
|
+
roomB: str # ID of the second room
|
|
106
|
+
id: Optional[str] = None # Auto-generated as "roomA_roomB" if not provided
|
|
107
|
+
|
|
108
|
+
def __post_init__(self):
|
|
109
|
+
# Auto-generate a stable connection ID if the user didn't supply one
|
|
110
|
+
if self.id is None:
|
|
111
|
+
self.id = f"{self.roomA}_{self.roomB}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class Site:
|
|
116
|
+
"""
|
|
117
|
+
Optional outer site boundary polygon.
|
|
118
|
+
If defined, the layout engine can attempt to fit the rooms within it.
|
|
119
|
+
"""
|
|
120
|
+
points: List[Dict[str, float]] # List of {x, y} dicts defining the polygon vertices
|
|
121
|
+
width: Optional[float] = None # Bounding width of the site (derived or explicit)
|
|
122
|
+
height: Optional[float] = None # Bounding height of the site (derived or explicit)
|
|
123
|
+
layerId: Optional[str] = None # Drawing layer for the site polygon
|
|
124
|
+
offsets: Optional[List[float]] = None # Setback distances [top, right, bottom, left]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class Road:
|
|
129
|
+
"""
|
|
130
|
+
Represents a road or access path adjacent to the site.
|
|
131
|
+
Used for orientation and entry point determination.
|
|
132
|
+
"""
|
|
133
|
+
id: str # Unique road identifier
|
|
134
|
+
points: List[Dict[str, float]] # Centreline polyline as list of {x, y}
|
|
135
|
+
width: float # Road width in metres
|
|
136
|
+
direction: Optional[str] = None # Cardinal direction of the road (N, S, E, W)
|
|
137
|
+
layerId: Optional[str] = None # Drawing layer identifier
|
|
138
|
+
isInternal: Optional[bool] = False # True = internal driveway or pedestrian path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# roomrubikspack utils
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import List, Dict, Any, Optional
|
|
2
|
+
|
|
3
|
+
_global_constraints: List[Dict[str, Any]] = []
|
|
4
|
+
|
|
5
|
+
def add_constraint(constraint_type: str, room_id: Optional[str] = None, value: Any = None):
|
|
6
|
+
"""
|
|
7
|
+
Appends a new constraint to the global constraint list.
|
|
8
|
+
Called internally by rr.constraint().
|
|
9
|
+
"""
|
|
10
|
+
_global_constraints.append({
|
|
11
|
+
"type": constraint_type, # One of: "position", "area", "perimeter"
|
|
12
|
+
"room_id": room_id, # Target room for positional constraints; None for global ones
|
|
13
|
+
"value": value # Meaning depends on type
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
def clear_constraints():
|
|
17
|
+
"""Resets the global constraint list. Called by rr.init() at session start."""
|
|
18
|
+
global _global_constraints
|
|
19
|
+
_global_constraints = []
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
utils/graph_utils.py
|
|
3
|
+
|
|
4
|
+
Graph utilities for the roomrubikspack layout engine.
|
|
5
|
+
|
|
6
|
+
Responsibilities:
|
|
7
|
+
- check_connectivity : verify all rooms form a single connected graph
|
|
8
|
+
- get_bfs_order : produce a Breadth-First Search traversal order for placement
|
|
9
|
+
- check_planarity : quick Euler formula check to warn about non-planar graphs
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
from typing import List, Dict, Optional, Set
|
|
14
|
+
from ..types import Connection
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_connectivity(room_ids: List[str], connections: List[Connection]) -> bool:
|
|
18
|
+
"""
|
|
19
|
+
Returns True if all rooms are reachable from the first room (i.e. the graph is connected).
|
|
20
|
+
A disconnected graph would mean some rooms can never be placed adjacent to the start.
|
|
21
|
+
"""
|
|
22
|
+
if len(room_ids) <= 1:
|
|
23
|
+
return True # A single room or empty list is trivially connected
|
|
24
|
+
|
|
25
|
+
# Build adjacency list from the connection list
|
|
26
|
+
adj: Dict[str, List[str]] = {r_id: [] for r_id in room_ids}
|
|
27
|
+
for conn in connections:
|
|
28
|
+
if conn.roomA in adj:
|
|
29
|
+
adj[conn.roomA].append(conn.roomB)
|
|
30
|
+
if conn.roomB in adj:
|
|
31
|
+
adj[conn.roomB].append(conn.roomA)
|
|
32
|
+
|
|
33
|
+
# Standard BFS from the first room
|
|
34
|
+
visited: Set[str] = set()
|
|
35
|
+
queue = [room_ids[0]]
|
|
36
|
+
visited.add(room_ids[0])
|
|
37
|
+
|
|
38
|
+
while queue:
|
|
39
|
+
u = queue.pop(0)
|
|
40
|
+
for v in adj.get(u, []):
|
|
41
|
+
if v not in visited:
|
|
42
|
+
visited.add(v)
|
|
43
|
+
queue.append(v)
|
|
44
|
+
|
|
45
|
+
# All rooms must be visited for the graph to be connected
|
|
46
|
+
return len(visited) == len(room_ids)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_bfs_order(
|
|
50
|
+
room_ids: List[str],
|
|
51
|
+
connections: List[Connection],
|
|
52
|
+
start_room_id: Optional[str] = None,
|
|
53
|
+
shuffle_list: bool = False
|
|
54
|
+
) -> List[str]:
|
|
55
|
+
"""
|
|
56
|
+
Returns the room IDs in Breadth-First Search order starting from start_room_id.
|
|
57
|
+
This order is used by the layout generator to place rooms one-by-one, ensuring
|
|
58
|
+
that each room's parent is already placed before the room itself is processed.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
room_ids : all room IDs to traverse
|
|
62
|
+
connections : adjacency edges between rooms
|
|
63
|
+
start_room_id : the room to begin traversal from (should be startSpace)
|
|
64
|
+
shuffle_list : if True, randomises neighbour order at each BFS step
|
|
65
|
+
(used by the GA to explore different layout topologies)
|
|
66
|
+
"""
|
|
67
|
+
if not room_ids:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
# Build undirected adjacency list restricted to provided room_ids
|
|
71
|
+
adj: Dict[str, List[str]] = {r_id: [] for r_id in room_ids}
|
|
72
|
+
for conn in connections:
|
|
73
|
+
if conn.roomA in adj:
|
|
74
|
+
adj[conn.roomA].append(conn.roomB)
|
|
75
|
+
if conn.roomB in adj:
|
|
76
|
+
adj[conn.roomB].append(conn.roomA)
|
|
77
|
+
|
|
78
|
+
order: List[str] = []
|
|
79
|
+
visited: Set[str] = set()
|
|
80
|
+
|
|
81
|
+
# Fall back to first room if start_room_id is not found
|
|
82
|
+
start_id = start_room_id if (start_room_id and start_room_id in room_ids) else room_ids[0]
|
|
83
|
+
|
|
84
|
+
queue = [start_id]
|
|
85
|
+
visited.add(start_id)
|
|
86
|
+
|
|
87
|
+
while queue:
|
|
88
|
+
u = queue.pop(0)
|
|
89
|
+
order.append(u)
|
|
90
|
+
|
|
91
|
+
neighbors = list(adj.get(u, []))
|
|
92
|
+
if shuffle_list:
|
|
93
|
+
random.shuffle(neighbors) # Random order = different layout exploration
|
|
94
|
+
else:
|
|
95
|
+
neighbors.sort() # Deterministic order for reproducibility
|
|
96
|
+
|
|
97
|
+
for v in neighbors:
|
|
98
|
+
if v not in visited:
|
|
99
|
+
visited.add(v)
|
|
100
|
+
queue.append(v)
|
|
101
|
+
|
|
102
|
+
# Handle disconnected components — append them after the main connected component
|
|
103
|
+
for r_id in room_ids:
|
|
104
|
+
if r_id not in visited:
|
|
105
|
+
sub_queue = [r_id]
|
|
106
|
+
visited.add(r_id)
|
|
107
|
+
while sub_queue:
|
|
108
|
+
u = sub_queue.pop(0)
|
|
109
|
+
order.append(u)
|
|
110
|
+
neighbors = list(adj.get(u, []))
|
|
111
|
+
if shuffle_list:
|
|
112
|
+
random.shuffle(neighbors)
|
|
113
|
+
else:
|
|
114
|
+
neighbors.sort()
|
|
115
|
+
for v in neighbors:
|
|
116
|
+
if v not in visited:
|
|
117
|
+
visited.add(v)
|
|
118
|
+
sub_queue.append(v)
|
|
119
|
+
|
|
120
|
+
return order
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def check_planarity(room_ids: List[str], connections: List[Connection]) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Quick necessary-condition check for graph planarity using Euler's formula.
|
|
126
|
+
For a planar graph: E ≤ 3V − 6 (where V = vertices, E = edges).
|
|
127
|
+
|
|
128
|
+
NOTE: This is a necessary but NOT sufficient condition. It will catch
|
|
129
|
+
obvious non-planar cases but not all of them. Use as a warning only.
|
|
130
|
+
|
|
131
|
+
Returns True if the graph MIGHT be planar, False if it is DEFINITELY non-planar.
|
|
132
|
+
"""
|
|
133
|
+
v = len(room_ids) # Number of vertices (rooms)
|
|
134
|
+
e = len(connections) # Number of edges (connections)
|
|
135
|
+
|
|
136
|
+
if v <= 2:
|
|
137
|
+
return True # Any graph with ≤2 vertices is always planar
|
|
138
|
+
|
|
139
|
+
# Euler's inequality: a planar graph cannot have more than 3V-6 edges
|
|
140
|
+
if e > 3 * v - 6:
|
|
141
|
+
return False # Definitely non-planar
|
|
142
|
+
|
|
143
|
+
return True # Likely planar (not guaranteed)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: roomrubikspack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Client wrapper for procedural architectural floorplan layout generator
|
|
5
|
+
Author-email: Vijesh Kumar V <your.email@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/yourusername/roomrubikspack
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/roomrubikspack/issues
|
|
9
|
+
Keywords: architecture,floorplan,layout,genetic-algorithm,CAD,procedural
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.25.0
|
|
14
|
+
Requires-Dist: matplotlib>=3.5.0
|
|
15
|
+
Requires-Dist: networkx>=2.6.0
|
|
16
|
+
Requires-Dist: ezdxf>=1.0.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# RoomRubiksPack (Client Library)
|
|
23
|
+
|
|
24
|
+
RoomRubiksPack is a lightweight Python package for generating architectural floorplan layouts using procedural generation and an Elitist Genetic Algorithm.
|
|
25
|
+
|
|
26
|
+
This client library maintains a local, stateful API for creating room models, local graphing/connectivity check, local plotting/visualisation via `matplotlib`, and local CAD exports via `ezdxf`. The computationally heavy layout generation (GA) and auto-dimensioning are offloaded to a RoomRubiks API Server.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### Option 1: Install from PyPI
|
|
33
|
+
Once published, install the package via:
|
|
34
|
+
```bash
|
|
35
|
+
pip install roomrubikspack
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Option 2: Install from GitHub
|
|
39
|
+
```bash
|
|
40
|
+
pip install git+https://github.com/yourusername/roomrubikspack.git
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Option 3: Local Installation (After Downloading)
|
|
44
|
+
Navigate to the directory containing `pyproject.toml` and run:
|
|
45
|
+
```bash
|
|
46
|
+
pip install .
|
|
47
|
+
```
|
|
48
|
+
For local developers who want to modify the source code, run in editable mode:
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Configuring the Server Connection
|
|
56
|
+
|
|
57
|
+
By default, the client library will look for your live API server running at `https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app`.
|
|
58
|
+
|
|
59
|
+
You can point to a different server endpoint in two ways:
|
|
60
|
+
|
|
61
|
+
### 1. In Python Code (Recommended)
|
|
62
|
+
Set the URL dynamically via `rr.settings()`:
|
|
63
|
+
```python
|
|
64
|
+
import roomrubikspack as rr
|
|
65
|
+
|
|
66
|
+
rr.init()
|
|
67
|
+
rr.settings(server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Via Environment Variable
|
|
71
|
+
Before running your script, set the `ROOMRUBIKSPACK_SERVER_URL` environment variable:
|
|
72
|
+
```bash
|
|
73
|
+
# Windows PowerShell
|
|
74
|
+
$env:ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
75
|
+
|
|
76
|
+
# Windows Command Prompt
|
|
77
|
+
set ROOMRUBIKSPACK_SERVER_URL=https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app
|
|
78
|
+
|
|
79
|
+
# Linux/macOS
|
|
80
|
+
export ROOMRUBIKSPACK_SERVER_URL="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
Here is a complete example. Make sure your local or remote RoomRubiks server is running before executing this script.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import roomrubikspack as rr
|
|
91
|
+
|
|
92
|
+
# Initialize session
|
|
93
|
+
rr.init()
|
|
94
|
+
|
|
95
|
+
# Configure the server (defaults to your live Cloud Run URL if omitted)
|
|
96
|
+
rr.settings(unit="m", server_url="https://roomrubikspack-0-1-0-private-942524616275.asia-south1.run.app")
|
|
97
|
+
|
|
98
|
+
# Define Rooms
|
|
99
|
+
rr.room("living", "Living Room", area=20.0, startSpace=True)
|
|
100
|
+
rr.room("kitchen", "Kitchen", w=3.0, h=3.0)
|
|
101
|
+
rr.room("bed1", "Master Bed", area=16.0)
|
|
102
|
+
rr.room("bath1", "Attached Bath", area=4.0, attachedSpace=True)
|
|
103
|
+
|
|
104
|
+
# Define site boundary
|
|
105
|
+
rr.site([{"x": 0, "y": 0}, {"x": 20, "y": 0}, {"x": 20, "y": 20}, {"x": 0, "y": 20}])
|
|
106
|
+
|
|
107
|
+
# Define connectivity
|
|
108
|
+
rr.connectivity(
|
|
109
|
+
("living", "kitchen"),
|
|
110
|
+
("living", "bed1"),
|
|
111
|
+
("bed1", "bath1")
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Optional: Draw connectivity graph locally
|
|
115
|
+
rr.connectivityshow()
|
|
116
|
+
|
|
117
|
+
# Add constraints to guide layout generation
|
|
118
|
+
rr.constraint("position", "bed1", "N")
|
|
119
|
+
rr.constraint("area", None, 120)
|
|
120
|
+
rr.constraint("perimeter", None, "minimize")
|
|
121
|
+
|
|
122
|
+
# Generate sizes for rooms missing width/height
|
|
123
|
+
rr.dimensiongen()
|
|
124
|
+
|
|
125
|
+
# Generate layout variations (sent to the server engine)
|
|
126
|
+
rr.generatelayout()
|
|
127
|
+
|
|
128
|
+
# View first variation locally
|
|
129
|
+
rr.showlayout(n=1, label=["name", "dim", "area"])
|
|
130
|
+
|
|
131
|
+
# Export layout to DXF locally
|
|
132
|
+
rr.exportlayout(n=1, filepath="output_layout.dxf")
|
|
133
|
+
|
|
134
|
+
# Blocks execution until plots are closed
|
|
135
|
+
rr.wait_for_plots()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## API Reference
|
|
141
|
+
|
|
142
|
+
- `rr.init()`: Clears all current session state.
|
|
143
|
+
- `rr.settings(unit, server_url)`: Set global measurement units (`'m'` or `'f'`) and configure the solver backend API endpoint.
|
|
144
|
+
- `rr.constructiongrid(add, remove, reset)`: View or manipulate the base construction grid sizes locally.
|
|
145
|
+
- `rr.room(id, name, w, h, area, startSpace, attachedSpace, ...)`: Register a room.
|
|
146
|
+
- `rr.site(points)`: Set an optional site boundary polygon.
|
|
147
|
+
- `rr.connectivity(*pairs)`: Define room connections. Planarity check runs instantly on the client.
|
|
148
|
+
- `rr.connectivityshow()`: Opens a Matplotlib window showing the adjacency graph.
|
|
149
|
+
- `rr.constraint(type, room_id, value)`: Registers a layout constraint.
|
|
150
|
+
- `rr.dimensiongen(avar, mar)`: Requests standard room dimensions from the server.
|
|
151
|
+
- `rr.generatelayout(lvar, sgap, max_variations)`: Sends session state to the server to run the GA layout engine.
|
|
152
|
+
- `rr.showlayout(n, label)`: Plots the `n`-th generated variation using Matplotlib.
|
|
153
|
+
- `rr.exportlayout(n, filepath)`: Saves the `n`-th layout to JSON or DXF.
|
|
154
|
+
- `rr.wait_for_plots()`: Helper to keep visual plots open.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/roomrubikspack/__init__.py
|
|
5
|
+
src/roomrubikspack/types.py
|
|
6
|
+
src/roomrubikspack.egg-info/PKG-INFO
|
|
7
|
+
src/roomrubikspack.egg-info/SOURCES.txt
|
|
8
|
+
src/roomrubikspack.egg-info/dependency_links.txt
|
|
9
|
+
src/roomrubikspack.egg-info/requires.txt
|
|
10
|
+
src/roomrubikspack.egg-info/top_level.txt
|
|
11
|
+
src/roomrubikspack/utils/__init__.py
|
|
12
|
+
src/roomrubikspack/utils/constraints.py
|
|
13
|
+
src/roomrubikspack/utils/graph_utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
roomrubikspack
|