2026-03-16 16:51:12 +08:00
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
import { fabric } from 'fabric-with-all'
|
|
|
|
|
|
import { createId } from '../../tools/tools'
|
|
|
|
|
|
import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod'
|
|
|
|
|
|
import { OperationType } from '../tools/layerHelper'
|
2026-04-01 16:03:20 +08:00
|
|
|
|
import { getArrowPath, getLinePath, cloneObjects, getStarArr } from '../tools/canvasMethod'
|
2026-03-16 16:51:12 +08:00
|
|
|
|
|
|
|
|
|
|
export const FillSourceToBase64 = (source) => {
|
|
|
|
|
|
if (source?.toDataURL) {
|
|
|
|
|
|
return source.toDataURL?.();
|
|
|
|
|
|
} else if (source?.src) {
|
|
|
|
|
|
return source.src;
|
|
|
|
|
|
}
|
|
|
|
|
|
return source;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 图片添加gap转换
|
|
|
|
|
|
* @param {HTMLCanvasElement} image - img元素
|
|
|
|
|
|
* @param {Number} gapX - 水平gap
|
|
|
|
|
|
* @param {Number} gapY - 垂直gap
|
|
|
|
|
|
* @returns {HTMLCanvasElement} 转换后的canvas元素
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function imageAddGapToCanvas(image, gapX, gapY) {
|
|
|
|
|
|
// 创建透明 Canvas
|
|
|
|
|
|
const tcanvas = document.createElement('canvas');
|
|
|
|
|
|
tcanvas.width = image.width + gapX;
|
|
|
|
|
|
tcanvas.height = image.height + gapY;
|
|
|
|
|
|
const ctx = tcanvas.getContext('2d', { willReadFrequently: true });
|
|
|
|
|
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
|
|
|
|
|
ctx.drawImage(image, 0, 0);
|
|
|
|
|
|
return tcanvas;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 创建缩放+旋转的变换矩阵
|
|
|
|
|
|
* @param {number} scale - 缩放比例
|
|
|
|
|
|
* @param {number} angle - 旋转角度(度)
|
|
|
|
|
|
* @returns {Array} 变换矩阵 [a, b, c, d, e, f]
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function createPatternTransform(scale, angle) {
|
|
|
|
|
|
const angle_ = angle * Math.PI / 180;
|
|
|
|
|
|
const cos = Math.cos(angle_);
|
|
|
|
|
|
const sin = Math.sin(angle_);
|
|
|
|
|
|
|
|
|
|
|
|
// 先缩放,后旋转
|
|
|
|
|
|
return [
|
|
|
|
|
|
scale * cos, // a
|
|
|
|
|
|
scale * sin, // b
|
|
|
|
|
|
-scale * sin, // c
|
|
|
|
|
|
scale * cos, // d
|
|
|
|
|
|
0, // e (水平位移)
|
|
|
|
|
|
0 // f (垂直位移)
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取变换矩阵的缩放、旋转
|
|
|
|
|
|
* @param {Array} Transform - 变换矩阵、
|
|
|
|
|
|
* @returns {Object} 缩放、旋转角度 {scale, angle}
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getTransformScaleAngle(Transform) {
|
|
|
|
|
|
const a = Transform[0];
|
|
|
|
|
|
const b = Transform[1];
|
|
|
|
|
|
const c = Transform[2];
|
|
|
|
|
|
const d = Transform[3];
|
|
|
|
|
|
const scale = Math.sqrt(a * a + b * b);
|
|
|
|
|
|
const angle = Math.round(Math.atan2(b, a) * 180 / Math.PI);
|
|
|
|
|
|
return { scale, angle };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export class ObjectManager {
|
|
|
|
|
|
stateManager: any
|
|
|
|
|
|
canvasManager: any
|
|
|
|
|
|
layerManager: any
|
|
|
|
|
|
constructor(options) {
|
|
|
|
|
|
this.stateManager = options.stateManager;
|
|
|
|
|
|
this.canvasManager = options.canvasManager;
|
|
|
|
|
|
this.layerManager = options.layerManager
|
|
|
|
|
|
}
|
|
|
|
|
|
onMounted() { }
|
2026-03-17 11:35:04 +08:00
|
|
|
|
/** 设置混合模式 */
|
|
|
|
|
|
setBlendMode(id: string, blendMode: string) {
|
|
|
|
|
|
const object = this.canvasManager.getObjectById(id)
|
|
|
|
|
|
if (!object) return console.warn('设置混合模式失败,对象不存在ID:', id)
|
|
|
|
|
|
object.set("globalCompositeOperation", blendMode)
|
|
|
|
|
|
this.canvasManager.renderAll()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 16:51:12 +08:00
|
|
|
|
/** 设置平铺状态 */
|
2026-03-24 11:49:53 +08:00
|
|
|
|
setFillRepeat(id: string, isRecord = true) {
|
2026-03-16 16:51:12 +08:00
|
|
|
|
const object = this.canvasManager.getObjectById(id)
|
|
|
|
|
|
if (!object) return console.warn('设置平铺状态失败,对象不存在ID:', id)
|
|
|
|
|
|
if (object.type !== 'image') return console.warn('设置平铺状态失败,对象不是图片类型:', id)
|
|
|
|
|
|
|
|
|
|
|
|
// 创建透明 Canvas
|
|
|
|
|
|
const imgEl = object.getElement();
|
|
|
|
|
|
const tcanvas = document.createElement('canvas');
|
|
|
|
|
|
tcanvas.width = imgEl.width;
|
|
|
|
|
|
tcanvas.height = imgEl.height;
|
|
|
|
|
|
const ctx = tcanvas.getContext('2d');
|
|
|
|
|
|
ctx.clearRect(0, 0, tcanvas.width, tcanvas.height);
|
|
|
|
|
|
ctx.drawImage(imgEl, 0, 0);
|
|
|
|
|
|
const fill = {
|
|
|
|
|
|
source: FillSourceToBase64(imgEl),
|
|
|
|
|
|
gapX: 0,
|
|
|
|
|
|
gapY: 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
const pattern = new fabric.Pattern({
|
|
|
|
|
|
source: tcanvas,
|
|
|
|
|
|
repeat: 'repeat',
|
|
|
|
|
|
patternTransform: createPatternTransform(1, 0),
|
|
|
|
|
|
offsetX: 0, // 水平偏移
|
|
|
|
|
|
offsetY: 0, // 垂直偏移
|
|
|
|
|
|
});
|
|
|
|
|
|
const info = {
|
|
|
|
|
|
...object.info,
|
|
|
|
|
|
fill,
|
|
|
|
|
|
}
|
|
|
|
|
|
const rect = new fabric.Rect({
|
|
|
|
|
|
width: object.width,
|
|
|
|
|
|
height: object.height,
|
|
|
|
|
|
top: object.top,
|
|
|
|
|
|
left: object.left,
|
|
|
|
|
|
angle: object.angle,
|
|
|
|
|
|
scaleX: object.scaleX,
|
|
|
|
|
|
scaleY: object.scaleY,
|
|
|
|
|
|
flipX: object.flipX,
|
|
|
|
|
|
flipY: object.flipY,
|
2026-03-17 11:35:04 +08:00
|
|
|
|
globalCompositeOperation: object.globalCompositeOperation,
|
2026-03-16 16:51:12 +08:00
|
|
|
|
originX: "left",
|
|
|
|
|
|
originY: "top",
|
|
|
|
|
|
info,
|
|
|
|
|
|
});
|
|
|
|
|
|
rect.set("fill", pattern)
|
|
|
|
|
|
this.canvasManager.canvas.remove(object)
|
2026-03-24 11:49:53 +08:00
|
|
|
|
this.canvasManager.add(rect, isRecord)
|
2026-03-16 16:51:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
/** 获取填充对象 */
|
|
|
|
|
|
getFillRepeatObject(id: string) {
|
|
|
|
|
|
const object = this.canvasManager.getObjectById(id)
|
|
|
|
|
|
if (!object) {
|
|
|
|
|
|
console.warn('getFillRepeatObject 对象不存在ID:', id)
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (object.type !== 'rect') {
|
|
|
|
|
|
console.warn('getFillRepeatObject 对象不是矩形类型:', id)
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (object.fill?.repeat !== 'repeat') {
|
|
|
|
|
|
console.warn('getFillRepeatObject 对象不是平铺填充类型:', id)
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return object
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 修改平铺参数
|
|
|
|
|
|
* @param id 目标对象ID
|
|
|
|
|
|
* @param options 参数
|
|
|
|
|
|
* @param options.scale 缩放比例
|
|
|
|
|
|
* @param options.angle 旋转角度
|
|
|
|
|
|
* @param options.offsetX 水平偏移
|
|
|
|
|
|
* @param options.offsetY 垂直偏移
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateFillRepeatTransform(id: string, options: any, isRecord: boolean) {
|
|
|
|
|
|
const object = this.getFillRepeatObject(id)
|
|
|
|
|
|
if (!object) return null
|
|
|
|
|
|
const fill = object.get("fill");
|
|
|
|
|
|
const scale = options.hasOwnProperty('scale') ? options.scale : getTransformScaleAngle(fill.patternTransform).scale
|
|
|
|
|
|
const angle = options.hasOwnProperty('angle') ? options.angle : getTransformScaleAngle(fill.patternTransform).angle
|
|
|
|
|
|
const offsetX = options.hasOwnProperty('offsetX') ? options.offsetX : fill.offsetX
|
|
|
|
|
|
const offsetY = options.hasOwnProperty('offsetY') ? options.offsetY : fill.offsetY
|
|
|
|
|
|
fill.patternTransform = createPatternTransform(scale, angle)
|
|
|
|
|
|
fill.offsetX = offsetX
|
|
|
|
|
|
fill.offsetY = offsetY
|
|
|
|
|
|
object.set("fill", new fabric.Pattern(fill));
|
|
|
|
|
|
this.canvasManager.renderAll()
|
2026-03-16 16:54:06 +08:00
|
|
|
|
if (isRecord) {
|
|
|
|
|
|
this.stateManager.recordState()
|
|
|
|
|
|
this.layerManager.updateLayerThumbnailsById(id)
|
|
|
|
|
|
}
|
2026-03-16 16:51:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 修改平铺间隙
|
|
|
|
|
|
* @param id 目标对象ID
|
|
|
|
|
|
* @param options 间隙参数
|
|
|
|
|
|
* @param options.gapX 水平间隙
|
|
|
|
|
|
* @param options.gapY 垂直间隙
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateFillRepeatGap(id: string, options: any, isRecord: boolean) {
|
|
|
|
|
|
const object = this.getFillRepeatObject(id)
|
|
|
|
|
|
if (!object) return null
|
|
|
|
|
|
const gapX = options.hasOwnProperty('gapX') ? options.gapX : object.info.fill.gapX
|
|
|
|
|
|
const gapY = options.hasOwnProperty('gapY') ? options.gapY : object.info.fill.gapY
|
|
|
|
|
|
const fill = object.get("fill");
|
|
|
|
|
|
const image = new Image();
|
|
|
|
|
|
image.crossOrigin = "anonymous";
|
|
|
|
|
|
image.src = object.info.fill.source;
|
|
|
|
|
|
await image.decode();
|
|
|
|
|
|
fill.source = imageAddGapToCanvas(image, gapX, gapY)
|
|
|
|
|
|
object.info.fill.gapX = gapX
|
|
|
|
|
|
object.info.fill.gapY = gapY
|
|
|
|
|
|
object.set("fill", new fabric.Pattern(fill));
|
|
|
|
|
|
this.canvasManager.renderAll()
|
2026-03-16 16:54:06 +08:00
|
|
|
|
if (isRecord) {
|
|
|
|
|
|
this.stateManager.recordState()
|
|
|
|
|
|
this.layerManager.updateLayerThumbnailsById(id)
|
|
|
|
|
|
}
|
2026-03-16 16:51:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 17:25:19 +08:00
|
|
|
|
/** 修改属性
|
2026-03-17 17:17:48 +08:00
|
|
|
|
* @param id 目标对象ID
|
2026-03-18 17:25:19 +08:00
|
|
|
|
* @param options 参数
|
2026-03-17 17:17:48 +08:00
|
|
|
|
* @param isRecord 是否记录
|
|
|
|
|
|
*/
|
2026-03-18 17:25:19 +08:00
|
|
|
|
async updateProperty(id: string, options: any, isRecord: boolean) {
|
|
|
|
|
|
const object = this.canvasManager.getObjectById(id)
|
2026-03-17 17:17:48 +08:00
|
|
|
|
if (!object) return null
|
2026-04-01 16:03:20 +08:00
|
|
|
|
const type = object.type
|
|
|
|
|
|
const isWidth = object.hasOwnProperty('width')
|
|
|
|
|
|
const isHeight = object.hasOwnProperty('height')
|
|
|
|
|
|
if (isWidth || isHeight) {
|
2026-04-01 16:10:16 +08:00
|
|
|
|
let width = isWidth ? options.width : object.width
|
|
|
|
|
|
let height = isHeight ? options.height : object.height
|
2026-04-01 16:03:20 +08:00
|
|
|
|
if (type === "polygon") {
|
|
|
|
|
|
if (object.points.length === 10) {// 五角星
|
|
|
|
|
|
options.points = getStarArr(width, height)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (type === "ellipse") {// 椭圆
|
|
|
|
|
|
options.rx = width / 2
|
|
|
|
|
|
options.ry = height / 2
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 17:25:19 +08:00
|
|
|
|
object.set(options);
|
2026-03-17 17:17:48 +08:00
|
|
|
|
this.canvasManager.renderAll()
|
|
|
|
|
|
if (isRecord) {
|
|
|
|
|
|
this.stateManager.recordState()
|
|
|
|
|
|
this.layerManager.updateLayerThumbnailsById(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 16:51:12 +08:00
|
|
|
|
dispose() { }
|
|
|
|
|
|
}
|