import * as fs from 'fs'; import { fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import * as db from './db.js'; import { computeCadShellHeatmapFromObj, computeCadShellHeatmapFromStl, } from './cadHeatmapCompute.js'; import { pickCadFileForHeatmap } from './cadFilePick.js'; import { sampleTaskPointclouds } from './sampleTaskPointclouds.js'; import { registerLegacyAnalysisChild } from './analysisJobControl.js'; const DATA_DIR = process.env.DATA_DIR || '/data'; const TASKS_DIR = path.join(DATA_DIR, 'tasks'); async function reportSmoothHeatmapProgress( resultId: number, progress: number, stage: string, ): Promise { const p = Math.min(1, Math.max(0, progress)); await db.updateTaskAnalysisResult(resultId, { resultData: { kind: 'smooth_heatmap', progress: p, stage, }, }); } function resolveSmoothHeatmapRunnerEntry(): string { const dir = path.dirname(fileURLToPath(import.meta.url)); if (process.env.NODE_ENV === 'production') { return path.join(dir, 'smoothHeatmapJobRunner.js'); } return path.join(dir, 'smoothHeatmapJobRunner.ts'); } /** * 在子进程中执行平滑热力(STL/OBJ 解析与顶点着色为 CPU 密集同步计算,会长时间阻塞事件循环)。 * 主进程只负责 fork,HTTP 与页面刷新可继续响应。 */ export function enqueueSmoothHeatmapJob(resultId: number, taskId: number, totalPoints: number): void { const script = resolveSmoothHeatmapRunnerEntry(); const isProd = process.env.NODE_ENV === 'production'; const child = fork(script, [String(resultId), String(taskId), String(totalPoints)], { env: { ...process.env }, // fork 必须保留 IPC:自定义 stdio 时须显式加入 'ipc',否则 Node 会抛错 stdio: ['ignore', 'inherit', 'inherit', 'ipc'], execArgv: isProd ? [] : ['--import', 'tsx'], }); registerLegacyAnalysisChild(resultId, taskId, 'smooth_heatmap', child); child.on('error', (err) => { console.error('[SmoothHeatmapJob] fork 失败:', err); }); child.on('exit', (code, signal) => { if (code !== 0 && code != null) { console.error(`[SmoothHeatmapJob] 子进程退出 code=${code} signal=${signal ?? ''}`); } }); } /** 供子进程入口调用;也可在测试中直接 await。 */ export async function runSmoothHeatmapJob(resultId: number, taskId: number, totalPoints: number): Promise { try { await reportSmoothHeatmapProgress(resultId, 0.06, '加载任务与 CAD…'); const task = await db.getInspectionTaskById(taskId); if (!task) throw new Error('task not found'); const cadFiles = await db.listModelCadFiles(task.modelId); const picked = pickCadFileForHeatmap(cadFiles); if (!picked) { throw new Error( '当前车型下未找到 STL 或 OBJ 的 CAD。请在与该检测任务相同的车型下上传 CAD,或确认文件扩展名为 .stl / .obj', ); } if (!fs.existsSync(picked.file.filePath)) throw new Error('CAD 文件不存在'); await reportSmoothHeatmapProgress(resultId, 0.14, '点云采样中…'); const allPoints = await sampleTaskPointclouds(taskId, totalPoints); if (allPoints.length === 0) throw new Error('任务下无可用点云采样'); await reportSmoothHeatmapProgress(resultId, 0.36, '读取 CAD 文件…'); const cadBuf = fs.readFileSync(picked.file.filePath); await reportSmoothHeatmapProgress(resultId, 0.42, '解析网格并计算顶点热力(耗时较长)…'); const computed = picked.ext === 'stl' ? computeCadShellHeatmapFromStl(cadBuf, allPoints) : computeCadShellHeatmapFromObj(cadBuf, allPoints); if (!computed) throw new Error('热力计算失败'); await reportSmoothHeatmapProgress(resultId, 0.88, '写入热力缓存文件…'); const outDir = path.join(TASKS_DIR, String(taskId), 'analysis'); fs.mkdirSync(outDir, { recursive: true }); const binName = `heatmap_${resultId}.bin`; const binPath = path.join(outDir, binName); fs.writeFileSync( binPath, Buffer.from(computed.colors.buffer, computed.colors.byteOffset, computed.colors.byteLength), ); await db.updateTaskAnalysisResult(resultId, { status: 'completed', resultData: { kind: 'smooth_heatmap', vertexCount: computed.vertexCount, dispMin: computed.dispMin, dispMax: computed.dispMax, binName, }, }); console.log( `[SmoothHeatmapJob] ok resultId=${resultId} taskId=${taskId} verts=${computed.vertexCount}`, ); } catch (err) { console.error('[SmoothHeatmapJob]', err); await db.updateTaskAnalysisResult(resultId, { status: 'error', resultData: { kind: 'smooth_heatmap', error: err instanceof Error ? err.message : String(err), }, }); } }