很多很多

This commit is contained in:
李志鹏
2026-01-15 13:42:33 +08:00
parent 9912f310ec
commit 7a4fc0736d
15 changed files with 491 additions and 984 deletions

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="201.000000pt" height="200.000000pt" viewBox="0 0 201.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1654 1990 c-26 -8 -153 -110 -162 -129 -1 -3 81 -89 183 -191 l185
-185 56 55 c30 30 62 67 70 82 22 41 18 123 -8 176 -29 57 -126 158 -169 176
-47 20 -118 27 -155 16z"/>
<path d="M868 1243 c-525 -527 -498 -492 -568 -725 -54 -179 -59 -219 -30
-248 19 -19 29 -21 64 -16 113 18 361 105 431 152 28 18 251 235 498 481 l447
448 -188 188 -187 187 -467 -467z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 799 B

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M757 1273 c-214 -214 -402 -406 -418 -427 -32 -43 -37 -82 -15 -124
23 -44 327 -338 376 -363 l45 -24 463 -3 462 -3 0 46 0 45 -342 0 -343 0 342
343 c187 188 346 352 352 364 16 32 13 74 -7 105 -9 14 -110 116 -224 227
l-207 201 -47 0 -48 0 -389 -387z m207 -342 l209 -209 -128 -131 c-76 -78
-144 -138 -169 -151 -51 -24 -106 -26 -149 -4 -41 20 -337 314 -337 334 0 13
347 370 360 370 3 0 99 -94 214 -209z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="200.000000pt" viewBox="0 0 200.000000 200.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,200.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M250 1545 l0 -205 80 0 80 0 0 -340 0 -340 -80 0 -80 0 0 -205 0
-205 205 0 205 0 0 80 0 80 340 0 340 0 0 -80 0 -80 205 0 205 0 0 205 0 205
-80 0 -80 0 0 340 0 340 80 0 80 0 0 205 0 205 -205 0 -205 0 0 -80 0 -80
-340 0 -340 0 0 80 0 80 -205 0 -205 0 0 -205z m320 0 l0 -125 -120 0 -120 0
0 125 0 125 120 0 120 0 0 -125z m1100 0 l0 -125 -120 0 -120 0 0 125 0 125
120 0 120 0 0 -125z m-330 -125 l0 -80 85 0 85 0 0 -340 0 -340 -85 0 -85 0 0
-80 0 -80 -340 0 -340 0 0 80 0 80 -85 0 -85 0 0 340 0 340 85 0 85 0 0 80 0
80 340 0 340 0 0 -80z m-770 -965 l0 -125 -120 0 -120 0 0 125 0 125 120 0
120 0 0 -125z m1100 0 l0 -125 -120 0 -120 0 0 125 0 125 120 0 120 0 0 -125z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -15,6 +15,20 @@
{{ t("Canvas.GarmentPartSelector") }} {{ t("Canvas.GarmentPartSelector") }}
</div> </div>
<!-- 移除关闭按钮完全通过工具切换控制显示隐藏 --> <!-- 移除关闭按钮完全通过工具切换控制显示隐藏 -->
<div class="tip">
<div>
<img
src="/src/assets/images/canvas/shubiao-l.png"
/>
<span>Left Click: Add</span>
</div>
<div>
<img
src="/src/assets/images/canvas/shubiao-r.png"
/>
<span>Right Click: Remove</span>
</div>
</div>
</div> </div>
<div class="tool-types"> <div class="tool-types">
@@ -23,9 +37,9 @@
:key="item.type" :key="item.type"
:class="[ :class="[
'tool-btn', 'tool-btn',
{ active: selectionType === item.type }, { active: toolType === item.type },
]" ]"
@click="setSelectionType(item.type)" @click="setPartType(item.type)"
> >
<svg-icon :name="item.icon" :size="item.size" /> <svg-icon :name="item.icon" :size="item.size" />
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
@@ -37,24 +51,18 @@
<!-- 底部选区操作工具栏 --> <!-- 底部选区操作工具栏 -->
<div class="tool-actions"> <div class="tool-actions">
<div class="action-btn" @click="copySelectionToNewLayer"> <div class="action-btn" @click="onCreate">
<svg-icon name="CPaste" size="16" /> <svg-icon name="CPaste" size="16" />
<span class="btn-text">{{ <span class="btn-text">{{
$t("Canvas.creation") $t("Canvas.creation")
}}</span> }}</span>
</div> </div>
<div class="action-btn" @click="cutSelectionToNewLayer"> <div class="action-btn" @click="onCopyCreate">
<svg-icon name="CCut" size="26" /> <svg-icon name="CCut" size="26" />
<span class="btn-text">{{ <span class="btn-text">{{
$t("Canvas.CreateAndCopy") $t("Canvas.CreateAndCopy")
}}</span> }}</span>
</div> </div>
<div class="action-btn" @click="clearSelectionContent">
<svg-icon name="CClear" size="18" />
<span class="btn-text">{{
$t("Canvas.TheClearlySelectedContent")
}}</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -90,7 +98,7 @@
type: Object, type: Object,
required: true, required: true,
}, },
selectionManager: { partManager: {
type: Object, type: Object,
required: true, required: true,
}, },
@@ -115,7 +123,7 @@
// 响应式数据 // 响应式数据
const visible = ref(false); const visible = ref(false);
const selectionType = ref("rectangle"); const toolType = ref(OperationType.PART);
//打开隐藏操作面板 //打开隐藏操作面板
const closePanel = ref(false); const closePanel = ref(false);
const setClosePanel = () => { const setClosePanel = () => {
@@ -132,20 +140,20 @@
{ {
type: OperationType.PART_RECTANGLE, type: OperationType.PART_RECTANGLE,
label: "Marquee Selection", label: "Marquee Selection",
icon: "CRectangle", icon: "CMarquee",
size: "26", size: "20",
}, },
{ {
type: OperationType.PART_BRUSH, type: OperationType.PART_BRUSH,
label: "Brush Selection", label: "Brush Selection",
icon: "CBrush", icon: "CBrush2",
size: "24", size: "16",
}, },
{ {
type: OperationType.PART_ERASER, type: OperationType.PART_ERASER,
label: "Erase", label: "Erase",
icon: "CEraser", icon: "CEraser2",
size: "24", size: "22",
}, },
]; ];
@@ -169,13 +177,13 @@
if (selectionTools.includes(newTool)) { if (selectionTools.includes(newTool)) {
show(); show();
// 根据工具类型设置选区类型 // 根据工具类型设置选区类型
selectionType.value = newTool; toolType.value = newTool;
// 更新选区管理器的选区类型 // 更新选区管理器的选区类型
if (props.selectionManager) { // if (props.partManager) {
props.selectionManager.setSelectionType(selectionType.value); // props.partManager.setPartType(toolType.value);
props.selectionManager.setupSelectionEvents(); // props.partManager.setupPartEvents();
} // }
} else { } else {
close(); close();
} }
@@ -201,20 +209,30 @@
/** /**
* 设置选区类型 * 设置选区类型
*/ */
function setSelectionType(type) { function setPartType(type) {
selectionType.value = type; toolType.value = type;
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager // 通过 ToolManager 切换工具,这会自动通知 partManager
if (props.toolManager) { if (props.toolManager) {
props.toolManager.setToolWithCommand(type); props.toolManager.setToolWithCommand(type);
} }
// 备用方案:如果没有 toolManager直接更新 selectionManager // // 备用方案:如果没有 toolManager直接更新 partManager
else if (props.selectionManager) { // else if (props.partManager) {
props.selectionManager.setSelectionType(type); // props.partManager.setPartType(type);
props.selectionManager.setupSelectionEvents(); // props.partManager.setupPartEvents();
// }
} }
// 创建
function onCreate() {
} }
// 复制并创建
function onCopyCreate() {
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -290,6 +308,28 @@
// border-bottom: 1px solid rgba(0, 0, 0, 0.05); // border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background-color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.8);
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
position: relative;
> .tip {
position: absolute;
top: 0;
right: 0;
display: flex;
> div {
display: flex;
align-items: center;
justify-content: center;
margin-left: 10px;
> img {
width: 12px;
height: 12px;
margin-right: 5px;
}
> span {
font-size: 10px;
color: #333;
}
}
}
} }
.header-title { .header-title {
@@ -370,9 +410,9 @@
.tool-actions { .tool-actions {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 5px; gap: 5px;
padding: 0 10px; padding: 0 30px;
} }
/* 平板适配 - 每行4个按钮 */ /* 平板适配 - 每行4个按钮 */
@@ -436,5 +476,4 @@
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
</style> </style>

View File

@@ -385,6 +385,8 @@ onMounted(async () => {
partManager = new PartManager({ partManager = new PartManager({
canvas: canvasManager.canvas, canvas: canvasManager.canvas,
layerManager, layerManager,
canvasManager,
toolManager,
}); });
canvasManager.setPartManager(partManager); canvasManager.setPartManager(partManager);
@@ -722,8 +724,13 @@ function addRemoveBtn(fun) {
}); });
} }
function deleteFun() { function deleteFun(e, control) {
const target = control.target;
if(target.onDelete){
target.onDelete(target);
}else if(target.id){
removeLayer(layerManager.activeLayerId.value); removeLayer(layerManager.activeLayerId.value);
}
} }
function removeLayer(layerId) { function removeLayer(layerId) {

View File

@@ -3,6 +3,8 @@ import { generateId } from "../utils/helper";
import { OperationType } from "../utils/layerHelper"; import { OperationType } from "../utils/layerHelper";
import { CreateSelectionCommand } from "../commands/SelectionCommands"; import { CreateSelectionCommand } from "../commands/SelectionCommands";
import { ClearSelectionCommand } from "../commands/LassoCutoutCommand"; import { ClearSelectionCommand } from "../commands/LassoCutoutCommand";
import addIcon from "@/assets/images/canvas/add.png";
import removeIcon from "@/assets/images/canvas/remove.png";
/** /**
* 部件选择管理器 * 部件选择管理器
@@ -13,41 +15,28 @@ export class PartManager {
* @param {Object} options 配置选项 * @param {Object} options 配置选项
* @param {Object} options.canvas fabric.js画布实例 * @param {Object} options.canvas fabric.js画布实例
* @param {Object} options.commandManager 命令管理器实例 * @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.canvasManager 画布管理实例
* @param {Object} options.layerManager 图层管理实例 * @param {Object} options.layerManager 图层管理实例
* @param {Object} options.toolManager 工具管理实例
*/ */
constructor(options = {}) { constructor(options = {}) {
this.canvas = options.canvas; this.canvas = options.canvas;
this.commandManager = options.commandManager; this.commandManager = options.commandManager;
this.layerManager = options.layerManager; this.layerManager = options.layerManager;
this.canvasManager = options.canvasManager;
this.toolManager = options.toolManager;
// 选区状态 // 状态
this.isActive = false; this.isActive = false;
this.selectionType = OperationType.LASSO_RECTANGLE; // 使用常量而不是字符串 this.partObject = null; // 当前选区对象
this.selectionObject = null; // 当前选区对象 this.partId = "part_selector";
this.selectionId = "selection_" + Date.now(); this.defaultCursor = "default";
this.featherAmount = 0; // 羽化值
// 选区样式配置
this.selectionStyle = {
stroke: "#0096ff",
strokeWidth: 1,
strokeDashArray: [5, 5],
fill: "rgba(0, 150, 255, 0.1)",
selectable: false,
evented: false,
excludeFromExport: true,
hoverCursor: "default",
moveCursor: "default",
};
// 绘制状态 // 绘制状态
this.drawingObject = null; this.drawingObject = null;
this.startPoint = null; this.startPoint = null;
this.selectionPath = null; // 存储选区路径数据 this.partPath = null; // 存储选区路径数据
// 自由选区相关状态
this.drawingPoints = null;
this.currentPathString = null;
// 不再直接绑定事件处理函数 // 不再直接绑定事件处理函数
this._mouseDownHandler = null; this._mouseDownHandler = null;
@@ -64,13 +53,10 @@ export class PartManager {
]; ];
// 当前工具 // 当前工具
this.currentTool = OperationType.SELECT; this.activeTool = this.toolManager.activeTool;
// 选区状态变化回调 // 选区状态变化回调
this.onSelectionChanged = null; this.onSelectionChanged = null;
// 不再自动初始化事件,改为手动控制
// this.initEvents();
} }
/** /**
@@ -78,8 +64,6 @@ export class PartManager {
* @param {String} toolId 工具ID * @param {String} toolId 工具ID
*/ */
setCurrentTool(toolId) { setCurrentTool(toolId) {
this.currentTool = toolId;
// 检查是否为选区工具 // 检查是否为选区工具
const wasActive = this.isActive; const wasActive = this.isActive;
this.isActive = this.tools.includes(toolId); this.isActive = this.tools.includes(toolId);
@@ -93,11 +77,6 @@ export class PartManager {
this.cleanupEvents(); this.cleanupEvents();
this.clearSelection(); this.clearSelection();
} }
// 根据工具类型设置选区类型
if (this.isActive) {
this.selectionType = toolId;
}
} }
/** /**
@@ -105,6 +84,7 @@ export class PartManager {
*/ */
initEvents() { initEvents() {
if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化 if (!this.canvas || this._mouseDownHandler) return; // 避免重复初始化
this.defaultCursor = this.canvas.defaultCursor;
// 保存实例引用,用于事件处理函数中 // 保存实例引用,用于事件处理函数中
const self = this; const self = this;
@@ -113,30 +93,23 @@ export class PartManager {
this._mouseDownHandler = (options) => { this._mouseDownHandler = (options) => {
// 如果选区功能未激活,不处理事件 // 如果选区功能未激活,不处理事件
if (!this.isActive) return; if (!this.isActive) return;
// 如果点击的是已有对象且不是选区对象,则不处理
if (
options.target &&
options.target.id !== this.selectionId &&
options.target.selectable !== false &&
options.target.type !== "selection"
) {
return;
}
// 阻止事件冒泡,避免与 CanvasEventManager 冲突 // 阻止事件冒泡,避免与 CanvasEventManager 冲突
options.e.stopPropagation(); options.e.stopPropagation();
switch (this.activeTool.value) {
case OperationType.PART:
this._pointDownkHandler(options);
break;
case OperationType.PART_RECTANGLE:
this._rectangleDownHandler(options);
break;
case OperationType.PART_BRUSH:
this._brushDownHandler(options);
break;
case OperationType.PART_ERASER:
this._eraseDownHandler(options);
break;
// 根据选区类型执行不同的起始操作 default:
switch (this.selectionType) {
case OperationType.LASSO:
this.startFreeSelection(options);
break;
case OperationType.LASSO_ELLIPSE:
this.startEllipseSelection(options);
break;
case OperationType.LASSO_RECTANGLE:
this.startRectangleSelection(options);
break; break;
} }
}; };
@@ -144,21 +117,24 @@ export class PartManager {
// 鼠标移动事件处理 // 鼠标移动事件处理
this._mouseMoveHandler = (options) => { this._mouseMoveHandler = (options) => {
// 如果选区功能未激活或没有正在绘制的对象,不处理事件 // 如果选区功能未激活或没有正在绘制的对象,不处理事件
if (!this.isActive || !this.drawingObject) return; if (!this.isActive) return;
// 阻止事件冒泡 // 阻止事件冒泡
options.e.stopPropagation(); options.e.stopPropagation();
switch (this.activeTool.value) {
case OperationType.PART:
this._pointMoveHandler(options);
break;
case OperationType.PART_RECTANGLE:
this._rectangleMoveHandler(options);
break;
case OperationType.PART_BRUSH:
this._brushMoveHandler(options);
break;
case OperationType.PART_ERASER:
this._eraseMoveHandler(options);
break;
// 根据选区类型执行不同的绘制操作 default:
switch (this.selectionType) {
case OperationType.LASSO_RECTANGLE:
this.drawRectangleSelection(options);
break;
case OperationType.LASSO_ELLIPSE:
this.drawEllipseSelection(options);
break;
case OperationType.LASSO:
this.drawFreeSelection(options);
break; break;
} }
}; };
@@ -166,36 +142,27 @@ export class PartManager {
// 鼠标抬起事件处理 // 鼠标抬起事件处理
this._mouseUpHandler = (options) => { this._mouseUpHandler = (options) => {
// 如果选区功能未激活或没有正在绘制的对象,不处理事件 // 如果选区功能未激活或没有正在绘制的对象,不处理事件
if (!this.isActive || !this.drawingObject) return; if (!this.isActive) return;
// 阻止事件冒泡 // 阻止事件冒泡
if (options && options.e) { if (options && options.e) {
options.e.stopPropagation(); options.e.stopPropagation();
} }
switch (this.activeTool.value) {
case OperationType.PART:
this._pointUpHandler(options);
break;
case OperationType.PART_RECTANGLE:
this._rectangleUpHandler(options);
break;
case OperationType.PART_BRUSH:
this._brushUpHandler(options);
break;
case OperationType.PART_ERASER:
this._eraseUpHandler(options);
break;
// 根据选区类型执行不同的完成操作 default:
switch (this.selectionType) {
case OperationType.LASSO_RECTANGLE:
this.endRectangleSelection();
break; break;
case OperationType.LASSO_ELLIPSE:
this.endEllipseSelection();
break;
case OperationType.LASSO:
this.endFreeSelection();
break;
}
// 如果有命令管理器,使用命令模式记录选区创建
if (this.commandManager && this.selectionObject) {
this.commandManager.execute(
new CreateSelectionCommand({
canvas: this.canvas,
selectionManager: this,
selectionObject: this.selectionObject,
selectionType: this.selectionType,
})
);
} }
}; };
@@ -203,27 +170,6 @@ export class PartManager {
this._keyDownHandler = (event) => { this._keyDownHandler = (event) => {
// 只在选区功能激活时处理键盘事件 // 只在选区功能激活时处理键盘事件
if (!this.isActive) return; if (!this.isActive) return;
if (event.key === "Escape") {
// ESC键取消当前选区操作
if (this.drawingObject) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
}
// 清除已有选区
else if (this.selectionObject) {
if (this.commandManager) {
this.commandManager.execute(
new ClearSelectionCommand({
selectionManager: this,
})
);
} else {
this.clearSelection();
}
}
}
}; };
// 添加事件监听 // 添加事件监听
@@ -260,138 +206,75 @@ export class PartManager {
} }
} }
/** // 点选工具模式下点击事件处理
* 获取选区对象 _pointDownkHandler(options) {
* @returns {Object} 选区对象 const button = options.button;
*/ const isLeft = button === 1;// 左键1添加 右键3删除
getSelectionObject() { const icon = `url("${isLeft ? addIcon : removeIcon}") 16 16, default`
return this.selectionObject; this.canvas.upperCanvasEl.style.cursor = icon;
}
// 点选工具模式下移动事件处理
_pointMoveHandler(options) {
}
// 点选工具模式下抬起事件处理
_pointUpHandler(options) {
const button = options.button;
const isLeft = button === 1;// 左键1添加 右键3删除
this.canvas.upperCanvasEl.style.cursor = this.defaultCursor;
const { x, y } = options.pointer;
const fixedObject = this.canvasManager.getFixedLayerObject({ x, y });
console.log("==========", fixedObject)
} }
/**
* 获取选区路径 // 框选工具模式下点击事件处理
* @returns {Array|String} 选区路径数据 _rectangleDownHandler(options) {
*/ }
getSelectionPath() { // 框选工具模式下移动事件处理
return this.selectionPath; _rectangleMoveHandler(options) {
}
// 框选工具模式下抬起事件处理
_rectangleUpHandler(options) {
} }
/**
* 获取羽化值 // 绘制工具模式下点击事件处理
* @returns {Number} 羽化值 _brushDownHandler(options) {
*/ }
getFeatherAmount() { // 绘制工具模式下移动事件处理
return this.featherAmount; _brushMoveHandler(options) {
}
// 绘制工具模式下抬起事件处理
_brushUpHandler(options) {
} }
/**
* 设置羽化值 // 擦除工具模式下抬起事件处理
* @param {Number} amount 羽化值 _eraseUpHandler(options) {
*/ }
setFeatherAmount(amount) { // 擦除工具模式下点击事件处理
this.featherAmount = amount; _eraseDownHandler(options) {
return this.updateSelectionAppearance(); }
// 擦除工具模式下移动事件处理
_eraseMoveHandler(options) {
} }
/**
* 设置选区对象
* @param {Object} object 选区对象
*/
setSelectionObject(object) {
// 如果已存在选区,先移除
if (this.selectionObject) {
this.removeSelectionFromCanvas();
}
// 更新选区对象
this.selectionObject = object;
this.selectionPath = object.path;
this.selectionId = object.id || generateId();
// 更新外观
this.updateSelectionAppearance();
// 添加到画布(确保在顶层)
if (this.canvas && this.selectionObject) {
this.canvas.add(this.selectionObject);
this.canvas.bringToFront(this.selectionObject);
this.canvas.renderAll();
}
// 触发选区变化回调
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
this.onSelectionChanged();
}
return true;
}
/**
* 从路径数据设置选区
* @param {Array|String} path 选区路径数据
*/
setSelectionFromPath(path) {
if (!path) return false;
// 创建选区对象
const selectionObj = new fabric.Path(path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
// 设置选区
return this.setSelectionObject(selectionObj);
}
/**
* 更新选区外观
*/
updateSelectionAppearance() {
if (!this.selectionObject) return false;
// 应用基本样式
Object.assign(this.selectionObject, this.selectionStyle);
// 应用羽化效果
if (this.featherAmount > 0) {
this.selectionObject.shadow = new fabric.Shadow({
color: "rgba(0, 150, 255, 0.5)",
blur: this.featherAmount,
offsetX: 0,
offsetY: 0,
});
} else {
this.selectionObject.shadow = null;
}
// 更新画布
this.canvas.renderAll();
return true;
}
/**
* 移除选区
*/
removeSelectionFromCanvas() {
if (this.canvas && this.selectionObject) {
this.canvas.remove(this.selectionObject);
this.canvas.renderAll();
}
}
/** /**
* 清除选区 * 清除选区
*/ */
clearSelection() { clearSelection() {
// 移除选区对象 // 移除选区对象
this.removeSelectionFromCanvas(); // this.removeSelectionFromCanvas();
// 重置选区状态 // 重置选区状态
this.selectionObject = null; this.partObject = null;
this.selectionPath = null; this.partPath = null;
this.selectionId = null;
this.featherAmount = 0;
// 触发选区变化回调 // 触发选区变化回调
if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") { if (this.onSelectionChanged && typeof this.onSelectionChanged === "function") {
@@ -401,533 +284,6 @@ export class PartManager {
return true; return true;
} }
/**
* 反转选区
*/
async invertSelection() {
if (!this.canvas || !this.selectionObject) return false;
// 获取画布范围
const canvasRect = new fabric.Rect({
left: 0,
top: 0,
width: this.canvas.width,
height: this.canvas.height,
selectable: false,
});
// 创建反选路径
let invertedPath;
try {
invertedPath = canvasRect.subtractPathFromRect(this.selectionObject.path);
} catch (error) {
console.error("无法反转选区:", error);
return false;
}
// 设置新的选区
const newSelection = new fabric.Path(invertedPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(newSelection);
}
/**
* 添加到选区
* @param {Object} newSelection 要添加的选区对象
*/
async addToSelection(newSelection) {
if (!this.canvas) return false;
// 如果当前没有选区,直接使用新选区
if (!this.selectionObject) {
return this.setSelectionObject(newSelection);
}
// 合并选区
let combinedPath;
try {
combinedPath = this.selectionObject.union(newSelection);
} catch (error) {
console.error("无法添加到选区:", error);
return false;
}
// 设置新的选区
const combinedSelection = new fabric.Path(combinedPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(combinedSelection);
}
/**
* 从选区中移除
* @param {Object} removeSelection 要移除的选区对象
*/
async removeFromSelection(removeSelection) {
if (!this.canvas || !this.selectionObject) return false;
// 从当前选区中减去新选区
let resultPath;
try {
resultPath = this.selectionObject.subtract(removeSelection);
} catch (error) {
console.error("无法从选区中移除:", error);
return false;
}
// 设置新的选区
const newSelection = new fabric.Path(resultPath.path, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
});
return this.setSelectionObject(newSelection);
}
/**
* 应用羽化效果
* @param {Number} amount 羽化值
*/
async featherSelection(amount) {
if (!this.selectionObject) return false;
// 更新羽化值
this.featherAmount = amount;
// 更新选区外观
return this.updateSelectionAppearance();
}
/**
* 检查对象是否在选区内
* @param {Object} object 要检查的对象
* @returns {Boolean} 是否在选区内
*/
isObjectInSelection(object) {
if (!this.selectionObject || !object) return false;
// 获取对象的边界框
const bounds = object.getBoundingRect();
const { left, top, width, height } = bounds;
// 检查对象的中心点和四个角是否在选区内
const centerX = left + width / 2;
const centerY = top + height / 2;
// 检查中心点
if (this.isPointInSelection(centerX, centerY)) return true;
// 检查四个角
if (this.isPointInSelection(left, top)) return true;
if (this.isPointInSelection(left + width, top)) return true;
if (this.isPointInSelection(left, top + height)) return true;
if (this.isPointInSelection(left + width, top + height)) return true;
return false;
}
/**
* 检查点是否在选区内
* @param {Number} x X坐标
* @param {Number} y Y坐标
* @returns {Boolean} 是否在选区内
*/
isPointInSelection(x, y) {
if (!this.selectionObject) return false;
// 使用fabric.js的containsPoint方法判断点是否在选区内
return this.selectionObject.containsPoint({ x, y });
}
/**
* 开始自由选区
* @param {Object} options 事件对象
*/
startFreeSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建用于绘制轨迹的点数组
this.drawingPoints = [pointer];
// 初始化SVG路径字符串
this.currentPathString = `M ${pointer.x} ${pointer.y}`;
// 创建临时路径对象用于实时显示
this.drawingObject = new fabric.Path(this.currentPathString, {
stroke: this.selectionStyle.stroke,
strokeWidth: this.selectionStyle.strokeWidth,
strokeDashArray: this.selectionStyle.strokeDashArray,
fill: "transparent",
selectable: false,
evented: false,
strokeLineCap: "round",
strokeLineJoin: "round",
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制自由选区
* @param {Object} options 事件对象
*/
drawFreeSelection(options) {
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 添加新的点,但避免添加过于密集的点
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const distance = Math.sqrt(
Math.pow(pointer.x - lastPoint.x, 2) + Math.pow(pointer.y - lastPoint.y, 2)
);
// 只有当距离大于2像素时才添加新点避免路径过于复杂
if (distance > 2) {
this.drawingPoints.push(pointer);
// 更新路径字符串
this.currentPathString += ` L ${pointer.x} ${pointer.y}`;
// 移除旧的绘制对象
this.canvas.remove(this.drawingObject);
// 创建新的路径对象
this.drawingObject = new fabric.Path(this.currentPathString, {
stroke: this.selectionStyle.stroke,
strokeWidth: this.selectionStyle.strokeWidth,
strokeDashArray: this.selectionStyle.strokeDashArray,
fill: "transparent",
selectable: false,
evented: false,
strokeLineCap: "round",
strokeLineJoin: "round",
});
// 重新添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
}
/**
* 结束自由选区
*/
endFreeSelection() {
if (!this.drawingObject || !this.drawingPoints || !this.isActive) return;
// 检查是否有足够的点来形成选区
if (this.drawingPoints.length < 3) {
// 点太少,清除绘制对象
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.drawingPoints = null;
this.startPoint = null;
this.currentPathString = null;
return;
}
// 自动闭合路径 - 连接最后一点到第一点
const firstPoint = this.drawingPoints[0];
const lastPoint = this.drawingPoints[this.drawingPoints.length - 1];
const closingDistance = Math.sqrt(
Math.pow(firstPoint.x - lastPoint.x, 2) + Math.pow(firstPoint.y - lastPoint.y, 2)
);
// 如果首尾距离较大,自动添加闭合线段
let finalPathString = this.currentPathString;
if (closingDistance > 10) {
finalPathString += ` L ${firstPoint.x} ${firstPoint.y}`;
}
finalPathString += " Z"; // 闭合路径
// 创建最终选区对象
const selectionObj = new fabric.Path(finalPathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.drawingPoints = null;
this.startPoint = null;
this.currentPathString = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 开始矩形选区
* @param {Object} options 事件对象
*/
startRectangleSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建矩形对象
this.drawingObject = new fabric.Rect({
left: pointer.x,
top: pointer.y,
width: 0,
height: 0,
...this.selectionStyle,
fill: "transparent", // 在绘制过程中不显示填充
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制矩形选区
* @param {Object} options 事件对象
*/
drawRectangleSelection(options) {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 计算宽度和高度
const width = Math.abs(pointer.x - this.startPoint.x);
const height = Math.abs(pointer.y - this.startPoint.y);
// 确定左上角坐标
const left = Math.min(this.startPoint.x, pointer.x);
const top = Math.min(this.startPoint.y, pointer.y);
// 更新矩形
this.drawingObject.set({
left: left,
top: top,
width: width,
height: height,
});
this.canvas.renderAll();
}
/**
* 结束矩形选区
*/
endRectangleSelection() {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 将矩形转换为路径
const left = this.drawingObject.left;
const top = this.drawingObject.top;
const width = this.drawingObject.width;
const height = this.drawingObject.height;
// 如果矩形太小,忽略
if (width < 5 || height < 5) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
return;
}
// 创建矩形路径字符串
const pathString = `M ${left} ${top} L ${left + width} ${top} L ${
left + width
} ${top + height} L ${left} ${top + height} Z`;
// 创建最终选区对象
const selectionObj = new fabric.Path(pathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.startPoint = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 开始椭圆选区
* @param {Object} options 事件对象
*/
startEllipseSelection(options) {
if (!this.canvas || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
this.startPoint = pointer;
// 创建椭圆对象
this.drawingObject = new fabric.Ellipse({
left: pointer.x,
top: pointer.y,
rx: 0,
ry: 0,
...this.selectionStyle,
fill: "transparent", // 在绘制过程中不显示填充
// originX: "left",
// originY: "top",
originX: "center",
originY: "center",
});
// 添加到画布
this.canvas.add(this.drawingObject);
this.canvas.renderAll();
}
/**
* 绘制椭圆选区
* @param {Object} options 事件对象
*/
drawEllipseSelection(options) {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取鼠标位置
const pointer = this.canvas.getPointer(options.e);
// 计算半径
const rx = Math.abs(pointer.x - this.startPoint.x) / 2;
const ry = Math.abs(pointer.y - this.startPoint.y) / 2;
// 确定中心坐标
const left = Math.min(this.startPoint.x, pointer.x);
const top = Math.min(this.startPoint.y, pointer.y);
// 更新椭圆
this.drawingObject.set({
left: left,
top: top,
rx: rx,
ry: ry,
originX: "left",
originY: "top",
});
this.canvas.renderAll();
}
/**
* 结束椭圆选区
*/
endEllipseSelection() {
if (!this.drawingObject || !this.startPoint || !this.isActive) return;
// 获取椭圆参数
const { left, top, rx, ry } = this.drawingObject;
// 如果椭圆太小,忽略
if (rx < 2 || ry < 2) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
return;
}
// 计算中心点
const cx = left + rx;
const cy = top + ry;
// 将椭圆转换为路径字符串
const pathString = this.ellipseToSVGPath(cx, cy, rx, ry);
// 创建最终选区对象
const selectionObj = new fabric.Path(pathString, {
...this.selectionStyle,
id: `selection_${Date.now()}`,
name: "selection",
fill: this.selectionStyle.fill, // 恢复填充
});
// 移除绘制中的临时对象
this.canvas.remove(this.drawingObject);
// 重置绘制状态
this.drawingObject = null;
this.startPoint = null;
// 设置选区
this.setSelectionObject(selectionObj);
}
/**
* 将椭圆转换为SVG路径字符串
* @param {Number} cx 中心点X坐标
* @param {Number} cy 中心点Y坐标
* @param {Number} rx X半径
* @param {Number} ry Y半径
* @returns {String} SVG路径字符串
*/
ellipseToSVGPath(cx, cy, rx, ry) {
// 使用椭圆弧命令创建完整椭圆
return `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${
cx + rx
} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
}
/**
* 设置选区工具
* @param {string} type 选区类型OperationType.LASSO, OperationType.LASSO_RECTANGLE, OperationType.LASSO_ELLIPSE
*/
setSelectionType(type) {
this.selectionType = type;
// 如果正在绘制,清除临时对象
if (this.drawingObject) {
this.canvas.remove(this.drawingObject);
this.drawingObject = null;
this.startPoint = null;
}
}
/**
* 设置选区工具的鼠标事件
*/
setupSelectionEvents() {
// 选区事件现在通过 setCurrentTool 方法管理
// 这个方法现在主要用于刷新或重置事件监听
if (!this.canvas || !this.isActive) return;
// 确保选区处于激活状态
if (this.tools.includes(this.currentTool)) {
this.isActive = true;
// 如果事件还没有初始化,初始化它们
if (!this._mouseDownHandler) {
this.initEvents();
}
}
}
/** /**
* 清理资源 * 清理资源
*/ */

View File

@@ -197,19 +197,19 @@ export class ToolManager {
name: "部件选取工具-矩形", name: "部件选取工具-矩形",
icon: "part", icon: "part",
cursor: "default", cursor: "default",
setup: this.setupPartTool.bind(this), setup: this.setupPartRectangleTool.bind(this),
}, },
[OperationType.PART_BRUSH]: { [OperationType.PART_BRUSH]: {
name: "部件选取工具-画笔", name: "部件选取工具-画笔",
icon: "part", icon: "part",
cursor: "default", cursor: "default",
setup: this.setupPartTool.bind(this), setup: this.setupPartBrushTool.bind(this),
}, },
[OperationType.PART_ERASER]: { [OperationType.PART_ERASER]: {
name: "部件选取工具-橡皮擦", name: "部件选取工具-橡皮擦",
icon: "part", icon: "part",
cursor: "default", cursor: "default",
setup: this.setupPartTool.bind(this), setup: this.setupPartEraserTool.bind(this),
}, },
// 红绿图模式专用工具 // 红绿图模式专用工具
@@ -705,7 +705,6 @@ export class ToolManager {
*/ */
setupPartTool() { setupPartTool() {
if (!this.canvas) return; if (!this.canvas) return;
if (this.checkToolCanOperateSelectedObject()) return;
this.canvas.isDrawingMode = false; this.canvas.isDrawingMode = false;
this.canvas.selection = false; this.canvas.selection = false;
@@ -713,6 +712,40 @@ export class ToolManager {
this.canvasManager.partManager.setCurrentTool(OperationType.PART); this.canvasManager.partManager.setCurrentTool(OperationType.PART);
} }
} }
/**
* 设置部件选取工具--矩形
*/
setupPartRectangleTool() {
if (!this.canvas) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = true;
if (this.canvasManager && this.canvasManager.partManager) {
this.canvasManager.partManager.setCurrentTool(OperationType.PART_RECTANGLE);
}
}
/**
* 设置部件选取工具--画笔
*/
setupPartBrushTool() {
if (!this.canvas) return;
this.canvas.isDrawingMode = true;
this.canvas.selection = false;
if (this.canvasManager && this.canvasManager.partManager) {
this.canvasManager.partManager.setCurrentTool(OperationType.PART_BRUSH);
}
}
/**
* 设置部件选取工具--橡皮擦
*/
setupPartEraserTool() {
if (!this.canvas) return;
this.canvas.isDrawingMode = false;
this.canvas.selection = false;
if (this.canvasManager && this.canvasManager.partManager) {
this.canvasManager.partManager.setCurrentTool(OperationType.PART_ERASER);
}
}
/** /**
* 设置波浪工具 * 设置波浪工具

View File

@@ -79,7 +79,9 @@
type="number" type="number"
v-model="item.object.scaleX" v-model="item.object.scaleX"
step="0.1" step="0.1"
@input="updateList(item, 'object.scaleX', item.object.scaleX)" @input="
updateList(item, 'object.scaleX', item.object.scaleX)
"
/> />
</div> </div>
<div> <div>
@@ -88,7 +90,9 @@
type="number" type="number"
v-model="item.object.scaleY" v-model="item.object.scaleY"
step="0.1" step="0.1"
@input="updateList(item, 'object.scaleY', item.object.scaleY)" @input="
updateList(item, 'object.scaleY', item.object.scaleY)
"
/> />
</div> </div>
<div> <div>
@@ -124,7 +128,9 @@
step="0.1" step="0.1"
min="0" min="0"
max="1" max="1"
@input="updateList(item, 'object.opacity', item.object.opacity)" @input="
updateList(item, 'object.opacity', item.object.opacity)
"
/> />
</div> </div>
</div> </div>
@@ -228,6 +234,8 @@
} }
} else if (item.action === ACTIONS.SELECT) { } else if (item.action === ACTIONS.SELECT) {
activeToken.value = item.token; activeToken.value = item.token;
} else if (item.action === ACTIONS.DELETE) {
list.value = list.value.filter((v) => v.token !== item.token);
} }
}); });
}; };
@@ -284,7 +292,7 @@
}; };
// 监听列表变化属性变更 // 监听列表变化属性变更
const updateList = (item, key, value) => { const updateList = (item, key, value) => {
if(key === "scale[0]") item.scale[1] = value; if (key === "scale[0]") item.scale[1] = value;
pingpuRef.value.updataList([ pingpuRef.value.updataList([
{ {
token: item.token, token: item.token,

View File

@@ -144,6 +144,13 @@
const list = [{ token, action: ACTIONS.SELECT }]; const list = [{ token, action: ACTIONS.SELECT }];
emit("change-canvas", list); emit("change-canvas", list);
}; };
// 删除对象
const onDeleteItem = (object) => {
const list = [{ token: object.token, action: ACTIONS.DELETE }];
emit("change-canvas", list);
canvas.remove(object);
canvas.renderAll();
};
const urlToCanvas = (url) => { const urlToCanvas = (url) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fabric.Image.fromURL( fabric.Image.fromURL(
@@ -181,6 +188,7 @@
height: cheight, height: cheight,
fill: pattern, fill: pattern,
...item.object, ...item.object,
onDelete: (v) => onDeleteItem(v),
}); });
canvas.add(rect); canvas.add(rect);
}; };

View File

@@ -335,15 +335,15 @@ const otherData = {
color: {rgba: {r:255,g:0,b:0,a:1}}, color: {rgba: {r:255,g:0,b:0,a:1}},
printObject: { printObject: {
prints: [ prints: [
{ // {
ifSingle: false, // ifSingle: false,
level2Type: "Pattern", // level2Type: "Pattern",
designType: "Library", // designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg", // path: "/src/assets/images/canvas/yinhua1.jpg",
location: [250, 780], // location: [250, 780],
scale: [0.3, 0.4], // scale: [0.3, 0.4],
angle: 0, // angle: 0,
}, // },
{ {
ifSingle: true, ifSingle: true,
level2Type: "Pattern", level2Type: "Pattern",

View File

@@ -5,11 +5,13 @@
</div> </div>
</div> --> </div> -->
<overall-canvas-demo /> <overall-canvas-demo />
<div style="width: 100px; height: 100px;position: relative;"><CanvasEditor /></div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { fabric } from "fabric-with-all"; import { fabric } from "fabric-with-all";
import { ref, watch, onMounted } from "vue"; import { ref, watch, onMounted } from "vue";
import CanvasEditor from "./CanvasEditor/index.vue";
import OverallCanvasDemo from "./OverallCanvas/demo.vue"; import OverallCanvasDemo from "./OverallCanvas/demo.vue";
const imageUrl = "/src/assets/images/canvas/xiangaofenge.png"; const imageUrl = "/src/assets/images/canvas/xiangaofenge.png";
const testRef = ref(null); const testRef = ref(null);