feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
143
server/stepParse.ts
Normal file
143
server/stepParse.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { spawnSync, type SpawnSyncOptions } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const DATA_DIR = process.env.DATA_DIR || '/data';
|
||||
|
||||
export function stepParseCacheDir(modelId: number, cadFileId: number): string {
|
||||
return path.join(DATA_DIR, 'models', String(modelId), 'cad', '.step_parse', String(cadFileId));
|
||||
}
|
||||
|
||||
function scriptPath(): string {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
return path.join(here, '..', 'scripts', 'step_parse_mvp.py');
|
||||
}
|
||||
|
||||
/**
|
||||
* 不依赖 existsSync:挂载/命名空间下路径探测可能不准;子进程 PATH 过短会导致 `python3` ENOENT。
|
||||
* 顺序尝试可执行文件,仅对 ENOENT 换下一个。
|
||||
*/
|
||||
function python3Candidates(): string[] {
|
||||
const fromEnv = process.env.PYTHON3_PATH?.trim();
|
||||
const list: string[] = [];
|
||||
if (fromEnv) list.push(fromEnv);
|
||||
if (process.platform !== 'win32') {
|
||||
list.push('/usr/bin/python3', '/usr/local/bin/python3');
|
||||
}
|
||||
list.push('python3');
|
||||
return Array.from(new Set(list));
|
||||
}
|
||||
|
||||
function spawnStepParseSync(script: string, stepAbsolutePath: string, outDir: string) {
|
||||
const envBase: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PYTHONUNBUFFERED: '1',
|
||||
// 保证按名查找 python3 时能找到 Debian/Ubuntu 常见路径
|
||||
PATH: `/usr/bin:/usr/local/bin:/bin:/usr/sbin:${process.env.PATH ?? ''}`,
|
||||
};
|
||||
const opts: SpawnSyncOptions = {
|
||||
encoding: 'utf-8',
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
timeout: 60 * 60 * 1000,
|
||||
env: envBase,
|
||||
};
|
||||
|
||||
const args = [script, stepAbsolutePath, outDir];
|
||||
let lastErr: NodeJS.ErrnoException | undefined;
|
||||
|
||||
for (const py of python3Candidates()) {
|
||||
const r = spawnSync(py, args, opts);
|
||||
if (!r.error) {
|
||||
return r;
|
||||
}
|
||||
lastErr = r.error as NodeJS.ErrnoException;
|
||||
if (lastErr.code !== 'ENOENT') {
|
||||
throw new Error(
|
||||
`无法启动 Python(${py}):${lastErr.message}。PYTHON3_PATH=${process.env.PYTHON3_PATH ?? '(未设置)'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`无法启动 Python(已尝试: ${python3Candidates().join(' → ')}):${lastErr?.message ?? 'ENOENT'}。` +
|
||||
`请确认镜像已安装 python3(docker/Dockerfile.dev),或设置 PYTHON3_PATH 为实际解释器路径。`,
|
||||
);
|
||||
}
|
||||
|
||||
export type StepManifestPart = { id: string; name: string; stlRelative: string };
|
||||
|
||||
export type StepManifest = {
|
||||
ok: boolean;
|
||||
parts: StepManifestPart[];
|
||||
sourceStep?: string;
|
||||
};
|
||||
|
||||
/** cadquery/OCP 可能在 stdout 打印非 JSON 日志,不能只取最后一行 */
|
||||
function parseJsonResultFromProcessOutput(stdout: string, stderr: string): {
|
||||
ok?: boolean;
|
||||
error?: string;
|
||||
parts?: StepManifestPart[];
|
||||
} | null {
|
||||
const text = `${stdout || ''}\n${stderr || ''}`;
|
||||
let last: { ok?: boolean; error?: string; parts?: StepManifestPart[] } | null = null;
|
||||
for (const line of text.split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t.startsWith('{') || !t.endsWith('}')) continue;
|
||||
try {
|
||||
const o = JSON.parse(t) as { ok?: boolean };
|
||||
if (typeof o.ok === 'boolean') {
|
||||
last = o as { ok?: boolean; error?: string; parts?: StepManifestPart[] };
|
||||
}
|
||||
} catch {
|
||||
/* 单行非完整 JSON */
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 Python cadquery 将 STEP 三角化为每零件 STL,并写入 manifest.json。
|
||||
* @returns manifest 或抛出带 message 的错误
|
||||
*/
|
||||
export function runStepParseMvp(stepAbsolutePath: string, outDir: string): StepManifest {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const script = scriptPath();
|
||||
if (!fs.existsSync(script)) {
|
||||
throw new Error(`未找到 STEP 解析脚本: ${script}`);
|
||||
}
|
||||
const r = spawnStepParseSync(script, stepAbsolutePath, outDir);
|
||||
|
||||
const out = (v: string | Buffer | null | undefined) =>
|
||||
v == null ? '' : typeof v === 'string' ? v : v.toString('utf-8');
|
||||
const combined = `${out(r.stdout)}\n${out(r.stderr)}`;
|
||||
let parsed = parseJsonResultFromProcessOutput(out(r.stdout), out(r.stderr));
|
||||
|
||||
const resultFile = path.join(outDir, '_parse_result.json');
|
||||
if ((!parsed || typeof parsed.ok !== 'boolean') && fs.existsSync(resultFile)) {
|
||||
try {
|
||||
parsed = JSON.parse(fs.readFileSync(resultFile, 'utf-8')) as typeof parsed;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed.ok !== 'boolean') {
|
||||
const sig = r.signal != null ? ` signal=${r.signal}` : '';
|
||||
throw new Error(
|
||||
`STEP 解析无有效 JSON 结果(exit=${r.status ?? 'null'}${sig})。` +
|
||||
`请确认 cadquery 可用。输出摘录:\n${combined.trim().slice(-2500)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!parsed.ok) {
|
||||
throw new Error(parsed.error || 'STEP 解析失败');
|
||||
}
|
||||
|
||||
const manFile = path.join(outDir, 'manifest.json');
|
||||
if (!fs.existsSync(manFile)) {
|
||||
throw new Error(`未生成 manifest.json: ${manFile}`);
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(manFile, 'utf-8')) as StepManifest;
|
||||
return raw;
|
||||
}
|
||||
Reference in New Issue
Block a user