Skip to content

Commit 97c1006

Browse files
author
阿岳
committed
fix: 选中图片右键时,增加红蓝通道对调功能、复制到粘贴板的功能
1 parent d4da971 commit 97c1006

File tree

2 files changed

+178
-53
lines changed

2 files changed

+178
-53
lines changed

app/src/components/context-menu-content.tsx

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ import {
8484
Equal,
8585
Save,
8686
} from "lucide-react";
87+
import { Image as TauriImage } from "@tauri-apps/api/image";
88+
import { writeImage } from "@tauri-apps/plugin-clipboard-manager";
8789
import { useTranslation } from "react-i18next";
8890
import { toast } from "sonner";
8991
import tailwindColors from "tailwindcss/colors";
@@ -1025,67 +1027,137 @@ export default function MyContextMenuContent() {
10251027

10261028
{/* 存在选中 ImageNode */}
10271029
{p.stageManager.getSelectedEntities().filter((it) => it instanceof ImageNode).length > 0 && (
1028-
<Item
1029-
onClick={async () => {
1030-
// 检查是否是草稿模式
1031-
if (p.isDraft) {
1032-
toast.error("请先保存项目后再导出图片");
1033-
return;
1034-
}
1030+
<>
1031+
<Item
1032+
onClick={async () => {
1033+
// 获取所有选中的 ImageNode
1034+
const selectedImageNodes = p.stageManager
1035+
.getSelectedEntities()
1036+
.filter((it) => it instanceof ImageNode) as ImageNode[];
10351037

1036-
// 获取所有选中的 ImageNode
1037-
const selectedImageNodes = p.stageManager
1038-
.getSelectedEntities()
1039-
.filter((it) => it instanceof ImageNode) as ImageNode[];
1038+
if (selectedImageNodes.length === 0) {
1039+
toast.error("请选中图片节点");
1040+
return;
1041+
}
10401042

1041-
if (selectedImageNodes.length === 0) {
1042-
toast.error("请选中图片节点");
1043-
return;
1044-
}
1043+
// 复制第一张图片到剪贴板(如果有多张图片,只复制第一张)
1044+
const imageNode = selectedImageNodes[0];
1045+
const blob = p.attachments.get(imageNode.attachmentId);
1046+
if (blob) {
1047+
try {
1048+
const arrayBuffer = await blob.arrayBuffer();
1049+
const tauriImage = await TauriImage.fromBytes(new Uint8Array(arrayBuffer));
1050+
await writeImage(tauriImage);
1051+
if (selectedImageNodes.length === 1) {
1052+
toast.success("已将选中的图片复制到系统剪贴板");
1053+
} else {
1054+
toast.success(`已将第1张图片复制到系统剪贴板(共${selectedImageNodes.length}张)`);
1055+
}
1056+
} catch (error) {
1057+
console.error("复制图片到剪贴板失败:", error);
1058+
toast.error("复制图片到剪贴板失败");
1059+
}
1060+
} else {
1061+
toast.error("无法获取图片数据");
1062+
}
1063+
}}
1064+
>
1065+
<Clipboard />
1066+
复制图片到系统剪贴板
1067+
</Item>
1068+
<Item
1069+
onClick={() => {
1070+
// 获取所有选中的 ImageNode
1071+
const selectedImageNodes = p.stageManager
1072+
.getSelectedEntities()
1073+
.filter((it) => it instanceof ImageNode) as ImageNode[];
10451074

1046-
// 根据图片数量决定提示信息
1047-
const isBatch = selectedImageNodes.length > 1;
1048-
const promptMessage = isBatch
1049-
? `请输入文件名(不含扩展名,将为 ${selectedImageNodes.length} 张图片添加数字后缀)`
1050-
: `请输入文件名(不含扩展名,将自动添加扩展名)`;
1075+
if (selectedImageNodes.length === 0) {
1076+
toast.error("请选中图片节点");
1077+
return;
1078+
}
10511079

1052-
// 弹出输入框 - 只弹出一次
1053-
const fileName = await Dialog.input("另存图片", promptMessage, {
1054-
placeholder: "image",
1055-
});
1080+
// 对每张图片进行红蓝通道对调
1081+
for (const imageNode of selectedImageNodes) {
1082+
imageNode.swapRedBlueChannels();
1083+
}
10561084

1057-
if (!fileName) {
1058-
return; // 用户取消
1059-
}
1085+
// 记录历史步骤
1086+
p.historyManager.recordStep();
10601087

1061-
// 验证文件名是否合法
1062-
const invalidChars = /[/\\:*?"<>|]/;
1063-
if (invalidChars.test(fileName)) {
1064-
toast.error('文件名包含非法字符:/ \\ : * ? " < > |');
1065-
return;
1066-
}
1088+
// 显示提示信息
1089+
if (selectedImageNodes.length === 1) {
1090+
toast.success("已对调图片的红蓝通道");
1091+
} else {
1092+
toast.success(`已对调 ${selectedImageNodes.length} 张图片的红蓝通道`);
1093+
}
1094+
}}
1095+
>
1096+
<ArrowLeftRight />
1097+
对调图片红蓝通道
1098+
</Item>
1099+
<Item
1100+
onClick={async () => {
1101+
// 检查是否是草稿模式
1102+
if (p.isDraft) {
1103+
toast.error("请先保存项目后再导出图片");
1104+
return;
1105+
}
1106+
1107+
// 获取所有选中的 ImageNode
1108+
const selectedImageNodes = p.stageManager
1109+
.getSelectedEntities()
1110+
.filter((it) => it instanceof ImageNode) as ImageNode[];
1111+
1112+
if (selectedImageNodes.length === 0) {
1113+
toast.error("请选中图片节点");
1114+
return;
1115+
}
10671116

1068-
// 调用工具函数导出图片
1069-
const { successCount, failedCount } = await exportImagesToProjectDirectory(
1070-
selectedImageNodes,
1071-
p.uri.fsPath,
1072-
p.attachments,
1073-
fileName,
1074-
);
1117+
// 根据图片数量决定提示信息
1118+
const isBatch = selectedImageNodes.length > 1;
1119+
const promptMessage = isBatch
1120+
? `请输入文件名(不含扩展名,将为 ${selectedImageNodes.length} 张图片添加数字后缀)`
1121+
: `请输入文件名(不含扩展名,将自动添加扩展名)`;
10751122

1076-
// 显示结果提示
1077-
if (successCount > 0 && failedCount === 0) {
1078-
toast.success(`成功保存 ${successCount} 张图片`);
1079-
} else if (successCount > 0 && failedCount > 0) {
1080-
toast.warning(`成功保存 ${successCount} 张图片,${failedCount} 张失败`);
1081-
} else {
1082-
toast.error(`保存失败,请检查文件名或文件权限`);
1083-
}
1084-
}}
1085-
>
1086-
<Save />
1087-
另存图片到当前prg所在目录下
1088-
</Item>
1123+
// 弹出输入框 - 只弹出一次
1124+
const fileName = await Dialog.input("另存图片", promptMessage, {
1125+
placeholder: "image",
1126+
});
1127+
1128+
if (!fileName) {
1129+
return; // 用户取消
1130+
}
1131+
1132+
// 验证文件名是否合法
1133+
const invalidChars = /[/\\:*?"<>|]/;
1134+
if (invalidChars.test(fileName)) {
1135+
toast.error('文件名包含非法字符:/ \\ : * ? " < > |');
1136+
return;
1137+
}
1138+
1139+
// 调用工具函数导出图片
1140+
const { successCount, failedCount } = await exportImagesToProjectDirectory(
1141+
selectedImageNodes,
1142+
p.uri.fsPath,
1143+
p.attachments,
1144+
fileName,
1145+
);
1146+
1147+
// 显示结果提示
1148+
if (successCount > 0 && failedCount === 0) {
1149+
toast.success(`成功保存 ${successCount} 张图片`);
1150+
} else if (successCount > 0 && failedCount > 0) {
1151+
toast.warning(`成功保存 ${successCount} 张图片,${failedCount} 张失败`);
1152+
} else {
1153+
toast.error(`保存失败,请检查文件名或文件权限`);
1154+
}
1155+
}}
1156+
>
1157+
<Save />
1158+
另存图片到当前prg所在目录下
1159+
</Item>
1160+
</>
10891161
)}
10901162
</Content>
10911163
);

app/src/core/stage/stageObject/entity/ImageNode.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,59 @@ export class ImageNode extends ConnectableEntity implements ResizeAble {
173173
});
174174
}
175175

176+
/**
177+
* 交换图片的红蓝通道
178+
* 将图片的红色和蓝色通道对调,绿色和alpha通道保持不变
179+
* 并将处理后的图片数据保存到project.attachments中,实现持久化存储
180+
*/
181+
swapRedBlueChannels() {
182+
if (!this.bitmap) return;
183+
184+
// 创建临时canvas
185+
const canvas = document.createElement("canvas");
186+
const ctx = canvas.getContext("2d");
187+
if (!ctx) return;
188+
189+
// 设置canvas尺寸
190+
canvas.width = this.bitmap.width;
191+
canvas.height = this.bitmap.height;
192+
193+
// 绘制原图
194+
ctx.drawImage(this.bitmap, 0, 0);
195+
196+
// 获取图像数据
197+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
198+
const data = imageData.data;
199+
200+
// 交换红色和蓝色通道
201+
for (let i = 0; i < data.length; i += 4) {
202+
const r = data[i]; // R
203+
const b = data[i + 2]; // B
204+
data[i] = b; // R = B
205+
data[i + 2] = r; // B = R
206+
// data[i + 1] 保持不变(绿色通道)
207+
// data[i + 3] 保持不变(alpha通道)
208+
}
209+
210+
// 将修改后的图像数据绘制回canvas
211+
ctx.putImageData(imageData, 0, 0);
212+
213+
// 创建新的ImageBitmap并保存到attachments中
214+
createImageBitmap(imageData).then((newBitmap) => {
215+
this.bitmap = newBitmap;
216+
217+
// 将canvas转换为Blob并保存到project.attachments中
218+
canvas.toBlob((blob) => {
219+
if (blob) {
220+
// 创建新的attachmentId并替换原有数据
221+
const newAttachmentId = this.project.addAttachment(blob);
222+
// 更新当前节点的attachmentId
223+
this.attachmentId = newAttachmentId;
224+
}
225+
}, "image/png");
226+
});
227+
}
228+
176229
/**
177230
* 处理拖拽缩放逻辑
178231
* @param delta 拖拽距离向量

0 commit comments

Comments
 (0)