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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,8 @@
1
+ requests>=2.25.0
2
+ matplotlib>=3.5.0
3
+ networkx>=2.6.0
4
+ ezdxf>=1.0.0
5
+
6
+ [dev]
7
+ pytest>=7.0
8
+ pytest-cov>=4.0
@@ -0,0 +1 @@
1
+ roomrubikspack