feat: initial commit - massive-cloud point cloud processing system

This commit is contained in:
2026-05-27 09:51:42 +00:00
commit 16811100d1
391 changed files with 78338 additions and 0 deletions

143
server/stepParse.ts Normal file
View 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;
}