refactor: print3d_model 슬라이서를 CuraEngine에서 PrusaSlicer로 교체
CuraEngine CLI는 구조적으로 예상 출력시간 헤더가 항상 더미값(6666)만 남는
한계가 있어(33b5a25 참고) PrusaSlicer(apt, gcode-flavor=klipper 네이티브
지원)로 교체. DISPLAY 없이도 --export-gcode가 완전 헤드리스로 동작함을
확인했고, 슬라이싱 후 파일에 실제 "estimated printing time"이 직접
기록되므로 stderr 파싱 없이 파일에서 바로 읽으면 됨. K2 프로파일/시작-매크로
값은 그대로 유지. 더 이상 쓰지 않는 resources/cura/*.def.json 제거.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,257 +0,0 @@
|
||||
{
|
||||
"name": "Extruder",
|
||||
"version": 2,
|
||||
"metadata":
|
||||
{
|
||||
"type": "extruder",
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Unknown",
|
||||
"setting_version": 20,
|
||||
"visible": false,
|
||||
"position": "0"
|
||||
},
|
||||
"settings":
|
||||
{
|
||||
"machine_settings":
|
||||
{
|
||||
"label": "Machine",
|
||||
"type": "category",
|
||||
"description": "Machine specific settings",
|
||||
"children":
|
||||
{
|
||||
"extruder_nr":
|
||||
{
|
||||
"label": "Extruder",
|
||||
"description": "The extruder train used for printing. This is used in multi-extrusion.",
|
||||
"type": "extruder",
|
||||
"default_value": "0",
|
||||
"settable_per_mesh": true,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_nozzle_id":
|
||||
{
|
||||
"label": "Nozzle ID",
|
||||
"description": "The nozzle ID for an extruder train, such as \"AA 0.4\" and \"BB 0.8\".",
|
||||
"type": "str",
|
||||
"default_value": "unknown",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
},
|
||||
"machine_nozzle_size":
|
||||
{
|
||||
"label": "Nozzle Diameter",
|
||||
"description": "The inner diameter of the nozzle. Change this setting when using a non-standard nozzle size.",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 0.4,
|
||||
"minimum_value": "0.001",
|
||||
"maximum_value_warning": "10",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
},
|
||||
"machine_nozzle_offset_x":
|
||||
{
|
||||
"label": "Nozzle X Offset",
|
||||
"description": "The x-coordinate of the offset of the nozzle.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_nozzle_offset_y":
|
||||
{
|
||||
"label": "Nozzle Y Offset",
|
||||
"description": "The y-coordinate of the offset of the nozzle.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_start_code":
|
||||
{
|
||||
"label": "Extruder Start G-Code",
|
||||
"description": "Start g-code to execute when switching to this extruder.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_start_pos_abs":
|
||||
{
|
||||
"label": "Extruder Start Position Absolute",
|
||||
"description": "Make the extruder starting position absolute rather than relative to the last-known location of the head.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_start_pos_x":
|
||||
{
|
||||
"label": "Extruder Start Position X",
|
||||
"description": "The x-coordinate of the starting position when turning the extruder on.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_start_pos_y":
|
||||
{
|
||||
"label": "Extruder Start Position Y",
|
||||
"description": "The y-coordinate of the starting position when turning the extruder on.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_end_code":
|
||||
{
|
||||
"label": "Extruder End G-Code",
|
||||
"description": "End g-code to execute when switching away from this extruder.",
|
||||
"type": "str",
|
||||
"default_value": "",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_end_pos_abs":
|
||||
{
|
||||
"label": "Extruder End Position Absolute",
|
||||
"description": "Make the extruder ending position absolute rather than relative to the last-known location of the head.",
|
||||
"type": "bool",
|
||||
"default_value": false,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_end_pos_x":
|
||||
{
|
||||
"label": "Extruder End Position X",
|
||||
"description": "The x-coordinate of the ending position when turning the extruder off.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"machine_extruder_end_pos_y":
|
||||
{
|
||||
"label": "Extruder End Position Y",
|
||||
"description": "The y-coordinate of the ending position when turning the extruder off.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
},
|
||||
"extruder_prime_pos_z":
|
||||
{
|
||||
"label": "Extruder Prime Z Position",
|
||||
"description": "The Z coordinate of the position where the nozzle primes at the start of printing.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"minimum_value_warning": "0",
|
||||
"maximum_value": "machine_height",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
},
|
||||
"machine_extruder_cooling_fan_number":
|
||||
{
|
||||
"label": "Extruder Print Cooling Fan",
|
||||
"description": "The number of the print cooling fan associated with this extruder. Only change this from the default value of 0 when you have a different print cooling fan for each extruder.",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": "0",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"settable_per_meshgroup": false,
|
||||
"settable_globally": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"platform_adhesion":
|
||||
{
|
||||
"label": "Build Plate Adhesion",
|
||||
"type": "category",
|
||||
"icon": "Adhesion",
|
||||
"description": "Adhesion",
|
||||
"children":
|
||||
{
|
||||
"extruder_prime_pos_x":
|
||||
{
|
||||
"label": "Extruder Prime X Position",
|
||||
"description": "The X coordinate of the position where the nozzle primes at the start of printing.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"minimum_value_warning": "machine_nozzle_offset_x",
|
||||
"maximum_value": "machine_width",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"enabled": false
|
||||
},
|
||||
"extruder_prime_pos_y":
|
||||
{
|
||||
"label": "Extruder Prime Y Position",
|
||||
"description": "The Y coordinate of the position where the nozzle primes at the start of printing.",
|
||||
"type": "float",
|
||||
"unit": "mm",
|
||||
"default_value": 0,
|
||||
"minimum_value_warning": "machine_nozzle_offset_y",
|
||||
"maximum_value_warning": "machine_depth",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true,
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"material":
|
||||
{
|
||||
"label": "Material",
|
||||
"icon": "Spool",
|
||||
"description": "Material",
|
||||
"type": "category",
|
||||
"children":
|
||||
{
|
||||
"material_diameter":
|
||||
{
|
||||
"label": "Diameter",
|
||||
"description": "Adjusts the diameter of the filament used. Match this value with the diameter of the used filament.",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 2.85,
|
||||
"minimum_value": "0.0001",
|
||||
"minimum_value_warning": "0.4",
|
||||
"maximum_value_warning": "3.5",
|
||||
"enabled": "machine_gcode_flavor != \"UltiGCode\"",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+57
-62
@@ -5,10 +5,18 @@ import { ToolResult } from '../types.js';
|
||||
import { getWorkspacePath } from '../config/paths.js';
|
||||
|
||||
// K2 콤보(Creality, Klipper+Moonraker)로 STL을 찾아서 바로 슬라이싱→업로드까지 자동화.
|
||||
// 2026-09-20: STL URL을 받아 CuraEngine(apt cura-engine, 클로서버에 설치)으로 슬라이싱하고
|
||||
// K2의 Moonraker(192.168.0.114:7125)에 업로드한다. 실제 출력 시작은 반드시 confirm:true
|
||||
// 재호출을 거쳐야 함(schedule_job의 확인 패턴과 동일) — 재료/시간 낭비 위험이 있는 물리적
|
||||
// 동작이라 자동시작 금지.
|
||||
// 2026-09-20: STL URL을 받아 슬라이싱하고 K2의 Moonraker(192.168.0.114:7125)에 업로드한다.
|
||||
// 실제 출력 시작은 반드시 confirm:true 재호출을 거쳐야 함(schedule_job의 확인 패턴과 동일)
|
||||
// — 재료/시간 낭비 위험이 있는 물리적 동작이라 자동시작 금지.
|
||||
//
|
||||
// 슬라이서 = PrusaSlicer(apt prusa-slicer, 클로서버 설치, "with GUI support" 빌드지만
|
||||
// DISPLAY 없이도 --export-gcode CLI는 완전히 헤드리스로 동작 확인됨).
|
||||
// 처음엔 CuraEngine(apt cura-engine)을 썼으나 예상 출력시간이 항상 6666초로 나오는 버그가
|
||||
// 있었음 — CuraEngine 소스 확인 결과 CLI 백엔드(CommandLine::isSequential()==true)가
|
||||
// 슬라이싱 "시작 전"에 헤더를 미리 써버리고 끝난 뒤 실제값으로 되돌아가 덮어쓰는 코드가
|
||||
// 없어서 항상 하드코딩된 더미값만 남는 구조적 한계였음(확률적 버그 아님). PrusaSlicer는
|
||||
// 슬라이싱이 끝난 뒤 파일 끝부분에 실제 "estimated printing time"을 직접 써주므로 이 문제가
|
||||
// 없다.
|
||||
//
|
||||
// 프린터 프로파일 값은 추측이 아니라 K2에 이미 올라가 있던 Creality Print 슬라이싱 샘플
|
||||
// (server/files/gcodes/3DBench_PLA_22m.gcode)의 실제 헤더/설정 주석에서 그대로 뽑아온 것:
|
||||
@@ -20,12 +28,7 @@ import { getWorkspacePath } from '../config/paths.js';
|
||||
|
||||
const K2_HOST = '192.168.0.114';
|
||||
const K2_MOONRAKER_PORT = 7125;
|
||||
const CURA_ENGINE_BIN = '/usr/bin/CuraEngine';
|
||||
|
||||
// dist/tools/print3d.js -> ../../resources/cura (repo root의 resources/, web-ui와 같은 패턴)
|
||||
const CURA_RESOURCES_DIR = path.join(__dirname, '..', '..', 'resources', 'cura');
|
||||
const FDMPRINTER_DEF = path.join(CURA_RESOURCES_DIR, 'fdmprinter.def.json');
|
||||
const FDMEXTRUDER_DEF = path.join(CURA_RESOURCES_DIR, 'fdmextruder.def.json');
|
||||
const PRUSA_SLICER_BIN = '/usr/bin/prusa-slicer';
|
||||
|
||||
interface MaterialPreset { nozzleTemp: number; bedTemp: number; verified: boolean; }
|
||||
// PLA는 K2 실제 슬라이싱 샘플에서 그대로 확인된 값(검증됨). PETG는 검증 안 된 일반적인
|
||||
@@ -36,8 +39,8 @@ const MATERIAL_PRESETS: Record<string, MaterialPreset> = {
|
||||
};
|
||||
|
||||
function buildStartGcode(nozzleTemp: number, bedTemp: number): string {
|
||||
// K2 실제 샘플 machine_start_gcode 그대로(OrcaSlicer식 대괄호 플레이스홀더만 실제 값으로
|
||||
// 치환 — CuraEngine은 그 문법을 모르므로 직접 값을 박아넣어야 함).
|
||||
// K2 실제 샘플 machine_start_gcode 그대로(OrcaSlicer/PrusaSlicer식 대괄호 플레이스홀더
|
||||
// 자리에 실제 값을 직접 박아넣음).
|
||||
return [
|
||||
'M140 S0',
|
||||
'M104 S0',
|
||||
@@ -66,6 +69,15 @@ function fmtDuration(sec: number): string {
|
||||
return `${m}분`;
|
||||
}
|
||||
|
||||
// PrusaSlicer가 파일 끝에 남기는 "estimated printing time (normal mode) = 1h 5m 30s" 형식을 초로 변환.
|
||||
function parsePrusaTime(text: string): number | null {
|
||||
const m = text.match(/estimated printing time \(normal mode\) = (?:(\d+)h\s*)?(?:(\d+)m\s*)?(?:(\d+)s)?/);
|
||||
if (!m) return null;
|
||||
const h = Number(m[1] || 0), min = Number(m[2] || 0), s = Number(m[3] || 0);
|
||||
if (!h && !min && !s) return null;
|
||||
return h * 3600 + min * 60 + s;
|
||||
}
|
||||
|
||||
async function downloadStl(url: string, outPath: string): Promise<{ ok: true } | { ok: false; detail: string }> {
|
||||
let u: URL;
|
||||
try { u = new URL(url); } catch { return { ok: false, detail: 'URL 형식이 잘못됐습니다.' }; }
|
||||
@@ -89,21 +101,20 @@ async function downloadStl(url: string, outPath: string): Promise<{ ok: true } |
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function runCuraEngine(args: string[], timeoutMs: number): Promise<{ ok: true; stdout: string; stderr: string } | { ok: false; detail: string }> {
|
||||
function runPrusaSlicer(args: string[], timeoutMs: number): Promise<{ ok: true } | { ok: false; detail: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(CURA_ENGINE_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
// DISPLAY가 실려있으면 GUI 초기화를 시도할 수 있어 명시적으로 제거하고 완전 헤드리스로 강제.
|
||||
const env = { ...process.env };
|
||||
delete env.DISPLAY;
|
||||
const proc = spawn(PRUSA_SLICER_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'], env });
|
||||
let stderr = '';
|
||||
proc.stdout?.on('data', (d: Buffer) => { stdout += d.toString('utf8'); });
|
||||
// 실패시 에러메시지용으로 뒤쪽만 있으면 되지만, 성공시엔 "Print time (s): N" 요약 줄을
|
||||
// 파싱해야 하므로(아래 CLI 헤더 버그 설명 참고) 여기서는 자르지 않고 전체 보존한다.
|
||||
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString('utf8'); });
|
||||
proc.stderr?.on('data', (d: Buffer) => { stderr += d.toString('utf8'); if (stderr.length > 4000) stderr = stderr.slice(-4000); });
|
||||
const killTimer = setTimeout(() => { try { proc.kill('SIGKILL'); } catch { /* noop */ } }, timeoutMs);
|
||||
proc.on('error', (e) => { clearTimeout(killTimer); resolve({ ok: false, detail: String(e.message || e) }); });
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(killTimer);
|
||||
if (code === 0) resolve({ ok: true, stdout, stderr });
|
||||
else resolve({ ok: false, detail: stderr.split('\n').filter(Boolean).slice(-10).join(' ') || `CuraEngine exit ${code}` });
|
||||
if (code === 0) resolve({ ok: true });
|
||||
else resolve({ ok: false, detail: stderr.split('\n').filter(Boolean).slice(-10).join(' ') || `prusa-slicer exit ${code}` });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -178,11 +189,8 @@ export const print3dModelTool = {
|
||||
const stlUrl = String(args?.stl_url || '').trim();
|
||||
if (!stlUrl) return { success: false, error: 'stl_url이 필요합니다.' };
|
||||
|
||||
if (!fs.existsSync(FDMPRINTER_DEF) || !fs.existsSync(FDMEXTRUDER_DEF)) {
|
||||
return { success: false, error: `CuraEngine 설정 파일을 찾을 수 없습니다: ${CURA_RESOURCES_DIR}` };
|
||||
}
|
||||
if (!fs.existsSync(CURA_ENGINE_BIN)) {
|
||||
return { success: false, error: `CuraEngine이 설치돼있지 않습니다(${CURA_ENGINE_BIN}). "sudo apt-get install -y cura-engine" 필요.` };
|
||||
if (!fs.existsSync(PRUSA_SLICER_BIN)) {
|
||||
return { success: false, error: `PrusaSlicer가 설치돼있지 않습니다(${PRUSA_SLICER_BIN}). "sudo apt-get install -y prusa-slicer" 필요.` };
|
||||
}
|
||||
|
||||
const materialKey = String(args?.material || 'PLA').toUpperCase();
|
||||
@@ -206,51 +214,38 @@ export const print3dModelTool = {
|
||||
const gcodePath = path.join(outDir, gcodeFilename);
|
||||
const startGcode = buildStartGcode(material.nozzleTemp, material.bedTemp);
|
||||
|
||||
const curaArgs = [
|
||||
'slice', '-v',
|
||||
'-j', FDMPRINTER_DEF,
|
||||
const psArgs = [
|
||||
'--export-gcode',
|
||||
'--gcode-flavor', 'klipper',
|
||||
// K2 콤보 실측값(3DBench_PLA_22m.gcode 헤더 주석에서 확인) — 빌드볼륨/속도/리트랙션.
|
||||
'-s', 'machine_width=260', '-s', 'machine_depth=260', '-s', 'machine_height=260',
|
||||
'-s', 'machine_center_is_zero=false',
|
||||
'-s', 'machine_heated_bed=true',
|
||||
'-s', 'machine_gcode_flavor=Marlin',
|
||||
'-s', 'machine_max_feedrate_x=800', '-s', 'machine_max_feedrate_y=800',
|
||||
'-s', 'machine_max_feedrate_z=5', '-s', 'machine_max_feedrate_e=50',
|
||||
'-s', `machine_start_gcode=${startGcode}`,
|
||||
'-s', `machine_end_gcode=${END_GCODE}`,
|
||||
'-s', `layer_height=${layerHeight}`, '-s', `layer_height_0=${layerHeight}`,
|
||||
'-s', `wall_line_count=${wallCount}`,
|
||||
'-s', `infill_sparse_density=${infillPercent}`,
|
||||
'-s', 'infill_pattern=grid',
|
||||
'-s', 'adhesion_type=skirt',
|
||||
'-s', `material_print_temperature=${material.nozzleTemp}`,
|
||||
'-s', `material_print_temperature_layer_0=${material.nozzleTemp}`,
|
||||
'-s', `material_bed_temperature=${material.bedTemp}`,
|
||||
'-s', `material_bed_temperature_layer_0=${material.bedTemp}`,
|
||||
'-e0', '-j', FDMEXTRUDER_DEF,
|
||||
'-s', 'machine_nozzle_size=0.4',
|
||||
'-s', 'material_diameter=1.75',
|
||||
'-s', 'retraction_amount=0.8',
|
||||
'-s', 'retraction_speed=40',
|
||||
'-l', stlPath,
|
||||
'--bed-shape', '0x0,260x0,260x260,0x260',
|
||||
'--max-print-height', '260',
|
||||
'--nozzle-diameter', '0.4',
|
||||
'--filament-diameter', '1.75',
|
||||
'--retract-length', '0.8', '--retract-speed', '40',
|
||||
'--layer-height', String(layerHeight), '--first-layer-height', String(layerHeight),
|
||||
'--perimeters', String(wallCount),
|
||||
'--fill-density', `${infillPercent}%`, '--fill-pattern', 'grid',
|
||||
'--skirts', '1',
|
||||
'--temperature', String(material.nozzleTemp), '--first-layer-temperature', String(material.nozzleTemp),
|
||||
'--bed-temperature', String(material.bedTemp), '--first-layer-bed-temperature', String(material.bedTemp),
|
||||
'--start-gcode', startGcode,
|
||||
'--end-gcode', END_GCODE,
|
||||
'-o', gcodePath,
|
||||
stlPath,
|
||||
];
|
||||
|
||||
const slice = await runCuraEngine(curaArgs, 5 * 60_000);
|
||||
const slice = await runPrusaSlicer(psArgs, 5 * 60_000);
|
||||
if (!slice.ok) return { success: false, error: `슬라이싱 실패: ${slice.detail}` };
|
||||
if (!fs.existsSync(gcodePath) || fs.statSync(gcodePath).size < 100) {
|
||||
return { success: false, error: '슬라이싱은 끝났는데 G-code 파일이 비정상입니다.' };
|
||||
}
|
||||
|
||||
// 주의: G-code 파일 자체의 ";TIME:" 헤더는 CLI 모드에서 절대 신뢰할 수 없다 — CuraEngine
|
||||
// CLI(CommandLine 백엔드)는 슬라이싱 "시작 전"에 헤더를 이미 써버리고(isSequential()이
|
||||
// 항상 true라 processStartingCode에서 print_time 없이 getFileHeader 호출) 끝난 뒤에도
|
||||
// 되돌아가 덮어쓰는 코드가 CuraEngine 소스 어디에도 없다 — 그래서 항상 코드에 하드코딩된
|
||||
// 더미값(gcodeExport.cpp의 `;TIME:6666`)이 그대로 남는다(2026-09-20 소스 확인, 5.0.0).
|
||||
// 실제 값은 슬라이싱이 끝난 뒤 stderr(verbose 로그, -v 필요)에 "Print time (s): N"으로만
|
||||
// 찍히므로 거기서 파싱해야 한다.
|
||||
const timeMatch = slice.stderr.match(/Print time \(s\):\s*(\d+)/);
|
||||
const printTimeSec = timeMatch ? Number(timeMatch[1]) : null;
|
||||
// PrusaSlicer는 슬라이싱이 끝난 뒤 파일 마지막 부분(전체 설정값 덤프보다 앞)에 실제
|
||||
// 예상 출력시간을 직접 써준다 — CuraEngine처럼 시작 전에 더미값을 박아넣고 끝나지 않는
|
||||
// 구조가 아니다. 뒤따르는 설정 덤프가 수백 줄로 커서 tail을 너무 짧게 자르면 이 줄을
|
||||
// 놓치므로 파일 전체에서 찾는다.
|
||||
const printTimeSec = parsePrusaTime(fs.readFileSync(gcodePath, 'utf8'));
|
||||
|
||||
const up = await uploadToMoonraker(gcodePath, gcodeFilename);
|
||||
if (!up.ok) return { success: false, error: `${up.detail} (슬라이싱은 성공, G-code는 ${gcodePath}에 있음)` };
|
||||
|
||||
Reference in New Issue
Block a user