526 lines
13 KiB
Vue
526 lines
13 KiB
Vue
|
|
<script setup>
|
|||
|
|
import {
|
|||
|
|
inject,
|
|||
|
|
ref,
|
|||
|
|
provide,
|
|||
|
|
onMounted,
|
|||
|
|
computed,
|
|||
|
|
watch,
|
|||
|
|
onUnmounted,
|
|||
|
|
} from "vue";
|
|||
|
|
import { OperationType } from "../utils/layerHelper";
|
|||
|
|
import BrushPanel from "./BrushPanel.vue";
|
|||
|
|
import { BrushStore } from "../store/BrushStore";
|
|||
|
|
|
|||
|
|
// 提供brushStore给子组件
|
|||
|
|
provide("brushStore", BrushStore);
|
|||
|
|
|
|||
|
|
const toolManager = inject("toolManager");
|
|||
|
|
const layerManager = inject("layerManager");
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
activeTool: String,
|
|||
|
|
canvasWidth: Number,
|
|||
|
|
canvasHeight: Number,
|
|||
|
|
canvasColor: String,
|
|||
|
|
brushSize: Number,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits([
|
|||
|
|
"update:canvasWidth",
|
|||
|
|
"update:canvasHeight",
|
|||
|
|
"update:canvasColor",
|
|||
|
|
"update:brushSize",
|
|||
|
|
"canvas-size-change",
|
|||
|
|
"canvas-color-change",
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// 笔刷面板相关状态
|
|||
|
|
const showBrushPanel = ref(false);
|
|||
|
|
const brushPanelRef = ref(null);
|
|||
|
|
|
|||
|
|
// 计算属性
|
|||
|
|
const shouldShowBrushSettings = computed(() => {
|
|||
|
|
return props.activeTool === OperationType.DRAW;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function updateCanvasSize() {
|
|||
|
|
if (!layerManager) {
|
|||
|
|
console.warn("LayerManager 未初始化,无法调整背景层尺寸");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查画布上是否有除了背景层的其他元素
|
|||
|
|
const hasOtherElements = layerManager.layers.value.some((layer) => {
|
|||
|
|
if (layer.isBackground) return false;
|
|||
|
|
// 检查普通图层是否有对象
|
|||
|
|
if (layer.fabricObjects && layer.fabricObjects.length > 0) return true;
|
|||
|
|
// 检查固定图层是否有对象
|
|||
|
|
if (layer.isFixed && layer.fabricObjects && layer.fabricObjects.length > 0)
|
|||
|
|
return true;
|
|||
|
|
return false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (hasOtherElements) {
|
|||
|
|
// 有其他元素时使用等比缩放命令
|
|||
|
|
layerManager.resizeCanvasWithScale(props.canvasWidth, props.canvasHeight);
|
|||
|
|
} else {
|
|||
|
|
// 只有背景层时使用普通调整命令
|
|||
|
|
layerManager.resizeCanvas(props.canvasWidth, props.canvasHeight);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
emit("canvas-size-change");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateCanvasColor() {
|
|||
|
|
if (!layerManager) {
|
|||
|
|
console.warn("LayerManager 未初始化,无法更改背景色");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新背景层颜色而不是画布颜色
|
|||
|
|
layerManager.updateBackgroundColor(props.canvasColor);
|
|||
|
|
|
|||
|
|
emit("canvas-color-change");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 切换笔刷面板显示状态
|
|||
|
|
function toggleBrushPanel() {
|
|||
|
|
showBrushPanel.value = !showBrushPanel.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理笔刷大小变化
|
|||
|
|
function handleBrushSizeChange(event) {
|
|||
|
|
const newSize = parseFloat(event.target.value);
|
|||
|
|
emit("update:brushSize", newSize);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理笔刷设置变化,将BrushStore的数据同步到brushManager
|
|||
|
|
function syncBrushStoreToManager() {
|
|||
|
|
if (!toolManager?.brushManager) return;
|
|||
|
|
|
|||
|
|
const brushManager = toolManager.brushManager;
|
|||
|
|
|
|||
|
|
// 检查画笔是否正在更新中
|
|||
|
|
if (brushManager.isUpdatingBrush) {
|
|||
|
|
console.warn("画笔正在更新中,请稍候...");
|
|||
|
|
// 延迟重试,确保在画笔更新完成后应用最新设置
|
|||
|
|
setTimeout(syncBrushStoreToManager, 100);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听BrushStore的变化,更新brushManager
|
|||
|
|
const size = BrushStore.state.size;
|
|||
|
|
const color = BrushStore.state.color;
|
|||
|
|
const type = BrushStore.state.type;
|
|||
|
|
const opacity = BrushStore.state.opacity;
|
|||
|
|
const textureEnabled = BrushStore.state.textureEnabled;
|
|||
|
|
const texturePath = BrushStore.state.texturePath;
|
|||
|
|
const textureScale = BrushStore.state.textureScale;
|
|||
|
|
|
|||
|
|
// 将所有更改一次性应用,减少updateBrush调用次数
|
|||
|
|
let needsUpdate = false;
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
brushManager.brushSize &&
|
|||
|
|
typeof brushManager.setBrushSize === "function" &&
|
|||
|
|
brushManager.getBrushSize() !== size
|
|||
|
|
) {
|
|||
|
|
brushManager.brushSize.value = size; // 直接设置值,避免触发updateBrush
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
brushManager.brushColor &&
|
|||
|
|
typeof brushManager.setBrushColor === "function" &&
|
|||
|
|
brushManager.getBrushColor() !== color
|
|||
|
|
) {
|
|||
|
|
brushManager.brushColor.value = color; // 直接设置值,避免触发updateBrush
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
typeof brushManager.setBrushType === "function" &&
|
|||
|
|
brushManager.getCurrentBrushType() !== type
|
|||
|
|
) {
|
|||
|
|
brushManager.setBrushType(type);
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (typeof brushManager.setBrushOpacity === "function") {
|
|||
|
|
brushManager.setBrushOpacity(opacity);
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 同步材质相关设置
|
|||
|
|
if (textureEnabled && texturePath) {
|
|||
|
|
if (typeof brushManager.setTexturePath === "function") {
|
|||
|
|
brushManager.setTexturePath(texturePath);
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (
|
|||
|
|
typeof brushManager.setTextureScale === "function" &&
|
|||
|
|
brushManager.getTextureScale() !== textureScale
|
|||
|
|
) {
|
|||
|
|
brushManager.textureScale.value = textureScale; // 直接设置值,避免触发updateBrush
|
|||
|
|
needsUpdate = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 只在有变化时调用一次updateBrush,减少重绘次数
|
|||
|
|
if (needsUpdate && typeof brushManager.updateBrush === "function") {
|
|||
|
|
brushManager.updateBrush();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 点击外部时关闭笔刷面板
|
|||
|
|
function handleClickOutside(event) {
|
|||
|
|
if (
|
|||
|
|
showBrushPanel.value &&
|
|||
|
|
brushPanelRef.value &&
|
|||
|
|
!brushPanelRef.value.contains(event.target) &&
|
|||
|
|
!event.target.closest(".brush-selector")
|
|||
|
|
) {
|
|||
|
|
showBrushPanel.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 获取工具管理器和笔刷管理器
|
|||
|
|
const brushManager = toolManager?.brushManager;
|
|||
|
|
|
|||
|
|
// 设置初始的可用笔刷类型
|
|||
|
|
if (brushManager) {
|
|||
|
|
const availableBrushes = brushManager.getBrushTypes();
|
|||
|
|
BrushStore.setAvailableBrushes(availableBrushes);
|
|||
|
|
|
|||
|
|
// 初始化BrushStore与brushManager的数据同步
|
|||
|
|
BrushStore.setBrushSize(brushManager.brushSize?.value || 5);
|
|||
|
|
BrushStore.setBrushColor(brushManager.brushColor?.value || "#000000");
|
|||
|
|
BrushStore.setBrushType(brushManager.getCurrentBrushType() || "pencil");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加点击外部关闭面板的事件监听
|
|||
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|||
|
|
|
|||
|
|
// 监听BrushStore的变化,同步到brushManager
|
|||
|
|
const unwatch = watch(
|
|||
|
|
() => [
|
|||
|
|
BrushStore.state.size,
|
|||
|
|
BrushStore.state.color,
|
|||
|
|
BrushStore.state.type,
|
|||
|
|
BrushStore.state.opacity,
|
|||
|
|
BrushStore.state.textureEnabled,
|
|||
|
|
BrushStore.state.texturePath,
|
|||
|
|
BrushStore.state.textureScale,
|
|||
|
|
],
|
|||
|
|
syncBrushStoreToManager,
|
|||
|
|
{ deep: true }
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 组件卸载时移除事件监听
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|||
|
|
unwatch();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="canvas-header">
|
|||
|
|
<span class="canvas-title">Canvas</span>
|
|||
|
|
|
|||
|
|
<!-- 默认设置 -->
|
|||
|
|
<div
|
|||
|
|
v-if="
|
|||
|
|
!activeTool ||
|
|||
|
|
activeTool === OperationType.SELECT ||
|
|||
|
|
activeTool === OperationType.PAN
|
|||
|
|
"
|
|||
|
|
class="canvas-settings"
|
|||
|
|
>
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Width</span>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
:value="canvasWidth"
|
|||
|
|
class="setting-input"
|
|||
|
|
@input="$emit('update:canvasWidth', Number($event.target.value))"
|
|||
|
|
@change="updateCanvasSize"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Height</span>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
:value="canvasHeight"
|
|||
|
|
class="setting-input"
|
|||
|
|
@input="$emit('update:canvasHeight', Number($event.target.value))"
|
|||
|
|
@change="updateCanvasSize"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Color</span>
|
|||
|
|
<div class="color-picker-wrapper">
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
:value="canvasColor"
|
|||
|
|
class="color-picker"
|
|||
|
|
@input="$emit('update:canvasColor', $event.target.value)"
|
|||
|
|
@change="updateCanvasColor"
|
|||
|
|
/>
|
|||
|
|
<span class="color-dropdown">▼</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 绘图工具设置 -->
|
|||
|
|
<div v-if="shouldShowBrushSettings" class="canvas-settings">
|
|||
|
|
<!-- 简化的笔刷控制UI -->
|
|||
|
|
<!-- <div class="setting-group">
|
|||
|
|
<span class="setting-label">大小:</span>
|
|||
|
|
<input
|
|||
|
|
type="range"
|
|||
|
|
:value="BrushStore.state.size"
|
|||
|
|
min="0.5"
|
|||
|
|
max="100"
|
|||
|
|
step="0.5"
|
|||
|
|
class="size-slider"
|
|||
|
|
@input="handleBrushSizeChange"
|
|||
|
|
/>
|
|||
|
|
<span class="size-value">{{ BrushStore.state.size }}px</span>
|
|||
|
|
</div> -->
|
|||
|
|
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">笔刷:</span>
|
|||
|
|
<div class="brush-selector" @click="toggleBrushPanel">
|
|||
|
|
<div
|
|||
|
|
class="brush-preview"
|
|||
|
|
:style="{
|
|||
|
|
backgroundColor: BrushStore.state.color,
|
|||
|
|
height: BrushStore.state.type === 'marker' ? '4px' : '2px',
|
|||
|
|
opacity: BrushStore.state.opacity,
|
|||
|
|
}"
|
|||
|
|
></div>
|
|||
|
|
<span class="brush-dropdown">▼</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 笔刷面板 -->
|
|||
|
|
<div
|
|||
|
|
v-if="showBrushPanel"
|
|||
|
|
class="brush-panel-container"
|
|||
|
|
ref="brushPanelRef"
|
|||
|
|
>
|
|||
|
|
<BrushPanel />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">颜色:</span>
|
|||
|
|
<div class="color-picker-wrapper">
|
|||
|
|
<input
|
|||
|
|
type="color"
|
|||
|
|
:value="BrushStore.state.color"
|
|||
|
|
class="color-picker"
|
|||
|
|
@input="BrushStore.setBrushColor($event.target.value)"
|
|||
|
|
/>
|
|||
|
|
<span class="color-dropdown">▼</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 文本工具设置 -->
|
|||
|
|
<div v-if="activeTool === OperationType.TEXT" class="canvas-settings">
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Font:</span>
|
|||
|
|
<select class="font-select">
|
|||
|
|
<option value="Arial">Arial</option>
|
|||
|
|
<option value="Times New Roman">Times New Roman</option>
|
|||
|
|
<option value="Courier New">Courier New</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Size:</span>
|
|||
|
|
<input
|
|||
|
|
type="number"
|
|||
|
|
class="setting-input"
|
|||
|
|
value="16"
|
|||
|
|
min="8"
|
|||
|
|
max="72"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Color:</span>
|
|||
|
|
<div class="color-picker-wrapper">
|
|||
|
|
<input type="color" class="color-picker" value="#000000" />
|
|||
|
|
<span class="color-dropdown">▼</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 上传工具设置 -->
|
|||
|
|
<div v-if="activeTool === OperationType.UPLOAD" class="canvas-settings">
|
|||
|
|
<div class="setting-group">
|
|||
|
|
<span class="setting-label">Upload Type:</span>
|
|||
|
|
<select class="setting-select">
|
|||
|
|
<option value="image">Image</option>
|
|||
|
|
<option value="vector">Vector Graphics</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 导出设置 -->
|
|||
|
|
<div class="setting-group export-group">
|
|||
|
|
<span class="export-model-select">exportModel.select:</span>
|
|||
|
|
<span class="export-model-dropdown">▼</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.canvas-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 10px 20px;
|
|||
|
|
border-bottom: 1px solid #e0e0e0;
|
|||
|
|
user-select: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-right: 30px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-title::before {
|
|||
|
|
content: "⟳";
|
|||
|
|
margin-right: 5px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.canvas-settings {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 15px;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.setting-group {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 5px;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.setting-label {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.setting-input {
|
|||
|
|
width: 60px;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.setting-select {
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.font-select {
|
|||
|
|
width: 150px;
|
|||
|
|
padding: 4px 8px;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.color-picker-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.color-picker {
|
|||
|
|
width: 30px;
|
|||
|
|
height: 30px;
|
|||
|
|
border: none;
|
|||
|
|
padding: 0;
|
|||
|
|
background: none;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.color-dropdown {
|
|||
|
|
font-size: 10px;
|
|||
|
|
margin-left: 5px;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.size-slider {
|
|||
|
|
width: 100px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.size-value {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #666;
|
|||
|
|
min-width: 30px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brush-selector {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
padding: 5px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
background-color: white;
|
|||
|
|
width: 80px;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brush-preview {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 2px;
|
|||
|
|
background-color: #000;
|
|||
|
|
border-radius: 1px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.brush-dropdown,
|
|||
|
|
.export-model-dropdown {
|
|||
|
|
font-size: 10px;
|
|||
|
|
margin-left: 5px;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.export-model-select {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #333;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.export-group {
|
|||
|
|
margin-left: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 笔刷面板 */
|
|||
|
|
.brush-panel-container {
|
|||
|
|
position: absolute;
|
|||
|
|
top: calc(100% + 5px);
|
|||
|
|
left: 0;
|
|||
|
|
z-index: 1000;
|
|||
|
|
width: 600px;
|
|||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
</style>
|