Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import ezdxf | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| import io | |
| import tempfile | |
| import json | |
| from pathlib import Path | |
| from PIL import Image | |
| from datetime import date | |
| import copy | |
| import math | |
| import random | |
| # ============================================================ | |
| # ۰. تنظیمات ثابت | |
| # ============================================================ | |
| PAPER_SIZES = { | |
| "A4_portrait": (210, 297), | |
| "A4_landscape": (297, 210), | |
| "A3_portrait": (297, 420), | |
| "A3_landscape": (420, 297), | |
| "A2_portrait": (420, 594), | |
| "A2_landscape": (594, 420), | |
| "A1_portrait": (594, 841), | |
| "A1_landscape": (841, 594), | |
| "A0_portrait": (841, 1189), | |
| "A0_landscape": (1189, 841), | |
| } | |
| # ============================================================ | |
| # ۱. Title Block Manager | |
| # ============================================================ | |
| class TitleBlockManager: | |
| LEVEL_CONFIGS = { | |
| 1: { | |
| "name": "BASIC (دانشجویی)", | |
| "description": "مناسب تمرینهای کلاسی و اسکیس", | |
| "fields": [ | |
| {"id": "project_name", "label": "نام پروژه", "width_ratio": 1.0}, | |
| {"id": "drawing_title", "label": "عنوان نقشه", "width_ratio": 1.0}, | |
| {"id": "scale", "label": "مقیاس", "width_ratio": 0.5}, | |
| {"id": "date", "label": "تاریخ", "width_ratio": 0.5}, | |
| {"id": "designer", "label": "طراح", "width_ratio": 1.0}, | |
| ] | |
| }, | |
| 2: { | |
| "name": "STANDARD (مسکونی ۱-۲ طبقه)", | |
| "description": "مناسب ساختمانهای کوچک و شخصی", | |
| "fields": [ | |
| {"id": "project_name", "label": "نام پروژه", "width_ratio": 1.0}, | |
| {"id": "client", "label": "کارفرما", "width_ratio": 0.5}, | |
| {"id": "drawing_title", "label": "عنوان نقشه", "width_ratio": 0.5}, | |
| {"id": "drawing_code", "label": "کد نقشه", "width_ratio": 0.5}, | |
| {"id": "sheet_number", "label": "شماره شیت", "width_ratio": 0.5}, | |
| {"id": "scale", "label": "مقیاس", "width_ratio": 0.5}, | |
| {"id": "date", "label": "تاریخ", "width_ratio": 0.5}, | |
| {"id": "designer", "label": "طراح", "width_ratio": 1.0}, | |
| ] | |
| }, | |
| 3: { | |
| "name": "PROFESSIONAL (ساختمان تیپ ۳)", | |
| "description": "مناسب ساختمانهای ۳-۶ طبقه تجاری-مسکونی", | |
| "fields": [ | |
| {"id": "project_name", "label": "نام پروژه", "width_ratio": 1.0}, | |
| {"id": "client", "label": "کارفرما", "width_ratio": 0.5}, | |
| {"id": "project_address", "label": "آدرس پروژه", "width_ratio": 0.5}, | |
| {"id": "drawing_title", "label": "عنوان نقشه", "width_ratio": 1.0}, | |
| {"id": "drawing_code", "label": "کد نقشه", "width_ratio": 0.5}, | |
| {"id": "sheet_number", "label": "شماره شیت", "width_ratio": 0.5}, | |
| {"id": "scale", "label": "مقیاس", "width_ratio": 0.5}, | |
| {"id": "date", "label": "تاریخ", "width_ratio": 0.5}, | |
| {"id": "phase", "label": "فاز طراحی", "width_ratio": 0.5}, | |
| {"id": "revision", "label": "بازبینی", "width_ratio": 0.5}, | |
| {"id": "designer", "label": "طراح", "width_ratio": 0.5}, | |
| {"id": "supervisor", "label": "ناظر", "width_ratio": 0.5}, | |
| ] | |
| }, | |
| 4: { | |
| "name": "ENTERPRISE (پروژه بزرگ)", | |
| "description": "مناسب برجها و مجتمعهای بزرگ", | |
| "fields": [ | |
| {"id": "project_name", "label": "نام پروژه", "width_ratio": 1.0}, | |
| {"id": "client", "label": "کارفرما", "width_ratio": 0.5}, | |
| {"id": "consultant", "label": "مهندس مشاور", "width_ratio": 0.5}, | |
| {"id": "project_address", "label": "آدرس پروژه", "width_ratio": 1.0}, | |
| {"id": "drawing_title", "label": "عنوان نقشه", "width_ratio": 1.0}, | |
| {"id": "drawing_code", "label": "کد نقشه", "width_ratio": 0.5}, | |
| {"id": "sheet_number", "label": "شماره شیت", "width_ratio": 0.5}, | |
| {"id": "scale", "label": "مقیاس", "width_ratio": 0.5}, | |
| {"id": "date", "label": "تاریخ", "width_ratio": 0.5}, | |
| {"id": "phase", "label": "فاز طراحی", "width_ratio": 0.5}, | |
| {"id": "revision", "label": "بازبینی", "width_ratio": 0.5}, | |
| {"id": "permit_number", "label": "شماره پروانه", "width_ratio": 0.5}, | |
| {"id": "municipality", "label": "شهرداری منطقه", "width_ratio": 0.5}, | |
| {"id": "designer", "label": "طراح", "width_ratio": 0.5}, | |
| {"id": "supervisor", "label": "ناظر", "width_ratio": 0.5}, | |
| {"id": "owner_signature", "label": "امضای مالک", "width_ratio": 0.5}, | |
| ] | |
| }, | |
| 5: { | |
| "name": "GOVERNMENT (دولتی/مناقصات)", | |
| "description": "مناسب پروژههای دولتی و بیمارستانها", | |
| "fields": [ | |
| {"id": "project_name", "label": "نام پروژه", "width_ratio": 1.0}, | |
| {"id": "client", "label": "کارفرما", "width_ratio": 0.5}, | |
| {"id": "government_body", "label": "سازمان مربوطه", "width_ratio": 0.5}, | |
| {"id": "consultant", "label": "مهندس مشاور", "width_ratio": 0.5}, | |
| {"id": "contractor", "label": "پیمانکار", "width_ratio": 0.5}, | |
| {"id": "project_address", "label": "آدرس پروژه", "width_ratio": 1.0}, | |
| {"id": "drawing_title", "label": "عنوان نقشه", "width_ratio": 1.0}, | |
| {"id": "drawing_code", "label": "کد نقشه", "width_ratio": 0.5}, | |
| {"id": "sheet_number", "label": "شماره شیت", "width_ratio": 0.5}, | |
| {"id": "scale", "label": "مقیاس", "width_ratio": 0.5}, | |
| {"id": "date", "label": "تاریخ", "width_ratio": 0.5}, | |
| {"id": "phase", "label": "فاز طراحی", "width_ratio": 0.5}, | |
| {"id": "revision", "label": "بازبینی", "width_ratio": 0.5}, | |
| {"id": "permit_number", "label": "شماره پروانه", "width_ratio": 0.5}, | |
| {"id": "tender_number", "label": "شماره مناقصه", "width_ratio": 0.5}, | |
| {"id": "contract_number", "label": "شماره قرارداد", "width_ratio": 0.5}, | |
| {"id": "municipality", "label": "شهرداری منطقه", "width_ratio": 0.5}, | |
| {"id": "designer", "label": "طراح", "width_ratio": 0.5}, | |
| {"id": "supervisor", "label": "ناظر", "width_ratio": 0.5}, | |
| {"id": "owner_signature", "label": "امضای مالک", "width_ratio": 0.5}, | |
| {"id": "government_stamp", "label": "مهر سازمان", "width_ratio": 0.5}, | |
| ] | |
| } | |
| } | |
| def __init__(self, level=3, paper_size="A3_landscape"): | |
| self.level = level | |
| self.config = self.LEVEL_CONFIGS.get(level, self.LEVEL_CONFIGS[3]) | |
| self.fields = self.config["fields"] | |
| self.paper_size = paper_size | |
| self.paper_width, self.paper_height = PAPER_SIZES.get(paper_size, (420, 297)) | |
| def create_title_block(self, msp, project_info, sheet_info): | |
| if "TITLE_BLOCK" not in [l.dxf.name for l in msp.doc.layers]: | |
| msp.doc.layers.add(name="TITLE_BLOCK", color=5) | |
| msp.doc.layers.add(name="TITLE_LINES", color=15) | |
| msp.doc.layers.add(name="TITLE_TEXT", color=7) | |
| margin = 10 | |
| inner_w = self.paper_width - 2 * margin | |
| inner_h = self.paper_height - 2 * margin | |
| msp.add_line((margin, margin), (self.paper_width - margin, margin), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((self.paper_width - margin, margin), (self.paper_width - margin, self.paper_height - margin), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((self.paper_width - margin, self.paper_height - margin), (margin, self.paper_height - margin), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((margin, self.paper_height - margin), (margin, margin), dxfattribs={'layer': 'TITLE_LINES'}) | |
| tb_width = min(180, inner_w * 0.4) | |
| tb_height = min(60, inner_h * 0.25) | |
| tb_x = self.paper_width - margin - tb_width | |
| tb_y = margin | |
| msp.add_line((tb_x, tb_y), (tb_x + tb_width, tb_y), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((tb_x + tb_width, tb_y), (tb_x + tb_width, tb_y + tb_height), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((tb_x + tb_width, tb_y + tb_height), (tb_x, tb_y + tb_height), dxfattribs={'layer': 'TITLE_LINES'}) | |
| msp.add_line((tb_x, tb_y + tb_height), (tb_x, tb_y), dxfattribs={'layer': 'TITLE_LINES'}) | |
| num_fields = len(self.fields) | |
| num_rows = (num_fields + 1) // 2 | |
| row_height = tb_height / num_rows | |
| for i in range(1, num_rows): | |
| y = tb_y + i * row_height | |
| msp.add_line((tb_x, y), (tb_x + tb_width, y), dxfattribs={'layer': 'TITLE_LINES'}) | |
| mid_x = tb_x + tb_width * 0.5 | |
| msp.add_line((mid_x, tb_y), (mid_x, tb_y + tb_height), dxfattribs={'layer': 'TITLE_LINES'}) | |
| merged_info = {**project_info, **sheet_info} | |
| for i, field in enumerate(self.fields): | |
| row = i // 2 | |
| col = i % 2 | |
| x_label = tb_x + 2 + col * (tb_width / 2) | |
| x_value = x_label + 25 | |
| y = tb_y + tb_height - (row + 1) * row_height + row_height * 0.7 | |
| msp.add_text(field["label"], dxfattribs={'layer': 'TITLE_TEXT', 'height': 2.0, 'color': 5}).set_placement((x_label, y)) | |
| value = merged_info.get(field["id"], "___") | |
| msp.add_text(str(value), dxfattribs={'layer': 'TITLE_TEXT', 'height': 2.5, 'color': 7}).set_placement((x_value, y - 3)) | |
| return True | |
| # ============================================================ | |
| # ۲. Sheet Series Generator | |
| # ============================================================ | |
| class SheetSeriesGenerator: | |
| SHEET_TEMPLATES = { | |
| "A_COVER": { | |
| "title": "مدارک عمومی", | |
| "sheets": [ | |
| {"code": "A-00", "title": "کاور و شناسنامه پروژه", "scale": "-"}, | |
| {"code": "A-01", "title": "فهرست نقشهها", "scale": "-"}, | |
| {"code": "A-02", "title": "راهنمای علائم و اختصارات", "scale": "-"}, | |
| {"code": "A-03", "title": "نقشه موقعیت سایت", "scale": "1/500"}, | |
| ] | |
| }, | |
| "B_SITE": { | |
| "title": "نقشههای سایت", | |
| "sheets": [ | |
| {"code": "B-01", "title": "پلان موقعیت و همجواریها", "scale": "1/200"}, | |
| {"code": "B-02", "title": "پلان محوطهسازی و دسترسیها", "scale": "1/200"}, | |
| ] | |
| }, | |
| "C_ARCHITECTURE": { | |
| "title": "نقشههای معماری", | |
| "sheets": [ | |
| {"code": "C-01", "title": "پلان زیرزمین — پارکینگ و انباری", "scale": "1/100"}, | |
| {"code": "C-02", "title": "پلان همکف — تجاری", "scale": "1/100"}, | |
| {"code": "C-03", "title": "پلان تیپ طبقات مسکونی", "scale": "1/100"}, | |
| {"code": "C-04", "title": "پلان بام — خرپشته و شیببندی", "scale": "1/100"}, | |
| {"code": "C-05", "title": "نماهای اصلی (شمالی و جنوبی)", "scale": "1/100"}, | |
| {"code": "C-06", "title": "نماهای فرعی (شرقی و غربی)", "scale": "1/100"}, | |
| {"code": "C-07", "title": "برش A-A (طول ساختمان)", "scale": "1/100"}, | |
| {"code": "C-08", "title": "برش B-B (عرض ساختمان)", "scale": "1/100"}, | |
| ] | |
| }, | |
| "D_STRUCTURE": { | |
| "title": "نقشههای سازه", | |
| "sheets": [ | |
| {"code": "D-01", "title": "پلان فونداسیون و ستونگذاری", "scale": "1/100"}, | |
| {"code": "D-02", "title": "پلان تیرریزی تیپ طبقات", "scale": "1/100"}, | |
| {"code": "D-03", "title": "جزئیات اتصالات و دتایلهای سازه", "scale": "1/20"}, | |
| ] | |
| }, | |
| "E_MECHANICAL": { | |
| "title": "نقشههای تأسیسات مکانیکی", | |
| "sheets": [ | |
| {"code": "E-01", "title": "پلان لولهکشی آب سرد و گرم — همکف", "scale": "1/100"}, | |
| {"code": "E-02", "title": "پلان لولهکشی آب سرد و گرم — طبقات", "scale": "1/100"}, | |
| {"code": "E-03", "title": "پلان فاضلاب و آب باران — زیرزمین", "scale": "1/100"}, | |
| {"code": "E-04", "title": "پلان فاضلاب و آب باران — طبقات", "scale": "1/100"}, | |
| {"code": "E-05", "title": "پلان گرمایش و شوفاژ", "scale": "1/100"}, | |
| {"code": "E-06", "title": "نقشه موتورخانه و تجهیزات", "scale": "1/50"}, | |
| ] | |
| }, | |
| "F_ELECTRICAL": { | |
| "title": "نقشههای تأسیسات برقی", | |
| "sheets": [ | |
| {"code": "F-01", "title": "پلان روشنایی — همکف تجاری", "scale": "1/100"}, | |
| {"code": "F-02", "title": "پلان روشنایی — طبقات مسکونی", "scale": "1/100"}, | |
| {"code": "F-03", "title": "پلان پریزها و سیمکشی", "scale": "1/100"}, | |
| {"code": "F-04", "title": "نقشه تابلوهای برق و رایزر دیاگرام", "scale": "1/50"}, | |
| ] | |
| } | |
| } | |
| def get_all_sheets(cls): | |
| sheets = [] | |
| sheet_num = 1 | |
| for cat_key, cat_data in cls.SHEET_TEMPLATES.items(): | |
| for s in cat_data["sheets"]: | |
| sheets.append({**s, "category": cat_key, "category_title": cat_data["title"], "sheet_number": sheet_num, "phase": "فاز یک — طراحی"}) | |
| sheet_num += 1 | |
| return sheets | |
| def get_total_sheets_count(cls): | |
| return len(cls.get_all_sheets()) | |
| # ============================================================ | |
| # ۳. Floorplan Engine | |
| # ============================================================ | |
| class FloorplanEngine: | |
| def __init__(self, site_width, site_depth, setback_front, setback_back, setback_left, setback_right, corridor_width=1.2): | |
| self.site_width = site_width | |
| self.site_depth = site_depth | |
| self.corridor_width = corridor_width | |
| self.min_x = setback_right | |
| self.max_x = site_width - setback_left | |
| self.min_y = setback_front | |
| self.max_y = site_depth - setback_back | |
| self.buildable_width = self.max_x - self.min_x | |
| self.buildable_depth = self.max_y - self.min_y | |
| self.grid_resolution = 0.1 | |
| self.grid_cols = int(self.buildable_width / self.grid_resolution) + 1 | |
| self.grid_rows = int(self.buildable_depth / self.grid_resolution) + 1 | |
| self.grid = np.zeros((self.grid_rows, self.grid_cols), dtype=bool) | |
| def _world_to_grid(self, x, y): | |
| col = int((x - self.min_x) / self.grid_resolution) | |
| row = int((y - self.min_y) / self.grid_resolution) | |
| return max(0, min(row, self.grid_rows - 1)), max(0, min(col, self.grid_cols - 1)) | |
| def _is_space_free(self, x, y, width, height): | |
| r1, c1 = self._world_to_grid(x, y) | |
| r2, c2 = self._world_to_grid(x + width, y + height) | |
| if r2 >= self.grid_rows or c2 >= self.grid_cols: | |
| return False | |
| margin = int(0.3 / self.grid_resolution) | |
| r1 = max(0, r1 - margin) | |
| c1 = max(0, c1 - margin) | |
| r2 = min(self.grid_rows - 1, r2 + margin) | |
| c2 = min(self.grid_cols - 1, c2 + margin) | |
| return not np.any(self.grid[r1:r2+1, c1:c2+1]) | |
| def _mark_occupied(self, x, y, width, height): | |
| r1, c1 = self._world_to_grid(x, y) | |
| r2, c2 = self._world_to_grid(x + width, y + height) | |
| self.grid[r1:r2+1, c1:c2+1] = True | |
| def _find_best_position(self, width, height, prefer_facade='south'): | |
| best_pos = None | |
| best_score = float('inf') | |
| step = int(0.5 / self.grid_resolution) | |
| for r in range(0, self.grid_rows, step): | |
| y = self.min_y + r * self.grid_resolution | |
| if y + height > self.max_y: | |
| break | |
| for c in range(0, self.grid_cols, step): | |
| x = self.min_x + c * self.grid_resolution | |
| if x + width > self.max_x: | |
| break | |
| if self._is_space_free(x, y, width, height): | |
| score = r * 10 | |
| if prefer_facade == 'north': | |
| score = (self.grid_rows - r) * 10 | |
| score += c | |
| if score < best_score: | |
| best_score = score | |
| best_pos = (x, y) | |
| return best_pos | |
| def layout(self, spaces): | |
| def sort_key(s): | |
| return (s.get('daylit', True) == False, -s['area']) | |
| sorted_spaces = sorted(spaces, key=sort_key) | |
| result = [] | |
| for space in sorted_spaces: | |
| area = space['area'] | |
| min_w = space.get('min_width', 2.0) | |
| daylit = space.get('daylit', True) | |
| best_w, best_h = self._optimal_dimensions(area, min_w) | |
| placed = False | |
| for scale in [1.0, 0.9, 1.1, 0.8, 1.2]: | |
| w = best_w * scale | |
| h = area / w | |
| pos = self._find_best_position(w, h, 'south' if daylit else 'north') | |
| if pos: | |
| x, y = pos | |
| self._mark_occupied(x, y, w, h) | |
| result.append({'name': space['name'], 'x': x, 'y': y, 'width': w, 'height': h, 'type': space.get('type', 'unknown')}) | |
| placed = True | |
| break | |
| if not placed: | |
| w = max(min_w, np.sqrt(area)) | |
| h = area / w | |
| pos = self._find_best_position(w, h, 'any') | |
| if pos: | |
| x, y = pos | |
| self._mark_occupied(x, y, w, h) | |
| result.append({'name': space['name'], 'x': x, 'y': y, 'width': w, 'height': h, 'type': space.get('type', 'unknown')}) | |
| return result | |
| def _optimal_dimensions(self, area, min_width): | |
| w = np.sqrt(area * 1.5) | |
| h = area / w | |
| if w < min_width: | |
| w = min_width | |
| h = area / w | |
| return w, h | |
| # ============================================================ | |
| # ۴. DXF Generator | |
| # ============================================================ | |
| def create_dxf_with_titleblock(layout, site_width, site_depth, setbacks, project_info, sheet_info, title_block_level, paper_size): | |
| doc = ezdxf.new(setup=True) | |
| doc.units = ezdxf.units.M | |
| msp = doc.modelspace() | |
| doc.layers.add(name="SITE", color=8) | |
| doc.layers.add(name="BUILDABLE", color=9) | |
| doc.layers.add(name="WALLS", color=1) | |
| doc.layers.add(name="TEXT", color=7) | |
| msp.add_line((0, 0), (site_width, 0), dxfattribs={'layer': 'SITE'}) | |
| msp.add_line((site_width, 0), (site_width, site_depth), dxfattribs={'layer': 'SITE'}) | |
| msp.add_line((site_width, site_depth), (0, site_depth), dxfattribs={'layer': 'SITE'}) | |
| msp.add_line((0, site_depth), (0, 0), dxfattribs={'layer': 'SITE'}) | |
| bx = setbacks['left'] | |
| by = setbacks['front'] | |
| bw = site_width - setbacks['left'] - setbacks['right'] | |
| bd = site_depth - setbacks['front'] - setbacks['back'] | |
| msp.add_line((bx, by), (bx + bw, by), dxfattribs={'layer': 'BUILDABLE'}) | |
| msp.add_line((bx + bw, by), (bx + bw, by + bd), dxfattribs={'layer': 'BUILDABLE'}) | |
| msp.add_line((bx + bw, by + bd), (bx, by + bd), dxfattribs={'layer': 'BUILDABLE'}) | |
| msp.add_line((bx, by + bd), (bx, by), dxfattribs={'layer': 'BUILDABLE'}) | |
| for space in layout: | |
| x, y, w, h = space['x'], space['y'], space['width'], space['height'] | |
| name = str(space['name']) | |
| msp.add_line((x, y), (x + w, y), dxfattribs={'layer': 'WALLS'}) | |
| msp.add_line((x + w, y), (x + w, y + h), dxfattribs={'layer': 'WALLS'}) | |
| msp.add_line((x + w, y + h), (x, y + h), dxfattribs={'layer': 'WALLS'}) | |
| msp.add_line((x, y + h), (x, y), dxfattribs={'layer': 'WALLS'}) | |
| mtext = msp.add_mtext(name, dxfattribs={'layer': 'TEXT', 'char_height': 0.35, 'width': w * 0.8, 'attachment_point': ezdxf.lldxf.const.MTEXT_MIDDLE_CENTER}) | |
| mtext.set_location((x + w/2, y + h/2)) | |
| if project_info: | |
| tb = TitleBlockManager(level=title_block_level, paper_size=paper_size) | |
| tb.create_title_block(msp, project_info, sheet_info) | |
| with tempfile.NamedTemporaryFile(suffix='.dxf', delete=False) as tmp: | |
| doc.saveas(tmp.name) | |
| return tmp.name | |
| # ============================================================ | |
| # ۵. Preview Generator | |
| # ============================================================ | |
| def create_preview_image(layout, site_width, site_depth, setbacks): | |
| fig, ax = plt.subplots(figsize=(14, 11)) | |
| ax.set_facecolor('#FAFAFA') | |
| site_rect = plt.Rectangle((0, 0), site_width, site_depth, linewidth=3, edgecolor='#333333', facecolor='#E8E8E8', alpha=0.5) | |
| ax.add_patch(site_rect) | |
| bx = setbacks['left'] | |
| by = setbacks['front'] | |
| bw = site_width - setbacks['left'] - setbacks['right'] | |
| bd = site_depth - setbacks['front'] - setbacks['back'] | |
| buildable_rect = plt.Rectangle((bx, by), bw, bd, linewidth=2, edgecolor='#FF9800', facecolor='none', linestyle='--') | |
| ax.add_patch(buildable_rect) | |
| colors = {'living_dining': '#FFE0B2', 'living': '#FFE0B2', 'bedroom': '#C8E6C9', 'kitchen': '#FFCCBC', 'bathroom': '#B3E5FC', 'wc': '#B3E5FC', 'corridor': '#EEEEEE', 'balcony': '#E1BEE7', 'parking': '#CFD8DC'} | |
| for space in layout: | |
| x, y, w, h = space['x'], space['y'], space['width'], space['height'] | |
| space_type = str(space.get('type', '')).lower() | |
| color = colors.get(space_type, '#FFF9C4') | |
| rect = plt.Rectangle((x, y), w, h, linewidth=2.5, edgecolor='#424242', facecolor=color, alpha=0.8) | |
| ax.add_patch(rect) | |
| name = str(space['name']) | |
| area = w * h | |
| ax.text(x + w/2, y + h/2 + 0.2, name, ha='center', va='center', fontsize=9, fontweight='bold', bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.85)) | |
| ax.text(x + w/2, y + h/2 - 0.5, f"{w:.1f}×{h:.1f}m | {area:.1f}m²", ha='center', va='center', fontsize=7, color='#666666') | |
| margin = 3 | |
| ax.set_xlim(-margin, site_width + margin) | |
| ax.set_ylim(-margin, site_depth + margin) | |
| ax.set_aspect('equal') | |
| ax.set_title('پیشنمایش پلان معماری', fontsize=15, fontweight='bold', pad=15) | |
| ax.grid(True, alpha=0.2, linestyle='--') | |
| from matplotlib.patches import Patch | |
| legend_elements = [Patch(facecolor='#FFE0B2', label='پذیرایی/نشیمن'), Patch(facecolor='#C8E6C9', label='اتاق خواب'), Patch(facecolor='#FFCCBC', label='آشپزخانه'), Patch(facecolor='#B3E5FC', label='سرویس'), Patch(facecolor='#E1BEE7', label='بالکن'), Patch(facecolor='#EEEEEE', label='راهرو')] | |
| ax.legend(handles=legend_elements, loc='upper right', fontsize=9, framealpha=0.9) | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=130, bbox_inches='tight') | |
| buf.seek(0) | |
| plt.close(fig) | |
| return Image.open(buf) | |
| # ============================================================ | |
| # ۶. Process Functions | |
| # ============================================================ | |
| def process_form(site_width, site_depth, setback_front, setback_back, setback_left, setback_right, corridor_width, spaces_json, project_name, client, project_address, drawing_title, drawing_code, sheet_number, scale, revision, designer, supervisor, title_block_level, paper_size): | |
| if not spaces_json or spaces_json.strip() == '' or spaces_json == '[]': | |
| return "❌ لطفاً حداقل یک فضا تعریف کنید.", None, None, None | |
| try: | |
| spaces = json.loads(spaces_json) | |
| except json.JSONDecodeError as e: | |
| return f"❌ خطا: {str(e)}", None, None, None | |
| if len(spaces) == 0: | |
| return "❌ لیست فضاها خالی است.", None, None, None | |
| total_area = sum(s.get('area', 0) for s in spaces) | |
| buildable_area = (site_width - setback_left - setback_right) * (site_depth - setback_front - setback_back) | |
| occupancy = (total_area / buildable_area * 100) if buildable_area > 0 else 0 | |
| if site_width <= 0 or site_depth <= 0: | |
| return "❌ ابعاد زمین باید مثبت باشد.", None, None, None | |
| if occupancy > 95: | |
| return f"❌ سطح اشغال {occupancy:.0f}٪ — فضای راهرو کافی نیست.", None, None, None | |
| try: | |
| engine = FloorplanEngine(site_width, site_depth, setback_front, setback_back, setback_left, setback_right, corridor_width) | |
| layout = engine.layout(spaces) | |
| if not layout: | |
| return "❌ نتونستم فضاها رو بچینم.", None, None, None | |
| setbacks = {'front': setback_front, 'back': setback_back, 'left': setback_left, 'right': setback_right} | |
| project_info = {'project_name': project_name or '___', 'client': client or '___', 'project_address': project_address or '___', 'designer': designer or '___', 'supervisor': supervisor or '___'} | |
| total_sheets = SheetSeriesGenerator.get_total_sheets_count() | |
| sheet_info = {'drawing_title': drawing_title or 'پلان معماری طبقه همکف', 'drawing_code': drawing_code or 'C-02', 'sheet_number': f"{sheet_number} از {total_sheets}", 'scale': scale or '1/100', 'date': date.today().strftime('%Y/%m/%d'), 'phase': 'فاز یک — طراحی معماری', 'revision': revision or 'Rev A'} | |
| dxf_path = create_dxf_with_titleblock(layout, site_width, site_depth, setbacks, project_info, sheet_info, int(title_block_level), paper_size) | |
| preview = create_preview_image(layout, site_width, site_depth, setbacks) | |
| output_data = {'site': {'width': site_width, 'depth': site_depth, 'setbacks': setbacks}, 'spaces': spaces, 'layout': layout, 'project_info': project_info, 'sheet_info': sheet_info, 'title_block_level': title_block_level, 'paper_size': paper_size, 'stats': {'total_area': total_area, 'buildable_area': buildable_area, 'occupancy_rate': round(occupancy, 1)}} | |
| with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w', encoding='utf-8') as tmp: | |
| json.dump(output_data, tmp, ensure_ascii=False, indent=2) | |
| json_path = tmp.name | |
| status = f"✅ چیدمان با موفقیت انجام شد.\n\n📐 {len(layout)} فضا چیده شد\n📏 مساحت کل: {total_area:.1f} m²\n🏠 سطح اشغال: {occupancy:.1f}٪\n📄 سایز کاغذ: {paper_size}\n🏷️ سطح کادر: {TitleBlockManager.LEVEL_CONFIGS[int(title_block_level)]['name']}\n💾 خروجیها: DXF + JSON" | |
| return status, preview, dxf_path, json_path | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(error_details) | |
| return f"❌ خطا: {str(e)}", None, None, None, None | |
| # ============================================================ | |
| # ۷. Space Management | |
| # ============================================================ | |
| def add_space(space_name, space_type, area, min_width, daylit, spaces_state): | |
| if not space_name or area <= 0: | |
| return spaces_state, "⚠️ نام فضا و مساحت را وارد کنید." | |
| try: | |
| spaces = json.loads(spaces_state) if spaces_state else [] | |
| except: | |
| spaces = [] | |
| spaces.append({'name': space_name, 'type': space_type, 'area': float(area), 'min_width': float(min_width), 'daylit': daylit}) | |
| summary = f"📋 {len(spaces)} فضا | مجموع: {sum(s['area'] for s in spaces):.1f} m²" | |
| return json.dumps(spaces, ensure_ascii=False), summary | |
| def clear_spaces(): | |
| return "[]", "📋 ۰ فضا" | |
| def get_spaces_table(spaces_json): | |
| if not spaces_json or spaces_json == '[]': | |
| return pd.DataFrame(columns=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر']) | |
| try: | |
| spaces = json.loads(spaces_json) | |
| data = [[s.get('name', ''), s.get('type', ''), s.get('area', 0), s.get('min_width', 0), '✅' if s.get('daylit', True) else '❌'] for s in spaces] | |
| return pd.DataFrame(data, columns=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر']) | |
| except: | |
| return pd.DataFrame(columns=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر']) | |
| def update_occupancy(site_w, site_d, sf, sb, sl, sr, spaces_json): | |
| if not spaces_json or spaces_json == '[]': | |
| return "📋 هنوز فضایی تعریف نشده." | |
| try: | |
| spaces = json.loads(spaces_json) | |
| total = sum(s['area'] for s in spaces) | |
| buildable = (site_w - sl - sr) * (site_d - sf - sb) | |
| if buildable <= 0: | |
| return "⚠️ ابعاد نامعتبر" | |
| occ = total / buildable * 100 | |
| if occ > 90: | |
| return f"⚠️ سطح اشغال: {occ:.1f}٪ — بسیار زیاد!" | |
| elif occ > 70: | |
| return f"⚡ سطح اشغال: {occ:.1f}٪ — نسبتاً زیاد" | |
| else: | |
| return f"✅ سطح اشغال: {occ:.1f}٪ | کل: {total:.1f} m² | محدوده ساخت: {buildable:.1f} m²" | |
| except: | |
| return "⚠️ خطا در محاسبه" | |
| def get_sheet_preview(title_block_level, paper_size): | |
| sheets = SheetSeriesGenerator.get_all_sheets() | |
| total = len(sheets) | |
| level_name = TitleBlockManager.LEVEL_CONFIGS[int(title_block_level)]['name'] | |
| paper_w, paper_h = PAPER_SIZES[paper_size] | |
| text = f"📄 **تنظیمات شیت:**\n• سطح کادر: {level_name}\n• سایز کاغذ: {paper_size} ({paper_w}×{paper_h} mm)\n• تعداد کل شیتها: {total}\n\n📋 **لیست شیتهای فاز یک:**\n" | |
| for cat_key, cat_data in SheetSeriesGenerator.SHEET_TEMPLATES.items(): | |
| text += f"\n**{cat_data['title']}:**\n" | |
| for s in cat_data['sheets']: | |
| text += f" • {s['code']} — {s['title']} (مقیاس: {s['scale']})\n" | |
| return text | |
| # ============================================================ | |
| # ۸. Gradio Interface | |
| # ============================================================ | |
| with gr.Blocks(title="پلتفرم طراحی پلان معماری — نسخه حرفهای") as demo: | |
| gr.Markdown(""" | |
| # 🏗️ پلتفرم تولید پلان معماری — نسخه حرفهای | |
| ### ✨ امکانات: ۵ سطح کادر نقشه | سایز کاغذ قابل تنظیم | شیتبندی کامل فاز یک | |
| """) | |
| spaces_state = gr.State(value="[]") | |
| with gr.Tabs(): | |
| # تب ۱: زمین | |
| with gr.Tab("🏠 زمین و ضوابط"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ابعاد زمین") | |
| site_width = gr.Number(label="عرض زمین (متر)", value=15.0, minimum=5, maximum=50) | |
| site_depth = gr.Number(label="عمق زمین (متر)", value=20.0, minimum=5, maximum=50) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### عقبنشینیها") | |
| setback_front = gr.Number(label="عقبنشینی جلو (متر)", value=3.0, minimum=0, maximum=10) | |
| setback_back = gr.Number(label="عقبنشینی عقب (متر)", value=2.0, minimum=0, maximum=10) | |
| setback_left = gr.Number(label="عقبنشینی چپ (متر)", value=1.5, minimum=0, maximum=10) | |
| setback_right = gr.Number(label="عقبنشینی راست (متر)", value=1.5, minimum=0, maximum=10) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### تنظیمات") | |
| corridor_width = gr.Slider(label="عرض راهرو (متر)", minimum=0.8, maximum=2.5, value=1.2, step=0.1) | |
| occupancy_info = gr.Textbox(label="اطلاعات سطح اشغال", value="📋 هنوز فضایی تعریف نشده.", interactive=False) | |
| # تب ۲: فضاها | |
| with gr.Tab("🚪 فضاها"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| gr.Markdown("### افزودن فضای جدید") | |
| space_name = gr.Textbox(label="نام فضا", placeholder="مثلاً: پذیرایی", scale=3) | |
| space_type = gr.Dropdown(label="نوع فضا", choices=['living_dining', 'bedroom', 'kitchen', 'bathroom', 'corridor', 'balcony', 'parking'], value='living_dining', scale=2) | |
| with gr.Row(): | |
| area = gr.Number(label="مساحت (m²)", value=25.0, minimum=2, maximum=200) | |
| min_width = gr.Number(label="حداقل عرض (m)", value=3.0, minimum=1, maximum=20) | |
| daylit = gr.Checkbox(label="نیاز به نورگیری", value=True) | |
| with gr.Row(): | |
| add_btn = gr.Button("➕ افزودن", variant="primary") | |
| clear_btn = gr.Button("🗑️ پاک کردن همه") | |
| space_message = gr.Textbox(label="وضعیت", interactive=False) | |
| with gr.Column(scale=3): | |
| gr.Markdown("### فضاهای تعریف شده") | |
| spaces_table = gr.Dataframe(headers=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر'], interactive=False) | |
| spaces_summary = gr.Textbox(label="خلاصه", value="📋 ۰ فضا", interactive=False) | |
| # تب ۳: پروژه و شیت | |
| with gr.Tab("📋 پروژه و شیت"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### اطلاعات پروژه") | |
| project_name = gr.Textbox(label="نام پروژه", value="ساختمان تجاری-مسکونی ستاره") | |
| client = gr.Textbox(label="کارفرما", value="شرکت آتیهسازان") | |
| project_address = gr.Textbox(label="آدرس پروژه", value="تهران، خیابان ولیعصر") | |
| designer = gr.Textbox(label="طراح", value="مهندس رضایی") | |
| supervisor = gr.Textbox(label="ناظر", value="مهندس حسینی") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### اطلاعات شیت") | |
| drawing_title = gr.Textbox(label="عنوان نقشه", value="پلان معماری طبقه همکف") | |
| all_codes = [s['code'] for s in SheetSeriesGenerator.get_all_sheets()] | |
| drawing_code = gr.Dropdown(label="کد نقشه", choices=all_codes, value="C-02") | |
| sheet_number = gr.Number(label="شماره شیت", value=5, minimum=1) | |
| scale = gr.Dropdown(label="مقیاس", choices=["1/20", "1/50", "1/100", "1/200", "1/500"], value="1/100") | |
| revision = gr.Textbox(label="بازبینی", value="Rev A") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### تنظیمات کادر و کاغذ") | |
| title_block_level = gr.Radio( | |
| choices=[ | |
| (f"۱ - {TitleBlockManager.LEVEL_CONFIGS[1]['name']}", 1), | |
| (f"۲ - {TitleBlockManager.LEVEL_CONFIGS[2]['name']}", 2), | |
| (f"۳ - {TitleBlockManager.LEVEL_CONFIGS[3]['name']} ⭐", 3), | |
| (f"۴ - {TitleBlockManager.LEVEL_CONFIGS[4]['name']}", 4), | |
| (f"۵ - {TitleBlockManager.LEVEL_CONFIGS[5]['name']}", 5), | |
| ], | |
| value=3, | |
| label="سطح کادر نقشه" | |
| ) | |
| paper_size = gr.Dropdown(label="سایز کاغذ", choices=list(PAPER_SIZES.keys()), value="A3_landscape") | |
| sheet_preview_btn = gr.Button("📋 مشاهده شیتهای پروژه") | |
| sheet_preview_text = gr.Textbox(label="راهنمای شیتبندی", interactive=False, lines=15, value="برای مشاهده لیست شیتها، روی دکمه بالا کلیک کنید.") | |
| # تب ۴: خروجی | |
| with gr.Tab("📊 خروجی"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| submit_btn = gr.Button("🚀 تولید پلان نهایی", variant="primary", size="lg") | |
| status_output = gr.Textbox(label="وضعیت", interactive=False, lines=8) | |
| with gr.Row(): | |
| dxf_output = gr.File(label="📥 DXF") | |
| json_output = gr.File(label="📥 JSON پروژه") | |
| with gr.Column(scale=2): | |
| preview_output = gr.Image(label="پیشنمایش", type="pil") | |
| # Events | |
| add_btn.click(fn=add_space, inputs=[space_name, space_type, area, min_width, daylit, spaces_state], outputs=[spaces_state, space_message] | |
| ).then(fn=get_spaces_table, inputs=[spaces_state], outputs=[spaces_table] | |
| ).then(fn=lambda s: f"📋 {len(json.loads(s))} فضا | مجموع: {sum(sp['area'] for sp in json.loads(s)):.1f} m²" if s and s != '[]' else "📋 ۰ فضا", inputs=[spaces_state], outputs=[spaces_summary]) | |
| clear_btn.click(fn=clear_spaces, inputs=[], outputs=[spaces_state, space_message] | |
| ).then(fn=lambda: (pd.DataFrame(columns=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر']), "📋 ۰ فضا"), inputs=[], outputs=[spaces_table, spaces_summary]) | |
| for inp in [site_width, site_depth, setback_front, setback_back, setback_left, setback_right]: | |
| inp.change(fn=update_occupancy, inputs=[site_width, site_depth, setback_front, setback_back, setback_left, setback_right, spaces_state], outputs=[occupancy_info]) | |
| spaces_state.change(fn=update_occupancy, inputs=[site_width, site_depth, setback_front, setback_back, setback_left, setback_right, spaces_state], outputs=[occupancy_info]) | |
| sheet_preview_btn.click(fn=get_sheet_preview, inputs=[title_block_level, paper_size], outputs=[sheet_preview_text]) | |
| submit_btn.click(fn=process_form, inputs=[site_width, site_depth, setback_front, setback_back, setback_left, setback_right, corridor_width, spaces_state, project_name, client, project_address, drawing_title, drawing_code, sheet_number, scale, revision, designer, supervisor, title_block_level, paper_size], outputs=[status_output, preview_output, dxf_output, json_output]) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft()) |