很多很多
17
src/assets/icons/CBrush2.svg
Normal 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 |
17
src/assets/icons/CEraser2.svg
Normal 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 |
20
src/assets/icons/CMarquee.svg
Normal 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 |
BIN
src/assets/images/canvas/add.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/images/canvas/remove.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
src/assets/images/canvas/shubiao-l.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
src/assets/images/canvas/shubiao-r.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清理资源
|
* 清理资源
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置波浪工具
|
* 设置波浪工具
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||