""" Render a floorplan JSON as an SVG or PNG image. Supports: plot boundary, buildable boundary, rooms (color-coded by type), doors, windows, dimension labels, north arrow. Usage: python render_floorplan.py --input floorplan.json --output floorplan.svg python render_floorplan.py --input floorplan.json --output floorplan.png """ import json import argparse from typing import List, Dict, Any, Tuple ROOM_COLORS = { "living": "#E8F4FD", "bedroom": "#FFF3E0", "master_bedroom": "#FFE0B2", "kitchen": "#F3E5F5", "dining": "#E0F2F1", "toilet": "#FFEBEE", "bathroom": "#FFEBEE", "pooja": "#FFFDE7", "study": "#E8F5E9", "balcony": "#F5F5F5", "parking": "#ECEFF1", "staircase": "#FBE9E7", "corridor": "#ECEFF1", "utility": "#ECEFF1", "store": "#ECEFF1", } ROOM_STROKES = { "living": "#1976D2", "bedroom": "#F57C00", "master_bedroom": "#E65100", "kitchen": "#7B1FA2", "dining": "#00796B", "toilet": "#C62828", "bathroom": "#C62828", "pooja": "#F9A825", "study": "#388E3C", "balcony": "#616161", "parking": "#455A64", "staircase": "#D84315", "corridor": "#78909C", "utility": "#78909C", "store": "#78909C", } def polygon_bbox(poly): xs = [p[0] for p in poly]; ys = [p[1] for p in poly] return min(xs), min(ys), max(xs), max(ys) def polygons_bbox(polys): all_x, all_y = [], [] for poly in polys: for p in poly: all_x.append(p[0]); all_y.append(p[1]) return min(all_x), min(all_y), max(all_x), max(all_y) def render_floorplan_svg(floorplan, width=1200, padding=80, show_dimensions=True, show_labels=True): plot = floorplan.get("plot", {}) rooms = floorplan.get("rooms", []) doors = floorplan.get("doors", []) windows = floorplan.get("windows", []) all_polys = [] if plot.get("outer_boundary"): all_polys.append(plot["outer_boundary"]) if plot.get("buildable_boundary"): all_polys.append(plot["buildable_boundary"]) for room in rooms: if room.get("polygon"): all_polys.append(room["polygon"]) if not all_polys: return '' minx, miny, maxx, maxy = polygons_bbox(all_polys) plot_w, plot_h = maxx - minx, maxy - miny scale = (width - 2 * padding) / max(plot_w, plot_h) svg_h = int(plot_h * scale + 2 * padding) svg_w = int(plot_w * scale + 2 * padding) def tx(x): return (x - minx) * scale + padding def ty(y): return svg_h - ((y - miny) * scale + padding) def tpts(poly): return " ".join(f"{tx(p[0])},{ty(p[1])}" for p in poly) lines = [] lines.append(f'') lines.append(f'') project_name = floorplan.get("project_name", "Floor Plan") lines.append(f'{project_name}') if plot.get("outer_boundary"): lines.append(f'') if plot.get("buildable_boundary"): lines.append(f'') for room in rooms: poly = room.get("polygon", []) if not poly: continue rtype = room.get("type", "unknown") fill = ROOM_COLORS.get(rtype, "#F5F5F5") stroke = ROOM_STROKES.get(rtype, "#333") lines.append(f'') if show_labels: rminx, rminy, rmaxx, rmaxy = polygon_bbox(poly) cx, cy = tx((rminx + rmaxx) / 2), ty((rminy + rmaxy) / 2) name = room.get("name", rtype) area = room.get("area_sqm", "") label = f"{name}" if area: label += f" ({area}m\u00b2)" lines.append(f'{label}') for door in doors: pos = door.get("position", [0, 0]) dw = door.get("width", 0.9) * scale orient = door.get("orientation", "horizontal") x, y = tx(pos[0]), ty(pos[1]) if orient == "horizontal": lines.append(f'') else: lines.append(f'') for win in windows: pos = win.get("position", [0, 0]) ww = win.get("width", 1.2) * scale orient = win.get("orientation", "horizontal") x, y = tx(pos[0]), ty(pos[1]) if orient == "horizontal": lines.append(f'') else: lines.append(f'') # North arrow arrow_x, arrow_y = svg_w - 60, 60 lines.append(f'') lines.append('') lines.append(f'N') lines.append('') # Legend legend_x, legend_y = 20, svg_h - 20 - len(ROOM_COLORS) * 18 lines.append(f'') lines.append(f'Legend') for i, (rtype, color) in enumerate(ROOM_COLORS.items()): ly = legend_y + 15 + i * 16 lines.append(f'') lines.append(f'{rtype.replace("_", " ").title()}') dims = floorplan.get("dimensions", {}) 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" lines.append(f'{meta_text}') lines.append('') return "\n".join(lines) def render_floorplan_png(floorplan, output_path, width=1200): try: import cairosvg except ImportError: raise ImportError("cairosvg required for PNG. Install: pip install cairosvg") svg_content = render_floorplan_svg(floorplan, width=width) cairosvg.svg2png(bytestring=svg_content.encode('utf-8'), write_to=output_path, output_width=width, output_height=int(width * 0.75)) print(f"PNG saved to {output_path}") def main(): parser = argparse.ArgumentParser(description="Render a floorplan JSON to SVG/PNG") parser.add_argument("--input", type=str, required=True, help="Path to floorplan JSON") parser.add_argument("--output", type=str, required=True, help="Output path (.svg or .png)") parser.add_argument("--width", type=int, default=1200) parser.add_argument("--no-labels", action="store_true") parser.add_argument("--no-dimensions", action="store_true") args = parser.parse_args() with open(args.input) as f: floorplan = json.load(f) show_labels = not args.no_labels show_dimensions = not args.no_dimensions if args.output.lower().endswith(".svg"): svg = render_floorplan_svg(floorplan, width=args.width, show_labels=show_labels, show_dimensions=show_dimensions) with open(args.output, "w") as f: f.write(svg) print(f"SVG saved to {args.output}") elif args.output.lower().endswith(".png"): render_floorplan_png(floorplan, args.output, width=args.width) else: print("Error: output must be .svg or .png") if __name__ == "__main__": main()