导出图片添加印花平铺判断

This commit is contained in:
李志鹏
2026-01-14 11:26:51 +08:00
parent d75e956fbf
commit dbe4557dc3
12 changed files with 523 additions and 887 deletions

View File

@@ -0,0 +1,31 @@
<?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="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M346 936 c-21 -13 -49 -41 -62 -62 -41 -67 -27 -180 27 -218 47 -32
53 -12 12 40 -71 94 -2 229 116 229 37 0 58 -7 87 -28 32 -22 40 -24 42 -12
14 64 -142 100 -222 51z"/>
<path d="M368 877 c-33 -28 -48 -57 -48 -96 0 -39 9 -61 26 -61 10 0 14 13 14
46 0 57 12 79 50 93 61 21 110 -22 110 -96 0 -48 14 -56 31 -19 31 67 -35 156
-114 156 -29 0 -50 -7 -69 -23z"/>
<path d="M580 794 c0 -58 -9 -84 -43 -121 -19 -21 -19 -23 -2 -29 34 -13 85
75 85 148 0 37 -10 58 -26 58 -10 0 -14 -15 -14 -56z"/>
<path d="M410 749 c-13 -6 -28 -15 -32 -22 -4 -7 -8 -106 -8 -222 l-1 -210
-27 34 c-62 80 -89 101 -126 101 -27 0 -39 -6 -52 -25 -15 -24 -15 -28 0 -68
21 -53 78 -123 94 -113 18 11 16 17 -28 75 -44 58 -48 72 -25 91 21 18 54 -6
93 -68 31 -47 74 -78 93 -67 5 4 9 101 9 224 0 225 3 241 42 241 10 0 19 -1
19 -2 1 -2 5 -86 8 -188 5 -165 8 -185 24 -188 15 -3 17 5 17 61 0 72 17 100
45 77 10 -9 15 -32 15 -77 0 -56 2 -64 18 -61 12 2 16 11 14 34 -5 48 14 86
41 82 20 -3 22 -9 25 -66 3 -53 6 -63 20 -60 12 2 19 16 22 43 4 32 10 41 27
43 36 5 46 -36 39 -166 -6 -127 -19 -160 -75 -195 -48 -29 -176 -34 -246 -10
-48 18 -56 25 -130 128 -16 22 -25 26 -37 19 -15 -8 -11 -18 31 -75 78 -105
94 -113 236 -117 119 -3 121 -3 167 27 69 44 82 74 86 214 6 179 -8 221 -72
216 -22 -2 -39 4 -55 20 -17 17 -32 22 -55 19 -19 -2 -38 2 -45 9 -6 7 -25 13
-40 13 l-29 0 -4 100 c-3 103 -11 122 -53 133 -11 3 -31 1 -45 -4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -872,13 +872,13 @@ export class ToggleChildLayerVisibilityCommand extends Command {
// this.oldVisibility = this.childLayer ? this.childLayer.visible : null; // this.oldVisibility = this.childLayer ? this.childLayer.visible : null;
} }
async execute() { async execute(visible) {
if (!this.childLayer) { if (!this.childLayer) {
throw new Error("找不到要切换可见性的子图层"); throw new Error("找不到要切换可见性的子图层");
} }
// 切换可见性 // 切换可见性
this.childLayer.visible = !this.childLayer.visible; this.childLayer.visible = typeof visible === "boolean" ? visible : !this.childLayer.visible;
// 更新画布上图层对象的可见性 // 更新画布上图层对象的可见性
if (this.canvas) { if (this.canvas) {

View File

@@ -287,7 +287,7 @@ const canDeleteComputed = computed(() => {
:is-child="isChild" :is-child="isChild"
:is-active="layer.id === activeLayerId" :is-active="layer.id === activeLayerId"
:is-selected="isLayerSelected(layer.id)" :is-selected="isLayerSelected(layer.id)"
:is-multi-select-mode="isMultiSelectMode && !layer.specialType" :is-multi-select-mode="isMultiSelectMode && !(layer.isPrintTrims || layer.isPrintTrimsGroup)"
:is-editing="editingLayerId === layer.id" :is-editing="editingLayerId === layer.id"
:editing-name="editingLayerName" :editing-name="editingLayerName"
:can-delete=" :can-delete="
@@ -296,7 +296,7 @@ const canDeleteComputed = computed(() => {
:expanded-group-ids="expandedGroupIds" :expanded-group-ids="expandedGroupIds"
@click="(...args) => forwardEvent('layer-click', ...args)" @click="(...args) => forwardEvent('layer-click', ...args)"
@double-click="(...args) => forwardEvent('layer-double-click', ...args)" @double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => !layer.specialType && forwardEvent('context-menu', ...args)" @context-menu="(...args) => !(layer.isPrintTrims || layer.isPrintTrimsGroup) && forwardEvent('context-menu', ...args)"
@checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)" @checkbox-change="(...args) => forwardEvent('checkbox-change', ...args)"
@toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)" @toggle-visibility="(...args) => forwardEvent('toggle-visibility', ...args)"
@toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)" @toggle-lock="(...args) => forwardEvent('toggle-lock', ...args)"
@@ -337,7 +337,7 @@ const canDeleteComputed = computed(() => {
:expanded-group-ids="expandedGroupIds" :expanded-group-ids="expandedGroupIds"
:isChild="true" :isChild="true"
:parentLayerId="layer.id" :parentLayerId="layer.id"
:group-name="layer.specialType || groupName" :group-name="groupName"
@layer-click="(...args) => forwardEvent('layer-click', ...args)" @layer-click="(...args) => forwardEvent('layer-click', ...args)"
@layer-double-click="(...args) => forwardEvent('layer-double-click', ...args)" @layer-double-click="(...args) => forwardEvent('layer-double-click', ...args)"
@context-menu="(...args) => forwardEvent('context-menu', ...args)" @context-menu="(...args) => forwardEvent('context-menu', ...args)"

View File

@@ -1242,7 +1242,7 @@ async function handleCrossLevelMove(moveData) {
try { try {
const layer = findLayerRecursively(layers.value, layerId).layer; const layer = findLayerRecursively(layers.value, layerId).layer;
const toLayer = findLayerRecursively(layers.value, toParentId).layer; const toLayer = findLayerRecursively(layers.value, toParentId).layer;
if(layer?.specialType || toLayer?.specialType) { if(layer?.isPrintTrims || layer?.isPrintTrimsGroup || toLayer?.isPrintTrims || toLayer?.isPrintTrimsGroup) {
console.warn("当前图层不可移动到外部"); console.warn("当前图层不可移动到外部");
return; return;
} }

View File

@@ -1,44 +1,34 @@
<template> <template>
<transition name="fade"> <transition name="fade">
<div class="part-selector-toolbar" v-if="visible" :class="{active:!closePanel}"> <div
<div class="btn" @click="setClosePanel"><i class="fi fi-br-angle-left"></i></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-section">
<div class="toolbar-header"> <div class="toolbar-header">
<div class="header-title">{{ t("Canvas.GarmentPartSelector") }}</div> <div class="header-title">
{{ t("Canvas.GarmentPartSelector") }}
</div>
<!-- 移除关闭按钮完全通过工具切换控制显示隐藏 --> <!-- 移除关闭按钮完全通过工具切换控制显示隐藏 -->
</div> </div>
<div class="tool-types"> <div class="tool-types">
<div <div
v-for="item in toolList"
:key="item.type"
:class="[ :class="[
'tool-btn', 'tool-btn',
{ active: selectionType === OperationType.LASSO }, { active: selectionType === item.type },
]" ]"
@click="setSelectionType(OperationType.LASSO)" @click="setSelectionType(item.type)"
> >
<svg-icon name="CFree" size="20" /> <svg-icon :name="item.icon" :size="item.size" />
<span>{{ $t("Canvas.freehandSketching") }}</span> <span>{{ item.label }}</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> </div>
@@ -49,131 +39,21 @@
<div class="tool-actions"> <div class="tool-actions">
<div class="action-btn" @click="copySelectionToNewLayer"> <div class="action-btn" @click="copySelectionToNewLayer">
<svg-icon name="CPaste" size="16" /> <svg-icon name="CPaste" size="16" />
<span class="btn-text">{{ $t("Canvas.creation") }}</span> <span class="btn-text">{{
$t("Canvas.creation")
}}</span>
</div> </div>
<div class="action-btn" @click="cutSelectionToNewLayer"> <div class="action-btn" @click="cutSelectionToNewLayer">
<svg-icon name="CCut" size="26" /> <svg-icon name="CCut" size="26" />
<span class="btn-text">{{ $t("Canvas.CreateAndCopy") }}</span> <span class="btn-text">{{
$t("Canvas.CreateAndCopy")
}}</span>
</div> </div>
<div class="action-btn" @click="clearSelectionContent"> <div class="action-btn" @click="clearSelectionContent">
<svg-icon name="CClear" size="18" /> <svg-icon name="CClear" size="18" />
<span class="btn-text">{{ $t("Canvas.TheClearlySelectedContent") }}</span> <span class="btn-text">{{
</div> $t("Canvas.TheClearlySelectedContent")
<!-- <button }}</span>
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>
</div> </div>
@@ -182,28 +62,26 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch } from "vue"; import { ref, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { import {
CreateSelectionCommand, CreateSelectionCommand,
InvertSelectionCommand, InvertSelectionCommand,
FeatherSelectionCommand, FeatherSelectionCommand,
FillSelectionCommand, FillSelectionCommand,
// CopySelectionToNewLayerCommand, } from "../commands/SelectionCommands";
// ClearSelectionContentCommand, import { ToolCommand } from "../commands/ToolCommands";
} from "../commands/SelectionCommands"; import {
import { ToolCommand } from "../commands/ToolCommands";
import {
LassoCutoutCommand, LassoCutoutCommand,
ClearSelectionCommand, ClearSelectionCommand,
// CutSelectionToNewLayerCommand, // CutSelectionToNewLayerCommand,
} from "../commands/LassoCutoutCommand"; } from "../commands/LassoCutoutCommand";
import { OperationType } from "../utils/layerHelper"; import { OperationType } from "../utils/layerHelper";
import { ClearSelectionContentCommand } from "../commands/ClearSelectionContentCommand"; import { ClearSelectionContentCommand } from "../commands/ClearSelectionContentCommand";
import { CutSelectionToNewLayerCommand } from "../commands/CutSelectionToNewLayerCommand"; import { CutSelectionToNewLayerCommand } from "../commands/CutSelectionToNewLayerCommand";
const props = defineProps({ const props = defineProps({
canvas: { canvas: {
type: Object, type: Object,
required: true, required: true,
@@ -229,41 +107,51 @@ const props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
}); });
// 响应式数据 // 响应式数据
const visible = ref(false); const visible = ref(false);
const selectionType = ref("rectangle"); const selectionType = ref("rectangle");
const featherAmount = ref(0); //打开隐藏操作面板
const fillColor = ref("#000000"); const closePanel = ref(false);
const hasSelection = ref(false); const setClosePanel = () => {
const showFeatherDialog = ref(false); closePanel.value = !closePanel.value;
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 变化 const toolList = [
watch( {
type: OperationType.PART,
label: "Point Selection",
icon: "CPoint",
size: "20",
},
{
type: OperationType.PART_RECTANGLE,
label: "Marquee Selection",
icon: "CRectangle",
size: "26",
},
{
type: OperationType.PART_BRUSH,
label: "Brush Selection",
icon: "CBrush",
size: "24",
},
{
type: OperationType.PART_ERASER,
label: "Erase",
icon: "CEraser",
size: "24",
},
];
// 国际化
const { t } = useI18n();
onMounted(() => {});
// 监听 activeTool 变化
watch(
() => props.activeTool, () => props.activeTool,
(newTool) => { (newTool) => {
// 当工具为LASSO或AREA类型时显示选区面板 // 当工具为LASSO或AREA类型时显示选区面板
@@ -271,7 +159,7 @@ watch(
OperationType.PART, OperationType.PART,
OperationType.PART_RECTANGLE, OperationType.PART_RECTANGLE,
OperationType.PART_BRUSH, OperationType.PART_BRUSH,
OperationType.PART_ERASER OperationType.PART_ERASER,
]; ];
if (selectionTools.includes(newTool)) { if (selectionTools.includes(newTool)) {
@@ -289,28 +177,27 @@ watch(
} }
}, },
{ immediate: true } { immediate: true }
); );
/** /**
* 显示面板 * 显示面板
*/ */
function show() { function show() {
visible.value = true; visible.value = true;
closePanel.value = true closePanel.value = true;
checkSelectionStatus(); }
}
/** /**
* 关闭面板 * 关闭面板
*/ */
function close() { function close() {
visible.value = false; visible.value = false;
} }
/** /**
* 设置选区类型 * 设置选区类型
*/ */
function setSelectionType(type) { function setSelectionType(type) {
selectionType.value = type; selectionType.value = type;
// 通过 ToolManager 切换工具,这会自动通知 SelectionManager // 通过 ToolManager 切换工具,这会自动通知 SelectionManager
@@ -323,193 +210,16 @@ function setSelectionType(type) {
props.selectionManager.setSelectionType(type); props.selectionManager.setSelectionType(type);
props.selectionManager.setupSelectionEvents(); 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> </script>
<style scoped lang="less"> <style scoped lang="less">
.part-selector-toolbar { .part-selector-toolbar {
position: absolute; position: absolute;
bottom: 22px; bottom: 22px;
left: 20px; left: 20px;
right: 20px; right: 20px;
max-width: min(90vw, 640px); max-width: min(90vw, 700px);
margin: 0 auto; margin: 0 auto;
background-color: rgba(255, 255, 255, 0.95); background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
@@ -520,15 +230,15 @@ function confirmColorPicker() {
color: #333; color: #333;
border: 1px solid rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.05);
user-select: none; user-select: none;
&.active{ &.active {
transform: translateY(100%); transform: translateY(100%);
> .btn{ > .btn {
> i{ > i {
transform: rotate(90deg); transform: rotate(90deg);
} }
} }
} }
> .btn{ > .btn {
width: 100%; width: 100%;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
@@ -537,15 +247,15 @@ function confirmColorPicker() {
justify-content: center; justify-content: center;
height: 22px; height: 22px;
> i{ > i {
font-size: 1.4rem; font-size: 1.4rem;
transform: rotate(270deg); transform: rotate(270deg);
} }
} }
} }
/* 平板和手机适配 */ /* 平板和手机适配 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.part-selector-toolbar { .part-selector-toolbar {
bottom: 15px; bottom: 15px;
left: 15px; left: 15px;
@@ -553,38 +263,38 @@ function confirmColorPicker() {
max-width: calc(100vw - 30px); max-width: calc(100vw - 30px);
border-radius: 6px; border-radius: 6px;
} }
} }
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
.part-selector-toolbar { .part-selector-toolbar {
bottom: 10px; bottom: 10px;
left: 10px; left: 10px;
right: 10px; right: 10px;
max-width: calc(100vw - 20px); max-width: calc(100vw - 20px);
} }
} }
.part-selector-toolbar.is-active { .part-selector-toolbar.is-active {
transform: translateY(0); transform: translateY(0);
} }
.toolbar-header { .toolbar-header {
// display: flex; // display: flex;
// justify-content: center; // justify-content: center;
// align-items: center; // align-items: center;
padding: 8px 0; padding: 8px 0;
// 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;
} }
.header-title { .header-title {
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
text-align: left; text-align: left;
} }
.header-btn { .header-btn {
background: none; background: none;
border: none; border: none;
color: #333; color: #333;
@@ -594,28 +304,28 @@ function confirmColorPicker() {
border-radius: 3px; border-radius: 3px;
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
min-width: 32px; min-width: 32px;
} }
.header-btn:hover { .header-btn:hover {
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
} }
.close-btn { .close-btn {
color: #666; color: #666;
} }
.toolbar-section { .toolbar-section {
padding: 0 3rem 1.2rem; padding: 0 3rem 1.2rem;
} }
.tool-types { .tool-types {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 8px; gap: 8px;
padding: 10px 0; padding: 10px 0;
} }
.tool-btn { .tool-btn {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -627,51 +337,51 @@ function confirmColorPicker() {
color: #333; color: #333;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.tool-btn span { .tool-btn span {
margin-top: 0; margin-top: 0;
font-size: 12px; font-size: 12px;
} }
.tool-btn svg { .tool-btn svg {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.tool-btn:hover { .tool-btn:hover {
background-color: rgba(0, 0, 0, 0.08); background-color: rgba(0, 0, 0, 0.08);
} }
.tool-btn.active { .tool-btn.active {
background-color: #007aff; background-color: #007aff;
color: white; color: white;
} }
.toolbar-divider { .toolbar-divider {
height: 1px; height: 1px;
background-color: rgba(0, 0, 0, 0.05); background-color: rgba(0, 0, 0, 0.05);
margin-bottom: 5px; margin-bottom: 5px;
} }
.tool-actions { .tool-actions {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 5px; gap: 5px;
padding: 0 10px; padding: 0 10px;
} }
/* 平板适配 - 每行4个按钮 */ /* 平板适配 - 每行4个按钮 */
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {
.tool-actions { .tool-actions {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 8px 6px; gap: 8px 6px;
padding: 0 8px; padding: 0 8px;
} }
} }
/* 手机适配 - 每行3个按钮 */ /* 手机适配 - 每行3个按钮 */
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
.tool-actions { .tool-actions {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 6px 4px; gap: 6px 4px;
@@ -683,9 +393,9 @@ function confirmColorPicker() {
padding: 2px 4px; padding: 2px 4px;
min-width: 28px; min-width: 28px;
} }
} }
.action-btn { .action-btn {
display: flex; display: flex;
// flex-direction: column; // flex-direction: column;
flex-direction: row; flex-direction: row;
@@ -700,156 +410,27 @@ function confirmColorPicker() {
.c-svg { .c-svg {
width: auto; width: auto;
} }
} }
.action-btn svg { .action-btn svg {
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-bottom: 8px; margin-bottom: 8px;
} }
.btn-text { .btn-text {
display: block; display: block;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
} }
.action-btn:hover { .action-btn:hover {
color: #007aff; color: #007aff;
} }
.action-btn:disabled { .action-btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; 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> </style>

View File

@@ -178,11 +178,7 @@
import { ref, onMounted, watch, onUnmounted, reactive } from "vue"; import { ref, onMounted, watch, onUnmounted, reactive } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
import { import { OperationType, SpecialLayerId } from "../../utils/layerHelper";
OperationType,
SpecialLayerId,
SpecialType,
} from "../../utils/layerHelper";
import { loadImageUrlToLayer } from "../../utils/imageHelper"; import { loadImageUrlToLayer } from "../../utils/imageHelper";
import { import {
calculateRotatedTopLeftDeg, calculateRotatedTopLeftDeg,
@@ -283,9 +279,6 @@
const getActiveObject = (e) => { const getActiveObject = (e) => {
console.log("==========切换激活对象", e, activeObjects); console.log("==========切换激活对象", e, activeObjects);
activeObjects.value = [...e.selected]; activeObjects.value = [...e.selected];
// .filter((v) =>
// v.specialType ? v.specialType === SpecialType.REPEAT_O : true
// );// 过滤出印花对象
activeObjects.value.forEach((v) => { activeObjects.value.forEach((v) => {
v.layer = props.layerManager.getLayerById(v.layerId); v.layer = props.layerManager.getLayerById(v.layerId);
}); });

View File

@@ -166,19 +166,19 @@ const normalToolsList = ref([
icon: { name: "CFont", size: "20" }, icon: { name: "CFont", size: "20" },
class: "text-btn", class: "text-btn",
}, },
{ // {
id: OperationType.PART, // id: OperationType.PART,
title: t("Canvas.GarmentPartSelector"), // title: t("Canvas.GarmentPartSelector"),
action: () => selectTool(OperationType.PART), // action: () => selectTool(OperationType.PART),
icon: { name: "CPart", size: "28" }, // icon: { name: "CPart", size: "28" },
class: "part-btn", // class: "part-btn",
activeList: [ // activeList: [
OperationType.PART, // OperationType.PART,
OperationType.PART_RECTANGLE, // OperationType.PART_RECTANGLE,
OperationType.PART_BRUSH, // OperationType.PART_BRUSH,
OperationType.PART_ERASER, // OperationType.PART_ERASER,
], // ],
}, // },
{ {
id: "help", id: "help",
title: t("Canvas.help"), title: t("Canvas.help"),

View File

@@ -1000,6 +1000,8 @@ defineExpose({
isContainBg = false, // 是否包含背景图层 isContainBg = false, // 是否包含背景图层
isContainFixed = false, // 是否包含固定图层 isContainFixed = false, // 是否包含固定图层
isContainFixedOther = false, // 是否包含其他固定图层 isContainFixedOther = false, // 是否包含其他固定图层
isPrintTrimsNoRepeat = true, // 是否包含印花图层的不平铺
isPrintTrimsRepeat = true, // 是否包含印花图层的平铺
isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小 isCropByBg = false, // 是否使用背景大小裁剪 // 如果为true则导出时裁剪到背景图层大小
layerId = "", // 导出具体图层ID layerId = "", // 导出具体图层ID
layerIdArray = [], // 导出多个图层ID数组 layerIdArray = [], // 导出多个图层ID数组
@@ -1010,6 +1012,8 @@ defineExpose({
isContainBg, isContainBg,
isContainFixed, isContainFixed,
isContainFixedOther, isContainFixedOther,
isPrintTrimsNoRepeat,
isPrintTrimsRepeat,
isCropByBg, isCropByBg,
layerId, layerId,
layerIdArray, layerIdArray,
@@ -1250,7 +1254,7 @@ defineExpose({
/> />
<!-- 部件选取面板 --> <!-- 部件选取面板 -->
<PartSelectorPanel <!-- <PartSelectorPanel
v-if="canvasManagerLoaded && !enabledRedGreenMode" v-if="canvasManagerLoaded && !enabledRedGreenMode"
:canvas="canvasManager && canvasManager.canvas" :canvas="canvasManager && canvasManager.canvas"
:commandManager="commandManager" :commandManager="commandManager"
@@ -1259,7 +1263,7 @@ defineExpose({
:canvasManager="canvasManager" :canvasManager="canvasManager"
:toolManager="toolManager" :toolManager="toolManager"
:activeTool="activeTool" :activeTool="activeTool"
/> /> -->
<!-- 文本编辑面板 --> <!-- 文本编辑面板 -->
<TextEditorPanel <TextEditorPanel
@@ -1414,6 +1418,7 @@ defineExpose({
/* background-color: #f8f8f8; */ /* background-color: #f8f8f8; */
:deep(.canvas-container) { :deep(.canvas-container) {
position: absolute !important; position: absolute !important;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1));
} }
} }
@@ -1426,33 +1431,31 @@ defineExpose({
} }
.background-grid { .background-grid {
--offsetX: 0px; --offsetX: 50%;
--offsetY: 0px; --offsetY: 50%;
--size: 8px; --size: 10px;
--color: #dedcdc; --color: rgba(229, 229,229,0.5);
background-image: -webkit-linear-gradient( background-image: -webkit-linear-gradient(
45deg, 90deg,
var(--color) 25%, var(--color) 1px,
transparent 0, transparent 0,
transparent 75%,
var(--color) 0
), ),
-webkit-linear-gradient(45deg, var(--color) 25%, transparent 0, transparent -webkit-linear-gradient(
75%, var(--color) 0); 0,
background-image: linear-gradient( var(--color) 1px,
45deg, transparent 0,
var(--color) 25%, );
background-image:linear-gradient(
90deg,
var(--color) 1px,
transparent 0, transparent 0,
transparent 75%,
var(--color) 0
), ),
linear-gradient( linear-gradient(
45deg, 0,
var(--color) 25%, var(--color) 1px,
transparent 0, transparent 0,
transparent 75%,
var(--color) 0
); );
background-color: #fafafa;
background-position: var(--offsetX) var(--offsetY), background-position: var(--offsetX) var(--offsetY),
calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY)); calc(var(--size) + var(--offsetX)) calc(var(--size) + var(--offsetY));
background-size: calc(var(--size) * 2) calc(var(--size) * 2); background-size: calc(var(--size) * 2) calc(var(--size) * 2);

View File

@@ -13,7 +13,6 @@ import {
createLayer, createLayer,
LayerType, LayerType,
SpecialLayerId, SpecialLayerId,
SpecialType,
BlendMode, BlendMode,
} from "../utils/layerHelper"; } from "../utils/layerHelper";
import { ObjectMoveCommand } from "../commands/ObjectCommands"; import { ObjectMoveCommand } from "../commands/ObjectCommands";
@@ -45,7 +44,7 @@ import {
} from "../utils/layerUtils"; } from "../utils/layerUtils";
import { imageModeHandler } from "../utils/imageHelper"; import { imageModeHandler } from "../utils/imageHelper";
import { getObjectAlphaToCanvas } from "../utils/objectHelper"; import { getObjectAlphaToCanvas } from "../utils/objectHelper";
import { AddLayerCommand, RemoveLayerCommand } from "../commands/LayerCommands"; import { AddLayerCommand, RemoveLayerCommand, ToggleChildLayerVisibilityCommand } from "../commands/LayerCommands";
import { fa, id } from "element-plus/es/locales.mjs"; import { fa, id } from "element-plus/es/locales.mjs";
import i18n from "@/lang/index.ts"; import i18n from "@/lang/index.ts";
const {t} = i18n.global; const {t} = i18n.global;
@@ -717,6 +716,8 @@ export class CanvasManager {
originX: backgroundLayerObject.originX || "left", originX: backgroundLayerObject.originX || "left",
originY: backgroundLayerObject.originY || "top", originY: backgroundLayerObject.originY || "top",
absolutePositioned: true, absolutePositioned: true,
rx: 15,
ry: 15,
}); });
} }
getBackgroundLayer() { getBackgroundLayer() {
@@ -827,7 +828,7 @@ export class CanvasManager {
updateMaskPosition(backgroundLayerObject) { updateMaskPosition(backgroundLayerObject) {
if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath) if (!backgroundLayerObject || !this.maskLayer || !this.canvas.clipPath)
return; return;
console.log("backgroundLayerObject");
const left = backgroundLayerObject.left; const left = backgroundLayerObject.left;
const top = backgroundLayerObject.top; const top = backgroundLayerObject.top;
@@ -922,6 +923,8 @@ export class CanvasManager {
* @param {Boolean} options.isContainBg 是否包含背景图层 * @param {Boolean} options.isContainBg 是否包含背景图层
* @param {Boolean} options.isContainFixed 是否包含固定图层 * @param {Boolean} options.isContainFixed 是否包含固定图层
* @param {Boolean} options.isContainFixedOther 是否包含其他固定图层 * @param {Boolean} options.isContainFixedOther 是否包含其他固定图层
* @param {Boolean} options.isPrintTrimsNoRepeat 是否包含印花图层的不平铺
* @param {Boolean} options.isPrintTrimsRepeat 是否包含印花图层的平铺
* @param {String} options.layerId 导出具体图层ID * @param {String} options.layerId 导出具体图层ID
* @param {Array} options.layerIdArray 导出多个图层ID数组 * @param {Array} options.layerIdArray 导出多个图层ID数组
* @param {String} options.expPicType 导出图片类型 (png/jpg/svg) * @param {String} options.expPicType 导出图片类型 (png/jpg/svg)
@@ -942,6 +945,8 @@ export class CanvasManager {
// this.canvas.renderAll(); // 重新渲染画布 // this.canvas.renderAll(); // 重新渲染画布
// 自动设置红绿图模式相关参数 // 自动设置红绿图模式相关参数
const enhancedOptions = { const enhancedOptions = {
isPrintTrimsNoRepeat: true,
isPrintTrimsRepeat: true,
...options, ...options,
// 如果没有明确指定,则根据当前模式自动设置 // 如果没有明确指定,则根据当前模式自动设置
restoreOpacityInRedGreen: restoreOpacityInRedGreen:
@@ -972,7 +977,46 @@ export class CanvasManager {
console.log("红绿图模式导出图层:", normalLayerIds); console.log("红绿图模式导出图层:", normalLayerIds);
} }
} }
return await this.exportManager.exportImage(enhancedOptions);
// 处理特殊图层的显示状态
const ptlids = [];
if(!enhancedOptions.isPrintTrimsNoRepeat || !enhancedOptions.isPrintTrimsRepeat){
let layers = this.layers?.value?.find((layer) => layer.isPrintTrimsGroup)?.children || [];
for(let layer of layers){
if(!layer.visible) continue;
let repeat = layer.fabricObjects?.[0]?.fill?.repeat || "no-repeat";
if(typeof repeat !== "string") repeat = "no-repeat";
if(repeat === "no-repeat"){
if(enhancedOptions.isPrintTrimsNoRepeat) continue;
}else{
if(enhancedOptions.isPrintTrimsRepeat) continue;
}
ptlids.push(layer.id);
const command = new ToggleChildLayerVisibilityCommand({
canvas: this.canvas,
layers: this.layers,
layerId: layer.id,
layerManager: this.layerManager,
});
await command.execute(false);
}
await this.changeCanvas();
}
const res = await this.exportManager.exportImage(enhancedOptions);
// 恢复特殊图层的显示状态
if(ptlids.length > 0){
for(let id of ptlids){
const command = new ToggleChildLayerVisibilityCommand({
canvas: this.canvas,
layers: this.layers,
layerId: id,
layerManager: this.layerManager,
});
await command.execute(true);
}
await this.changeCanvas();
}
return res;
} catch (error) { } catch (error) {
console.warn("CanvasManager导出图片失败:", error); console.warn("CanvasManager导出图片失败:", error);
throw error; throw error;
@@ -1533,7 +1577,7 @@ export class CanvasManager {
selectable: true, selectable: true,
hasControls: true, hasControls: true,
hasBorders: true, hasBorders: true,
specialType: SpecialType.PRINT_TRIMS_O, isPrintTrims: true,
globalCompositeOperation: BlendMode.MULTIPLY, globalCompositeOperation: BlendMode.MULTIPLY,
}); });
resolve(fabricImage); resolve(fabricImage);
@@ -1547,7 +1591,7 @@ export class CanvasManager {
visible: true, visible: true,
locked: false, locked: false,
opacity: 1.0, opacity: 1.0,
specialType: SpecialType.PRINT_TRIMS_L, isPrintTrims: true,
blendMode: BlendMode.MULTIPLY, blendMode: BlendMode.MULTIPLY,
fabricObjects: [image.toObject(["id", "layerId", "layerName"])], fabricObjects: [image.toObject(["id", "layerId", "layerName"])],
metadata: {sourceData: item}, metadata: {sourceData: item},
@@ -1601,7 +1645,7 @@ export class CanvasManager {
width: image.width, width: image.width,
height: image.height, height: image.height,
}, },
specialType: SpecialType.REPEAT_O, isPrintTrims: true,
}); });
this.canvas.add(rect); this.canvas.add(rect);
let layer = createLayer({ let layer = createLayer({
@@ -1611,7 +1655,7 @@ export class CanvasManager {
visible: true, visible: true,
locked: true, locked: true,
opacity: 1, opacity: 1,
specialType: SpecialType.REPEAT_L, isPrintTrims: true,
blendMode: BlendMode.MULTIPLY, blendMode: BlendMode.MULTIPLY,
fabricObjects: [rect.toObject(["id", "layerId", "layerName"])], fabricObjects: [rect.toObject(["id", "layerId", "layerName"])],
metadata: {sourceData: item}, metadata: {sourceData: item},
@@ -1630,7 +1674,7 @@ export class CanvasManager {
// }) // })
// children.push(layer); // children.push(layer);
// } // }
// if(children.length === 0) return; if(children.length === 0) return;
const groupRect = new fabric.Rect({}); const groupRect = new fabric.Rect({});
await this.setObjecCliptInfo(groupRect); await this.setObjecCliptInfo(groupRect);
// 插入组图层 // 插入组图层
@@ -1646,11 +1690,8 @@ export class CanvasManager {
children: children, children: children,
clippingMask: groupRect.toObject(), clippingMask: groupRect.toObject(),
isPrintTrimsGroup: true, isPrintTrimsGroup: true,
specialType: SpecialType.PRINT_TRIMS_G,
}); });
this.layers.value.splice(groupIndex, 0, groupLayer); this.layers.value.splice(groupIndex, 0, groupLayer);
console.log("==========layers", [...this.layers.value]);
} }
/** /**

View File

@@ -502,7 +502,6 @@ export class ToolManager {
if (!this.canvas) return; if (!this.canvas) return;
this.canvas.isDrawingMode = false; this.canvas.isDrawingMode = false;
this.canvas.selection = true; this.canvas.selection = true;
} }
/** /**
@@ -511,7 +510,7 @@ export class ToolManager {
*/ */
checkToolCanOperateSelectedObject() { checkToolCanOperateSelectedObject() {
const layer = this.layerManager?.getActiveLayer(); const layer = this.layerManager?.getActiveLayer();
const isSpecialLayer = !!layer?.specialType; const isSpecialLayer = !!layer?.isPrintTrims || !!layer?.isPrintTrimsGroup;
if (isSpecialLayer) { if (isSpecialLayer) {
this._disableBrushIndicator(); this._disableBrushIndicator();
this.canvas.defaultCursor = "not-allowed"; this.canvas.defaultCursor = "not-allowed";

View File

@@ -25,18 +25,6 @@ export const SpecialLayerId = {
SPECIAL_GROUP: "group_special", // 特殊组 SPECIAL_GROUP: "group_special", // 特殊组
COLOR: "special_color", // 颜色图层 COLOR: "special_color", // 颜色图层
} }
/**
* 特殊类型
*/
export const SpecialType = {
PRINT_TRIMS_G: "print_trims_group", // 印花和元素图层组
PRINT_TRIMS_L: "print_trims_layer", // 印花和元素图层
PRINT_TRIMS_O: "print_trims_object", // 印花和元素图层对象
REPEAT_L: "repeat_layer",// 平铺图层
REPEAT_O: "repeat_object",// 平铺图层对象
}
/** /**

View File

@@ -335,20 +335,20 @@ 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,
// level2Type: "Pattern",
// designType: "Library",
// path: "/src/assets/images/canvas/yinhua1.jpg",
// location: [250, 780],
// scale: [0.3, 0.4],
// angle: 0,
// },
{ {
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],
scale: [0.3, 0.4],
angle: 0,
},
{
ifSingle: true,
level2Type: "Pattern",
designType: "Library",
path: "/src/assets/images/canvas/yinhua1.jpg",
location: [550, 650], location: [550, 650],
scale: [0.15, 0.2], scale: [0.15, 0.2],
angle: 0, angle: 0,