fix: 修复多个已知问题

This commit is contained in:
bighuixiang
2025-06-29 23:29:47 +08:00
parent 6fc2a8fc57
commit 4a95f27966
41 changed files with 2266 additions and 351 deletions

View File

@@ -257,6 +257,12 @@ function setBrushOpacity(opacity) {
// 如果工具管理器存在,立即应用此更改
if (toolManager) {
toolManager.updateBrushOpacity(opacity);
// 同时更新颜色以确保透明度生效
const currentColor = BrushStore.state.color;
if (currentColor) {
toolManager.updateBrushColor(currentColor);
}
}
}

View File

@@ -345,7 +345,224 @@
</div>
</div>
</div> -->
<!-- 阴影设置 -->
<div class="brush-section">
<div class="section-header">
<span>{{ $t("阴影设置") }}</span>
</div>
<div class="property-list">
<!-- 阴影开关 -->
<div class="property-item">
<div class="checkbox-property">
<span>{{ $t("启用阴影") }}</span>
<div class="toggle-switch">
<input
type="checkbox"
:checked="brushStore.state.shadowEnabled"
@change="
(e) =>
handleShadowPropertyChange(
'shadowEnabled',
e.target.checked
)
"
id="shadow-enabled"
/>
<label for="shadow-enabled"></label>
</div>
</div>
</div>
<!-- 阴影控制组 - 仅在启用阴影时显示 -->
<template v-if="brushStore.state.shadowEnabled">
<!-- 阴影颜色 -->
<div class="property-item">
<div class="color-property">
<div class="color-header">
<span>{{ $t("阴影颜色") }}</span>
<div
class="color-preview"
:style="{ backgroundColor: brushStore.state.shadowColor }"
></div>
</div>
<div class="color-row">
<input
type="color"
:value="brushStore.state.shadowColor"
@input="
(e) =>
handleShadowPropertyChange(
'shadowColor',
e.target.value
)
"
class="color-picker"
/>
</div>
</div>
</div>
<!-- 阴影宽度 -->
<div class="property-item">
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影宽度") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowWidth }}px</span
>
</div>
<div class="slider-container">
<input
type="range"
:value="brushStore.state.shadowWidth"
@input="
(e) =>
handleShadowPropertyChange(
'shadowWidth',
parseFloat(e.target.value)
)
"
:min="0"
:max="50"
:step="1"
class="property-slider"
/>
</div>
<div class="property-presets">
<button
v-for="preset in [0, 5, 10, 15, 20]"
:key="preset"
@click="handleShadowPropertyChange('shadowWidth', preset)"
:class="{
active:
Math.abs(brushStore.state.shadowWidth - preset) < 0.1,
}"
>
{{ preset }}
</button>
</div>
</div>
</div>
<!-- 阴影X偏移 -->
<div class="property-item">
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影X偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetX }}px</span
>
</div>
<div class="slider-container">
<input
type="range"
:value="brushStore.state.shadowOffsetX"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetX',
parseFloat(e.target.value)
)
"
:min="-50"
:max="50"
:step="1"
class="property-slider"
/>
</div>
<div class="property-presets">
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetX', preset)
"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetX - preset) <
0.1,
}"
>
{{ preset }}
</button>
</div>
</div>
</div>
<!-- 阴影Y偏移 -->
<div class="property-item">
<div class="slider-property">
<div class="slider-header">
<span>{{ $t("阴影Y偏移") }}</span>
<span class="slider-value"
>{{ brushStore.state.shadowOffsetY }}px</span
>
</div>
<div class="slider-container">
<input
type="range"
:value="brushStore.state.shadowOffsetY"
@input="
(e) =>
handleShadowPropertyChange(
'shadowOffsetY',
parseFloat(e.target.value)
)
"
:min="-50"
:max="50"
:step="1"
class="property-slider"
/>
</div>
<div class="property-presets">
<button
v-for="preset in [-10, -5, 0, 5, 10]"
:key="preset"
@click="
handleShadowPropertyChange('shadowOffsetY', preset)
"
:class="{
active:
Math.abs(brushStore.state.shadowOffsetY - preset) <
0.1,
}"
>
{{ preset }}
</button>
</div>
</div>
</div>
<!-- 阴影预览 -->
<div class="property-item">
<div class="shadow-preview-container">
<div class="shadow-preview-title">{{ $t("阴影预览") }}</div>
<div class="shadow-preview-box">
<div
class="shadow-preview-element"
:style="{
backgroundColor: brushStore.state.color,
width: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
height: `${Math.max(
20,
Math.min(60, brushStore.state.size)
)}px`,
boxShadow: brushStore.state.shadowEnabled
? `${brushStore.state.shadowOffsetX}px ${brushStore.state.shadowOffsetY}px ${brushStore.state.shadowWidth}px ${brushStore.state.shadowColor}`
: 'none',
}"
></div>
</div>
</div>
</div>
</template>
</div>
</div>
<!-- 笔刷预设 -->
<div class="brush-section">
<div class="section-header">
@@ -625,6 +842,14 @@ const debouncedPropertyCommand = debounce((propId, value) => {
commandManager.execute(command, { nonUndoable: true });
}, 200);
// 处理阴影属性变化的防抖函数
const debouncedShadowCommand = debounce((propId, value) => {
// 通知工具管理器更新阴影
if (toolManager?.brushManager) {
toolManager.brushManager.updateShadow();
}
}, 100);
// 处理属性变化
function handlePropertyChange(propId, value) {
// 直接更新UI通过防抖函数延迟创建命令
@@ -632,6 +857,25 @@ function handlePropertyChange(propId, value) {
debouncedPropertyCommand(propId, value);
}
// 处理阴影属性变化
function handleShadowPropertyChange(propId, value) {
// 更新BrushStore中的阴影属性
if (propId === "shadowEnabled") {
BrushStore.setShadowEnabled(value);
} else if (propId === "shadowColor") {
BrushStore.setShadowColor(value);
} else if (propId === "shadowWidth") {
BrushStore.setShadowWidth(value);
} else if (propId === "shadowOffsetX") {
BrushStore.setShadowOffsetX(value);
} else if (propId === "shadowOffsetY") {
BrushStore.setShadowOffsetY(value);
}
// 通知笔刷管理器更新阴影设置
debouncedShadowCommand(propId, value);
}
// 使用命令设置笔刷类型
function setBrushTypeWithCommand(type) {
const command = new BrushTypeCommand({
@@ -1761,4 +2005,65 @@ const brushStore = BrushStore;
background-color: rgba(244, 67, 54, 1);
transform: scale(1.2) !important;
}
/* 阴影设置样式 */
.shadow-preview-container {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.shadow-preview-title {
font-weight: 500;
color: #333;
font-size: 14px;
text-align: center;
}
.shadow-preview-box {
background: repeating-conic-gradient(#f5f5f5 0% 25%, #ffffff 0% 50%) 50% /
10px 10px;
border-radius: 8px;
padding: 30px;
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.shadow-preview-element {
border-radius: 50%;
transition: all 0.3s ease;
position: relative;
z-index: 1;
background-clip: padding-box;
}
/* 响应式设计 */
@media (max-width: 768px) {
.brush-panel {
width: 95%;
right: 2.5%;
max-height: 75vh;
}
.shadow-preview-box {
min-height: 100px;
padding: 20px;
}
.property-presets {
justify-content: center;
}
.brush-type-grid,
.presets-container,
.texture-grid {
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
}
}
</style>

View File

@@ -332,6 +332,7 @@ async function prepareForLiquify(targetObj) {
targetObject: targetObject.value,
targetLayerId: targetLayerId.value,
originalData: originalImageData.value,
liquifyManager: props.liquifyManager,
layerManager: props.layerManager,
});
@@ -483,6 +484,7 @@ function showPanel(event) {
targetLayerId: targetLayerId.value,
originalData: originalImageData.value,
layerManager: props.layerManager,
liquifyManager: props.liquifyManager,
});
}
@@ -739,10 +741,59 @@ function removeCanvasListeners() {
_handleMouseUp = null;
}
/**
* 获取当前图像的实际状态数据
* @param {Object} targetObject Fabric图像对象
* @returns {Promise<ImageData|null>} 当前图像数据或null
*/
async function getCurrentImageData(targetObject) {
if (!targetObject || !targetObject._element) {
return null;
}
try {
// 创建临时canvas来获取当前图像状态
const tempCanvas = document.createElement("canvas");
const element = targetObject._element;
// 设置canvas尺寸为原始图像尺寸
if (originalImageData.value) {
tempCanvas.width = originalImageData.value.width;
tempCanvas.height = originalImageData.value.height;
} else {
tempCanvas.width = element.naturalWidth || element.width;
tempCanvas.height = element.naturalHeight || element.height;
}
const tempCtx = tempCanvas.getContext("2d");
// 绘制当前图像到临时canvas
tempCtx.drawImage(element, 0, 0, tempCanvas.width, tempCanvas.height);
// 获取ImageData
const imageData = tempCtx.getImageData(
0,
0,
tempCanvas.width,
tempCanvas.height
);
console.log(
"✅ 成功获取当前图像状态,尺寸:",
imageData.width,
"x",
imageData.height
);
return imageData;
} catch (error) {
console.warn("获取当前图像数据失败:", error);
return null;
}
}
/**
* 鼠标按下事件处理
*/
function handleMouseDown(event) {
async function handleMouseDown(event) {
if (!isEditing.value || !visible.value || !props.liquifyManager) return;
isDrawing.value = true;
@@ -760,29 +811,43 @@ function handleMouseDown(event) {
lastX.value = pointer.x;
lastY.value = pointer.y;
// === 修复:记录初始图像数据 ===
// === 修复:记录当前图像状态(而不是原始状态)===
try {
const currentTarget = getCurrentTargetObject();
if (currentTarget && originalImageData.value) {
console.log("🎯 记录液化操作初始状态对象ID:", targetObjectId.value);
if (currentTarget) {
console.log("🎯 记录液化操作当前状态对象ID:", targetObjectId.value);
// 记录初始图像数据(深拷贝
const originalData = originalImageData.value;
initialImageData.value = new ImageData(
new Uint8ClampedArray(originalData.data),
originalData.width,
originalData.height
);
// 获取当前图像的实际状态(而不是原始状态
const currentImageData = await getCurrentImageData(currentTarget);
if (currentImageData) {
// 记录当前图像数据(深拷贝)
initialImageData.value = new ImageData(
new Uint8ClampedArray(currentImageData.data),
currentImageData.width,
currentImageData.height
);
console.log(
"✅ 当前图像状态已记录,尺寸:",
initialImageData.value.width,
"x",
initialImageData.value.height
);
} else {
// 如果无法获取当前状态,使用原始状态作为备用
if (originalImageData.value) {
const originalData = originalImageData.value;
initialImageData.value = new ImageData(
new Uint8ClampedArray(originalData.data),
originalData.width,
originalData.height
);
console.log("⚠️ 使用原始状态作为备用初始状态");
}
}
// 备用:也保存序列化状态
initialObjectState.value = serializeFabricObject(currentTarget);
console.log(
"✅ 初始图像数据已记录,尺寸:",
initialImageData.value.width,
"x",
initialImageData.value.height
);
}
} catch (error) {
console.error("❌ 记录初始状态失败:", error);
@@ -965,6 +1030,7 @@ async function handleMouseUp() {
currentLiquifyCommand.value = createLiquifyStateCommand({
canvas: props.canvas,
layerManager: props.layerManager,
liquifyManager: props.liquifyManager,
targetObject: currentTarget,
targetLayerId: targetLayerId.value,
targetObjectId: targetObjectId.value,
@@ -1646,7 +1712,7 @@ function stopPressTimer() {
/* 平板适配最多6列 */
@media screen and (max-width: 768px) {
.liquify-modes {
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 3px;
}
@@ -1663,7 +1729,7 @@ function stopPressTimer() {
/* 手机适配最多4列 */
@media screen and (max-width: 480px) {
.liquify-modes {
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 2px;
}

View File

@@ -4,7 +4,7 @@
<!-- 顶部选区类型工具栏 -->
<div class="toolbar-section">
<div class="toolbar-header">
<div class="header-title">选区工具</div>
<div class="header-title">{{ t("选区工具") }}</div>
<!-- 移除关闭按钮完全通过工具切换控制显示隐藏 -->
</div>
@@ -182,6 +182,7 @@
<script setup>
import { ref, onMounted, watch } from "vue";
import { useI18n } from "vue-i18n";
import {
CreateSelectionCommand,
InvertSelectionCommand,
@@ -232,8 +233,8 @@ const hasSelection = ref(false);
const showFeatherDialog = ref(false);
const showColorPicker = ref(false);
// 国际化函数 (简单实现,可根据需要替换为实际的国际化方案)
const $t = (key) => key;
// 国际化
const { t } = useI18n();
onMounted(() => {
// 为选区管理器添加监听,以便在选区变化时更新状态
@@ -647,7 +648,7 @@ function confirmColorPicker() {
.tool-actions {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 5px;
padding: 0 10px;
}