@@ -23744,6 +24144,13 @@
icon: "📋",
description: "مدیریت بردهای انتشار",
tools: [
+ {
+ id: "drawings",
+ title: "ترسیمها",
+ icon: "🎨",
+ action: "showDrawings",
+ description: "ترسیمها و دیاگرامها",
+ },
{
id: "release-boards",
title: "بردهای انتشار",
@@ -23812,10 +24219,11 @@
},
],
appData: {
- environments: [],
- products: [],
boards: [],
+ products: [],
+ drawings: [],
boardTags: [],
+ environments: [],
environmentData: [],
},
};
@@ -24605,6 +25013,10 @@
});
});
+ if (!ld.drawings) {
+ ld.drawings = [];
+ }
+
if (!ld.environmentData) {
ld.environmentData = [];
}
@@ -24632,10 +25044,11 @@
if (!confirm("⚠️ تأیید نهایی؟")) return;
HistoryManager.pushState();
projectData.appData = {
- environments: [],
- products: [],
boards: [],
+ products: [],
+ drawings: [],
boardTags: [],
+ environments: [],
environmentData: [],
};
AutosaveManager.clear();
@@ -24662,6 +25075,9 @@
showRiskAnalysis() {
RiskAnalysisManager.open();
},
+ showDrawings() {
+ DrawingsManager.open();
+ },
showReleaseBoards() {
ReleaseBoardsManager.open();
},
@@ -30417,6 +30833,695 @@ ${checklistHtml}
);
},
};
+ /* ==================== DRAWINGS MANAGER ==================== */
+ const DrawingsManager = {
+ activeDrawingId: null,
+ currentTool: "select",
+ currentColor: "#4299e1",
+ zoom: 1,
+ undoStack: [],
+ redoStack: [],
+ isDrawing: false,
+ startX: 0,
+ startY: 0,
+ currentPath: [],
+ selectedShapeId: null,
+ dragOffset: { x: 0, y: 0 },
+ isDragging: false,
+
+ open() {
+ ModalManager.open("drawingsModal");
+ setTimeout(() => {
+ this.renderTabs();
+ this.bindCanvasEvents();
+ if (projectData.appData.drawings?.length > 0 && !this.activeDrawingId) {
+ this.setActiveDrawing(projectData.appData.drawings[0].id);
+ } else {
+ this.updateCanvasVisibility();
+ }
+ }, 100);
+ },
+
+ getActiveDrawing() {
+ return (projectData.appData.drawings || []).find((d) => d.id === this.activeDrawingId);
+ },
+
+ renderTabs() {
+ const tabsBar = document.getElementById("drawingsTabsBar");
+ if (!tabsBar) return;
+ const drawings = projectData.appData.drawings || [];
+
+ if (drawings.length === 0) {
+ tabsBar.innerHTML = '
هیچ ترسیمی ایجاد نشده است
';
+ return;
+ }
+
+ tabsBar.innerHTML = drawings.map((d) => {
+ const isActive = d.id === this.activeDrawingId;
+ return `
+ 🎨 ${this.escapeHtml(d.title)}
+ ✕
+
`;
+ }).join("");
+ },
+
+ openNewDrawingModal() {
+ document.getElementById("newDrawingTitle").value = "";
+ document.getElementById("newDrawingDescription").value = "";
+ ModalManager.open("newDrawingModal");
+ },
+
+ createDrawing() {
+ const title = document.getElementById("newDrawingTitle").value.trim();
+ const description = document.getElementById("newDrawingDescription").value.trim();
+
+ if (!title) {
+ showToast("⚠️ عنوان الزامی است", "error");
+ return;
+ }
+
+ HistoryManager.pushState();
+ AutosaveManager.scheduleSave();
+
+ if (!projectData.appData.drawings) projectData.appData.drawings = [];
+
+ const newDrawing = {
+ id: `draw-${Date.now()}`,
+ title,
+ description,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ canvasWidth: 1200,
+ canvasHeight: 800,
+ shapes: [],
+ };
+
+ projectData.appData.drawings.push(newDrawing);
+ this.activeDrawingId = newDrawing.id;
+ this.undoStack = [];
+ this.redoStack = [];
+
+ ModalManager.close("newDrawingModal");
+ this.renderTabs();
+ this.updateCanvasVisibility();
+ this.setupCanvas();
+ this.redraw();
+
+ document.getElementById("drawingsActiveTitle").textContent = title;
+ showToast(`✅ ترسیم «${title}» ایجاد شد`, "success");
+ },
+
+ setActiveDrawing(drawingId) {
+ this.activeDrawingId = drawingId;
+ this.undoStack = [];
+ this.redoStack = [];
+ this.renderTabs();
+ this.updateCanvasVisibility();
+ this.setupCanvas();
+ this.redraw();
+
+ const drawing = this.getActiveDrawing();
+ if (drawing) {
+ document.getElementById("drawingsActiveTitle").textContent = drawing.title;
+ }
+ },
+
+ deleteDrawing(drawingId) {
+ const drawing = (projectData.appData.drawings || []).find((d) => d.id === drawingId);
+ if (!drawing) return;
+
+ if (!confirm(`حذف ترسیم «${drawing.title}»؟`)) return;
+
+ HistoryManager.pushState();
+ AutosaveManager.scheduleSave();
+
+ projectData.appData.drawings = projectData.appData.drawings.filter((d) => d.id !== drawingId);
+
+ if (this.activeDrawingId === drawingId) {
+ this.activeDrawingId = projectData.appData.drawings[0]?.id || null;
+ }
+
+ this.renderTabs();
+ this.updateCanvasVisibility();
+
+ if (this.activeDrawingId) {
+ this.setupCanvas();
+ this.redraw();
+ const activeDrawing = this.getActiveDrawing();
+ document.getElementById("drawingsActiveTitle").textContent = activeDrawing?.title || "";
+ } else {
+ document.getElementById("drawingsActiveTitle").textContent = "هیچ ترسیمی فعال نیست";
+ }
+
+ showToast("🗑️ ترسیم حذف شد", "warning");
+ },
+
+ updateCanvasVisibility() {
+ const toolsBar = document.getElementById("drawingsToolsBar");
+ const emptyState = document.getElementById("drawingsEmptyState");
+ const canvas = document.getElementById("drawingsCanvas");
+ const hasDrawing = !!this.getActiveDrawing();
+
+ toolsBar.style.display = hasDrawing ? "flex" : "none";
+ emptyState.style.display = hasDrawing ? "none" : "flex";
+ canvas.style.display = hasDrawing ? "block" : "none";
+ },
+
+ setupCanvas() {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+
+ const canvas = document.getElementById("drawingsCanvas");
+ canvas.width = drawing.canvasWidth;
+ canvas.height = drawing.canvasHeight;
+
+ document.getElementById("drawingsCanvasWidth").value = drawing.canvasWidth;
+ document.getElementById("drawingsCanvasHeight").value = drawing.canvasHeight;
+
+ this.zoom = 1;
+ this.updateZoomLabel();
+ canvas.style.transform = "";
+ },
+
+ resizeCanvas() {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+
+ const newWidth = parseInt(document.getElementById("drawingsCanvasWidth").value) || 1200;
+ const newHeight = parseInt(document.getElementById("drawingsCanvasHeight").value) || 800;
+
+ if (newWidth < 400 || newWidth > 5000 || newHeight < 300 || newHeight > 5000) {
+ showToast("⚠️ ابعاد باید بین ۴۰۰ تا ۵۰۰۰ باشد", "warning");
+ return;
+ }
+
+ this.pushUndo();
+ drawing.canvasWidth = newWidth;
+ drawing.canvasHeight = newHeight;
+ drawing.updatedAt = new Date().toISOString();
+
+ AutosaveManager.scheduleSave();
+ this.setupCanvas();
+ this.redraw();
+ showToast("📐 ابعاد کانواس تغییر کرد", "success");
+ },
+
+ bindCanvasEvents() {
+ const canvas = document.getElementById("drawingsCanvas");
+
+ canvas.addEventListener("mousedown", (e) => this.onMouseDown(e));
+ canvas.addEventListener("mousemove", (e) => this.onMouseMove(e));
+ canvas.addEventListener("mouseup", (e) => this.onMouseUp(e));
+ canvas.addEventListener("mouseleave", (e) => this.onMouseUp(e));
+
+ // اتصال رویداد ابزارها
+ document.querySelectorAll(".drawings-tools-bar .tool-btn[data-tool]").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ this.currentTool = btn.dataset.tool;
+ document.querySelectorAll(".drawings-tools-bar .tool-btn[data-tool]").forEach((b) => b.classList.remove("active"));
+ btn.classList.add("active");
+ });
+ });
+
+ // اتصال رویداد رنگها
+ document.querySelectorAll(".color-option").forEach((opt) => {
+ opt.addEventListener("click", () => {
+ this.currentColor = opt.dataset.color;
+ document.getElementById("drawingsColorSwatch").style.background = this.currentColor;
+ document.getElementById("drawingsColorMenu").style.display = "none";
+ });
+ });
+
+ document.getElementById("drawingsCustomColor")?.addEventListener("input", (e) => {
+ this.currentColor = e.target.value;
+ document.getElementById("drawingsColorSwatch").style.background = this.currentColor;
+ });
+ },
+
+ toggleColorMenu() {
+ const menu = document.getElementById("drawingsColorMenu");
+ menu.style.display = menu.style.display === "none" ? "block" : "none";
+ },
+
+ getCanvasCoords(e) {
+ const canvas = document.getElementById("drawingsCanvas");
+ const rect = canvas.getBoundingClientRect();
+ return {
+ x: (e.clientX - rect.left) / this.zoom,
+ y: (e.clientY - rect.top) / this.zoom,
+ };
+ },
+
+ onMouseDown(e) {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+
+ const { x, y } = this.getCanvasCoords(e);
+ this.startX = x;
+ this.startY = y;
+ this.isDrawing = true;
+
+ if (this.currentTool === "select") {
+ const shape = this.findShapeAt(x, y);
+ if (shape) {
+ this.selectedShapeId = shape.id;
+ this.isDragging = true;
+ this.dragOffset = { x: x - shape.x, y: y - shape.y };
+ this.pushUndo();
+ }
+ this.redraw();
+ return;
+ }
+
+ if (this.currentTool === "pen") {
+ this.currentPath = [{ x, y }];
+ }
+
+ if (this.currentTool === "text") {
+ const text = prompt("متن را وارد کنید:");
+ if (text) {
+ this.pushUndo();
+ this.addShape({
+ type: "text",
+ x, y,
+ text,
+ fillColor: this.currentColor,
+ });
+ }
+ this.isDrawing = false;
+ return;
+ }
+
+ if (this.currentTool === "eraser") {
+ const shape = this.findShapeAt(x, y);
+ if (shape) {
+ this.pushUndo();
+ drawing.shapes = drawing.shapes.filter((s) => s.id !== shape.id);
+ AutosaveManager.scheduleSave();
+ drawing.updatedAt = new Date().toISOString();
+ this.redraw();
+ }
+ this.isDrawing = false;
+ return;
+ }
+ },
+
+ onMouseMove(e) {
+ const drawing = this.getActiveDrawing();
+ if (!drawing || !this.isDrawing) return;
+
+ const { x, y } = this.getCanvasCoords(e);
+
+ if (this.isDragging && this.selectedShapeId) {
+ const shape = drawing.shapes.find((s) => s.id === this.selectedShapeId);
+ if (shape) {
+ shape.x = x - this.dragOffset.x;
+ shape.y = y - this.dragOffset.y;
+ this.redraw();
+ }
+ return;
+ }
+
+ if (this.currentTool === "pen") {
+ this.currentPath.push({ x, y });
+ this.redraw();
+ // رسم موقت مسیر فعلی
+ const ctx = document.getElementById("drawingsCanvas").getContext("2d");
+ ctx.strokeStyle = this.currentColor;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ this.currentPath.forEach((p, i) => {
+ if (i === 0) ctx.moveTo(p.x, p.y);
+ else ctx.lineTo(p.x, p.y);
+ });
+ ctx.stroke();
+ return;
+ }
+
+ // پیشنمایش شکل در حال رسم
+ this.redraw();
+ const ctx = document.getElementById("drawingsCanvas").getContext("2d");
+ ctx.strokeStyle = this.currentColor;
+ ctx.lineWidth = 2;
+ this.drawShapePreview(ctx, this.startX, this.startY, x, y);
+ },
+
+ onMouseUp(e) {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+
+ if (this.isDragging) {
+ this.isDragging = false;
+ AutosaveManager.scheduleSave();
+ drawing.updatedAt = new Date().toISOString();
+ return;
+ }
+
+ if (!this.isDrawing) return;
+ this.isDrawing = false;
+
+ const { x, y } = this.getCanvasCoords(e);
+
+ if (this.currentTool === "pen" && this.currentPath.length > 1) {
+ this.pushUndo();
+ this.addShape({
+ type: "pen",
+ points: [...this.currentPath],
+ strokeColor: this.currentColor,
+ });
+ this.currentPath = [];
+ return;
+ }
+
+ if (["line", "rectangle", "circle", "diamond", "arrow",
+ "uml-class", "uml-interface", "uml-package",
+ "flow-start", "flow-process", "flow-decision", "flow-io"].includes(this.currentTool)) {
+ if (Math.abs(x - this.startX) > 5 || Math.abs(y - this.startY) > 5) {
+ this.pushUndo();
+ this.addShape({
+ type: this.currentTool,
+ x: Math.min(this.startX, x),
+ y: Math.min(this.startY, y),
+ width: Math.abs(x - this.startX),
+ height: Math.abs(y - this.startY),
+ fillColor: this.currentColor,
+ strokeColor: this.darkenColor(this.currentColor),
+ });
+ }
+ }
+
+ this.redraw();
+ },
+
+ addShape(shapeData) {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+
+ drawing.shapes.push({
+ id: `shape-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
+ createdAt: new Date().toISOString(),
+ strokeWidth: 2,
+ text: "",
+ ...shapeData,
+ });
+
+ drawing.updatedAt = new Date().toISOString();
+ AutosaveManager.scheduleSave();
+ this.redraw();
+ },
+
+ drawShapePreview(ctx, x1, y1, x2, y2) {
+ const tool = this.currentTool;
+ ctx.beginPath();
+
+ if (tool === "line" || tool === "arrow") {
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.stroke();
+ if (tool === "arrow") this.drawArrowHead(ctx, x1, y1, x2, y2);
+ } else if (tool === "rectangle" || tool === "flow-process" || tool === "uml-class") {
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
+ } else if (tool === "circle" || tool === "flow-start") {
+ ctx.ellipse((x1 + x2) / 2, (y1 + y2) / 2, Math.abs(x2 - x1) / 2, Math.abs(y2 - y1) / 2, 0, 0, Math.PI * 2);
+ ctx.stroke();
+ } else if (tool === "diamond" || tool === "flow-decision") {
+ const cx = (x1 + x2) / 2, cy = (y1 + y2) / 2;
+ ctx.moveTo(cx, y1);
+ ctx.lineTo(x2, cy);
+ ctx.lineTo(cx, y2);
+ ctx.lineTo(x1, cy);
+ ctx.closePath();
+ ctx.stroke();
+ } else if (tool === "flow-io") {
+ const skew = 20;
+ ctx.moveTo(x1 + skew, y1);
+ ctx.lineTo(x2, y1);
+ ctx.lineTo(x2 - skew, y2);
+ ctx.lineTo(x1, y2);
+ ctx.closePath();
+ ctx.stroke();
+ } else if (tool === "uml-interface") {
+ ctx.arc((x1 + x2) / 2, (y1 + y2) / 2, Math.min(Math.abs(x2 - x1), Math.abs(y2 - y1)) / 2, 0, Math.PI * 2);
+ ctx.stroke();
+ } else if (tool === "uml-package") {
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
+ ctx.strokeRect(x1, y1, (x2 - x1) / 3, 20);
+ }
+ },
+
+ drawArrowHead(ctx, x1, y1, x2, y2) {
+ const angle = Math.atan2(y2 - y1, x2 - x1);
+ const headLen = 12;
+ ctx.beginPath();
+ ctx.moveTo(x2, y2);
+ ctx.lineTo(x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6));
+ ctx.moveTo(x2, y2);
+ ctx.lineTo(x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6));
+ ctx.stroke();
+ },
+
+ redraw() {
+ const canvas = document.getElementById("drawingsCanvas");
+ const ctx = canvas.getContext("2d");
+ const drawing = this.getActiveDrawing();
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = "#ffffff";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ if (!drawing) return;
+
+ drawing.shapes.forEach((shape) => {
+ this.drawShape(ctx, shape);
+ });
+ },
+
+ drawShape(ctx, shape) {
+ ctx.save();
+ ctx.strokeStyle = shape.strokeColor || "#000";
+ ctx.fillStyle = shape.fillColor || "transparent";
+ ctx.lineWidth = shape.strokeWidth || 2;
+
+ const isSelected = shape.id === this.selectedShapeId;
+ if (isSelected) {
+ ctx.shadowColor = "#ffd700";
+ ctx.shadowBlur = 8;
+ }
+
+ switch (shape.type) {
+ case "pen":
+ ctx.beginPath();
+ (shape.points || []).forEach((p, i) => {
+ if (i === 0) ctx.moveTo(p.x, p.y);
+ else ctx.lineTo(p.x, p.y);
+ });
+ ctx.stroke();
+ break;
+
+ case "line":
+ ctx.beginPath();
+ ctx.moveTo(shape.x, shape.y);
+ ctx.lineTo(shape.x + shape.width, shape.y + shape.height);
+ ctx.stroke();
+ break;
+
+ case "arrow":
+ ctx.beginPath();
+ ctx.moveTo(shape.x, shape.y);
+ ctx.lineTo(shape.x + shape.width, shape.y + shape.height);
+ ctx.stroke();
+ this.drawArrowHead(ctx, shape.x, shape.y, shape.x + shape.width, shape.y + shape.height);
+ break;
+
+ case "rectangle":
+ case "flow-process":
+ case "uml-class":
+ if (shape.fillColor && shape.fillColor !== "transparent") {
+ ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
+ }
+ ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
+ if (shape.text) {
+ ctx.fillStyle = "#000";
+ ctx.font = "12px Vazirmatn, Tahoma";
+ ctx.textAlign = "center";
+ ctx.fillText(shape.text, shape.x + shape.width / 2, shape.y + shape.height / 2);
+ }
+ break;
+
+ case "circle":
+ case "flow-start":
+ case "uml-interface":
+ ctx.beginPath();
+ ctx.ellipse(shape.x + shape.width / 2, shape.y + shape.height / 2, shape.width / 2, shape.height / 2, 0, 0, Math.PI * 2);
+ if (shape.fillColor && shape.fillColor !== "transparent") ctx.fill();
+ ctx.stroke();
+ break;
+
+ case "diamond":
+ case "flow-decision": {
+ const cx = shape.x + shape.width / 2, cy = shape.y + shape.height / 2;
+ ctx.beginPath();
+ ctx.moveTo(cx, shape.y);
+ ctx.lineTo(shape.x + shape.width, cy);
+ ctx.lineTo(cx, shape.y + shape.height);
+ ctx.lineTo(shape.x, cy);
+ ctx.closePath();
+ if (shape.fillColor && shape.fillColor !== "transparent") ctx.fill();
+ ctx.stroke();
+ break;
+ }
+
+ case "flow-io": {
+ const skew = 20;
+ ctx.beginPath();
+ ctx.moveTo(shape.x + skew, shape.y);
+ ctx.lineTo(shape.x + shape.width, shape.y);
+ ctx.lineTo(shape.x + shape.width - skew, shape.y + shape.height);
+ ctx.lineTo(shape.x, shape.y + shape.height);
+ ctx.closePath();
+ if (shape.fillColor && shape.fillColor !== "transparent") ctx.fill();
+ ctx.stroke();
+ break;
+ }
+
+ case "uml-package":
+ ctx.strokeRect(shape.x, shape.y, shape.width, shape.height);
+ ctx.strokeRect(shape.x, shape.y, shape.width / 3, 20);
+ break;
+
+ case "text":
+ ctx.fillStyle = shape.fillColor || "#000";
+ ctx.font = "14px Vazirmatn, Tahoma";
+ ctx.textAlign = "left";
+ ctx.fillText(shape.text || "", shape.x, shape.y);
+ break;
+ }
+
+ ctx.restore();
+ },
+
+ findShapeAt(x, y) {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return null;
+
+ // جستجو از آخر به اول (آخرین شکل در بالا)
+ for (let i = drawing.shapes.length - 1; i >= 0; i--) {
+ const s = drawing.shapes[i];
+ if (s.type === "pen") continue;
+ if (s.type === "text") {
+ if (x >= s.x - 5 && x <= s.x + 100 && y >= s.y - 20 && y <= s.y + 5) return s;
+ continue;
+ }
+ if (x >= s.x && x <= s.x + s.width && y >= s.y && y <= s.y + s.height) {
+ return s;
+ }
+ }
+ return null;
+ },
+
+ pushUndo() {
+ const drawing = this.getActiveDrawing();
+ if (!drawing) return;
+ this.undoStack.push(JSON.stringify(drawing.shapes));
+ if (this.undoStack.length > 50) this.undoStack.shift();
+ this.redoStack = [];
+ },
+
+ undo() {
+ const drawing = this.getActiveDrawing();
+ if (!drawing || this.undoStack.length === 0) {
+ showToast("⚠️ عملی برای برگشت نیست", "warning");
+ return;
+ }
+ this.redoStack.push(JSON.stringify(drawing.shapes));
+ drawing.shapes = JSON.parse(this.undoStack.pop());
+ drawing.updatedAt = new Date().toISOString();
+ AutosaveManager.scheduleSave();
+ this.redraw();
+ showToast("↩️ برگشت انجام شد", "info", 1500);
+ },
+
+ redo() {
+ const drawing = this.getActiveDrawing();
+ if (!drawing || this.redoStack.length === 0) {
+ showToast("⚠️ عملی برای بازانجام نیست", "warning");
+ return;
+ }
+ this.undoStack.push(JSON.stringify(drawing.shapes));
+ drawing.shapes = JSON.parse(this.redoStack.pop());
+ drawing.updatedAt = new Date().toISOString();
+ AutosaveManager.scheduleSave();
+ this.redraw();
+ showToast("↪️ بازانجام انجام شد", "info", 1500);
+ },
+
+ zoomIn() {
+ this.zoom = Math.min(3, this.zoom + 0.1);
+ this.applyZoom();
+ },
+
+ zoomOut() {
+ this.zoom = Math.max(0.3, this.zoom - 0.1);
+ this.applyZoom();
+ },
+
+ resetZoom() {
+ this.zoom = 1;
+ this.applyZoom();
+ },
+
+ applyZoom() {
+ const canvas = document.getElementById("drawingsCanvas");
+ canvas.style.transform = `scale(${this.zoom})`;
+ canvas.style.transformOrigin = "top left";
+ this.updateZoomLabel();
+ },
+
+ updateZoomLabel() {
+ document.getElementById("drawingsZoomLabel").textContent = `${Math.round(this.zoom * 100)}%`;
+ },
+
+ exportPNG() {
+ const canvas = document.getElementById("drawingsCanvas");
+ const drawing = this.getActiveDrawing();
+ if (!drawing) {
+ showToast("❌ ترسیمی برای خروجی وجود ندارد", "error");
+ return;
+ }
+
+ canvas.toBlob((blob) => {
+ if (!blob) {
+ showToast("❌ خطا در تولید تصویر", "error");
+ return;
+ }
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
+ a.download = `${drawing.title}-${ts}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ showToast("📷 خروجی PNG ذخیره شد", "success");
+ }, "image/png");
+ },
+
+ darkenColor(hex) {
+ if (!hex || hex.length < 7) return hex;
+ const r = Math.max(0, parseInt(hex.slice(1, 3), 16) - 40);
+ const g = Math.max(0, parseInt(hex.slice(3, 5), 16) - 40);
+ const b = Math.max(0, parseInt(hex.slice(5, 7), 16) - 40);
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
+ },
+
+ escapeHtml(str) {
+ if (!str) return "";
+ const div = document.createElement("div");
+ div.textContent = str;
+ return div.innerHTML;
+ },
+ };
/* ==================== ANALYSIS MANAGERS ==================== */
const DependencyAnalysisManager = {
open() {