Karthik8nitt commited on
Commit
c9fa3b2
·
verified ·
1 Parent(s): 8acc209

Add floorplan SVG/PNG renderer

Browse files
Files changed (1) hide show
  1. render_floorplan.py +170 -0
render_floorplan.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Render a floorplan JSON as an SVG or PNG image.
3
+ Supports: plot boundary, buildable boundary, rooms (color-coded by type),
4
+ doors, windows, dimension labels, north arrow.
5
+ Usage:
6
+ python render_floorplan.py --input floorplan.json --output floorplan.svg
7
+ python render_floorplan.py --input floorplan.json --output floorplan.png
8
+ """
9
+ import json
10
+ import argparse
11
+ from typing import List, Dict, Any, Tuple
12
+
13
+ ROOM_COLORS = {
14
+ "living": "#E8F4FD", "bedroom": "#FFF3E0", "master_bedroom": "#FFE0B2",
15
+ "kitchen": "#F3E5F5", "dining": "#E0F2F1", "toilet": "#FFEBEE",
16
+ "bathroom": "#FFEBEE", "pooja": "#FFFDE7", "study": "#E8F5E9",
17
+ "balcony": "#F5F5F5", "parking": "#ECEFF1", "staircase": "#FBE9E7",
18
+ "corridor": "#ECEFF1", "utility": "#ECEFF1", "store": "#ECEFF1",
19
+ }
20
+ ROOM_STROKES = {
21
+ "living": "#1976D2", "bedroom": "#F57C00", "master_bedroom": "#E65100",
22
+ "kitchen": "#7B1FA2", "dining": "#00796B", "toilet": "#C62828",
23
+ "bathroom": "#C62828", "pooja": "#F9A825", "study": "#388E3C",
24
+ "balcony": "#616161", "parking": "#455A64", "staircase": "#D84315",
25
+ "corridor": "#78909C", "utility": "#78909C", "store": "#78909C",
26
+ }
27
+
28
+ def polygon_bbox(poly):
29
+ xs = [p[0] for p in poly]; ys = [p[1] for p in poly]
30
+ return min(xs), min(ys), max(xs), max(ys)
31
+
32
+ def polygons_bbox(polys):
33
+ all_x, all_y = [], []
34
+ for poly in polys:
35
+ for p in poly: all_x.append(p[0]); all_y.append(p[1])
36
+ return min(all_x), min(all_y), max(all_x), max(all_y)
37
+
38
+ def render_floorplan_svg(floorplan, width=1200, padding=80, show_dimensions=True, show_labels=True):
39
+ plot = floorplan.get("plot", {})
40
+ rooms = floorplan.get("rooms", [])
41
+ doors = floorplan.get("doors", [])
42
+ windows = floorplan.get("windows", [])
43
+
44
+ all_polys = []
45
+ if plot.get("outer_boundary"): all_polys.append(plot["outer_boundary"])
46
+ if plot.get("buildable_boundary"): all_polys.append(plot["buildable_boundary"])
47
+ for room in rooms:
48
+ if room.get("polygon"): all_polys.append(room["polygon"])
49
+
50
+ if not all_polys:
51
+ return '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
52
+
53
+ minx, miny, maxx, maxy = polygons_bbox(all_polys)
54
+ plot_w, plot_h = maxx - minx, maxy - miny
55
+ scale = (width - 2 * padding) / max(plot_w, plot_h)
56
+ svg_h = int(plot_h * scale + 2 * padding)
57
+ svg_w = int(plot_w * scale + 2 * padding)
58
+
59
+ def tx(x): return (x - minx) * scale + padding
60
+ def ty(y): return svg_h - ((y - miny) * scale + padding)
61
+ def tpts(poly): return " ".join(f"{tx(p[0])},{ty(p[1])}" for p in poly)
62
+
63
+ lines = []
64
+ lines.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="{svg_w}" height="{svg_h}" viewBox="0 0 {svg_w} {svg_h}">')
65
+ lines.append(f'<rect width="{svg_w}" height="{svg_h}" fill="white"/>')
66
+
67
+ project_name = floorplan.get("project_name", "Floor Plan")
68
+ lines.append(f'<text x="{svg_w//2}" y="30" text-anchor="middle" font-family="Arial,sans-serif" font-size="18" font-weight="bold">{project_name}</text>')
69
+
70
+ if plot.get("outer_boundary"):
71
+ lines.append(f'<polygon points="{tpts(plot["outer_boundary"])}" fill="none" stroke="#333" stroke-width="3"/>')
72
+ if plot.get("buildable_boundary"):
73
+ lines.append(f'<polygon points="{tpts(plot["buildable_boundary"])}" fill="none" stroke="#666" stroke-width="2" stroke-dasharray="8,4"/>')
74
+
75
+ for room in rooms:
76
+ poly = room.get("polygon", [])
77
+ if not poly: continue
78
+ rtype = room.get("type", "unknown")
79
+ fill = ROOM_COLORS.get(rtype, "#F5F5F5")
80
+ stroke = ROOM_STROKES.get(rtype, "#333")
81
+ lines.append(f'<polygon points="{tpts(poly)}" fill="{fill}" stroke="{stroke}" stroke-width="1.5"/>')
82
+
83
+ if show_labels:
84
+ rminx, rminy, rmaxx, rmaxy = polygon_bbox(poly)
85
+ cx, cy = tx((rminx + rmaxx) / 2), ty((rminy + rmaxy) / 2)
86
+ name = room.get("name", rtype)
87
+ area = room.get("area_sqm", "")
88
+ label = f"{name}"
89
+ if area: label += f" ({area}m\u00b2)"
90
+ lines.append(f'<text x="{cx}" y="{cy+4}" text-anchor="middle" font-family="Arial,sans-serif" font-size="10" fill="#333">{label}</text>')
91
+
92
+ for door in doors:
93
+ pos = door.get("position", [0, 0])
94
+ dw = door.get("width", 0.9) * scale
95
+ orient = door.get("orientation", "horizontal")
96
+ x, y = tx(pos[0]), ty(pos[1])
97
+ if orient == "horizontal":
98
+ lines.append(f'<line x1="{x-dw/2}" y1="{y}" x2="{x+dw/2}" y2="{y}" stroke="#2E7D32" stroke-width="4"/>')
99
+ else:
100
+ lines.append(f'<line x1="{x}" y1="{y-dw/2}" x2="{x}" y2="{y+dw/2}" stroke="#2E7D32" stroke-width="4"/>')
101
+
102
+ for win in windows:
103
+ pos = win.get("position", [0, 0])
104
+ ww = win.get("width", 1.2) * scale
105
+ orient = win.get("orientation", "horizontal")
106
+ x, y = tx(pos[0]), ty(pos[1])
107
+ if orient == "horizontal":
108
+ lines.append(f'<line x1="{x-ww/2}" y1="{y}" x2="{x+ww/2}" y2="{y}" stroke="#1565C0" stroke-width="3" stroke-dasharray="4,2"/>')
109
+ else:
110
+ lines.append(f'<line x1="{x}" y1="{y-ww/2}" x2="{x}" y2="{y+ww/2}" stroke="#1565C0" stroke-width="3" stroke-dasharray="4,2"/>')
111
+
112
+ # North arrow
113
+ arrow_x, arrow_y = svg_w - 60, 60
114
+ lines.append(f'<g transform="translate({arrow_x}, {arrow_y})">')
115
+ lines.append('<polygon points="0,-20 -6,10 6,10" fill="#D32F2F"/>')
116
+ lines.append(f'<text x="0" y="25" text-anchor="middle" font-family="Arial,sans-serif" font-size="12" font-weight="bold">N</text>')
117
+ lines.append('</g>')
118
+
119
+ # Legend
120
+ legend_x, legend_y = 20, svg_h - 20 - len(ROOM_COLORS) * 18
121
+ lines.append(f'<rect x="{legend_x-5}" y="{legend_y-15}" width="150" height="{len(ROOM_COLORS)*18+25}" fill="white" stroke="#ccc" stroke-width="1"/>')
122
+ lines.append(f'<text x="{legend_x}" y="{legend_y}" font-family="Arial,sans-serif" font-size="11" font-weight="bold">Legend</text>')
123
+ for i, (rtype, color) in enumerate(ROOM_COLORS.items()):
124
+ ly = legend_y + 15 + i * 16
125
+ lines.append(f'<rect x="{legend_x}" y="{ly-8}" width="12" height="12" fill="{color}" stroke="#666" stroke-width="0.5"/>')
126
+ lines.append(f'<text x="{legend_x+18}" y="{ly+2}" font-family="Arial,sans-serif" font-size="10">{rtype.replace("_", " ").title()}</text>')
127
+
128
+ dims = floorplan.get("dimensions", {})
129
+ meta_text = f"Built-up: {dims.get('total_built_up_area_sqm', 'N/A')}m\u00b2 | Carpet: {dims.get('total_carpet_area_sqm', 'N/A')}m\u00b2"
130
+ lines.append(f'<text x="{svg_w//2}" y="{svg_h-10}" text-anchor="middle" font-family="Arial,sans-serif" font-size="11" fill="#666">{meta_text}</text>')
131
+ lines.append('</svg>')
132
+ return "\n".join(lines)
133
+
134
+ def render_floorplan_png(floorplan, output_path, width=1200):
135
+ try:
136
+ import cairosvg
137
+ except ImportError:
138
+ raise ImportError("cairosvg required for PNG. Install: pip install cairosvg")
139
+ svg_content = render_floorplan_svg(floorplan, width=width)
140
+ cairosvg.svg2png(bytestring=svg_content.encode('utf-8'), write_to=output_path,
141
+ output_width=width, output_height=int(width * 0.75))
142
+ print(f"PNG saved to {output_path}")
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(description="Render a floorplan JSON to SVG/PNG")
146
+ parser.add_argument("--input", type=str, required=True, help="Path to floorplan JSON")
147
+ parser.add_argument("--output", type=str, required=True, help="Output path (.svg or .png)")
148
+ parser.add_argument("--width", type=int, default=1200)
149
+ parser.add_argument("--no-labels", action="store_true")
150
+ parser.add_argument("--no-dimensions", action="store_true")
151
+ args = parser.parse_args()
152
+
153
+ with open(args.input) as f:
154
+ floorplan = json.load(f)
155
+
156
+ show_labels = not args.no_labels
157
+ show_dimensions = not args.no_dimensions
158
+
159
+ if args.output.lower().endswith(".svg"):
160
+ svg = render_floorplan_svg(floorplan, width=args.width, show_labels=show_labels, show_dimensions=show_dimensions)
161
+ with open(args.output, "w") as f:
162
+ f.write(svg)
163
+ print(f"SVG saved to {args.output}")
164
+ elif args.output.lower().endswith(".png"):
165
+ render_floorplan_png(floorplan, args.output, width=args.width)
166
+ else:
167
+ print("Error: output must be .svg or .png")
168
+
169
+ if __name__ == "__main__":
170
+ main()