Skip to content

Commit 4d88094

Browse files
committed
feat: 新增 fullmap、json、mttpl 三种导出方式
1 parent ce0a63f commit 4d88094

File tree

17 files changed

+866
-249
lines changed

17 files changed

+866
-249
lines changed

template-creator/export-image.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<title>About</title>
9+
</head>
10+
11+
<body>
12+
<div id="app">
13+
14+
</div>
15+
<script type="module" src="/src/export-image.ts"></script>
16+
</body>
17+
18+
</html>

template-creator/package-lock.json

Lines changed: 190 additions & 210 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

template-creator/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ tauri-build = { version = "1.2", features = [] }
1515
[dependencies]
1616
serde_json = "1.0"
1717
serde = { version = "1.0", features = ["derive"] }
18-
tauri = { version = "1.2", features = ["app-all", "devtools", "dialog-open", "dialog-save", "fs-read-file", "fs-write-file", "notification-all", "os-all", "protocol-all", "shell-open", "window-hide", "window-set-size", "window-set-title", "window-show", "window-start-dragging"] }
18+
tauri = { version = "1.2", features = ["app-all", "devtools", "dialog-open", "dialog-save", "fs-read-file", "fs-write-file", "notification-all", "os-all", "protocol-all", "shell-open", "window-all"] }
1919

2020
[features]
2121
# by default Tauri runs in production mode

template-creator/src-tauri/tauri.conf.json

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
"removeDir": false,
2828
"removeFile": false,
2929
"renameFile": false,
30-
"scope": [],
30+
"scope": [
31+
"$DOWNLOAD/*"
32+
],
3133
"writeFile": true
3234
},
3335
"dialog": {
@@ -44,36 +46,7 @@
4446
"assetScope": []
4547
},
4648
"window": {
47-
"all": false,
48-
"center": false,
49-
"close": false,
50-
"create": false,
51-
"hide": true,
52-
"maximize": false,
53-
"minimize": false,
54-
"print": false,
55-
"requestUserAttention": false,
56-
"setAlwaysOnTop": false,
57-
"setCursorGrab": false,
58-
"setCursorIcon": false,
59-
"setCursorPosition": false,
60-
"setCursorVisible": false,
61-
"setDecorations": false,
62-
"setFocus": false,
63-
"setFullscreen": false,
64-
"setIcon": false,
65-
"setIgnoreCursorEvents": false,
66-
"setMaxSize": false,
67-
"setMinSize": false,
68-
"setPosition": false,
69-
"setResizable": false,
70-
"setSize": true,
71-
"setSkipTaskbar": false,
72-
"setTitle": true,
73-
"show": true,
74-
"startDragging": true,
75-
"unmaximize": false,
76-
"unminimize": false
49+
"all": true
7750
},
7851
"notification": {
7952
"all": true

template-creator/src/App.vue

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { setTitle } from "@/funcs/title";
2121
import { cameraTypeInjectKey, modalInjectKey, notificationsInjectKey, projectInjectKey } from "@/utils/injects";
2222
import Notifications from "./components/Notifications.vue";
2323
import { RegionOfInterest } from "./dtos/ROI";
24+
import { exportImage } from "./core/exports/image";
25+
import { exportMTTPL } from "./core/exports/mttpl";
26+
import { exportJSON } from "./core/exports/json";
2427
// import { config, container as JenesiusModalContainer, promptModal } from "jenesius-vue-modal";
2528
2629
// config({
@@ -170,6 +173,26 @@ async function changeRoiName(uuid?: string) {
170173
}
171174
}
172175
176+
async function exportTo(exType: string) {
177+
if (project.value?.image) {
178+
const bitmap = await createImageBitmap(project.value.image);
179+
switch (exType) {
180+
case 'fullmap':
181+
exportImage(bitmap, project.value.toDrawable());
182+
break;
183+
case 'mttpl':
184+
exportMTTPL(bitmap, project.value.toDrawable());
185+
break;
186+
case 'json':
187+
exportJSON(bitmap, project.value.toDrawable());
188+
break;
189+
default:
190+
alert('未知的导出类型');
191+
break;
192+
}
193+
}
194+
}
195+
173196
function showNotification() {
174197
console.log('showNotification');
175198
notifications.value?.addNotification('Hello World');
@@ -214,7 +237,7 @@ onUnmounted(() => {
214237
<Sidebar>
215238
<template #top>
216239
<Toolbar @add="addROI" @remove="removeROI" @clear="clearROIs" @switch="switchCamera" @rename="changeProjectName"
217-
@roi-rename="changeRoiName" @project-export="showNotification" />
240+
@roi-rename="changeRoiName" @project-export="exportTo" />
218241
<RectList :rois="project?.rois" :selected="project?.selectedRoiId"
219242
@select="($event: string) => project && (project.selectedRoiId = $event)" @rename="changeRoiName"
220243
@remove="removeROI" @duplicate="duplicateROI" />

template-creator/src/Export.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<template>
2+
<div class="window">
3+
<!-- <div>
4+
{{ output }}
5+
</div> -->
6+
<img v-if="dataURL" :src="dataURL" alt="">
7+
</div>
8+
</template>
9+
10+
<script setup lang="ts">
11+
import { listen } from '@tauri-apps/api/event';
12+
import { WebviewWindow } from '@tauri-apps/api/window';
13+
import { onMounted, ref } from 'vue';
14+
15+
const output = ref('');
16+
const dataURL = ref<string>();
17+
18+
interface ExportInitedPayload {
19+
dataURL: string;
20+
}
21+
22+
23+
24+
onMounted(() => {
25+
listen<ExportInitedPayload>('export-load', (e) => {
26+
console.log(e.payload.dataURL);
27+
output.value = e.payload.dataURL;
28+
dataURL.value = e.payload.dataURL;
29+
});
30+
WebviewWindow.getByLabel('main')?.emit('export-inited');
31+
});
32+
</script>
33+
34+
<style scoped lang="scss">
35+
div.window {
36+
display: flex;
37+
width: 100vw;
38+
height: 100vh;
39+
overflow: hidden;
40+
justify-content: center;
41+
align-items: center;
42+
43+
img {
44+
max-width: 98%;
45+
max-height: 98%;
46+
}
47+
}
48+
</style>

template-creator/src/components/sidebar/Toolbar.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
操作
55
</div>
66
<ToolbarButton icon="add" @click="emit('add')" title="添加 ROI" />
7-
<ToolbarButton icon="remove" @click="emit('remove')" disabled title="移除选中的 ROI" />
7+
<!-- <ToolbarButton icon="remove" @click="emit('remove')" disabled title="移除选中的 ROI" /> -->
88
<ToolbarButton icon="clear_all" @click="emit('clear')" title="清除所有的 ROI" />
9-
<ToolbarButton icon="chip_extraction" title="导出" disabled @click="emit('project-export')" />
9+
<ToolbarButton icon="photo_library" title="导出示意图" @click="emit('project-export', 'fullmap')" />
10+
<ToolbarButton icon="data_object" title="导出 JSON" @click="emit('project-export', 'json')" />
11+
<ToolbarButton icon="ungroup" title="导出模板文件" @click="emit('project-export', 'mttpl')" />
1012
<ToolbarButton icon="edit" title="重命名项目" @click="emit('rename')" />
11-
<ToolbarButton icon="edit_note" title="重命名选中的 ROI" disabled @click="emit('roi-rename')" />
1213
<ToolbarButton icon="tune" disabled title="设置" />
1314
<ToolbarButton icon="cameraswitch" @click="emit('switch')" title="切换 ROI 和 Rect" />
1415
</div>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { WebviewWindow } from '@tauri-apps/api/window';
2+
import { DrawableProject } from '../../dtos/Drawable';
3+
4+
export function exportImage(image: ImageBitmap, drawable: DrawableProject) {
5+
const layer0 = document.createElement('canvas');
6+
layer0.width = image.width;
7+
layer0.height = image.height;
8+
const layer1 = document.createElement('canvas');
9+
layer1.width = image.width;
10+
layer1.height = image.height;
11+
const ctx0 = layer0.getContext('2d')!;
12+
const ctx1 = layer1.getContext('2d')!;
13+
ctx0.drawImage(image, 0, 0);
14+
// draw warning strip in layer1
15+
const pattern = drawWarningStrip(ctx1);
16+
ctx1.fillStyle = pattern;
17+
ctx1.fillRect(0, 0, layer1.width, layer1.height);
18+
ctx1.clearRect(drawable.bounds.x, drawable.bounds.y, drawable.bounds.width, drawable.bounds.height);
19+
ctx1.fillStyle = 'rgba(255, 255, 255, 0.5)';
20+
// draw layer1 on layer0
21+
ctx0.drawImage(layer1, 0, 0);
22+
// export to data url and open in webview
23+
const dataURL = layer0.toDataURL('image/png');
24+
console.log(dataURL);
25+
openExportedImage(dataURL);
26+
}
27+
28+
export async function openExportedImage(dataURL: string) {
29+
let webview: WebviewWindow;
30+
try {
31+
webview = WebviewWindow.getByLabel('export-image')!;
32+
if (!webview) {
33+
throw new Error('webview not found');
34+
}
35+
webview.emit('export-load', { dataURL });
36+
} catch (error) {
37+
webview = new WebviewWindow('export-image', {
38+
url: 'export-image.html',
39+
center: true,
40+
resizable: true,
41+
minHeight: 600,
42+
minWidth: 600,
43+
width: 600,
44+
height: 600,
45+
title: 'Export Image',
46+
});
47+
await webview.once('export-inited', (e) => {
48+
console.log('export-inited');
49+
WebviewWindow.getByLabel(e.windowLabel)?.emit('export-load', { dataURL });
50+
});
51+
} finally {
52+
webview!.setFocus();
53+
}
54+
}
55+
56+
/**
57+
* 绘制倾角为 45° 的黄黑相间的警告条纹
58+
* @param ctx Canvas 的上下文
59+
* @param interval 同色条纹之间的间隔
60+
*/
61+
function drawWarningStrip(ctx: CanvasRenderingContext2D, interval = 100) {
62+
const patternCanvas = document.createElement('canvas');
63+
patternCanvas.width = interval * 2;
64+
patternCanvas.height = interval * 2;
65+
const pCtx = patternCanvas.getContext('2d')!;
66+
pCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
67+
pCtx.beginPath();
68+
pCtx.moveTo(0, 0);
69+
pCtx.lineTo(interval, 0);
70+
pCtx.lineTo(0, interval);
71+
pCtx.closePath();
72+
pCtx.fill();
73+
pCtx.beginPath();
74+
pCtx.moveTo(interval * 2, 0);
75+
pCtx.lineTo(0, interval * 2);
76+
pCtx.lineTo(interval, interval * 2);
77+
pCtx.lineTo(interval * 2, interval);
78+
pCtx.closePath();
79+
pCtx.fill();
80+
pCtx.fillStyle = 'rgba(255, 255, 0, 0.5)';
81+
pCtx.beginPath();
82+
pCtx.moveTo(0, interval);
83+
pCtx.lineTo(0, interval * 2);
84+
pCtx.lineTo(interval * 2, 0);
85+
pCtx.lineTo(interval, 0);
86+
pCtx.closePath();
87+
pCtx.fill();
88+
pCtx.beginPath();
89+
pCtx.moveTo(interval, interval * 2);
90+
pCtx.lineTo(interval * 2, interval * 2);
91+
pCtx.lineTo(interval * 2, interval);
92+
pCtx.closePath();
93+
pCtx.fill();
94+
return ctx.createPattern(patternCanvas, 'repeat')!;
95+
}
Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,65 @@
1-
import { Project } from "@/dtos/Project";
1+
import { DrawableProject } from "@/dtos/Drawable";
2+
import { writeBinaryFile, BaseDirectory, writeTextFile } from "@tauri-apps/api/fs";
23

4+
export interface JSONMetadata {
5+
version: number;
6+
name: string;
7+
width: number;
8+
height: number;
9+
screenRatio: number;
10+
screenHeight: number;
11+
screenWidth: number;
12+
roi: {
13+
x: number;
14+
y: number;
15+
width: number;
16+
height: number;
17+
anchor: number;
18+
}
19+
}
320

4-
export function export2json(project: Project) {
5-
21+
export async function exportJSON(image: ImageBitmap, project: DrawableProject) {
22+
for (const roi of project.rois) {
23+
const { x, y, width, height } = roi.rect;
24+
const tpl = document.createElement('canvas');
25+
tpl.width = width;
26+
tpl.height = height;
27+
const ctx = tpl.getContext('2d')!;
28+
ctx.drawImage(image, x, y, width, height, 0, 0, width, height);
29+
// convert canvas to binary file as uint8array
30+
const uint8array = await new Promise<Uint8Array>(resolve => {
31+
tpl.toBlob(blob => {
32+
const reader = new FileReader();
33+
reader.onload = () => {
34+
const arrayBuffer = reader.result as ArrayBuffer;
35+
const uint8array = new Uint8Array(arrayBuffer);
36+
resolve(uint8array);
37+
};
38+
reader.readAsArrayBuffer(blob!);
39+
});
40+
});
41+
// write binary file
42+
const tplname = `${project.name}-${roi.name}.png`;
43+
const jsonname = `${project.name}-${roi.name}.json`;
44+
const metadata: JSONMetadata = {
45+
version: 1,
46+
name: project.name,
47+
width: width,
48+
height: height,
49+
screenRatio: project.ratio,
50+
screenHeight: project.screenHeight,
51+
screenWidth: project.screenWidth,
52+
roi: {
53+
x: roi.x,
54+
y: roi.y,
55+
width: roi.width,
56+
height: roi.height,
57+
anchor: roi.anchor,
58+
}
59+
}
60+
await writeBinaryFile(tplname, uint8array, { dir: BaseDirectory.Download });
61+
console.log(`exported ${tplname}`);
62+
await writeTextFile(jsonname, JSON.stringify(metadata), { dir: BaseDirectory.Download });
63+
console.log(`exported ${jsonname}`);
64+
}
665
}

0 commit comments

Comments
 (0)