diff --git a/app/api/convert/route.ts b/app/api/convert/route.ts index 1b9eeec..9859fe5 100644 --- a/app/api/convert/route.ts +++ b/app/api/convert/route.ts @@ -13,32 +13,31 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "No file provided" }, { status: 400 }); } - // 1. Setup workspace const tempDir = path.join(os.tmpdir(), "xgrids-pipeline"); await mkdir(tempDir, { recursive: true }); - // FIX: Sanitize filename to avoid shell/path issues with spaces - const safeName = file.name.replace(/[^a-z0-8.]/gi, "_").toLowerCase(); + const safeName = file.name.replace(/[^a-z0-9.]/gi, "_").toLowerCase(); const timestamp = Date.now(); const inputPath = path.join(tempDir, `${timestamp}_${safeName}`); + const outputPath = inputPath.replace(/\.(lcc|lci|bin)$/i, ".ply"); - // Ensure we replace the extension correctly for the output - const outputPath = inputPath.replace(/\.(lcc|lci)$/i, ".ply"); + // DETERMINE WHICH SCRIPT TO RUN + let scriptName = "convert_lci_to_ply.py"; + if (file.name.toLowerCase().includes("environment.bin")) { + scriptName = "convert_env_to_ply.py"; + } const scriptPath = path.join( process.cwd(), "scripts", "preprocess", - "convert_lci_to_ply.py", + scriptName, ); - // 2. Write the file const buffer = Buffer.from(await file.arrayBuffer()); await writeFile(inputPath, buffer); - // 3. Execute Python return new Promise((resolve) => { - // spawn handles arguments as an array, which is safer than exec for spaces const pythonProcess = spawn("python3", [ scriptPath, inputPath, @@ -52,14 +51,10 @@ export async function POST(req: NextRequest) { pythonProcess.on("close", async (code) => { if (code !== 0) { - console.error("Python Error:", errorOutput); - // Cleanup input even on failure await unlink(inputPath).catch(() => {}); return resolve( NextResponse.json( - { - error: `Python script failed with code ${code}. ${errorOutput}`, - }, + { error: `Python failed (${scriptName}): ${errorOutput}` }, { status: 500 }, ), ); @@ -67,8 +62,6 @@ export async function POST(req: NextRequest) { try { const plyBuffer = await readFile(outputPath); - - // Cleanup await Promise.all([ unlink(inputPath).catch(() => {}), unlink(outputPath).catch(() => {}), @@ -86,7 +79,7 @@ export async function POST(req: NextRequest) { } catch (e) { resolve( NextResponse.json( - { error: "Failed to read generated PLY file" }, + { error: "Failed to read output PLY" }, { status: 500 }, ), ); @@ -94,7 +87,6 @@ export async function POST(req: NextRequest) { }); }); } catch (error: any) { - console.error("API Route Error:", error); return NextResponse.json({ error: error.message }, { status: 500 }); } } diff --git a/scripts/preprocess/convert_env_to_ply.py b/scripts/preprocess/convert_env_to_ply.py new file mode 100644 index 0000000..0e9554e --- /dev/null +++ b/scripts/preprocess/convert_env_to_ply.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +import argparse +import struct +import sys +from pathlib import Path + +def convert_env_to_ply(input_path, output_path, verbose=False): + """ + Parses Xgrids environment.bin using standard library only. + Format: 44 bytes per splat (11 little-endian floats). + """ + input_path = Path(input_path) + output_path = Path(output_path) + + if not input_path.exists(): + print(f"✗ Error: File '{input_path}' not found.") + sys.exit(1) + + # 44 bytes per point (Position x,y,z | Scale x,y,z | Rotation q1,q2,q3,q4 | Opacity) + POINT_SIZE = 44 + + try: + file_size = input_path.stat().st_size + num_points = file_size // POINT_SIZE + + if verbose: + print("-" * 50) + print(f"Input: {input_path}") + print(f"Output: {output_path}") + print(f"Size: {file_size / (1024*1024):.2f} MB") + print(f"Points: {num_points:,}") + print("-" * 50) + + with open(input_path, "rb") as f_in, open(output_path, "w") as f_out: + # 1. Write PLY Header + f_out.write("ply\n") + f_out.write("format ascii 1.0\n") + f_out.write(f"element vertex {num_points}\n") + f_out.write("property float x\n") + f_out.write("property float y\n") + f_out.write("property float z\n") + f_out.write("end_header\n") + + # 2. Process Binary in Chunks (to keep RAM usage low) + # 10,000 points per chunk is a good balance for standard Python + chunk_size = 10000 + points_processed = 0 + + while points_processed < num_points: + remaining = num_points - points_processed + batch_size = min(chunk_size, remaining) + + # Read binary chunk + chunk_data = f_in.read(batch_size * POINT_SIZE) + if not chunk_data: + break + + # Unpack and write + # '<3f' grabs just the first 3 floats (XYZ) and ignores the rest of the 44 bytes + for i in range(batch_size): + offset = i * POINT_SIZE + # We only unpack the first 12 bytes (3 floats) of the 44-byte block + x, y, z = struct.unpack_from("