Mhdeusi's picture
Update app.py
a507f73 verified
Raw
History Blame Contribute Delete
40.2 kB
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"},
]
}
}
@classmethod
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
@classmethod
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())