281 lines
8.4 KiB
JavaScript
281 lines
8.4 KiB
JavaScript
/**
|
||
* 画个标 · 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 };
|
||
})();
|