Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CivilCAD - Engineering Drawing Application</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-primary: #1e1e1e; | |
| --bg-secondary: #252526; | |
| --bg-tertiary: #2d2d30; | |
| --bg-hover: #3e3e42; | |
| --text-primary: #cccccc; | |
| --text-secondary: #969696; | |
| --accent: #007acc; | |
| --accent-hover: #1e90ff; | |
| --border: #3e3e42; | |
| --success: #4ec9b0; | |
| --warning: #ce9178; | |
| --error: #f44747; | |
| --grid-color: #2a2a2a; | |
| --snap-color: #4a9eff; | |
| } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| user-select: none; | |
| } | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| flex-direction: column; | |
| } | |
| /* Header */ | |
| .header { | |
| background: var(--bg-secondary); | |
| border-bottom: 1px solid var(--border); | |
| padding: 8px 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| min-height: 48px; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| } | |
| .logo { | |
| font-size: 20px; | |
| font-weight: 600; | |
| color: var(--accent); | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .logo::before { | |
| content: "⚙"; | |
| font-size: 24px; | |
| } | |
| .header-nav { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .nav-btn { | |
| padding: 6px 12px; | |
| background: transparent; | |
| color: var(--text-primary); | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| font-size: 13px; | |
| } | |
| .nav-btn:hover { | |
| background: var(--bg-hover); | |
| } | |
| /* Main Layout */ | |
| .main-content { | |
| display: flex; | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| /* Toolbar */ | |
| .toolbar { | |
| background: var(--bg-secondary); | |
| width: 60px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding: 8px 0; | |
| gap: 4px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .tool-btn { | |
| width: 44px; | |
| height: 44px; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 20px; | |
| transition: all 0.2s; | |
| position: relative; | |
| } | |
| .tool-btn:hover { | |
| background: var(--bg-hover); | |
| color: var(--text-primary); | |
| } | |
| .tool-btn.active { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .tool-tooltip { | |
| position: absolute; | |
| left: 54px; | |
| background: var(--bg-tertiary); | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| z-index: 1000; | |
| border: 1px solid var(--border); | |
| } | |
| .tool-btn:hover .tool-tooltip { | |
| opacity: 1; | |
| } | |
| .tool-separator { | |
| width: 30px; | |
| height: 1px; | |
| background: var(--border); | |
| margin: 4px 0; | |
| } | |
| /* Canvas Area */ | |
| .canvas-container { | |
| flex: 1; | |
| background: var(--bg-primary); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| #drawingCanvas { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| cursor: crosshair; | |
| } | |
| .canvas-grid { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| } | |
| /* Properties Panel */ | |
| .properties-panel { | |
| background: var(--bg-secondary); | |
| width: 280px; | |
| border-left: 1px solid var(--border); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .panel-header { | |
| padding: 12px 16px; | |
| background: var(--bg-tertiary); | |
| border-bottom: 1px solid var(--border); | |
| font-size: 13px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .panel-content { | |
| flex: 1; | |
| padding: 16px; | |
| overflow-y: auto; | |
| } | |
| .property-group { | |
| margin-bottom: 20px; | |
| } | |
| .property-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| margin-bottom: 6px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .property-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| } | |
| .property-input { | |
| flex: 1; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| } | |
| .property-input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .property-select { | |
| flex: 1; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| color: var(--text-primary); | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| cursor: pointer; | |
| } | |
| /* Layers Panel */ | |
| .layers-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .layer-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px; | |
| background: var(--bg-tertiary); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .layer-item:hover { | |
| background: var(--bg-hover); | |
| } | |
| .layer-item.active { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .layer-visibility { | |
| width: 16px; | |
| height: 16px; | |
| cursor: pointer; | |
| } | |
| .layer-name { | |
| flex: 1; | |
| font-size: 13px; | |
| } | |
| .layer-color { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 2px; | |
| border: 1px solid var(--border); | |
| } | |
| /* Status Bar */ | |
| .status-bar { | |
| background: var(--bg-tertiary); | |
| border-top: 1px solid var(--border); | |
| padding: 4px 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| min-height: 28px; | |
| } | |
| .status-left { | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .status-right { | |
| display: flex; | |
| gap: 20px; | |
| } | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .status-label { | |
| color: var(--text-secondary); | |
| } | |
| .status-value { | |
| color: var(--text-primary); | |
| font-weight: 500; | |
| } | |
| /* Context Menu */ | |
| .context-menu { | |
| position: absolute; | |
| background: var(--bg-tertiary); | |
| border: 1px solid var(--border); | |
| border-radius: 4px; | |
| padding: 4px 0; | |
| display: none; | |
| z-index: 1000; | |
| min-width: 150px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| } | |
| .context-menu-item { | |
| padding: 6px 12px; | |
| font-size: 13px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| .context-menu-item:hover { | |
| background: var(--bg-hover); | |
| } | |
| /* Color Picker */ | |
| .color-picker { | |
| display: flex; | |
| gap: 4px; | |
| flex-wrap: wrap; | |
| } | |
| .color-option { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| border: 2px solid transparent; | |
| transition: all 0.2s; | |
| } | |
| .color-option:hover { | |
| transform: scale(1.1); | |
| } | |
| .color-option.selected { | |
| border-color: white; | |
| box-shadow: 0 0 8px rgba(255,255,255,0.3); | |
| } | |
| /* Snap Indicator */ | |
| .snap-indicator { | |
| position: absolute; | |
| width: 8px; | |
| height: 8px; | |
| background: var(--snap-color); | |
| border-radius: 50%; | |
| pointer-events: none; | |
| display: none; | |
| z-index: 100; | |
| transform: translate(-50%, -50%); | |
| } | |
| /* Dimension Text */ | |
| .dimension-text { | |
| position: absolute; | |
| background: var(--bg-primary); | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| font-size: 11px; | |
| color: var(--text-primary); | |
| pointer-events: none; | |
| white-space: nowrap; | |
| border: 1px solid var(--border); | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 6px 12px; | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| transition: background 0.2s; | |
| } | |
| .btn:hover { | |
| background: var(--accent-hover); | |
| } | |
| .btn-secondary { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| .btn-secondary:hover { | |
| background: var(--bg-hover); | |
| } | |
| .btn-small { | |
| padding: 4px 8px; | |
| font-size: 12px; | |
| } | |
| /* Zoom Controls */ | |
| .zoom-controls { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 300px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 4px; | |
| display: flex; | |
| gap: 4px; | |
| align-items: center; | |
| } | |
| .zoom-btn { | |
| width: 28px; | |
| height: 28px; | |
| background: transparent; | |
| color: var(--text-primary); | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: background 0.2s; | |
| } | |
| .zoom-btn:hover { | |
| background: var(--bg-hover); | |
| } | |
| .zoom-level { | |
| padding: 0 8px; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| min-width: 50px; | |
| text-align: center; | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(30, 30, 30, 0.9); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 2000; | |
| } | |
| .loading-spinner { | |
| width: 48px; | |
| height: 48px; | |
| border: 4px solid var(--bg-tertiary); | |
| border-top-color: var(--accent); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .properties-panel { | |
| position: absolute; | |
| right: -280px; | |
| transition: right 0.3s; | |
| z-index: 100; | |
| } | |
| .properties-panel.open { | |
| right: 0; | |
| } | |
| .zoom-controls { | |
| right: 20px; | |
| } | |
| } | |
| /* Link styling */ | |
| .credit-link { | |
| color: var(--accent); | |
| text-decoration: none; | |
| font-size: 12px; | |
| } | |
| .credit-link:hover { | |
| text-decoration: underline; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="logo">CivilCAD</div> | |
| <nav class="header-nav"> | |
| <button class="nav-btn" onclick="newDrawing()">New</button> | |
| <button class="nav-btn" onclick="saveDrawing()">Save</button> | |
| <button class="nav-btn" onclick="loadDrawing()">Load</button> | |
| <button class="nav-btn" onclick="exportImage()">Export</button> | |
| </nav> | |
| </div> | |
| <div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="credit-link" target="_blank">Built with anycoder</a> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Toolbar --> | |
| <aside class="toolbar"> | |
| <button class="tool-btn active" data-tool="pointer" onclick="selectTool('pointer')"> | |
| ↖ | |
| <span class="tool-tooltip">Select (S)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="line" onclick="selectTool('line')"> | |
| ➖ | |
| <span class="tool-tooltip">Line (L)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="rectangle" onclick="selectTool('rectangle')"> | |
| ▢ | |
| <span class="tool-tooltip">Rectangle (R)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="circle" onclick="selectTool('circle')"> | |
| ○ | |
| <span class="tool-tooltip">Circle (C)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="arc" onclick="selectTool('arc')"> | |
| ⌒ | |
| <span class="tool-tooltip">Arc (A)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="polyline" onclick="selectTool('polyline')"> | |
| ⋰ | |
| <span class="tool-tooltip">Polyline (P)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="dimension" onclick="selectTool('dimension')"> | |
| 📏 | |
| <span class="tool-tooltip">Dimension (D)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="text" onclick="selectTool('text')"> | |
| T | |
| <span class="tool-tooltip">Text (T)</span> | |
| </button> | |
| <div class="tool-separator"></div> | |
| <button class="tool-btn" data-tool="pan" onclick="selectTool('pan')"> | |
| ✋ | |
| <span class="tool-tooltip">Pan (H)</span> | |
| </button> | |
| <button class="tool-btn" data-tool="zoom" onclick="selectTool('zoom')"> | |
| 🔍 | |
| <span class="tool-tooltip">Zoom Window (Z)</span> | |
| </button> | |
| <div class="tool-separator"></div> | |
| <button class="tool-btn" onclick="undo()"> | |
| ↶ | |
| <span class="tool-tooltip">Undo (Ctrl+Z)</span> | |
| </button> | |
| <button class="tool-btn" onclick="redo()"> | |
| ↷ | |
| <span class="tool-tooltip">Redo (Ctrl+Y)</span> | |
| </button> | |
| <button class="tool-btn" onclick="deleteSelected()"> | |
| 🗑 | |
| <span class="tool-tooltip">Delete (Del)</span> | |
| </button> | |
| </aside> | |
| <!-- Canvas Container --> | |
| <main class="canvas-container" id="canvasContainer"> | |
| <canvas id="drawingCanvas"></canvas> | |
| <canvas class="canvas-grid" id="gridCanvas"></canvas> | |
| <div class="snap-indicator" id="snapIndicator"></div> | |
| <!-- Zoom Controls --> | |
| <div class="zoom-controls"> | |
| <button class="zoom-btn" onclick="zoomOut()">−</button> | |
| <span class="zoom-level" id="zoomLevel">100%</span> | |
| <button class="zoom-btn" onclick="zoomIn()">+</button> | |
| <button class="zoom-btn" onclick="fitToScreen()">⊡</button> | |
| </div> | |
| </main> | |
| <!-- Properties Panel --> | |
| <aside class="properties-panel"> | |
| <div class="panel-header"> | |
| Properties | |
| <button class="btn btn-small btn-secondary" onclick="togglePanel()">Hide</button> | |
| </div> | |
| <div class="panel-content"> | |
| <!-- Drawing Properties --> | |
| <div class="property-group"> | |
| <div class="property-label">Drawing</div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Units:</label> | |
| <select class="property-select" id="units" onchange="updateUnits()"> | |
| <option value="mm">Millimeters</option> | |
| <option value="cm">Centimeters</option> | |
| <option value="m" selected>Meters</option> | |
| <option value="ft">Feet</option> | |
| <option value="in">Inches</option> | |
| </select> | |
| </div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Grid:</label> | |
| <input type="checkbox" id="gridToggle" checked onchange="toggleGrid()"> | |
| <label style="font-size: 12px;">Snap:</label> | |
| <input type="checkbox" id="snapToggle" checked onchange="toggleSnap()"> | |
| </div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Grid Size:</label> | |
| <input type="number" class="property-input" id="gridSize" value="10" min="1" max="100" onchange="updateGridSize()"> | |
| </div> | |
| </div> | |
| <!-- Line Properties --> | |
| <div class="property-group"> | |
| <div class="property-label">Line</div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Color:</label> | |
| <div class="color-picker" id="lineColorPicker"> | |
| <div class="color-option selected" style="background: #ffffff" data-color="#ffffff"></div> | |
| <div class="color-option" style="background: #ff0000" data-color="#ff0000"></div> | |
| <div class="color-option" style="background: #00ff00" data-color="#00ff00"></div> | |
| <div class="color-option" style="background: #0000ff" data-color="#0000ff"></div> | |
| <div class="color-option" style="background: #ffff00" data-color="#ffff00"></div> | |
| <div class="color-option" style="background: #ff00ff" data-color="#ff00ff"></div> | |
| <div class="color-option" style="background: #00ffff" data-color="#00ffff"></div> | |
| <div class="color-option" style="background: #ff8800" data-color="#ff8800"></div> | |
| </div> | |
| </div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Width:</label> | |
| <input type="number" class="property-input" id="lineWidth" value="1" min="0.1" max="10" step="0.1"> | |
| </div> | |
| <div class="property-row"> | |
| <label style="font-size: 12px;">Style:</label> | |
| <select class="property-select" id="lineStyle"> | |
| <option value="solid">Solid</option> | |
| <option value="dashed">Dashed</option> | |
| <option value="dotted">Dotted</option> | |
| <option value="center">Center Line</option> | |
| </select> | |
| </div> | |
| </div> | |
| <!-- Layers --> | |
| <div class="property-group"> | |
| <div class="property-label">Layers</div> | |
| <div class="layers-list" id="layersList"> | |
| <div class="layer-item active" data-layer="0"> | |
| <input type="checkbox" class="layer-visibility" checked> | |
| <span class="layer-name">Layer 1</span> | |
| <div class="layer-color" style="background: #ffffff"></div> | |
| </div> | |
| </div> | |
| <div style="margin-top: 8px; display: flex; gap: 4px;"> | |
| <button class="btn btn-small" onclick="addLayer()">Add</button> | |
| <button class="btn btn-small btn-secondary" onclick="deleteLayer()">Delete</button> | |
| </div> | |
| </div> | |
| <!-- Object Info --> | |
| <div class="property-group"> | |
| <div class="property-label">Selected Object</div> | |
| <div id="objectInfo"> | |
| <div style="font-size: 12px; color: var(--text-secondary);">No object selected</div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Status Bar --> | |
| <footer class="status-bar"> | |
| <div class="status-left"> | |
| <div class="status-item"> | |
| <span class="status-label">X:</span> | |
| <span class="status-value" id="mouseX">0.00</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">Y:</span> | |
| <span class="status-value" id="mouseY">0.00</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">Tool:</span> | |
| <span class="status-value" id="currentTool">Select</span> | |
| </div> | |
| </div> | |
| <div class="status-right"> | |
| <div class="status-item"> | |
| <span class="status-label">Objects:</span> | |
| <span class="status-value" id="objectCount">0</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">Layer:</span> | |
| <span class="status-value" id="currentLayer">Layer 1</span> | |
| </div> | |
| <div class="status-item"> | |
| <span class="status-label">Snap:</span> | |
| <span class="status-value" id="snapStatus">ON</span> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <!-- Context Menu --> | |
| <div class="context-menu" id="contextMenu"> | |
| <div class="context-menu-item" onclick="copyObject()">Copy</div> | |
| <div class="context-menu-item" onclick="pasteObject()">Paste</div> | |
| <div class="context-menu-item" onclick="deleteSelected()">Delete</div> | |
| <div class="context-menu-item" onclick="bringToFront()">Bring to Front</div> | |
| <div class="context-menu-item" onclick="sendToBack()">Send to Back</div> | |
| </div> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-spinner"></div> | |
| </div> | |
| <script> | |
| // Global Variables | |
| const canvas = document.getElementById('drawingCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const gridCanvas = document.getElementById('gridCanvas'); | |
| const gridCtx = gridCanvas.getContext('2d'); | |
| let currentTool = 'pointer'; | |
| let isDrawing = false; | |
| let startX, startY; | |
| let objects = []; | |
| let selectedObject = null; | |
| let currentLayer = 0; | |
| let layers = [{ name: 'Layer 1', visible: true, color: '#ffffff' }]; | |
| let zoom = 1; | |
| let panX = 0, panY = 0; | |
| let isPanning = false; | |
| let lastPanX, lastPanY; | |
| let gridSize = 10; | |
| let snapEnabled = true; | |
| let gridEnabled = true; | |
| let units = 'm'; | |
| let history = []; | |
| let historyStep = -1; | |
| let clipboard = null; | |
| let tempPoints = []; | |
| let lineColor = '#ffffff'; | |
| let lineWidth = 1; | |
| let lineStyle = 'solid'; | |
| // Initialize | |
| function init() { | |
| resizeCanvas(); | |
| drawGrid(); | |
| setupEventListeners(); | |
| setupColorPicker(); | |
| window.addEventListener('resize', resizeCanvas); | |
| } | |
| // Resize Canvas | |
| function resizeCanvas() { | |
| const container = document.getElementById('canvasContainer'); | |
| canvas.width = container.clientWidth; | |
| canvas.height = container.clientHeight; | |
| gridCanvas.width = container.clientWidth; | |
| gridCanvas.height = container.clientHeight; | |
| redraw(); | |
| drawGrid(); | |
| } | |
| // Setup Event Listeners | |
| function setupEventListeners() { | |
| canvas.addEventListener('mousedown', handleMouseDown); | |
| canvas.addEventListener('mousemove', handleMouseMove); | |
| canvas.addEventListener('mouseup', handleMouseUp); | |
| canvas.addEventListener('wheel', handleWheel); | |
| canvas.addEventListener('contextmenu', handleContextMenu); | |
| document.addEventListener('keydown', handleKeyDown); | |
| document.addEventListener('keyup', handleKeyUp); | |
| } | |
| // Mouse Events | |
| function handleMouseDown(e) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = (e.clientX - rect.left - panX) / zoom; | |
| const y = (e.clientY - rect.top - panY) / zoom; | |
| if (snapEnabled) { | |
| const snapped = snapToGrid(x, y); | |
| startX = snapped.x; | |
| startY = snapped.y; | |
| } else { | |
| startX = x; | |
| startY = y; | |
| } | |
| if (currentTool === 'pointer') { | |
| selectObjectAt(x, y); | |
| } else if (currentTool === 'pan') { | |
| isPanning = true; | |
| lastPanX = e.clientX; | |
| lastPanY = e.clientY; | |
| canvas.style.cursor = 'grabbing'; | |
| } else if (currentTool === 'polyline') { | |
| tempPoints.push({ x: startX, y: startY }); | |
| if (tempPoints.length > 1) { | |
| drawPolylinePreview(); | |
| } | |
| } else if (currentTool === 'text') { | |
| const text = prompt('Enter text:'); | |
| if (text) { | |
| addObject({ | |
| type: 'text', | |
| x: startX, | |
| y: startY, | |
| text: text, | |
| color: lineColor, | |
| layer: currentLayer | |
| }); | |
| } | |
| } else { | |
| isDrawing = true; | |
| } | |
| } | |
| function handleMouseMove(e) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = (e.clientX - rect.left - panX) / zoom; | |
| const y = (e.clientY - rect.top - panY) / zoom; | |
| updateMousePosition(x, y); | |
| if (snapEnabled && currentTool !== 'pan') { | |
| const snapped = snapToGrid(x, y); | |
| showSnapIndicator(snapped.x, snapped.y); | |
| } else { | |
| hideSnapIndicator(); | |
| } | |
| if (isPanning) { | |
| panX += e.clientX - lastPanX; | |
| panY += e.clientY - lastPanY; | |
| lastPanX = e.clientX; | |
| lastPanY = e.clientY; | |
| redraw(); | |
| drawGrid(); | |
| } else if (isDrawing) { | |
| redraw(); | |
| drawPreview(x, y); | |
| } else if (currentTool === 'polyline' && tempPoints.length > 0) { | |
| redraw(); | |
| drawPolylinePreview(x, y); | |
| } | |
| } | |
| function handleMouseUp(e) { | |
| if (isPanning) { | |
| isPanning = false; | |
| canvas.style.cursor = 'grab'; | |
| return; | |
| } | |
| if (!isDrawing) return; | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = (e.clientX - rect.left - panX) / zoom; | |
| const y = (e.clientY - rect.top - panY) / zoom; | |
| let endX = x, endY = y; | |
| if (snapEnabled) { | |
| const snapped = snapToGrid(x, y); | |
| endX = snapped.x; | |
| endY = snapped.y; | |
| } | |
| if (currentTool === 'line') { | |
| addObject({ | |
| type: 'line', | |
| x1: startX, | |
| y1: startY, | |
| x2: endX, | |
| y2: endY, | |
| color: lineColor, | |
| width: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } else if (currentTool === 'rectangle') { | |
| addObject({ | |
| type: 'rectangle', | |
| x: Math.min(startX, endX), | |
| y: Math.min(startY, endY), | |
| width: Math.abs(endX - startX), | |
| height: Math.abs(endY - startY), | |
| color: lineColor, | |
| lineWidth: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } else if (currentTool === 'circle') { | |
| const radius = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); | |
| addObject({ | |
| type: 'circle', | |
| x: startX, | |
| y: startY, | |
| radius: radius, | |
| color: lineColor, | |
| lineWidth: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } else if (currentTool === 'arc') { | |
| const radius = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); | |
| const angle = Math.atan2(endY - startY, endX - startX); | |
| addObject({ | |
| type: 'arc', | |
| x: startX, | |
| y: startY, | |
| radius: radius, | |
| startAngle: 0, | |
| endAngle: angle, | |
| color: lineColor, | |
| lineWidth: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } else if (currentTool === 'dimension') { | |
| const distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); | |
| const midX = (startX + endX) / 2; | |
| const midY = (startY + endY) / 2; | |
| addObject({ | |
| type: 'dimension', | |
| x1: startX, | |
| y1: startY, | |
| x2: endX, | |
| y2: endY, | |
| text: formatDistance(distance), | |
| color: lineColor, | |
| layer: currentLayer | |
| }); | |
| } else if (currentTool === 'zoom') { | |
| const zoomRect = { | |
| x: Math.min(startX, endX), | |
| y: Math.min(startY, endY), | |
| width: Math.abs(endX - startX), | |
| height: Math.abs(endY - startY) | |
| }; | |
| zoomToRect(zoomRect); | |
| } | |
| isDrawing = false; | |
| redraw(); | |
| } | |
| function handleWheel(e) { | |
| e.preventDefault(); | |
| const delta = e.deltaY > 0 ? 0.9 : 1.1; | |
| const newZoom = Math.max(0.1, Math.min(10, zoom * delta)); | |
| const rect = canvas.getBoundingClientRect(); | |
| const mouseX = e.clientX - rect.left; | |
| const mouseY = e.clientY - rect.top; | |
| panX = mouseX - (mouseX - panX) * (newZoom / zoom); | |
| panY = mouseY - (mouseY - panY) * (newZoom / zoom); | |
| zoom = newZoom; | |
| updateZoomDisplay(); | |
| redraw(); | |
| drawGrid(); | |
| } | |
| function handleContextMenu(e) { | |
| e.preventDefault(); | |
| const contextMenu = document.getElementById('contextMenu'); | |
| contextMenu.style.display = 'block'; | |
| contextMenu.style.left = e.clientX + 'px'; | |
| contextMenu.style.top = e.clientY + 'px'; | |
| } | |
| function handleKeyDown(e) { | |
| // Keyboard shortcuts | |
| if (e.ctrlKey || e.metaKey) { | |
| switch(e.key.toLowerCase()) { | |
| case 'z': | |
| e.preventDefault(); | |
| undo(); | |
| break; | |
| case 'y': | |
| e.preventDefault(); | |
| redo(); | |
| break; | |
| case 's': | |
| e.preventDefault(); | |
| saveDrawing(); | |
| break; | |
| case 'o': | |
| e.preventDefault(); | |
| loadDrawing(); | |
| break; | |
| case 'c': | |
| e.preventDefault(); | |
| copyObject(); | |
| break; | |
| case 'v': | |
| e.preventDefault(); | |
| pasteObject(); | |
| break; | |
| } | |
| } else { | |
| switch(e.key.toLowerCase()) { | |
| case 'delete': | |
| case 'backspace': | |
| deleteSelected(); | |
| break; | |
| case 's': | |
| selectTool('pointer'); | |
| break; | |
| case 'l': | |
| selectTool('line'); | |
| break; | |
| case 'r': | |
| selectTool('rectangle'); | |
| break; | |
| case 'c': | |
| selectTool('circle'); | |
| break; | |
| case 'a': | |
| selectTool('arc'); | |
| break; | |
| case 'p': | |
| selectTool('polyline'); | |
| break; | |
| case 'd': | |
| selectTool('dimension'); | |
| break; | |
| case 't': | |
| selectTool('text'); | |
| break; | |
| case 'h': | |
| selectTool('pan'); | |
| break; | |
| case 'z': | |
| selectTool('zoom'); | |
| break; | |
| case 'escape': | |
| if (currentTool === 'polyline') { | |
| if (tempPoints.length > 1) { | |
| addObject({ | |
| type: 'polyline', | |
| points: [...tempPoints], | |
| color: lineColor, | |
| width: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } | |
| tempPoints = []; | |
| redraw(); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| function handleKeyUp(e) { | |
| document.addEventListener('click', () => { | |
| document.getElementById('contextMenu').style.display = 'none'; | |
| }, { once: true }); | |
| } | |
| // Tool Selection | |
| function selectTool(tool) { | |
| currentTool = tool; | |
| document.querySelectorAll('.tool-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.querySelector(`[data-tool="${tool}"]`).classList.add('active'); | |
| document.getElementById('currentTool').textContent = | |
| tool.charAt(0).toUpperCase() + tool.slice(1); | |
| // Update cursor | |
| if (tool === 'pan') { | |
| canvas.style.cursor = 'grab'; | |
| } else if (tool === 'zoom') { | |
| canvas.style.cursor = 'zoom-in'; | |
| } else if (tool === 'text') { | |
| canvas.style.cursor = 'text'; | |
| } else { | |
| canvas.style.cursor = 'crosshair'; | |
| } | |
| // Clear polyline if switching tools | |
| if (tool !== 'polyline' && tempPoints.length > 0) { | |
| if (tempPoints.length > 1) { | |
| addObject({ | |
| type: 'polyline', | |
| points: [...tempPoints], | |
| color: lineColor, | |
| width: lineWidth, | |
| style: lineStyle, | |
| layer: currentLayer | |
| }); | |
| } | |
| tempPoints = []; | |
| redraw(); | |
| } | |
| } | |
| // Drawing Functions | |
| function drawGrid() { |