cad_template / app.py
Mhdeusi's picture
Update app.py
e0e3576 verified
Raw
History Blame Contribute Delete
25.1 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
# ============================================================
# ۱. الگوریتم چیدمان هوشمند (Rectangle Packing)
# ============================================================
class FloorplanEngine:
"""
موتور چیدمان پلان مبتنی بر Rectangle Packing
با رعایت محدودیت‌های معماری
"""
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
# شبکه اشغال (Grid) برای تشخیص فضای خالی
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):
"""
چیدمان فضاها با الگوریتم Packing
spaces: لیست dict با keys: name, area, min_width, type, daylit, privacy
"""
# مرتب‌سازی: اول فضاهای بزرگ، سپس فضاهای نورگیر
def sort_key(s):
return (s.get('daylit', True) == False, -s['area'])
sorted_spaces = sorted(spaces, key=sort_key)
layout = []
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, 1.3]:
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)
layout.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)
layout.append({
'name': space['name'],
'x': x, 'y': y,
'width': w, 'height': h,
'type': space.get('type', 'unknown')
})
return layout
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
# ============================================================
# ۲. تولید خروجی‌ها
# ============================================================
def create_dxf(layout, site_width, site_depth, setbacks):
"""تولید فایل DXF با زمین و دیوارها"""
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))
with tempfile.NamedTemporaryFile(suffix='.dxf', delete=False) as tmp:
doc.saveas(tmp.name)
return tmp.name
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',
}
default_color = '#FFF9C4'
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, default_color)
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_xlabel('X (متر)', fontsize=12)
ax.set_ylabel('Y (متر)', fontsize=12)
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)
# ============================================================
# ۳. تابع اصلی پردازش
# ============================================================
def process_form(
site_width, site_depth,
setback_front, setback_back, setback_left, setback_right,
corridor_width,
spaces_json
):
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
}
dxf_path = create_dxf(layout, site_width, site_depth, setbacks)
preview = create_preview_image(layout, site_width, site_depth, setbacks)
output_json = json.dumps({
'site': {'width': site_width, 'depth': site_depth, 'setbacks': setbacks},
'spaces': spaces,
'layout': layout,
'stats': {
'total_area': total_area,
'buildable_area': buildable_area,
'occupancy_rate': round(occupancy, 1)
}
}, ensure_ascii=False, indent=2)
with tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w', encoding='utf-8') as tmp:
tmp.write(output_json)
json_path = tmp.name
placed_count = len(layout)
unplaced_count = len(spaces) - placed_count
status = f"✅ چیدمان با موفقیت انجام شد.\n\n"
status += f"📐 {placed_count} فضا چیده شد"
if unplaced_count > 0:
status += f" ({unplaced_count} فضا جا نشد)"
status += f"\n📏 مساحت کل: {total_area:.1f} m²"
status += f"\n🏠 سطح اشغال: {occupancy:.1f}٪"
status += f"\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
# ============================================================
# ۴. مدیریت فضاها
# ============================================================
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 = []
for s in spaces:
data.append([
s.get('name', ''),
s.get('type', ''),
s.get('area', 0),
s.get('min_width', 0),
'✅' if s.get('daylit', True) else '❌'
])
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 "⚠️ خطا در محاسبه"
# ============================================================
# ۵. اینترفیس Gradio
# ============================================================
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("### افزودن فضای جدید")
with gr.Row():
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, scale=1)
min_width = gr.Number(label="حداقل عرض (m)", value=3.0, minimum=1, maximum=20, scale=1)
daylit = gr.Checkbox(label="نیاز به نورگیری", value=True, scale=1)
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=['نام فضا', 'نوع', 'مساحت', 'حداقل عرض', 'نورگیر'],
label="",
interactive=False,
)
spaces_summary = gr.Textbox(label="خلاصه", value="📋 ۰ فضا", interactive=False)
# تب ۳: خروجی
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")
# رویدادها
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]
)
submit_btn.click(
fn=process_form,
inputs=[site_width, site_depth, setback_front, setback_back, setback_left, setback_right,
corridor_width, spaces_state],
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()
)