Files
2026-09-07 22:39:50 +03:30

2360 lines
170 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ابزار رسم نمودارهای UML</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/vazirmatn@5.0.0/index.css">
<style>
body{font-family:'Vazirmatn',sans-serif;overflow:hidden}
:root{
--bg-primary:#f7fafc;--bg-secondary:#fff;--bg-tertiary:#edf2f7;--bg-hover:#f0f4f8;
--text-primary:#2d3748;--text-secondary:#4a5568;--text-muted:#718096;--border-color:#e2e8f0;
--sidebar-bg:#fff;--canvas-bg:#fff;--grid-line:rgba(0,0,0,.06);--tab-bg:#fff;
--tab-bar-bg:linear-gradient(to bottom,#f8fafc,#f1f5f9);--status-bg:#2d3748;--status-text:#fff;
--modal-bg:#fff;--modal-overlay:rgba(0,0,0,.5);--input-bg:#fff;--input-border:#cbd5e0;
--input-readonly-bg:#f7fafc;--card-bg:#f7fafc;--card-border:#e2e8f0;--section-border:#edf2f7;
--shadow-sm:0 1px 3px rgba(0,0,0,.08);--shadow-md:0 4px 12px rgba(0,0,0,.1);
--shadow-lg:0 10px 30px rgba(0,0,0,.12);
--actor-stroke:#2d3748;--uc-fill:#f7fafc;--uc-stroke:#2d3748;--rel-stroke:#2d3748;
--sb-stroke:#4a5568;--label-bg:#fff;--state-fill:#ebf8ff;--state-stroke:#2b6cb0;
--state-init:#2d3748;--state-final-outer:#2d3748;--state-final-inner:#2d3748;
--transition-stroke:#4a5568;--transition-label:#2d3748;--activity-fill:#f0fff4;
--activity-stroke:#276749;--decision-fill:#fffaf0;--decision-stroke:#c05621;
--fork-stroke:#2d3748;--flow-stroke:#4a5568;--flow-label:#2d3748;
--comp-fill:#faf5ff;--comp-stroke:#6b46c1;--comp-icon:#6b46c1;--iface-stroke:#6b46c1;
--pkg-fill:#fffaf0;--pkg-stroke:#c05621;--dep-stroke:#4a5568;
--node-face-top:#e6fffa;--node-face-front:#b2f5ea;--node-face-side:#81e6d9;
--node-stroke:#234e52;--artifact-fill:#fff5f5;--artifact-stroke:#c53030;
--class-fill:#ebf8ff;--class-stroke:#2c5282;--class-header:#bee3f8;
--class-attr:#2c5282;--class-method:#2f855a;
--seq-lifeline:#4a5568;--seq-participant-fill:#e2e8f0;--seq-participant-stroke:#2d3748;
--seq-activation:#bee3f8;--seq-activation-stroke:#2c5282;--seq-msg:#2d3748;--seq-return:#718096;
--seq-fragment-fill:#fffaf0;--seq-fragment-stroke:#c05621;
--swimlane-default:#ebf8ff;--swimlane-stroke:#2c5282;
}
body.dark{
--bg-primary:#0f1724;--bg-secondary:#1a2332;--bg-tertiary:#243044;--bg-hover:#2a3a52;
--text-primary:#e2e8f0;--text-secondary:#cbd5e0;--text-muted:#8899aa;--border-color:#2d3f56;
--sidebar-bg:#1a2332;--canvas-bg:#0f1724;--grid-line:rgba(255,255,255,.05);--tab-bg:#1a2332;
--tab-bar-bg:linear-gradient(to bottom,#1a2332,#162030);--status-bg:#0a1018;--status-text:#cbd5e0;
--modal-bg:#1a2332;--modal-overlay:rgba(0,0,0,.7);--input-bg:#243044;--input-border:#3a5068;
--input-readonly-bg:#1e2d40;--card-bg:#1e2d40;--card-border:#2d3f56;--section-border:#2d3f56;
--shadow-sm:0 1px 3px rgba(0,0,0,.3);--shadow-md:0 4px 12px rgba(0,0,0,.4);
--shadow-lg:0 10px 30px rgba(0,0,0,.5);
--actor-stroke:#cbd5e0;--uc-fill:#1e2d40;--uc-stroke:#cbd5e0;--rel-stroke:#a0b4c8;
--sb-stroke:#8899aa;--label-bg:#1a2332;--state-fill:#1e2d40;--state-stroke:#63b3ed;
--state-init:#e2e8f0;--state-final-outer:#e2e8f0;--state-final-inner:#e2e8f0;
--transition-stroke:#8899aa;--transition-label:#cbd5e0;--activity-fill:#1e2d40;
--activity-stroke:#68d391;--decision-fill:#2d2418;--decision-stroke:#fbd38d;
--fork-stroke:#e2e8f0;--flow-stroke:#8899aa;--flow-label:#cbd5e0;
--comp-fill:#2a1f3d;--comp-stroke:#b794f4;--comp-icon:#b794f4;--iface-stroke:#b794f4;
--pkg-fill:#2d2418;--pkg-stroke:#fbd38d;--dep-stroke:#8899aa;
--node-face-top:#234e52;--node-face-front:#1d4044;--node-face-side:#1a3a3e;
--node-stroke:#81e6d9;--artifact-fill:#3b1a1a;--artifact-stroke:#fc8181;
--class-fill:#1e2d40;--class-stroke:#63b3ed;--class-header:#2c5282;
--class-attr:#90cdf4;--class-method:#9ae6b4;
--seq-lifeline:#8899aa;--seq-participant-fill:#243044;--seq-participant-stroke:#cbd5e0;
--seq-activation:#2c5282;--seq-activation-stroke:#63b3ed;--seq-msg:#cbd5e0;--seq-return:#8899aa;
--seq-fragment-fill:#2d2418;--seq-fragment-stroke:#fbd38d;
--swimlane-default:#1e2d40;--swimlane-stroke:#63b3ed;
}
body{background:var(--bg-primary);color:var(--text-primary)}
.tool-btn{transition:all .2s;background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid transparent}
.tool-btn:hover{transform:translateY(-1px);box-shadow:var(--shadow-sm);background:var(--bg-hover)}
.tool-btn.active{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%)!important;color:#fff!important;border-color:transparent!important}
body.dark .tool-btn-red{background:#3b1a1a!important;color:#fc8181!important}
body.dark .tool-btn-orange{background:#3b2a1a!important;color:#fbd38d!important}
body.dark .tool-btn-purple{background:#2a1a3b!important;color:#d6bcfa!important}
body.dark .tool-btn-teal{background:#1a3b35!important;color:#81e6d9!important}
body.dark .tool-btn-cyan{background:#1a2e3b!important;color:#76e4f7!important}
body.dark .tool-btn-green{background:#1a3b1e!important;color:#9ae6b4!important}
body.dark .tool-btn-blue{background:#1a253b!important;color:#90cdf4!important}
body.dark .tool-btn-indigo{background:#1e2544!important;color:#a3bffa!important}
.tool-btn-red{background:#fff5f5!important;color:#c53030!important}
.tool-btn-orange{background:#fffaf0!important;color:#c05621!important}
.tool-btn-purple{background:#faf5ff!important;color:#6b46c1!important}
.tool-btn-teal{background:#e6fffa!important;color:#285e61!important}
.tool-btn-cyan{background:#edfdfd!important;color:#0987a0!important}
.tool-btn-green{background:#f0fff4!important;color:#276749!important}
.tool-btn-blue{background:#ebf8ff!important;color:#2b6cb0!important}
.tool-btn-indigo{background:#ebf4ff!important;color:#4c51bf!important}
canvas{cursor:crosshair}
.canvas-container{background-color:var(--canvas-bg)}
.sidebar-wrapper{transition:width .3s cubic-bezier(.4,0,.2,1),opacity .2s ease;width:260px;overflow:hidden;flex-shrink:0;background:var(--sidebar-bg);border-left:1px solid var(--border-color)}
.sidebar-wrapper.collapsed{width:0;opacity:0;pointer-events:none}
.sidebar-inner{width:260px;height:100%;overflow-y:auto}
.tool-group{margin-bottom:12px}
.tool-group-header{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;padding-bottom:6px;margin-bottom:6px;border-bottom:1px solid var(--section-border);cursor:pointer;user-select:none;transition:color .2s}
.tool-group-header:hover{color:var(--text-secondary)}
.tool-group-arrow{font-size:10px;transition:transform .3s ease;color:var(--text-muted)}
.tool-group.collapsed .tool-group-arrow{transform:rotate(-90deg)}
.tool-group-content{max-height:1500px;overflow:hidden;transition:max-height .35s ease,opacity .25s ease;opacity:1}
.tool-group.collapsed .tool-group-content{max-height:0;opacity:0}
.properties-overlay{position:fixed;bottom:40px;left:50%;transform:translateX(-50%) translateY(120%);z-index:1000;transition:transform .35s cubic-bezier(.4,0,.2,1),opacity .35s ease;opacity:0;pointer-events:none;min-width:520px;max-width:90vw}
.properties-overlay.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
.properties-overlay .panel-shadow{background:var(--modal-bg);box-shadow:0 -8px 30px rgba(0,0,0,.25),0 0 0 1px var(--border-color)}
.modal-overlay{position:fixed;inset:0;background:var(--modal-overlay);backdrop-filter:blur(4px);z-index:2000;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .3s ease}
.modal-overlay.visible{opacity:1;pointer-events:auto}
.modal-content{background:var(--modal-bg);border-radius:16px;box-shadow:var(--shadow-lg);border:1px solid var(--border-color);max-width:600px;width:90%;max-height:80vh;overflow-y:auto;transform:scale(.9) translateY(20px);transition:transform .3s cubic-bezier(.4,0,.2,1);color:var(--text-primary)}
.modal-overlay.visible .modal-content{transform:scale(1) translateY(0)}
.save-modal-content{max-width:520px}
.class-editor-modal{max-width:720px}
.tab-bar{background:var(--tab-bar-bg);border-bottom:1px solid var(--border-color);display:flex;align-items:center;padding:0 8px;flex-shrink:0;height:44px;gap:4px}
.tabs-scroll{display:flex;overflow-x:auto;overflow-y:hidden;flex:1;gap:4px;padding:6px 4px;scrollbar-width:thin}
.tabs-scroll::-webkit-scrollbar{height:4px}
.tabs-scroll::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:2px}
.diagram-tab{display:flex;align-items:center;gap:6px;padding:6px 10px 6px 12px;background:var(--tab-bg);border:1px solid var(--border-color);border-radius:8px 8px 0 0;cursor:pointer;min-width:140px;max-width:240px;transition:all .2s;user-select:none;flex-shrink:0}
.diagram-tab:hover{background:var(--bg-hover)}
.diagram-tab.active{background:var(--bg-secondary);border-color:#667eea;border-bottom:2px solid var(--bg-secondary);margin-bottom:-1px;box-shadow:0 -2px 8px rgba(102,126,234,.15)}
.diagram-tab.active .tab-title{color:#667eea;font-weight:600}
.tab-title{font-size:12px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.tab-type-badge{font-size:9px;padding:1px 5px;border-radius:4px;background:var(--bg-tertiary);color:var(--text-muted);font-weight:600;flex-shrink:0}
.tab-close{width:18px;height:18px;display:flex;align-items:center;justify-content:center;border-radius:4px;color:var(--text-muted);font-size:14px;transition:all .15s}
.tab-close:hover{background:#fed7d7;color:#c53030}
.header-bar-btn{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:#fff;border-radius:8px;cursor:pointer;font-size:16px;transition:all .2s;flex-shrink:0}
.header-bar-btn:hover{transform:scale(1.08)}
.btn-new{background:linear-gradient(135deg,#48bb78,#38a169);box-shadow:0 2px 4px rgba(72,187,120,.3)}
.btn-toggle{background:linear-gradient(135deg,#667eea,#764ba2);box-shadow:0 2px 4px rgba(102,126,234,.3)}
.btn-theme{background:linear-gradient(135deg,#ed8936,#dd6b20);box-shadow:0 2px 4px rgba(237,137,54,.3);font-size:18px}
.btn-undo{background:linear-gradient(135deg,#4299e1,#3182ce);box-shadow:0 2px 4px rgba(66,153,225,.3);font-size:14px}
.btn-redo{background:linear-gradient(135deg,#4299e1,#3182ce);box-shadow:0 2px 4px rgba(66,153,225,.3);font-size:14px}
.btn-zoom{background:linear-gradient(135deg,#9f7aea,#805ad5);box-shadow:0 2px 4px rgba(159,122,234,.3);font-size:14px;width:28px;height:28px}
.zoom-display{font-size:11px;color:var(--text-muted);min-width:45px;text-align:center;font-family:monospace;background:var(--bg-tertiary);padding:4px 6px;border-radius:6px;cursor:pointer;user-select:none}
.zoom-display:hover{background:var(--bg-hover);color:var(--text-secondary)}
.themed-input{background:var(--input-bg);border:1px solid var(--input-border);color:var(--text-primary);border-radius:8px;padding:8px 12px;font-size:13px;transition:border-color .2s,box-shadow .2s;width:100%}
.themed-input:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px rgba(102,126,234,.15)}
.themed-input-readonly{background:var(--input-readonly-bg);border:1px solid var(--border-color);color:var(--text-muted);border-radius:8px;padding:8px 12px;font-size:13px;width:100%}
.themed-card{background:var(--card-bg);border:1px solid var(--card-border);border-radius:8px;padding:12px}
.status-bar{background:var(--status-bg);color:var(--status-text);border-top:1px solid var(--border-color)}
.main-header{background:linear-gradient(to left,#4c51bf,#6b46c1);box-shadow:var(--shadow-md)}
.grid-type-selector{display:flex;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;padding:2px;gap:2px}
.grid-type-option{flex:1;display:flex;align-items:center;justify-content:center;gap:4px;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:11px;font-weight:500;color:var(--text-muted);transition:all .2s;user-select:none}
.grid-type-option:hover{color:var(--text-secondary);background:var(--bg-hover)}
.grid-type-option.active{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;box-shadow:0 2px 4px rgba(102,126,234,.25)}
.diagram-type-card{border:2px solid var(--border-color);border-radius:12px;padding:16px;cursor:pointer;transition:all .2s;background:var(--card-bg)}
.diagram-type-card:hover{border-color:#667eea;transform:translateY(-2px);box-shadow:var(--shadow-md)}
.diagram-type-card.selected{border-color:#667eea;background:linear-gradient(135deg,rgba(102,126,234,.08),rgba(118,75,162,.08));box-shadow:0 0 0 3px rgba(102,126,234,.15)}
.diagram-type-card.disabled{opacity:.45;cursor:not-allowed;filter:grayscale(.5)}
.diagram-type-card.disabled:hover{border-color:var(--border-color);transform:none;box-shadow:none}
.diagram-type-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:24px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;margin-bottom:10px}
.diagram-type-card.disabled .diagram-type-icon{background:var(--bg-tertiary)}
.file-preview{background:linear-gradient(135deg,rgba(102,126,234,.08),rgba(118,75,162,.08));border:1px dashed #667eea;border-radius:12px;padding:14px;display:flex;align-items:center;gap:12px}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-muted)}
.class-editor-tabs{display:flex;border-bottom:2px solid var(--border-color);margin-bottom:16px}
.class-editor-tab{flex:1;padding:10px;text-align:center;cursor:pointer;font-size:13px;font-weight:500;color:var(--text-muted);border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s}
.class-editor-tab:hover{color:var(--text-secondary)}
.class-editor-tab.active{color:#667eea;border-bottom-color:#667eea}
.member-table{width:100%;border-collapse:collapse;font-size:12px}
.member-table th{background:var(--bg-tertiary);color:var(--text-muted);padding:6px 8px;text-align:right;font-weight:600;border-bottom:1px solid var(--border-color)}
.member-table td{padding:4px;border-bottom:1px solid var(--border-color)}
.member-table input,.member-table select{background:var(--input-bg);border:1px solid var(--input-border);color:var(--text-primary);border-radius:4px;padding:4px 6px;font-size:12px;width:100%}
.member-table input:focus,.member-table select:focus{outline:none;border-color:#667eea}
.member-btn{padding:4px 8px;border-radius:4px;font-size:11px;cursor:pointer;border:none;transition:all .15s}
.member-btn-add{background:#48bb78;color:#fff}
.member-btn-add:hover{background:#38a169}
.member-btn-del{background:#fc8181;color:#fff}
.member-btn-del:hover{background:#f56565}
@media print{.no-print{display:none!important}canvas{page-break-inside:avoid}}
</style>
</head>
<body class="dark">
<div class="flex flex-col h-screen">
<header class="main-header text-white flex-shrink-0 no-print">
<div class="px-4 py-3">
<div class="flex justify-between items-center">
<div>
<h1 class="text-xl font-bold">🎯 ابزار رسم نمودارهای UML</h1>
<p class="text-xs opacity-90 mt-1">مدیریت چند نمودار با قابلیت ذخیره و بازیابی</p>
</div>
<div class="text-left text-sm"><div class="opacity-90">طراح: هادی خزاعی اصل</div></div>
</div>
</div>
</header>
<div class="tab-bar no-print">
<button id="toggleSidebarBtn" class="header-bar-btn btn-toggle" title="نمایش/مخفی ابزارها (B)">☰</button>
<button id="newDiagramBtn" class="header-bar-btn btn-new" title="دیاگرام جدید (Ctrl+N)">+</button>
<button id="themeToggleBtn" class="header-bar-btn btn-theme" title="تغییر تم">🌙</button>
<div style="width:1px;height:24px;background:var(--border-color);margin:0 4px"></div>
<button id="undoBtn" class="header-bar-btn btn-undo" title="بازگشت (Ctrl+Z)">↶</button>
<button id="redoBtn" class="header-bar-btn btn-redo" title="از نو (Ctrl+Y)">↷</button>
<div style="width:1px;height:24px;background:var(--border-color);margin:0 4px"></div>
<button id="zoomOutBtn" class="header-bar-btn btn-zoom" title="کوچک‌نمایی">−</button>
<div id="zoomDisplay" class="zoom-display" title="کلیک برای بازنشانی">100%</div>
<button id="zoomInBtn" class="header-bar-btn btn-zoom" title="بزرگ‌نمایی">+</button>
<div style="width:1px;height:24px;background:var(--border-color);margin:0 4px"></div>
<div id="tabsContainer" class="tabs-scroll"></div>
</div>
<div class="flex flex-1 overflow-hidden">
<div id="sidebarWrapper" class="sidebar-wrapper no-print">
<div class="sidebar-inner p-3">
<h2 class="font-bold mb-3 text-base" style="color:var(--text-primary)">🛠️ ابزارها</h2>
<div class="tool-group collapsed" data-group="selection">
<div class="tool-group-header"><span>🖱️</span><span style="flex:1">انتخاب</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn active w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="select" data-types="all"><span>🖱️</span><span>انتخاب و جابجایی</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="uc-elements" data-diagram-types="usecase">
<div class="tool-group-header"><span>📐</span><span style="flex:1">المان‌های Use Case</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="actor" data-types="usecase"><span>👤</span><span>Actor</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="useCase" data-types="usecase"><span>⭕</span><span>Use Case</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="boundary" data-types="usecase"><span>📦</span><span>System Boundary</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="uc-relations" data-diagram-types="usecase">
<div class="tool-group-header"><span>🔗</span><span style="flex:1">روابط Use Case</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="association" data-types="usecase"><span>➖</span><span>Association</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="include" data-types="usecase"><span>🔗</span><span>Include</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="extend" data-types="usecase"><span>🔀</span><span>Extend</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="generalization" data-types="usecase"><span>🔺</span><span>Generalization</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="sm-elements" data-diagram-types="statemachine">
<div class="tool-group-header"><span>🔵</span><span style="flex:1">المان‌های State Machine</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="initialState" data-types="statemachine"><span>⚫</span><span>Initial State</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="state" data-types="statemachine"><span>🔲</span><span>State</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="finalState" data-types="statemachine"><span>🎯</span><span>Final State</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="compositeState" data-types="statemachine"><span>📦</span><span>Composite State</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="sm-relations" data-diagram-types="statemachine">
<div class="tool-group-header"><span>➡️</span><span style="flex:1">روابط State Machine</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="transition" data-types="statemachine"><span>➡️</span><span>Transition</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="act-elements" data-diagram-types="activity">
<div class="tool-group-header"><span>🔷</span><span style="flex:1">المان‌های Activity</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="initialNode" data-types="activity"><span>⚫</span><span>Initial Node</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="finalNode" data-types="activity"><span>🎯</span><span>Final Node</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="action" data-types="activity"><span>▭</span><span>Action</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="decision" data-types="activity"><span>◇</span><span>Decision</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="merge" data-types="activity"><span>◇</span><span>Merge</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="forkJoin" data-types="activity"><span>▬</span><span>Fork/Join</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="act-flows" data-diagram-types="activity">
<div class="tool-group-header"><span>➡️</span><span style="flex:1">جریان‌های Activity</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="controlFlow" data-types="activity"><span>→</span><span>Control Flow</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="objectFlow" data-types="activity"><span>⇢</span><span>Object Flow</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="comp-elements" data-diagram-types="component">
<div class="tool-group-header"><span>🧩</span><span style="flex:1">المان‌های Component</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="component" data-types="component"><span>🧩</span><span>Component</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="interfaceProv" data-types="component"><span>🍭</span><span>Interface Provided</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="interfaceReq" data-types="component"><span>◗</span><span>Interface Required</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="package" data-types="component"><span>📁</span><span>Package</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="comp-relations" data-diagram-types="component">
<div class="tool-group-header"><span>🔗</span><span style="flex:1">روابط Component</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="dependency" data-types="component"><span>⇢</span><span>Dependency</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="connector" data-types="component"><span>—</span><span>Connector</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="compAssociation" data-types="component"><span>➖</span><span>Association</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="dep-elements" data-diagram-types="deployment">
<div class="tool-group-header"><span>🖥️</span><span style="flex:1">المان‌های Deployment</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="node" data-types="deployment"><span>🖥️</span><span>Node</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="device" data-types="deployment"><span>💻</span><span>Device</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="execEnv" data-types="deployment"><span>⚙️</span><span>Execution Env</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="artifact" data-types="deployment"><span>📄</span><span>Artifact</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="dep-relations" data-diagram-types="deployment">
<div class="tool-group-header"><span>🔗</span><span style="flex:1">روابط Deployment</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="commPath" data-types="deployment"><span>⇌</span><span>Communication Path</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="deployRel" data-types="deployment"><span>⇣</span><span>Deployment</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="manifestRel" data-types="deployment"><span>⊳</span><span>Manifestation</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="cls-elements" data-diagram-types="class">
<div class="tool-group-header"><span>🗂️</span><span style="flex:1">المان‌های Class</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="class" data-types="class"><span>🗂️</span><span>Class</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="interfaceClass" data-types="class"><span>◯</span><span>Interface</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="abstractClass" data-types="class"><span>🗂️</span><span>Abstract Class</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="enumeration" data-types="class"><span>📋</span><span>Enumeration</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="cls-relations" data-diagram-types="class">
<div class="tool-group-header"><span>🔗</span><span style="flex:1">روابط Class</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="clsAssociation" data-types="class"><span>➖</span><span>Association</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="aggregation" data-types="class"><span>◇</span><span>Aggregation</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="composition" data-types="class"><span>◆</span><span>Composition</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="inheritance" data-types="class"><span>△</span><span>Inheritance</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="clsDependency" data-types="class"><span>⇢</span><span>Dependency</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="realization" data-types="class"><span>▷</span><span>Realization</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="seq-elements" data-diagram-types="sequence">
<div class="tool-group-header"><span>🔢</span><span style="flex:1">المان‌های Sequence</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="participant" data-types="sequence"><span>▭</span><span>Participant</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="activation" data-types="sequence"><span>▮</span><span>Activation</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="fragment" data-types="sequence"><span>▣</span><span>Combined Fragment</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="seq-messages" data-diagram-types="sequence">
<div class="tool-group-header"><span>➡️</span><span style="flex:1">پیام‌های Sequence</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="syncMsg" data-types="sequence"><span>→●</span><span>Synchronous</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="asyncMsg" data-types="sequence"><span>→○</span><span>Asynchronous</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="returnMsg" data-types="sequence"><span>⇠</span><span>Return</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="selfMsg" data-types="sequence"><span>↷</span><span>Self Message</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="sw-elements" data-diagram-types="swimlane">
<div class="tool-group-header"><span>▤</span><span style="flex:1">المان‌های Swimlane</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swimlane" data-types="swimlane"><span>▤</span><span>Swimlane</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swAction" data-types="swimlane"><span>▭</span><span>Action</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swDecision" data-types="swimlane"><span>◇</span><span>Decision</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swForkJoin" data-types="swimlane"><span>▬</span><span>Fork/Join</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swInitial" data-types="swimlane"><span>⚫</span><span>Initial Node</span></button>
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swFinal" data-types="swimlane"><span>🎯</span><span>Final Node</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="sw-flows" data-diagram-types="swimlane">
<div class="tool-group-header"><span>➡️</span><span style="flex:1">جریان‌های Swimlane</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button class="tool-btn w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm" data-tool="swFlow" data-types="swimlane"><span>→</span><span>Control Flow</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="display">
<div class="tool-group-header"><span>📏</span><span style="flex:1">نوع خط‌کشی بوم</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<div class="grid-type-selector">
<div class="grid-type-option" data-grid-type="none"><span>⊘</span><span>خاموش</span></div>
<div class="grid-type-option" data-grid-type="lines"><span>▦</span><span>خط</span></div>
<div class="grid-type-option active" data-grid-type="dots"><span>⠿</span><span>نقطه</span></div>
</div>
</div>
</div>
<div class="tool-group collapsed" data-group="actions">
<div class="tool-group-header"><span>⚡</span><span style="flex:1">عملیات</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button id="deleteBtn" class="tool-btn tool-btn-red w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>🗑️</span><span>حذف انتخاب شده</span></button>
<button id="clearBtn" class="tool-btn tool-btn-orange w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>🧹</span><span>پاک کردن دیاگرام</span></button>
<button id="exportPngBtn" class="tool-btn tool-btn-purple w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>🖼️</span><span>خروجی PNG</span></button>
</div>
</div>
<div class="tool-group collapsed" data-group="file">
<div class="tool-group-header"><span>💾</span><span style="flex:1">فایل و پروژه</span><span class="tool-group-arrow">▼</span></div>
<div class="tool-group-content">
<button id="fileSettingsBtn" class="tool-btn tool-btn-teal w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>📋</span><span>تنظیمات فایل پروژه</span></button>
<button id="settingsBtn" class="tool-btn tool-btn-cyan w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>⚙️</span><span>تنظیمات دیاگرام</span></button>
<button id="saveBtn" class="tool-btn tool-btn-green w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>💾</span><span>ذخیره پروژه</span></button>
<button id="loadBtn" class="tool-btn tool-btn-blue w-full py-2 px-3 rounded-lg mb-1 flex items-center gap-2 text-sm"><span>📂</span><span>بارگذاری فایل</span></button>
<input type="file" id="fileInput" accept=".json" class="hidden">
</div>
</div>
<div class="themed-card text-xs" style="border-color:rgba(102,126,234,.3)">
<h4 class="font-bold mb-1" style="color:var(--text-primary)">💡 راهنما:</h4>
<ul class="space-y-1 leading-relaxed" style="color:var(--text-muted)">
<li>• <b>Ctrl+Wheel</b> زوم | <b>Space+Drag</b> Pan</li>
<li>• <b>Ctrl+Z/Y</b> Undo/Redo</li>
<li>• <b>دابل‌کلیک Class</b>: ویرایشگر</li>
<li>• <b>دابل‌کلیک Swimlane</b>: تغییر جهت</li>
</ul>
</div>
</div>
</div>
<main class="flex-1 relative overflow-hidden">
<div id="canvasWrapper" class="w-full h-full canvas-container"><canvas id="canvas" class="block"></canvas></div>
<div id="emptyState" class="empty-state absolute inset-0 hidden">
<div class="text-6xl mb-4">📊</div>
<h3 class="text-xl font-bold mb-2" style="color:var(--text-secondary)">هیچ دیاگرامی وجود ندارد</h3>
<p class="text-sm mb-4" style="color:var(--text-muted)">برای شروع، روی دکمه + کلیک کنید</p>
<button onclick="showNewDiagramModal()" class="bg-gradient-to-l from-indigo-600 to-purple-600 text-white py-2 px-6 rounded-lg text-sm font-medium">+ ایجاد اولین دیاگرام</button>
</div>
</main>
</div>
<footer class="status-bar px-4 py-1.5 text-xs no-print flex-shrink-0 z-50">
<div class="flex justify-between items-center"><div id="statusText">آماده</div><div id="diagramInfo">دیاگرام‌ها: 0</div></div>
</footer>
</div>
<!-- New Diagram Modal -->
<div id="newDiagramModal" class="modal-overlay no-print">
<div class="modal-content" style="max-width:720px">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold flex items-center gap-2"><span>📊</span> ایجاد دیاگرام جدید</h3>
<button id="closeNewDiagram" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button>
</div>
<p class="text-sm mb-4" style="color:var(--text-secondary)">نوع نمودار مورد نظر خود را انتخاب کنید:</p>
<div class="grid grid-cols-2 gap-3 mb-4" id="diagramTypeGrid"></div>
<div class="flex gap-3 pt-2">
<button id="confirmNewDiagram" class="flex-1 bg-gradient-to-l from-green-600 to-emerald-600 text-white py-2.5 px-4 rounded-lg text-sm font-medium">✓ ایجاد دیاگرام</button>
<button id="cancelNewDiagram" class="flex-1 py-2.5 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button>
</div>
</div>
</div>
</div>
<!-- Properties Panel -->
<div id="propertiesOverlay" class="properties-overlay no-print">
<div class="panel-shadow rounded-t-2xl border-t border-r border-l" style="border-color:var(--border-color)">
<div class="flex justify-center pt-2 pb-1"><div class="w-10 h-1 rounded-full" style="background:var(--border-color)"></div></div>
<div class="px-5 pb-4">
<div class="flex items-center justify-between mb-3">
<h3 class="font-bold text-sm flex items-center gap-2" style="color:var(--text-primary)"><span>⚙️</span> ویژگی‌های المان</h3>
<button id="closeProperties" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button>
</div>
<div class="grid grid-cols-3 gap-3">
<div><label class="block text-xs mb-1" style="color:var(--text-muted)">نام</label><input type="text" id="elementName" class="themed-input"></div>
<div><label class="block text-xs mb-1" style="color:var(--text-muted)">نوع</label><input type="text" id="elementType" class="themed-input-readonly" readonly></div>
<div><label class="block text-xs mb-1" style="color:var(--text-muted)">موقعیت</label><input type="text" id="elementPosition" class="themed-input-readonly" readonly></div>
</div>
<div id="swimlaneColorSection" style="display:none" class="mt-3 pt-3 border-t" style="border-color:var(--border-color)">
<label class="block text-xs mb-1" style="color:var(--text-muted)">رنگ Swimlane</label>
<input type="color" id="swimlaneColorPicker" class="w-full h-10 rounded cursor-pointer">
</div>
</div>
</div>
</div>
<!-- Diagram Settings Modal -->
<div id="settingsModal" class="modal-overlay no-print"><div class="modal-content"><div class="p-6">
<div class="flex items-center justify-between mb-4"><h3 class="text-lg font-bold"><span>⚙️</span> تنظیمات دیاگرام</h3><button id="closeSettings" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium mb-2">شناسه</label><input type="text" id="diagramId" class="themed-input-readonly" readonly></div>
<div><label class="block text-sm font-medium mb-2">نوع نمودار</label><input type="text" id="diagramType" class="themed-input-readonly" readonly></div>
<div><label class="block text-sm font-medium mb-2">عنوان <span class="text-red-500">*</span></label><input type="text" id="diagramTitle" class="themed-input"></div>
<div><label class="block text-sm font-medium mb-2">توضیحات</label><textarea id="diagramDescription" rows="3" class="themed-input resize-none"></textarea></div>
<div class="flex gap-3 pt-2"><button id="saveSettings" class="flex-1 bg-gradient-to-l from-indigo-600 to-purple-600 text-white py-2 px-4 rounded-lg text-sm font-medium">💾 ذخیره</button><button id="cancelSettings" class="flex-1 py-2 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button></div>
</div>
</div></div></div>
<!-- File Settings Modal -->
<div id="fileSettingsModal" class="modal-overlay no-print"><div class="modal-content"><div class="p-6">
<div class="flex items-center justify-between mb-4"><h3 class="text-lg font-bold"><span>📋</span> تنظیمات فایل پروژه</h3><button id="closeFileSettings" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium mb-2">عنوان پروژه <span class="text-red-500">*</span></label><input type="text" id="fileProjectTitle" class="themed-input"></div>
<div><label class="block text-sm font-medium mb-2">توضیحات</label><textarea id="fileProjectDescription" rows="3" class="themed-input resize-none"></textarea></div>
<div class="grid grid-cols-2 gap-3"><div><label class="block text-sm font-medium mb-2">نویسنده</label><input type="text" id="fileProjectAuthor" class="themed-input"></div><div><label class="block text-sm font-medium mb-2">سازمان</label><input type="text" id="fileProjectOrganization" class="themed-input"></div></div>
<div class="themed-card text-xs space-y-1"><div class="flex justify-between"><span style="color:var(--text-muted)">تعداد:</span><span id="fileProjectDiagramCount" class="font-bold">0</span></div><div class="flex justify-between"><span style="color:var(--text-muted)">ایجاد:</span><span id="fileProjectCreatedAt" class="font-bold">-</span></div><div class="flex justify-between"><span style="color:var(--text-muted)">ویرایش:</span><span id="fileProjectUpdatedAt" class="font-bold">-</span></div></div>
<div class="flex gap-3 pt-2"><button id="saveFileSettings" class="flex-1 bg-gradient-to-l from-teal-600 to-cyan-600 text-white py-2 px-4 rounded-lg text-sm font-medium">💾 ذخیره</button><button id="cancelFileSettings" class="flex-1 py-2 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button></div>
</div>
</div></div></div>
<!-- Save Modal -->
<div id="saveModal" class="modal-overlay no-print"><div class="modal-content save-modal-content"><div class="p-6">
<div class="flex items-center justify-between mb-4"><h3 class="text-lg font-bold"><span>💾</span> ذخیره فایل پروژه</h3><button id="closeSaveModal" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium mb-2">نام فایل <span class="text-red-500">*</span></label><div class="relative"><input type="text" id="saveFileName" class="themed-input pl-14" dir="ltr" style="font-family:monospace"><span class="absolute left-3 top-1/2 -translate-y-1/2 text-xs px-2 py-0.5 rounded" style="color:var(--text-muted);background:var(--bg-tertiary);font-family:monospace">.json</span></div></div>
<div class="file-preview"><div class="w-11 h-11 rounded-xl flex items-center justify-center text-2xl flex-shrink-0" style="background:linear-gradient(135deg,#667eea,#764ba2)">📄</div><div class="flex-1 min-w-0"><div class="text-sm font-semibold truncate" style="font-family:monospace;direction:ltr;text-align:left" id="saveFilePreviewName">-</div><div class="text-xs mt-1" style="color:var(--text-muted)" id="saveFilePreviewMeta">-</div></div></div>
<div class="themed-card text-xs space-y-1.5"><div class="flex justify-between"><span style="color:var(--text-muted)">📊 تعداد:</span><span id="saveDiagramCount" class="font-bold">0</span></div><div class="flex justify-between"><span style="color:var(--text-muted)">📦 حجم:</span><span id="saveFileSize" class="font-bold">-</span></div></div>
<div class="flex gap-3 pt-2"><button id="confirmSaveBtn" class="flex-1 bg-gradient-to-l from-green-600 to-emerald-600 text-white py-2.5 px-4 rounded-lg text-sm font-medium">💾 ذخیره</button><button id="cancelSaveBtn" class="flex-1 py-2.5 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button></div>
</div>
</div></div></div>
<!-- Export PNG Modal -->
<div id="exportPngModal" class="modal-overlay no-print"><div class="modal-content save-modal-content"><div class="p-6">
<div class="flex items-center justify-between mb-4"><h3 class="text-lg font-bold"><span>🖼️</span> خروجی PNG</h3><button id="closeExportPng" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button></div>
<div class="space-y-4">
<div><label class="block text-sm font-medium mb-2">رنگ پس‌زمینه</label><div class="flex items-center gap-3"><input type="color" id="pngBgColor" value="#ffffff" class="w-12 h-10 rounded-lg cursor-pointer border-0 p-0" style="background:transparent"><input type="text" id="pngBgColorText" value="#ffffff" class="themed-input flex-1" dir="ltr" style="font-family:monospace"><button id="pngBgTransparent" class="py-2 px-3 rounded-lg text-xs font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary);border:1px solid var(--border-color)">شفاف</button></div></div>
<div id="pngBgPreview" class="w-full h-16 rounded-lg border-2 flex items-center justify-center text-sm font-medium" style="border-color:var(--border-color);background:#fff;color:#333">پیش‌نمایش</div>
<div class="flex gap-3 pt-2"><button id="confirmExportPng" class="flex-1 bg-gradient-to-l from-purple-600 to-pink-600 text-white py-2.5 px-4 rounded-lg text-sm font-medium">🖼️ دانلود</button><button id="cancelExportPng" class="flex-1 py-2.5 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button></div>
</div>
</div></div></div>
<!-- Class Editor Modal -->
<div id="classEditorModal" class="modal-overlay no-print">
<div class="modal-content class-editor-modal">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold flex items-center gap-2"><span>🗂️</span> ویرایشگر Class</h3>
<button id="closeClassEditor" class="p-1 rounded-lg hover:opacity-70" style="color:var(--text-muted)">✕</button>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">نام Class</label>
<input type="text" id="classEditorName" class="themed-input">
</div>
<div class="class-editor-tabs">
<div class="class-editor-tab active" data-tab="attributes">📋 Attributes</div>
<div class="class-editor-tab" data-tab="methods">⚙️ Methods</div>
</div>
<div id="attributesTab" class="class-editor-panel">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium" style="color:var(--text-secondary)">ویژگی‌ها</span>
<button id="addAttrBtn" class="member-btn member-btn-add">+ افزودن</button>
</div>
<table class="member-table">
<thead><tr><th style="width:60px">Visibility</th><th>نام</th><th style="width:100px">نوع</th><th style="width:50px">Static</th><th style="width:40px"></th></tr></thead>
<tbody id="attributesTableBody"></tbody>
</table>
</div>
<div id="methodsTab" class="class-editor-panel" style="display:none">
<div class="flex justify-between items-center mb-2">
<span class="text-sm font-medium" style="color:var(--text-secondary)">متدها</span>
<button id="addMethodBtn" class="member-btn member-btn-add">+ افزودن</button>
</div>
<table class="member-table">
<thead><tr><th style="width:60px">Visibility</th><th>نام</th><th style="width:120px">پارامترها</th><th style="width:80px">نوع بازگشت</th><th style="width:50px">Static</th><th style="width:40px"></th></tr></thead>
<tbody id="methodsTableBody"></tbody>
</table>
</div>
<div class="flex gap-3 pt-4 mt-4" style="border-top:1px solid var(--border-color)">
<button id="saveClassEditor" class="flex-1 bg-gradient-to-l from-indigo-600 to-purple-600 text-white py-2 px-4 rounded-lg text-sm font-medium">💾 ذخیره تغییرات</button>
<button id="cancelClassEditor" class="flex-1 py-2 px-4 rounded-lg text-sm font-medium" style="background:var(--bg-tertiary);color:var(--text-secondary)">❌ انصراف</button>
</div>
</div>
</div>
</div>
<script>
// ===== DIAGRAM TYPES REGISTRY =====
const DIAGRAM_TYPES = {
usecase: { id:'usecase', name:'نمودار مورد کاربرد (Use Case)', nameShort:'Use Case', icon:'⭕', available:true,
tools:['select','actor','useCase','boundary','association','include','extend','generalization'],
groups:['selection','uc-elements','uc-relations','display','actions','file'] },
statemachine: { id:'statemachine', name:'نمودار ماشین حالت (State Machine)', nameShort:'State Machine', icon:'🔄', available:true,
tools:['select','initialState','state','finalState','compositeState','transition'],
groups:['selection','sm-elements','sm-relations','display','actions','file'] },
activity: { id:'activity', name:'نمودار فعالیت (Activity Diagram)', nameShort:'Activity', icon:'🔀', available:true,
tools:['select','initialNode','finalNode','action','decision','merge','forkJoin','controlFlow','objectFlow'],
groups:['selection','act-elements','act-flows','display','actions','file'] },
component: { id:'component', name:'نمودار کامپوننت (Component Diagram)', nameShort:'Component', icon:'🧩', available:true,
tools:['select','component','interfaceProv','interfaceReq','package','dependency','connector','compAssociation'],
groups:['selection','comp-elements','comp-relations','display','actions','file'] },
deployment: { id:'deployment', name:'نمودار استقرار (Deployment Diagram)', nameShort:'Deployment', icon:'🖥️', available:true,
tools:['select','node','device','execEnv','artifact','commPath','deployRel','manifestRel'],
groups:['selection','dep-elements','dep-relations','display','actions','file'] },
class: { id:'class', name:'نمودار کلاس (Class Diagram)', nameShort:'Class', icon:'🗂️', available:true,
tools:['select','class','interfaceClass','abstractClass','enumeration','clsAssociation','aggregation','composition','inheritance','clsDependency','realization'],
groups:['selection','cls-elements','cls-relations','display','actions','file'] },
sequence: { id:'sequence', name:'نمودار توالی (Sequence Diagram)', nameShort:'Sequence', icon:'🔢', available:true,
tools:['select','participant','activation','fragment','syncMsg','asyncMsg','returnMsg','selfMsg'],
groups:['selection','seq-elements','seq-messages','display','actions','file'] },
swimlane: { id:'swimlane', name:'نمودار Swimlane', nameShort:'Swimlane', icon:'▤', available:true,
tools:['select','swimlane','swAction','swDecision','swForkJoin','swInitial','swFinal','swFlow'],
groups:['selection','sw-elements','sw-flows','display','actions','file'] }
};
// ===== STATE =====
const state = {
diagrams: [], activeDiagramId: null, currentTool: 'select',
selectedElement: null, isDragging: false, dragStart: null,
isDrawingRelationship: false, relationshipStart: null, relationshipStartId: null,
mousePos: { x:0, y:0 }, sidebarVisible: true, gridType: 'dots', darkMode: true,
collapsedGroups: new Set(), newDiagramSelectedType: 'usecase',
editingClassId: null, resizingSwimlane: null, resizeHandle: null,
// ZOOM & PAN
zoom: 1.0, panX: 0, panY: 0, isPanning: false, panStart: null, spacePressed: false,
fileMetadata: { title:'', description:'', author:'هادی خزاعی اصل', organization:'رایان پرداز کاوش', createdAt:new Date().toISOString(), updatedAt:new Date().toISOString() }
};
// ===== UTILITIES =====
function genId(){ return 'diagram-'+Date.now()+'-'+Math.random().toString(36).substr(2,9); }
function genElId(d){ return d.nextId++; }
function genTS(){ const n=new Date(); return `${n.getFullYear()}${String(n.getMonth()+1).padStart(2,'0')}${String(n.getDate()).padStart(2,'0')}-${String(n.getHours()).padStart(2,'0')}${String(n.getMinutes()).padStart(2,'0')}`; }
function sanitize(n){ return n.replace(/[\\/:*?"<>|]/g,'_').replace(/\s+/g,' ').trim().substring(0,100); }
function fmtDate(iso){ if(!iso) return '-'; try{ return new Date(iso).toLocaleDateString('fa-IR',{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit'}); }catch{return '-';} }
function fmtSize(b){ if(b<1024) return b+' B'; if(b<1048576) return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(2)+' MB'; }
function getActive(){ return state.diagrams.find(d=>d.id===state.activeDiagramId)||null; }
// ===== HISTORY (UNDO/REDO) =====
const MAX_HISTORY = 50;
function getDiagramData(d){
// Extract only the data fields (not history/metadata)
return {
actors:d.actors||[], useCases:d.useCases||[], systemBoundary:d.systemBoundary||null,
states:d.states||[], transitions:d.transitions||[],
nodes:d.nodes||[], flows:d.flows||[],
components:d.components||[], interfaces:d.interfaces||[], packages:d.packages||[], compRelations:d.compRelations||[],
deployNodes:d.deployNodes||[], artifacts:d.artifacts||[], deployRelations:d.deployRelations||[],
classes:d.classes||[], classRelations:d.classRelations||[],
participants:d.participants||[], activations:d.activations||[], messages:d.messages||[], fragments:d.fragments||[],
swimlanes:d.swimlanes||[], swNodes:d.swNodes||[], swFlows:d.swFlows||[],
relationships:d.relationships||[], nextId:d.nextId
};
}
function pushHistory(d){
if(!d) return;
if(!d._history) d._history = [];
if(!d._historyIndex && d._historyIndex !== 0) d._historyIndex = -1;
// Remove any future states (if we undid some)
d._history = d._history.slice(0, d._historyIndex + 1);
// Push current state
d._history.push(JSON.stringify(getDiagramData(d)));
// Limit history size
if(d._history.length > MAX_HISTORY){
d._history.shift();
} else {
d._historyIndex++;
}
}
function restoreFromHistory(d, snapshot){
const data = JSON.parse(snapshot);
Object.assign(d, data);
}
function canUndo(){
const d = getActive();
return d && d._history && d._historyIndex > 0;
}
function canRedo(){
const d = getActive();
return d && d._history && d._historyIndex < d._history.length - 1;
}
function undo(){
const d = getActive();
if(!canUndo()) return;
d._historyIndex--;
restoreFromHistory(d, d._history[d._historyIndex]);
state.selectedElement = null;
hideProps();
draw();
updateStatus('↶ بازگشت');
}
function redo(){
const d = getActive();
if(!canRedo()) return;
d._historyIndex++;
restoreFromHistory(d, d._history[d._historyIndex]);
state.selectedElement = null;
hideProps();
draw();
updateStatus('↷ از نو');
}
// ===== THEME =====
function applyTheme(){ document.body.classList.toggle('dark',state.darkMode); document.getElementById('themeToggleBtn').textContent=state.darkMode?'☀️':'🌙'; draw(); }
document.getElementById('themeToggleBtn').addEventListener('click',()=>{ state.darkMode=!state.darkMode; applyTheme(); });
// ===== GRID =====
document.querySelectorAll('.grid-type-option').forEach(o=>o.addEventListener('click',()=>{ state.gridType=o.dataset.gridType; document.querySelectorAll('.grid-type-option').forEach(x=>x.classList.toggle('active',x===o)); draw(); }));
function setGridUI(t){ document.querySelectorAll('.grid-type-option').forEach(o=>o.classList.toggle('active',o.dataset.gridType===t)); }
// ===== COLLAPSIBLE GROUPS =====
document.querySelectorAll('.tool-group-header').forEach(h=>h.addEventListener('click',()=>{ const g=h.parentElement; const id=g.dataset.group; if(state.collapsedGroups.has(id)){state.collapsedGroups.delete(id);g.classList.remove('collapsed');}else{state.collapsedGroups.add(id);g.classList.add('collapsed');} }));
// ===== TOOL VISIBILITY =====
function updateToolVisibility(){
const d = getActive();
const allGroups = document.querySelectorAll('.tool-group');
const allBtns = document.querySelectorAll('.tool-btn[data-tool]');
if(!d){
// Hide all diagram-specific groups, show only selection/display/actions/file
allGroups.forEach(g=>{
const dt=g.dataset.diagramTypes;
if(dt) g.style.display='none';
else g.style.display='';
});
allBtns.forEach(b=>{ b.style.display = b.dataset.tool==='select'?'':'none'; });
return;
}
const dt = DIAGRAM_TYPES[d.diagramType];
if(!dt) return;
const allowedGroups = dt.groups;
allGroups.forEach(g=>{
const req = g.dataset.diagramTypes;
if(!req){ g.style.display=''; return; }
g.style.display = allowedGroups.includes(g.dataset.group)?'':'none';
});
const allowedTools = dt.tools;
allBtns.forEach(b=>{
if(b.dataset.tool==='select'){ b.style.display=''; return; }
const req = b.dataset.types;
if(!req||req==='all'){ b.style.display=''; return; }
const types = req.split(',');
b.style.display = types.includes(d.diagramType)?'':'none';
if(b.style.display==='none' && state.currentTool===b.dataset.tool) setCurrentTool('select');
});
}
// ===== ZOOM & PAN =====
const MIN_ZOOM = 0.25;
const MAX_ZOOM = 4.0;
function setZoom(newZoom, centerX, centerY){
const oldZoom = state.zoom;
newZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
if(centerX !== undefined && centerY !== undefined){
// Zoom towards center point
const worldX = (centerX - state.panX) / oldZoom;
const worldY = (centerY - state.panY) / oldZoom;
state.panX = centerX - worldX * newZoom;
state.panY = centerY - worldY * newZoom;
}
state.zoom = newZoom;
updateZoomDisplay();
draw();
}
function resetZoom(){
state.zoom = 1.0;
state.panX = 0;
state.panY = 0;
updateZoomDisplay();
draw();
}
function updateZoomDisplay(){
document.getElementById('zoomDisplay').textContent = Math.round(state.zoom * 100) + '%';
}
document.getElementById('zoomInBtn').addEventListener('click', ()=>{
const rect = canvas.getBoundingClientRect();
setZoom(state.zoom * 1.2, rect.width/2, rect.height/2);
});
document.getElementById('zoomOutBtn').addEventListener('click', ()=>{
const rect = canvas.getBoundingClientRect();
setZoom(state.zoom / 1.2, rect.width/2, rect.height/2);
});
document.getElementById('zoomDisplay').addEventListener('click', resetZoom);
// ===== NEW DIAGRAM MODAL =====
function buildDiagramTypeGrid(){
const grid = document.getElementById('diagramTypeGrid');
grid.innerHTML = '';
Object.values(DIAGRAM_TYPES).forEach(dt=>{
const card = document.createElement('div');
card.className = 'diagram-type-card' + (dt.available?'':' disabled') + (dt.id===state.newDiagramSelectedType&&dt.available?' selected':'');
card.dataset.diagramType = dt.id;
card.innerHTML = `<div class="flex items-start gap-3">
<div class="diagram-type-icon">${dt.icon}</div>
<div class="flex-1">
<h4 class="font-bold text-sm mb-1" style="color:var(--text-primary)">${dt.name}</h4>
${dt.available?'':'<span style="display:inline-block;font-size:9px;padding:2px 6px;border-radius:4px;background:var(--bg-tertiary);color:var(--text-muted);font-weight:600;margin-top:6px">به‌زودی</span>'}
</div>
${dt.available&&dt.id===state.newDiagramSelectedType?'<div style="color:#667eea">✓</div>':''}
</div>`;
if(dt.available) card.addEventListener('click',()=>{ state.newDiagramSelectedType=dt.id; buildDiagramTypeGrid(); });
grid.appendChild(card);
});
}
function showNewDiagramModal(){ state.newDiagramSelectedType='usecase'; buildDiagramTypeGrid(); document.getElementById('newDiagramModal').classList.add('visible'); }
function hideNewDiagramModal(){ document.getElementById('newDiagramModal').classList.remove('visible'); }
document.getElementById('newDiagramBtn').addEventListener('click', showNewDiagramModal);
document.getElementById('closeNewDiagram').addEventListener('click', hideNewDiagramModal);
document.getElementById('cancelNewDiagram').addEventListener('click', hideNewDiagramModal);
document.getElementById('newDiagramModal').addEventListener('click', e=>{ if(e.target.id==='newDiagramModal') hideNewDiagramModal(); });
document.getElementById('confirmNewDiagram').addEventListener('click',()=>{
const dt=DIAGRAM_TYPES[state.newDiagramSelectedType];
if(!dt||!dt.available){ alert('این نوع نمودار هنوز پشتیبانی نمی‌شود'); return; }
hideNewDiagramModal(); createNewDiagram(state.newDiagramSelectedType);
});
// ===== DIAGRAM MANAGEMENT =====
function createNewDiagram(type='usecase'){
const dt=DIAGRAM_TYPES[type]; if(!dt) return;
const count=state.diagrams.filter(d=>d.diagramType===type).length+1;
const nd = {
id:genId(), diagramType:type, title:`${dt.nameShort} جدید ${count}`, description:'',
createdAt:new Date().toISOString(), updatedAt:new Date().toISOString(),
author:state.fileMetadata.author, version:'1.0', nextId:1,
actors:[], useCases:[], systemBoundary:null,
states:[], transitions:[],
nodes:[], flows:[],
components:[], interfaces:[], packages:[], compRelations:[],
deployNodes:[], artifacts:[], deployRelations:[],
classes:[], classRelations:[],
participants:[], activations:[], messages:[], fragments:[],
swimlanes:[], swNodes:[], swFlows:[],
relationships:[],
_history: [], _historyIndex: -1
};
state.diagrams.push(nd);
pushHistory(nd); // Initial state
state.fileMetadata.updatedAt=new Date().toISOString();
switchDiagram(nd.id); renderTabs(); updateInfo();
updateStatus(`✅ ${dt.nameShort}: "${nd.title}" ایجاد شد`);
}
function deleteDiagram(id){
const d=state.diagrams.find(x=>x.id===id); if(!d) return;
if(!confirm(`حذف "${d.title}"؟`)) return;
state.diagrams=state.diagrams.filter(x=>x.id!==id);
if(state.activeDiagramId===id){
if(state.diagrams.length>0) switchDiagram(state.diagrams[0].id);
else{
state.activeDiagramId=null;
state.selectedElement=null;
hideProps();
// Reset zoom/pan
state.zoom = 1.0;
state.panX = 0;
state.panY = 0;
updateZoomDisplay();
updateToolVisibility(); // Hide diagram-specific tools
}
}
renderTabs(); updateInfo(); updateVis(); updateStatus('🗑️ حذف شد');
}
function switchDiagram(id){
if(state.activeDiagramId===id) return;
state.activeDiagramId=id; state.selectedElement=null; state.currentTool='select';
state.isDragging=false; state.isDrawingRelationship=false;
// Reset zoom/pan when switching diagrams
state.zoom = 1.0;
state.panX = 0;
state.panY = 0;
updateZoomDisplay();
setActiveUI('select'); hideProps(); renderTabs(); updateVis(); updateToolVisibility();
const d=getActive(); if(d){ const dt=DIAGRAM_TYPES[d.diagramType]; updateStatus(`📊 ${dt?.nameShort}: ${d.title}`); }
scheduleResize();
}
function updateVis(){ const c=document.getElementById('canvasWrapper'),e=document.getElementById('emptyState'); if(!state.diagrams.length){c.classList.add('hidden');e.classList.remove('hidden');}else{c.classList.remove('hidden');e.classList.add('hidden');} }
function toggleSidebar(){ state.sidebarVisible=!state.sidebarVisible; document.getElementById('sidebarWrapper').classList.toggle('collapsed',!state.sidebarVisible); setTimeout(scheduleResize,320); }
document.getElementById('toggleSidebarBtn').addEventListener('click', toggleSidebar);
// Undo/Redo buttons
document.getElementById('undoBtn').addEventListener('click', undo);
document.getElementById('redoBtn').addEventListener('click', redo);
// ===== TABS =====
function renderTabs(){
const c=document.getElementById('tabsContainer'); c.innerHTML='';
state.diagrams.forEach(d=>{
const dt=DIAGRAM_TYPES[d.diagramType]||DIAGRAM_TYPES.usecase;
const tab=document.createElement('div');
tab.className='diagram-tab'+(d.id===state.activeDiagramId?' active':'');
tab.innerHTML=`<span style="font-size:14px">${dt.icon}</span><span class="tab-title" title="${d.title}">${d.title||'بدون عنوان'}</span><span class="tab-type-badge">${dt.nameShort}</span><span class="tab-close" title="حذف">×</span>`;
tab.querySelector('.tab-close').addEventListener('click',e=>{e.stopPropagation();deleteDiagram(d.id);});
tab.addEventListener('click',()=>switchDiagram(d.id));
tab.addEventListener('dblclick',e=>{e.stopPropagation();switchDiagram(d.id);showSettings();});
c.appendChild(tab);
});
const at=c.querySelector('.diagram-tab.active'); if(at) at.scrollIntoView({behavior:'smooth',block:'nearest',inline:'center'});
}
function updateInfo(){
const a=getActive(); let info=`دیاگرام‌ها: ${state.diagrams.length}`;
if(state.fileMetadata.title) info=`📁 ${state.fileMetadata.title} | `+info;
if(a){ const dt=DIAGRAM_TYPES[a.diagramType]||DIAGRAM_TYPES.usecase;
let ec=0;
if(a.diagramType==='usecase') ec=a.actors.length+a.useCases.length+(a.systemBoundary?1:0)+a.relationships.length;
else if(a.diagramType==='statemachine') ec=a.states.length+a.transitions.length;
else if(a.diagramType==='activity') ec=(a.nodes||[]).length+(a.flows||[]).length;
else if(a.diagramType==='component') ec=(a.components||[]).length+(a.interfaces||[]).length+(a.packages||[]).length+(a.compRelations||[]).length;
else if(a.diagramType==='deployment') ec=(a.deployNodes||[]).length+(a.artifacts||[]).length+(a.deployRelations||[]).length;
else if(a.diagramType==='class') ec=(a.classes||[]).length+(a.classRelations||[]).length;
else if(a.diagramType==='sequence') ec=(a.participants||[]).length+(a.activations||[]).length+(a.messages||[]).length+(a.fragments||[]).length;
else if(a.diagramType==='swimlane') ec=(a.swimlanes||[]).length+(a.swNodes||[]).length+(a.swFlows||[]).length;
info+=` | ${dt.nameShort}: ${a.title} | المان‌ها: ${ec}`;
}
document.getElementById('diagramInfo').textContent=info;
}
function updateStatus(t){ document.getElementById('statusText').textContent=t; }
// ===== CANVAS =====
const canvas=document.getElementById('canvas'), ctx=canvas.getContext('2d'), cw=document.getElementById('canvasWrapper');
function resizeCanvas(){ if(cw.classList.contains('hidden')||cw.offsetParent===null) return false; const w=cw.clientWidth,h=cw.clientHeight; if(w<=0||h<=0) return false; if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;} draw(); return true; }
function scheduleResize(){ requestAnimationFrame(()=>requestAnimationFrame(()=>{ if(!resizeCanvas()) setTimeout(resizeCanvas,50); })); }
window.addEventListener('resize', scheduleResize);
// ===== PROPERTIES =====
const propsOvl=document.getElementById('propertiesOverlay');
function showProps(){ propsOvl.classList.add('visible'); }
function hideProps(){ propsOvl.classList.remove('visible'); }
document.getElementById('closeProperties').addEventListener('click',()=>{ state.selectedElement=null; hideProps(); draw(); });
function updatePropsPanel(){
if(!state.selectedElement){ hideProps(); return; }
showProps();
document.getElementById('elementName').value=state.selectedElement.name||'';
const tn={actor:'Actor',useCase:'Use Case',systemBoundary:'System Boundary',initialState:'Initial State',state:'State',finalState:'Final State',compositeState:'Composite State',initialNode:'Initial Node',finalNode:'Final Node',action:'Action',decision:'Decision',merge:'Merge',forkJoin:'Fork/Join',component:'Component',interfaceProv:'Interface Provided',interfaceReq:'Interface Required',package:'Package',node:'Node',device:'Device',execEnv:'Execution Env',artifact:'Artifact',class:'Class',interfaceClass:'Interface',abstractClass:'Abstract Class',enumeration:'Enumeration',participant:'Participant',activation:'Activation',fragment:'Combined Fragment',swimlane:'Swimlane',swAction:'Action',swDecision:'Decision',swForkJoin:'Fork/Join',swInitial:'Initial Node',swFinal:'Final Node'};
document.getElementById('elementType').value=tn[state.selectedElement.elType]||state.selectedElement.elType;
document.getElementById('elementPosition').value=state.selectedElement.x!==undefined?`${Math.round(state.selectedElement.x)}, ${Math.round(state.selectedElement.y)}`:'N/A';
const colorSection = document.getElementById('swimlaneColorSection');
if(state.selectedElement.elType==='swimlane'){
colorSection.style.display='block';
document.getElementById('swimlaneColorPicker').value=state.selectedElement.color||'#ebf8ff';
} else {
colorSection.style.display='none';
}
}
document.getElementById('swimlaneColorPicker').addEventListener('input',e=>{
if(!state.selectedElement||state.selectedElement.elType!=='swimlane') return;
const d=getActive(); if(!d) return;
const i=(d.swimlanes||[]).findIndex(s=>s.id===state.selectedElement.id);
if(i>=0){ d.swimlanes[i].color=e.target.value; draw(); }
});
// ===== TOOL SELECTION =====
const toolNames={select:'انتخاب',actor:'Actor',useCase:'Use Case',boundary:'Boundary',association:'Association',include:'Include',extend:'Extend',generalization:'Generalization',initialState:'Initial State',state:'State',finalState:'Final State',compositeState:'Composite State',transition:'Transition',initialNode:'Initial Node',finalNode:'Final Node',action:'Action',decision:'Decision',merge:'Merge',forkJoin:'Fork/Join',controlFlow:'Control Flow',objectFlow:'Object Flow',component:'Component',interfaceProv:'Interface Provided',interfaceReq:'Interface Required',package:'Package',dependency:'Dependency',connector:'Connector',compAssociation:'Association',node:'Node',device:'Device',execEnv:'Execution Env',artifact:'Artifact',commPath:'Communication Path',deployRel:'Deployment',manifestRel:'Manifestation',class:'Class',interfaceClass:'Interface',abstractClass:'Abstract Class',enumeration:'Enumeration',clsAssociation:'Association',aggregation:'Aggregation',composition:'Composition',inheritance:'Inheritance',clsDependency:'Dependency',realization:'Realization',participant:'Participant',activation:'Activation',fragment:'Fragment',syncMsg:'Synchronous',asyncMsg:'Asynchronous',returnMsg:'Return',selfMsg:'Self Message',swimlane:'Swimlane',swAction:'Action',swDecision:'Decision',swForkJoin:'Fork/Join',swInitial:'Initial Node',swFinal:'Final Node',swFlow:'Control Flow'};
function setActiveUI(t){ document.querySelectorAll('.tool-btn').forEach(b=>b.classList.remove('active')); const b=document.getElementById(t+'Tool'); if(b) b.classList.add('active'); }
function setCurrentTool(t){ state.currentTool=t; setActiveUI(t); updateStatus(`آماده | ابزار: ${toolNames[t]||t}`); }
function switchToSelect(){ setCurrentTool('select'); }
document.querySelectorAll('.tool-btn[data-tool]').forEach(b=>b.addEventListener('click',()=>setCurrentTool(b.dataset.tool)));
// ===== MODALS =====
const settingsModal=document.getElementById('settingsModal');
function showSettings(){ const d=getActive(); if(!d){alert('دیاگرامی نیست');return;} const dt=DIAGRAM_TYPES[d.diagramType]||DIAGRAM_TYPES.usecase; document.getElementById('diagramId').value=d.id; document.getElementById('diagramType').value=dt.name; document.getElementById('diagramTitle').value=d.title; document.getElementById('diagramDescription').value=d.description; settingsModal.classList.add('visible'); }
function hideSettings(){ settingsModal.classList.remove('visible'); }
document.getElementById('settingsBtn').addEventListener('click',showSettings);
document.getElementById('closeSettings').addEventListener('click',hideSettings);
document.getElementById('cancelSettings').addEventListener('click',hideSettings);
document.getElementById('saveSettings').addEventListener('click',()=>{
const d=getActive(); if(!d) return;
const t=document.getElementById('diagramTitle').value.trim();
if(!t){alert('عنوان را وارد کنید');return;}
d.title=t; d.description=document.getElementById('diagramDescription').value.trim();
d.updatedAt=new Date().toISOString();
pushHistory(d); // Save change
hideSettings(); renderTabs(); updateInfo(); updateStatus('✅ ذخیره شد');
});
settingsModal.addEventListener('click',e=>{if(e.target===settingsModal)hideSettings();});
const fileSettingsModal=document.getElementById('fileSettingsModal');
function showFileSettings(){ document.getElementById('fileProjectTitle').value=state.fileMetadata.title||''; document.getElementById('fileProjectDescription').value=state.fileMetadata.description||''; document.getElementById('fileProjectAuthor').value=state.fileMetadata.author||''; document.getElementById('fileProjectOrganization').value=state.fileMetadata.organization||''; document.getElementById('fileProjectDiagramCount').textContent=state.diagrams.length; document.getElementById('fileProjectCreatedAt').textContent=fmtDate(state.fileMetadata.createdAt); document.getElementById('fileProjectUpdatedAt').textContent=fmtDate(state.fileMetadata.updatedAt); fileSettingsModal.classList.add('visible'); }
function hideFileSettings(){ fileSettingsModal.classList.remove('visible'); }
document.getElementById('fileSettingsBtn').addEventListener('click',showFileSettings);
document.getElementById('closeFileSettings').addEventListener('click',hideFileSettings);
document.getElementById('cancelFileSettings').addEventListener('click',hideFileSettings);
document.getElementById('saveFileSettings').addEventListener('click',()=>{ const t=document.getElementById('fileProjectTitle').value.trim(); if(!t){alert('عنوان را وارد کنید');return;} state.fileMetadata.title=t; state.fileMetadata.description=document.getElementById('fileProjectDescription').value.trim(); state.fileMetadata.author=document.getElementById('fileProjectAuthor').value.trim()||'هادی خزاعی اصل'; state.fileMetadata.organization=document.getElementById('fileProjectOrganization').value.trim()||'رایان پرداز کاوش'; state.fileMetadata.updatedAt=new Date().toISOString(); hideFileSettings(); updateInfo(); updateStatus('✅ ذخیره شد'); });
fileSettingsModal.addEventListener('click',e=>{if(e.target===fileSettingsModal)hideFileSettings();});
const saveModal=document.getElementById('saveModal'), saveFN=document.getElementById('saveFileName');
function showSaveModal(){ if(!state.diagrams.length){alert('دیاگرامی نیست');return;} const unt=state.diagrams.filter(d=>!d.title?.trim()); if(unt.length){alert(`${unt.length} دیاگرام بدون عنوان`);return;} saveFN.value=(state.fileMetadata.title?sanitize(state.fileMetadata.title):'uml-diagrams')+'-'+genTS(); updateSavePreview(); document.getElementById('saveDiagramCount').textContent=state.diagrams.length; saveModal.classList.add('visible'); setTimeout(()=>{saveFN.focus();saveFN.select();},100); }
function hideSaveModal(){ saveModal.classList.remove('visible'); }
function buildSaveData(){
return {
version:'5.2', fileTitle:state.fileMetadata.title, fileDescription:state.fileMetadata.description,
author:state.fileMetadata.author, organization:state.fileMetadata.organization,
createdAt:state.fileMetadata.createdAt, updatedAt:new Date().toISOString(),
preferences:{gridType:state.gridType,darkMode:state.darkMode},
activeDiagramId:state.activeDiagramId,
diagrams:state.diagrams.map(d=>{
const data = getDiagramData(d);
return {
id:d.id, diagramType:d.diagramType, title:d.title, description:d.description,
createdAt:d.createdAt, updatedAt:new Date().toISOString(),
author:d.author, version:d.version,
...data
};
})
};
}
function updateSavePreview(){ const sn=sanitize(saveFN.value.trim()); document.getElementById('saveFilePreviewName').textContent=sn?sn+'.json':'(نام وارد نشده)'; document.getElementById('saveFilePreviewMeta').textContent=`JSON • ${new Date().toLocaleString('fa-IR',{year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}`; try{document.getElementById('saveFileSize').textContent=fmtSize(new Blob([JSON.stringify(buildSaveData(),null,2)]).size);}catch{document.getElementById('saveFileSize').textContent='-';} }
saveFN.addEventListener('input',updateSavePreview);
document.getElementById('closeSaveModal').addEventListener('click',hideSaveModal);
document.getElementById('cancelSaveBtn').addEventListener('click',hideSaveModal);
saveModal.addEventListener('click',e=>{if(e.target===saveModal)hideSaveModal();});
document.getElementById('confirmSaveBtn').addEventListener('click',()=>{ const sn=sanitize(saveFN.value.trim()); if(!sn){alert('نام معتبر');saveFN.focus();return;} const b=new Blob([JSON.stringify(buildSaveData(),null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(b); a.download=sn+'.json'; a.click(); URL.revokeObjectURL(a.href); hideSaveModal(); updateStatus(`✅ "${sn}.json" ذخیره شد`); });
document.getElementById('saveBtn').addEventListener('click',showSaveModal);
// Export PNG
const expModal=document.getElementById('exportPngModal'), pngC=document.getElementById('pngBgColor'), pngCT=document.getElementById('pngBgColorText'), pngP=document.getElementById('pngBgPreview');
let pngTrans=false;
function updPng(){ const c=pngC.value; pngCT.value=c; pngP.style.background=c; const r=parseInt(c.substr(1,2),16),g=parseInt(c.substr(3,2),16),b=parseInt(c.substr(5,2),16); pngP.style.color=(0.299*r+0.587*g+0.114*b)/255>0.5?'#333':'#eee'; pngTrans=false; }
pngC.addEventListener('input',updPng);
pngCT.addEventListener('input',()=>{ if(/^#[0-9a-fA-F]{6}$/.test(pngCT.value.trim())){pngC.value=pngCT.value.trim();updPng();} });
document.getElementById('pngBgTransparent').addEventListener('click',()=>{ pngTrans=true; pngP.style.background='repeating-conic-gradient(#ccc 0% 25%,#fff 0% 50%) 0 0/16px 16px'; pngP.style.color='#666'; pngCT.value='transparent'; });
function showExpPng(){ const d=getActive(); if(!d){alert('دیاگرامی نیست');return;} pngC.value=state.darkMode?'#1a202c':'#ffffff'; pngTrans=false; updPng(); expModal.classList.add('visible'); }
function hideExpPng(){ expModal.classList.remove('visible'); }
document.getElementById('exportPngBtn').addEventListener('click',showExpPng);
document.getElementById('closeExportPng').addEventListener('click',hideExpPng);
document.getElementById('cancelExportPng').addEventListener('click',hideExpPng);
expModal.addEventListener('click',e=>{if(e.target===expModal)hideExpPng();});
document.getElementById('confirmExportPng').addEventListener('click',()=>{
const d=getActive(); if(!d) return;
const ts=state.selectedElement; state.selectedElement=null;
// Export without zoom/pan (1:1 scale)
const oc=document.createElement('canvas'); oc.width=canvas.width; oc.height=canvas.height; const ox=oc.getContext('2d');
if(!pngTrans){ox.fillStyle=pngC.value;ox.fillRect(0,0,oc.width,oc.height);}
const tc=getTC(); drawGrid(ox,oc.width,oc.height,tc,pngTrans);
// Draw diagram at 1:1 scale (no zoom)
const savedZoom = state.zoom, savedPanX = state.panX, savedPanY = state.panY;
state.zoom = 1; state.panX = 0; state.panY = 0;
drawDiag(ox,d,tc);
state.zoom = savedZoom; state.panX = savedPanX; state.panY = savedPanY;
const a=document.createElement('a'); a.download=`${d.title}-${Date.now()}.png`; a.href=oc.toDataURL('image/png'); a.click();
state.selectedElement=ts; draw(); hideExpPng(); updateStatus('✅ PNG ذخیره شد');
});
// Load
document.getElementById('loadBtn').addEventListener('click',()=>document.getElementById('fileInput').click());
document.getElementById('fileInput').addEventListener('change',e=>{
const f=e.target.files[0]; if(!f) return;
if(state.diagrams.length&&!confirm('جایگزین شود؟')){e.target.value='';return;}
const r=new FileReader(); r.onload=ev=>{
try{
const d=JSON.parse(ev.target.result);
state.fileMetadata={title:d.fileTitle||d.title||'',description:d.fileDescription||d.description||'',author:d.author||'هادی خزاعی اصل',organization:d.organization||'رایان پرداز کاوش',createdAt:d.createdAt||new Date().toISOString(),updatedAt:d.updatedAt||new Date().toISOString()};
if(d.preferences){if(d.preferences.gridType){state.gridType=d.preferences.gridType;setGridUI(state.gridType);}if(typeof d.preferences.darkMode==='boolean'){state.darkMode=d.preferences.darkMode;applyTheme();}}
if(d.diagrams&&Array.isArray(d.diagrams)){
state.diagrams=d.diagrams.map(di=>{
const loaded = {
...di,
diagramType:di.diagramType||di.type||'usecase',
states:di.states||[], transitions:di.transitions||[],
actors:di.actors||[], useCases:di.useCases||[],
nodes:di.nodes||[], flows:di.flows||[],
components:di.components||[], interfaces:di.interfaces||[], packages:di.packages||[], compRelations:di.compRelations||[],
deployNodes:di.deployNodes||[], artifacts:di.artifacts||[], deployRelations:di.deployRelations||[],
classes:di.classes||[], classRelations:di.classRelations||[],
participants:di.participants||[], activations:di.activations||[], messages:di.messages||[], fragments:di.fragments||[],
swimlanes:di.swimlanes||[], swNodes:di.swNodes||[], swFlows:di.swFlows||[],
_history: [], _historyIndex: -1
};
return loaded;
});
state.activeDiagramId=d.activeDiagramId||state.diagrams[0]?.id||null;
// Initialize history for loaded diagrams
state.diagrams.forEach(di => pushHistory(di));
} else if(d.id||d.actors){
const loaded = {
id:d.id||genId(), diagramType:d.diagramType||d.type||'usecase',
title:d.title||'وارد شده', description:d.description||'',
createdAt:d.createdAt||new Date().toISOString(), updatedAt:new Date().toISOString(),
author:d.author||'هادی خزاعی اصل', version:'1.0',
actors:d.actors||[], useCases:d.useCases||[], systemBoundary:d.systemBoundary||null,
states:d.states||[], transitions:d.transitions||[],
nodes:d.nodes||[], flows:d.flows||[],
components:d.components||[], interfaces:d.interfaces||[], packages:d.packages||[], compRelations:d.compRelations||[],
deployNodes:d.deployNodes||[], artifacts:d.artifacts||[], deployRelations:d.deployRelations||[],
classes:d.classes||[], classRelations:d.classRelations||[],
participants:d.participants||[], activations:d.activations||[], messages:d.messages||[], fragments:d.fragments||[],
swimlanes:d.swimlanes||[], swNodes:d.swNodes||[], swFlows:d.swFlows||[],
_history: [], _historyIndex: -1,
nextId:d.nextId||1
};
state.diagrams=[loaded];
state.activeDiagramId=loaded.id;
pushHistory(loaded);
} else throw new Error('فرمت نامعتبر');
state.selectedElement=null; state.currentTool='select'; setActiveUI('select');
state.zoom = 1; state.panX = 0; state.panY = 0; updateZoomDisplay();
renderTabs(); updateInfo(); updateVis(); updatePropsPanel(); updateToolVisibility(); scheduleResize();
updateStatus(`✅ بارگذاری شد`);
}catch(err){alert('خطا: '+err.message);}
}; r.readAsText(f); e.target.value='';
});
// ===== DRAWING ENGINE =====
function getTC(){
const cs=getComputedStyle(document.body);
return { actorStroke:cs.getPropertyValue('--actor-stroke').trim(), ucFill:cs.getPropertyValue('--uc-fill').trim(), ucStroke:cs.getPropertyValue('--uc-stroke').trim(), relStroke:cs.getPropertyValue('--rel-stroke').trim(), sbStroke:cs.getPropertyValue('--sb-stroke').trim(), labelBg:cs.getPropertyValue('--label-bg').trim(), textPrimary:cs.getPropertyValue('--text-primary').trim(), textMuted:cs.getPropertyValue('--text-muted').trim(), gridLine:cs.getPropertyValue('--grid-line').trim(), canvasBg:cs.getPropertyValue('--canvas-bg').trim(), selected:'#667eea', stateFill:cs.getPropertyValue('--state-fill').trim(), stateStroke:cs.getPropertyValue('--state-stroke').trim(), stateInit:cs.getPropertyValue('--state-init').trim(), stateFinalOuter:cs.getPropertyValue('--state-final-outer').trim(), stateFinalInner:cs.getPropertyValue('--state-final-inner').trim(), transStroke:cs.getPropertyValue('--transition-stroke').trim(), transLabel:cs.getPropertyValue('--transition-label').trim(), activityFill:cs.getPropertyValue('--activity-fill').trim(), activityStroke:cs.getPropertyValue('--activity-stroke').trim(), decisionFill:cs.getPropertyValue('--decision-fill').trim(), decisionStroke:cs.getPropertyValue('--decision-stroke').trim(), forkStroke:cs.getPropertyValue('--fork-stroke').trim(), flowStroke:cs.getPropertyValue('--flow-stroke').trim(), flowLabel:cs.getPropertyValue('--flow-label').trim(), compFill:cs.getPropertyValue('--comp-fill').trim(), compStroke:cs.getPropertyValue('--comp-stroke').trim(), compIcon:cs.getPropertyValue('--comp-icon').trim(), ifaceStroke:cs.getPropertyValue('--iface-stroke').trim(), pkgFill:cs.getPropertyValue('--pkg-fill').trim(), pkgStroke:cs.getPropertyValue('--pkg-stroke').trim(), depStroke:cs.getPropertyValue('--dep-stroke').trim(), nodeFaceTop:cs.getPropertyValue('--node-face-top').trim(), nodeFaceFront:cs.getPropertyValue('--node-face-front').trim(), nodeFaceSide:cs.getPropertyValue('--node-face-side').trim(), nodeStroke:cs.getPropertyValue('--node-stroke').trim(), artifactFill:cs.getPropertyValue('--artifact-fill').trim(), artifactStroke:cs.getPropertyValue('--artifact-stroke').trim(), classFill:cs.getPropertyValue('--class-fill').trim(), classStroke:cs.getPropertyValue('--class-stroke').trim(), classHeader:cs.getPropertyValue('--class-header').trim(), classAttr:cs.getPropertyValue('--class-attr').trim(), classMethod:cs.getPropertyValue('--class-method').trim(), seqLifeline:cs.getPropertyValue('--seq-lifeline').trim(), seqParticipantFill:cs.getPropertyValue('--seq-participant-fill').trim(), seqParticipantStroke:cs.getPropertyValue('--seq-participant-stroke').trim(), seqActivation:cs.getPropertyValue('--seq-activation').trim(), seqActivationStroke:cs.getPropertyValue('--seq-activation-stroke').trim(), seqMsg:cs.getPropertyValue('--seq-msg').trim(), seqReturn:cs.getPropertyValue('--seq-return').trim(), seqFragmentFill:cs.getPropertyValue('--seq-fragment-fill').trim(), seqFragmentStroke:cs.getPropertyValue('--seq-fragment-stroke').trim(), swimlaneDefault:cs.getPropertyValue('--swimlane-default').trim(), swimlaneStroke:cs.getPropertyValue('--swimlane-stroke').trim() };
}
function drawGrid(c,w,h,tc,trans){
if(state.gridType==='none') return;
const s=20;
// Calculate visible area in world coordinates
const startX = -state.panX / state.zoom;
const startY = -state.panY / state.zoom;
const endX = startX + w / state.zoom;
const endY = startY + h / state.zoom;
// Snap to grid
const gridStartX = Math.floor(startX / s) * s;
const gridStartY = Math.floor(startY / s) * s;
if(state.gridType==='lines'){
c.strokeStyle=trans?'rgba(128,128,128,.15)':tc.gridLine;
c.lineWidth=.5 / state.zoom;
c.setLineDash([]);
for(let x=gridStartX;x<=endX;x+=s){c.beginPath();c.moveTo(x,gridStartY);c.lineTo(x,endY);c.stroke();}
for(let y=gridStartY;y<=endY;y+=s){c.beginPath();c.moveTo(gridStartX,y);c.lineTo(endX,y);c.stroke();}
} else if(state.gridType==='dots'){
c.fillStyle=trans?'rgba(128,128,128,.3)':tc.gridLine;
const r=(state.darkMode?1.2:1) / state.zoom;
for(let x=gridStartX;x<=endX;x+=s){
for(let y=gridStartY;y<=endY;y+=s){
c.beginPath();c.arc(x,y,r,0,Math.PI*2);c.fill();
}
}
}
}
function draw(){
if(canvas.width<=0||canvas.height<=0) return;
const d=getActive(), tc=getTC();
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.fillStyle=tc.canvasBg; ctx.fillRect(0,0,canvas.width,canvas.height);
// Apply zoom & pan transform
ctx.save();
ctx.translate(state.panX, state.panY);
ctx.scale(state.zoom, state.zoom);
drawGrid(ctx,canvas.width,canvas.height,tc,false);
if(d) drawDiag(ctx,d,tc);
ctx.restore();
}
function drawDiag(c,d,tc){
if(d.diagramType==='usecase') drawUseCase(c,d,tc);
else if(d.diagramType==='statemachine') drawStateMachine(c,d,tc);
else if(d.diagramType==='activity') drawActivity(c,d,tc);
else if(d.diagramType==='component') drawComponent(c,d,tc);
else if(d.diagramType==='deployment') drawDeployment(c,d,tc);
else if(d.diagramType==='class') drawClassDiagram(c,d,tc);
else if(d.diagramType==='sequence') drawSequenceDiagram(c,d,tc);
else if(d.diagramType==='swimlane') drawSwimlaneDiagram(c,d,tc);
if(state.isDrawingRelationship&&state.relationshipStart&&c===ctx){
c.strokeStyle=tc.selected; c.lineWidth=2/state.zoom; c.setLineDash([5/state.zoom,5/state.zoom]);
c.beginPath(); c.moveTo(state.relationshipStart.x,state.relationshipStart.y); c.lineTo(state.mousePos.x,state.mousePos.y); c.stroke(); c.setLineDash([]);
}
if(state.selectedElement&&c===ctx) drawSel(c,state.selectedElement,tc);
}
// ===== USE CASE =====
function drawUseCase(c,d,tc){
if(d.systemBoundary){ const sb=d.systemBoundary, isS=state.selectedElement?.id===sb.id; c.strokeStyle=isS?tc.selected:tc.sbStroke; c.lineWidth=(isS?3:2)/state.zoom; c.setLineDash([]); c.strokeRect(sb.x,sb.y,sb.width,sb.height); c.fillStyle=tc.textPrimary; c.font=`bold ${16/state.zoom}px Vazirmatn`; c.textAlign='center'; c.textBaseline='bottom'; c.fillText(sb.name,sb.x+sb.width/2,sb.y-8/state.zoom); }
d.relationships.forEach(r=>drawRel(c,r,d,tc));
d.actors.forEach(a=>{ const isS=state.selectedElement?.id===a.id; c.strokeStyle=isS?tc.selected:tc.actorStroke; c.lineWidth=(isS?3:2)/state.zoom; c.beginPath();c.arc(a.x,a.y-20/state.zoom,10/state.zoom,0,Math.PI*2);c.stroke(); c.beginPath();c.moveTo(a.x,a.y-10/state.zoom);c.lineTo(a.x,a.y+15/state.zoom);c.stroke(); c.beginPath();c.moveTo(a.x-15/state.zoom,a.y);c.lineTo(a.x+15/state.zoom,a.y);c.stroke(); c.beginPath();c.moveTo(a.x,a.y+15/state.zoom);c.lineTo(a.x-12/state.zoom,a.y+30/state.zoom);c.stroke(); c.beginPath();c.moveTo(a.x,a.y+15/state.zoom);c.lineTo(a.x+12/state.zoom,a.y+30/state.zoom);c.stroke(); c.fillStyle=tc.textPrimary;c.font=`${13/state.zoom}px Vazirmatn`;c.textAlign='center';c.textBaseline='top';c.fillText(a.name,a.x,a.y+36/state.zoom); });
d.useCases.forEach(u=>{ const isS=state.selectedElement?.id===u.id; c.fillStyle=isS?(state.darkMode?'#2a2050':'#eef2ff'):tc.ucFill; c.strokeStyle=isS?tc.selected:tc.ucStroke; c.lineWidth=(isS?3:2)/state.zoom; c.beginPath();c.ellipse(u.x,u.y,u.radius*1.5,u.radius,0,0,Math.PI*2);c.fill();c.stroke(); c.fillStyle=tc.textPrimary;c.font=`${13/state.zoom}px Vazirmatn`;c.textAlign='center';c.textBaseline='middle'; c.fillText(u.name,u.x,u.y); });
}
function drawRel(c,r,d,tc){
const from=findEl(r.from,d), to=findEl(r.to,d); if(!from||!to) return;
const fp=elCenter(from), tp=elCenter(to);
c.strokeStyle=tc.relStroke; c.lineWidth=2/state.zoom; c.setLineDash([]);
if(r.type==='association'){ c.beginPath();c.moveTo(fp.x,fp.y);c.lineTo(tp.x,tp.y);c.stroke(); }
else if(r.type==='include'||r.type==='extend'){ c.setLineDash([6/state.zoom,4/state.zoom]);c.beginPath();c.moveTo(fp.x,fp.y);c.lineTo(tp.x,tp.y);c.stroke();c.setLineDash([]); drawArr(c,tp.x,tp.y,fp.x,fp.y,tc); const mx=(fp.x+tp.x)/2,my=(fp.y+tp.y)/2,lbl=`<<${r.type}>>`,tw=c.measureText(lbl).width+8/state.zoom; c.fillStyle=tc.labelBg;c.fillRect(mx-tw/2,my-18/state.zoom,tw,16/state.zoom); c.fillStyle=tc.textMuted;c.font=`${11/state.zoom}px Vazirmatn`;c.textAlign='center';c.textBaseline='middle';c.fillText(lbl,mx,my-10/state.zoom); }
else if(r.type==='generalization'){ c.beginPath();c.moveTo(fp.x,fp.y);c.lineTo(tp.x,tp.y);c.stroke(); drawTri(c,tp.x,tp.y,fp.x,fp.y,tc); }
}
function drawArr(c,tx,ty,fx,fy,tc){ const a=Math.atan2(ty-fy,tx-fx),l=12/state.zoom,ang=Math.PI/7; c.strokeStyle=tc.relStroke; c.lineWidth=2/state.zoom; c.beginPath();c.moveTo(tx,ty);c.lineTo(tx-l*Math.cos(a-ang),ty-l*Math.sin(a-ang));c.moveTo(tx,ty);c.lineTo(tx-l*Math.cos(a+ang),ty-l*Math.sin(a+ang));c.stroke(); }
function drawTri(c,tx,ty,fx,fy,tc){ const a=Math.atan2(ty-fy,tx-fx),s=14/state.zoom; c.fillStyle=tc.labelBg;c.strokeStyle=tc.relStroke;c.lineWidth=2/state.zoom; c.beginPath();c.moveTo(tx,ty);c.lineTo(tx-s*Math.cos(a-Math.PI/6),ty-s*Math.sin(a-Math.PI/6));c.lineTo(tx-s*Math.cos(a+Math.PI/6),ty-s*Math.sin(a+Math.PI/6));c.closePath();c.fill();c.stroke(); }
// ===== STATE MACHINE =====
function drawStateMachine(c,d,tc){
(d.transitions||[]).forEach(t=>drawTransition(c,t,d,tc));
(d.states||[]).forEach(s=>drawState(c,s,tc));
}
function drawState(c,s,tc){
const isS = state.selectedElement?.id===s.id;
const selColor = tc.selected;
const z = state.zoom;
if(s.stateType==='initial'){ c.fillStyle = isS ? selColor : tc.stateInit; c.beginPath(); c.arc(s.x, s.y, 10/z, 0, Math.PI*2); c.fill(); if(isS){ c.strokeStyle=selColor; c.lineWidth=2/z; c.setLineDash([4/z,4/z]); c.beginPath(); c.arc(s.x,s.y,15/z,0,Math.PI*2); c.stroke(); c.setLineDash([]); } }
else if(s.stateType==='final'){ c.strokeStyle = isS ? selColor : tc.stateFinalOuter; c.lineWidth = (isS ? 3 : 2)/z; c.beginPath(); c.arc(s.x, s.y, 14/z, 0, Math.PI*2); c.stroke(); c.fillStyle = isS ? selColor : tc.stateFinalInner; c.beginPath(); c.arc(s.x, s.y, 8/z, 0, Math.PI*2); c.fill(); }
else if(s.stateType==='state'){ const w=s.width||120, h=s.height||50, r=12; c.fillStyle = isS ? (state.darkMode?'#2a2050':'#eef2ff') : tc.stateFill; c.strokeStyle = isS ? selColor : tc.stateStroke; c.lineWidth = (isS ? 3 : 2)/z; roundRect(c, s.x-w/2, s.y-h/2, w, h, r); c.fill(); c.stroke(); c.fillStyle = tc.textPrimary; c.font = `bold ${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(s.name, s.x, s.y); }
else if(s.stateType==='composite'){ const w=s.width||180, h=s.height||120, r=12; c.fillStyle = isS ? (state.darkMode?'#1e2540':'#f0f4ff') : tc.stateFill; c.strokeStyle = isS ? selColor : tc.stateStroke; c.lineWidth = (isS ? 3 : 2)/z; roundRect(c, s.x-w/2, s.y-h/2, w, h, r); c.fill(); c.stroke(); const headerH = 28; c.beginPath(); c.moveTo(s.x-w/2, s.y-h/2+headerH); c.lineTo(s.x+w/2, s.y-h/2+headerH); c.stroke(); c.fillStyle = tc.textPrimary; c.font = `bold ${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(s.name, s.x, s.y-h/2+headerH/2); c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`; c.fillText('(زیرحالت‌ها)', s.x, s.y+10/z); }
}
function drawTransition(c,t,d,tc){
const from = (d.states||[]).find(s=>s.id===t.from); const to = (d.states||[]).find(s=>s.id===t.to);
if(!from||!to) return;
const fp = {x:from.x, y:from.y}; const tp = {x:to.x, y:to.y};
const z = state.zoom;
if(t.from === t.to){ const loopR = 30/z; c.strokeStyle = tc.transStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.arc(fp.x + loopR, fp.y - loopR, loopR, Math.PI*0.3, Math.PI*1.7); c.stroke(); const endAngle = Math.PI*1.7; const ax = fp.x + loopR + loopR*Math.cos(endAngle); const ay = fp.y - loopR + loopR*Math.sin(endAngle); drawSmallArrow(c, ax, ay, endAngle + Math.PI/2, tc); if(t.label){ c.fillStyle = tc.transLabel; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'bottom'; c.fillText(t.label, fp.x + loopR*2 + 5/z, fp.y - loopR); } }
else { c.strokeStyle = tc.transStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.moveTo(fp.x, fp.y); c.lineTo(tp.x, tp.y); c.stroke(); const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x); drawSmallArrow(c, tp.x, tp.y, angle, tc); if(t.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; const tw = c.measureText(t.label).width + 10/z; c.fillStyle = tc.labelBg; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.transLabel; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(t.label, mx, my-10/z); } }
}
function drawSmallArrow(c, x, y, angle, tc){ const z=state.zoom; const len=10/z, a=Math.PI/6; c.fillStyle = tc.transStroke; c.beginPath(); c.moveTo(x, y); c.lineTo(x-len*Math.cos(angle-a), y-len*Math.sin(angle-a)); c.lineTo(x-len*Math.cos(angle+a), y-len*Math.sin(angle+a)); c.closePath(); c.fill(); }
// ===== ACTIVITY =====
function drawActivity(c,d,tc){ (d.flows||[]).forEach(f=>drawFlow(c,f,d,tc)); (d.nodes||[]).forEach(n=>drawNode(c,n,tc)); }
function drawNode(c,n,tc){
const isS = state.selectedElement?.id===n.id; const selColor = tc.selected;
const z = state.zoom;
if(n.nodeType==='initialNode'){ c.fillStyle = isS ? selColor : tc.stateInit; c.beginPath(); c.arc(n.x, n.y, 10/z, 0, Math.PI*2); c.fill(); if(isS){ c.strokeStyle=selColor; c.lineWidth=2/z; c.setLineDash([4/z,4/z]); c.beginPath(); c.arc(n.x,n.y,15/z,0,Math.PI*2); c.stroke(); c.setLineDash([]); } }
else if(n.nodeType==='finalNode'){ c.strokeStyle = isS ? selColor : tc.stateFinalOuter; c.lineWidth = (isS ? 3 : 2)/z; c.beginPath(); c.arc(n.x, n.y, 14/z, 0, Math.PI*2); c.stroke(); c.fillStyle = isS ? selColor : tc.stateFinalInner; c.beginPath(); c.arc(n.x, n.y, 8/z, 0, Math.PI*2); c.fill(); }
else if(n.nodeType==='action'){ const w=n.width||120, h=n.height||50, r=12; c.fillStyle = isS ? (state.darkMode?'#1e3a2e':'#e6fffa') : tc.activityFill; c.strokeStyle = isS ? selColor : tc.activityStroke; c.lineWidth = (isS ? 3 : 2)/z; roundRect(c, n.x-w/2, n.y-h/2, w, h, r); c.fill(); c.stroke(); c.fillStyle = tc.textPrimary; c.font = `${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(n.name, n.x, n.y); }
else if(n.nodeType==='decision' || n.nodeType==='merge'){ const size = n.size || 40; c.fillStyle = isS ? (state.darkMode?'#3a2e1e':'#fffaf0') : tc.decisionFill; c.strokeStyle = isS ? selColor : tc.decisionStroke; c.lineWidth = (isS ? 3 : 2)/z; c.beginPath(); c.moveTo(n.x, n.y-size/z); c.lineTo(n.x+size/z, n.y); c.lineTo(n.x, n.y+size/z); c.lineTo(n.x-size/z, n.y); c.closePath(); c.fill(); c.stroke(); if(n.name){ c.fillStyle = tc.textPrimary; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(n.name, n.x, n.y); } }
else if(n.nodeType==='forkJoin'){ const w = n.orientation==='horizontal' ? (n.width||150) : 6/z; const h = n.orientation==='horizontal' ? 6/z : (n.height||150); c.fillStyle = isS ? selColor : tc.forkStroke; c.fillRect(n.x-w/2, n.y-h/2, w, h); if(isS){ c.strokeStyle = selColor; c.lineWidth = 2/z; c.setLineDash([4/z,4/z]); c.strokeRect(n.x-w/2-4/z, n.y-h/2-4/z, w+8/z, h+8/z); c.setLineDash([]); } }
}
function drawFlow(c,f,d,tc){ const from = (d.nodes||[]).find(n=>n.id===f.from); const to = (d.nodes||[]).find(n=>n.id===f.to); if(!from||!to) return; const fp = {x:from.x, y:from.y}; const tp = {x:to.x, y:to.y}; const z=state.zoom; c.strokeStyle = tc.flowStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.moveTo(fp.x, fp.y); c.lineTo(tp.x, tp.y); c.stroke(); const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x); drawSmallArrow(c, tp.x, tp.y, angle, tc); if(f.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; const tw = c.measureText(f.label).width + 10/z; c.fillStyle = tc.labelBg; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.flowLabel; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(f.label, mx, my-10/z); } }
// ===== COMPONENT =====
function drawComponent(c,d,tc){ (d.packages||[]).forEach(p=>drawPackage(c,p,tc)); (d.interfaces||[]).forEach(i=>drawInterface(c,i,tc)); (d.compRelations||[]).forEach(r=>drawCompRelation(c,r,d,tc)); (d.components||[]).forEach(comp=>drawComponentEl(c,comp,tc)); }
function drawComponentEl(c,comp,tc){ const isS = state.selectedElement?.id===comp.id; const z=state.zoom; const w=comp.width||140, h=comp.height||70; const x=comp.x-w/2, y=comp.y-h/2; c.fillStyle = isS ? (state.darkMode?'#3a2e55':'#f3e8ff') : tc.compFill; c.strokeStyle = isS ? tc.selected : tc.compStroke; c.lineWidth = (isS ? 3 : 2)/z; c.setLineDash([]); c.fillRect(x, y, w, h); c.strokeRect(x, y, w, h); const iconX = x + w - 25/z, iconY = y + 8/z; drawComponentIcon(c, iconX, iconY, tc); c.fillStyle = tc.textPrimary; c.font = `bold ${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(comp.name, comp.x, comp.y + 5/z); }
function drawComponentIcon(c, x, y, tc){ const z=state.zoom; c.strokeStyle = tc.compIcon; c.lineWidth = 1.5/z; c.strokeRect(x, y, 18/z, 14/z); c.fillStyle = tc.compIcon; c.fillRect(x-4/z, y+2/z, 4/z, 3/z); c.fillRect(x-4/z, y+9/z, 4/z, 3/z); }
function drawInterface(c,iface,tc){ const isS = state.selectedElement?.id===iface.id; const z=state.zoom; if(iface.ifaceType==='provided'){ const stickLen = 30/z; const dir = iface.direction || 'right'; let ex, ey; if(dir==='right'){ ex=iface.x+stickLen; ey=iface.y; } else if(dir==='left'){ ex=iface.x-stickLen; ey=iface.y; } else if(dir==='up'){ ex=iface.x; ey=iface.y-stickLen; } else { ex=iface.x; ey=iface.y+stickLen; } c.strokeStyle = isS ? tc.selected : tc.ifaceStroke; c.lineWidth = (isS ? 3 : 2)/z; c.setLineDash([]); c.beginPath(); c.moveTo(iface.x, iface.y); c.lineTo(ex, ey); c.stroke(); c.beginPath(); c.arc(ex, ey, 8/z, 0, Math.PI*2); c.stroke(); if(iface.name){ c.fillStyle = tc.textPrimary; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'bottom'; c.fillText(iface.name, ex, ey-12/z); } } else { const dir = iface.direction || 'right'; c.strokeStyle = isS ? tc.selected : tc.ifaceStroke; c.lineWidth = (isS ? 3 : 2)/z; c.setLineDash([]); let startAngle, endAngle; if(dir==='right'){ startAngle=-Math.PI/2; endAngle=Math.PI/2; } else if(dir==='left'){ startAngle=Math.PI/2; endAngle=-Math.PI/2; } else if(dir==='up'){ startAngle=Math.PI; endAngle=0; } else { startAngle=0; endAngle=Math.PI; } c.beginPath(); c.arc(iface.x, iface.y, 10/z, startAngle, endAngle); c.stroke(); if(iface.name){ c.fillStyle = tc.textPrimary; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'bottom'; c.fillText(iface.name, iface.x, iface.y-14/z); } } }
function drawPackage(c,pkg,tc){ const isS = state.selectedElement?.id===pkg.id; const z=state.zoom; const w=pkg.width||200, h=pkg.height||150; const x=pkg.x, y=pkg.y; const tabW = 60/z, tabH = 18/z; c.fillStyle = isS ? (state.darkMode?'#3a2e1e':'#fffaf0') : tc.pkgFill; c.strokeStyle = isS ? tc.selected : tc.pkgStroke; c.lineWidth = (isS ? 3 : 2)/z; c.setLineDash([]); c.beginPath(); c.moveTo(x, y); c.lineTo(x+tabW, y); c.lineTo(x+tabW, y-tabH); c.lineTo(x, y-tabH); c.closePath(); c.fill(); c.stroke(); c.fillRect(x, y, w, h); c.strokeRect(x, y, w, h); c.fillStyle = tc.textPrimary; c.font = `bold ${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(pkg.name, x+w/2, y-tabH/2); }
function drawCompRelation(c,r,d,tc){ const from = findElInComp(r.from,d); const to = findElInComp(r.to,d); if(!from||!to) return; const fp = elCenter(from), tp = elCenter(to); const z=state.zoom; if(r.relType==='dependency'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([6/z,4/z]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); c.setLineDash([]); const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x); drawOpenArrow(c, tp.x, tp.y, angle, tc); if(r.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; c.fillStyle = tc.labelBg; const tw = c.measureText(r.label).width + 8/z; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(r.label, mx, my-10/z); } } else if(r.relType==='connector'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); } else if(r.relType==='compAssociation'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); if(r.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; c.fillStyle = tc.labelBg; const tw = c.measureText(r.label).width + 8/z; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(r.label, mx, my-10/z); } } }
function drawOpenArrow(c, x, y, angle, tc){ const z=state.zoom; const len=12/z, a=Math.PI/6; c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.beginPath(); c.moveTo(x-len*Math.cos(angle-a), y-len*Math.sin(angle-a)); c.lineTo(x, y); c.lineTo(x-len*Math.cos(angle+a), y-len*Math.sin(angle+a)); c.stroke(); }
// ===== DEPLOYMENT =====
function drawDeployment(c,d,tc){ (d.deployRelations||[]).forEach(r=>drawDeployRelation(c,r,d,tc)); (d.artifacts||[]).forEach(a=>drawArtifact(c,a,tc)); (d.deployNodes||[]).forEach(n=>drawDeployNode(c,n,tc)); }
function drawDeployNode(c,node,tc){ const isS = state.selectedElement?.id===node.id; const z=state.zoom; const w=node.width||160, h=node.height||100; const offset = 20/z; const x=node.x-w/2, y=node.y-h/2; let topFace = tc.nodeFaceTop, frontFace = tc.nodeFaceFront, sideFace = tc.nodeFaceSide; if(node.nodeType==='device'){ topFace = state.darkMode?'#1e3a3a':'#b2f5ea'; frontFace = state.darkMode?'#1a3030':'#81e6d9'; sideFace = state.darkMode?'#162828':'#4fd1c5'; } else if(node.nodeType==='execEnv'){ topFace = state.darkMode?'#3a2e1e':'#fefcbf'; frontFace = state.darkMode?'#302818':'#f6e05e'; sideFace = state.darkMode?'#282010':'#ecc94b'; } if(isS){ topFace = '#4c51bf'; frontFace = '#5a67d8'; sideFace = '#667eea'; } c.fillStyle = frontFace; c.strokeStyle = tc.nodeStroke; c.lineWidth = (isS ? 3 : 2)/z; c.beginPath(); c.moveTo(x, y); c.lineTo(x+w, y); c.lineTo(x+w, y+h); c.lineTo(x, y+h); c.closePath(); c.fill(); c.stroke(); c.fillStyle = topFace; c.beginPath(); c.moveTo(x, y); c.lineTo(x+offset, y-offset); c.lineTo(x+w+offset, y-offset); c.lineTo(x+w, y); c.closePath(); c.fill(); c.stroke(); c.fillStyle = sideFace; c.beginPath(); c.moveTo(x+w, y); c.lineTo(x+w+offset, y-offset); c.lineTo(x+w+offset, y+h-offset); c.lineTo(x+w, y+h); c.closePath(); c.fill(); c.stroke(); const stereotype = node.nodeType==='device'?'«device»':node.nodeType==='execEnv'?'«executionEnvironment»':'«node»'; c.fillStyle = tc.textMuted; c.font = `italic ${10/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'bottom'; c.fillText(stereotype, node.x, y-2/z); c.fillStyle = tc.textPrimary; c.font = `bold ${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(node.name, node.x, node.y); }
function drawArtifact(c,art,tc){ const isS = state.selectedElement?.id===art.id; const z=state.zoom; const w=art.width||120, h=art.height||50; const x=art.x-w/2, y=art.y-h/2; const corner = 12/z; c.fillStyle = isS ? (state.darkMode?'#3b1a1a':'#fff5f5') : tc.artifactFill; c.strokeStyle = isS ? tc.selected : tc.artifactStroke; c.lineWidth = (isS ? 3 : 2)/z; c.setLineDash([]); c.beginPath(); c.moveTo(x, y); c.lineTo(x+w-corner, y); c.lineTo(x+w, y+corner); c.lineTo(x+w, y+h); c.lineTo(x, y+h); c.closePath(); c.fill(); c.stroke(); c.beginPath(); c.moveTo(x+w-corner, y); c.lineTo(x+w-corner, y+corner); c.lineTo(x+w, y+corner); c.stroke(); c.fillStyle = tc.textMuted; c.font = `italic ${10/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'bottom'; c.fillText('«artifact»', art.x, y-2/z); c.fillStyle = tc.textPrimary; c.font = `${13/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(art.name, art.x, art.y+5/z); }
function drawDeployRelation(c,r,d,tc){ const from = findElInDeploy(r.from,d); const to = findElInDeploy(r.to,d); if(!from||!to) return; const fp = elCenter(from), tp = elCenter(to); const z=state.zoom; if(r.relType==='commPath'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); if(r.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; c.fillStyle = tc.labelBg; const tw = c.measureText(r.label).width + 8/z; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(r.label, mx, my-10/z); } } else if(r.relType==='deployRel'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([6/z,4/z]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); c.setLineDash([]); const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x); drawOpenArrow(c, tp.x, tp.y, angle, tc); const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; const label = r.label || '«deploy»'; const tw = c.measureText(label).width + 8/z; c.fillStyle = tc.labelBg; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.textMuted; c.font = `italic ${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(label, mx, my-10/z); } else if(r.relType==='manifestRel'){ c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.setLineDash([6/z,4/z]); c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke(); c.setLineDash([]); drawHollowTriangle(c, tp.x, tp.y, fp.x, fp.y, tc); if(r.label){ const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2; c.fillStyle = tc.labelBg; const tw = c.measureText(r.label).width + 8/z; c.fillRect(mx-tw/2, my-18/z, tw, 16/z); c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`; c.textAlign = 'center'; c.textBaseline = 'middle'; c.fillText(r.label, mx, my-10/z); } } }
function drawHollowTriangle(c, tx, ty, fx, fy, tc){ const z=state.zoom; const angle = Math.atan2(ty-fy, tx-fx); const s = 12/z; c.fillStyle = tc.labelBg; c.strokeStyle = tc.depStroke; c.lineWidth = 2/z; c.beginPath(); c.moveTo(tx, ty); c.lineTo(tx-s*Math.cos(angle-Math.PI/6), ty-s*Math.sin(angle-Math.PI/6)); c.lineTo(tx-s*Math.cos(angle+Math.PI/6), ty-s*Math.sin(angle+Math.PI/6)); c.closePath(); c.fill(); c.stroke(); }
// ===== CLASS DIAGRAM =====
function drawClassDiagram(c,d,tc){
(d.classRelations||[]).forEach(r=>drawClassRelation(c,r,d,tc));
(d.classes||[]).forEach(cls=>drawClassEl(c,cls,tc));
}
function drawClassEl(c,cls,tc){
const isS = state.selectedElement?.id===cls.id;
const z=state.zoom;
const w=cls.width||180;
const headerH=30/z;
const attrH=Math.max(20/z, (cls.attributes||[]).length*18/z + 10/z);
const methH=Math.max(20/z, (cls.methods||[]).length*18/z + 10/z);
const h=headerH+attrH+methH;
cls._computedH = h;
const x=cls.x-w/2, y=cls.y-h/2;
c.fillStyle = isS ? (state.darkMode?'#1e3a55':'#dbeafe') : tc.classFill;
c.strokeStyle = isS ? tc.selected : tc.classStroke;
c.lineWidth = (isS ? 3 : 2)/z;
c.setLineDash([]);
c.fillRect(x, y, w, h);
c.strokeRect(x, y, w, h);
c.fillStyle = tc.classHeader;
c.fillRect(x, y, w, headerH);
c.strokeRect(x, y, w, headerH);
let stereotype = '';
if(cls.classType==='interface') stereotype = '«interface»';
else if(cls.classType==='abstract') stereotype = '«abstract»';
else if(cls.classType==='enumeration') stereotype = '«enumeration»';
if(stereotype){
c.fillStyle = tc.textMuted;
c.font = `italic ${10/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'bottom';
c.fillText(stereotype, cls.x, y+13/z);
}
c.fillStyle = tc.textPrimary;
c.font = cls.classType==='abstract' ? `italic bold ${13/z}px Vazirmatn` : `bold ${13/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
const nameY = stereotype ? y+headerH/2+5/z : y+headerH/2;
c.fillText(cls.name, cls.x, nameY);
c.fillStyle = tc.classAttr;
c.font = `${11/z}px Vazirmatn`;
c.textAlign = 'left'; c.textBaseline = 'top';
let attrY = y + headerH + 5/z;
if((cls.attributes||[]).length === 0){
c.fillStyle = tc.textMuted;
c.fillText('(بدون ویژگی)', x+8/z, attrY);
} else {
(cls.attributes||[]).forEach(attr=>{
const vis = attr.visibility || '+';
const staticMark = attr.isStatic ? '_' : '';
const typeStr = attr.type ? ': '+attr.type : '';
const text = `${vis} ${staticMark}${attr.name}${typeStr}`;
c.fillStyle = tc.classAttr;
c.fillText(text, x+8/z, attrY);
attrY += 18/z;
});
}
const sep1Y = y + headerH + attrH;
c.beginPath(); c.moveTo(x, sep1Y); c.lineTo(x+w, sep1Y); c.stroke();
c.font = `${11/z}px Vazirmatn`;
let methY = sep1Y + 5/z;
if((cls.methods||[]).length === 0){
c.fillStyle = tc.textMuted;
c.fillText('(بدون متد)', x+8/z, methY);
} else {
(cls.methods||[]).forEach(meth=>{
const vis = meth.visibility || '+';
const staticMark = meth.isStatic ? '_' : '';
const params = meth.params ? `(${meth.params})` : '()';
const retType = meth.returnType ? ': '+meth.returnType : '';
const text = `${vis} ${staticMark}${meth.name}${params}${retType}`;
c.fillStyle = tc.classMethod;
c.fillText(text, x+8/z, methY);
methY += 18/z;
});
}
}
function drawClassRelation(c,r,d,tc){
const from = (d.classes||[]).find(cls=>cls.id===r.from);
const to = (d.classes||[]).find(cls=>cls.id===r.to);
if(!from||!to) return;
const fp = {x:from.x, y:from.y};
const tp = {x:to.x, y:to.y};
const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x);
const z=state.zoom;
c.strokeStyle = tc.depStroke;
c.lineWidth = 2/z;
if(r.relType==='clsAssociation'){
c.setLineDash([]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
} else if(r.relType==='aggregation'){
c.setLineDash([]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
drawDiamond(c, fp.x, fp.y, angle, false, tc);
} else if(r.relType==='composition'){
c.setLineDash([]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
drawDiamond(c, fp.x, fp.y, angle, true, tc);
} else if(r.relType==='inheritance'){
c.setLineDash([]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
drawHollowTriangle(c, tp.x, tp.y, fp.x, fp.y, tc);
} else if(r.relType==='clsDependency'){
c.setLineDash([6/z,4/z]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
c.setLineDash([]);
drawOpenArrow(c, tp.x, tp.y, angle, tc);
} else if(r.relType==='realization'){
c.setLineDash([6/z,4/z]);
c.beginPath(); c.moveTo(fp.x,fp.y); c.lineTo(tp.x,tp.y); c.stroke();
c.setLineDash([]);
drawHollowTriangle(c, tp.x, tp.y, fp.x, fp.y, tc);
}
if(r.label || r.fromMultiplicity || r.toMultiplicity){
const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2;
if(r.label){
const tw = c.measureText(r.label).width + 8/z;
c.fillStyle = tc.labelBg; c.fillRect(mx-tw/2, my-18/z, tw, 16/z);
c.fillStyle = tc.textMuted; c.font = `${11/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(r.label, mx, my-10/z);
}
if(r.fromMultiplicity){
c.fillStyle = tc.textMuted; c.font = `${10/z}px Vazirmatn`;
c.textAlign = 'left'; c.textBaseline = 'bottom';
c.fillText(r.fromMultiplicity, fp.x+5/z, fp.y-3/z);
}
if(r.toMultiplicity){
c.fillStyle = tc.textMuted; c.font = `${10/z}px Vazirmatn`;
c.textAlign = 'right'; c.textBaseline = 'bottom';
c.fillText(r.toMultiplicity, tp.x-5/z, tp.y-3/z);
}
}
}
function drawDiamond(c, x, y, angle, filled, tc){
const z=state.zoom;
const size = 10/z;
c.save();
c.translate(x, y);
c.rotate(angle);
c.beginPath();
c.moveTo(0, 0);
c.lineTo(-size, -size/2);
c.lineTo(-size*2, 0);
c.lineTo(-size, size/2);
c.closePath();
if(filled){
c.fillStyle = tc.depStroke;
c.fill();
} else {
c.fillStyle = tc.labelBg;
c.fill();
}
c.strokeStyle = tc.depStroke;
c.lineWidth = 2/z;
c.stroke();
c.restore();
}
// ===== SEQUENCE DIAGRAM =====
function drawSequenceDiagram(c,d,tc){
(d.fragments||[]).forEach(f=>drawFragment(c,f,tc));
(d.participants||[]).forEach(p=>drawParticipant(c,p,d,tc));
(d.activations||[]).forEach(a=>drawActivation(c,a,d,tc));
(d.messages||[]).forEach(m=>drawMessage(c,m,d,tc));
}
function drawParticipant(c,p,d,tc){
const isS = state.selectedElement?.id===p.id;
const z=state.zoom;
const w=p.width||120, h=40/z;
const x=p.x-w/2, y=p.y-h/2;
c.fillStyle = isS ? (state.darkMode?'#2a3a55':'#cbd5e0') : tc.seqParticipantFill;
c.strokeStyle = isS ? tc.selected : tc.seqParticipantStroke;
c.lineWidth = (isS ? 3 : 2)/z;
c.setLineDash([]);
c.fillRect(x, y, w, h);
c.strokeRect(x, y, w, h);
c.fillStyle = tc.textPrimary;
c.font = `bold ${12/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(p.name, p.x, p.y);
const canvasBottom = (canvas.height - state.panY) / state.zoom - 20/z;
c.strokeStyle = tc.seqLifeline;
c.lineWidth = 1/z;
c.setLineDash([5/z,5/z]);
c.beginPath();
c.moveTo(p.x, p.y+h/2);
c.lineTo(p.x, canvasBottom);
c.stroke();
c.setLineDash([]);
}
function drawActivation(c,a,d,tc){
const isS = state.selectedElement?.id===a.id;
const z=state.zoom;
const participant = (d.participants||[]).find(p=>p.id===a.participantId);
if(!participant) return;
const w=12/z, h=a.height||60/z;
const x=participant.x-w/2, y=a.y;
c.fillStyle = isS ? tc.selected : tc.seqActivation;
c.strokeStyle = isS ? tc.selected : tc.seqActivationStroke;
c.lineWidth = (isS ? 2 : 1.5)/z;
c.setLineDash([]);
c.fillRect(x, y, w, h);
c.strokeRect(x, y, w, h);
}
function drawFragment(c,f,tc){
const isS = state.selectedElement?.id===f.id;
const z=state.zoom;
const w=f.width||300/z, h=f.height||150/z;
const x=f.x, y=f.y;
const tabW=50/z, tabH=20/z;
c.fillStyle = isS ? (state.darkMode?'#3a2e1e':'#fefcbf') : tc.seqFragmentFill;
c.strokeStyle = isS ? tc.selected : tc.seqFragmentStroke;
c.lineWidth = (isS ? 3 : 2)/z;
c.setLineDash([]);
c.fillRect(x, y, w, h);
c.strokeRect(x, y, w, h);
c.beginPath();
c.moveTo(x, y);
c.lineTo(x+tabW, y);
c.lineTo(x+tabW, y+tabH);
c.lineTo(x, y+tabH);
c.closePath();
c.fill(); c.stroke();
const typeLabel = f.fragmentType==='alt'?'alt':f.fragmentType==='opt'?'opt':f.fragmentType==='loop'?'loop':'break';
c.fillStyle = tc.textPrimary;
c.font = `bold italic ${11/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(`[${typeLabel}]`, x+tabW/2, y+tabH/2);
if(f.label){
c.fillStyle = tc.textMuted;
c.font = `italic ${10/z}px Vazirmatn`;
c.textAlign = 'left'; c.textBaseline = 'middle';
c.fillText(f.label, x+tabW+5/z, y+tabH/2);
}
if(f.operands && f.operands.length > 1){
c.strokeStyle = tc.seqFragmentStroke;
c.lineWidth = 1/z;
c.setLineDash([4/z,4/z]);
let currentY = y + tabH;
f.operands.forEach((op, idx)=>{
if(idx < f.operands.length - 1){
currentY += (op.height || 40/z);
c.beginPath();
c.moveTo(x, currentY);
c.lineTo(x+w, currentY);
c.stroke();
if(op.label){
c.fillStyle = tc.textMuted;
c.font = `italic ${10/z}px Vazirmatn`;
c.textAlign = 'left'; c.textBaseline = 'middle';
c.fillText(`[${op.label}]`, x+5/z, currentY-8/z);
}
}
});
c.setLineDash([]);
}
}
function drawMessage(c,m,d,tc){
const fromP = (d.participants||[]).find(p=>p.id===m.fromParticipant);
const toP = (d.participants||[]).find(p=>p.id===m.toParticipant);
if(!fromP || !toP) return;
const z=state.zoom;
const y = m.y;
const fromX = fromP.x;
const toX = toP.x;
c.lineWidth = 2/z;
c.setLineDash([]);
if(m.msgType==='syncMsg'){
c.strokeStyle = tc.seqMsg;
c.beginPath(); c.moveTo(fromX, y); c.lineTo(toX, y); c.stroke();
const angle = toX > fromX ? 0 : Math.PI;
drawFilledArrow(c, toX, y, angle, tc);
} else if(m.msgType==='asyncMsg'){
c.strokeStyle = tc.seqMsg;
c.beginPath(); c.moveTo(fromX, y); c.lineTo(toX, y); c.stroke();
const angle = toX > fromX ? 0 : Math.PI;
drawOpenArrowSide(c, toX, y, angle, tc);
} else if(m.msgType==='returnMsg'){
c.strokeStyle = tc.seqReturn;
c.setLineDash([5/z,5/z]);
c.beginPath(); c.moveTo(fromX, y); c.lineTo(toX, y); c.stroke();
c.setLineDash([]);
const angle = toX > fromX ? 0 : Math.PI;
drawOpenArrowSide(c, toX, y, angle, tc);
} else if(m.msgType==='selfMsg'){
c.strokeStyle = tc.seqMsg;
const loopW = 30/z, loopH = 20/z;
c.beginPath();
c.moveTo(fromX, y);
c.lineTo(fromX+loopW, y);
c.lineTo(fromX+loopW, y+loopH);
c.lineTo(fromX, y+loopH);
c.stroke();
drawFilledArrow(c, fromX, y+loopH, Math.PI, tc);
}
if(m.label){
const mx = (fromX + toX) / 2;
c.fillStyle = tc.labelBg;
const tw = c.measureText(m.label).width + 8/z;
c.fillRect(mx-tw/2, y-18/z, tw, 16/z);
c.fillStyle = tc.textPrimary;
c.font = `${11/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(m.label, mx, y-10/z);
}
}
function drawFilledArrow(c, x, y, angle, tc){
const z=state.zoom;
const len=10/z, a=Math.PI/6;
c.fillStyle = tc.seqMsg;
c.beginPath();
c.moveTo(x, y);
c.lineTo(x-len*Math.cos(angle-a), y-len*Math.sin(angle-a));
c.lineTo(x-len*Math.cos(angle+a), y-len*Math.sin(angle+a));
c.closePath();
c.fill();
}
function drawOpenArrowSide(c, x, y, angle, tc){
const z=state.zoom;
const len=10/z, a=Math.PI/6;
c.strokeStyle = tc.seqMsg;
c.lineWidth = 2/z;
c.beginPath();
c.moveTo(x-len*Math.cos(angle-a), y-len*Math.sin(angle-a));
c.lineTo(x, y);
c.lineTo(x-len*Math.cos(angle+a), y-len*Math.sin(angle+a));
c.stroke();
}
// ===== SWIMLANE DIAGRAM =====
function drawSwimlaneDiagram(c,d,tc){
(d.swimlanes||[]).forEach(sw=>drawSwimlane(c,sw,d,tc));
(d.swFlows||[]).forEach(f=>drawSwFlow(c,f,d,tc));
(d.swNodes||[]).forEach(n=>drawSwNode(c,n,tc));
}
function drawSwimlane(c,sw,d,tc){
const isS = state.selectedElement?.id===sw.id;
const z=state.zoom;
const w=sw.width||400/z, h=sw.height||300/z;
const x=sw.x, y=sw.y;
const headerSize = 40/z;
const fillColor = sw.color || tc.swimlaneDefault;
c.fillStyle = isS ? (state.darkMode?'#2a3a55':'#dbeafe') : fillColor;
c.strokeStyle = isS ? tc.selected : tc.swimlaneStroke;
c.lineWidth = (isS ? 3 : 2)/z;
c.setLineDash([]);
c.fillRect(x, y, w, h);
c.strokeRect(x, y, w, h);
if(sw.orientation==='vertical'){
c.fillStyle = tc.swimlaneStroke;
c.fillRect(x+w-headerSize, y, headerSize, h);
c.strokeRect(x+w-headerSize, y, headerSize, h);
c.save();
c.translate(x+w-headerSize/2, y+h/2);
c.rotate(-Math.PI/2);
c.fillStyle = tc.textPrimary;
c.font = `bold ${13/z}px Vazirmatn`;
c.textAlign = 'center';
c.textBaseline = 'middle';
c.fillText(sw.name, 0, 0);
c.restore();
} else {
c.fillStyle = tc.swimlaneStroke;
c.fillRect(x, y, w, headerSize);
c.strokeRect(x, y, w, headerSize);
c.fillStyle = tc.textPrimary;
c.font = `bold ${13/z}px Vazirmatn`;
c.textAlign = 'center';
c.textBaseline = 'middle';
c.fillText(sw.name, x+w/2, y+headerSize/2);
}
if(isS){
drawResizeHandles(c, sw);
}
}
function drawResizeHandles(c, sw){
const handles = getResizeHandles(sw);
const z=state.zoom;
c.fillStyle = '#667eea';
c.strokeStyle = '#ffffff';
c.lineWidth = 2/z;
handles.forEach(h=>{
c.fillRect(h.x-4/z, h.y-4/z, 8/z, 8/z);
c.strokeRect(h.x-4/z, h.y-4/z, 8/z, 8/z);
});
}
function getResizeHandles(sw){
const z=state.zoom;
const w=(sw.width||400/z), h=(sw.height||300/z);
const x=sw.x, y=sw.y;
return [
{x:x, y:y, pos:'tl'},
{x:x+w, y:y, pos:'tr'},
{x:x, y:y+h, pos:'bl'},
{x:x+w, y:y+h, pos:'br'},
{x:x+w/2, y:y, pos:'t'},
{x:x+w/2, y:y+h, pos:'b'},
{x:x, y:y+h/2, pos:'l'},
{x:x+w, y:y+h/2, pos:'r'}
];
}
function drawSwNode(c,n,tc){
const isS = state.selectedElement?.id===n.id;
const selColor = tc.selected;
const z=state.zoom;
if(n.nodeType==='swInitial'){
c.fillStyle = isS ? selColor : tc.stateInit;
c.beginPath(); c.arc(n.x, n.y, 10/z, 0, Math.PI*2); c.fill();
if(isS){ c.strokeStyle=selColor; c.lineWidth=2/z; c.setLineDash([4/z,4/z]); c.beginPath(); c.arc(n.x,n.y,15/z,0,Math.PI*2); c.stroke(); c.setLineDash([]); }
}
else if(n.nodeType==='swFinal'){
c.strokeStyle = isS ? selColor : tc.stateFinalOuter;
c.lineWidth = (isS ? 3 : 2)/z;
c.beginPath(); c.arc(n.x, n.y, 14/z, 0, Math.PI*2); c.stroke();
c.fillStyle = isS ? selColor : tc.stateFinalInner;
c.beginPath(); c.arc(n.x, n.y, 8/z, 0, Math.PI*2); c.fill();
}
else if(n.nodeType==='swAction'){
const w=n.width||120/z, h=n.height||50/z, r=12/z;
c.fillStyle = isS ? (state.darkMode?'#1e3a2e':'#e6fffa') : tc.activityFill;
c.strokeStyle = isS ? selColor : tc.activityStroke;
c.lineWidth = (isS ? 3 : 2)/z;
roundRect(c, n.x-w/2, n.y-h/2, w, h, r);
c.fill(); c.stroke();
c.fillStyle = tc.textPrimary; c.font = `${13/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(n.name, n.x, n.y);
}
else if(n.nodeType==='swDecision'){
const size = (n.size || 40)/z;
c.fillStyle = isS ? (state.darkMode?'#3a2e1e':'#fffaf0') : tc.decisionFill;
c.strokeStyle = isS ? selColor : tc.decisionStroke;
c.lineWidth = (isS ? 3 : 2)/z;
c.beginPath();
c.moveTo(n.x, n.y-size);
c.lineTo(n.x+size, n.y);
c.lineTo(n.x, n.y+size);
c.lineTo(n.x-size, n.y);
c.closePath();
c.fill(); c.stroke();
if(n.name){
c.fillStyle = tc.textPrimary; c.font = `${11/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(n.name, n.x, n.y);
}
}
else if(n.nodeType==='swForkJoin'){
const w = n.orientation==='horizontal' ? (n.width||150/z) : 6/z;
const h = n.orientation==='horizontal' ? 6/z : (n.height||150/z);
c.fillStyle = isS ? selColor : tc.forkStroke;
c.fillRect(n.x-w/2, n.y-h/2, w, h);
if(isS){
c.strokeStyle = selColor; c.lineWidth = 2/z; c.setLineDash([4/z,4/z]);
c.strokeRect(n.x-w/2-4/z, n.y-h/2-4/z, w+8/z, h+8/z);
c.setLineDash([]);
}
}
}
function drawSwFlow(c,f,d,tc){
const from = (d.swNodes||[]).find(n=>n.id===f.from);
const to = (d.swNodes||[]).find(n=>n.id===f.to);
if(!from||!to) return;
const fp = {x:from.x, y:from.y};
const tp = {x:to.x, y:to.y};
const z=state.zoom;
c.strokeStyle = tc.flowStroke; c.lineWidth = 2/z; c.setLineDash([]);
c.beginPath(); c.moveTo(fp.x, fp.y); c.lineTo(tp.x, tp.y); c.stroke();
const angle = Math.atan2(tp.y-fp.y, tp.x-fp.x);
drawSmallArrow(c, tp.x, tp.y, angle, tc);
if(f.label){
const mx=(fp.x+tp.x)/2, my=(fp.y+tp.y)/2;
const tw = c.measureText(f.label).width + 10/z;
c.fillStyle = tc.labelBg; c.fillRect(mx-tw/2, my-18/z, tw, 16/z);
c.fillStyle = tc.flowLabel; c.font = `${11/z}px Vazirmatn`;
c.textAlign = 'center'; c.textBaseline = 'middle';
c.fillText(f.label, mx, my-10/z);
}
}
function roundRect(c, x, y, w, h, r){ c.beginPath(); c.moveTo(x+r, y); c.lineTo(x+w-r, y); c.quadraticCurveTo(x+w, y, x+w, y+r); c.lineTo(x+w, y+h-r); c.quadraticCurveTo(x+w, y+h, x+w-r, y+h); c.lineTo(x+r, y+h); c.quadraticCurveTo(x, y+h, x, y+h-r); c.lineTo(x, y+r); c.quadraticCurveTo(x, y, x+r, y); c.closePath(); }
function drawSel(c,el,tc){
const z=state.zoom;
c.strokeStyle=tc.selected; c.lineWidth=2/z; c.setLineDash([5/z,5/z]);
if(el.elType==='actor') c.strokeRect(el.x-25/z,el.y-35/z,50/z,80/z);
else if(el.elType==='useCase'){c.beginPath();c.ellipse(el.x,el.y,el.radius*1.5+6/z,el.radius+6/z,0,0,Math.PI*2);c.stroke();}
else if(el.elType==='systemBoundary') c.strokeRect(el.x-5/z,el.y-5/z,el.width+10/z,el.height+10/z);
else if(el.elType==='state'||el.elType==='composite'){ const w=(el.width||120)+10/z,h=(el.height||50)+10/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='initial'||el.elType==='initialNode'||el.elType==='swInitial') { c.beginPath();c.arc(el.x,el.y,16/z,0,Math.PI*2);c.stroke(); }
else if(el.elType==='final'||el.elType==='finalNode'||el.elType==='swFinal') { c.beginPath();c.arc(el.x,el.y,20/z,0,Math.PI*2);c.stroke(); }
else if(el.elType==='action'||el.elType==='swAction'){ const w=(el.width||120)+10/z,h=(el.height||50)+10/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='decision'||el.elType==='merge'||el.elType==='swDecision'){ const s=(el.size||40)+6/z; c.strokeRect(el.x-s,el.y-s,s*2,s*2); }
else if(el.elType==='forkJoin'||el.elType==='swForkJoin'){ const w=el.orientation==='horizontal'?(el.width||150):6/z, h=el.orientation==='horizontal'?6/z:(el.height||150); c.strokeRect(el.x-w/2-4/z,el.y-h/2-4/z,w+8/z,h+8/z); }
else if(el.elType==='component'){ const w=(el.width||140)+10/z,h=(el.height||70)+10/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='interfaceProv'||el.elType==='interfaceReq'){ c.beginPath(); c.arc(el.x,el.y,20/z,0,Math.PI*2); c.stroke(); }
else if(el.elType==='package'){ const w=(el.width||200)+10/z,h=(el.height||150)+28/z; c.strokeRect(el.x-5/z,el.y-23/z,w,h); }
else if(el.elType==='node'||el.elType==='device'||el.elType==='execEnv'){ const w=(el.width||160)+30/z,h=(el.height||100)+30/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='artifact'){ const w=(el.width||120)+10/z,h=(el.height||50)+10/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='class'||el.elType==='interfaceClass'||el.elType==='abstractClass'||el.elType==='enumeration'){ const w=(el.width||180)+10/z,h=(el._computedH||100)+10/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='participant'){ const w=(el.width||120)+10/z,h=50/z; c.strokeRect(el.x-w/2,el.y-h/2,w,h); }
else if(el.elType==='activation'){ c.strokeRect(el.x-10/z,el.y-4/z,20/z,(el.height||60/z)+8/z); }
else if(el.elType==='fragment'){ const w=(el.width||300)+10/z,h=(el.height||150)+10/z; c.strokeRect(el.x-5/z,el.y-5/z,w,h); }
else if(el.elType==='swimlane'){ const w=(el.width||400)+10/z,h=(el.height||300)+10/z; c.strokeRect(el.x-5/z,el.y-5/z,w,h); }
c.setLineDash([]);
}
// ===== HELPERS =====
function findEl(id,d){
if(d.diagramType==='usecase'){ let e=d.actors.find(a=>a.id===id); if(e) return {...e,elType:'actor'}; e=d.useCases.find(u=>u.id===id); if(e) return {...e,elType:'useCase'}; if(d.systemBoundary?.id===id) return {...d.systemBoundary,elType:'systemBoundary'}; }
else if(d.diagramType==='statemachine'){ let e=(d.states||[]).find(s=>s.id===id); if(e) return {...e,elType:e.stateType}; }
else if(d.diagramType==='activity'){ let e=(d.nodes||[]).find(n=>n.id===id); if(e) return {...e,elType:e.nodeType}; }
else if(d.diagramType==='component'){ return findElInComp(id,d); }
else if(d.diagramType==='deployment'){ return findElInDeploy(id,d); }
else if(d.diagramType==='class'){ let e=(d.classes||[]).find(cls=>cls.id===id); if(e) return {...e,elType:e.classType}; }
else if(d.diagramType==='sequence'){ let e=(d.participants||[]).find(p=>p.id===id); if(e) return {...e,elType:'participant'}; e=(d.activations||[]).find(a=>a.id===id); if(e) return {...e,elType:'activation'}; e=(d.fragments||[]).find(f=>f.id===id); if(e) return {...e,elType:'fragment'}; }
else if(d.diagramType==='swimlane'){ let e=(d.swimlanes||[]).find(s=>s.id===id); if(e) return {...e,elType:'swimlane'}; e=(d.swNodes||[]).find(n=>n.id===id); if(e) return {...e,elType:e.nodeType}; }
return null;
}
function findElInComp(id,d){ let e=(d.components||[]).find(c=>c.id===id); if(e) return {...e,elType:'component'}; e=(d.interfaces||[]).find(i=>i.id===id); if(e) return {...e,elType:e.ifaceType==='provided'?'interfaceProv':'interfaceReq'}; e=(d.packages||[]).find(p=>p.id===id); if(e) return {...e,elType:'package'}; return null; }
function findElInDeploy(id,d){ let e=(d.deployNodes||[]).find(n=>n.id===id); if(e) return {...e,elType:e.nodeType}; e=(d.artifacts||[]).find(a=>a.id===id); if(e) return {...e,elType:'artifact'}; return null; }
function elCenter(el){ return {x:el.x,y:el.y}; }
// Convert screen coordinates to world coordinates (accounting for zoom & pan)
function screenToWorld(sx, sy){
return {
x: (sx - state.panX) / state.zoom,
y: (sy - state.panY) / state.zoom
};
}
function hitTest(x,y){
const d=getActive(); if(!d) return null;
if(d.diagramType==='usecase'){ for(const a of d.actors) if(Math.abs(x-a.x)<25&&Math.abs(y-a.y)<45) return {...a,elType:'actor'}; for(const u of d.useCases){ const dx=(x-u.x)/(u.radius*1.5),dy=(y-u.y)/u.radius; if(dx*dx+dy*dy<=1) return {...u,elType:'useCase'}; } if(d.systemBoundary){ const s=d.systemBoundary; if(x>=s.x&&x<=s.x+s.width&&y>=s.y&&y<=s.y+s.height) return {...s,elType:'systemBoundary'}; } }
else if(d.diagramType==='statemachine'){ for(const s of (d.states||[])){ if(s.stateType==='initial'){ if(Math.hypot(x-s.x,y-s.y)<=14) return {...s,elType:'initialState'}; } else if(s.stateType==='final'){ if(Math.hypot(x-s.x,y-s.y)<=18) return {...s,elType:'finalState'}; } else if(s.stateType==='state'){ const w=(s.width||120)/2,h=(s.height||50)/2; if(x>=s.x-w&&x<=s.x+w&&y>=s.y-h&&y<=s.y+h) return {...s,elType:'state'}; } else if(s.stateType==='composite'){ const w=(s.width||180)/2,h=(s.height||120)/2; if(x>=s.x-w&&x<=s.x+w&&y>=s.y-h&&y<=s.y+h) return {...s,elType:'compositeState'}; } } }
else if(d.diagramType==='activity'){ for(const n of (d.nodes||[])){ if(n.nodeType==='initialNode'||n.nodeType==='finalNode'){ if(Math.hypot(x-n.x,y-n.y)<=18) return {...n,elType:n.nodeType}; } else if(n.nodeType==='action'){ const w=(n.width||120)/2,h=(n.height||50)/2; if(x>=n.x-w&&x<=n.x+w&&y>=n.y-h&&y<=n.y+h) return {...n,elType:'action'}; } else if(n.nodeType==='decision'||n.nodeType==='merge'){ const s=n.size||40; const dx=Math.abs(x-n.x),dy=Math.abs(y-n.y); if(dx+dy<=s) return {...n,elType:n.nodeType}; } else if(n.nodeType==='forkJoin'){ const w=n.orientation==='horizontal'?(n.width||150)/2:3, h=n.orientation==='horizontal'?3:(n.height||150)/2; if(x>=n.x-w&&x<=n.x+w&&y>=n.y-h&&y<=n.y+h) return {...n,elType:'forkJoin'}; } } }
else if(d.diagramType==='component'){ for(const comp of (d.components||[])){ const w=(comp.width||140)/2, h=(comp.height||70)/2; if(x>=comp.x-w&&x<=comp.x+w&&y>=comp.y-h&&y<=comp.y+h) return {...comp,elType:'component'}; } for(const iface of (d.interfaces||[])){ if(Math.hypot(x-iface.x,y-iface.y)<=15) return {...iface,elType:iface.ifaceType==='provided'?'interfaceProv':'interfaceReq'}; } for(const pkg of (d.packages||[])){ const w=pkg.width||200, h=pkg.height||150; if(x>=pkg.x&&x<=pkg.x+w&&y>=pkg.y-18&&y<=pkg.y+h) return {...pkg,elType:'package'}; } }
else if(d.diagramType==='deployment'){ for(const art of (d.artifacts||[])){ const w=(art.width||120)/2, h=(art.height||50)/2; if(x>=art.x-w&&x<=art.x+w&&y>=art.y-h&&y<=art.y+h) return {...art,elType:'artifact'}; } for(const n of (d.deployNodes||[])){ const w=(n.width||160)/2, h=(n.height||100)/2; if(x>=n.x-w&&x<=n.x+w&&y>=n.y-h&&y<=n.y+h) return {...n,elType:n.nodeType}; } }
else if(d.diagramType==='class'){ for(const cls of (d.classes||[])){ const w=(cls.width||180)/2, h=(cls._computedH||100)/2; if(x>=cls.x-w&&x<=cls.x+w&&y>=cls.y-h&&y<=cls.y+h) return {...cls,elType:cls.classType}; } }
else if(d.diagramType==='sequence'){ for(const p of (d.participants||[])){ const w=(p.width||120)/2, h=20; if(x>=p.x-w&&x<=p.x+w&&y>=p.y-h&&y<=p.y+h) return {...p,elType:'participant'}; } for(const a of (d.activations||[])){ const part=(d.participants||[]).find(p=>p.id===a.participantId); if(part){ const h=a.height||60; if(x>=part.x-6&&x<=part.x+6&&y>=a.y&&y<=a.y+h) return {...a,elType:'activation'}; } } for(const f of (d.fragments||[])){ const w=f.width||300, h=f.height||150; if(x>=f.x&&x<=f.x+w&&y>=f.y&&y<=f.y+h) return {...f,elType:'fragment'}; } }
else if(d.diagramType==='swimlane'){
for(const sw of (d.swimlanes||[])){
const w=sw.width||400, h=sw.height||300;
if(x>=sw.x&&x<=sw.x+w&&y>=sw.y&&y<=sw.y+h) return {...sw,elType:'swimlane'};
}
for(const n of (d.swNodes||[])){
if(n.nodeType==='swInitial'||n.nodeType==='swFinal'){ if(Math.hypot(x-n.x,y-n.y)<=18) return {...n,elType:n.nodeType}; }
else if(n.nodeType==='swAction'){ const w=(n.width||120)/2,h=(n.height||50)/2; if(x>=n.x-w&&x<=n.x+w&&y>=n.y-h&&y<=n.y+h) return {...n,elType:'swAction'}; }
else if(n.nodeType==='swDecision'){ const s=n.size||40; const dx=Math.abs(x-n.x),dy=Math.abs(y-n.y); if(dx+dy<=s) return {...n,elType:'swDecision'}; }
else if(n.nodeType==='swForkJoin'){ const w=n.orientation==='horizontal'?(n.width||150)/2:3, h=n.orientation==='horizontal'?3:(n.height||150)/2; if(x>=n.x-w&&x<=n.x+w&&y>=n.y-h&&y<=n.y+h) return {...n,elType:'swForkJoin'}; }
}
}
return null;
}
// ===== CANVAS EVENTS =====
function getPos(e){
const r=canvas.getBoundingClientRect();
const sx = (e.clientX-r.left)*(canvas.width/r.width);
const sy = (e.clientY-r.top)*(canvas.height/r.height);
return screenToWorld(sx, sy);
}
canvas.addEventListener('mousedown',e=>{
const d=getActive(); if(!d||canvas.width<=0) return;
const {x,y}=getPos(e);
const tool=state.currentTool;
// Pan with middle mouse or space+left click
if(e.button === 1 || (e.button === 0 && state.spacePressed)){
state.isPanning = true;
state.panStart = { x: e.clientX - state.panX, y: e.clientY - state.panY };
canvas.style.cursor = 'grabbing';
return;
}
if(tool==='select'){
const el=hitTest(x,y);
if(el&&el.elType==='swimlane'){
const handles = getResizeHandles(el);
for(const h of handles){
if(Math.abs(x-h.x)<=6&&Math.abs(y-h.y)<=6){
state.resizingSwimlane = el;
state.resizeHandle = h.pos;
state.dragStart = {x, y, elX:el.x, elY:el.y, elW:el.width||400, elH:el.height||300};
return;
}
}
}
state.selectedElement=el; updatePropsPanel();
if(el){state.isDragging=true;state.dragStart={x,y,elX:el.x||0,elY:el.y||0};}
}
else if(tool==='actor'){ d.actors.push({id:genElId(d),name:'Actor',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='useCase'){ d.useCases.push({id:genElId(d),name:'Use Case',x,y,radius:40}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='boundary'){ if(!d.systemBoundary){d.systemBoundary={id:genElId(d),name:'System',x:x-150,y:y-100,width:300,height:250};pushHistory(d);updateInfo();draw();switchToSelect();return;} }
else if(tool==='initialState'){ d.states.push({id:genElId(d),stateType:'initial',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='state'){ d.states.push({id:genElId(d),stateType:'state',name:'State',x,y,width:120,height:50}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='finalState'){ d.states.push({id:genElId(d),stateType:'final',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='compositeState'){ d.states.push({id:genElId(d),stateType:'composite',name:'Composite',x,y,width:180,height:120}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='initialNode'){ d.nodes.push({id:genElId(d),nodeType:'initialNode',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='finalNode'){ d.nodes.push({id:genElId(d),nodeType:'finalNode',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='action'){ d.nodes.push({id:genElId(d),nodeType:'action',name:'Action',x,y,width:120,height:50}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='decision'){ d.nodes.push({id:genElId(d),nodeType:'decision',name:'',x,y,size:40}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='merge'){ d.nodes.push({id:genElId(d),nodeType:'merge',name:'',x,y,size:40}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='forkJoin'){ d.nodes.push({id:genElId(d),nodeType:'forkJoin',name:'',x,y,width:150,height:150,orientation:'horizontal'}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='component'){ d.components.push({id:genElId(d),name:'Component',x,y,width:140,height:70}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='interfaceProv'){ d.interfaces.push({id:genElId(d),ifaceType:'provided',name:'IProvided',x,y,direction:'right'}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='interfaceReq'){ d.interfaces.push({id:genElId(d),ifaceType:'required',name:'IRequired',x,y,direction:'right'}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='package'){ d.packages.push({id:genElId(d),name:'Package',x:x-100,y:y-90,width:200,height:150}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='node'){ d.deployNodes.push({id:genElId(d),nodeType:'node',name:'Node',x:x-80,y:y-50,width:160,height:100}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='device'){ d.deployNodes.push({id:genElId(d),nodeType:'device',name:'Device',x:x-80,y:y-50,width:160,height:100}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='execEnv'){ d.deployNodes.push({id:genElId(d),nodeType:'execEnv',name:'ExecEnv',x:x-80,y:y-50,width:160,height:100}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='artifact'){ d.artifacts.push({id:genElId(d),name:'Artifact',x,y,width:120,height:50}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='class'){ d.classes.push({id:genElId(d),classType:'class',name:'NewClass',x,y,width:180,attributes:[],methods:[]}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='interfaceClass'){ d.classes.push({id:genElId(d),classType:'interface',name:'INewInterface',x,y,width:180,attributes:[],methods:[]}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='abstractClass'){ d.classes.push({id:genElId(d),classType:'abstract',name:'AbstractClass',x,y,width:180,attributes:[],methods:[]}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='enumeration'){ d.classes.push({id:genElId(d),classType:'enumeration',name:'Enum',x,y,width:180,attributes:[],methods:[]}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='participant'){ d.participants.push({id:genElId(d),name:'Participant',x,y:60,width:120}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='activation'){
let nearest=null, minDist=Infinity;
(d.participants||[]).forEach(p=>{ const dist=Math.abs(x-p.x); if(dist<minDist){minDist=dist;nearest=p;} });
if(nearest && minDist<50){
d.activations.push({id:genElId(d),participantId:nearest.id,x:nearest.x,y:y,width:12,height:60});
pushHistory(d); updateInfo(); draw(); switchToSelect(); return;
}
}
else if(tool==='fragment'){
const fragType = prompt('نوع Fragment (alt/opt/loop/break):','alt') || 'alt';
if(['alt','opt','loop','break'].includes(fragType)){
d.fragments.push({id:genElId(d),fragmentType:fragType,label:'',x,y,width:300,height:150,operands:[{label:'',height:70},{label:'',height:70}]});
pushHistory(d); updateInfo(); draw(); switchToSelect(); return;
}
}
else if(tool==='swimlane'){ d.swimlanes.push({id:genElId(d),name:'Swimlane',x:x-200,y:y-150,width:400,height:300,orientation:'vertical',color:null}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='swAction'){ d.swNodes.push({id:genElId(d),nodeType:'swAction',name:'Action',x,y,width:120,height:50}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='swDecision'){ d.swNodes.push({id:genElId(d),nodeType:'swDecision',name:'',x,y,size:40}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='swForkJoin'){ d.swNodes.push({id:genElId(d),nodeType:'swForkJoin',name:'',x,y,width:150,height:150,orientation:'horizontal'}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='swInitial'){ d.swNodes.push({id:genElId(d),nodeType:'swInitial',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(tool==='swFinal'){ d.swNodes.push({id:genElId(d),nodeType:'swFinal',name:'',x,y}); pushHistory(d); updateInfo(); draw(); switchToSelect(); return; }
else if(['association','include','extend','generalization','transition','controlFlow','objectFlow','dependency','connector','compAssociation','commPath','deployRel','manifestRel','clsAssociation','aggregation','composition','inheritance','clsDependency','realization','syncMsg','asyncMsg','returnMsg','selfMsg','swFlow'].includes(tool)){
const el=hitTest(x,y);
if(el){state.isDrawingRelationship=true;state.relationshipStart=elCenter(el);state.relationshipStartId=el.id;}
}
draw();
});
canvas.addEventListener('mousemove',e=>{
const d=getActive(); if(!d) return;
const {x,y}=getPos(e); state.mousePos={x,y};
// Panning
if(state.isPanning){
state.panX = e.clientX - state.panStart.x;
state.panY = e.clientY - state.panStart.y;
draw();
return;
}
if(state.resizingSwimlane){
const dx=x-state.dragStart.x, dy=y-state.dragStart.y;
const sw=state.resizingSwimlane;
const i=(d.swimlanes||[]).findIndex(s=>s.id===sw.id);
if(i>=0){
const handle = state.resizeHandle;
let newX=state.dragStart.elX, newY=state.dragStart.elY, newW=state.dragStart.elW, newH=state.dragStart.elH;
if(handle.includes('r')) newW = Math.max(100, state.dragStart.elW + dx);
if(handle.includes('l')){ newX = state.dragStart.elX + dx; newW = Math.max(100, state.dragStart.elW - dx); }
if(handle.includes('b')) newH = Math.max(100, state.dragStart.elH + dy);
if(handle.includes('t')){ newY = state.dragStart.elY + dy; newH = Math.max(100, state.dragStart.elH - dy); }
d.swimlanes[i].x = newX;
d.swimlanes[i].y = newY;
d.swimlanes[i].width = newW;
d.swimlanes[i].height = newH;
}
draw();
return;
}
if(state.isDragging&&state.selectedElement){
const dx=x-state.dragStart.x, dy=y-state.dragStart.y;
const el=state.selectedElement;
if(d.diagramType==='usecase'){
if(el.elType==='actor'){const i=d.actors.findIndex(a=>a.id===el.id);if(i>=0){d.actors[i].x=state.dragStart.elX+dx;d.actors[i].y=state.dragStart.elY+dy;}}
else if(el.elType==='useCase'){const i=d.useCases.findIndex(u=>u.id===el.id);if(i>=0){d.useCases[i].x=state.dragStart.elX+dx;d.useCases[i].y=state.dragStart.elY+dy;}}
else if(el.elType==='systemBoundary'){d.systemBoundary.x=state.dragStart.elX+dx;d.systemBoundary.y=state.dragStart.elY+dy;}
} else if(d.diagramType==='statemachine'){ const i=(d.states||[]).findIndex(s=>s.id===el.id); if(i>=0){d.states[i].x=state.dragStart.elX+dx;d.states[i].y=state.dragStart.elY+dy;} }
else if(d.diagramType==='activity'){ const i=(d.nodes||[]).findIndex(n=>n.id===el.id); if(i>=0){d.nodes[i].x=state.dragStart.elX+dx;d.nodes[i].y=state.dragStart.elY+dy;} }
else if(d.diagramType==='component'){ if(el.elType==='component'){const i=d.components.findIndex(c=>c.id===el.id);if(i>=0){d.components[i].x=state.dragStart.elX+dx;d.components[i].y=state.dragStart.elY+dy;}} else if(el.elType==='interfaceProv'||el.elType==='interfaceReq'){const i=d.interfaces.findIndex(iface=>iface.id===el.id);if(i>=0){d.interfaces[i].x=state.dragStart.elX+dx;d.interfaces[i].y=state.dragStart.elY+dy;}} else if(el.elType==='package'){const i=d.packages.findIndex(p=>p.id===el.id);if(i>=0){d.packages[i].x=state.dragStart.elX+dx;d.packages[i].y=state.dragStart.elY+dy;}} }
else if(d.diagramType==='deployment'){ if(el.elType==='node'||el.elType==='device'||el.elType==='execEnv'){const i=d.deployNodes.findIndex(n=>n.id===el.id);if(i>=0){d.deployNodes[i].x=state.dragStart.elX+dx;d.deployNodes[i].y=state.dragStart.elY+dy;}} else if(el.elType==='artifact'){const i=d.artifacts.findIndex(a=>a.id===el.id);if(i>=0){d.artifacts[i].x=state.dragStart.elX+dx;d.artifacts[i].y=state.dragStart.elY+dy;}} }
else if(d.diagramType==='class'){ const i=(d.classes||[]).findIndex(cls=>cls.id===el.id); if(i>=0){d.classes[i].x=state.dragStart.elX+dx;d.classes[i].y=state.dragStart.elY+dy;} }
else if(d.diagramType==='sequence'){
if(el.elType==='participant'){const i=d.participants.findIndex(p=>p.id===el.id);if(i>=0){d.participants[i].x=state.dragStart.elX+dx;d.participants[i].y=state.dragStart.elY+dy;}}
else if(el.elType==='activation'){const i=d.activations.findIndex(a=>a.id===el.id);if(i>=0){d.activations[i].y=state.dragStart.elY+dy;}}
else if(el.elType==='fragment'){const i=d.fragments.findIndex(f=>f.id===el.id);if(i>=0){d.fragments[i].x=state.dragStart.elX+dx;d.fragments[i].y=state.dragStart.elY+dy;}}
}
else if(d.diagramType==='swimlane'){
if(el.elType==='swimlane'){const i=d.swimlanes.findIndex(s=>s.id===el.id);if(i>=0){d.swimlanes[i].x=state.dragStart.elX+dx;d.swimlanes[i].y=state.dragStart.elY+dy;}}
else {const i=(d.swNodes||[]).findIndex(n=>n.id===el.id);if(i>=0){d.swNodes[i].x=state.dragStart.elX+dx;d.swNodes[i].y=state.dragStart.elY+dy;}}
}
updatePropsPanel(); draw();
} else if(state.isDrawingRelationship) draw();
});
canvas.addEventListener('mouseup',e=>{
const d=getActive(); if(!d) return;
// End panning
if(state.isPanning){
state.isPanning = false;
canvas.style.cursor = 'crosshair';
return;
}
const {x,y}=getPos(e);
if(state.resizingSwimlane){
pushHistory(d);
state.resizingSwimlane = null;
state.resizeHandle = null;
return;
}
if(state.isDrawingRelationship){
const el=hitTest(x,y);
if(el&&el.id!==state.relationshipStartId){
let changed = false;
if(state.currentTool==='transition'){ const label = prompt('برچسب Transition (اختیاری):','') || ''; d.transitions.push({id:genElId(d),from:state.relationshipStartId,to:el.id,label:label}); changed=true; }
else if(state.currentTool==='controlFlow'||state.currentTool==='objectFlow'){ const label = prompt('برچسب Flow (اختیاری):','') || ''; d.flows.push({id:genElId(d),from:state.relationshipStartId,to:el.id,flowType:state.currentTool,label:label}); changed=true; }
else if(state.currentTool==='dependency'||state.currentTool==='connector'||state.currentTool==='compAssociation'){ const label = prompt('برچسب رابطه (اختیاری):','') || ''; d.compRelations.push({id:genElId(d),from:state.relationshipStartId,to:el.id,relType:state.currentTool,label:label}); changed=true; }
else if(state.currentTool==='commPath'||state.currentTool==='deployRel'||state.currentTool==='manifestRel'){ const label = prompt('برچسب رابطه (اختیاری):','') || ''; d.deployRelations.push({id:genElId(d),from:state.relationshipStartId,to:el.id,relType:state.currentTool,label:label}); changed=true; }
else if(state.currentTool==='clsAssociation'||state.currentTool==='aggregation'||state.currentTool==='composition'||state.currentTool==='inheritance'||state.currentTool==='clsDependency'||state.currentTool==='realization'){
const label = prompt('برچسب رابطه (اختیاری):','') || '';
const fromMult = prompt('Multiplicity مبدأ (مثال: 1, 0..*, 1..*):','') || '';
const toMult = prompt('Multiplicity مقصد (مثال: 1, 0..*, 1..*):','') || '';
d.classRelations.push({id:genElId(d),from:state.relationshipStartId,to:el.id,relType:state.currentTool,label:label,fromMultiplicity:fromMult,toMultiplicity:toMult});
changed=true;
}
else if(state.currentTool==='syncMsg'||state.currentTool==='asyncMsg'||state.currentTool==='returnMsg'){
const label = prompt('برچسب پیام (مثال: doSomething()):','') || '';
d.messages.push({id:genElId(d),fromParticipant:state.relationshipStartId,toParticipant:el.id,msgType:state.currentTool,label:label,y:y});
changed=true;
}
else if(state.currentTool==='selfMsg'){
const label = prompt('برچسب پیام خودی:','') || '';
d.messages.push({id:genElId(d),fromParticipant:state.relationshipStartId,toParticipant:state.relationshipStartId,msgType:'selfMsg',label:label,y:y});
changed=true;
}
else if(state.currentTool==='swFlow'){
const label = prompt('برچسب Flow (اختیاری):','') || '';
d.swFlows.push({id:genElId(d),from:state.relationshipStartId,to:el.id,label:label});
changed=true;
}
else { d.relationships.push({id:genElId(d),from:state.relationshipStartId,to:el.id,type:state.currentTool}); changed=true; }
if(changed) pushHistory(d);
updateInfo(); draw(); switchToSelect();
}
state.isDrawingRelationship=false; state.relationshipStart=null; state.relationshipStartId=null;
}
if(state.isDragging && state.selectedElement){
pushHistory(d);
}
state.isDragging=false; draw();
});
canvas.addEventListener('dblclick', e=>{
const d=getActive(); if(!d) return;
const {x,y}=getPos(e);
const el=hitTest(x,y);
if(!el) return;
if((el.elType==='interfaceProv'||el.elType==='interfaceReq') && d.diagramType==='component'){
const i=d.interfaces.findIndex(iface=>iface.id===el.id);
if(i>=0){ const dirs=['right','down','left','up']; const cur=d.interfaces[i].direction||'right'; d.interfaces[i].direction = dirs[(dirs.indexOf(cur)+1)%4]; pushHistory(d); draw(); }
}
else if((el.elType==='class'||el.elType==='interfaceClass'||el.elType==='abstractClass'||el.elType==='enumeration') && d.diagramType==='class'){
openClassEditor(el.id);
}
else if(el.elType==='swimlane' && d.diagramType==='swimlane'){
const i=d.swimlanes.findIndex(s=>s.id===el.id);
if(i>=0){
d.swimlanes[i].orientation = d.swimlanes[i].orientation==='vertical' ? 'horizontal' : 'vertical';
pushHistory(d);
draw();
}
}
});
// Zoom with mouse wheel (Ctrl + Wheel)
canvas.addEventListener('wheel', e=>{
e.preventDefault();
if(e.ctrlKey || e.metaKey){
const rect = canvas.getBoundingClientRect();
const centerX = (e.clientX - rect.left) * (canvas.width / rect.width);
const centerY = (e.clientY - rect.top) * (canvas.height / rect.height);
const delta = e.deltaY > 0 ? 0.9 : 1.1;
setZoom(state.zoom * delta, centerX, centerY);
}
}, {passive: false});
// ===== CLASS EDITOR =====
const classEditorModal = document.getElementById('classEditorModal');
let classEditorData = { attributes: [], methods: [] };
let activeEditorTab = 'attributes';
function openClassEditor(classId){
const d=getActive(); if(!d) return;
const cls = (d.classes||[]).find(c=>c.id===classId);
if(!cls) return;
state.editingClassId = classId;
document.getElementById('classEditorName').value = cls.name || '';
classEditorData = {
attributes: JSON.parse(JSON.stringify(cls.attributes||[])),
methods: JSON.parse(JSON.stringify(cls.methods||[]))
};
activeEditorTab = 'attributes';
updateEditorTabsUI();
renderAttributesTable();
renderMethodsTable();
classEditorModal.classList.add('visible');
}
function closeClassEditor(){
classEditorModal.classList.remove('visible');
state.editingClassId = null;
}
function updateEditorTabsUI(){
document.querySelectorAll('.class-editor-tab').forEach(t=>{
t.classList.toggle('active', t.dataset.tab===activeEditorTab);
});
document.getElementById('attributesTab').style.display = activeEditorTab==='attributes'?'':'none';
document.getElementById('methodsTab').style.display = activeEditorTab==='methods'?'':'none';
}
document.querySelectorAll('.class-editor-tab').forEach(t=>{
t.addEventListener('click', ()=>{
activeEditorTab = t.dataset.tab;
updateEditorTabsUI();
});
});
function renderAttributesTable(){
const tbody = document.getElementById('attributesTableBody');
tbody.innerHTML = '';
classEditorData.attributes.forEach((attr, idx)=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<select data-idx="${idx}" data-field="visibility">
<option value="+" ${attr.visibility==='+'?'selected':''}>+ public</option>
<option value="-" ${attr.visibility==='-'?'selected':''}>- private</option>
<option value="#" ${attr.visibility==='#'?'selected':''}># protected</option>
<option value="~" ${attr.visibility==='~'?'selected':''}>~ package</option>
</select>
</td>
<td><input type="text" value="${attr.name||''}" data-idx="${idx}" data-field="name" placeholder="نام"></td>
<td><input type="text" value="${attr.type||''}" data-idx="${idx}" data-field="type" placeholder="نوع"></td>
<td style="text-align:center"><input type="checkbox" ${attr.isStatic?'checked':''} data-idx="${idx}" data-field="isStatic"></td>
<td><button class="member-btn member-btn-del" data-idx="${idx}" data-action="del-attr">×</button></td>
`;
tbody.appendChild(tr);
});
tbody.querySelectorAll('select, input').forEach(el=>{
el.addEventListener('change', e=>{
const idx = parseInt(e.target.dataset.idx);
const field = e.target.dataset.field;
if(field==='isStatic'){
classEditorData.attributes[idx].isStatic = e.target.checked;
} else {
classEditorData.attributes[idx][field] = e.target.value;
}
});
});
tbody.querySelectorAll('[data-action="del-attr"]').forEach(btn=>{
btn.addEventListener('click', e=>{
const idx = parseInt(e.target.dataset.idx);
classEditorData.attributes.splice(idx, 1);
renderAttributesTable();
});
});
}
function renderMethodsTable(){
const tbody = document.getElementById('methodsTableBody');
tbody.innerHTML = '';
classEditorData.methods.forEach((meth, idx)=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td>
<select data-idx="${idx}" data-field="visibility">
<option value="+" ${meth.visibility==='+'?'selected':''}>+ public</option>
<option value="-" ${meth.visibility==='-'?'selected':''}>- private</option>
<option value="#" ${meth.visibility==='#'?'selected':''}># protected</option>
<option value="~" ${meth.visibility==='~'?'selected':''}>~ package</option>
</select>
</td>
<td><input type="text" value="${meth.name||''}" data-idx="${idx}" data-field="name" placeholder="نام"></td>
<td><input type="text" value="${meth.params||''}" data-idx="${idx}" data-field="params" placeholder="x: int, y: string"></td>
<td><input type="text" value="${meth.returnType||''}" data-idx="${idx}" data-field="returnType" placeholder="void"></td>
<td style="text-align:center"><input type="checkbox" ${meth.isStatic?'checked':''} data-idx="${idx}" data-field="isStatic"></td>
<td><button class="member-btn member-btn-del" data-idx="${idx}" data-action="del-meth">×</button></td>
`;
tbody.appendChild(tr);
});
tbody.querySelectorAll('select, input').forEach(el=>{
el.addEventListener('change', e=>{
const idx = parseInt(e.target.dataset.idx);
const field = e.target.dataset.field;
if(field==='isStatic'){
classEditorData.methods[idx].isStatic = e.target.checked;
} else {
classEditorData.methods[idx][field] = e.target.value;
}
});
});
tbody.querySelectorAll('[data-action="del-meth"]').forEach(btn=>{
btn.addEventListener('click', e=>{
const idx = parseInt(e.target.dataset.idx);
classEditorData.methods.splice(idx, 1);
renderMethodsTable();
});
});
}
document.getElementById('addAttrBtn').addEventListener('click', ()=>{
classEditorData.attributes.push({visibility:'+',name:'newAttr',type:'String',isStatic:false});
renderAttributesTable();
});
document.getElementById('addMethodBtn').addEventListener('click', ()=>{
classEditorData.methods.push({visibility:'+',name:'newMethod',params:'',returnType:'void',isStatic:false});
renderMethodsTable();
});
document.getElementById('closeClassEditor').addEventListener('click', closeClassEditor);
document.getElementById('cancelClassEditor').addEventListener('click', closeClassEditor);
classEditorModal.addEventListener('click', e=>{ if(e.target===classEditorModal) closeClassEditor(); });
document.getElementById('saveClassEditor').addEventListener('click', ()=>{
const d=getActive(); if(!d) return;
const cls = (d.classes||[]).find(c=>c.id===state.editingClassId);
if(!cls) return;
cls.name = document.getElementById('classEditorName').value.trim() || 'Unnamed';
cls.attributes = classEditorData.attributes;
cls.methods = classEditorData.methods;
pushHistory(d);
closeClassEditor();
draw();
updateStatus('✅ Class ویرایش شد');
});
// ===== PROPERTIES INPUT =====
document.getElementById('elementName').addEventListener('input',e=>{
if(!state.selectedElement) return;
const d=getActive(); if(!d) return;
const id=state.selectedElement.id, v=e.target.value;
if(d.diagramType==='usecase'){ if(state.selectedElement.elType==='actor'){const i=d.actors.findIndex(a=>a.id===id);if(i>=0)d.actors[i].name=v;} else if(state.selectedElement.elType==='useCase'){const i=d.useCases.findIndex(u=>u.id===id);if(i>=0)d.useCases[i].name=v;} else if(state.selectedElement.elType==='systemBoundary') d.systemBoundary.name=v; }
else if(d.diagramType==='statemachine'){ const i=(d.states||[]).findIndex(s=>s.id===id); if(i>=0) d.states[i].name=v; }
else if(d.diagramType==='activity'){ const i=(d.nodes||[]).findIndex(n=>n.id===id); if(i>=0) d.nodes[i].name=v; }
else if(d.diagramType==='component'){ if(state.selectedElement.elType==='component'){const i=d.components.findIndex(c=>c.id===id);if(i>=0)d.components[i].name=v;} else if(state.selectedElement.elType==='interfaceProv'||state.selectedElement.elType==='interfaceReq'){const i=d.interfaces.findIndex(iface=>iface.id===id);if(i>=0)d.interfaces[i].name=v;} else if(state.selectedElement.elType==='package'){const i=d.packages.findIndex(p=>p.id===id);if(i>=0)d.packages[i].name=v;} }
else if(d.diagramType==='deployment'){ if(state.selectedElement.elType==='node'||state.selectedElement.elType==='device'||state.selectedElement.elType==='execEnv'){const i=d.deployNodes.findIndex(n=>n.id===id);if(i>=0)d.deployNodes[i].name=v;} else if(state.selectedElement.elType==='artifact'){const i=d.artifacts.findIndex(a=>a.id===id);if(i>=0)d.artifacts[i].name=v;} }
else if(d.diagramType==='class'){ const i=(d.classes||[]).findIndex(cls=>cls.id===id); if(i>=0) d.classes[i].name=v; }
else if(d.diagramType==='sequence'){ if(state.selectedElement.elType==='participant'){const i=d.participants.findIndex(p=>p.id===id);if(i>=0)d.participants[i].name=v;} }
else if(d.diagramType==='swimlane'){
if(state.selectedElement.elType==='swimlane'){const i=d.swimlanes.findIndex(s=>s.id===id);if(i>=0)d.swimlanes[i].name=v;}
else {const i=(d.swNodes||[]).findIndex(n=>n.id===id);if(i>=0)d.swNodes[i].name=v;}
}
draw();
});
// ===== ACTION BUTTONS =====
document.getElementById('deleteBtn').addEventListener('click',()=>{
if(!state.selectedElement) return;
const d=getActive(); if(!d) return;
const {id,elType}=state.selectedElement;
if(d.diagramType==='usecase'){ if(elType==='actor') d.actors=d.actors.filter(a=>a.id!==id); else if(elType==='useCase') d.useCases=d.useCases.filter(u=>u.id!==id); else if(elType==='systemBoundary') d.systemBoundary=null; d.relationships=d.relationships.filter(r=>r.from!==id&&r.to!==id); }
else if(d.diagramType==='statemachine'){ d.states=(d.states||[]).filter(s=>s.id!==id); d.transitions=(d.transitions||[]).filter(t=>t.from!==id&&t.to!==id); }
else if(d.diagramType==='activity'){ d.nodes=(d.nodes||[]).filter(n=>n.id!==id); d.flows=(d.flows||[]).filter(f=>f.from!==id&&f.to!==id); }
else if(d.diagramType==='component'){ if(elType==='component'){ d.components=d.components.filter(c=>c.id!==id); } else if(elType==='interfaceProv'||elType==='interfaceReq'){ d.interfaces=d.interfaces.filter(i=>i.id!==id); } else if(elType==='package'){ d.packages=d.packages.filter(p=>p.id!==id); } d.compRelations=(d.compRelations||[]).filter(r=>r.from!==id&&r.to!==id); }
else if(d.diagramType==='deployment'){ if(elType==='node'||elType==='device'||elType==='execEnv'){ d.deployNodes=d.deployNodes.filter(n=>n.id!==id); } else if(elType==='artifact'){ d.artifacts=d.artifacts.filter(a=>a.id!==id); } d.deployRelations=(d.deployRelations||[]).filter(r=>r.from!==id&&r.to!==id); }
else if(d.diagramType==='class'){ d.classes=(d.classes||[]).filter(c=>c.id!==id); d.classRelations=(d.classRelations||[]).filter(r=>r.from!==id&&r.to!==id); }
else if(d.diagramType==='sequence'){
if(elType==='participant'){ d.participants=d.participants.filter(p=>p.id!==id); d.activations=d.activations.filter(a=>a.participantId!==id); d.messages=d.messages.filter(m=>m.fromParticipant!==id&&m.toParticipant!==id); }
else if(elType==='activation'){ d.activations=d.activations.filter(a=>a.id!==id); }
else if(elType==='fragment'){ d.fragments=d.fragments.filter(f=>f.id!==id); }
}
else if(d.diagramType==='swimlane'){
if(elType==='swimlane'){ d.swimlanes=d.swimlanes.filter(s=>s.id!==id); }
else { d.swNodes=(d.swNodes||[]).filter(n=>n.id!==id); }
d.swFlows=(d.swFlows||[]).filter(f=>f.from!==id&&f.to!==id);
}
pushHistory(d);
state.selectedElement=null; updatePropsPanel(); updateInfo(); draw();
});
document.getElementById('clearBtn').addEventListener('click',()=>{
const d=getActive(); if(!d) return;
if(!confirm(`پاک کردن "${d.title}"؟`)) return;
if(d.diagramType==='usecase'){d.actors=[];d.useCases=[];d.systemBoundary=null;d.relationships=[];}
else if(d.diagramType==='statemachine'){d.states=[];d.transitions=[];}
else if(d.diagramType==='activity'){d.nodes=[];d.flows=[];}
else if(d.diagramType==='component'){d.components=[];d.interfaces=[];d.packages=[];d.compRelations=[];}
else if(d.diagramType==='deployment'){d.deployNodes=[];d.artifacts=[];d.deployRelations=[];}
else if(d.diagramType==='class'){d.classes=[];d.classRelations=[];}
else if(d.diagramType==='sequence'){d.participants=[];d.activations=[];d.messages=[];d.fragments=[];}
else if(d.diagramType==='swimlane'){d.swimlanes=[];d.swNodes=[];d.swFlows=[];}
pushHistory(d);
state.selectedElement=null; updatePropsPanel(); updateInfo(); draw();
});
// ===== KEYBOARD =====
document.addEventListener('keydown',e=>{
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT'){
if(e.key==='Enter'&&saveModal.classList.contains('visible')&&e.target===saveFN){e.preventDefault();document.getElementById('confirmSaveBtn').click();}
return;
}
if(e.key==='Delete'||e.key==='Backspace') document.getElementById('deleteBtn').click();
else if(e.key===' '){
e.preventDefault();
state.spacePressed = true;
canvas.style.cursor = 'grab';
}
else if(e.key==='Escape'){
if(classEditorModal.classList.contains('visible')) closeClassEditor();
else if(document.getElementById('newDiagramModal').classList.contains('visible')) hideNewDiagramModal();
else if(expModal.classList.contains('visible')) hideExpPng();
else if(saveModal.classList.contains('visible')) hideSaveModal();
else if(fileSettingsModal.classList.contains('visible')) hideFileSettings();
else if(settingsModal.classList.contains('visible')) hideSettings();
else { state.selectedElement=null; updatePropsPanel(); draw(); }
}
else if(e.ctrlKey&&e.key==='n'){e.preventDefault();showNewDiagramModal();}
else if(e.ctrlKey&&e.key==='s'){e.preventDefault();showSaveModal();}
else if(e.ctrlKey&&e.key==='z'){e.preventDefault();undo();}
else if(e.ctrlKey&&(e.key==='y'||(e.shiftKey&&e.key==='Z'))){e.preventDefault();redo();}
else if((e.key==='b'||e.key==='B')&&!e.ctrlKey&&!e.altKey) toggleSidebar();
else if((e.key==='g'||e.key==='G')&&!e.ctrlKey&&!e.altKey){ const o=['none','lines','dots']; state.gridType=o[(o.indexOf(state.gridType)+1)%3]; setGridUI(state.gridType); draw(); }
});
document.addEventListener('keyup',e=>{
if(e.key===' '){
state.spacePressed = false;
if(!state.isPanning) canvas.style.cursor = 'crosshair';
}
});
// ===== INIT =====
// Initialize collapsed groups (all except selection)
['uc-elements','uc-relations','sm-elements','sm-relations','act-elements','act-flows',
'comp-elements','comp-relations','dep-elements','dep-relations','cls-elements','cls-relations',
'seq-elements','seq-messages','sw-elements','sw-flows','display','actions','file'].forEach(g=>state.collapsedGroups.add(g));
// Apply collapsed class to all groups except selection
document.querySelectorAll('.tool-group').forEach(g=>{
if(g.dataset.group !== 'selection'){
g.classList.add('collapsed');
}
});
applyTheme(); setGridUI(state.gridType); updateToolVisibility(); updateZoomDisplay();
renderTabs(); updateInfo(); updateVis(); scheduleResize();
</script>
</body>
</html>