/** * 画个标 · UI:画布编辑器交互 * 职责:拖拽移动、缩放手柄、旋转手柄、双击编辑文字、快捷键 */ const HGB_Editor = (function () { 'use strict'; let svg, stage; let drag = null; // 当前拖拽状态 let clipboard = []; // 复制/粘贴剪贴板 const MIN_SIZE = 16; /* ---------- 初始化 ---------- */ function init() { svg = HGB_Renderer.svg; stage = document.getElementById('canvas-stage'); // 画布空白处点击取消选中 svg.addEventListener('mousedown', onCanvasMouseDown); window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); // 双击编辑文字 svg.addEventListener('dblclick', onDblClick); // 快捷键 document.addEventListener('keydown', onKeyDown); // 阻止 SVG 内元素默认拖拽(避免浏览器拖拽行为) svg.addEventListener('dragstart', (e) => e.preventDefault()); } /* ---------- 鼠标事件 ---------- */ function onCanvasMouseDown(e) { const target = e.target; const t = target.tagName; // 1) 手柄缩放/旋转 const handle = target.getAttribute && target.getAttribute('data-handle'); if (target.classList && target.classList.contains('hgb-handle')) { e.preventDefault(); const el = HGB_Store.selected(); if (!el) return; const start = HGB_Renderer.toSvgPoint(e.clientX, e.clientY); // 旋转起始角:以元素中心为原点 const center = { x: el.x + el.w / 2, y: el.y + el.h / 2 }; const startAngle = Math.atan2(start.y - center.y, start.x - center.x) * 180 / Math.PI; drag = { type: 'resize', handle, elId: el.id, startX: start.x, startY: start.y, startAngle: startAngle - (el.rotation || 0), orig: { ...el }, moved: false }; return; } // 2) 元素命中 const el = HGB_Renderer.hitTest(e.clientX, e.clientY); if (el) { e.preventDefault(); // 已选中组内元素 → 保持当前多选状态,整体拖动;未选中 → 选中(Shift 追加) if (!HGB_Store.get().selectedIds.includes(el.id)) { HGB_Store.select(el.id, e.shiftKey); } const start = HGB_Renderer.toSvgPoint(e.clientX, e.clientY); drag = { type: 'move', elId: el.id, startX: start.x, startY: start.y, orig: { ...el }, moved: false }; return; } // 3) 空白处:取消选中 HGB_Store.select(null); } function onMouseMove(e) { if (!drag) return; const p = HGB_Renderer.toSvgPoint(e.clientX, e.clientY); const dx = p.x - drag.startX; const dy = p.y - drag.startY; if (!drag.moved && Math.abs(dx) + Math.abs(dy) > 2) drag.moved = true; const el = HGB_Store.get().elements.find(x => x.id === drag.elId); if (!el) return; if (drag.type === 'move') { // 以主元素原始位置为基准计算位移,整体移动全部选中元素(moveSelection 内部 emit change → refreshAll) const nX = Math.round(drag.orig.x + dx); const nY = Math.round(drag.orig.y + dy); const boundX = Math.max(-drag.orig.w * 0.5, Math.min(HGB_Store.CANVAS_W - drag.orig.w * 0.5, nX)); const boundY = Math.max(-drag.orig.h * 0.5, Math.min(HGB_Store.CANVAS_H - drag.orig.h * 0.5, nY)); HGB_Store.moveSelection(boundX - drag.orig.x, boundY - drag.orig.y); } else if (drag.type === 'resize') { if (drag.handle === 'rotate') { rotateElement(el, drag, p); } else { resizeElement(el, drag, p, e); } } } function onMouseUp() { if (!drag) return; // 拖拽/缩放完成后记录历史 if (drag.moved) { HGB_Store.pushHistory(); } drag = null; HGB_Renderer.render(); HGB_UI.refreshAll(); } /* ---------- 缩放逻辑 ---------- */ function resizeElement(el, drag, p, ev) { const o = drag.orig; let x = o.x, y = o.y, w = o.w, h = o.h; const isE = drag.handle.includes('e'); const isW = drag.handle.includes('w'); const isS = drag.handle.includes('s'); const isN = drag.handle.includes('n'); if (isE) w = Math.max(MIN_SIZE, o.w + (p.x - (o.x + o.w))); if (isS) h = Math.max(MIN_SIZE, o.h + (p.y - (o.y + o.h))); if (isW) { const nw = Math.max(MIN_SIZE, o.w + (o.x - p.x)); x = o.x + o.w - nw; w = nw; } if (isN) { const nh = Math.max(MIN_SIZE, o.h + (o.y - p.y)); y = o.y + o.h - nh; h = nh; } // 等比缩放(按住 Shift) if (ev && ev.shiftKey) { const ratio = o.w / o.h; if (w / h > ratio) h = w / ratio; else w = h * ratio; // 重新计算锚点 if (isW) x = o.x + o.w - w; if (isN) y = o.y + o.h - h; } el.x = Math.round(x); el.y = Math.round(y); el.w = Math.round(w); el.h = Math.round(h); HGB_Renderer.render(); HGB_UI.refreshProps(); } /* ---------- 旋转逻辑 ---------- */ function rotateElement(el, drag, p) { const center = { x: drag.orig.x + drag.orig.w / 2, y: drag.orig.y + drag.orig.h / 2 }; const startAngle = drag.startAngle; const angle = Math.atan2(p.y - center.y, p.x - center.x) * 180 / Math.PI; el.rotation = Math.round((angle - startAngle) * 10) / 10; HGB_Renderer.render(); HGB_UI.refreshProps(); } /* ---------- 双击编辑文字 ---------- */ function onDblClick(e) { const el = HGB_Renderer.hitTest(e.clientX, e.clientY); if (!el || el.type !== 'text' || el.locked) return; HGB_Store.select(el.id); const val = prompt('编辑文字内容:', el.text || ''); if (val !== null) { HGB_Store.updateElement(el.id, { text: val }); } } /* ---------- 快捷键 ---------- */ function onKeyDown(e) { const tag = (document.activeElement && document.activeElement.tagName) || ''; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') { // 输入框内不触发全局快捷键(Ctrl+Z 撤销文字除外) if (e.ctrlKey || e.metaKey) { if (e.key === 'z' || e.key === 'Z') { e.preventDefault(); } } return; } const ctrl = e.ctrlKey || e.metaKey; if (ctrl && (e.key === 'z' || e.key === 'Z')) { e.preventDefault(); e.shiftKey ? HGB_Store.redo() : HGB_Store.undo(); HGB_UI.refreshAll(); return; } if (ctrl && (e.key === 'y' || e.key === 'Y')) { e.preventDefault(); HGB_Store.redo(); HGB_UI.refreshAll(); return; } if (ctrl && (e.key === 'd' || e.key === 'D')) { e.preventDefault(); HGB_Store.duplicateSelection(); HGB_UI.refreshAll(); return; } if (ctrl && (e.key === 'c' || e.key === 'C')) { e.preventDefault(); clipboard = HGB_Store.selectedElements().map(el => JSON.parse(JSON.stringify(el))); HGB_UI.toast(`📋 已复制 ${clipboard.length} 个元素`); return; } if (ctrl && (e.key === 'v' || e.key === 'V')) { e.preventDefault(); if (clipboard.length) { HGB_Store.pasteElements(clipboard); HGB_UI.refreshAll(); } return; } if (e.key === 'Delete' || e.key === 'Backspace') { HGB_Store.removeSelection(); HGB_UI.refreshAll(); return; } if (e.key === 'Escape') { HGB_Store.select(null); HGB_UI.refreshAll(); } } /* ---------- 添加到画布 ---------- */ function addText() { const el = { id: HGB_Store.uid(), type: 'text', name: '文字', text: 'EDUCATION', x: 200, y: 260, w: 200, h: 60, rotation: 0, fontSize: 36, fontWeight: 700, fontFamily: 'Arial', letterSpacing: 2, fill: '#1B4F72', stroke: 'none', strokeWidth: 0, opacity: 1, visible: true, locked: false }; HGB_Store.addElement(el); HGB_UI.refreshAll(); return el; } function addIcon(iconId, name) { const el = { id: HGB_Store.uid(), type: 'icon', name: name || iconId, iconId: iconId, svg: HGB_Store.iconSvg(iconId), x: 220, y: 170, w: 160, h: 160, rotation: 0, fill: 'none', stroke: '#1B4F72', strokeWidth: 2, opacity: 1, visible: true, locked: false }; HGB_Store.addElement(el); HGB_UI.refreshAll(); return el; } /* ---------- 导出 ---------- */ function exportPngFlow() { // 打开导出对话框 HGB_UI.openExportModal('png'); } return { init, addText, addIcon, exportPngFlow }; })();