Files
SoonDesign/frontend-web/js/design2/core.js
T
24kycj 784cacf259 fix(web): design1/2 缩略图与 design2 设为背景交互修复
- design1:预览/最近文件缩略图按 background 边界截取,去掉多余白盘
- design2:设为背景贴齐 1012x648 卡框,保存/打开保留用户背景 src
- design2:列表可选中锁定对象并加载完整属性,缩略图叠加 front_bg 模板
- design2:固定卡框锚点,避免选中背景时标尺/蒙版随属性面板漂移

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 19:26:17 +08:00

3023 lines
111 KiB
JavaScript
Raw 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.
// design2核心功能模块
// 注意:此代码在layui.use回调函数内部执行
// 全局变量已在主入口文件中定义,这里只需要初始化
function soonDesign2MainHeight() {
var main = document.querySelector('.layui-fluid.main');
if (main && main.clientHeight > 40) return main.clientHeight;
var tb = document.getElementById('portal-topbar');
return Math.max(480, window.innerHeight - (tb ? tb.offsetHeight : 0));
}
function soonDesign2SidebarWidth() {
var colRight = document.querySelector('body.soon-design-page .col-right');
if (colRight && colRight.offsetWidth > 0) return colRight.offsetWidth;
return 320;
}
function soonDesign2ToolboxWidth() {
var toolbox = document.querySelector('body.soon-design-page .container .left');
if (toolbox && toolbox.offsetWidth > 0) return toolbox.offsetWidth;
return 50;
}
function soonDesign2CanvasWidth() {
var canvasDiv = document.getElementById('canvas-div');
if (canvasDiv && canvasDiv.clientWidth > 0) return canvasDiv.clientWidth;
var colLeft = document.querySelector('body.soon-design-page .col-left');
if (colLeft && colLeft.clientWidth > 0) {
return Math.max(320, colLeft.clientWidth - soonDesign2ToolboxWidth());
}
var docW = document.documentElement.clientWidth || window.innerWidth;
return Math.max(320, docW - soonDesign2SidebarWidth() - soonDesign2ToolboxWidth());
}
function layoutDesign2Shell() {
var isWeb = !!(window.SOON_DEPLOY_CONFIG);
var mainH = soonDesign2MainHeight();
var canvasW = soonDesign2CanvasWidth();
var canvasH = isWeb ? Math.max(400, mainH - 130) : (document.body.clientHeight - 100);
$('#canvas1').attr('width', canvasW).attr('height', canvasH);
$('#canvas2').attr('width', canvasW).attr('height', canvasH);
if (!isWeb) {
$('#object_attribute').css('height', '470px');
$('#obj_list').css('height', Math.max(120, mainH - 520));
$('.obj_list').css('height', Math.max(80, $('#obj_list').height() - 32));
}
return { canvasW: canvasW, canvasH: canvasH };
}
var mainEl = document.querySelector('.layui-fluid.main');
if (mainEl) mainEl.style.display = 'flex';
else $('.main').show();
var _design2Layout = layoutDesign2Shell();
// 初始化Canvas对象
// 优化性能:在创建 Fabric Canvas 之前,先设置 canvas context 的 willReadFrequently
try {
const canvas1El = document.getElementById('canvas1');
const canvas2El = document.getElementById('canvas2');
if (canvas1El) {
canvas1El.getContext('2d', { willReadFrequently: true });
}
if (canvas2El) {
canvas2El.getContext('2d', { willReadFrequently: true });
}
} catch (e) {
// 如果浏览器不支持 willReadFrequently,忽略错误
}
canvas1 = new fabric.Canvas('canvas1')
canvas2 = new fabric.Canvas('canvas2')
// 同步到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问
window.canvas1 = canvas1;
window.canvas2 = canvas2;
// 撤销/重做过程中使用的标志位,防止并发操作和误记录历史
window._isApplyingHistory = false;
fabric.Object.prototype.objectCaching = false
ctx1 = canvas1.getSelectionContext()
ctx2 = canvas2.getSelectionContext()
is_bgi_add = false
// 同步到 window 对象
window.ctx1 = ctx1;
window.ctx2 = ctx2;
window.is_bgi_add = is_bgi_add;
canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), (function () {
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
return divW / 1200 > 1 ? 1 : divW / 1200;
})())
canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), (function () {
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
return divW / 1200 > 1 ? 1 : divW / 1200;
})())
zoom = (function () {
var divW = $('#canvas-div').width() || _design2Layout.canvasW || 800;
var z = divW / 1200 > 1 ? 1 : divW / 1200;
return z > 0 ? z : 0.5;
})()
window.zoom = zoom;
// 初始化标尺
function initRulers() {
// dpi已在主入口文件中定义为全局变量
const mmToPx = dpi / 25.4; // 1mm = 11.811px (300 DPI)
const rulerHeight = 20;
const rulerWidth = 20;
// 获取实际画布的像素尺寸(px
// design2.html 的实际画布尺寸是 1012px x 648px
const actualCanvasWidth = canvas1.width || 1012; // 实际画布宽度(px
const actualCanvasHeight = canvas1.height || 648; // 实际画布高度(px
// 获取显示区域的尺寸(包括 margin)
const canvasDiv = $('#canvas-div');
const canvasWrapper = $('.canvas-wrapper');
// 使用 canvas-wrapper 的宽度,如果没有则使用 canvas_div 的宽度
let canvasWidth = canvasWrapper.width();
let canvasHeight = canvasWrapper.height();
if (!canvasWidth || canvasWidth === 0) {
const canvas1Width = $('#canvas1').width() || canvasDiv.width() - 20;
canvasWidth = canvas1Width + 20; // 加上 margin-left: 20px
}
if (!canvasHeight || canvasHeight === 0) {
const canvas1Height = $('#canvas1').height() || canvasDiv.height() - 20;
canvasHeight = canvas1Height + 20; // 加上 margin-top: 20px
}
// 设置标尺尺寸(使用显示区域的尺寸)
$('#ruler-h').attr('width', canvasWidth).attr('height', rulerHeight);
$('#ruler-v').attr('width', rulerWidth).attr('height', canvasHeight);
$('.ruler-horizontal').css('width', canvasWidth + 'px');
$('.ruler-vertical').css('height', canvasHeight + 'px');
// 绘制水平标尺
function drawHorizontalRuler() {
const ctx = document.getElementById('ruler-h').getContext('2d', { willReadFrequently: true });
ctx.clearRect(0, 0, canvasWidth, rulerHeight);
// 背景
ctx.fillStyle = '#2C2C2C';
ctx.fillRect(0, 0, canvasWidth, rulerHeight);
// 边框
ctx.strokeStyle = '#1E1E1E';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, canvasWidth, rulerHeight);
ctx.beginPath();
ctx.moveTo(0, rulerHeight - 1);
ctx.lineTo(canvasWidth, rulerHeight - 1);
ctx.stroke();
// 刻度线
ctx.strokeStyle = '#888888';
ctx.fillStyle = '#CCCCCC';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算
// 实际画布物理宽度 = actualCanvasWidth / dpi * 25.4 (mm)
// 显示区域宽度 = actualCanvasWidth * zoom (px)
// 所以:1mm 在显示区域中 = (actualCanvasWidth * zoom) / (actualCanvasWidth / dpi * 25.4) = zoom * dpi / 25.4
const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数
const marginOffset = 20; // canvas 的 margin-left: 20px
// 标尺0点应该在background_image的左上角位置
// background_image在画布坐标系中的left值,需要转换到标尺坐标系
// 画布从标尺的marginOffset位置开始,所以:标尺0点 = marginOffset + background_image.left (如果存在)
let originX = marginOffset;
var rulerSide = ($('#back_side').hasClass('ui-button-active')) ? canvas2 : canvas1;
var anchorX = typeof soonDesign2GetFrameAnchor === 'function'
? soonDesign2GetFrameAnchor(rulerSide)
: null;
if (anchorX) {
originX = marginOffset + anchorX.left;
} else if (typeof background_image !== 'undefined' && background_image) {
originX = marginOffset + background_image.left;
}
const startX = 0;
const endX = canvasWidth;
// 绘制主要刻度(每10mm
// 从0点向左绘制负值刻度
for (let mm = -10; originX + mm * pxPerMm >= startX; mm -= 10) {
const x = originX + mm * pxPerMm;
if (x < startX) break;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, rulerHeight);
ctx.stroke();
// 数字(负值)
ctx.fillText(mm + 'mm', x + 2, 2);
}
// 从0点向右绘制正值刻度
for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 10) {
const x = originX + mm * pxPerMm;
if (x > endX) break;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, rulerHeight);
ctx.stroke();
// 数字
ctx.fillText(mm + 'mm', x + 2, 2);
}
// 绘制次要刻度(每1mm
// 从0点向左绘制负值刻度
for (let mm = -1; originX + mm * pxPerMm >= startX; mm -= 1) {
const x = originX + mm * pxPerMm;
if (x < startX) break;
if (mm % 10 === 0) continue; // 跳过主要刻度
ctx.beginPath();
ctx.moveTo(x, rulerHeight - 8);
ctx.lineTo(x, rulerHeight);
ctx.stroke();
}
// 从0点向右绘制正值刻度
for (let mm = 0; originX + mm * pxPerMm <= endX; mm += 1) {
const x = originX + mm * pxPerMm;
if (x > endX) break;
if (mm % 10 === 0) continue; // 跳过主要刻度
ctx.beginPath();
ctx.moveTo(x, rulerHeight - 8);
ctx.lineTo(x, rulerHeight);
ctx.stroke();
}
}
// 绘制垂直标尺
function drawVerticalRuler() {
const ctx = document.getElementById('ruler-v').getContext('2d', { willReadFrequently: true });
ctx.clearRect(0, 0, rulerWidth, canvasHeight);
// 背景
ctx.fillStyle = '#2C2C2C';
ctx.fillRect(0, 0, rulerWidth, canvasHeight);
// 边框
ctx.strokeStyle = '#1E1E1E';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, rulerWidth, canvasHeight);
ctx.beginPath();
ctx.moveTo(rulerWidth - 1, 0);
ctx.lineTo(rulerWidth - 1, canvasHeight);
ctx.stroke();
// 刻度线
ctx.strokeStyle = '#888888';
ctx.fillStyle = '#CCCCCC';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// 计算:实际画布尺寸(px)对应的物理尺寸(mm),然后根据显示区域的缩放比例计算
const pxPerMm = zoom * mmToPx; // 在显示区域中,1mm对应的像素数
const marginOffset = 20; // canvas 的 margin-top: 20px (但还要加上ruler的高度)
const rulerHeightOffset = 20; // ruler 的高度
// 标尺0点应该在background_image的左上角位置
// background_image在画布坐标系中的top值,需要转换到标尺坐标系
// 画布从标尺的marginOffset + rulerHeightOffset位置开始,所以:标尺0点 = marginOffset + rulerHeightOffset + background_image.top (如果存在)
let originY = marginOffset + rulerHeightOffset;
var rulerSideY = ($('#back_side').hasClass('ui-button-active')) ? canvas2 : canvas1;
var anchorY = typeof soonDesign2GetFrameAnchor === 'function'
? soonDesign2GetFrameAnchor(rulerSideY)
: null;
if (anchorY) {
originY = marginOffset + rulerHeightOffset + anchorY.top;
} else if (typeof background_image !== 'undefined' && background_image) {
originY = marginOffset + rulerHeightOffset + background_image.top;
}
const startY = 0;
const endY = canvasHeight;
// 绘制主要刻度(每10mm
// 从0点向上绘制负值刻度
for (let mm = -10; originY + mm * pxPerMm >= startY; mm -= 10) {
const y = originY + mm * pxPerMm;
if (y < startY) break;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(rulerWidth, y);
ctx.stroke();
// 数字(旋转90度,负值)
ctx.save();
ctx.translate(2, y + 8);
ctx.rotate(-Math.PI / 2);
ctx.fillText(mm + 'mm', 0, 0);
ctx.restore();
}
// 从0点向下绘制正值刻度
for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 10) {
const y = originY + mm * pxPerMm;
if (y > endY) break;
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(rulerWidth, y);
ctx.stroke();
// 数字(旋转90度)
ctx.save();
ctx.translate(2, y + 8);
ctx.rotate(-Math.PI / 2);
ctx.fillText(mm + 'mm', 0, 0);
ctx.restore();
}
// 绘制次要刻度(每1mm
// 从0点向上绘制负值刻度
for (let mm = -1; originY + mm * pxPerMm >= startY; mm -= 1) {
const y = originY + mm * pxPerMm;
if (y < startY) break;
if (mm % 10 === 0) continue; // 跳过主要刻度
ctx.beginPath();
ctx.moveTo(rulerWidth - 8, y);
ctx.lineTo(rulerWidth, y);
ctx.stroke();
}
// 从0点向下绘制正值刻度
for (let mm = 0; originY + mm * pxPerMm <= endY; mm += 1) {
const y = originY + mm * pxPerMm;
if (y > endY) break;
if (mm % 10 === 0) continue; // 跳过主要刻度
ctx.beginPath();
ctx.moveTo(rulerWidth - 8, y);
ctx.lineTo(rulerWidth, y);
ctx.stroke();
}
}
// 初始绘制
drawHorizontalRuler();
drawVerticalRuler();
// 监听窗口大小变化
$(window).on('resize', function () {
setTimeout(function () {
const canvasWrapper = $('.canvas-wrapper');
let newWidth = canvasWrapper.width();
let newHeight = canvasWrapper.height();
if (!newWidth || newWidth === 0) {
const canvas1Width = $('#canvas1').width() || $('#canvas-div').width() - 20;
newWidth = canvas1Width + 20; // 加上 margin-left: 20px
}
if (!newHeight || newHeight === 0) {
const canvas1Height = $('#canvas1').height() || $('#canvas-div').height() - 20;
newHeight = canvas1Height + 20; // 加上 margin-top: 20px
}
if (newWidth !== canvasWidth || newHeight !== canvasHeight) {
canvasWidth = newWidth;
canvasHeight = newHeight;
$('#ruler-h').attr('width', newWidth);
$('#ruler-v').attr('height', newHeight);
$('.ruler-horizontal').css('width', newWidth + 'px');
$('.ruler-vertical').css('height', newHeight + 'px');
drawHorizontalRuler();
drawVerticalRuler();
}
}, 100);
});
canvas1.off('after:render');
canvas2.off('after:render');
canvas1.on('after:render', function () {
drawHorizontalRuler();
drawVerticalRuler();
soonDesign2DrawCardMask(canvas1, ctx1, document.getElementById('source_front'));
soonDesign2DrawTemplateOverlay(canvas1, ctx1, document.getElementById('source_front'));
});
canvas2.on('after:render', function () {
drawHorizontalRuler();
drawVerticalRuler();
soonDesign2DrawCardMask(canvas2, ctx2, document.getElementById('source_back'));
soonDesign2DrawTemplateOverlay(canvas2, ctx2, document.getElementById('source_back'));
});
// --- 辅助线功能 (参考 design1) ---
// canvasWrapper 已在上面声明,直接使用
const canvasBox = $('#canvas-div'); // design2 使用 canvas-div 作为容器
// 确保容器定位
if (canvasWrapper.css('position') === 'static') canvasWrapper.css('position', 'relative');
// 定义临时拖拽线 DOM
let $dragLine = null;
// 通用拖拽开始函数
const startDragGuide = (type, e) => {
e.preventDefault();
e.stopPropagation();
// 防止重复调用:如果正在拖拽,直接返回
if ($dragLine && $dragLine.length > 0) {
return;
}
// 创建临时显示的线
$dragLine = $('<div class="guide-line-drag"></div>');
canvasWrapper.append($dragLine);
if (type === 'horizontal') {
// 从上往下拖 (水平线)
$dragLine.css({
left: 0, width: '100%', height: '1px', top: e.offsetY
});
} else {
// 从左往右拖 (垂直线)
$dragLine.css({
top: 0, height: '100%', width: '1px', left: e.offsetX
});
}
// 绑定 document 移动事件
$(document).on('mousemove.ruler', function (moveEvent) {
let wrapperOffset = canvasWrapper.offset();
if (type === 'horizontal') {
let relY = moveEvent.pageY - wrapperOffset.top;
$dragLine.css('top', relY + 'px');
} else {
let relX = moveEvent.pageX - wrapperOffset.left;
$dragLine.css('left', relX + 'px');
}
});
// 绑定松开事件 (创建真实的 Fabric 线)
$(document).on('mouseup.ruler', function (upEvent) {
$(document).off('mousemove.ruler mouseup.ruler');
if (!$dragLine) return;
// 获取最终位置
let wrapperOffset = canvasWrapper.offset();
let finalX = upEvent.pageX - wrapperOffset.left;
let finalY = upEvent.pageY - wrapperOffset.top;
// 移除临时线
$dragLine.remove();
$dragLine = null;
// 只有拖入画布区域才创建
if (finalX > rulerWidth && finalY > rulerHeight) {
createFabricGuide(type, finalX, finalY);
}
});
};
// 绑定到标尺 DOM
$('#ruler-h').off('mousedown').on('mousedown', (e) => startDragGuide('horizontal', e));
$('#ruler-v').off('mousedown').on('mousedown', (e) => startDragGuide('vertical', e));
// --- 在画布中创建辅助线 ---
function createFabricGuide(type, domX, domY) {
// domX 和 domY 是相对于 canvas-wrapper 的坐标
// 需要转换为相对于 canvas 的坐标,然后再转换为 Fabric 画布坐标
// 获取 canvas 相对于 canvas-wrapper 的位置
let canvas1El = $('#canvas1');
let canvasPosition = canvas1El.position();
let canvasMarginLeft = parseFloat(canvas1El.css('margin-left')) || 0;
let canvasMarginTop = parseFloat(canvas1El.css('margin-top')) || 0;
// 计算相对于 canvas 的坐标(减去 canvas 在 wrapper 中的位置和 margin
let relX = domX - canvasPosition.left - canvasMarginLeft;
let relY = domY - canvasPosition.top - canvasMarginTop;
// 转为 Fabric 坐标 (除以 zoom)
let canvasX = relX / zoom;
let canvasY = relY / zoom;
// 使用与画布尺寸相关的长度,避免“整块区域都像辅助线”
let lineExtent = Math.max(canvas1.width, canvas1.height) * 2;
// 构造线对象
let linePoints = [];
if (type === 'horizontal') {
linePoints = [-lineExtent, canvasY, lineExtent, canvasY];
} else {
linePoints = [canvasX, -lineExtent, canvasX, lineExtent];
}
let guideLine = new fabric.Line(linePoints, {
stroke: '#0066FF',
strokeWidth: 1 / zoom,
opacity: 0.9,
shadow: {
color: '#0066FF',
blur: 6,
offsetX: 0,
offsetY: 0,
affectStroke: true
},
selectable: true,
evented: true,
hasControls: false,
hasBorders: false,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
hoverCursor: type === 'horizontal' ? 'ns-resize' : 'ew-resize',
// 只在靠近线条时触发命中,避免“大块区域都像辅助线”
// 同时适当放宽 tolerance,保证易于点中拖拽
perPixelTargetFind: true,
targetFindTolerance: 6,
padding: 2,
isGuideLine: true,
ignoreSave: true
});
// 限制移动方向(只约束方向,不把整条线拖到极远,避免“整片区域是辅助线”的感觉)
guideLine.on('moving', function () {
if (type === 'horizontal') {
// 水平线:根据当前中心点的 Y,重算 x1/x2,使其横穿画布
const cy = this.getCenterPoint().y;
this.set({
x1: -lineExtent,
y1: cy,
x2: lineExtent,
y2: cy
});
} else {
// 垂直线:根据当前中心点的 X,重算 y1/y2,使其竖穿画布
const cx = this.getCenterPoint().x;
this.set({
x1: cx,
y1: -lineExtent,
x2: cx,
y2: lineExtent
});
}
});
// 确定应该添加到哪个 canvas(使用当前活动的 canvas
let targetCanvas = canvas;
if (!targetCanvas || (targetCanvas !== canvas1 && targetCanvas !== canvas2)) {
// 如果没有活动的 canvas,默认使用 canvas1
targetCanvas = canvas1;
}
targetCanvas.add(guideLine);
targetCanvas.setActiveObject(guideLine);
targetCanvas.renderAll();
}
}
function relayoutDesign2Canvas() {
var layout = layoutDesign2Shell();
var divW = layout.canvasW;
zoom = divW / 1200 > 1 ? 1 : divW / 1200;
if (zoom <= 0) zoom = 0.5;
window.zoom = zoom;
canvas1.setWidth(layout.canvasW);
canvas1.setHeight(layout.canvasH);
canvas2.setWidth(layout.canvasW);
canvas2.setHeight(layout.canvasH);
canvas1.zoomToPoint(new fabric.Point(canvas1.width / 2, canvas1.height / 2), zoom);
canvas2.zoomToPoint(new fabric.Point(canvas2.width / 2, canvas2.height / 2), zoom);
canvas1.renderAll();
canvas2.renderAll();
initRulers();
if (typeof window.soonDesign2ScaleBgSources === 'function') {
window.soonDesign2ScaleBgSources();
}
}
function soonDesign2ScaleBgSources() {
var z = zoom || window.zoom || 0.5;
var sf = document.getElementById('source_front');
var sb = document.getElementById('source_back');
if (sf && sf.naturalWidth > 0) {
sf.style.width = (sf.naturalWidth * z) + 'px';
sf.style.height = (sf.naturalHeight * z) + 'px';
}
if (sb && sb.naturalWidth > 0) {
sb.style.width = (sb.naturalWidth * z) + 'px';
sb.style.height = (sb.naturalHeight * z) + 'px';
}
}
function soonDesign2BgOverlayMetrics(imgEl) {
if (!imgEl || !imgEl.complete || imgEl.naturalWidth <= 0) return null;
var z = zoom || window.zoom || 0.5;
var w = parseFloat(imgEl.style.width);
if (!w || w <= 0) w = imgEl.naturalWidth * z;
var h = parseFloat(imgEl.style.height);
if (!h || h <= 0) h = imgEl.naturalHeight * z;
return { w: w, h: h };
}
/** 卡面区域:与 background_image 对齐(1012×648),fallback 模板线框 */
function soonDesign2ResolveCardFrame(fabricCanvas, imgEl) {
if (!fabricCanvas) return null;
var metaList = (fabricCanvas === canvas2) ? (window.objs2 || objs2) : (window.objs1 || objs1);
if (soonDesign2IsUserBgMeta(metaList)) {
var anchor = soonDesign2GetFrameAnchor(fabricCanvas);
var card = soonDesign2CardSize();
if (anchor) {
return { left: anchor.left, top: anchor.top, width: card.w, height: card.h };
}
}
var bg = (fabricCanvas === canvas1) ? background_image1 : background_image2;
if (bg && bg.width) {
var sx = bg.scaleX == null ? 1 : bg.scaleX;
var sy = bg.scaleY == null ? 1 : bg.scaleY;
return {
left: bg.left,
top: bg.top,
width: bg.width * sx,
height: bg.height * sy
};
}
var cw = fabricCanvas.width || 1012;
var ch = fabricCanvas.height || 648;
if (imgEl) {
var metrics = soonDesign2BgOverlayMetrics(imgEl);
if (metrics) {
return {
left: (cw - metrics.w) / 2,
top: (ch - metrics.h) / 2,
width: metrics.w,
height: metrics.h
};
}
}
return { left: 0, top: 0, width: cw, height: ch };
}
function soonDesign2CardSize() {
return { w: 1012, h: 648 };
}
function soonDesign2IsUserBgMeta(metaList) {
return !!(metaList && metaList.length > 0 && metaList[0] && metaList[0].type === 1);
}
function soonDesign2SyncFrameAnchor(fabricCanvas, frame) {
if (!frame) return;
var pt = { left: frame.left, top: frame.top };
if (fabricCanvas === canvas2) {
window.soonDesign2FrameAnchor2 = pt;
} else {
window.soonDesign2FrameAnchor1 = pt;
}
var active = window.canvas || canvas;
if (!active || fabricCanvas === active) {
window.soonDesign2FrameAnchor = pt;
}
}
function soonDesign2GetFrameAnchor(fabricCanvas) {
if (fabricCanvas === canvas2 && window.soonDesign2FrameAnchor2) {
return window.soonDesign2FrameAnchor2;
}
if (window.soonDesign2FrameAnchor1) {
return window.soonDesign2FrameAnchor1;
}
return window.soonDesign2FrameAnchor || null;
}
/** 用户设为背景的对象:坐标系锚点,属性面板不应再移动它 */
function soonDesign2ActiveIsAnchorBg() {
var c = window.canvas || canvas;
if (!c) return false;
var ao = c.getActiveObject();
if (!ao) return false;
var meta = (c === canvas2) ? (window.objs2 || objs2) : (window.objs1 || objs1);
if (c === (window.canvas || canvas)) {
meta = window.objs || objs || meta;
}
if (!soonDesign2IsUserBgMeta(meta)) return false;
var bg = (c === canvas2) ? (window.background_image2 || background_image2) : (window.background_image1 || background_image1);
return ao === bg;
}
function soonDesign2FrameOrigin(fabricCanvas) {
var frame = soonDesign2ResolveCardFrame(fabricCanvas, null);
if (frame) return frame;
var card = soonDesign2CardSize();
var cw = (fabricCanvas && fabricCanvas.width) || card.w;
var ch = (fabricCanvas && fabricCanvas.height) || card.h;
return { left: (cw - card.w) / 2, top: (ch - card.h) / 2, width: card.w, height: card.h };
}
/** 用户图铺满卡面框(1012×648),与蒙版/线框同源坐标 */
function soonDesign2SnapImageToCardFrame(img, fabricCanvas, frame) {
if (!img || !img.width) return frame || soonDesign2FrameOrigin(fabricCanvas);
frame = frame || soonDesign2FrameOrigin(fabricCanvas);
var card = soonDesign2CardSize();
img.set({
originX: 'left',
originY: 'top',
scaleX: card.w / img.width,
scaleY: card.h / img.height,
left: frame.left,
top: frame.top
});
img.setCoords();
soonDesign2SyncFrameAnchor(fabricCanvas, frame);
return frame;
}
function soonDesign2ApplyUserBgAsCanvas0(userImg, fabricCanvas, oldBg) {
if (!userImg || !fabricCanvas) return;
if (oldBg && oldBg !== userImg) {
fabricCanvas.remove(oldBg);
}
fabricCanvas.sendToBack(userImg);
if (fabricCanvas === canvas2) {
background_image2 = userImg;
window.background_image2 = userImg;
} else {
background_image1 = userImg;
window.background_image1 = userImg;
}
var activeCanvas = window.canvas || canvas;
if (fabricCanvas === activeCanvas) {
background_image = userImg;
window.background_image = userImg;
}
}
function soonDesign2LockBgObject(bgObj) {
if (!bgObj) return;
bgObj.set({
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockSkewingX: true,
lockSkewingY: true,
selectable: false,
evented: false,
hoverCursor: 'default'
});
}
/** 打开旧文件:去掉残留模板层,用户背景独占 canvas[0] 并贴齐卡框 */
function soonDesign2NormalizeUserBg(fabricCanvas, metaList) {
if (!soonDesign2IsUserBgMeta(metaList) || !fabricCanvas) return null;
var all = fabricCanvas.getObjects().filter(function (o) { return !o.isGuideLine; });
if (!all.length) return null;
var frame = soonDesign2FrameOrigin(fabricCanvas);
var userBg = all[0];
if (all.length > 1) {
fabricCanvas.remove(all[0]);
userBg = fabricCanvas.getObjects().filter(function (o) { return !o.isGuideLine; })[0];
}
if (!userBg) return null;
soonDesign2SnapImageToCardFrame(userBg, fabricCanvas, frame);
soonDesign2ApplyUserBgAsCanvas0(userBg, fabricCanvas, null);
soonDesign2LockBgObject(userBg);
return userBg;
}
window.soonDesign2CardSize = soonDesign2CardSize;
window.soonDesign2IsUserBgMeta = soonDesign2IsUserBgMeta;
window.soonDesign2SyncFrameAnchor = soonDesign2SyncFrameAnchor;
window.soonDesign2GetFrameAnchor = soonDesign2GetFrameAnchor;
window.soonDesign2ActiveIsAnchorBg = soonDesign2ActiveIsAnchorBg;
window.soonDesign2FrameOrigin = soonDesign2FrameOrigin;
window.soonDesign2SnapImageToCardFrame = soonDesign2SnapImageToCardFrame;
window.soonDesign2ApplyUserBgAsCanvas0 = soonDesign2ApplyUserBgAsCanvas0;
window.soonDesign2LockBgObject = soonDesign2LockBgObject;
window.soonDesign2NormalizeUserBg = soonDesign2NormalizeUserBg;
function soonDesign2DrawCardMask(fabricCanvas, ctx, imgEl) {
if (window._soonSkipDesign2CardMask || !fabricCanvas || !ctx) return;
var frame = soonDesign2ResolveCardFrame(fabricCanvas, imgEl);
if (!frame) return;
var left = frame.left;
var top = frame.top;
var cardW = frame.width;
var cardH = frame.height;
ctx.save();
ctx.beginPath();
ctx.rect(0, 0, fabricCanvas.width, fabricCanvas.height);
ctx.moveTo(left + cardW, top);
ctx.lineTo(left, top);
ctx.lineTo(left, top + cardH);
ctx.lineTo(left + cardW, top + cardH);
ctx.closePath();
ctx.fillStyle = 'rgba(0, 0, 0, 0.9)';
ctx.fill('evenodd');
ctx.restore();
}
function soonDesign2DrawTemplateOverlay(fabricCanvas, ctx, imgEl) {
if (!window.is_bgi_add || !fabricCanvas || !ctx || !imgEl) return;
var frame = soonDesign2ResolveCardFrame(fabricCanvas, imgEl);
if (!frame) return;
ctx.drawImage(imgEl, frame.left, frame.top, frame.width, frame.height);
}
function soonDesign2PrepareBgSources(done) {
var sf = document.getElementById('source_front');
var sb = document.getElementById('source_back');
if (!sf || !sb) {
is_bgi_add = true;
window.is_bgi_add = true;
if (typeof done === 'function') done();
return;
}
var ver = typeof bg_version !== 'undefined' ? bg_version : 1;
var frontName = 'front_bg' + ver + '_2.png';
if (!(sf.complete && sf.naturalWidth > 0 && (sf.src || '').indexOf(frontName) !== -1)) {
sf.src = soonAsset(frontName);
}
var pending = 2;
function finishOne() {
pending--;
if (pending > 0) return;
soonDesign2ScaleBgSources();
is_bgi_add = true;
window.is_bgi_add = true;
canvas1.requestRenderAll();
canvas2.requestRenderAll();
if (typeof done === 'function') done();
}
function watch(img) {
if (img.complete && img.naturalWidth > 0) {
finishOne();
return;
}
img.onload = finishOne;
img.onerror = finishOne;
}
watch(sf);
watch(sb);
}
function soonDesign2StartBackground() {
soonDesign2PrepareBgSources(addBackground);
}
function soonDesign2Kickoff() {
if (window._soonDesign2KickoffDone) return;
window._soonDesign2KickoffDone = true;
if (window.SOON_DEPLOY_CONFIG && ($('#canvas-div').width() || 0) < 100) {
relayoutDesign2Canvas();
}
soonDesign2StartBackground();
}
window.soonDesign2ScaleBgSources = soonDesign2ScaleBgSources;
window.soonDesign2PrepareBgSources = soonDesign2PrepareBgSources;
window.soonDesign2Kickoff = soonDesign2Kickoff;
initRulers();
$('.canvas-container:eq(1)').hide()
canvas = canvas1
window.canvas = canvas
fabric.Object.prototype.set({
borderColor: '#9013FE',
cornerColor: '#9013FE', //激活状态角落图标的填充颜色
cornerStrokeColor: '#9013FE', //激活状态角落图标的边框颜色
borderOpacityWhenMoving: 1,
borderScaleFactor: 1,
cornerSize: 6,
cornerStyle: 'circle', //rect,circle
centeredScaling: false, //角落放大缩小是否是以图形中心为放大原点
centeredRotation: true, //旋转按钮旋转是否是左上角为圆心旋转
transparentCorners: false, //激活状态角落的图标是否透明
rotatingPointOffset: 20, //旋转距旋转体的距离
padding: 0
})
canvas1.preserveObjectStacking = true
canvas2.preserveObjectStacking = true
// ==========================================
// 对象选择和属性更新函数
// ==========================================
// 将 updateControls 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问
window._soonSyncingFontControls = false;
function soonFontSizeUiVal(fontSizePx) {
var d = window.dpi || 300;
var uiVal = parseFloat(fontSizePx) * 72 / d;
if (uiVal % 0.5 !== 0) uiVal = Math.round(uiVal * 2) / 2;
return uiVal;
}
function soonSyncActiveFontSizeInput(activeObj, currentCanvas) {
if (!activeObj || !currentCanvas) return;
var idx = getIndex(activeObj, currentCanvas);
if (idx <= 0) return;
var objData = (currentCanvas === canvas1 ? objs1 : objs2)[idx - 1];
if (!objData) return;
var uiVal = soonFontSizeUiVal(activeObj.get('fontSize'));
if (objData.type === 3) $("#text_size").val(uiVal);
else if (objData.type === 4) $("#out_text_size").val(uiVal);
else if (objData.type === 10) $("#count_size").val(uiVal);
}
window.updateControls = function () {
const currentCanvas = window.canvas || canvas;
const currentBackgroundImage = window.background_image || background_image;
if (!currentCanvas || !currentCanvas.getActiveObject()) return;
const activeObj = currentCanvas.getActiveObject();
window._soonSyncingFontControls = true;
try {
$("#text_r").val(activeObj.get("angle").toFixed(0));
$("#text_x").val((activeObj.get("left") - currentBackgroundImage.left).toFixed(0));
$("#text_y").val((activeObj.get("top") - currentBackgroundImage.top).toFixed(0));
$("#text_h").val((activeObj.get("scaleY") * activeObj.get("height")).toFixed(0));
$("#text_w").val((activeObj.get("scaleX") * activeObj.get("width")).toFixed(0));
soonSyncActiveFontSizeInput(activeObj, currentCanvas);
if (activeObj.get("lockMovementX")) {
$("#lock_img").attr("src", soonAsset("lock20.png"));
} else {
$("#lock_img").attr("src", soonAsset("unlock20.png"));
}
} finally {
window._soonSyncingFontControls = false;
}
};
// 向后兼容
function updateControls() {
return window.updateControls();
}
function selectObject(target, index) {
updateControls();
$("#base_control").show();
}
function selectPic(target, index) {
// 检查是否有选中对象,如果没有则直接返回
if (!canvas || !canvas.getActiveObject()) return;
$("#pic_control").show();
$("#set_bg").show();
let activeObj = canvas.getActiveObject();
// 检查对象是否被锁定:
// - 从画布点击时(_selectingFromList 为 false):不允许选中,被锁定的对象保持不可选状态
// - 从对象列表点击时(_selectingFromList 为 true):允许选中,但对象在画布上仍然保持锁定
if (!window._selectingFromList && isObjectLocked(activeObj)) {
canvas.discardActiveObject();
canvas.renderAll();
return;
}
if (typeof pic_op !== 'undefined' && pic_op && typeof pic_op.setValue === 'function') {
pic_op.setValue(100 - (activeObj.get("opacity") * 100));
} else {
$("#pic_op").next().children(".layui-slider-bar").css("width", (1 - activeObj.get("opacity")) * 100 + "%");
$("#pic_op").next().children(".layui-slider-handle").css("left", (1 - activeObj.get("opacity")) * 100 + "%");
}
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectOutPic(target, index) {
$("#out_pic_control").show();
if (typeof out_pic_op !== 'undefined' && out_pic_op.setValue) {
out_pic_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100));
} else {
$("#out_pic_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
$("#out_pic_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
}
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#out_pic_field_name").val(objs[idx - 1].name);
$("#pic_control").hide();
$("#set_bg").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectText(target, index) {
$("#text_control").show();
$("#text_color").val(canvas.getActiveObject().get("fill"));
$("#text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor"));
let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi
if (val % 0.5 !== 0) val = Math.round(val * 2) / 2
$("#text_size").val(val);
$("#text_text").val(canvas.getActiveObject().get("text"));
$("#text_font_family").val(canvas.getActiveObject().get("fontFamily"));
if (canvas.getActiveObject().get("underline") == true) {
$("#text_underline").addClass("ui-radio-acitve")
} else {
$("#text_underline").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("fontWeight") == "bold") {
$("#text_bold").addClass("ui-radio-acitve")
} else {
$("#text_bold").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("fontStyle") == "italic") {
$("#text_italics").addClass("ui-radio-acitve")
} else {
$("#text_italics").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("linethrough") == true) {
$("#text_linethrough").addClass("ui-radio-acitve")
} else {
$("#text_linethrough").removeClass("ui-radio-acitve")
}
$("#circle_text_control").hide();
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectCircleText(target, index) {
const activeObj = target || canvas.getActiveObject();
if (!activeObj) return;
let idx = getIndex(activeObj);
$("#circle_text_control").show();
$("#circle_text_color").val(activeObj.get("fill"));
$("#circle_text_back_color").val(activeObj.get("textBackgroundColor") == "" ? '#ffffff' : activeObj.get("textBackgroundColor"));
if (typeof circle_text_size !== 'undefined' && circle_text_size && typeof circle_text_size.setValue === 'function') {
if (typeof circle_text_size_updating !== 'undefined') {
circle_text_size_updating = true;
}
circle_text_size.setValue(activeObj.get("fontSize") - 10);
if (typeof circle_text_size_updating !== 'undefined') {
circle_text_size_updating = false;
}
}
if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter && typeof circle_text_diameter.setValue === 'function') {
circle_text_diameter.setValue(activeObj.get("diameter") - 50);
}
$("#circle_text_center").prop("checked", objs[idx - 1].circleCenter ? 'checked' : '');
$("#circle_text_text").val(activeObj.get("text"));
$("#circle_text_font_family").val(activeObj.get("fontFamily"));
if (activeObj.get("underline") == true) {
$("#circle_text_underline").addClass("ui-radio-acitve")
} else {
$("#circle_text_underline").removeClass("ui-radio-acitve")
}
if (activeObj.get("fontWeight") == "bold") {
$("#circle_text_bold").addClass("ui-radio-acitve")
} else {
$("#circle_text_bold").removeClass("ui-radio-acitve")
}
if (activeObj.get("fontStyle") == "italic") {
$("#circle_text_italics").addClass("ui-radio-acitve")
} else {
$("#circle_text_italics").removeClass("ui-radio-acitve")
}
if (activeObj.get("linethrough") == true) {
$("#circle_text_linethrough").addClass("ui-radio-acitve")
} else {
$("#circle_text_linethrough").removeClass("ui-radio-acitve")
}
$("#pic_control").hide();
$("#set_bg").hide();
$("#rotate").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectOutText(target, index) {
const align = canvas.getActiveObject().get('textAlign')
if (align === 'left') {
$('#out_left_alignment').parent().addClass('tool1')
} else if (align === 'right') {
$('#out_right_alignment').parent().addClass('tool1')
} else {
$('#out_center_alignment').parent().addClass('tool1')
}
$("#out_text_control").show();
$("#out_text_color").val(canvas.getActiveObject().get("fill"));
$("#out_text_text").val(canvas.getActiveObject().get("text"));
let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi
if (val % 0.5 !== 0) val = Math.round(val * 2) / 2
$("#out_text_size").val(val);
$("#out_text_back_color").val(canvas.getActiveObject().get("textBackgroundColor") == "" ? '#ffffff' : canvas.getActiveObject().get("textBackgroundColor"));
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#out_text_field_name").val(objs[idx - 1].name);
$("#out_text_font_family").val(canvas.getActiveObject().get("fontFamily"));
$("#exceed").prop("checked", objs[idx - 1].exceed ? 'checked' : '');
$("#autoWrap").prop("checked", objs[idx - 1].autoWrap ? 'checked' : '');
$("#fixSize").prop("checked", objs[idx - 1].fixSize ? 'checked' : '');
if (canvas.getActiveObject().get("underline") == true) {
$("#out_text_underline").addClass("ui-radio-acitve")
} else {
$("#out_text_underline").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("fontWeight") == "bold") {
$("#out_text_bold").addClass("ui-radio-acitve")
} else {
$("#out_text_bold").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("fontStyle") == "italic") {
$("#out_text_italics").addClass("ui-radio-acitve")
} else {
$("#out_text_italics").removeClass("ui-radio-acitve")
}
if (canvas.getActiveObject().get("linethrough") == true) {
$("#out_text_linethrough").addClass("ui-radio-acitve")
} else {
$("#out_text_linethrough").removeClass("ui-radio-acitve")
}
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectRect(target, index) {
$("#rect_control").show();
$("#rect_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill"));
$("#rect_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke"));
$("#rect_stroke_width").val(canvas.getActiveObject().get("strokeWidth"));
$("#rect_radius").val(canvas.getActiveObject().get("rx"));
if (typeof rect_op !== 'undefined' && rect_op.setValue) {
rect_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100));
} else {
$("#rect_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
$("#rect_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
}
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#circle_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectCircle(target, index) {
$("#circle_control").show();
$("#circle_color").val(canvas.getActiveObject().get("fill") == "" ? "#ffffff" : canvas.getActiveObject().get("fill"));
$("#circle_stroke").val(canvas.getActiveObject().get("stroke") == "" ? "#ffffff" : canvas.getActiveObject().get("stroke"));
$("#circle_stroke_width").val(canvas.getActiveObject().get("strokeWidth"));
if (typeof circle_op !== 'undefined' && circle_op.setValue) {
circle_op.setValue(100 - (canvas.getActiveObject().get("opacity") * 100));
} else {
$("#circle_op").next().children(".layui-slider-bar").css("width", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
$("#circle_op").next().children(".layui-slider-handle").css("left", (1 - canvas.getActiveObject().get("opacity")) * 100 + "%");
}
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#line_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectLine(target, index) {
$("#line_control").show();
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#line_color").val(canvas.getActiveObject().get("stroke"));
$("#line_dist").val(canvas.getActiveObject().get("strokeDashArray")[0]);
$("#text_h").val(canvas.getActiveObject().get("strokeWidth"))
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function selectQrCode(target, index) {
$("#qrcode_Controll").show();
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#qrcode_field_name").val(objs[idx - 1].name);
$("#qrcode_color").val(objs[idx - 1].color);
$("#qrcode_val").val(objs[idx - 1].val);
$("#line_control").hide();
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#counter_control").hide();
$("#barcode_controll").hide();
}
function selectBarCode(target, index) {
$("#barcode_controll").show();
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#barcode_field_name").val(objs[idx - 1].name);
$("#barcode_font_family").val(objs[idx - 1].font);
let val = parseFloat(objs[idx - 1].fontSize) * 72 / dpi
if (val % 0.5 !== 0) val = Math.round(val * 2) / 2
$("#barcode_size").val(val);
$("#barcode_val").val(objs[idx - 1].val);
$("#barcode_show").prop("checked", objs[idx - 1].show ? 'checked' : '');
$("#line_control").hide();
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#counter_control").hide();
$("#qrcode_Controll").hide();
}
function selectCounter(target, index) {
$("#counter_control").show();
let idx = getIndex(canvas.getActiveObject(), canvas);
$("#counter_start").val(objs[idx - 1].start);
$("#counter_add").val(objs[idx - 1].add);
$("#counter_color").val(canvas.getActiveObject().get("fill"));
$("#count_font_family").val(canvas.getActiveObject().get("fontFamily"));
let val = parseFloat(canvas.getActiveObject().get("fontSize")) * 72 / dpi
if (val % 0.5 !== 0) val = Math.round(val * 2) / 2
$("#count_size").val(val);
$("#line_control").hide();
$("#pic_control").hide();
$("#set_bg").hide();
$("#out_pic_control").hide();
$("#text_control").hide();
$("#circle_text_control").hide();
$("#out_text_control").hide();
$("#rect_control").hide();
$("#circle_control").hide();
$("#qrcode_Controll").hide();
$("#barcode_controll").hide();
}
function handleObjectSelected(e) {
// 确定当前活动的 canvas(从事件参数或全局变量)
let currentCanvas = (e && e.target && (e.target.canvas === canvas1 || e.target.canvas === canvas2))
? (e.target.canvas === canvas1 ? canvas1 : canvas2)
: canvas;
// 如果事件来自 canvas,更新全局 canvas 变量
if (currentCanvas === canvas1 || currentCanvas === canvas2) {
canvas = currentCanvas;
objs = (currentCanvas === canvas1) ? objs1 : objs2;
window.canvas = canvas;
window.objs = objs;
}
let select_obj = canvas.getActiveObject();
// 1. 未选中任何对象
if (!select_obj) {
$("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide();
$("#component_type").text(language_str("bg"));
selectObj(-1, true); // 传 true,只更新UI
return;
}
// 2. 如果是辅助线,直接返回,不处理(在获取索引之前检查)
if (select_obj.isGuideLine) {
return;
}
// 3. 解析 objs 下标(用户设为背景时在 canvas[0],不能仅用 getIndex
let metaIdx = typeof soonDesign2ResolveObjMetaIndex === 'function'
? soonDesign2ResolveObjMetaIndex(select_obj, canvas)
: (getIndex(select_obj, canvas) > 0 ? getIndex(select_obj, canvas) - 1 : -1);
// 如果不是通过对象列表选中的,且对象被锁定,则取消选中
if (!window._selectingFromList && isObjectLocked(select_obj)) {
canvas.discardActiveObject();
canvas.renderAll();
$("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide();
$("#component_type").text(language_str("bg"));
selectObj(-1, true);
return;
}
if (metaIdx >= 0) {
if (objs[metaIdx] && typeof objs[metaIdx].type !== 'undefined') {
switch (objs[metaIdx].type) {
case 1: // 图片
$("#component_type").text(language_str("img"));
selectObject(select_obj, metaIdx); selectPic(select_obj, metaIdx); break;
case 2: // 合成图片
$("#component_type").text(language_str("simg"));
selectObject(select_obj, metaIdx); selectOutPic(select_obj, metaIdx); break;
case 3: // 文本
$("#component_type").text(language_str("text"));
selectObject(select_obj, metaIdx); selectText(select_obj, metaIdx); break;
case 4: // 合成文本
$("#component_type").text(language_str("stext"));
selectObject(select_obj, metaIdx); selectOutText(select_obj, metaIdx); break;
case 5: // 矩形
$("#component_type").text(language_str("rect"));
selectObject(select_obj, metaIdx); selectRect(select_obj, metaIdx); break;
case 6: // 圆形
$("#component_type").text(language_str("circ"));
selectObject(select_obj, metaIdx); selectCircle(select_obj, metaIdx); break;
case 7: // 直线
$("#component_type").text(language_str("line"));
selectObject(select_obj, metaIdx); selectLine(select_obj, metaIdx); break;
case 8: // 二维码
$("#component_type").text(language_str("qrc"));
selectObject(select_obj, metaIdx); selectQrCode(select_obj, metaIdx); break;
case 9: // 条形码
$("#component_type").text(language_str("barc"));
selectObject(select_obj, metaIdx); selectBarCode(select_obj, metaIdx); break;
case 10: // 计数器
$("#component_type").text(language_str("counter"));
selectObject(select_obj, metaIdx); selectCounter(select_obj, metaIdx); break;
case 11: // 圆形文本
$("#component_type").text(language_str("ctext"));
selectObject(select_obj, metaIdx); selectCircleText(select_obj, metaIdx); break;
}
}
selectObj(metaIdx, true);
} else {
// --- 模板背景(canvas[0] 且不在 objs 中)---
$("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide();
$("#component_type").text(language_str("bg"));
selectObj(-1, true);
}
}
function handleObjectMoving(e) {
let obj = e.target;
// 跳过辅助线
if (obj.isGuideLine) {
updateControls();
return;
}
let idx = getIndex(obj, canvas);
// 圆形文本特殊处理
// 安全检查:确保 objs[idx - 1] 存在且有 type 属性
if (idx > 0 && idx <= objs.length && objs[idx - 1] && typeof objs[idx - 1].type !== 'undefined' && objs[idx - 1].type === 11 && $("#circle_text_center").prop("checked")) {
let pointer = { x: obj.left, y: obj.top };
let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2);
let angleDeg = getDeg(pointer);
let flipped = pointer.y > canvas.height / 2 ? true : false;
if (typeof circle_text_diameter !== 'undefined' && circle_text_diameter.setValue) {
circle_text_diameter.setValue(distance * 2 - 50);
}
obj.set("diameter", distance * 2);
obj.set("angle", angleDeg);
obj.set("flipped", flipped);
}
updateControls();
}
// 辅助函数:检查对象是否被锁定
// 只有当 lockMovementX 为 true 时才认为对象被锁定
// selectable 为 false 可能是其他原因(如背景图),所以只检查 lockMovementX
function isObjectLocked(obj) {
if (!obj) return false;
// 主要检查 lockMovementX,这是锁定的主要标志
return obj.get('lockMovementX') === true;
}
// 暴露到全局作用域,确保其他模块可以访问
window.isObjectLocked = isObjectLocked;
// 统一的 toJSON 属性列表,不包含锁定相关属性(锁定状态不应被保存)
// 包含所有需要保存的属性,特别是自定义对象的属性(如 CurvedText 的 diameter, flipped
const TO_JSON_PROPERTIES = [
"selectable", "hoverable", "hoverCursor", "text", "fontStyle", "fontWeight", "underline",
"evented", "linethrough", "textBackgroundColor", "diameter", "flipped"
];
// 暴露到全局作用域
window.TO_JSON_PROPERTIES = TO_JSON_PROPERTIES;
// 辅助函数:解锁所有对象(加载时使用)
function unlockAllObjects(canvas) {
if (!canvas) return;
const objects = canvas.getObjects();
objects.forEach((obj, index) => {
// 背景对象(索引0)保持锁定状态
if (index === 0) {
obj.set({
lockMovementX: true,
lockMovementY: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
lockSkewingX: true,
lockSkewingY: true,
selectable: false,
evented: false
});
} else {
// 其他对象默认解锁
obj.set({
lockMovementX: false,
lockMovementY: false,
lockRotation: false,
lockScalingX: false,
lockScalingY: false,
lockSkewingX: false,
lockSkewingY: false,
selectable: true,
evented: true
});
}
});
}
// 暴露到全局作用域
window.unlockAllObjects = unlockAllObjects;
// 将 handleObjectSelected 附加到 window 对象,确保全局可访问
window.handleObjectSelected = handleObjectSelected;
// 防止重复初始化事件监听器
var _canvasEventsInitialized = false;
function initCanvasEvents() {
// 如果已经初始化过,直接返回
if (_canvasEventsInitialized) {
return;
}
_canvasEventsInitialized = true;
[canvas1, canvas2].forEach(currentCanvas => {
// 1. 鼠标移动
currentCanvas.on("mouse:move", (e) => {
let offsetX = 0, offsetY = 0;
if (currentCanvas === canvas2 && typeof background_image !== 'undefined') {
offsetX = background_image.left;
offsetY = background_image.top;
}
if (e.pointer) {
$("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0));
}
});
// 2. 鼠标移出
currentCanvas.on("mouse:out", () => {
// 可以在这里隐藏标尺辅助线
});
// 3. 鼠标松开 (添加对象)
currentCanvas.on("mouse:up", (e) => {
if (addState === 0) return;
const ptr = e.pointer;
switch (addState) {
case 1: addPic(ptr); break;
case 2: addOutPic(ptr); break;
case 3: addText(ptr); break;
case 4: addOutText(ptr); break;
case 5: addRect(ptr); break;
case 6: addCircle(ptr); break;
case 7: addLine(ptr); break;
case 8: addQrCode(ptr); break;
case 9: addBarCode(ptr); break;
case 10: addCounter(ptr); break;
case 11: addCircleText(ptr); break;
}
addState = 0;
if (typeof background_image !== 'undefined') background_image.hoverCursor = "default";
currentCanvas.renderAll();
$("#addPic, #addOutPic, #addText, #addOutText, #addRect, #addCircle, #addLine, #addQrCode, #addBarCode, #addCounter, #addCircleText").css("background-color", "");
});
// 4. 选中处理
currentCanvas.on('mouse:down', (e) => {
// 更新全局 canvas 变量
canvas = currentCanvas;
objs = (currentCanvas === canvas1) ? objs1 : objs2;
handleObjectSelected(e);
});
// 阻止被锁定的对象被框选选中,同时彻底排除辅助线
currentCanvas.on('before:selection:created', (e) => {
if (!e.targets || e.targets.length === 0) return;
// 调试日志:原始选区
console.log('[Guide] before:selection:created raw targets', e.targets.map(t => ({
isGuideLine: !!t.isGuideLine,
type: t.type,
left: t.left,
top: t.top
})));
// 选区里彻底排除辅助线:不允许辅助线进入 ActiveSelection
// 只保留未锁定的普通对象
e.targets = e.targets.filter(target => {
if (target.isGuideLine) return false; // 完全移除辅助线
return !isObjectLocked(target); // 过滤掉被锁定的普通对象
});
// 调试日志:过滤后的选区
console.log('[Guide] before:selection:created filtered targets', e.targets.map(t => ({
isGuideLine: !!t.isGuideLine,
type: t.type,
left: t.left,
top: t.top
})));
});
// 添加 selection 事件以确保对象选中时正确显示属性面板
// 注意:真正排除辅助线的逻辑已经在 before:selection:created 里完成,
// 这里不再对 ActiveSelection 做任何重建,避免 Fabric 内部递归导致栈溢出。
currentCanvas.on('selection:created', (e) => {
canvas = currentCanvas;
objs = (currentCanvas === canvas1) ? objs1 : objs2;
handleObjectSelected(e);
});
currentCanvas.on('selection:updated', (e) => {
canvas = currentCanvas;
objs = (currentCanvas === canvas1) ? objs1 : objs2;
handleObjectSelected(e);
});
currentCanvas.on('object:selected', (e) => {
canvas = currentCanvas;
objs = (currentCanvas === canvas1) ? objs1 : objs2;
// 如果不是通过对象列表选中的,且对象被锁定,则取消选中
// 注意:只有真正被锁定的对象才会被取消选中,未锁定的对象应该正常选中
if (!window._selectingFromList && e.target && isObjectLocked(e.target)) {
currentCanvas.discardActiveObject();
currentCanvas.renderAll();
return;
}
handleObjectSelected(e);
// 更新对象列表以同步锁状态
if (typeof window.updateList === 'function') {
window.updateList();
}
});
// 5. 对象移动
currentCanvas.on("object:moving", (e) => {
// 阻止背景图(索引0)被移动(如果已锁定)
let movingObj = e.target;
let idx = getIndex(movingObj, currentCanvas);
if (idx === 0) {
let bgObj = currentCanvas.getObjects()[0];
if (bgObj && bgObj.get('lockMovementX') === true) {
e.e.preventDefault();
e.e.stopPropagation();
return false;
}
}
handleObjectMoving(e);
});
// 6. 变换事件
currentCanvas.on("object:rotating", updateControls);
currentCanvas.on("object:scaling", updateControls);
currentCanvas.on("object:rotated", () => { recordState(); });
currentCanvas.on('text:editing:entered', () => { _textEdit = true; });
currentCanvas.on('text:editing:exited', () => { _textEdit = false; });
currentCanvas.on("text:changed", (e) => { $("#text_text").val(e.target.get("text")); });
// 7. 对象修改后
currentCanvas.on('object:modified', function (event) {
if (currentCanvas.getActiveObjects().length > 1) return;
// 跳过辅助线
if (event.target.isGuideLine) {
return;
}
let idx = getIndex(event.target, currentCanvas);
if (idx <= 0) return;
let objData = objs[idx - 1];
// 安全检查:确保 objData 存在且有 type 属性
if (!objData || typeof objData.type === 'undefined') {
return;
}
let target = event.target;
var modAction = event.action || (event.transform && event.transform.action);
if ([3, 4, 10].includes(objData.type) && modAction === 'scale') {
target.set({
fontSize: Math.round(target.fontSize * (target.scaleX || 1)),
scaleX: 1,
scaleY: 1,
});
target._clearCache();
currentCanvas.renderAll();
} else if (objData.type == 5 && modAction === 'scale') {
target.width *= target.scaleX || 1;
target.height *= target.scaleY || 1;
target.scaleX = 1;
target.scaleY = 1;
}
recordState();
updateControls();
// 更新对象列表以同步锁状态
if (typeof window.updateList === 'function') {
window.updateList();
}
});
});
}
// 将 updateList 附加到 window 对象,确保全局可访问
window.updateList = function updateList() {
// 确保使用正确的 objs(优先使用 window.objs,因为它可能被撤回/重做更新)
const currentObjs = window.objs || objs;
const currentCanvas = window.canvas || canvas;
let str = "";
let i = 0;
// 定义行样式:Flex布局,两端对齐,相对定位防止溢出
const rowStyle = "display:flex; justify-content:space-between; align-items:center; height:32px; padding:0 5px; cursor:pointer; border-bottom:1px solid #444; color:#ccc; font-size:12px; position:relative;";
// 定义左侧内容样式
const leftStyle = "display:flex; align-items:center; overflow:hidden; white-space:nowrap; max-width:180px;";
// 定义序号样式:靠右,有左边框
const indexStyle = "min-width:24px; text-align:center; padding-left:5px; color:#666; border-left:1px solid #555;";
for (let item of currentObjs) {
let displayIndex = i + 1; // 序号底层为1
// 图标逻辑
let iconName = "rect.svg";
switch (item.type) {
case 1: iconName = "images.svg"; break;
case 2: iconName = "images_add.svg"; break;
case 3: iconName = "text.svg"; break;
case 4: iconName = "text2.svg"; break;
case 5: iconName = "rect.svg"; break;
case 6: iconName = "round.svg"; break;
case 7: iconName = "line.svg"; break;
case 8: iconName = "qrcode.svg"; break;
case 9: iconName = "barcode.svg"; break;
case 10: iconName = "counter.svg"; break;
case 11: iconName = "text3.svg"; break;
}
// 名称逻辑
let nameText = item.name ? item.name :
(item.type === 3 ? "TEXT_" + displayIndex :
(item.type === 1 ? "IMG_" + displayIndex : "OBJ_" + displayIndex));
// 检查锁定状态(通过lockMovementX判断,因为锁定时会设置所有lock属性)
// 注意:索引0是背景图,objs数组是从下标0开始存第1个对象的,所以是 i+1
let isLocked = false;
var canvasObj = typeof window.soonCanvasObjForListIndex === 'function'
? window.soonCanvasObjForListIndex(i)
: currentCanvas.getObjects()[i + 1];
if (canvasObj) {
isLocked = canvasObj.get('lockMovementX') === true;
}
// 锁定状态图标
const lockIcon = isLocked ?
`<img src="${soonAsset('lock20.png')}" style="width:14px; height:14px; margin-left:5px; opacity:0.7;" title="已锁定">` :
`<img src="${soonAsset('unlock20.png')}" style="width:14px; height:14px; margin-left:5px; opacity:0.5;" title="未锁定">`;
str = `<div style="${rowStyle}">
<div style="${leftStyle}">
<img src="${soonAsset(iconName)}" id="obj${i}" style="width:16px; margin-right:5px;">
<span title="${nameText}">${nameText}</span>
${lockIcon}
</div>
<span style="${indexStyle}">${displayIndex}</span>
</div>` + str;
i++;
}
$(".obj_list").html(str);
// 重新绑定点击事件(防止 updateList 后点击失效)
$(".obj_list > div").on("click", function () {
let imgId = $(this).find("img[id^='obj']").attr("id");
if (imgId) {
let idx = parseInt(imgId.replace("obj", ""), 10);
selectObj(idx);
}
});
}
// 标记是否通过对象列表选中(用于区分鼠标点击和列表点击)
window._selectingFromList = false;
// 对象列表下标 → 画布对象(设为背景后 objs 与 canvas 一一对应)
function soonCanvasObjForListIndex(listIdx) {
var c = window.canvas || canvas;
var metaList = window.objs || objs;
if (!c || listIdx < 0 || listIdx >= metaList.length) return null;
var all = c.getObjects().filter(function (o) { return !o.isGuideLine; });
var bgListedInObjs = metaList.length >= all.length;
if (bgListedInObjs) {
return all[listIdx] || null;
}
for (var j = 0; j < all.length; j++) {
var o = all[j];
var ci = getIndex(o, c);
if (ci > 0 && ci - 1 === listIdx) return o;
}
return null;
}
window.soonCanvasObjForListIndex = soonCanvasObjForListIndex;
/** 画布选中对象 → objs 下标;-1 表示模板背景(不在 objs 中) */
function soonDesign2ResolveObjMetaIndex(select_obj, fabricCanvas) {
if (!select_obj || !fabricCanvas) return -1;
var metaList = fabricCanvas === canvas2 ? (window.objs2 || objs2) : (window.objs1 || objs1);
if (fabricCanvas === (window.canvas || canvas)) {
metaList = window.objs || objs || metaList;
}
if (!metaList || !metaList.length) return -1;
for (var li = 0; li < metaList.length; li++) {
if (soonCanvasObjForListIndex(li) === select_obj) return li;
}
return -1;
}
window.soonDesign2ResolveObjMetaIndex = soonDesign2ResolveObjMetaIndex;
// 将 selectObj 附加到 window 对象,确保全局可访问
window.selectObj = function selectObj(objIndices, isFromCanvas = false) {
let indicesArray = (Array.isArray(objIndices)) ? objIndices : ((typeof objIndices === 'number' && objIndices >= 0) ? [objIndices] : []);
// 清除所有高亮
$('.obj_list > div').css("background-color", "transparent");
// 遍历列表查找并高亮
$('.obj_list > div').each(function () {
let $img = $(this).find("img[id^='obj']");
if ($img.length === 0) return;
let idIndex = parseInt($img.attr("id").replace("obj", ""), 10);
if (indicesArray.includes(idIndex)) {
$(this).css("background-color", "#6F7A84");
// 自动滚动到可视区域
if (isFromCanvas && indicesArray[0] === idIndex) {
this.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
});
// 如果是点击列表,触发画布选中
if (!isFromCanvas && indicesArray.length === 1) {
let target = soonCanvasObjForListIndex(indicesArray[0]);
if (target) {
// 检查对象是否被锁定
let isLocked = isObjectLocked(target);
// 设置标志位,表示这是通过对象列表选中的(无论是否锁定)
window._selectingFromList = true;
// 如果被锁定,临时设置 selectable 和 evented 为 true,以便能够选中
if (isLocked) {
target.set({
selectable: true,
evented: true
});
}
canvas.setActiveObject(target);
canvas.requestRenderAll();
// 如果对象被锁定,选中后立即恢复锁定状态(但保持选中状态)
if (isLocked) {
setTimeout(() => {
target.set({
selectable: false,
evented: false
});
canvas.requestRenderAll();
}, 0);
}
// 清除标志位(延迟清除,确保事件处理完成)
setTimeout(() => {
window._selectingFromList = false;
}, 10);
handleObjectSelected();
}
}
}
// 将 recordState 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问
window.recordState = function recordState() {
// 撤销/重做过程中不记录历史,避免产生“中间状态”,导致需要点多次才能回到预期
if (window._isApplyingHistory) {
return;
}
// 确定当前是正面还是背面,直接使用对应的数组
const isFront = $('#front_side').hasClass('ui-button-active');
const currentStep = isFront ? (window.step1 || step1) : (window.step2 || step2);
const currentRecordObjs = isFront ? (window.recordObjs1 || recordObjs1) : (window.recordObjs2 || recordObjs2);
const currentRecordJson = isFront ? (window.recordJson1 || recordJson1) : (window.recordJson2 || recordJson2);
const currentObjs = window.objs || objs;
const currentCanvas = window.canvas || canvas;
++currentStep.val;
for (let i = currentStep.val; i < currentRecordObjs.length; i++) {
currentRecordObjs.pop();
currentRecordJson.pop();
}
currentRecordObjs.push(JSON.stringify(currentObjs))
// 保存所有锁定相关的属性,确保锁定状态可以正确恢复
let j = currentCanvas.toJSON(TO_JSON_PROPERTIES);
j.objects[0].hoverCursor = "default";
currentRecordJson.push(j);
// 同步更新 window 变量和局部变量
if (isFront) {
window.step1 = currentStep;
window.recordObjs1 = currentRecordObjs;
window.recordJson1 = currentRecordJson;
window.step = currentStep;
window.recordObjs = currentRecordObjs;
window.recordJson = currentRecordJson;
step1 = currentStep;
recordObjs1 = currentRecordObjs;
recordJson1 = currentRecordJson;
step = currentStep;
recordObjs = currentRecordObjs;
recordJson = currentRecordJson;
} else {
window.step2 = currentStep;
window.recordObjs2 = currentRecordObjs;
window.recordJson2 = currentRecordJson;
window.step = currentStep;
window.recordObjs = currentRecordObjs;
window.recordJson = currentRecordJson;
step2 = currentStep;
recordObjs2 = currentRecordObjs;
recordJson2 = currentRecordJson;
step = currentStep;
recordObjs = currentRecordObjs;
recordJson = currentRecordJson;
}
};
// 向后兼容
function recordState() {
return window.recordState();
}
// 将 getIndex 附加到 window 对象,确保在 eval() 加载 .jsc 文件时也能访问
window.getIndex = function getIndex(target, _canvas = null) {
let temp_objs;
// 使用 window.canvas 作为默认值,确保在 eval() 执行时也能访问
const currentCanvas = _canvas || window.canvas || canvas;
if (!currentCanvas) {
return 0;
}
temp_objs = currentCanvas.getObjects();
let i = 0;
for (let res of temp_objs) {
if (target == res) {
return i;
}
i++;
}
return 0;
};
// 向后兼容
function getIndex(target, _canvas = null) {
return window.getIndex(target, _canvas);
}
function getType(target) {
// 如果是辅助线,返回 null
if (target.isGuideLine) {
return null;
}
let temp_objs = canvas.getObjects();
let i = 0;
for (let res of temp_objs) {
if (target == res) {
// 安全检查:确保 objs[i] 存在且有 type 属性
if (!objs[i] || typeof objs[i].type === 'undefined') {
return null;
}
return objs[i].type;
}
i++;
}
return null;
}
function checkName(name) {
//检查字段名是否重复了
for (let item of objs1) {
if (item.name == name) {
return false;
}
}
for (let item of objs2) {
if (item.name == name) {
return false;
}
}
return true;
}
function resName(pre_name) {
//返回的是不重复的字段名
let num = (objs1.length + objs2.length) - 1;
do {
num++;
}
while (!checkName(pre_name + num));//重复了就再加1
return pre_name + num;
}
function getDisdance(x1, y1, x2, y2) {
var dx = Math.abs(x2 - x1); // 计算x轴上的距离差,并取绝对值
var dy = Math.abs(y2 - y1); // 计算y轴上的距离差,并取绝对值
var distance = Math.sqrt(dx * dx + dy * dy); // 应用勾股定理计算距离
return distance;
}
function getDeg(pointer) {
// 计算点击位置与画布中心的坐标差
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var mouseX = pointer.x;
var mouseY = pointer.y;
var dx = mouseX - centerX;
var dy = mouseY - centerY;
// 计算旋转角度(弧度)
var angleRad = Math.atan2(dy, dx);
// 将角度转换为度数
var angleDeg = angleRad * (180 / Math.PI);
if (angleDeg < 0) {
angleDeg += 360;
}
angleDeg += 90
return angleDeg
}
function getCoordsMinX(acoords) {
x = acoords[0].x;
for (let item of acoords) {
if (item.x < x) {
x = item.x
}
}
return x;
}
function getCoordsMaxX(acoords) {
x = acoords[0].x;
for (let item of acoords) {
if (item.x > x) {
x = item.x
}
}
return x;
}
function getCoordsMinY(acoords) {
y = acoords[0].y;
for (let item of acoords) {
if (item.y < y) {
y = item.y
}
}
return y;
}
function getCoordsMaxY(acoords) {
y = acoords[0].y;
for (let item of acoords) {
if (item.y > y) {
y = item.y
}
}
return y;
}
function soonPlaceBgOnCanvas(image, canvasSelector) {
var cw = $(canvasSelector).width() || (document.querySelector(canvasSelector) && document.querySelector(canvasSelector).clientWidth) || 800
var ch = $(canvasSelector).height() || (document.querySelector(canvasSelector) && document.querySelector(canvasSelector).clientHeight) || 600
image.left = (cw - image.width * image.get('scaleX')) / 2
image.top = (ch - image.height * image.get('scaleY')) / 2
image.selectable = false
image.hoverable = false
image.hoverCursor = 'default'
var fc = (canvasSelector === '#canvas2') ? canvas2 : canvas1;
if (fc && typeof soonDesign2SyncFrameAnchor === 'function') {
soonDesign2SyncFrameAnchor(fc, {
left: image.left,
top: image.top,
width: image.width * (image.get('scaleX') || 1),
height: image.height * (image.get('scaleY') || 1)
});
}
}
function soonOpenPendingDesignFile() {
var file = typeof window.soonResolveOpenFileKey === 'function'
? window.soonResolveOpenFileKey(GetFile)
: (GetFile().get('file') || '');
if (file && file != 'empty') {
if (typeof window.soonConsumeOpenMeta === 'function') {
window.soonConsumeOpenMeta(file);
}
window.openFile(file);
}
}
function addBackground() {
if (background_image1 && background_image2) return;
var loadBg = typeof window.soonFabricImageFromAsset === 'function'
? window.soonFabricImageFromAsset
: null;
var whenReady = typeof window.soonRunWhenCanvasReady === 'function'
? window.soonRunWhenCanvasReady
: function (_sel, fn) { fn(); };
var props = window.TO_JSON_PROPERTIES || ['selectable', 'hoverable', 'hoverCursor', 'text', 'fontStyle', 'fontWeight', 'underline', 'evented', 'linethrough', 'textBackgroundColor', 'diameter', 'flipped'];
function finishBack(backImg) {
if (backImg) {
soonPlaceBgOnCanvas(backImg, '#canvas2');
background_image2 = backImg;
window.background_image2 = backImg;
canvas2.add(backImg);
canvas2.renderAll();
recordObjs2.push(JSON.stringify(objs2));
recordJson2.push(canvas2.toJSON(TO_JSON_PROPERTIES));
}
soonOpenPendingDesignFile();
}
function startLoad() {
if (!loadBg) return;
loadBg('bg_front_2.png', function (frontImg) {
if (!frontImg) return;
soonPlaceBgOnCanvas(frontImg, '#canvas1');
background_image1 = frontImg;
background_image = frontImg;
window.background_image1 = frontImg;
window.background_image = frontImg;
canvas1.add(frontImg);
canvas1.renderAll();
recordObjs1.push(JSON.stringify(objs1));
recordJson1.push(canvas1.toJSON(props));
loadBg('bg_back_2.png', finishBack, function () { loadBg('bg_back_2.png', finishBack, null, true); }, true);
}, function () {
setTimeout(startLoad, 200);
}, null, true);
}
whenReady('#canvas1', startLoad);
}
function addPic(pointer) {
pre_add_image.left = pointer.x
pre_add_image.top = pointer.y
canvas.add(pre_add_image)
objs.push({ type: 1, black: false })
canvas.renderAll()
updateList()
recordState()
}
function addOutPic(pointer) {
fabric.Image.fromURL(soonAsset('outpic_extra.png'), function (image) {
let width = canvas.getWidth()
let height = canvas.getHeight()
image.left = pointer.x
image.top = pointer.y
image.opacity = 1
image.setSrc(image.toDataURL(), function (image) {
canvas.add(image)
objs.push({ type: 2, name: resName('OUT_PIC_'), black: false })
canvas.renderAll()
updateList()
recordState()
})
})
}
function addText(pointer) {
// 设置默认字号为30px(像素值),这样显示的字号约为 30 * 72 / 300 = 7.2pt
let text = new fabric.IText('TEXT', {
left: pointer.x,
top: pointer.y,
fill: '#000000',
textBackgroundColor: '',
fontStyle: 'normal',
fontWeight: 'normal',
fontSize: 30,
underline: false,
linethrough: false,
fontFamily: 'Arial'
})
text.setControlVisible('ml', false)
text.setControlVisible('mt', false)
text.setControlVisible('mr', false)
text.setControlVisible('mb', false)
canvas.add(text)
objs.push({ type: 3, black: false })
canvas.renderAll()
updateList()
recordState()
}
function addCircleText(pointer) {
let distance = getDisdance(pointer.x, pointer.y, canvas.width / 2, canvas.height / 2)
let angleDeg = getDeg(pointer)
let text = new fabric.CurvedText('Circle TEXT', {
left: pointer.x,
top: pointer.y,
fill: "#000000",
textBackgroundColor: "",
fontStyle: "normal",
fontWeight: "normal",
diameter: distance * 2,
fontSize: 30,
underline: false,
linethrough: false,
lockScalingX: true,
lockScalingY: true,
fontFamily: 'Arial',
originX: 'center',
originY: 'center',
angle: angleDeg,
flipped: pointer.y > canvas.height / 2 ? true : false
});
text.setControlVisible("ml", false);
text.setControlVisible("mt", false);
text.setControlVisible("mr", false);
text.setControlVisible("mb", false);
canvas.add(text);
objs.push({ type: 11, black: false, circleCenter: 0 });
canvas.renderAll();
updateList();
recordState();
}
function addOutText(pointer) {
let text = new fabric.Text('MERGE TEXT', {
left: pointer.x,
top: pointer.y,
fill: '#000000',
fontStyle: 'normal',
fontWeight: 'normal',
textBackgroundColor: '',
underline: false,
linethrough: false,
fontFamily: 'Arial',
textAlign: 'center'
})
text.setControlVisible('ml', false)
text.setControlVisible('mt', false)
text.setControlVisible('mr', false)
text.setControlVisible('mb', false)
canvas.add(text)
objs.push({ type: 4, name: resName('OUT_TEXT_'), exceed: false, autoWrap: false, fixSize: false, black: false, align: 'center' })
canvas.renderAll()
updateList()
recordState()
}
function addRect(pointer) {
let rect = new fabric.Rect({
left: pointer.x,
top: pointer.y,
fill: '',
stroke: "#ff0000",
strokeWidth: 3,
width: 100,
height: 100
})
canvas.add(rect)
objs.push({ type: 5, black: false })
canvas.renderAll()
updateList()
recordState()
}
function addCircle(pointer) {
let circle = new fabric.Circle({
left: pointer.x,
top: pointer.y,
fill: '',
stroke: '#ff0000',
strokeWidth: 3,
radius: 100,
strokeUniform: true
})
canvas.add(circle)
objs.push({ type: 6, black: false })
canvas.renderAll()
updateList()
recordState()
}
function addLine(pointer) {
let line = new fabric.Line([50, 100, 200, 100], {
left: pointer.x,
top: pointer.y,
stroke: '#ff0000',
strokeWidth: 3,
strokeDashArray: [0, 0],
})
line.setControlVisible('tl', false)
line.setControlVisible('tr', false)
line.setControlVisible('br', false)
line.setControlVisible('bl', false)
line.setControlVisible('mt', false)
line.setControlVisible('mb', false)
canvas.add(line)
objs.push({ type: 7, black: false })
canvas.renderAll()
updateList()
recordState()
}
function addQrCode(pointer) {
let qr = jrQrcode.getQrBase64('https://www.cardsoon.com', { padding: 0 })
fabric.Image.fromURL(qr, function (image) {
image.left = pointer.x
image.top = pointer.y
canvas.add(image)
objs.push({ type: 8, name: resName('QR_CODE_'), color: '#000000', val: 'http://www.cardsoon.com', black: false })
canvas.renderAll()
updateList()
recordState()
})
}
function addBarCode(pointer) {
JsBarcode('#barcode', '123456789', { margin: 0, lineColor: '#000000', fontSize: 18, font: 'Arial' })
let barcode = document.getElementById('barcode')
let bar = barcode.toDataURL('image/png')
fabric.Image.fromURL(bar, function (image) {
image.left = pointer.x
image.top = pointer.y
canvas.add(image)
objs.push({ type: 9, name: resName('BAR_CODE_'), val: '123456789', color: '#000000', fontSize: 18, font: 'Arial', show: false, black: false })
canvas.renderAll()
updateList()
recordState()
})
}
function addCounter(pointer) {
let text = new fabric.Text('0', {
left: pointer.x,
top: pointer.y,
fill: '#000000'
})
canvas.add(text)
objs.push({ type: 10, name: resName('COUNTER_'), start: 0, add: 1, black: false })
canvas.renderAll()
updateList()
recordState()
}
var front_list = []
function applyFontList(data) {
var newFontList = []
;(data || []).map(function(item) {
if (item.indexOf('"') === 0) {
newFontList.push(item.replace(/^"|"$/g, ''))
} else {
newFontList.push(item)
}
})
front_list = newFontList
for (var i = 0; i < front_list.length; i++) {
var item = front_list[i]
$('#text_font_family').append("<option value='" + item + "'>" + item + '</option>')
$("#circle_text_font_family").append("<option value='" + item + "'>" + item + "</option>")
$('#out_text_font_family').append("<option value='" + item + "'>" + item + '</option>')
$('#barcode_font_family').append("<option value='" + item + "'>" + item + '</option>')
$('#count_font_family').append("<option value='" + item + "'>" + item + '</option>')
}
}
if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getSystemFonts) {
window.platformBridge.getSystemFonts().then(applyFontList)
} else if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('font-list', function(event, data) { applyFontList(data); })
}
function applySysLan(data) {
let lang = localStorage.getItem('lang')
if (lang) {
s_lan = lang
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge(lang)
$('#language_select').val(lang)
return
}
switch (data) {
case 'zh-CN':
s_lan = 'zh'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('zh')
$('#language_select').val('zh')
localStorage.setItem('lang', 'zh')
break
case 'zh':
s_lan = 'zh'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('zh')
$('#language_select').val('zh')
localStorage.setItem('lang', 'zh')
break
case 'zh-TW':
case 'ozh':
s_lan = 'ozh'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('ozh')
$('#language_select').val('ozh')
localStorage.setItem('lang', 'ozh')
break
case 'en':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-AU':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-CA':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-GB':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-NZ':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-US':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
case 'en-ZA':
s_lan = 'en'
// 同步到全局作用域(用于 .jsc 文件加载)
if (typeof window !== 'undefined') {
window.s_lan = s_lan;
}
if (typeof global !== 'undefined') {
global.s_lan = s_lan;
}
langua_ge('en')
$('#language_select').val('en')
localStorage.setItem('lang', 'en')
break
}
}
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('sys-lan', function(event, data) { applySysLan(data); })
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.getLocale) {
applySysLan(window.platformBridge.getLocale())
}
function onCloseConfirm() {
layer.confirm(
language_str('whetherSave'),
{
btn: [language_str('save'), language_str('noSave'), language_str('cancel')],
btn3: function (index, layero) { }
},
function (index, layero) {
if (typeof window.output === 'function') {
window.output(function() {
if (ipcRenderer) ipcRenderer.send('run-close')
else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose()
})
}
},
function (index) {
if (ipcRenderer) ipcRenderer.send('run-close')
else if (window.platformBridge && window.platformBridge.runClose) window.platformBridge.runClose()
}
)
}
if (typeof ipcRenderer !== 'undefined' && ipcRenderer) {
ipcRenderer.on('close', function(event, message) { onCloseConfirm(); })
} else if (typeof window !== 'undefined' && window.platformBridge && window.platformBridge.onClose) {
window.platformBridge.onClose(onCloseConfirm)
}
initAligningGuidelines(canvas1)
initAligningGuidelines(canvas2)
// 初始化 Canvas 事件监听
initCanvasEvents();
// 初始化所有 Slider
var circle_text_size_updating = false; // 标志位,防止循环更新
var circle_text_size = slider.render({
elem: '#circle_text_size',
min: 10,
max: 120,
value: 50,
change: function (value) {
if (circle_text_size_updating) return;
if (canvas && canvas.getActiveObject && canvas.getActiveObject()) {
canvas.getActiveObject().set('fontSize', value);
canvas.renderAll();
if (typeof updateControls === 'function') {
updateControls();
}
if (typeof recordState === 'function') {
recordState();
}
}
}
});
var circle_text_diameter = slider.render({
elem: '#circle_text_diameter',
min: 50,
max: 1100,
value: 500,
change: function (value) {
if (canvas && canvas.getActiveObject && canvas.getActiveObject()) {
canvas.getActiveObject().set('diameter', value);
canvas.renderAll();
if (typeof updateControls === 'function') {
updateControls();
}
if (typeof recordState === 'function') {
recordState();
}
}
}
});
var out_pic_op = slider.render({
elem: '#out_pic_op',
min: 0,
max: 100,
value: 0,
change: function (value) {
if (canvas.getActiveObject()) {
canvas.getActiveObject().set('opacity', 1 - value / 100);
canvas.renderAll();
window._soonSyncingFontControls = true;
try {
updateControls();
} finally {
window._soonSyncingFontControls = false;
}
recordState();
}
}
});
var pic_op = slider.render({
elem: '#pic_op',
min: 0,
max: 100,
value: 0,
change: function (value) {
if (canvas.getActiveObject()) {
canvas.getActiveObject().set('opacity', 1 - value / 100);
canvas.renderAll();
window._soonSyncingFontControls = true;
try {
updateControls();
} finally {
window._soonSyncingFontControls = false;
}
recordState();
}
}
});
var rect_op = slider.render({
elem: '#rect_op',
min: 0,
max: 100,
value: 0,
change: function (value) {
if (canvas.getActiveObject()) {
canvas.getActiveObject().set('opacity', 1 - value / 100);
canvas.renderAll();
updateControls();
recordState();
}
}
});
var circle_op = slider.render({
elem: '#circle_op',
min: 0,
max: 100,
value: 0,
change: function (value) {
if (canvas.getActiveObject()) {
canvas.getActiveObject().set('opacity', 1 - value / 100);
canvas.renderAll();
updateControls();
recordState();
}
}
});
//canvas.setZoom(0.5)
//recordObjs1.push(JSON.stringify(objs1));
//recordJson1.push(canvas1.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]));
//recordObjs2.push(JSON.stringify(objs2));
//recordJson2.push(canvas2.toJSON(["selectable","hoverable","hoverCursor","text","fontStyle","fontWeight","underline"]));
canvas.renderAll() //重新渲染画布
var zoomSlider = slider.render({
elem: '#zoomSlide', //绑定元素
min: 25,
max: 500,
value: 100,
change(val) {
canvas.zoomToPoint(new fabric.Point(canvas.width / 2, canvas.height / 2), val / 100)
zoom = val / 100
//$(".canvas-container").css("transform","scale(" + val / 100 + ")")
}
})
window.rotate = function (id) {
let angle = $(`#${id}`).attr('angle') - 0
angle += 90
$(`#${id}`).css('transform', `rotate(${angle}deg)`)
$(`#${id}`).attr('angle', angle)
}
// 注意:#display 事件绑定已在 ui.js 中定义(第2633行),此处已删除重复定义
// 导出文件
// 将 savePdf 附加到 window 对象,确保在 output.js 中也能访问
window.savePdf = async function savePdf(bufferOrBlob) {
var dialogApi = (typeof dialog !== 'undefined' && dialog) ? dialog : (window.platformBridge && window.platformBridge.showSaveDialog ? { showSaveDialog: function(opts) { return window.platformBridge.showSaveDialog(opts); } } : null);
if (!dialogApi) return;
var defaultPath = (typeof path !== 'undefined' && path && exePath) ? path.join(exePath, 'output.pdf') : 'output.pdf';
var result = await dialogApi.showSaveDialog({
title: '保存文件',
defaultPath: defaultPath,
filters: [{ name: 'PDF 文件', extensions: ['pdf'] }]
});
var canceled = result.canceled;
var filePath = result.filePath || (result.fileHandle && result.fileHandle.name);
if (canceled || (!filePath && !result.fileHandle)) return;
var name = (typeof filePath === 'string' ? filePath.split(/[/\\]/).pop() : 'output.pdf') || 'output.pdf';
if (!/\.pdf$/i.test(name)) name = name + '.pdf';
if (window.platformBridge && window.platformBridge.writeFile) {
var blob = bufferOrBlob instanceof Blob ? bufferOrBlob : (bufferOrBlob && bufferOrBlob.buffer ? new Blob([bufferOrBlob]) : new Blob([bufferOrBlob]));
window.platformBridge.writeFile(result.fileHandle || filePath, blob).then(function() {
layer.msg(language_str("saveSucc") + name);
});
return;
}
var fs = typeof require !== 'undefined' ? require('fs') : null;
if (fs && filePath) {
var buf = bufferOrBlob && bufferOrBlob.buffer ? Buffer.from(bufferOrBlob) : (bufferOrBlob instanceof Uint8Array ? Buffer.from(bufferOrBlob) : bufferOrBlob);
fs.writeFile(filePath, buf, function(err) {
if (!err) layer.msg(language_str("saveSucc") + filePath);
});
}
};
// 向后兼容
async function savePdf(buffer) {
return window.savePdf(buffer);
}
// display_func 函数已移至 output.js,避免重复定义
// function display_func(img1, img2, img3) { ... }
// output 函数已移至 output.js,避免重复定义
// function output(callback = null, _save = save) { ... }
var _keyctrl = false,
_keyc = false,
_textEdit = false,
_inputEdit = false
$('input').focus(function () {
_inputEdit = true
})
$('input').blur(function () {
_inputEdit = false
})
$('textarea').focus(function () {
_inputEdit = true
})
$('textarea').blur(function () {
_inputEdit = false
})
$('body').keyup(function (event) {
// 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta
if (event.keyCode == 17 || event.keyCode == 91 || event.keyCode == 93) { // Ctrl 键或 Meta 键(Mac Cmd)释放
_keyctrl = false
}
if (event.keyCode == 67) {
_keyc = false
}
})
$('body').keydown(function (event) {
// 统一的快捷键处理(兼容 Windows/Linux: Ctrl, Mac: Cmd/Meta
const isModifierKey = event.ctrlKey || event.metaKey; // 兼容三端
// 快捷键处理(在输入框或文本区域中时不触发,除了删除相关的)
if (!_inputEdit && !_textEdit) {
// CTRL/CMD+N - 新建
if (isModifierKey && event.keyCode == 78) {
event.preventDefault();
$('#new').click();
return;
}
// CTRL/CMD+O - 打开
if (isModifierKey && event.keyCode == 79) {
event.preventDefault();
$('#open').click();
return;
}
// CTRL/CMD+S - 保存
if (isModifierKey && !event.shiftKey && event.keyCode == 83) {
event.preventDefault();
$('#save').click();
return;
}
// CTRL/CMD+SHIFT+S - 保存副本
if (isModifierKey && event.shiftKey && event.keyCode == 83) {
event.preventDefault();
if (typeof window.output === 'function') {
window.output(null, saveAs);
}
return;
}
// CTRL/CMD+Z - 还原
if (isModifierKey && !event.shiftKey && event.keyCode == 90) {
event.preventDefault();
$('#previous').click();
return;
}
// CTRL/CMD+Y - 重做
if (isModifierKey && !event.shiftKey && event.keyCode == 89) {
event.preventDefault();
$('#next').click();
return;
}
// CTRL/CMD+C - 复制
if (isModifierKey && event.keyCode == 67 && event.target.nodeName == 'BODY') {
event.preventDefault();
window.copySelection();
return;
}
// CTRL/CMD+V - 粘贴
if (isModifierKey && event.keyCode == 86 && event.target.nodeName == 'BODY') {
event.preventDefault();
window.pasteFromClipboard();
return;
}
// CTRL/CMD+SHIFT+DEL - 清空
if (isModifierKey && event.shiftKey && event.keyCode == 46) {
event.preventDefault();
$('#clean').click();
return;
}
// CTRL/CMD+DEL - 删除
if (isModifierKey && !event.shiftKey && event.keyCode == 46) {
event.preventDefault();
$('#delete').click();
return;
}
}
// 兼容三端的 modifier key 检测(Windows/Linux: Ctrl, Mac: Cmd/Meta
if (event.keyCode == 17 || event.metaKey) { // Ctrl 键或 Meta 键(Mac Cmd
_keyctrl = true
if (_keyc && _textEdit) {
//同时按下且在编辑状态
setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1)
//clipboard.writeText((clipboard.readText()+' '))
}
}
if (event.keyCode == 67) {
_keyc = true
if (_keyctrl & _textEdit) {
//同时按下且在编辑状态
setTimeout("clipboard.writeText( (clipboard.readText()+' ') );", 1)
//clipboard.writeText((clipboard.readText()+' '))
}
}
if (event.keyCode == 37) {
//left
let v = -1
if (_keyctrl || event.ctrlKey || event.metaKey) {
v = -5
}
if (canvas.getActiveObject()) {
let x = canvas.getActiveObject().get('left')
x += v
canvas.getActiveObject().set('left', x)
canvas.renderAll()
recordState()
updateControls()
}
}
if (event.keyCode == 38) {
//up
let v = -1
if (_keyctrl || event.ctrlKey || event.metaKey) {
v = -5
}
if (canvas.getActiveObject()) {
let y = canvas.getActiveObject().get('top')
y += v
canvas.getActiveObject().set('top', y)
canvas.renderAll()
recordState()
updateControls()
}
}
if (event.keyCode == 39) {
//right
let v = 1
if (_keyctrl || event.ctrlKey || event.metaKey) {
v = 5
}
if (canvas.getActiveObject()) {
let x = canvas.getActiveObject().get('left')
x += v
canvas.getActiveObject().set('left', x)
canvas.renderAll()
recordState()
updateControls()
}
}
if (event.keyCode == 40) {
//down
let v = +1
if (_keyctrl || event.ctrlKey || event.metaKey) {
v = 5
}
if (canvas.getActiveObject()) {
let y = canvas.getActiveObject().get('top')
y += v
canvas.getActiveObject().set('top', y)
canvas.renderAll()
recordState()
updateControls()
}
}
// 普通删除键(DEL键,没有按CTRL/CMD
if (event.keyCode == 46 && !isModifierKey && !_inputEdit && !_textEdit) {
let actObjs = canvas.getActiveObjects()
let allObjs = canvas.getObjects()
let flag = 0
for (let item of actObjs) {
if (item != allObjs[0]) {
let i = getIndex(item, canvas) //0是背景,所以从1开始,但objs不包含背景图片,故要-1
objs.splice(i - 1, 1)
canvas.remove(item)
flag = 1
}
}
updateList()
canvas.discardActiveObject()
if (flag == 1) {
recordState()
canvas.renderAll()
handleObjectSelected()
}
}
})
// 复制选中对象
window.copySelection = function copySelection() {
const activeObjects = canvas.getActiveObjects();
if (!activeObjects || activeObjects.length === 0) {
layer.msg("未选中任何对象")
return;
}
// 【核心修复】遍历选中的对象,同时保存它的 Fabric 画面数据 和 我们的 objs 业务数据
clipboardData.data = activeObjects.map(obj => {
let idx = getIndex(obj, canvas); // 获取对象在画布中的索引
// 获取业务属性 (objs数组中对应的那一项)
// 注意:idx=0 是背景图,objs数组是从下标0开始存第1个对象的,所以是 idx-1
let appData = (idx > 0 && objs[idx - 1]) ? objs[idx - 1] : { type: 0 };
return {
// 1. 保存画面样式 (坐标、颜色、字号等,包含图片的 src)
fabricJson: obj.toObject(['id', 'name', 'selectable', 'evented', 'data', 'src']),
// 2. 保存业务属性 (深拷贝,防止引用冲突)
appData: JSON.parse(JSON.stringify(appData))
};
});
clipboardData.offset = 10; // 重置偏移量
layer.msg("已复制");
}
// 粘贴对象
window.pasteFromClipboard = function pasteFromClipboard() {
// 检查是否有数据
if (!clipboardData.data || clipboardData.data.length === 0) {
layer.msg("剪贴板为空")
return;
}
// 防止重复调用:如果正在粘贴,直接返回
if (window._isPasting) {
return;
}
window._isPasting = true;
// 1. 提取 Fabric JSON 准备恢复画面
const jsonToEnliven = clipboardData.data.map(item => item.fabricJson);
// 2. 恢复对象 (异步过程)
fabric.util.enlivenObjects(jsonToEnliven, function (objects) {
canvas.discardActiveObject();
objects.forEach((clone, index) => {
// --- A. 处理画面位置 ---
clone.set({
left: clone.left + clipboardData.offset,
top: clone.top + clipboardData.offset,
evented: true
});
// 如果是组内对象,需要特殊处理坐标
if (clone.group) {
clone.set({
left: clone.group.left + clipboardData.offset,
top: clone.group.top + clipboardData.offset
});
}
// --- B. 处理业务数据 ---
// 取出复制时保存的 appData
let savedAppData = clipboardData.data[index].appData;
let newAppData = JSON.parse(JSON.stringify(savedAppData)); // 再次深拷贝
// 修改名称,防止 ID 冲突 (例如 TEXT_1 -> TEXT_1_copy)
if (newAppData.name) {
newAppData.name = newAppData.name + "_copy";
}
// --- C. 添加到画布和数据数组 ---
canvas.add(clone);
objs.push(newAppData); // 将新业务数据推入当前页面的 objs 数组
});
// 3. 选中新粘贴出来的对象
let sel = new fabric.ActiveSelection(objects, { canvas: canvas });
canvas.setActiveObject(sel);
canvas.requestRenderAll();
// 4. 刷新列表和历史记录
if (typeof window.updateList === 'function') {
window.updateList();
}
recordState();
// 5. 高亮左侧列表最后一项 (即新粘贴的项)
if (typeof window.selectObj === 'function') {
if (objs.length > 0) {
window.selectObj(objs.length - 1);
}
}
// 6. 增加下次粘贴的偏移量 (让用户知道粘贴了新东西)
clipboardData.offset += 10;
// 清除粘贴标志
window._isPasting = false;
});
}
// 将 onStateRestored 附加到 window 对象,确保全局可访问
// 此函数在撤回/重做后调用,用于恢复画布状态
window.onStateRestored = function onStateRestored() {
// 1. 重新获取背景图引用 (因为旧对象被销毁了)
// 注意:canvas 是全局变量,指向当前激活的画板
if (canvas && canvas.getObjects().length > 0) {
background_image = canvas.getObjects()[0];
background_image.selectable = false; // 确保背景不可选
window.background_image = background_image;
background_image.evented = false;
}
// 2. 渲染画布
canvas.requestRenderAll();
// 3. 取消选中状态
if (typeof window.selectObj === 'function') {
window.selectObj(-1);
}
// 4. 隐藏所有属性面板
$("#base_control, #line_control, #pic_control, #out_pic_control, #text_control, #circle_text_control, #out_text_control, #rect_control, #circle_control, #counter_control, #qrcode_Controll, #barcode_controll").hide();
// 5. 刷新左侧列表
if (typeof window.updateList === 'function') {
window.updateList();
}
}