sort-dialog.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. /**
  2. * 步骤:
  3. * 1. 创建startSortDialog()函数
  4. * 2. 创建ocrTxtResultArr用来接收外部传入的识别结果txt文件路径数组
  5. * 3. 根据check文件夹里面的txt文件先排序,文件名的index越小越靠数组前位,为每一个check文件夹创建一个数组,把文件夹内文件名排序结果保存到数组里。
  6. * 4. 再新建一个外围数组,根据check名字的index顺序,把每一个check文件夹内的数组保存到外围数组里。index越小越靠前。
  7. * 5. 返回最终的二维数组
  8. */
  9. import fs from 'fs';
  10. import path from 'path';
  11. function startSortDialog(ocrTxtResultArr) {
  12. try {
  13. console.log('🚀 开始整理对话排序流程...');
  14. // 步骤2: 创建变量ocrTxtResultArr 用来接收外部传入的识别结果txt文件路径数组
  15. console.log('\n📋 步骤2: 验证txt文件路径数组参数');
  16. if (!ocrTxtResultArr || !Array.isArray(ocrTxtResultArr)) {
  17. throw new Error('步骤2失败: ocrTxtResultArr 必须是一个数组');
  18. }
  19. if (ocrTxtResultArr.length === 0) {
  20. throw new Error('步骤2失败: ocrTxtResultArr 不能为空数组');
  21. }
  22. console.log(`✅ txt文件路径数组长度: ${ocrTxtResultArr.length}`);
  23. // 步骤3: 根据check文件夹里面的txt文件先排序
  24. console.log('\n📁 步骤3: 按check文件夹分组并排序txt文件...');
  25. // 按check文件夹分组
  26. const checkGroups = {}; // { checkIndex: [txtPaths] }
  27. for (const txtPath of ocrTxtResultArr) {
  28. // 获取txt文件所在的文件夹路径
  29. const txtDir = path.dirname(txtPath);
  30. const txtFileName = path.basename(txtPath);
  31. // 提取check文件夹名称(如 check1, check2 等)
  32. const checkFolderName = path.basename(txtDir);
  33. const checkMatch = checkFolderName.match(/^check(\d+)$/);
  34. if (checkMatch) {
  35. const checkIndex = parseInt(checkMatch[1]);
  36. if (!checkGroups[checkIndex]) {
  37. checkGroups[checkIndex] = [];
  38. }
  39. checkGroups[checkIndex].push(txtPath);
  40. } else {
  41. console.log(` ⚠️ 跳过非check文件夹: ${checkFolderName}`);
  42. }
  43. }
  44. console.log(`✅ 找到 ${Object.keys(checkGroups).length} 个check文件夹`);
  45. // 为每一个check文件夹创建一个数组,把文件夹内文件名排序结果保存到数组里
  46. const sortedCheckGroups = {};
  47. for (const [checkIndexStr, txtPaths] of Object.entries(checkGroups)) {
  48. const checkIndex = parseInt(checkIndexStr);
  49. // 按文件名中的index排序(文件名的index越小越靠数组前位)
  50. const sortedPaths = txtPaths.sort((a, b) => {
  51. const fileNameA = path.basename(a, path.extname(a));
  52. const fileNameB = path.basename(b, path.extname(b));
  53. // 提取文件名中的数字(如 dialog_1.txt -> 1)
  54. const matchA = fileNameA.match(/(\d+)$/);
  55. const matchB = fileNameB.match(/(\d+)$/);
  56. const indexA = matchA ? parseInt(matchA[1]) : 0;
  57. const indexB = matchB ? parseInt(matchB[1]) : 0;
  58. return indexA - indexB; // index越小越靠前
  59. });
  60. sortedCheckGroups[checkIndex] = sortedPaths;
  61. console.log(` 📁 check${checkIndex}: ${sortedPaths.length} 个txt文件`);
  62. sortedPaths.forEach((p, i) => {
  63. console.log(` ${i + 1}. ${path.basename(p)}`);
  64. });
  65. }
  66. // 步骤4: 再新建一个外围数组,根据check名字的index顺序,把每一个check文件夹内的数组保存到外围数组里
  67. console.log('\n📦 步骤4: 按check文件夹index顺序组织二维数组...');
  68. // 获取所有check索引并排序
  69. const checkIndices = Object.keys(sortedCheckGroups).map(Number).sort((a, b) => a - b);
  70. // 创建外围数组(二维数组)
  71. const resultArray = [];
  72. for (const checkIndex of checkIndices) {
  73. resultArray.push(sortedCheckGroups[checkIndex]);
  74. console.log(` ✅ check${checkIndex}: 已添加到外围数组 (${sortedCheckGroups[checkIndex].length} 个文件)`);
  75. }
  76. // 步骤5: 返回最终的二维数组
  77. console.log('\n📋 步骤5: 准备返回最终的二维数组...');
  78. console.log(`📊 二维数组结构:`);
  79. console.log(` 外层数组长度: ${resultArray.length} (${checkIndices.length} 个check文件夹)`);
  80. resultArray.forEach((checkArray, index) => {
  81. console.log(` [${index}] check${checkIndices[index]}: ${checkArray.length} 个txt文件`);
  82. });
  83. console.log(`✅ 步骤5完成: 二维数组已准备就绪`);
  84. console.log('\n🎉 所有步骤完成!');
  85. console.log(`📊 共处理 ${ocrTxtResultArr.length} 个txt文件`);
  86. console.log(`📊 共组织成 ${resultArray.length} 个check文件夹数组`);
  87. return resultArray; // 步骤5: 返回最终的二维数组
  88. } catch (error) {
  89. console.error(`\n❌ 整理对话排序失败: ${error.message}`);
  90. throw error;
  91. }
  92. }
  93. /**
  94. * 旧的函数:根据panels排序dialogues(保留用于向后兼容)
  95. * @param {Array} dialogues - 对话列表(外部传入)
  96. * @param {Array} panels - 格子列表(外部传入)
  97. * @param {number} imageWidth - 图片宽度(外部传入)
  98. * @param {number} imageHeight - 图片高度(外部传入)
  99. * @param {string} imageName - 图片名称(用于生成txt文件名)
  100. * @param {string} outputDir - 输出目录(外部传入)
  101. * @returns {Object} 包含排序后的对话列表和生成的文件信息
  102. */
  103. async function startSortDialogByPanels(dialogues, panels, imageWidth, imageHeight, imageName, outputDir) {
  104. try {
  105. // 步骤2: 创建变量dialogues 用来接收外部传入的对话列表
  106. if (!dialogues || !Array.isArray(dialogues)) {
  107. throw new Error('dialogues 参数必须是一个数组');
  108. }
  109. // 步骤3: 创建变量panels 用来接收外部传入的格子列表
  110. if (!panels || !Array.isArray(panels)) {
  111. throw new Error('panels 参数必须是一个数组');
  112. }
  113. // 步骤4: 创建变量imageWidth 用来接收外部传入的图片宽度
  114. if (!imageWidth || typeof imageWidth !== 'number') {
  115. throw new Error('imageWidth 参数必须是一个数字');
  116. }
  117. // 步骤5: 创建变量imageHeight 用来接收外部传入的图片高度
  118. if (!imageHeight || typeof imageHeight !== 'number') {
  119. throw new Error('imageHeight 参数必须是一个数字');
  120. }
  121. if (!imageName) {
  122. throw new Error('imageName 参数不能为空');
  123. }
  124. if (!outputDir) {
  125. throw new Error('outputDir 参数不能为空');
  126. }
  127. // 确保输出目录存在
  128. if (!fs.existsSync(outputDir)) {
  129. fs.mkdirSync(outputDir, { recursive: true });
  130. }
  131. console.log('='.repeat(60));
  132. console.log('📝 排序对话并生成txt文件');
  133. console.log('='.repeat(60));
  134. console.log(`📋 对话数量: ${dialogues.length}`);
  135. console.log(`📦 格子数量: ${panels.length}`);
  136. console.log(`📐 图片尺寸: ${imageWidth}x${imageHeight}`);
  137. console.log(`📂 输出目录: ${outputDir}`);
  138. // 步骤6: 根据dialogues和panels调用sortDialoguesByPanels()函数排序对话
  139. console.log('\n📋 步骤6: 调用sortDialoguesByPanels()函数排序对话...');
  140. const sortedDialogues = sortDialoguesByPanels(dialogues, panels, imageWidth, imageHeight);
  141. console.log(`✅ 已排序 ${sortedDialogues.length} 段对话`);
  142. // 步骤7: 根据outputDir路径保存结果txt文件
  143. console.log('\n💾 步骤7: 根据outputDir路径保存结果txt文件...');
  144. const result = generateCheckTxtFiles(sortedDialogues, imageName, outputDir, 5);
  145. // 统计生成的文件
  146. const txtFiles = fs.readdirSync(outputDir)
  147. .filter(f => f.startsWith(imageName) && f.endsWith('.txt'))
  148. .sort();
  149. console.log(`✅ 已生成 ${txtFiles.length} 个txt文件`);
  150. console.log('\n' + '='.repeat(60));
  151. console.log('✅ 处理完成!');
  152. console.log('='.repeat(60));
  153. return {
  154. sortedDialogues: sortedDialogues,
  155. txtFiles: txtFiles,
  156. outputDir: outputDir
  157. };
  158. } catch (error) {
  159. console.error(`\n❌ 处理失败: ${error.message}`);
  160. if (error.stack) {
  161. console.error(error.stack);
  162. }
  163. throw error;
  164. }
  165. }
  166. /**
  167. * 步骤6: 根据dialogues和panels调用sortDialoguesByPanels()函数排序对话
  168. * 按格子位置排序对话
  169. * 排序规则:
  170. * 1. 先按格子的Y坐标(从上到下)- 越往上越靠前
  171. * 2. 再按格子的X坐标(从右到左)- 越往右越靠前
  172. * 3. 同一格子内,按文字块的X坐标从右到左
  173. *
  174. * @param {Array} dialogues - 对话列表,每个包含bbox和text信息
  175. * @param {Array} panels - 格子列表
  176. * @param {number} imageWidth - 图片宽度
  177. * @param {number} imageHeight - 图片高度
  178. * @returns {Array} 排序后的对话列表
  179. */
  180. function sortDialoguesByPanels(dialogues, panels, imageWidth, imageHeight) {
  181. if (!dialogues || dialogues.length === 0) {
  182. return [];
  183. }
  184. // 计算每个对话的中心点和所属格子
  185. const dialoguesWithInfo = dialogues.map((dialogue, index) => {
  186. const bbox = dialogue.bbox;
  187. const centerX = (bbox.x1 + bbox.x2) / 2;
  188. const centerY = (bbox.y1 + bbox.y2) / 2;
  189. const panelIndex = getTextBlockPanel(dialogue, panels);
  190. return {
  191. ...dialogue,
  192. centerX,
  193. centerY,
  194. panelIndex,
  195. originalIndex: index
  196. };
  197. });
  198. // 排序规则:从右到左、从上到下(日式漫画阅读顺序)
  199. // 1. 先按格子的Y坐标(从上到下)- Y坐标越小(越往上)越靠前
  200. // 2. 再按格子的X坐标(从右到左)- X坐标越大(越靠右)越靠前
  201. // 3. 同一格子内,先按文字块的Y坐标从上到下,再按X坐标从右到左
  202. dialoguesWithInfo.sort((a, b) => {
  203. const panelA = a.panelIndex >= 0 && a.panelIndex < panels.length
  204. ? panels[a.panelIndex]
  205. : null;
  206. const panelB = b.panelIndex >= 0 && b.panelIndex < panels.length
  207. ? panels[b.panelIndex]
  208. : null;
  209. if (panelA && panelB) {
  210. // 两个都在格子里
  211. // 先按格子的Y坐标(从上到下)- Y坐标越小(越往上)越靠前
  212. const yDiff = panelA.y - panelB.y;
  213. if (Math.abs(yDiff) > imageHeight * 0.05) {
  214. return yDiff;
  215. }
  216. // 再按格子的X坐标(从右到左)- X坐标越大(越靠右)越靠前
  217. const xDiff = panelB.center_x - panelA.center_x;
  218. if (Math.abs(xDiff) > imageWidth * 0.1) {
  219. return xDiff;
  220. }
  221. // 同一格子内,先按文字块的Y坐标从上到下,再按X坐标从右到左
  222. const textYDiff = a.centerY - b.centerY;
  223. if (Math.abs(textYDiff) > (panelA.height || panelB.height) * 0.1) {
  224. return textYDiff;
  225. }
  226. // 同一行内,按X坐标从右到左
  227. return b.centerX - a.centerX;
  228. } else if (panelA) {
  229. // A在格子里,B不在
  230. return -1;
  231. } else if (panelB) {
  232. // B在格子里,A不在
  233. return 1;
  234. } else {
  235. // 两个都不在格子里,按Y坐标从上到下,X坐标从右到左
  236. const yDiff = a.centerY - b.centerY;
  237. if (Math.abs(yDiff) > imageHeight * 0.05) {
  238. return yDiff;
  239. }
  240. return b.centerX - a.centerX;
  241. }
  242. });
  243. // 移除临时添加的属性
  244. return dialoguesWithInfo.map(({ centerX, centerY, panelIndex, originalIndex, ...rest }) => rest);
  245. }
  246. /**
  247. * 步骤7: 根据outputDir路径保存结果txt文件
  248. * 生成check.txt文件
  249. * 对对话进行排序和分组,然后生成txt文件
  250. *
  251. * @param {Array} dialogues - 对话列表,每个包含bbox和text信息
  252. * @param {string} imageName - 图片名称(不含扩展名)
  253. * @param {string} outputDir - 输出目录
  254. * @param {number} yThreshold - y1分组阈值,默认5(正负5以内的对话归为同一气泡框)
  255. * @returns {Array} 排序后的对话列表
  256. */
  257. function generateCheckTxtFiles(dialogues, imageName, outputDir, yThreshold = 5) {
  258. if (!dialogues || dialogues.length === 0) {
  259. return [];
  260. }
  261. // 确保输出目录存在
  262. if (!fs.existsSync(outputDir)) {
  263. fs.mkdirSync(outputDir, { recursive: true });
  264. }
  265. // 检查是否有panel_index和block_index,如果有则按panel_index和block_index分组
  266. const hasPanelIndex = dialogues.some(d => d.panel_index !== undefined);
  267. const hasBlockIndex = dialogues.some(d => d.block_index !== undefined);
  268. if (hasPanelIndex && hasBlockIndex) {
  269. // 先清理所有旧的txt文件(按panel分组清理)
  270. const panelsToClean = new Set();
  271. for (const dialogue of dialogues) {
  272. const panelIndex = dialogue.panel_index || 1;
  273. panelsToClean.add(panelIndex);
  274. }
  275. for (const panelIndex of panelsToClean) {
  276. const checkNFolder = path.join(outputDir, `check${panelIndex}`);
  277. if (fs.existsSync(checkNFolder)) {
  278. try {
  279. const files = fs.readdirSync(checkNFolder);
  280. for (const file of files) {
  281. // 删除所有checkN*.txt文件(包括checkN.txt和checkN_M.txt)
  282. if (file.match(/^check\d+(_\d+)?\.txt$/)) {
  283. const filePath = path.join(checkNFolder, file);
  284. fs.unlinkSync(filePath);
  285. }
  286. }
  287. } catch (error) {
  288. console.log(` ⚠️ 清理check${panelIndex}文件夹的旧txt文件失败: ${error.message}`);
  289. }
  290. }
  291. }
  292. // 按panel_index和block_index分组
  293. const dialoguesByPanelAndBlock = {};
  294. for (const dialogue of dialogues) {
  295. const panelIndex = dialogue.panel_index || 1;
  296. const blockIndex = dialogue.block_index || 1;
  297. const key = `${panelIndex}_${blockIndex}`;
  298. if (!dialoguesByPanelAndBlock[key]) {
  299. dialoguesByPanelAndBlock[key] = {
  300. panelIndex,
  301. blockIndex,
  302. dialogues: []
  303. };
  304. }
  305. dialoguesByPanelAndBlock[key].dialogues.push(dialogue);
  306. }
  307. // 为每个panel和block组合生成txt文件
  308. const generatedFiles = [];
  309. const sortedKeys = Object.keys(dialoguesByPanelAndBlock).sort((a, b) => {
  310. const [panelA, blockA] = a.split('_').map(Number);
  311. const [panelB, blockB] = b.split('_').map(Number);
  312. if (panelA !== panelB) {
  313. return panelA - panelB;
  314. }
  315. return blockA - blockB;
  316. });
  317. for (const key of sortedKeys) {
  318. const { panelIndex, blockIndex, dialogues: blockDialogues } = dialoguesByPanelAndBlock[key];
  319. // 对同一个block内的对话进行排序:先按y坐标从上到下,再按x坐标从右到左
  320. const sortedDialogues = [...blockDialogues].sort((a, b) => {
  321. const y1A = a.bbox?.y1 || 0;
  322. const y1B = b.bbox?.y1 || 0;
  323. if (Math.abs(y1A - y1B) > 5) {
  324. return y1A - y1B; // 从上到下
  325. }
  326. const x1A = a.bbox?.x1 || 0;
  327. const x1B = b.bbox?.x1 || 0;
  328. return x1B - x1A; // 从右到左
  329. });
  330. // 合并所有对话为一个txt文件
  331. const txtContent = sortedDialogues.map(d => d.text).join('');
  332. // 将txt文件保存到对应的checkN文件夹下
  333. const checkNFolder = path.join(outputDir, `check${panelIndex}`);
  334. if (!fs.existsSync(checkNFolder)) {
  335. fs.mkdirSync(checkNFolder, { recursive: true });
  336. }
  337. // 文件名格式:checkN_M.txt,其中N是panel_index,M是block_index
  338. const txtFileName = `check${panelIndex}_${blockIndex}.txt`;
  339. const txtPath = path.join(checkNFolder, txtFileName);
  340. fs.writeFileSync(txtPath, txtContent, 'utf-8');
  341. generatedFiles.push(path.join(`check${panelIndex}`, txtFileName));
  342. console.log(` 📄 已生成txt文件: check${panelIndex}/${txtFileName} (${sortedDialogues.length}段对话)`);
  343. }
  344. // 返回所有对话(已排序)
  345. return dialogues;
  346. } else if (hasPanelIndex) {
  347. // 只有panel_index,没有block_index,使用原来的逻辑(按y坐标分组)
  348. // 按panel_index分组
  349. const dialoguesByPanel = {};
  350. for (const dialogue of dialogues) {
  351. const panelIndex = dialogue.panel_index || 1;
  352. if (!dialoguesByPanel[panelIndex]) {
  353. dialoguesByPanel[panelIndex] = [];
  354. }
  355. dialoguesByPanel[panelIndex].push(dialogue);
  356. }
  357. // 为每个panel生成txt文件
  358. const generatedFiles = [];
  359. for (const panelIndex of Object.keys(dialoguesByPanel).sort((a, b) => parseInt(a) - parseInt(b))) {
  360. const panelDialogues = dialoguesByPanel[panelIndex];
  361. // 根据 bbox.x1 排序(x1 越大越靠前,从右到左的阅读顺序)
  362. const sortedDialogues = [...panelDialogues].sort((a, b) => {
  363. const x1A = a.bbox?.x1 || 0;
  364. const x1B = b.bbox?.x1 || 0;
  365. return x1B - x1A; // 从右到左:x1 越大越靠前
  366. });
  367. // 按 y1 分组(y1 相同或相近的归为一组)
  368. const groups = [];
  369. let currentGroup = [];
  370. let currentY1 = null;
  371. for (const dialogue of sortedDialogues) {
  372. const y1 = dialogue.bbox?.y1 || 0;
  373. if (currentY1 === null || Math.abs(y1 - currentY1) <= yThreshold) {
  374. currentGroup.push(dialogue);
  375. if (currentY1 === null) {
  376. currentY1 = y1;
  377. }
  378. } else {
  379. if (currentGroup.length > 0) {
  380. groups.push(currentGroup);
  381. }
  382. currentGroup = [dialogue];
  383. currentY1 = y1;
  384. }
  385. }
  386. if (currentGroup.length > 0) {
  387. groups.push(currentGroup);
  388. }
  389. // 为每组生成一个 txt 文件
  390. // 将txt文件保存到对应的checkN文件夹下
  391. const checkNFolder = path.join(outputDir, `check${panelIndex}`);
  392. if (!fs.existsSync(checkNFolder)) {
  393. fs.mkdirSync(checkNFolder, { recursive: true });
  394. }
  395. for (let i = 0; i < groups.length; i++) {
  396. const group = groups[i];
  397. const txtContent = group.map(d => d.text).join('');
  398. // 如果只有一组,使用checkN.txt;否则使用checkN_M.txt
  399. const txtFileName = groups.length === 1
  400. ? `check${panelIndex}.txt`
  401. : `check${panelIndex}_${i + 1}.txt`;
  402. const txtPath = path.join(checkNFolder, txtFileName);
  403. fs.writeFileSync(txtPath, txtContent, 'utf-8');
  404. generatedFiles.push(path.join(`check${panelIndex}`, txtFileName));
  405. console.log(` 📄 已生成txt文件: check${panelIndex}/${txtFileName} (${group.length}段对话, y1≈${group[0].bbox?.y1 || 0})`);
  406. }
  407. }
  408. // 返回所有对话(已排序)
  409. return dialogues;
  410. } else {
  411. // 没有panel_index,使用原来的逻辑
  412. // 根据 bbox.x1 排序(x1 越大越靠前,从右到左的阅读顺序)
  413. const sortedDialogues = [...dialogues].sort((a, b) => {
  414. const x1A = a.bbox?.x1 || 0;
  415. const x1B = b.bbox?.x1 || 0;
  416. return x1B - x1A; // 从右到左:x1 越大越靠前
  417. });
  418. // 按 y1 分组(y1 相同或相近的归为一组)
  419. const groups = [];
  420. let currentGroup = [];
  421. let currentY1 = null;
  422. for (const dialogue of sortedDialogues) {
  423. const y1 = dialogue.bbox?.y1 || 0;
  424. if (currentY1 === null || Math.abs(y1 - currentY1) <= yThreshold) {
  425. currentGroup.push(dialogue);
  426. if (currentY1 === null) {
  427. currentY1 = y1;
  428. }
  429. } else {
  430. if (currentGroup.length > 0) {
  431. groups.push(currentGroup);
  432. }
  433. currentGroup = [dialogue];
  434. currentY1 = y1;
  435. }
  436. }
  437. if (currentGroup.length > 0) {
  438. groups.push(currentGroup);
  439. }
  440. // 为每组生成一个 txt 文件
  441. const generatedFiles = [];
  442. for (let i = 0; i < groups.length; i++) {
  443. const group = groups[i];
  444. const txtContent = group.map(d => d.text).join('');
  445. // 如果只有一组,使用原文件名;否则使用带序号的文件名
  446. const txtFileName = groups.length === 1
  447. ? `${imageName}.txt`
  448. : `${imageName}_${i + 1}.txt`;
  449. const txtPath = path.join(outputDir, txtFileName);
  450. fs.writeFileSync(txtPath, txtContent, 'utf-8');
  451. generatedFiles.push(txtFileName);
  452. console.log(` 📄 已生成txt文件: ${txtFileName} (${group.length}段对话, y1≈${group[0].bbox?.y1 || 0})`);
  453. }
  454. return sortedDialogues;
  455. }
  456. }
  457. /**
  458. * 判断文字块属于哪个格子(辅助函数,被sortDialoguesByPanels调用)
  459. * @param {Object} textBlock - 文字块,包含bbox信息
  460. * @param {Array} panels - 格子列表
  461. * @returns {number} 格子索引,如果不在任何格子内返回-1
  462. */
  463. function getTextBlockPanel(textBlock, panels) {
  464. const bbox = textBlock.bbox;
  465. const centerX = (bbox.x1 + bbox.x2) / 2;
  466. const centerY = (bbox.y1 + bbox.y2) / 2;
  467. for (let i = 0; i < panels.length; i++) {
  468. const panel = panels[i];
  469. if (
  470. panel.x <= centerX && centerX <= panel.x + panel.width &&
  471. panel.y <= centerY && centerY <= panel.y + panel.height
  472. ) {
  473. return i;
  474. }
  475. }
  476. return -1;
  477. }
  478. // 导出函数
  479. export { startSortDialog, sortDialoguesByPanels };