const BlenderProcess = require("./BlenderProcess.js"); const path = require("path"); const fs = require("fs"); const STR_STATUS_IDLE = "idle"; const STR_STATUS_RUNNING = "running"; const STR_STATUS_PAUSED = "paused"; class QueueManager { constructor(obj_window) { this.obj_window = obj_window; this.list_queue = []; this.nb_current_index = 0; this.str_status = STR_STATUS_IDLE; this.obj_current_process = null; this.nb_last_render_ms = 0; this.str_last_image_path = null; } start(obj_config) { if (this.str_status === STR_STATUS_PAUSED) { this.str_status = STR_STATUS_RUNNING; this._send_log("Reprise du rendu..."); this._process_next(); return Promise.resolve({ is_success: true }); } this.list_queue = this._build_queue(obj_config); this.nb_current_index = 0; this.str_status = STR_STATUS_RUNNING; this.nb_last_render_ms = 0; this.str_last_image_path = null; this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite"; this._send_log("File de rendu construite : " + this.list_queue.length + " elements."); this._send_progress(); this._process_next(); return Promise.resolve({ is_success: true, nb_total: this.list_queue.length }); } pause() { if (this.str_status !== STR_STATUS_RUNNING) { return Promise.resolve({ is_success: false }); } this.str_status = STR_STATUS_PAUSED; this._send_log("Rendu en pause."); return Promise.resolve({ is_success: true }); } stop() { this.str_status = STR_STATUS_IDLE; if (this.obj_current_process) { this.obj_current_process.kill("SIGTERM"); this.obj_current_process = null; } this._send_log("Rendu arrete."); return Promise.resolve({ is_success: true }); } _build_queue(obj_config) { let list_queue = []; let str_blend_path = obj_config.str_blend_file; let str_mode = obj_config.str_render_mode; let str_base_output = obj_config.str_output_path; let str_output_mode = obj_config.str_output_mode || "subfolder"; let list_cameras = obj_config.list_cameras; let list_enabled = []; for (let obj_cam of list_cameras) { if (obj_cam.is_enabled) { list_enabled.push(obj_cam); } } if (str_mode === "camera_by_camera") { for (let obj_cam of list_enabled) { for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame++) { list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame)); } } } else { let nb_min_frame = Infinity; let nb_max_frame = -Infinity; for (let obj_cam of list_enabled) { if (obj_cam.nb_frame_start < nb_min_frame) { nb_min_frame = obj_cam.nb_frame_start; } if (obj_cam.nb_frame_end > nb_max_frame) { nb_max_frame = obj_cam.nb_frame_end; } } for (let nb_frame = nb_min_frame; nb_frame <= nb_max_frame; nb_frame++) { for (let obj_cam of list_enabled) { if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end) { list_queue.push(this._create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame)); } } } } return list_queue; } _create_queue_item(str_blend_path, str_base_output, str_output_mode, obj_cam, nb_frame) { let str_padded_frame = String(nb_frame).padStart(5, "0"); let str_ext = obj_cam.str_format.toLowerCase(); if (str_ext === "open_exr") { str_ext = "exr"; } else if (str_ext === "jpeg") { str_ext = "jpg"; } else if (str_ext === "tiff") { str_ext = "tif"; } let str_output_path = ""; let str_expected_file = ""; if (str_output_mode === "prefix") { // Flat: /sortie/Camera.001_frame_##### str_output_path = path.join(str_base_output, obj_cam.str_name + "_frame_#####"); str_expected_file = path.join(str_base_output, obj_cam.str_name + "_frame_" + str_padded_frame + "." + str_ext); } else { // Subfolder: /sortie/Camera.001/frame_##### let str_output_dir = path.join(str_base_output, obj_cam.str_name); str_output_path = path.join(str_output_dir, "frame_#####"); str_expected_file = path.join(str_output_dir, "frame_" + str_padded_frame + "." + str_ext); } return { str_blend_path: str_blend_path, str_camera_name: obj_cam.str_name, nb_frame: nb_frame, nb_resolution_x: obj_cam.nb_resolution_x, nb_resolution_y: obj_cam.nb_resolution_y, str_format: obj_cam.str_format, str_output_path: str_output_path, str_expected_file: str_expected_file, str_status: "pending", }; } _process_next() { if (this.str_status !== STR_STATUS_RUNNING) { return; } // Batch skip : boucle iterative pour eviter un stack overflow recursif let nb_skip_count = 0; while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") { let obj_check = this.list_queue[this.nb_current_index]; if (!fs.existsSync(obj_check.str_expected_file)) { break; } let obj_stats = fs.statSync(obj_check.str_expected_file); if (obj_stats.size === 0) { this._send_log("Placeholder vide detecte, re-rendu : " + obj_check.str_camera_name + " F" + obj_check.nb_frame); break; } obj_check.str_status = "skipped"; this.nb_current_index++; nb_skip_count++; } if (nb_skip_count > 0) { this._send_log("Skip : " + nb_skip_count + " fichier(s) existant(s)"); this._send_progress(); } if (this.nb_current_index >= this.list_queue.length) { this.str_status = STR_STATUS_IDLE; this._send_log("Tous les rendus sont termines !"); this._send_event("render-complete", { is_all_done: true }); return; } let obj_item = this.list_queue[this.nb_current_index]; obj_item.str_status = "rendering"; if (this.str_overwrite_mode === "skip") { try { let str_dir = path.dirname(obj_item.str_expected_file); if (!fs.existsSync(str_dir)) { fs.mkdirSync(str_dir, { recursive: true }); } fs.writeFileSync(obj_item.str_expected_file, ""); } catch (obj_file_err) { this._send_log("ERREUR creation placeholder : " + obj_file_err.message); } } this._send_log("Rendu : " + obj_item.str_camera_name + " - Frame " + obj_item.nb_frame); this._send_progress(); let nb_start = Date.now(); BlenderProcess.render_frame({ str_blend_path: obj_item.str_blend_path, str_camera_name: obj_item.str_camera_name, nb_frame: obj_item.nb_frame, nb_resolution_x: obj_item.nb_resolution_x, nb_resolution_y: obj_item.nb_resolution_y, str_format: obj_item.str_format, str_output_path: obj_item.str_output_path, fn_on_stdout: (str_data) => { this._send_log(str_data.trim()); }, fn_on_process: (obj_process) => { this.obj_current_process = obj_process; }, }) .then((obj_result) => { this.obj_current_process = null; if (this.str_status !== STR_STATUS_RUNNING) { return; } obj_item.str_status = "done"; this.nb_last_render_ms = Date.now() - nb_start; let str_image = obj_result.str_rendered_file || obj_item.str_expected_file; this.str_last_image_path = str_image; this._send_event("preview-update", str_image); this._send_log("Termine : " + str_image); this.nb_current_index++; this._send_progress(); this._process_next(); }) .catch((obj_err) => { this.obj_current_process = null; if (this.str_overwrite_mode === "skip" && fs.existsSync(obj_item.str_expected_file)) { try { let obj_stats = fs.statSync(obj_item.str_expected_file); if (obj_stats.size === 0) { fs.unlinkSync(obj_item.str_expected_file); } } catch (obj_cleanup_err) { // Ignore cleanup errors } } if (this.str_status !== STR_STATUS_RUNNING) { return; } obj_item.str_status = "error"; this.nb_last_render_ms = Date.now() - nb_start; this.str_last_image_path = null; this._send_log("ERREUR : " + obj_err.str_message); this._send_event("render-error", { str_camera: obj_item.str_camera_name, nb_frame: obj_item.nb_frame, str_error: obj_err.str_message, }); this.nb_current_index++; this._send_progress(); this._process_next(); }); } _send_progress() { let obj_item = this.list_queue[this.nb_current_index] || {}; let list_skipped = []; for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) { if (this.list_queue[nb_i].str_status === "skipped") { list_skipped.push(nb_i); } } this._send_event("render-progress", { nb_current: this.nb_current_index, nb_total: this.list_queue.length, str_camera: obj_item.str_camera_name || "-", nb_frame: obj_item.nb_frame || 0, str_status: this.str_status, nb_last_render_ms: this.nb_last_render_ms, str_last_image_path: this.str_last_image_path, list_skipped: list_skipped, }); } _send_log(str_message) { this._send_event("log", str_message); } _send_event(str_channel, obj_data) { if (this.obj_window && !this.obj_window.isDestroyed()) { this.obj_window.webContents.send(str_channel, obj_data); } } } module.exports = QueueManager;