Files

281 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 画个标 · 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 };
})();