feat: 初始化画个标仓库 - AI驱动设计系统(BYOK模式) - 224源/371504图标+25海报源+130技能

This commit is contained in:
sgp
2026-10-03 21:44:18 +08:00
commit 9e222877ea
497 changed files with 43611 additions and 0 deletions
+280
View File
@@ -0,0 +1,280 @@
/**
* 画个标 · 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 };
})();