平铺元素ui更改
This commit is contained in:
68
src/assets/icons/CPart.svg
Normal file
68
src/assets/icons/CPart.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?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="103.000000pt" height="92.000000pt" viewBox="0 0 103.000000 92.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,92.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M365 895 c-5 -2 -36 -6 -67 -10 -45 -5 -58 -10 -59 -23 0 -11 -2 -12
|
||||
-6 -4 -7 17 -32 15 -40 -4 -4 -11 -8 -12 -13 -4 -5 8 -13 9 -21 4 -7 -4 -22
|
||||
-9 -34 -10 -11 -1 -41 -11 -66 -21 l-47 -18 20 -63 c11 -38 23 -60 30 -56 6 4
|
||||
8 -1 3 -15 -3 -11 -3 -21 2 -21 4 0 9 -12 9 -26 1 -14 5 -28 8 -32 4 -3 33 3
|
||||
66 15 33 11 61 19 62 18 2 -2 1 -139 -2 -304 l-5 -301 309 2 309 3 -2 108 c-2
|
||||
77 2 113 11 125 10 12 10 14 1 8 -7 -4 -13 -2 -13 4 0 6 10 8 23 5 18 -5 26
|
||||
-1 36 17 9 17 10 18 6 3 -5 -16 -4 -18 6 -7 18 17 -1 34 -40 37 l-32 2 -2 151
|
||||
c-1 82 0 147 3 144 9 -12 44 -11 52 1 5 8 8 7 8 -4 0 -14 26 -28 52 -29 11 0
|
||||
39 70 33 80 -3 5 1 11 7 13 10 4 9 8 -2 16 -13 10 -8 15 12 12 8 -1 38 91 32
|
||||
98 -2 2 -10 -2 -18 -8 -9 -7 -17 -8 -20 -2 -3 5 0 11 6 14 7 2 -18 14 -56 26
|
||||
-38 12 -71 19 -74 16 -3 -3 -11 0 -18 6 -8 6 -20 9 -28 6 -8 -3 -16 -2 -18 3
|
||||
-5 15 -154 30 -286 29 -70 0 -131 -2 -137 -4z m21 -30 c-6 -18 3 -47 14 -40 4
|
||||
2 18 -7 30 -20 27 -29 17 -34 -14 -7 -20 16 -20 16 -7 -1 23 -29 66 -47 112
|
||||
-47 34 0 40 3 35 16 -5 14 -4 15 9 4 13 -11 19 -9 36 6 12 11 29 35 38 54 15
|
||||
31 20 35 58 35 24 0 43 -2 43 -5 0 -11 115 -27 121 -18 3 5 9 2 13 -7 4 -13
|
||||
14 -16 34 -12 15 2 36 1 47 -4 16 -7 14 -8 -10 -5 l-30 5 32 -14 c39 -18 39
|
||||
-19 14 -83 -15 -38 -17 -52 -8 -61 9 -9 8 -11 -7 -5 -16 6 -18 4 -12 -19 3
|
||||
-14 2 -29 -4 -32 -6 -3 -7 1 -4 9 6 16 -9 20 -73 19 -12 0 -20 4 -17 8 3 5 -4
|
||||
9 -15 9 -20 0 -21 -5 -21 -151 l0 -151 -27 8 c-16 4 -38 7 -50 7 -19 -1 -21 2
|
||||
-13 17 15 28 12 57 -6 57 -11 0 -15 -8 -12 -27 4 -24 1 -27 -23 -26 -15 1 -25
|
||||
4 -22 9 2 4 -2 7 -10 7 -9 0 -14 -10 -14 -25 0 -13 3 -22 8 -19 5 3 6 -1 3 -9
|
||||
-2 -7 0 -25 5 -40 8 -20 7 -25 -3 -21 -7 3 -13 -2 -13 -11 0 -12 7 -15 26 -11
|
||||
14 3 21 3 14 0 -7 -3 -9 -12 -6 -20 3 -7 10 -11 16 -7 5 3 7 1 4 -4 -9 -14 3
|
||||
-74 12 -68 4 2 7 -6 7 -18 -1 -32 32 -34 44 -3 5 14 7 34 5 46 -3 13 0 18 7
|
||||
14 8 -5 9 -1 5 10 -4 9 -3 15 2 12 5 -3 12 1 15 10 3 8 2 12 -4 9 -6 -3 -10
|
||||
-1 -10 4 0 13 3 13 24 5 13 -5 16 -24 16 -103 0 -63 4 -102 13 -111 10 -12 9
|
||||
-12 -4 -2 -13 10 -88 12 -300 10 l-284 -3 3 303 3 302 -26 0 c-14 0 -25 -4
|
||||
-25 -10 0 -5 -7 -6 -17 -3 -9 4 -14 2 -10 -3 4 -6 -6 -9 -24 -6 -28 4 -41 -11
|
||||
-19 -21 6 -3 5 -4 -2 -3 -7 2 -13 11 -13 22 -1 32 -25 104 -35 104 -5 0 -6 7
|
||||
-3 17 4 10 2 14 -5 9 -7 -4 -10 2 -8 17 3 29 14 47 29 47 7 0 3 -8 -8 -17
|
||||
l-20 -16 20 8 c11 4 28 10 37 12 10 3 18 9 18 13 0 4 6 7 12 7 22 -2 158 26
|
||||
158 32 0 3 20 6 45 5 25 0 45 3 45 8 0 4 3 8 6 8 3 0 4 -7 0 -15z m237 -5 c-3
|
||||
-9 1 -8 11 4 9 11 16 15 16 9 0 -6 -7 -16 -15 -23 -8 -7 -15 -9 -15 -4 0 10
|
||||
-29 -28 -30 -40 0 -4 8 -5 17 -2 15 6 15 4 -2 -14 -22 -24 -37 -26 -28 -4 5
|
||||
14 3 15 -15 5 -26 -14 -77 -14 -103 -1 -10 6 -27 27 -38 48 l-19 37 113 0 c95
|
||||
0 112 -2 108 -15z"/>
|
||||
<path d="M346 641 c-3 -5 1 -12 10 -15 23 -9 36 -7 29 4 -3 6 1 7 9 4 9 -3 16
|
||||
-1 16 5 0 13 -56 15 -64 2z"/>
|
||||
<path d="M440 640 c0 -16 33 -26 38 -12 2 7 8 10 13 6 5 -3 9 0 9 5 0 6 -13
|
||||
11 -30 11 -16 0 -30 -5 -30 -10z"/>
|
||||
<path d="M530 641 c0 -12 37 -24 50 -16 20 12 10 25 -20 25 -16 0 -30 -4 -30
|
||||
-9z"/>
|
||||
<path d="M620 641 c0 -12 37 -24 50 -16 20 12 10 25 -20 25 -16 0 -30 -4 -30
|
||||
-9z"/>
|
||||
<path d="M310 593 c0 -20 5 -30 16 -30 10 0 14 8 12 25 -4 37 -28 40 -28 5z"/>
|
||||
<path d="M697 613 c-13 -13 -7 -50 8 -50 10 0 15 10 15 29 0 27 -9 35 -23 21z"/>
|
||||
<path d="M317 534 c-4 -4 -7 -20 -7 -36 0 -35 23 -34 28 1 4 25 -10 46 -21 35z"/>
|
||||
<path d="M692 503 c4 -39 28 -42 28 -4 0 21 -5 31 -16 31 -11 0 -14 -8 -12
|
||||
-27z"/>
|
||||
<path d="M312 415 c4 -33 22 -33 26 0 2 18 -1 25 -13 25 -12 0 -15 -7 -13 -25z"/>
|
||||
<path d="M312 329 c2 -19 8 -33 13 -31 15 3 12 55 -3 60 -10 3 -13 -5 -10 -29z"/>
|
||||
<path d="M342 278 c3 -7 19 -14 37 -16 24 -3 32 0 29 10 -3 7 -19 14 -37 16
|
||||
-24 3 -32 0 -29 -10z"/>
|
||||
<path d="M443 275 c0 -10 10 -15 29 -15 18 0 28 5 28 15 0 10 -10 15 -28 15
|
||||
-19 0 -29 -5 -29 -15z"/>
|
||||
<path d="M530 275 c0 -10 10 -15 33 -15 22 0 28 3 18 9 -11 7 -11 9 0 14 8 3
|
||||
-1 6 -18 6 -23 1 -33 -4 -33 -14z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/assets/images/icon/xyz.png
Normal file
BIN
src/assets/images/icon/xyz.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,855 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div class="part-selector-toolbar" v-if="visible" :class="{active:!closePanel}">
|
||||
<div class="btn" @click="setClosePanel"><i class="fi fi-br-angle-left"></i></div>
|
||||
<!-- 顶部选区类型工具栏 -->
|
||||
<div class="toolbar-section">
|
||||
<div class="toolbar-header">
|
||||
<div class="header-title">{{ t("Canvas.GarmentPartSelector") }}</div>
|
||||
<!-- 移除关闭按钮,完全通过工具切换控制显示隐藏 -->
|
||||
</div>
|
||||
|
||||
<div class="tool-types">
|
||||
<div
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: selectionType === OperationType.LASSO },
|
||||
]"
|
||||
@click="setSelectionType(OperationType.LASSO)"
|
||||
>
|
||||
<svg-icon name="CFree" size="20" />
|
||||
<span>{{ $t("Canvas.freehandSketching") }}</span>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: selectionType === OperationType.LASSO_RECTANGLE },
|
||||
]"
|
||||
@click="setSelectionType(OperationType.LASSO_RECTANGLE)"
|
||||
>
|
||||
<svg-icon name="CRectangle" size="26" />
|
||||
<span>{{ $t("Canvas.rectangle") }}</span>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'tool-btn',
|
||||
{ active: selectionType === OperationType.LASSO_ELLIPSE },
|
||||
]"
|
||||
@click="setSelectionType(OperationType.LASSO_ELLIPSE)"
|
||||
>
|
||||
<svg-icon name="CEllipse" size="24" />
|
||||
<span>{{ $t("Canvas.ellipse") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<!-- 底部选区操作工具栏 -->
|
||||
<div class="tool-actions">
|
||||
<div class="action-btn" @click="copySelectionToNewLayer">
|
||||
<svg-icon name="CPaste" size="16" />
|
||||
<span class="btn-text">{{ $t("Canvas.creation") }}</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="cutSelectionToNewLayer">
|
||||
<svg-icon name="CCut" size="26" />
|
||||
<span class="btn-text">{{ $t("Canvas.CreateAndCopy") }}</span>
|
||||
</div>
|
||||
<div class="action-btn" @click="clearSelectionContent">
|
||||
<svg-icon name="CClear" size="18" />
|
||||
<span class="btn-text">{{ $t("Canvas.TheClearlySelectedContent") }}</span>
|
||||
</div>
|
||||
<!-- <button
|
||||
class="action-btn"
|
||||
@click="addSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="添加"
|
||||
>
|
||||
<svg-icon name="plus" />
|
||||
<span class="btn-text">{{ $t("添加") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="removeSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="移除"
|
||||
>
|
||||
<svg-icon name="minus" />
|
||||
<span class="btn-text">{{ $t("移除") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="invertSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="反转"
|
||||
>
|
||||
<svg-icon name="flip-horizontal" />
|
||||
<span class="btn-text">{{ $t("反转") }}</span>
|
||||
</button> -->
|
||||
<!-- <button
|
||||
class="action-btn"
|
||||
@click="copySelectionToNewLayer"
|
||||
:disabled="!hasSelection"
|
||||
title="拷贝并粘贴"
|
||||
>
|
||||
<svg-icon name="copy" />
|
||||
<span class="btn-text">{{ $t("拷贝并粘贴") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="openFeatherDialog"
|
||||
:disabled="!hasSelection"
|
||||
title="羽化"
|
||||
>
|
||||
<svg-icon name="feather" />
|
||||
<span class="btn-text">{{ $t("羽化") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="fillSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="颜色填充"
|
||||
>
|
||||
<svg-icon name="fill-color" />
|
||||
<span class="btn-text">{{ $t("颜色填充") }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="action-btn"
|
||||
@click="clearSelection"
|
||||
:disabled="!hasSelection"
|
||||
title="清除"
|
||||
>
|
||||
<svg-icon name="trash" />
|
||||
<span class="btn-text">{{ $t("清除") }}</span>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 羽化设置弹窗 -->
|
||||
<div v-if="showFeatherDialog" class="dialog-overlay">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ $t("羽化") }}</h3>
|
||||
<button class="close-dialog-btn" @click="cancelFeather">×</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<div class="feather-control">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="50"
|
||||
v-model.number="featherAmount"
|
||||
class="slider-control"
|
||||
/>
|
||||
<div class="feather-value">{{ featherAmount }}px</div>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="cancel-btn" @click="cancelFeather">
|
||||
{{ $t("Canvas.close") }}
|
||||
</button>
|
||||
<button class="confirm-btn" @click="applyFeather">
|
||||
{{ $t("Canvas.confirmEdit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 颜色选择器 -->
|
||||
<div v-if="showColorPicker" class="dialog-overlay">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ $t("Canvas.SelectFillColor") }}</h3>
|
||||
<button class="close-dialog-btn" @click="cancelColorPicker">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<input type="color" v-model="fillColor" class="color-picker" />
|
||||
<div class="dialog-buttons">
|
||||
<button class="cancel-btn" @click="cancelColorPicker">
|
||||
{{ $t("Canvas.close") }}
|
||||
</button>
|
||||
<button class="confirm-btn" @click="confirmColorPicker">
|
||||
{{ $t("Canvas.confirmEdit") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
CreateSelectionCommand,
|
||||
InvertSelectionCommand,
|
||||
FeatherSelectionCommand,
|
||||
FillSelectionCommand,
|
||||
// CopySelectionToNewLayerCommand,
|
||||
// ClearSelectionContentCommand,
|
||||
} from "../commands/SelectionCommands";
|
||||
import { ToolCommand } from "../commands/ToolCommands";
|
||||
import {
|
||||
LassoCutoutCommand,
|
||||
ClearSelectionCommand,
|
||||
// CutSelectionToNewLayerCommand,
|
||||
} from "../commands/LassoCutoutCommand";
|
||||
|
||||
import { OperationType } from "../utils/layerHelper";
|
||||
import { ClearSelectionContentCommand } from "../commands/ClearSelectionContentCommand";
|
||||
import { CutSelectionToNewLayerCommand } from "../commands/CutSelectionToNewLayerCommand";
|
||||
|
||||
const props = defineProps({
|
||||
canvas: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commandManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selectionManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
layerManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
toolManager: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activeTool: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false);
|
||||
const selectionType = ref("rectangle");
|
||||
const featherAmount = ref(0);
|
||||
const fillColor = ref("#000000");
|
||||
const hasSelection = ref(false);
|
||||
const showFeatherDialog = ref(false);
|
||||
const showColorPicker = ref(false);
|
||||
//打开隐藏操作面板
|
||||
const closePanel = ref(false)
|
||||
const setClosePanel = ()=>{
|
||||
closePanel.value = !closePanel.value
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n();
|
||||
|
||||
onMounted(() => {
|
||||
// 为选区管理器添加监听,以便在选区变化时更新状态
|
||||
if (props.selectionManager) {
|
||||
// 在选区管理器中添加选区变化的监听
|
||||
checkSelectionStatus();
|
||||
|
||||
// 设置选区状态变化的回调
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.selectionManager.onSelectionChanged = () => {
|
||||
checkSelectionStatus();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 activeTool 变化
|
||||
watch(
|
||||
() => props.activeTool,
|
||||
(newTool) => {
|
||||
// 当工具为LASSO或AREA类型时显示选区面板
|
||||
const selectionTools = [
|
||||
OperationType.PART,
|
||||
OperationType.PART_RECTANGLE,
|
||||
OperationType.PART_BRUSH,
|
||||
OperationType.PART_ERASER
|
||||
];
|
||||
|
||||
if (selectionTools.includes(newTool)) {
|
||||
show();
|
||||
// 根据工具类型设置选区类型
|
||||
selectionType.value = newTool;
|
||||
|
||||
// 更新选区管理器的选区类型
|
||||
if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(selectionType.value);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示面板
|
||||
*/
|
||||
function show() {
|
||||
visible.value = true;
|
||||
closePanel.value = true
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭面板
|
||||
*/
|
||||
function close() {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选区类型
|
||||
*/
|
||||
function setSelectionType(type) {
|
||||
selectionType.value = type;
|
||||
|
||||
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager
|
||||
if (props.toolManager) {
|
||||
props.toolManager.setToolWithCommand(type);
|
||||
}
|
||||
|
||||
// 备用方案:如果没有 toolManager,直接更新 selectionManager
|
||||
else if (props.selectionManager) {
|
||||
props.selectionManager.setSelectionType(type);
|
||||
props.selectionManager.setupSelectionEvents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查选区状态
|
||||
*/
|
||||
function checkSelectionStatus() {
|
||||
hasSelection.value =
|
||||
props.selectionManager &&
|
||||
props.selectionManager.getSelectionObject() !== null;
|
||||
|
||||
// 同步羽化值
|
||||
if (hasSelection.value) {
|
||||
featherAmount.value = props.selectionManager.getFeatherAmount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选区
|
||||
*/
|
||||
function addSelection() {
|
||||
// TODO: 实现添加选区功能
|
||||
console.log("添加选区功能尚未实现");
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选区
|
||||
*/
|
||||
function removeSelection() {
|
||||
// TODO: 实现移除选区功能
|
||||
console.log("移除选区功能尚未实现");
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转选区
|
||||
*/
|
||||
function invertSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new InvertSelectionCommand({
|
||||
canvas: props.canvas,
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选区
|
||||
*/
|
||||
function clearSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new ClearSelectionCommand({
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用羽化效果
|
||||
*/
|
||||
function applyFeather() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new FeatherSelectionCommand({
|
||||
selectionManager: props.selectionManager,
|
||||
featherAmount: featherAmount.value,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充选区
|
||||
*/
|
||||
function fillSelection() {
|
||||
if (!hasSelection.value) return;
|
||||
showColorPicker.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 套索抠图到新图层
|
||||
*/
|
||||
function copySelectionToNewLayer() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new LassoCutoutCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
toolManager: props.toolManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 剪切选区到新图层
|
||||
*/
|
||||
function cutSelectionToNewLayer() {
|
||||
if (!hasSelection.value) return;
|
||||
props.commandManager.execute(
|
||||
new CutSelectionToNewLayerCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
toolManager: props.toolManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除选区内容
|
||||
*/
|
||||
function clearSelectionContent() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new ClearSelectionContentCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开羽化设置弹窗
|
||||
*/
|
||||
function openFeatherDialog() {
|
||||
showFeatherDialog.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消羽化设置
|
||||
*/
|
||||
function cancelFeather() {
|
||||
showFeatherDialog.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认羽化设置
|
||||
*/
|
||||
function confirmFeather() {
|
||||
applyFeather();
|
||||
showFeatherDialog.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消颜色选择
|
||||
*/
|
||||
function cancelColorPicker() {
|
||||
showColorPicker.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认颜色选择
|
||||
*/
|
||||
function confirmColorPicker() {
|
||||
if (!hasSelection.value) return;
|
||||
|
||||
props.commandManager.execute(
|
||||
new FillSelectionCommand({
|
||||
canvas: props.canvas,
|
||||
layerManager: props.layerManager,
|
||||
selectionManager: props.selectionManager,
|
||||
color: fillColor.value,
|
||||
})
|
||||
);
|
||||
checkSelectionStatus();
|
||||
showColorPicker.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.part-selector-toolbar {
|
||||
position: absolute;
|
||||
bottom: 22px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
max-width: min(90vw, 640px);
|
||||
margin: 0 auto;
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
color: #333;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
user-select: none;
|
||||
&.active{
|
||||
transform: translateY(100%);
|
||||
> .btn{
|
||||
> i{
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
> .btn{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 22px;
|
||||
|
||||
> i{
|
||||
font-size: 1.4rem;
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板和手机适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.part-selector-toolbar {
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
max-width: calc(100vw - 30px);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.part-selector-toolbar {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
max-width: calc(100vw - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.part-selector-toolbar.is-active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.toolbar-header {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
padding: 8px 0;
|
||||
// border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
.header-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 3px 0;
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s ease;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.header-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
padding: 0 3rem 1.2rem;
|
||||
}
|
||||
|
||||
.tool-types {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tool-btn span {
|
||||
margin-top: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-btn svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
height: 1px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tool-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 5px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* 平板适配 - 每行4个按钮 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 - 每行3个按钮 */
|
||||
@media screen and (max-width: 480px) {
|
||||
.tool-actions {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
.c-svg {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
color: #007aff;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 对话框样式 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
-webkit-backdrop-filter: blur(5px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
background-color: #ffffff;
|
||||
border-radius: 12px;
|
||||
width: 280px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-dialog-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.feather-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.slider-control {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.slider-control::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #007aff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.feather-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
background-color: #007aff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div class="repeat-setting">
|
||||
<div class="title">{{ t("Canvas.repeatSetting") }}</div>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.angle") }}</span>
|
||||
<angle-tool
|
||||
:angle="angle"
|
||||
@input="(e) => emit('inputFillAngle', e)"
|
||||
@change="(e) => emit('changeFillAngle', e)"
|
||||
style-type="2"
|
||||
/>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.scale") }}</span>
|
||||
<slider
|
||||
@@ -22,7 +23,6 @@
|
||||
@change="changeFillScale"
|
||||
/>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">Gap X</span>
|
||||
<slider
|
||||
@@ -36,7 +36,6 @@
|
||||
@change="(e) => emit('changeFill_Gap', e, gapY)"
|
||||
/>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">Gap Y</span>
|
||||
<slider
|
||||
@@ -50,14 +49,23 @@
|
||||
@change="(e) => emit('changeFill_Gap', gapX, e)"
|
||||
/>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="repeat-setting-item">
|
||||
<span class="label">{{ t("Canvas.offset") }}</span>
|
||||
<offset-tool
|
||||
:top="(props.object.fill?.offsetY / props.object.height) * 100"
|
||||
:left="(props.object.fill?.offsetX / props.object.width) * 100"
|
||||
:left="offsetX"
|
||||
:top="offsetY"
|
||||
@input="(e) => emit('inputFillOffset', e)"
|
||||
@change="(e) => emit('changeFillOffset', e)"
|
||||
:show-dish="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="repeat-setting-item offset">
|
||||
<offset-tool
|
||||
:left="offsetX"
|
||||
:top="offsetY"
|
||||
@input="(e) => emit('inputFillOffset', e)"
|
||||
@change="(e) => emit('changeFillOffset', e)"
|
||||
:show-input="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,6 +96,12 @@
|
||||
});
|
||||
const gapX = computed(() => props.object.fill_?.gapX || 0);
|
||||
const gapY = computed(() => props.object.fill_?.gapY || 0);
|
||||
const offsetX = computed(
|
||||
() => (props.object.fill?.offsetX / props.object.width) * 100
|
||||
);
|
||||
const offsetY = computed(
|
||||
() => (props.object.fill?.offsetY / props.object.height) * 100
|
||||
);
|
||||
const emit = defineEmits([
|
||||
"inputFillAngle",
|
||||
"changeFillAngle",
|
||||
@@ -111,23 +125,36 @@
|
||||
<style scoped lang="less">
|
||||
.repeat-setting {
|
||||
user-select: none;
|
||||
width: 228px;
|
||||
> .title {
|
||||
line-height: 35px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: -12px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
> .repeat-setting-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
//虚线
|
||||
margin-bottom: 10px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&.offset {
|
||||
justify-content: center;
|
||||
}
|
||||
> .label {
|
||||
min-width: 50px;
|
||||
font-size: 14px;
|
||||
min-width: 68px;
|
||||
font-size: 12px;
|
||||
}
|
||||
> .angle-tool {
|
||||
&:not(.offset) > div {
|
||||
width: 120px;
|
||||
flex: 1;
|
||||
}
|
||||
> .slider {
|
||||
--slider-thumb-color1: #000;
|
||||
--slider-thumb-color2: #eee;
|
||||
}
|
||||
}
|
||||
> p {
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-bottom: 1px dashed #e5e5e5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -132,7 +132,6 @@
|
||||
v-if="v.type === 'rect'"
|
||||
trigger="click"
|
||||
destroyTooltipOnHide
|
||||
:title="t('Canvas.repeatSetting')"
|
||||
>
|
||||
<template #content>
|
||||
<repeat-setting
|
||||
@@ -784,6 +783,7 @@
|
||||
}
|
||||
> .list {
|
||||
display: flex;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -166,6 +166,19 @@ const normalToolsList = ref([
|
||||
icon: { name: "CFont", size: "20" },
|
||||
class: "text-btn",
|
||||
},
|
||||
{
|
||||
id: OperationType.PART,
|
||||
title: t("Canvas.GarmentPartSelector"),
|
||||
action: () => selectTool(OperationType.PART),
|
||||
icon: { name: "CPart", size: "28" },
|
||||
class: "part-btn",
|
||||
activeList: [
|
||||
OperationType.PART,
|
||||
OperationType.PART_RECTANGLE,
|
||||
OperationType.PART_BRUSH,
|
||||
OperationType.PART_ERASER,
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
title: t("Canvas.help"),
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
<template>
|
||||
<div class="angle-tool" :disabled="disabled">
|
||||
<div
|
||||
ref="dishRef"
|
||||
class="dish"
|
||||
@mousedown.stop="mousedown"
|
||||
@touchmove.stop="mousedown"
|
||||
>
|
||||
<div class="pointer" :style="{ transform: `rotate(${angle}deg)` }">
|
||||
<span></span>
|
||||
<template v-if="styleType === '1'">
|
||||
<div
|
||||
ref="dishRef"
|
||||
class="dish"
|
||||
@mousedown.stop="mousedown"
|
||||
@touchmove.stop="mousedown"
|
||||
>
|
||||
<div
|
||||
class="pointer"
|
||||
:style="{ transform: `rotate(${angle}deg)` }"
|
||||
>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input
|
||||
type="number"
|
||||
v-model="angle"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
<div class="input">
|
||||
<input
|
||||
type="number"
|
||||
v-model="angle"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<my-input
|
||||
v-if="styleType === '2'"
|
||||
v-model="angle"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
after="°"
|
||||
icon="icon-angle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import { calculateAngle } from "../../utils/helper";
|
||||
import MyInput from "./MyInput.vue";
|
||||
// Props
|
||||
const props = defineProps({
|
||||
styleType: {
|
||||
type: String,
|
||||
default: "1",
|
||||
},
|
||||
angle: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -139,5 +160,8 @@
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
> .my-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="my-input">
|
||||
<span class="decorate"></span>
|
||||
<span v-show="icon" :class="['iconfont', icon]"></span>
|
||||
<span v-show="before" class="before">{{ before }}</span>
|
||||
<input v-bind="$attrs" :value="modelValue" @input="onInput" />
|
||||
<span v-show="after" class="after">{{ after }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
const props = defineProps({
|
||||
modelValue: { type: Number, default: 0 },
|
||||
icon: { default: "", type: String },
|
||||
before: { default: "", type: String },
|
||||
after: { default: "", type: String },
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "input"]);
|
||||
const onInput = (e) => {
|
||||
const value = e.target.value;
|
||||
emit("update:modelValue", value);
|
||||
emit("input", value);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="less">
|
||||
.my-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 20px;
|
||||
padding: 0 4px 0 2px;
|
||||
> .decorate {
|
||||
width: 2px;
|
||||
background-color: rgba(230, 230, 231, 1);
|
||||
border-radius: 3px;
|
||||
height: 85%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
> .iconfont {
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .before {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
> .after {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
}
|
||||
> input {
|
||||
font-size: 12px;
|
||||
width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
outline: none;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,84 +1,100 @@
|
||||
<template>
|
||||
<div class="offset-tool">
|
||||
<div class="input" v-show="showInput">
|
||||
<my-input
|
||||
v-model="left"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
type="number"
|
||||
before="X"
|
||||
after="%"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
<my-input
|
||||
v-model="top"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
type="number"
|
||||
before="Y"
|
||||
after="%"
|
||||
:min="-100"
|
||||
:max="100"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="dish"
|
||||
@mousedown="mousedown"
|
||||
@touchstart="mousedown"
|
||||
ref="dishRef"
|
||||
v-show="showDish"
|
||||
>
|
||||
<span
|
||||
:style="{ top: data.top + '%', left: data.left + '%' }"
|
||||
></span>
|
||||
<img src="/src/assets/images/icon/xyz.png" />
|
||||
<span class="ball" :style="ballStyle"></span>
|
||||
<span class="tip x">X: {{ left }}%</span>
|
||||
<span class="tip y">Y: {{ top }}%</span>
|
||||
<span class="line x"></span>
|
||||
<span class="line y"></span>
|
||||
<span class="line z" :style="lineZStyle"></span>
|
||||
</div>
|
||||
<input
|
||||
class="top"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
v-model="data.top"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
<input
|
||||
class="left"
|
||||
type="range"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.1"
|
||||
v-model="data.left"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
/>
|
||||
<span class="tip"
|
||||
>x:{{ tofix(data.left) }}% y:{{ tofix(data.top) }}%</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import { ref, defineProps, defineEmits, watch, computed } from "vue";
|
||||
import MyInput from "./MyInput.vue";
|
||||
const props = defineProps({
|
||||
top: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
default: 0,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showInput: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showDish: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const tofix = (v: number | string) => Number(Number(v).toFixed(1));
|
||||
const emit = defineEmits(["change", "input"]);
|
||||
const data = reactive({
|
||||
top: tofix(props.top),
|
||||
left: tofix(props.left),
|
||||
});
|
||||
watch(
|
||||
() => props.top,
|
||||
(v) => (data.top = tofix(v))
|
||||
);
|
||||
// 工具的实际坐标 -100 ~ 100
|
||||
const top = ref(Math.round(props.top));
|
||||
const left = ref(Math.round(props.left));
|
||||
|
||||
// 原点的坐标 0 ~ 100
|
||||
const ballStyle = computed(() => ({
|
||||
top: 50 + Number(top.value) / 2 + "%",
|
||||
left: 50 + Number(left.value) / 2 + "%",
|
||||
}));
|
||||
watch(
|
||||
() => props.left,
|
||||
(v) => (data.left = tofix(v))
|
||||
(v) => (left.value = Math.round(v))
|
||||
);
|
||||
watch(
|
||||
() => props.top,
|
||||
(v) => (top.value = Math.round(v))
|
||||
);
|
||||
const dishRef = ref<HTMLDivElement>();
|
||||
const mousedown = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const mousemove = (e: MouseEvent | TouchEvent) => {
|
||||
if (!dishRef.value) return;
|
||||
const { left, top, width, height } =
|
||||
dishRef.value.getBoundingClientRect();
|
||||
const rect = dishRef.value.getBoundingClientRect();
|
||||
const X = e.clientX || (e as TouchEvent).touches[0].clientX;
|
||||
const Y = e.clientY || (e as TouchEvent).touches[0].clientY;
|
||||
var x = ((X - left) / width) * 100;
|
||||
var y = ((Y - top) / height) * 100;
|
||||
var x = ((X - rect.left) / rect.width) * 100;
|
||||
var y = ((Y - rect.top) / rect.height) * 100;
|
||||
if (x < 0) x = 0;
|
||||
if (x > 100) x = 100;
|
||||
if (y < 0) y = 0;
|
||||
if (y > 100) y = 100;
|
||||
data.left = tofix(x);
|
||||
data.top = tofix(y);
|
||||
left.value = Math.round((x - 50) * 2);
|
||||
top.value = Math.round((y - 50) * 2);
|
||||
onInput();
|
||||
};
|
||||
mousemove(e);
|
||||
@@ -94,96 +110,125 @@
|
||||
document.addEventListener("mouseup", mouseup);
|
||||
document.addEventListener("touchend", mouseup);
|
||||
};
|
||||
const onInput = () => emit("input", { ...data });
|
||||
const onInput = () => {
|
||||
emit("input", { left: left.value, top: top.value });
|
||||
};
|
||||
var changeTime: any = null;
|
||||
const onChange = () => {
|
||||
clearTimeout(changeTime);
|
||||
changeTime = setTimeout(() => emit("change", { ...data }), 500);
|
||||
changeTime = setTimeout(() => {
|
||||
emit("change", {
|
||||
left: left.value,
|
||||
top: top.value,
|
||||
});
|
||||
}, 500);
|
||||
};
|
||||
// var offsetTime = null;
|
||||
// watch(data, (v) => {
|
||||
// const obj = { ...v };
|
||||
// emit("input", obj);
|
||||
// clearTimeout(offsetTime);
|
||||
// offsetTime = setTimeout(() => emit("change", obj), 50);
|
||||
// });
|
||||
|
||||
// defineExpose({
|
||||
// open,
|
||||
// close,
|
||||
// });
|
||||
const lineZStyle = computed(() => ({
|
||||
"--rotateZ": calculateAngle(0, 0, left.value, top.value) + "deg",
|
||||
width: calculateDistance(0, 0, left.value, top.value) / 2 + "%",
|
||||
}));
|
||||
// 计算角度
|
||||
function calculateAngle(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y1 - y2;
|
||||
let angle = Math.atan2(deltaX, deltaY) * (180 / Math.PI) - 90;
|
||||
return angle;
|
||||
}
|
||||
// 计算距离
|
||||
function calculateDistance(x1: number, y1: number, x2: number, y2: number) {
|
||||
const deltaX = x2 - x1;
|
||||
const deltaY = y2 - y1;
|
||||
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
return distance;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.offset-tool {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--gap: 15px;
|
||||
> .input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> * {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .dish {
|
||||
margin: var(--gap) 0 0 var(--gap);
|
||||
flex: 1;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
> span {
|
||||
background-color: #f6f6f6;
|
||||
margin-top: 24px;
|
||||
> * {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
top: 0%;
|
||||
left: 0%;
|
||||
}
|
||||
> img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
> .ball {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: #000;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px solid #fff;
|
||||
background-color: #333;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0px 0.68px 1.7px 0px rgba(0, 0, 0, 0.26);
|
||||
}
|
||||
}
|
||||
> .tip {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 0;
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
color: #666;
|
||||
}
|
||||
> input.left {
|
||||
right: 0;
|
||||
}
|
||||
> input.top {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform-origin: left bottom;
|
||||
transform: rotate(90deg) translateX(-100%);
|
||||
}
|
||||
> input {
|
||||
position: absolute;
|
||||
width: calc(100% - var(--gap));
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||
// outline: none;
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4285f4; /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
> .tip {
|
||||
font-size: 10px;
|
||||
color: #000;
|
||||
line-height: 24px;
|
||||
&.x {
|
||||
top: 50%;
|
||||
right: 0%;
|
||||
transform: translate(100%, -50%);
|
||||
padding-left: 6px;
|
||||
}
|
||||
&.y {
|
||||
top: 0%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
background: #3b77db;
|
||||
transform: scale(1.1);
|
||||
> .line {
|
||||
border-color: #d9d9d9;
|
||||
border-style: dashed;
|
||||
border-width: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
&.x {
|
||||
width: 100%;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
&.y {
|
||||
height: 100%;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
&.z {
|
||||
width: 50%;
|
||||
border-top-width: 1px;
|
||||
border-color: #454754;
|
||||
transform: translate(0%, -50%) rotateZ(var(--rotateZ));
|
||||
transform-origin: left center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<div class="slider" :disabled="disabled">
|
||||
<div class="input-range">
|
||||
<span
|
||||
class="tip"
|
||||
:style="{
|
||||
'--progress': (value - props.min) / (props.max - props.min),
|
||||
}"
|
||||
>{{ props.tipFormatter(value) }}</span
|
||||
>
|
||||
<div
|
||||
class="input-range"
|
||||
:style="{
|
||||
'--progress': (value - props.min) / (props.max - props.min),
|
||||
}"
|
||||
>
|
||||
<span class="tip">{{ props.tipFormatter(value) }}</span>
|
||||
<input
|
||||
type="range"
|
||||
v-model="value"
|
||||
@@ -20,8 +19,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="input" v-show="isInput">
|
||||
<input
|
||||
type="number"
|
||||
<my-input
|
||||
v-model="value"
|
||||
:min="props.min"
|
||||
:max="props.max"
|
||||
@@ -29,6 +27,7 @@
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
:disabled="disabled"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,6 +35,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
import MyInput from "./MyInput.vue";
|
||||
const props = defineProps({
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -86,9 +86,10 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--input-thumb-size: 12px;
|
||||
width: 150px;
|
||||
// &:focus-within,
|
||||
--input-thumb-size: 10px;
|
||||
--backcolor1: var(--slider-thumb-color1, #4285f4);
|
||||
--backcolor2: var(--slider-thumb-color2, rgba(0, 0, 0, 0.1));
|
||||
&:hover {
|
||||
> .input-range > .tip {
|
||||
display: block;
|
||||
@@ -103,21 +104,26 @@
|
||||
appearance: none;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
background: rgba(0, 0, 0, 0.1); /* 更柔和的颜色 */
|
||||
outline: none;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
var(--backcolor1) 0%,
|
||||
var(--backcolor1) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) calc(var(--progress) * 100%),
|
||||
var(--backcolor2) 100%
|
||||
);
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: var(--input-thumb-size);
|
||||
height: var(--input-thumb-size);
|
||||
border-radius: 50%;
|
||||
background: #4285f4; /* 蓝色滑块 */
|
||||
background: var(--backcolor1); /* 蓝色滑块 */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&::-webkit-slider-thumb:hover {
|
||||
background: #3b77db;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import LiquifyPanel from "./components/LiquifyPanel.vue"; // 引入液化编辑
|
||||
import PalletPanel from "./components/PalletPanel/index.vue";
|
||||
import SelectMenuPanel from "./components/SelectMenuPanel/index.vue"; // 引入选择工具菜单组件
|
||||
import SelectionPanel from "./components/SelectionPanel.vue"; // 引入选区面板
|
||||
import PartSelectorPanel from "./components/PartSelectorPanel.vue"; // 引入部件选取面板
|
||||
import { LayerType, OperationType } from "./utils/layerHelper.js";
|
||||
import { ToolManager } from "./managers/ToolManager.js";
|
||||
import { fabric } from "fabric-with-all";
|
||||
@@ -1248,6 +1249,18 @@ defineExpose({
|
||||
:activeTool="activeTool"
|
||||
/>
|
||||
|
||||
<!-- 部件选取面板 -->
|
||||
<PartSelectorPanel
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
:canvas="canvasManager && canvasManager.canvas"
|
||||
:commandManager="commandManager"
|
||||
:selectionManager="selectionManager"
|
||||
:layerManager="layerManager"
|
||||
:canvasManager="canvasManager"
|
||||
:toolManager="toolManager"
|
||||
:activeTool="activeTool"
|
||||
/>
|
||||
|
||||
<!-- 文本编辑面板 -->
|
||||
<TextEditorPanel
|
||||
v-if="canvasManagerLoaded && !enabledRedGreenMode"
|
||||
|
||||
@@ -67,6 +67,12 @@ export class ToolManager {
|
||||
|
||||
// 工具列表 - 与OperationType保持一致
|
||||
this.tools = {
|
||||
// 禁用工具
|
||||
[OperationType.DISABLED]: {
|
||||
name: "禁用工具",
|
||||
icon: "disabled",
|
||||
cursor: "not-allowed",
|
||||
},
|
||||
// 基础工具
|
||||
[OperationType.SELECT]: {
|
||||
name: "选择工具",
|
||||
@@ -83,6 +89,7 @@ export class ToolManager {
|
||||
shortcut: "B",
|
||||
setup: this.setupBrushTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.ERASER]: {
|
||||
name: "橡皮擦",
|
||||
@@ -91,6 +98,7 @@ export class ToolManager {
|
||||
shortcut: "E",
|
||||
setup: this.setupEraserTool.bind(this),
|
||||
allowedInRedGreen: true, // 红绿图模式允许橡皮擦
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.EYEDROPPER]: {
|
||||
name: "吸色工具",
|
||||
@@ -117,6 +125,7 @@ export class ToolManager {
|
||||
shortcut: "L",
|
||||
setup: this.setupLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.LASSO_RECTANGLE]: {
|
||||
name: "矩形套索工具",
|
||||
@@ -126,6 +135,7 @@ export class ToolManager {
|
||||
altKey: true,
|
||||
setup: this.setupRectangleLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.LASSO_ELLIPSE]: {
|
||||
name: "椭圆形套索工具",
|
||||
@@ -135,6 +145,7 @@ export class ToolManager {
|
||||
altKey: true,
|
||||
setup: this.setupEllipseLassoTool.bind(this),
|
||||
allowedInRedGreen: false,
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
|
||||
// 选区工具 - 只需要矩形选区
|
||||
@@ -164,6 +175,7 @@ export class ToolManager {
|
||||
shortcut: "J",
|
||||
setup: this.setupLiquifyTool.bind(this),
|
||||
allowedInRedGreen: false, // 红绿图模式不允许液化
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.TEXT]: {
|
||||
name: "文本工具",
|
||||
@@ -174,6 +186,36 @@ export class ToolManager {
|
||||
allowedInRedGreen: false, // 红绿图模式不允许文本
|
||||
},
|
||||
|
||||
// 部件选取工具
|
||||
[OperationType.PART]: {
|
||||
name: "部件选取工具",
|
||||
icon: "part",
|
||||
cursor: "crosshair",
|
||||
// setup: this.setupLassoTool.bind(this),
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.PART_RECTANGLE]: {
|
||||
name: "部件选取工具-矩形",
|
||||
icon: "part",
|
||||
cursor: "crosshair",
|
||||
// setup: this.setupRectangleLassoTool.bind(this),
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.PART_BRUSH]: {
|
||||
name: "部件选取工具-画笔",
|
||||
icon: "part",
|
||||
cursor: "crosshair",
|
||||
// setup: this.setupEllipseLassoTool.bind(this),
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
[OperationType.PART_ERASER]: {
|
||||
name: "部件选取工具-橡皮擦",
|
||||
icon: "part",
|
||||
cursor: "crosshair",
|
||||
// setup: this.setupEllipseLassoTool.bind(this),
|
||||
specialLayerDisabled: true,
|
||||
},
|
||||
|
||||
// 红绿图模式专用工具
|
||||
[OperationType.RED_BRUSH]: {
|
||||
name: "红色笔刷",
|
||||
@@ -331,8 +373,9 @@ export class ToolManager {
|
||||
* @param {String} toolId 工具ID
|
||||
*/
|
||||
setTool(toolId) {
|
||||
const tool = this.tools[toolId];
|
||||
// 检查工具是否存在
|
||||
if (!this.tools[toolId]) {
|
||||
if (!tool) {
|
||||
console.error(`工具 '${toolId}' 不存在`);
|
||||
return;
|
||||
}
|
||||
@@ -348,15 +391,20 @@ export class ToolManager {
|
||||
console.warn(`工具 '${toolId}' 只能在红绿图模式下使用`);
|
||||
return;
|
||||
}
|
||||
if(tool?.specialLayerDisabled && this.checkToolCanOperateSelectedObject()){
|
||||
console.warn(`工具 '${toolId}' 不能在当前选中对象上操作`);
|
||||
toolId = OperationType.DISABLED;
|
||||
}
|
||||
|
||||
// 保存先前的工具
|
||||
// 保存先前的工具
|
||||
this.previousTool = this.activeTool.value;
|
||||
|
||||
// 取消画布的选中状态
|
||||
this.canvas?.discardActiveObject();
|
||||
this.canvasManager?.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
this.canvas?.renderAll();
|
||||
|
||||
if(toolId !== OperationType.DISABLED){
|
||||
this.canvas?.discardActiveObject();
|
||||
this.canvasManager?.layerManager?.updateLayersObjectsInteractivity?.();
|
||||
this.canvas?.renderAll();
|
||||
}
|
||||
// 隐藏文本编辑面板
|
||||
this.hideTextEditor();
|
||||
|
||||
@@ -374,7 +422,6 @@ export class ToolManager {
|
||||
}
|
||||
|
||||
// 设置工具特定的状态
|
||||
const tool = this.tools[toolId];
|
||||
if (tool && typeof tool.setup === "function") {
|
||||
console.log(`画布切换工具:${tool.name}(${toolId})`)
|
||||
this.canvas.toolId = toolId;
|
||||
@@ -424,7 +471,7 @@ export class ToolManager {
|
||||
|
||||
const currentTool = this.activeTool.value;
|
||||
const tool = this.tools[currentTool];
|
||||
|
||||
if(tool?.specialLayerDisabled && this.checkToolCanOperateSelectedObject()) return;
|
||||
// 根据当前工具设置selection状态
|
||||
if (currentTool === OperationType.SELECT) {
|
||||
this.canvas.selection = true;
|
||||
@@ -460,19 +507,15 @@ export class ToolManager {
|
||||
|
||||
/**
|
||||
* 检查当前工具是否禁止操作当前选中的对象
|
||||
* @param {Boolean} isBrushTool 是否为画笔工具
|
||||
* @returns {Boolean} 是否可以切换
|
||||
*/
|
||||
checkToolCanOperateSelectedObject(isBrushTool = false) {
|
||||
checkToolCanOperateSelectedObject() {
|
||||
const layer = this.layerManager?.getActiveLayer();
|
||||
const isSpecialLayer = !!layer?.specialType;
|
||||
if (isSpecialLayer) {
|
||||
if(isBrushTool){
|
||||
this._disableBrushIndicator();
|
||||
}
|
||||
this._disableBrushIndicator();
|
||||
this.canvas.defaultCursor = "not-allowed";
|
||||
}
|
||||
console.log("===========",isSpecialLayer, this.canvas.defaultCursor);
|
||||
return isSpecialLayer;
|
||||
}
|
||||
|
||||
@@ -482,7 +525,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupBrushTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject(true)) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.selection = false;
|
||||
@@ -526,7 +569,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupEraserTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject(true)) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = true;
|
||||
this.canvas.selection = false;
|
||||
@@ -580,6 +623,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupLassoTool() {
|
||||
if (!this.canvas) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = false;
|
||||
@@ -676,7 +720,7 @@ export class ToolManager {
|
||||
*/
|
||||
setupLiquifyTool() {
|
||||
if (!this.canvas || !this.layerManager) return;
|
||||
if (this.checkToolCanOperateSelectedObject(true)) return;
|
||||
if (this.checkToolCanOperateSelectedObject()) return;
|
||||
|
||||
this.canvas.isDrawingMode = false;
|
||||
this.canvas.selection = false;
|
||||
|
||||
@@ -1005,3 +1005,92 @@ export async function base64ToCanvas(base64, scale = 1, sr = false) {
|
||||
image.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片边界跟踪算法(透明底)
|
||||
* @param {HTMLCanvasElement} canvas - canvas元素
|
||||
* @returns {Array} 边界点数组 [{x, y}, ...]
|
||||
*/
|
||||
export function traceImageContour(canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
// 查找起始点(第一个不透明像素)
|
||||
let startX = -1;
|
||||
let startY = -1;
|
||||
|
||||
outer: for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
startX = x;
|
||||
startY = y;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startX === -1) return []; // 没有不透明像素
|
||||
|
||||
// Moore-Neighbor边界跟踪算法
|
||||
const contour = [];
|
||||
const visited = new Set();
|
||||
const directions = [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[-1, 1],
|
||||
];
|
||||
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
let backtrackDir = 4; // 起始方向:右
|
||||
|
||||
do {
|
||||
const pointKey = `${currentX},${currentY}`;
|
||||
if (!visited.has(pointKey)) {
|
||||
contour.push({ x: currentX, y: currentY });
|
||||
visited.add(pointKey);
|
||||
}
|
||||
|
||||
// 从右方向开始顺时针查找
|
||||
let found = false;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const dir = (backtrackDir + i) % 8;
|
||||
const dx = directions[dir][0];
|
||||
const dy = directions[dir][1];
|
||||
const checkX = currentX + dx;
|
||||
const checkY = currentY + dy;
|
||||
|
||||
if (
|
||||
checkX >= 0 &&
|
||||
checkX < width &&
|
||||
checkY >= 0 &&
|
||||
checkY < height
|
||||
) {
|
||||
const index = (checkY * width + checkX) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
currentX = checkX;
|
||||
currentY = checkY;
|
||||
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
} while (
|
||||
!(currentX === startX && currentY === startY) &&
|
||||
visited.size < width * height
|
||||
);
|
||||
|
||||
return contour;
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export const SpecialType = {
|
||||
*/
|
||||
export const OperationType = {
|
||||
// 编辑器模式
|
||||
DISABLED: "disabled", // 禁用
|
||||
DRAW: "draw", // 绘画模式
|
||||
ERASER: "eraser", // 橡皮擦模式
|
||||
SELECT: "select", // 选择模式
|
||||
@@ -76,6 +77,12 @@ export const OperationType = {
|
||||
RED_BRUSH: "red_brush", // 红色笔刷
|
||||
GREEN_BRUSH: "green_brush", // 绿色笔刷
|
||||
|
||||
// 部件选取工具
|
||||
PART: "part", // 部件选取工具模式 - 点选模式
|
||||
PART_RECTANGLE: "part_rectangle", // 部件选取工具模式 - 矩形模式
|
||||
PART_BRUSH: "part_brush", // 部件选取工具模式 - 笔刷模式
|
||||
PART_ERASER: "part_eraser", // 部件选取工具模式 - 橡皮擦模式
|
||||
|
||||
// SHAPE: "shape", // 形状模式
|
||||
// 可以根据需要添加更多工具
|
||||
};
|
||||
|
||||
@@ -345,7 +345,7 @@ const otherData = {
|
||||
// angle: 0,
|
||||
// },
|
||||
{
|
||||
ifSingle: true,
|
||||
ifSingle: false,
|
||||
level2Type: "Pattern",
|
||||
designType: "Library",
|
||||
path: "/src/assets/images/canvas/yinhua1.jpg",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="test" ref="testRef">
|
||||
<!-- <div class="canvas-container">
|
||||
<div class="canvas-container">
|
||||
<canvas id="canvas"></canvas>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -35,53 +35,104 @@
|
||||
canvas1.height = height;
|
||||
const ctx1 = canvas1.getContext("2d");
|
||||
ctx1.drawImage(image, 0, 0, width, height);
|
||||
const data = ctx1.getImageData(0, 0, width, height);
|
||||
testRef.value.appendChild(canvas1);
|
||||
const testData = test(data);
|
||||
const canvas2 = document.createElement("canvas");
|
||||
canvas2.width = width;
|
||||
canvas2.height = height;
|
||||
const ctx2 = canvas2.getContext("2d");
|
||||
ctx2.putImageData(testData, 0, 0);
|
||||
testRef.value.appendChild(canvas2);
|
||||
const arr = traceImageContour(canvas1);
|
||||
const str = arr.map((v) => `${v.x} ${v.y}`).join(" L ");
|
||||
const path = new fabric.Path(`M ${str} z`);
|
||||
path.set({
|
||||
fill: "rgba(127, 255, 127, 0.3)",
|
||||
stroke: "#2AA81B",
|
||||
strokeWidth: 2,
|
||||
strokeDashArray: [8, 4],
|
||||
strokeLineCap: "round",// 折线端点样式
|
||||
strokeLineJoin: "bevel", // 折线连接样式
|
||||
strokeUniform: true, // 保持描边宽度不随缩放改变
|
||||
});
|
||||
canvas.add(path);
|
||||
};
|
||||
});
|
||||
// 获取图片轮廓点位
|
||||
function test(data) {
|
||||
// 找过的点位
|
||||
const visited = [];
|
||||
// 轮廓点位
|
||||
const contours = [];
|
||||
const { width, height } = data;
|
||||
function cd(x, y) {
|
||||
const arr = [
|
||||
[x, y], // 当前
|
||||
[x, y - 1], // 上
|
||||
[x + 1, y], // 右
|
||||
[x, y + 1], // 下
|
||||
[x - 1, y], // 左
|
||||
];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let [x1, y1] = arr[i];
|
||||
if (x1 < 0 || x1 >= width || y1 < 0 || y1 >= height) continue;
|
||||
let key = `${x1},${y1}`;
|
||||
if (visited.includes(key)) continue;
|
||||
visited.push(key);
|
||||
let index = (y1 * width + x1) * 4;
|
||||
let r = data.data[index];
|
||||
let g = data.data[index + 1];
|
||||
let b = data.data[index + 2];
|
||||
let a = data.data[index + 3];
|
||||
if ((r || g || b) && a) {
|
||||
contours.push({ x: x1, y: y1 });
|
||||
} else {
|
||||
if (i > 0) cd(x1, y1);
|
||||
// 边界追踪
|
||||
function traceImageContour(canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
// 查找起始点(第一个不透明像素)
|
||||
let startX = -1;
|
||||
let startY = -1;
|
||||
|
||||
outer: for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const index = (y * width + x) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
startX = x;
|
||||
startY = y;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
cd(0, 0);
|
||||
console.log(contours);
|
||||
return data;
|
||||
|
||||
if (startX === -1) return []; // 没有不透明像素
|
||||
|
||||
// Moore-Neighbor边界跟踪算法
|
||||
const contour = [];
|
||||
const visited = new Set();
|
||||
const directions = [
|
||||
[-1, 0],
|
||||
[-1, -1],
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[0, 1],
|
||||
[-1, 1],
|
||||
];
|
||||
|
||||
let currentX = startX;
|
||||
let currentY = startY;
|
||||
let backtrackDir = 4; // 起始方向:右
|
||||
|
||||
do {
|
||||
const pointKey = `${currentX},${currentY}`;
|
||||
if (!visited.has(pointKey)) {
|
||||
contour.push({ x: currentX, y: currentY });
|
||||
visited.add(pointKey);
|
||||
}
|
||||
|
||||
// 从右方向开始顺时针查找
|
||||
let found = false;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const dir = (backtrackDir + i) % 8;
|
||||
const dx = directions[dir][0];
|
||||
const dy = directions[dir][1];
|
||||
const checkX = currentX + dx;
|
||||
const checkY = currentY + dy;
|
||||
|
||||
if (
|
||||
checkX >= 0 &&
|
||||
checkX < width &&
|
||||
checkY >= 0 &&
|
||||
checkY < height
|
||||
) {
|
||||
const index = (checkY * width + checkX) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
currentX = checkX;
|
||||
currentY = checkY;
|
||||
backtrackDir = (dir + 5) % 8; // 下一个开始查找的方向
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) break;
|
||||
} while (
|
||||
!(currentX === startX && currentY === startY) &&
|
||||
visited.size < width * height
|
||||
);
|
||||
|
||||
return contour;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1497,6 +1497,7 @@ export default {
|
||||
CompositeColorTip: '颜色:保留原图像饱和度,改变新图像颜色',
|
||||
CompositeLuminosity: '亮度',
|
||||
CompositeLuminosityTip: '亮度:保留原图像颜色,改变新图像亮度',
|
||||
GarmentPartSelector: '服装部件选取',
|
||||
},
|
||||
speedList: {
|
||||
High: '高级',
|
||||
|
||||
@@ -1547,7 +1547,8 @@ export default {
|
||||
'Color: Preserve the original image saturation and change the color of the new image',
|
||||
CompositeLuminosity: 'Luminosity',
|
||||
CompositeLuminosityTip:
|
||||
'Luminosity: Preserve the original image color and change the luminosity of the new image'
|
||||
'Luminosity: Preserve the original image color and change the luminosity of the new image',
|
||||
GarmentPartSelector: 'Garment Part Selector',
|
||||
},
|
||||
speedList: {
|
||||
High: 'High',
|
||||
|
||||
Reference in New Issue
Block a user