@zzalai/leafer-multi-roi 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/README_EN.md +293 -0
- package/docs/assets/index-B2aZIWia.css +1 -0
- package/docs/assets/index-BrSsc-mD.js +1 -0
- package/docs/assets/vite-CMPW0ETM.svg +130 -0
- package/docs/index.html +14 -0
- package/index.html +13 -0
- package/package.json +61 -0
- package/project-docs/ARCHITECTURE.md +129 -0
- package/project-docs/REQUIREMENTS.md +113 -0
- package/src/App.vue +284 -0
- package/src/components/RoiEditor.vue +1544 -0
- package/src/index.ts +10 -0
- package/src/main.ts +4 -0
- package/src/types/index.ts +49 -0
- package/src/utils/coordinates.ts +46 -0
- package/src/utils/icons.ts +41 -0
- package/src/utils/uuid.ts +4 -0
- package/src/vite-env.d.ts +7 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +39 -0
- package/vite.docs.config.ts +29 -0
- package/vite.svg +130 -0
package/src/App.vue
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="app">
|
|
3
|
+
<h1>LeaferJS Multi ROI Test</h1>
|
|
4
|
+
|
|
5
|
+
<div class="editor-container">
|
|
6
|
+
<RoiEditor
|
|
7
|
+
ref="roiEditor"
|
|
8
|
+
:imageSource="imageSource"
|
|
9
|
+
:options="editorOptions"
|
|
10
|
+
@roiChange="handleRoiChange"
|
|
11
|
+
@loadStart="handleLoadStart"
|
|
12
|
+
@loadSuccess="handleLoadSuccess"
|
|
13
|
+
@loadError="handleLoadError"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="controls">
|
|
18
|
+
<h2>Controls</h2>
|
|
19
|
+
<div class="control-group">
|
|
20
|
+
<label for="imageUrl">Image URL:</label>
|
|
21
|
+
<input
|
|
22
|
+
type="text"
|
|
23
|
+
id="imageUrl"
|
|
24
|
+
v-model="imageUrl"
|
|
25
|
+
placeholder="Enter image URL"
|
|
26
|
+
/>
|
|
27
|
+
<!-- <button @click="updateImageUrl">Update</button> -->
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="control-group">
|
|
31
|
+
<button @click="fetchRoiData">Get ROI Data</button>
|
|
32
|
+
<button @click="exportData">Export ROI Data</button>
|
|
33
|
+
<!-- <button @click="clearAll">Clear All</button> -->
|
|
34
|
+
<button @click="refreshImage">Refresh Image</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="control-group">
|
|
38
|
+
<h3>Canvas Export/Import</h3>
|
|
39
|
+
<button @click="exportCanvasJSON">Export Canvas JSON</button>
|
|
40
|
+
<input
|
|
41
|
+
type="file"
|
|
42
|
+
ref="fileInput"
|
|
43
|
+
style="display: none"
|
|
44
|
+
accept=".json"
|
|
45
|
+
@change="importCanvasJSON"
|
|
46
|
+
/>
|
|
47
|
+
<button @click="triggerFileInput">Import Canvas JSON</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="output">
|
|
52
|
+
<h2>ROI Data</h2>
|
|
53
|
+
<pre>{{ roiData }}</pre>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="status">
|
|
57
|
+
<h2>Status</h2>
|
|
58
|
+
<p>Image Load Status: {{ loadStatus }}</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import { ref, computed } from 'vue'
|
|
65
|
+
import RoiEditor from './components/RoiEditor.vue'
|
|
66
|
+
|
|
67
|
+
// 图片URL
|
|
68
|
+
const imageUrl = ref('https://picsum.photos/1280/1080')
|
|
69
|
+
const imageSource = computed(() => ({
|
|
70
|
+
id: 'test-image',
|
|
71
|
+
url: imageUrl.value
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
// 编辑器选项
|
|
75
|
+
const editorOptions = ref({
|
|
76
|
+
regionStyle: {
|
|
77
|
+
fill: '#fcc',
|
|
78
|
+
stroke: '#c63',
|
|
79
|
+
strokeWidth: 10
|
|
80
|
+
},
|
|
81
|
+
selectedRegionStyle: {
|
|
82
|
+
fill: '#f6c',
|
|
83
|
+
stroke: '#a9d',
|
|
84
|
+
strokeWidth: 10
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// 状态
|
|
89
|
+
const loadStatus = ref('idle')
|
|
90
|
+
const roiData = ref('')
|
|
91
|
+
const roiEditor = ref<InstanceType<typeof RoiEditor> | null>(null)
|
|
92
|
+
|
|
93
|
+
// 处理ROI变化
|
|
94
|
+
const handleRoiChange = (data: any) => {
|
|
95
|
+
roiData.value = JSON.stringify(data, null, 2)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 处理图片加载开始
|
|
99
|
+
const handleLoadStart = () => {
|
|
100
|
+
loadStatus.value = 'loading'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 处理图片加载成功
|
|
104
|
+
const handleLoadSuccess = () => {
|
|
105
|
+
loadStatus.value = 'success'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 处理图片加载失败
|
|
109
|
+
const handleLoadError = (error: any) => {
|
|
110
|
+
loadStatus.value = 'error'
|
|
111
|
+
console.error('Image load error:', error)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 更新图片URL
|
|
115
|
+
// const updateImageUrl = () => {
|
|
116
|
+
// // 图片URL会通过computed自动更新
|
|
117
|
+
// }
|
|
118
|
+
|
|
119
|
+
// 重新加载图片
|
|
120
|
+
const refreshImage = () => {
|
|
121
|
+
if (roiEditor.value) {
|
|
122
|
+
roiEditor.value.loadImage()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 导出ROI数据
|
|
127
|
+
const exportData = () => {
|
|
128
|
+
// 调用RoiEditor的getROIAnnotations方法获取最新数据
|
|
129
|
+
if (roiEditor.value) {
|
|
130
|
+
const annotations = roiEditor.value.getROIAnnotations()
|
|
131
|
+
const data = JSON.stringify(annotations, null, 2)
|
|
132
|
+
|
|
133
|
+
const blob = new Blob([data], { type: 'application/json' })
|
|
134
|
+
const url = URL.createObjectURL(blob)
|
|
135
|
+
const a = document.createElement('a')
|
|
136
|
+
a.href = url
|
|
137
|
+
a.download = 'roi-data.json'
|
|
138
|
+
a.click()
|
|
139
|
+
URL.revokeObjectURL(url)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 清空所有ROI
|
|
144
|
+
// const clearAll = () => {
|
|
145
|
+
// // 后续实现
|
|
146
|
+
// }
|
|
147
|
+
|
|
148
|
+
// 手动获取ROI数据
|
|
149
|
+
const fetchRoiData = () => {
|
|
150
|
+
if (roiEditor.value) {
|
|
151
|
+
const annotations = roiEditor.value.getROIAnnotations()
|
|
152
|
+
roiData.value = JSON.stringify(annotations, null, 2)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 导出画布JSON
|
|
157
|
+
const exportCanvasJSON = () => {
|
|
158
|
+
if (roiEditor.value) {
|
|
159
|
+
const json = roiEditor.value.exportCanvasJSON()
|
|
160
|
+
|
|
161
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
162
|
+
const url = URL.createObjectURL(blob)
|
|
163
|
+
const a = document.createElement('a')
|
|
164
|
+
a.href = url
|
|
165
|
+
a.download = 'canvas-data.json'
|
|
166
|
+
a.click()
|
|
167
|
+
URL.revokeObjectURL(url)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 触发文件输入
|
|
172
|
+
const fileInput = ref<HTMLInputElement | null>(null)
|
|
173
|
+
const triggerFileInput = () => {
|
|
174
|
+
fileInput.value?.click()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 导入画布JSON
|
|
178
|
+
const importCanvasJSON = async (event: Event) => {
|
|
179
|
+
const target = event.target as HTMLInputElement
|
|
180
|
+
const file = target.files?.[0]
|
|
181
|
+
if (file) {
|
|
182
|
+
const reader = new FileReader()
|
|
183
|
+
reader.onload = async (e) => {
|
|
184
|
+
const jsonString = e.target?.result as string
|
|
185
|
+
if (roiEditor.value) {
|
|
186
|
+
const success = await roiEditor.value.importCanvasJSON(jsonString, { resetZoom: true })
|
|
187
|
+
if (success) {
|
|
188
|
+
alert('Canvas imported successfully!')
|
|
189
|
+
} else {
|
|
190
|
+
alert('Failed to import canvas.')
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
reader.readAsText(file)
|
|
195
|
+
}
|
|
196
|
+
// 重置文件输入
|
|
197
|
+
target.value = ''
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<style scoped>
|
|
202
|
+
.app {
|
|
203
|
+
max-width: 1200px;
|
|
204
|
+
margin: 0 auto;
|
|
205
|
+
padding: 20px;
|
|
206
|
+
font-family: Arial, sans-serif;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
h1 {
|
|
210
|
+
text-align: center;
|
|
211
|
+
margin-bottom: 30px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.editor-container {
|
|
215
|
+
width: 100%;
|
|
216
|
+
height: 600px;
|
|
217
|
+
border: 1px solid #ddd;
|
|
218
|
+
border-radius: 8px;
|
|
219
|
+
overflow: hidden;
|
|
220
|
+
margin-bottom: 30px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.controls {
|
|
224
|
+
margin-bottom: 30px;
|
|
225
|
+
padding: 20px;
|
|
226
|
+
background-color: #f5f5f5;
|
|
227
|
+
border-radius: 8px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.control-group {
|
|
231
|
+
margin-bottom: 15px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
label {
|
|
235
|
+
display: block;
|
|
236
|
+
margin-bottom: 5px;
|
|
237
|
+
font-weight: bold;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
input {
|
|
241
|
+
width: 100%;
|
|
242
|
+
padding: 8px;
|
|
243
|
+
border: 1px solid #ddd;
|
|
244
|
+
border-radius: 4px;
|
|
245
|
+
margin-bottom: 10px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
button {
|
|
249
|
+
padding: 8px 16px;
|
|
250
|
+
background-color: #007bff;
|
|
251
|
+
color: white;
|
|
252
|
+
border: none;
|
|
253
|
+
border-radius: 4px;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
margin-right: 10px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
button:hover {
|
|
259
|
+
background-color: #0069d9;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.output {
|
|
263
|
+
margin-bottom: 30px;
|
|
264
|
+
padding: 20px;
|
|
265
|
+
background-color: #f5f5f5;
|
|
266
|
+
border-radius: 8px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pre {
|
|
270
|
+
white-space: pre-wrap;
|
|
271
|
+
word-wrap: break-word;
|
|
272
|
+
background-color: white;
|
|
273
|
+
padding: 15px;
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
max-height: 300px;
|
|
276
|
+
overflow-y: auto;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.status {
|
|
280
|
+
padding: 20px;
|
|
281
|
+
background-color: #f5f5f5;
|
|
282
|
+
border-radius: 8px;
|
|
283
|
+
}
|
|
284
|
+
</style>
|