This commit is contained in:
sorlinv
2026-02-20 19:27:27 +01:00
commit b556cce88c
23 changed files with 8182 additions and 0 deletions

300
src/main/QueueManager.js Normal file
View File

@@ -0,0 +1,300 @@
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;