// FlowSketcher Pro - Main Application Logic class FlowSketcherApp { constructor() { this.canvas = null; this.ctx = null; this.shapes = []; this.selectedShapes = []; this.currentTool = 'select'; this.isDrawing = false; this.isDragging = false; this.isResizing = false; this.startPoint = { x: 0, y: 0 }; this.currentShape = null; this.resizeHandle = null; this.gridSize = 20; this.history = []; this.historyIndex = -1; this.colors = { stroke: '#000000', fill: '#ffffff' }; this.strokeWidth = 2; this.tools = { SELECT: 'select', RECTANGLE: 'rectangle', CIRCLE: 'circle', DIAMOND: 'diamond', ARROW: 'arrow', TEXT: 'text', FREEHAND: 'freehand' }; this.init(); } init() { this.setupEventListeners(); this.setupCanvas(); this.setupKeyboardShortcuts(); this.loadFromLocalStorage(); } setupCanvas() { // Wait for canvas-area to be fully rendered const canvasArea = document.querySelector('canvas-area'); if (canvasArea) { // Use setTimeout to ensure shadow DOM is rendered setTimeout(() => { this.canvas = canvasArea.shadowRoot.querySelector('#main-canvas'); if (this.canvas) { this.ctx = this.canvas.getContext('2d'); this.resizeCanvas(); window.addEventListener('resize', () => this.resizeCanvas()); console.log('Canvas initialized successfully'); } else { console.error('Canvas element not found in shadow DOM'); } }, 100); } else { console.error('Canvas-area component not found'); } } resizeCanvas() { const canvasArea = document.querySelector('canvas-area'); const rect = canvasArea.getBoundingClientRect(); this.canvas.width = rect.width; this.canvas.height = rect.height; this.redrawCanvas(); } setupEventListeners() { document.addEventListener('tool-changed', (e) => { this.currentTool = e.detail.tool; this.canvas.style.cursor = this.getCursorForTool(this.currentTool); }); document.addEventListener('color-changed', (e) => { this.colors[e.detail.type] = e.detail.color; if (this.selectedShapes.length > 0) { this.updateSelectedShapes({ [e.detail.type]: e.detail.color }); } }); document.addEventListener('stroke-width-changed', (e) => { this.strokeWidth = e.detail.width; if (this.selectedShapes.length > 0) { this.updateSelectedShapes({ strokeWidth: e.detail.width }); } }); document.addEventListener('clear-canvas', () => this.clearCanvas()); document.addEventListener('export-png', () => this.exportAsPNG()); document.addEventListener('undo', () => this.undo()); document.addEventListener('redo', () => this.redo()); document.addEventListener('delete-selected', () => this.deleteSelected()); // Canvas events document.addEventListener('canvas-mousedown', (e) => this.handleMouseDown(e.detail)); document.addEventListener('canvas-mousemove', (e) => this.handleMouseMove(e.detail)); document.addEventListener('canvas-mouseup', (e) => this.handleMouseUp(e.detail)); document.addEventListener('canvas-dblclick', (e) => this.handleDoubleClick(e.detail)); // Quick shape insertion document.addEventListener('insert-quick-shape', (e) => { this.insertQuickShape(e.detail); }); } setupKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch(e.key) { case 'z': e.preventDefault(); if (e.shiftKey) { this.redo(); } else { this.undo(); } break; case 'y': e.preventDefault(); this.redo(); break; case 's': e.preventDefault(); this.exportAsPNG(); break; case 'a': e.preventDefault(); this.selectAll(); break; } } else if (e.key === 'Delete') { this.deleteSelected(); } else if (e.key === 'Escape') { this.deselectAll(); } }); } handleMouseDown(event) { if (!this.canvas || !this.ctx) { console.warn('Canvas not ready'); return; } const { offsetX, offsetY } = event; this.startPoint = { x: offsetX, y: offsetY }; if (this.currentTool === this.tools.SELECT) { const clickedShape = this.getShapeAtPoint(offsetX, offsetY); if (clickedShape) { if (!event.shiftKey && !this.selectedShapes.includes(clickedShape)) { this.deselectAll(); } this.selectedShapes.push(clickedShape); this.isDragging = true; this.updatePropertiesPanel(); } else { this.deselectAll(); this.isDrawing = true; } } else { this.isDrawing = true; this.startNewShape(offsetX, offsetY); } this.redrawCanvas(); } handleMouseMove(event) { const { offsetX, offsetY } = event; if (this.isDragging && this.selectedShapes.length > 0) { this.dragSelectedShapes(offsetX - this.startPoint.x, offsetY - this.startPoint.y); this.startPoint = { x: offsetX, y: offsetY }; } else if (this.isDrawing && this.currentShape) { this.updateCurrentShape(offsetX, offsetY); } else if (this.isResizing && this.resizeHandle) { this.resizeSelectedShape(offsetX, offsetY); } this.redrawCanvas(); } handleMouseUp(event) { if (this.isDrawing && this.currentShape) { if (this.currentShape.width > 5 || this.currentShape.height > 5 || (this.currentShape.points && this.currentShape.points.length > 2)) { this.shapes.push(this.currentShape); this.saveToHistory(); } this.currentShape = null; } this.isDrawing = false; this.isDragging = false; this.isResizing = false; this.resizeHandle = null; if (this.currentTool !== this.tools.SELECT) { this.currentTool = this.tools.SELECT; document.dispatchEvent(new CustomEvent('tool-changed', { detail: { tool: this.tools.SELECT } })); } } handleDoubleClick(event) { const { offsetX, offsetY } = event; const clickedShape = this.getShapeAtPoint(offsetX, offsetY); if (clickedShape && clickedShape.type === this.tools.TEXT) { this.editTextShape(clickedShape, event); } } startNewShape(x, y) { const shape = { id: 'shape_' + Date.now(), type: this.currentTool, x: x, y: y, width: 0, height: 0, strokeColor: this.colors.stroke, fillColor: this.colors.fill, strokeWidth: this.strokeWidth, roughness: 1.5, text: '', points: this.currentTool === this.tools.FREEHAND ? [{x, y}] : null }; this.currentShape = shape; } updateCurrentShape(x, y) { if (!this.currentShape) return; if (this.currentShape.type === this.tools.FREEHAND) { this.currentShape.points.push({ x, y }); } else if (this.currentShape.type === this.tools.ARROW) { this.currentShape.x2 = x; this.currentShape.y2 = y; } else { this.currentShape.width = x - this.startPoint.x; this.currentShape.height = y - this.startPoint.y; } } dragSelectedShapes(dx, dy) { this.selectedShapes.forEach(shape => { shape.x += dx; shape.y += dy; if (shape.points) { shape.points.forEach(p => { p.x += dx; p.y += dy; }); } }); } resizeSelectedShape(x, y) { if (this.selectedShapes.length !== 1) return; const shape = this.selectedShapes[0]; const handle = this.resizeHandle; if (handle.includes('e')) { shape.width = x - shape.x; } if (handle.includes('s')) { shape.height = y - shape.y; } if (handle.includes('w')) { const newWidth = shape.x + shape.width - x; if (newWidth > 20) { shape.width = newWidth; shape.x = x; } } if (handle.includes('n')) { const newHeight = shape.y + shape.height - y; if (newHeight > 20) { shape.height = newHeight; shape.y = y; } } } redrawCanvas() { if (!this.ctx) return; const { width, height } = this.canvas; this.ctx.clearRect(0, 0, width, height); // Draw shapes this.shapes.concat(this.currentShape ? [this.currentShape] : []).forEach(shape => { this.drawShape(shape); }); // Draw selection this.drawSelection(); } drawShape(shape) { if (!shape) return; this.ctx.save(); this.ctx.strokeStyle = shape.strokeColor; this.ctx.fillStyle = shape.fillColor; this.ctx.lineWidth = shape.strokeWidth; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; switch(shape.type) { case this.tools.RECTANGLE: this.drawRoughRectangle(shape); break; case this.tools.CIRCLE: this.drawRoughCircle(shape); break; case this.tools.DIAMOND: this.drawRoughDiamond(shape); break; case this.tools.ARROW: this.drawArrow(shape); break; case this.tools.TEXT: this.drawText(shape); break; case this.tools.FREEHAND: this.drawFreehand(shape); break; } this.ctx.restore(); } drawRoughRectangle(shape) { const { x, y, width, height } = shape; if (width === 0 || height === 0) return; this.ctx.beginPath(); this.ctx.moveTo(this.roughX(x), this.roughY(y)); this.ctx.lineTo(this.roughX(x + width), this.roughY(y)); this.ctx.lineTo(this.roughX(x + width), this.roughY(y + height)); this.ctx.lineTo(this.roughX(x), this.roughY(y + height)); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); } drawRoughCircle(shape) { const { x, y, width, height } = shape; const centerX = x + width / 2; const centerY = y + height / 2; const radius = Math.min(Math.abs(width), Math.abs(height)) / 2; this.ctx.beginPath(); this.ctx.arc(this.roughX(centerX), this.roughY(centerY), radius, 0, 2 * Math.PI); this.ctx.fill(); this.ctx.stroke(); } drawRoughDiamond(shape) { const { x, y, width, height } = shape; const centerX = x + width / 2; const centerY = y + height / 2; this.ctx.beginPath(); this.ctx.moveTo(this.roughX(centerX), this.roughY(y)); this.ctx.lineTo(this.roughX(x + width), this.roughY(centerY)); this.ctx.lineTo(this.roughX(centerX), this.roughY(y + height)); this.ctx.lineTo(this.roughX(x), this.roughY(centerY)); this.ctx.closePath(); this.ctx.fill(); this.ctx.stroke(); } drawArrow(shape) { if (!shape.x2 || !shape.y2) return; const { x, y, x2, y2 } = shape; const angle = Math.atan2(y2 - y, x2 - x); const arrowLength = 15; this.ctx.beginPath(); this.ctx.moveTo(this.roughX(x), this.roughY(y)); this.ctx.lineTo(this.roughX(x2), this.roughY(y2)); this.ctx.stroke(); // Arrow head this.ctx.beginPath(); this.ctx.moveTo(this.roughX(x2), this.roughY(y2)); this.ctx.lineTo( this.roughX(x2 - arrowLength * Math.cos(angle - Math.PI / 6)), this.roughY(y2 - arrowLength * Math.sin(angle - Math.PI / 6)) ); this.ctx.moveTo(this.roughX(x2), this.roughY(y2)); this.ctx.lineTo( this.roughX(x2 - arrowLength * Math.cos(angle + Math.PI / 6)), this.roughY(y2 - arrowLength * Math.sin(angle + Math.PI / 6)) ); this.ctx.stroke(); } drawText(shape) { const { x, y, width, height, text } = shape; this.ctx.font = `${Math.abs(height) || 20}px Inter, sans-serif`; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'middle'; this.ctx.fillStyle = shape.strokeColor; this.ctx.fillText(text || '双击编辑', x + width / 2, y + (height || 20) / 2); } drawFreehand(shape) { if (!shape.points || shape.points.length < 2) return; this.ctx.beginPath(); this.ctx.moveTo(this.roughX(shape.points[0].x), this.roughY(shape.points[0].y)); for (let i = 1; i < shape.points.length; i++) { const point = shape.points[i]; this.ctx.lineTo(this.roughX(point.x), this.roughY(point.y)); } this.ctx.stroke(); } drawSelection() { this.selectedShapes.forEach(shape => { this.drawResizeHandles(shape); }); } drawResizeHandles(shape) { const handles = this.getResizeHandles(shape); handles.forEach(handle => { this.ctx.fillStyle = '#6366f1'; this.ctx.strokeStyle = 'white'; this.ctx.lineWidth = 2; this.ctx.beginPath(); this.ctx.arc(handle.x, handle.y, 5, 0, 2 * Math.PI); this.ctx.fill(); this.ctx.stroke(); }); } getResizeHandles(shape) { const handles = []; const { x, y, width, height } = shape; if (shape.type !== this.tools.FREEHAND) { handles.push({ x: x, y: y, position: 'nw' }); handles.push({ x: x + width, y: y, position: 'ne' }); handles.push({ x: x, y: y + height, position: 'sw' }); handles.push({ x: x + width, y: y + height, position: 'se' }); } return handles; } roughX(x) { return x + (Math.random() - 0.5) * this.getRoughness(); } roughY(y) { return y + (Math.random() - 0.5) * this.getRoughness(); } getRoughness() { return 1.5; } getShapeAtPoint(x, y) { for (let i = this.shapes.length - 1; i >= 0; i--) { const shape = this.shapes[i]; if (this.isPointInShape(x, y, shape)) { return shape; } } return null; } isPointInShape(x, y, shape) { if (shape.type === this.tools.FREEHAND) { return this.isPointNearFreehand(x, y, shape); } return x >= shape.x && x <= shape.x + shape.width && y >= shape.y && y <= shape.y + shape.height; } isPointNearFreehand(x, y, shape) { if (!shape.points) return false; for (const point of shape.points) { const distance = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2); if (distance < 10) return true; } return false; } updateSelectedShapes(updates) { this.selectedShapes.forEach(shape => { Object.assign(shape, updates); }); this.redrawCanvas(); } deselectAll() { this.selectedShapes = []; this.updatePropertiesPanel(); this.redrawCanvas(); } selectAll() { this.selectedShapes = [...this.shapes]; this.redrawCanvas(); } insertQuickShape(config) { const canvasRect = this.canvas.getBoundingClientRect(); const shape = { id: 'shape_' + Date.now(), type: config.type, x: canvasRect.width / 2 - config.width / 2, y: canvasRect.height / 2 - config.height / 2, width: config.width, height: config.height, strokeColor: this.colors.stroke, fillColor: this.colors.fill, strokeWidth: this.strokeWidth, roughness: 1.5, text: '', points: null }; this.shapes.push(shape); this.saveToHistory(); this.redrawCanvas(); } deleteSelected() { this.shapes = this.shapes.filter(shape => !this.selectedShapes.includes(shape)); this.selectedShapes = []; this.saveToHistory(); this.redrawCanvas(); } editTextShape(shape, event) { const input = document.createElement('textarea'); input.className = 'shape-text-input'; input.value = shape.text; input.style.left = `${shape.x}px`; input.style.top = `${shape.y}px`; input.style.width = `${Math.max(shape.width, 100)}px`; input.style.height = `${Math.max(shape.height, 30)}px`; input.style.color = shape.strokeColor; const canvasArea = document.querySelector('canvas-area'); canvasArea.shadowRoot.appendChild(input); input.focus(); input.select(); const finishEdit = () => { shape.text = input.value; canvasArea.shadowRoot.removeChild(input); this.saveToHistory(); this.redrawCanvas(); }; input.addEventListener('blur', finishEdit); input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); finishEdit(); } }); } updatePropertiesPanel() { const panel = document.querySelector('properties-panel'); if (panel) { panel.updateSelection(this.selectedShapes); } } saveToHistory() { this.history = this.history.slice(0, this.historyIndex + 1); this.history.push(JSON.stringify(this.shapes)); this.historyIndex++; if (this.history.length > 50) { this.history.shift(); this.historyIndex--; } this.saveToLocalStorage(); } undo() { if (this.historyIndex > 0) { this.historyIndex--; this.shapes = JSON.parse(this.history[this.historyIndex]); this.selectedShapes = []; this.redrawCanvas(); } } redo() { if (this.historyIndex < this.history.length - 1) { this.historyIndex++; this.shapes = JSON.parse(this.history[this.historyIndex]); this.selectedShapes = []; this.redrawCanvas(); } } clearCanvas() { if (confirm('Are you sure you want to clear the canvas? This action cannot be undone.')) { this.shapes = []; this.selectedShapes = []; this.history = []; this.historyIndex = -1; this.saveToHistory(); this.redrawCanvas(); } } exportAsPNG() { const tempCanvas = document.createElement('canvas'); tempCanvas.width = this.canvas.width; tempCanvas.height = this.canvas.height; const tempCtx = tempCanvas.getContext('2d'); // White background tempCtx.fillStyle = 'white'; tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height); // Draw all shapes this.shapes.forEach(shape => { this.drawShapeOnContext(shape, tempCtx); }); // Download const link = document.createElement('a'); link.download = `flowchart-${Date.now()}.png`; link.href = tempCanvas.toDataURL(); link.click(); // Show success notification document.dispatchEvent(new CustomEvent('show-notification', { detail: { message: 'Flowchart exported successfully!' } })); } drawShapeOnContext(shape, ctx) { const originalCtx = this.ctx; this.ctx = ctx; this.drawShape(shape); this.ctx = originalCtx; } saveToLocalStorage() { try { localStorage.setItem('flowsketcher-autosave', JSON.stringify({ shapes: this.shapes, timestamp: Date.now() })); } catch (e) { console.warn('Failed to save to localStorage:', e); } } loadFromLocalStorage() { try { const saved = localStorage.getItem('flowsketcher-autosave'); if (saved) { const data = JSON.parse(saved); this.shapes = data.shapes || []; this.saveToHistory(); this.redrawCanvas(); } } catch (e) { console.warn('Failed to load from localStorage:', e); } } getCursorForTool(tool) { const cursors = { [this.tools.SELECT]: 'default', [this.tools.RECTANGLE]: 'crosshair', [this.tools.CIRCLE]: 'crosshair', [this.tools.DIAMOND]: 'crosshair', [this.tools.ARROW]: 'crosshair', [this.tools.TEXT]: 'text', [this.tools.FREEHAND]: 'crosshair' }; return cursors[tool] || 'default'; } } // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.FlowSketcher = new FlowSketcherApp(); });