v1
This commit is contained in:
103
src/main/BlenderProcess.js
Normal file
103
src/main/BlenderProcess.js
Normal file
@@ -0,0 +1,103 @@
|
||||
const { spawn } = require("child_process");
|
||||
const PathResolver = require("./PathResolver.js");
|
||||
|
||||
const BlenderProcess = {
|
||||
render_frame: (obj_params) => {
|
||||
let str_blend_path = obj_params.str_blend_path;
|
||||
let str_camera_name = obj_params.str_camera_name;
|
||||
let nb_frame = obj_params.nb_frame;
|
||||
let nb_resolution_x = obj_params.nb_resolution_x;
|
||||
let nb_resolution_y = obj_params.nb_resolution_y;
|
||||
let str_format = obj_params.str_format;
|
||||
let str_output_path = obj_params.str_output_path;
|
||||
|
||||
let str_safe_name = str_camera_name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
||||
|
||||
let str_python_expr = [
|
||||
"import bpy",
|
||||
"scene=bpy.context.scene",
|
||||
"scene.camera=bpy.data.objects['" + str_safe_name + "']",
|
||||
"scene.render.resolution_x=" + nb_resolution_x,
|
||||
"scene.render.resolution_y=" + nb_resolution_y,
|
||||
].join(";");
|
||||
|
||||
let list_args = [
|
||||
"-b", str_blend_path,
|
||||
"--python-expr", str_python_expr,
|
||||
"-o", str_output_path,
|
||||
"-F", str_format,
|
||||
"-f", String(nb_frame),
|
||||
];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let str_stdout = "";
|
||||
let str_stderr = "";
|
||||
|
||||
let obj_process = spawn(PathResolver.get_blender_path(), list_args);
|
||||
|
||||
obj_process.stdout.on("data", (obj_data) => {
|
||||
str_stdout += obj_data.toString();
|
||||
if (obj_params.fn_on_stdout) {
|
||||
obj_params.fn_on_stdout(obj_data.toString());
|
||||
}
|
||||
});
|
||||
|
||||
obj_process.stderr.on("data", (obj_data) => {
|
||||
str_stderr += obj_data.toString();
|
||||
});
|
||||
|
||||
obj_process.on("close", (nb_code) => {
|
||||
if (nb_code !== 0) {
|
||||
reject({
|
||||
str_message: "Blender a quitte avec le code " + nb_code,
|
||||
str_stderr: str_stderr,
|
||||
str_stdout: str_stdout,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let str_rendered_file = BlenderProcess._find_rendered_file(str_stdout);
|
||||
resolve({
|
||||
str_rendered_file: str_rendered_file,
|
||||
str_stdout: str_stdout,
|
||||
});
|
||||
});
|
||||
|
||||
obj_process.on("error", (obj_err) => {
|
||||
reject({
|
||||
str_message: "Impossible de lancer Blender : " + obj_err.message,
|
||||
str_stderr: "",
|
||||
str_stdout: "",
|
||||
});
|
||||
});
|
||||
|
||||
// Store process reference for kill support
|
||||
if (obj_params.fn_on_process) {
|
||||
obj_params.fn_on_process(obj_process);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_find_rendered_file: (str_stdout) => {
|
||||
let list_lines = str_stdout.split("\n");
|
||||
for (let str_line of list_lines) {
|
||||
let nb_index = str_line.indexOf("Saved: ");
|
||||
if (nb_index !== -1) {
|
||||
let str_path = str_line.substring(nb_index + 7).trim();
|
||||
// Remove trailing info like " Time: 00:01.23"
|
||||
let nb_time_index = str_path.indexOf(" Time:");
|
||||
if (nb_time_index !== -1) {
|
||||
str_path = str_path.substring(0, nb_time_index).trim();
|
||||
}
|
||||
// Blender 5.x wraps path in single quotes: Saved: '/path/file.png'
|
||||
if (str_path.startsWith("'") && str_path.endsWith("'")) {
|
||||
str_path = str_path.substring(1, str_path.length - 1);
|
||||
}
|
||||
return str_path;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = BlenderProcess;
|
||||
72
src/main/CameraParser.js
Normal file
72
src/main/CameraParser.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const { spawn } = require("child_process");
|
||||
const PathResolver = require("./PathResolver.js");
|
||||
|
||||
const STR_CAMERA_MARKER = "CAMERAS_JSON:";
|
||||
|
||||
const STR_PYTHON_EXPR = [
|
||||
"import bpy, json, sys;",
|
||||
"cams=[o.name for o in bpy.data.objects if o.type=='CAMERA'];",
|
||||
"sys.stdout.write('CAMERAS_JSON:' + json.dumps(cams) + '\\n');",
|
||||
"sys.stdout.flush()",
|
||||
].join("");
|
||||
|
||||
const CameraParser = {
|
||||
list_cameras: (str_blend_path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let str_stdout = "";
|
||||
let str_stderr = "";
|
||||
|
||||
let obj_process = spawn(PathResolver.get_blender_path(), [
|
||||
"-b", str_blend_path,
|
||||
"--python-expr", STR_PYTHON_EXPR,
|
||||
]);
|
||||
|
||||
obj_process.stdout.on("data", (obj_data) => {
|
||||
str_stdout += obj_data.toString();
|
||||
});
|
||||
|
||||
obj_process.stderr.on("data", (obj_data) => {
|
||||
str_stderr += obj_data.toString();
|
||||
});
|
||||
|
||||
obj_process.on("close", (nb_code) => {
|
||||
if (nb_code !== 0) {
|
||||
reject(new Error("Blender a quitte avec le code " + nb_code + " : " + str_stderr));
|
||||
return;
|
||||
}
|
||||
|
||||
let list_cameras = CameraParser._parse_camera_output(str_stdout);
|
||||
if (list_cameras === null) {
|
||||
console.error("[CameraParser] Stdout Blender:\n" + str_stdout.substring(str_stdout.length - 2000));
|
||||
reject(new Error("Impossible de parser les cameras. Verifiez que le fichier .blend contient des cameras."));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(list_cameras);
|
||||
});
|
||||
|
||||
obj_process.on("error", (obj_err) => {
|
||||
reject(new Error("Impossible de lancer Blender. Verifiez qu'il est installe et accessible dans le PATH. " + obj_err.message));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_parse_camera_output: (str_stdout) => {
|
||||
let list_lines = str_stdout.split("\n");
|
||||
for (let str_line of list_lines) {
|
||||
let nb_index = str_line.indexOf(STR_CAMERA_MARKER);
|
||||
if (nb_index !== -1) {
|
||||
let str_json = str_line.substring(nb_index + STR_CAMERA_MARKER.length).trim();
|
||||
try {
|
||||
return JSON.parse(str_json);
|
||||
} catch (obj_err) {
|
||||
console.error("[CameraParser] JSON parse error:", obj_err.message, "raw:", str_json);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = CameraParser;
|
||||
66
src/main/ConfigManager.js
Normal file
66
src/main/ConfigManager.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const fs = require("fs");
|
||||
const { dialog } = require("electron");
|
||||
|
||||
const ConfigManager = {
|
||||
save: (obj_window, obj_config) => {
|
||||
return dialog.showSaveDialog(obj_window, {
|
||||
title: "Sauvegarder la configuration",
|
||||
defaultPath: "render_config.json",
|
||||
filters: [
|
||||
{ name: "Configuration JSON", extensions: ["json"] },
|
||||
],
|
||||
})
|
||||
.then((obj_result) => {
|
||||
if (obj_result.canceled || !obj_result.filePath) {
|
||||
return { is_success: false };
|
||||
}
|
||||
|
||||
let str_json = JSON.stringify(obj_config, null, 4);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(obj_result.filePath, str_json, "utf8", (obj_err) => {
|
||||
if (obj_err) {
|
||||
reject(new Error("Impossible de sauvegarder : " + obj_err.message));
|
||||
return;
|
||||
}
|
||||
resolve({ is_success: true, str_path: obj_result.filePath });
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
load: (obj_window) => {
|
||||
return dialog.showOpenDialog(obj_window, {
|
||||
title: "Charger une configuration",
|
||||
filters: [
|
||||
{ name: "Configuration JSON", extensions: ["json"] },
|
||||
],
|
||||
properties: ["openFile"],
|
||||
})
|
||||
.then((obj_result) => {
|
||||
if (obj_result.canceled || obj_result.filePaths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let str_file_path = obj_result.filePaths[0];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(str_file_path, "utf8", (obj_err, str_data) => {
|
||||
if (obj_err) {
|
||||
reject(new Error("Impossible de lire : " + obj_err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let obj_config = JSON.parse(str_data);
|
||||
resolve(obj_config);
|
||||
} catch (obj_parse_err) {
|
||||
reject(new Error("Fichier corrompu : " + obj_parse_err.message));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ConfigManager;
|
||||
52
src/main/PathResolver.js
Normal file
52
src/main/PathResolver.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
|
||||
|
||||
const PathResolver = {
|
||||
_str_blender_path: null,
|
||||
|
||||
get_blender_path: () => {
|
||||
if (PathResolver._str_blender_path) {
|
||||
return PathResolver._str_blender_path;
|
||||
}
|
||||
|
||||
// Mode package : resources/blender/
|
||||
let str_resources_dir = path.join(process.resourcesPath, "blender");
|
||||
let str_found = PathResolver._find_in_dir(str_resources_dir);
|
||||
if (str_found) {
|
||||
PathResolver._str_blender_path = str_found;
|
||||
return str_found;
|
||||
}
|
||||
|
||||
// Mode dev : racine projet/blender/
|
||||
let str_dev_dir = path.join(__dirname, "..", "..", "blender");
|
||||
str_found = PathResolver._find_in_dir(str_dev_dir);
|
||||
if (str_found) {
|
||||
PathResolver._str_blender_path = str_found;
|
||||
return str_found;
|
||||
}
|
||||
|
||||
// Fallback : PATH systeme
|
||||
PathResolver._str_blender_path = "blender";
|
||||
return "blender";
|
||||
},
|
||||
|
||||
_find_in_dir: (str_dir) => {
|
||||
if (!fs.existsSync(str_dir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let list_entries = fs.readdirSync(str_dir);
|
||||
for (let str_entry of list_entries) {
|
||||
let str_exe = path.join(str_dir, str_entry, STR_EXE_NAME);
|
||||
if (fs.existsSync(str_exe)) {
|
||||
return str_exe;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = PathResolver;
|
||||
300
src/main/QueueManager.js
Normal file
300
src/main/QueueManager.js
Normal 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;
|
||||
30
src/python/list_cameras.py
Normal file
30
src/python/list_cameras.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Script execute par Blender pour lister les cameras d'un fichier .blend."""
|
||||
|
||||
import bpy
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
obj_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_list_cameras() -> list[str]:
|
||||
"""Recupere la liste des noms de cameras dans la scene Blender."""
|
||||
list_cameras = []
|
||||
for obj_item in bpy.data.objects:
|
||||
if obj_item.type == "CAMERA":
|
||||
list_cameras.append(obj_item.name)
|
||||
return list_cameras
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Point d'entree principal du script."""
|
||||
list_cameras = get_list_cameras()
|
||||
str_output = json.dumps(list_cameras)
|
||||
# stdout utilise pour communiquer avec Node.js
|
||||
sys.stdout.write("CAMERAS_JSON:" + str_output + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
main()
|
||||
249
src/renderer/index.html
Normal file
249
src/renderer/index.html
Normal file
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;">
|
||||
<title>Multi Render Blender</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Material Design Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<!-- App CSS -->
|
||||
<link rel="stylesheet" href="styles/Main.css">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<!-- ── Top Bar ──────────────────────────────────────────── -->
|
||||
<nav class="navbar navbar-dark bg-dark border-bottom border-secondary px-3">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<i class="mdi mdi-blender-software me-2"></i>Multi Render Blender
|
||||
</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btn_load_config" class="btn btn-sm btn-outline-secondary" title="Charger config">
|
||||
<i class="mdi mdi-folder-open-outline"></i>
|
||||
</button>
|
||||
<button id="btn_save_config" class="btn btn-sm btn-outline-secondary" title="Sauvegarder config">
|
||||
<i class="mdi mdi-content-save-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid p-3">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ── Left Column : File + Cameras ─────────────── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Blend file selection -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-file-outline me-1"></i>Fichier Blender
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="input_blend_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun fichier selectionne" readonly>
|
||||
<button id="btn_select_blend" class="btn btn-outline-primary" type="button">
|
||||
<i class="mdi mdi-folder-search-outline"></i> Parcourir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output folder -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-folder-outline me-1"></i>Dossier de sortie
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="input_output_path" class="form-control bg-dark text-light border-secondary" placeholder="Selectionnez un dossier" readonly>
|
||||
<button id="btn_select_output" class="btn btn-outline-primary" type="button">
|
||||
<i class="mdi mdi-folder-search-outline"></i> Parcourir
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_subfolder" value="subfolder" checked>
|
||||
<label class="form-check-label" for="radio_output_subfolder">
|
||||
Sous-dossier par camera
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_prefix" value="prefix">
|
||||
<label class="form-check-label" for="radio_output_prefix">
|
||||
Nom camera dans le fichier
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<small id="label_output_example" class="text-light-emphasis">Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render mode -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-swap-horizontal me-1"></i>Mode de rendu
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="render_mode" id="radio_camera_by_camera" value="camera_by_camera" checked>
|
||||
<label class="form-check-label" for="radio_camera_by_camera">
|
||||
Camera par camera
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="render_mode" id="radio_frame_by_frame" value="frame_by_frame">
|
||||
<label class="form-check-label" for="radio_frame_by_frame">
|
||||
Frame par frame
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-2 border-secondary">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="overwrite_mode" id="radio_overwrite" value="overwrite" checked>
|
||||
<label class="form-check-label" for="radio_overwrite">
|
||||
Ecraser les fichiers existants
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="overwrite_mode" id="radio_skip" value="skip">
|
||||
<label class="form-check-label" for="radio_skip">
|
||||
Passer si le fichier existe (multi-PC)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera list -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-camera-outline me-1"></i>Cameras</span>
|
||||
<span id="badge_camera_count" class="badge bg-secondary">0</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_camera_list" class="list-group list-group-flush overflow-auto" style="max-height: 400px;">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
<i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>
|
||||
Chargez un fichier .blend
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Center Column : Camera Config + Controls ── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Camera config -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-cog-outline me-1"></i>Configuration : <span id="label_selected_camera">-</span>
|
||||
</div>
|
||||
<div class="card-body" id="container_camera_config">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
Selectionnez une camera
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render controls -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-play-circle-outline me-1"></i>Controles
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="btn_start" class="btn btn-success flex-fill" disabled>
|
||||
<i class="mdi mdi-play"></i> Start
|
||||
</button>
|
||||
<button id="btn_pause" class="btn btn-warning flex-fill" disabled>
|
||||
<i class="mdi mdi-pause"></i> Pause
|
||||
</button>
|
||||
<button id="btn_stop" class="btn btn-danger flex-fill" disabled>
|
||||
<i class="mdi mdi-stop"></i> Stop
|
||||
</button>
|
||||
</div>
|
||||
<!-- Progress -->
|
||||
<div id="container_progress" class="mb-2">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small id="label_progress_status">En attente</small>
|
||||
<small id="label_progress_count">0 / 0</small>
|
||||
</div>
|
||||
<div class="progress bg-secondary" style="height: 8px;">
|
||||
<div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-light-emphasis">Camera : <span id="label_current_camera">-</span></small>
|
||||
<small class="text-light-emphasis">Frame : <span id="label_current_frame">-</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render queue -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-format-list-numbered me-1"></i>File de rendu</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small id="label_queue_time_estimate" class="queue-time-estimate"></small>
|
||||
<span id="badge_queue_count" class="badge bg-secondary">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_render_queue" class="list-group list-group-flush overflow-auto" style="max-height: 300px;">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
File vide
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Right Column : Preview + Console ─────────── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-image-outline me-1"></i>Preview
|
||||
</div>
|
||||
<div class="card-body p-2 text-center" id="container_preview">
|
||||
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;">
|
||||
<div class="text-light-emphasis">
|
||||
<i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i>
|
||||
Aucun rendu disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console logs -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-console me-1"></i>Console</span>
|
||||
<button id="btn_clear_console" class="btn btn-sm btn-outline-secondary" title="Vider">
|
||||
<i class="mdi mdi-delete-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_console" class="console-output overflow-auto p-2" style="max-height: 300px; min-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/ConsoleLog.js"></script>
|
||||
<script src="scripts/CameraList.js"></script>
|
||||
<script src="scripts/CameraConfig.js"></script>
|
||||
<script src="scripts/RenderQueue.js"></script>
|
||||
<script src="scripts/PreviewPanel.js"></script>
|
||||
<script src="scripts/ProgressBar.js"></script>
|
||||
<script src="scripts/App.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
311
src/renderer/scripts/App.js
Normal file
311
src/renderer/scripts/App.js
Normal file
@@ -0,0 +1,311 @@
|
||||
const App = {
|
||||
str_blend_path: null,
|
||||
str_output_path: null,
|
||||
|
||||
init: () => {
|
||||
ConsoleLog.init();
|
||||
CameraList.init(App._on_camera_select);
|
||||
CameraConfig.init();
|
||||
RenderQueue.init();
|
||||
PreviewPanel.init();
|
||||
ProgressBar.init();
|
||||
|
||||
App._bind_events();
|
||||
App._bind_render_events();
|
||||
|
||||
ConsoleLog.add("Application prete.");
|
||||
},
|
||||
|
||||
_bind_events: () => {
|
||||
let obj_btn_blend = document.getElementById("btn_select_blend");
|
||||
obj_btn_blend.addEventListener("click", () => {
|
||||
App._select_blend_file();
|
||||
});
|
||||
|
||||
let obj_btn_output = document.getElementById("btn_select_output");
|
||||
obj_btn_output.addEventListener("click", () => {
|
||||
App._select_output_folder();
|
||||
});
|
||||
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
obj_btn_start.addEventListener("click", () => {
|
||||
App._start_render();
|
||||
});
|
||||
|
||||
let obj_btn_pause = document.getElementById("btn_pause");
|
||||
obj_btn_pause.addEventListener("click", () => {
|
||||
App._pause_render();
|
||||
});
|
||||
|
||||
let obj_btn_stop = document.getElementById("btn_stop");
|
||||
obj_btn_stop.addEventListener("click", () => {
|
||||
App._stop_render();
|
||||
});
|
||||
|
||||
let obj_btn_save = document.getElementById("btn_save_config");
|
||||
obj_btn_save.addEventListener("click", () => {
|
||||
App._save_config();
|
||||
});
|
||||
|
||||
let obj_btn_load = document.getElementById("btn_load_config");
|
||||
obj_btn_load.addEventListener("click", () => {
|
||||
App._load_config();
|
||||
});
|
||||
|
||||
let obj_radio_subfolder = document.getElementById("radio_output_subfolder");
|
||||
let obj_radio_prefix = document.getElementById("radio_output_prefix");
|
||||
obj_radio_subfolder.addEventListener("change", () => { App._update_output_example(); });
|
||||
obj_radio_prefix.addEventListener("change", () => { App._update_output_example(); });
|
||||
},
|
||||
|
||||
_bind_render_events: () => {
|
||||
window.api.on_render_complete((obj_data) => {
|
||||
if (obj_data.is_all_done) {
|
||||
ConsoleLog.add("Tous les rendus sont termines !");
|
||||
App._set_controls_state("idle");
|
||||
}
|
||||
});
|
||||
|
||||
window.api.on_render_error((obj_data) => {
|
||||
ConsoleLog.add("ERREUR sur " + obj_data.str_camera + " frame " + obj_data.nb_frame + " : " + obj_data.str_error);
|
||||
});
|
||||
},
|
||||
|
||||
_update_output_example: () => {
|
||||
let str_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let obj_label = document.getElementById("label_output_example");
|
||||
if (str_mode === "subfolder") {
|
||||
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png';
|
||||
} else {
|
||||
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>_frame_00001.png';
|
||||
}
|
||||
},
|
||||
|
||||
// ── Actions ────────────────────────────────────────────
|
||||
|
||||
_select_blend_file: () => {
|
||||
let obj_btn_blend = document.getElementById("btn_select_blend");
|
||||
|
||||
window.api.select_blend_file()
|
||||
.then((str_path) => {
|
||||
if (!str_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_blend_path = str_path;
|
||||
document.getElementById("input_blend_path").value = str_path;
|
||||
ConsoleLog.add("Fichier charge : " + str_path);
|
||||
|
||||
obj_btn_blend.disabled = true;
|
||||
CameraList.show_loading();
|
||||
|
||||
return window.api.get_cameras(str_path);
|
||||
})
|
||||
.then((list_cameras) => {
|
||||
obj_btn_blend.disabled = false;
|
||||
|
||||
if (!list_cameras) {
|
||||
return;
|
||||
}
|
||||
|
||||
CameraList.set_cameras(list_cameras);
|
||||
CameraConfig.clear();
|
||||
ConsoleLog.add(list_cameras.length + " camera(s) trouvee(s).");
|
||||
App._update_start_button();
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
obj_btn_blend.disabled = false;
|
||||
ConsoleLog.add("Erreur chargement : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_select_output_folder: () => {
|
||||
window.api.select_output_folder()
|
||||
.then((str_path) => {
|
||||
if (!str_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_output_path = str_path;
|
||||
document.getElementById("input_output_path").value = str_path;
|
||||
ConsoleLog.add("Dossier de sortie : " + str_path);
|
||||
App._update_start_button();
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur selection dossier : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_on_camera_select: (obj_camera) => {
|
||||
CameraConfig.show(obj_camera);
|
||||
},
|
||||
|
||||
_start_render: () => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
if (obj_btn_start.disabled) {
|
||||
return;
|
||||
}
|
||||
obj_btn_start.disabled = true;
|
||||
|
||||
if (!App.str_output_path) {
|
||||
ConsoleLog.add("Veuillez selectionner un dossier de sortie.");
|
||||
App._set_controls_state("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
|
||||
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let str_overwrite_mode = document.querySelector('input[name="overwrite_mode"]:checked').value;
|
||||
let list_cameras = CameraList.list_cameras;
|
||||
|
||||
let obj_config = {
|
||||
str_blend_file: App.str_blend_path,
|
||||
str_render_mode: str_mode,
|
||||
str_output_mode: str_output_mode,
|
||||
str_overwrite_mode: str_overwrite_mode,
|
||||
str_output_path: App.str_output_path,
|
||||
list_cameras: list_cameras,
|
||||
};
|
||||
|
||||
RenderQueue.build_display(str_mode, list_cameras);
|
||||
ProgressBar.reset();
|
||||
|
||||
window.api.start_render(obj_config)
|
||||
.then(() => {
|
||||
App._set_controls_state("running");
|
||||
ConsoleLog.add("Rendu lance en mode : " + str_mode);
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
App._set_controls_state("idle");
|
||||
ConsoleLog.add("Erreur lancement rendu : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_pause_render: () => {
|
||||
window.api.pause_render()
|
||||
.then(() => {
|
||||
App._set_controls_state("paused");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur pause : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_stop_render: () => {
|
||||
window.api.stop_render()
|
||||
.then(() => {
|
||||
App._set_controls_state("idle");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur arret : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_save_config: () => {
|
||||
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
|
||||
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let str_overwrite_mode = document.querySelector('input[name="overwrite_mode"]:checked').value;
|
||||
let obj_config = {
|
||||
str_blend_file: App.str_blend_path,
|
||||
str_render_mode: str_mode,
|
||||
str_output_mode: str_output_mode,
|
||||
str_overwrite_mode: str_overwrite_mode,
|
||||
str_output_path: App.str_output_path,
|
||||
list_cameras: CameraList.list_cameras,
|
||||
};
|
||||
|
||||
window.api.save_config(obj_config)
|
||||
.then((obj_result) => {
|
||||
if (obj_result && obj_result.is_success) {
|
||||
ConsoleLog.add("Configuration exportee : " + obj_result.str_path);
|
||||
}
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur sauvegarde : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_load_config: () => {
|
||||
window.api.load_config()
|
||||
.then((obj_config) => {
|
||||
if (!obj_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_blend_path = obj_config.str_blend_file;
|
||||
App.str_output_path = obj_config.str_output_path || null;
|
||||
document.getElementById("input_blend_path").value = obj_config.str_blend_file || "";
|
||||
document.getElementById("input_output_path").value = obj_config.str_output_path || "";
|
||||
|
||||
if (obj_config.str_render_mode === "frame_by_frame") {
|
||||
document.getElementById("radio_frame_by_frame").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_camera_by_camera").checked = true;
|
||||
}
|
||||
|
||||
if (obj_config.str_output_mode === "prefix") {
|
||||
document.getElementById("radio_output_prefix").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_output_subfolder").checked = true;
|
||||
}
|
||||
|
||||
if (obj_config.str_overwrite_mode === "skip") {
|
||||
document.getElementById("radio_skip").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_overwrite").checked = true;
|
||||
}
|
||||
|
||||
App._update_output_example();
|
||||
|
||||
if (obj_config.list_cameras && obj_config.list_cameras.length > 0) {
|
||||
CameraList.list_cameras = obj_config.list_cameras;
|
||||
CameraList.str_selected_camera = null;
|
||||
CameraList.render();
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = String(obj_config.list_cameras.length);
|
||||
}
|
||||
|
||||
CameraConfig.clear();
|
||||
App._update_start_button();
|
||||
ConsoleLog.add("Configuration importee.");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur chargement config : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
// ── UI State ───────────────────────────────────────────
|
||||
|
||||
_set_controls_state: (str_state) => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
let obj_btn_pause = document.getElementById("btn_pause");
|
||||
let obj_btn_stop = document.getElementById("btn_stop");
|
||||
|
||||
if (str_state === "running") {
|
||||
obj_btn_start.disabled = true;
|
||||
obj_btn_pause.disabled = false;
|
||||
obj_btn_stop.disabled = false;
|
||||
} else if (str_state === "paused") {
|
||||
obj_btn_start.disabled = false;
|
||||
obj_btn_pause.disabled = true;
|
||||
obj_btn_stop.disabled = false;
|
||||
} else {
|
||||
App._update_start_button();
|
||||
obj_btn_pause.disabled = true;
|
||||
obj_btn_stop.disabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
_update_start_button: () => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
let is_ready = App.str_blend_path
|
||||
&& App.str_output_path
|
||||
&& CameraList.get_enabled_cameras().length > 0;
|
||||
obj_btn_start.disabled = !is_ready;
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
App.init();
|
||||
});
|
||||
86
src/renderer/scripts/CameraConfig.js
Normal file
86
src/renderer/scripts/CameraConfig.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const CameraConfig = {
|
||||
obj_current_camera: null,
|
||||
|
||||
init: () => {
|
||||
// Initialized on demand when a camera is selected
|
||||
},
|
||||
|
||||
show: (obj_camera) => {
|
||||
CameraConfig.obj_current_camera = obj_camera;
|
||||
|
||||
let obj_label = document.getElementById("label_selected_camera");
|
||||
obj_label.textContent = obj_camera.str_name;
|
||||
|
||||
let obj_container = document.getElementById("container_camera_config");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
let str_html = ""
|
||||
+ '<div class="row g-2">'
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Resolution X</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_res_x" value="' + obj_camera.nb_resolution_x + '" min="1">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Resolution Y</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_res_y" value="' + obj_camera.nb_resolution_y + '" min="1">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Frame start</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_start" value="' + obj_camera.nb_frame_start + '" min="0">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Frame end</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_end" value="' + obj_camera.nb_frame_end + '" min="0">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-12">'
|
||||
+ ' <label class="form-label form-label-sm">Format</label>'
|
||||
+ ' <select class="form-select form-select-sm bg-dark text-light border-secondary" id="select_format">'
|
||||
+ ' <option value="PNG"' + (obj_camera.str_format === "PNG" ? " selected" : "") + ">PNG</option>"
|
||||
+ ' <option value="JPEG"' + (obj_camera.str_format === "JPEG" ? " selected" : "") + ">JPEG</option>"
|
||||
+ ' <option value="OPEN_EXR"' + (obj_camera.str_format === "OPEN_EXR" ? " selected" : "") + ">EXR</option>"
|
||||
+ ' <option value="BMP"' + (obj_camera.str_format === "BMP" ? " selected" : "") + ">BMP</option>"
|
||||
+ ' <option value="TIFF"' + (obj_camera.str_format === "TIFF" ? " selected" : "") + ">TIFF</option>"
|
||||
+ " </select>"
|
||||
+ " </div>"
|
||||
+ ' <div class="col-12 mt-3">'
|
||||
+ ' <button class="btn btn-sm btn-primary w-100" id="btn_apply_config">'
|
||||
+ ' <i class="mdi mdi-check me-1"></i>Appliquer'
|
||||
+ " </button>"
|
||||
+ " </div>"
|
||||
+ "</div>";
|
||||
|
||||
obj_container.innerHTML = str_html;
|
||||
|
||||
CameraConfig._bind_events();
|
||||
},
|
||||
|
||||
_bind_events: () => {
|
||||
let obj_btn_apply = document.getElementById("btn_apply_config");
|
||||
obj_btn_apply.addEventListener("click", () => {
|
||||
CameraConfig._apply();
|
||||
});
|
||||
},
|
||||
|
||||
_apply: () => {
|
||||
let obj_cam = CameraConfig.obj_current_camera;
|
||||
if (!obj_cam) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj_cam.nb_resolution_x = parseInt(document.getElementById("input_res_x").value, 10) || 1920;
|
||||
obj_cam.nb_resolution_y = parseInt(document.getElementById("input_res_y").value, 10) || 1080;
|
||||
obj_cam.nb_frame_start = parseInt(document.getElementById("input_frame_start").value, 10) || 1;
|
||||
obj_cam.nb_frame_end = parseInt(document.getElementById("input_frame_end").value, 10) || 250;
|
||||
obj_cam.str_format = document.getElementById("select_format").value;
|
||||
|
||||
ConsoleLog.add("Config appliquee pour " + obj_cam.str_name);
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
CameraConfig.obj_current_camera = null;
|
||||
let obj_label = document.getElementById("label_selected_camera");
|
||||
obj_label.textContent = "-";
|
||||
let obj_container = document.getElementById("container_camera_config");
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">Selectionnez une camera</div>';
|
||||
},
|
||||
};
|
||||
116
src/renderer/scripts/CameraList.js
Normal file
116
src/renderer/scripts/CameraList.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const CameraList = {
|
||||
list_cameras: [],
|
||||
str_selected_camera: null,
|
||||
fn_on_select: null,
|
||||
|
||||
init: (fn_on_select) => {
|
||||
CameraList.fn_on_select = fn_on_select;
|
||||
},
|
||||
|
||||
set_cameras: (list_names) => {
|
||||
CameraList.list_cameras = [];
|
||||
|
||||
for (let str_name of list_names) {
|
||||
CameraList.list_cameras.push({
|
||||
str_name: str_name,
|
||||
is_enabled: true,
|
||||
nb_resolution_x: 1920,
|
||||
nb_resolution_y: 1080,
|
||||
nb_frame_start: 1,
|
||||
nb_frame_end: 250,
|
||||
str_format: "PNG",
|
||||
});
|
||||
}
|
||||
|
||||
CameraList.str_selected_camera = null;
|
||||
CameraList.render();
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = String(list_names.length);
|
||||
},
|
||||
|
||||
get_camera_by_name: (str_name) => {
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
if (obj_cam.str_name === str_name) {
|
||||
return obj_cam;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
get_enabled_cameras: () => {
|
||||
let list_enabled = [];
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
if (obj_cam.is_enabled) {
|
||||
list_enabled.push(obj_cam);
|
||||
}
|
||||
}
|
||||
return list_enabled;
|
||||
},
|
||||
|
||||
show_loading: () => {
|
||||
let obj_container = document.getElementById("container_camera_list");
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-loading mdi-spin d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargement des cameras..."
|
||||
+ "</div>";
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = "0";
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let obj_container = document.getElementById("container_camera_list");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
if (CameraList.list_cameras.length === 0) {
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargez un fichier .blend"
|
||||
+ "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
let obj_item = document.createElement("div");
|
||||
obj_item.classList.add("list-group-item", "list-group-item-action", "bg-dark", "text-light", "border-secondary", "d-flex", "align-items-center", "gap-2");
|
||||
|
||||
if (obj_cam.str_name === CameraList.str_selected_camera) {
|
||||
obj_item.classList.add("active");
|
||||
}
|
||||
|
||||
let obj_checkbox = document.createElement("input");
|
||||
obj_checkbox.type = "checkbox";
|
||||
obj_checkbox.classList.add("form-check-input");
|
||||
obj_checkbox.checked = obj_cam.is_enabled;
|
||||
obj_checkbox.addEventListener("change", (event) => {
|
||||
event.stopPropagation();
|
||||
obj_cam.is_enabled = obj_checkbox.checked;
|
||||
});
|
||||
|
||||
let obj_icon = document.createElement("i");
|
||||
obj_icon.classList.add("mdi", "mdi-camera-outline");
|
||||
|
||||
let obj_label = document.createElement("span");
|
||||
obj_label.classList.add("flex-grow-1");
|
||||
obj_label.textContent = obj_cam.str_name;
|
||||
|
||||
obj_item.appendChild(obj_checkbox);
|
||||
obj_item.appendChild(obj_icon);
|
||||
obj_item.appendChild(obj_label);
|
||||
|
||||
obj_item.addEventListener("click", (event) => {
|
||||
if (event.target === obj_checkbox) {
|
||||
return;
|
||||
}
|
||||
CameraList.str_selected_camera = obj_cam.str_name;
|
||||
CameraList.render();
|
||||
if (CameraList.fn_on_select) {
|
||||
CameraList.fn_on_select(obj_cam);
|
||||
}
|
||||
});
|
||||
|
||||
obj_container.appendChild(obj_item);
|
||||
}
|
||||
},
|
||||
};
|
||||
78
src/renderer/scripts/ConsoleLog.js
Normal file
78
src/renderer/scripts/ConsoleLog.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const NB_MAX_CONSOLE_LINES = 500;
|
||||
const NB_FLUSH_INTERVAL = 100;
|
||||
|
||||
const ConsoleLog = {
|
||||
obj_container: null,
|
||||
_list_buffer: [],
|
||||
_nb_flush_timer: null,
|
||||
|
||||
init: () => {
|
||||
ConsoleLog.obj_container = document.getElementById("container_console");
|
||||
|
||||
let obj_btn_clear = document.getElementById("btn_clear_console");
|
||||
obj_btn_clear.addEventListener("click", () => {
|
||||
ConsoleLog.clear();
|
||||
});
|
||||
|
||||
window.api.on_log((str_message) => {
|
||||
ConsoleLog.add(str_message);
|
||||
});
|
||||
},
|
||||
|
||||
add: (str_message) => {
|
||||
ConsoleLog._list_buffer.push(str_message);
|
||||
|
||||
if (ConsoleLog._nb_flush_timer === null) {
|
||||
ConsoleLog._nb_flush_timer = setTimeout(() => {
|
||||
ConsoleLog._flush();
|
||||
}, NB_FLUSH_INTERVAL);
|
||||
}
|
||||
},
|
||||
|
||||
_flush: () => {
|
||||
ConsoleLog._nb_flush_timer = null;
|
||||
|
||||
let list_messages = ConsoleLog._list_buffer;
|
||||
ConsoleLog._list_buffer = [];
|
||||
|
||||
let obj_fragment = document.createDocumentFragment();
|
||||
|
||||
for (let str_message of list_messages) {
|
||||
let obj_line = document.createElement("div");
|
||||
obj_line.classList.add("console-line");
|
||||
|
||||
let obj_time = document.createElement("span");
|
||||
obj_time.classList.add("console-time");
|
||||
let obj_date = new Date();
|
||||
let str_time = String(obj_date.getHours()).padStart(2, "0")
|
||||
+ ":" + String(obj_date.getMinutes()).padStart(2, "0")
|
||||
+ ":" + String(obj_date.getSeconds()).padStart(2, "0");
|
||||
obj_time.textContent = str_time;
|
||||
|
||||
let obj_text = document.createElement("span");
|
||||
obj_text.classList.add("console-text");
|
||||
obj_text.textContent = str_message;
|
||||
|
||||
obj_line.appendChild(obj_time);
|
||||
obj_line.appendChild(obj_text);
|
||||
obj_fragment.appendChild(obj_line);
|
||||
}
|
||||
|
||||
ConsoleLog.obj_container.appendChild(obj_fragment);
|
||||
|
||||
while (ConsoleLog.obj_container.childElementCount > NB_MAX_CONSOLE_LINES) {
|
||||
ConsoleLog.obj_container.removeChild(ConsoleLog.obj_container.firstChild);
|
||||
}
|
||||
|
||||
ConsoleLog.obj_container.scrollTop = ConsoleLog.obj_container.scrollHeight;
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
ConsoleLog._list_buffer = [];
|
||||
if (ConsoleLog._nb_flush_timer !== null) {
|
||||
clearTimeout(ConsoleLog._nb_flush_timer);
|
||||
ConsoleLog._nb_flush_timer = null;
|
||||
}
|
||||
ConsoleLog.obj_container.innerHTML = "";
|
||||
},
|
||||
};
|
||||
58
src/renderer/scripts/PreviewPanel.js
Normal file
58
src/renderer/scripts/PreviewPanel.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const PreviewPanel = {
|
||||
obj_container: null,
|
||||
|
||||
init: () => {
|
||||
PreviewPanel.obj_container = document.getElementById("container_preview");
|
||||
|
||||
window.api.on_preview_update((str_image_path) => {
|
||||
PreviewPanel.show_image(str_image_path);
|
||||
});
|
||||
},
|
||||
|
||||
show_image: (str_image_path) => {
|
||||
if (!str_image_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-loading mdi-spin d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargement..."
|
||||
+ "</div>";
|
||||
|
||||
window.api.read_image(str_image_path)
|
||||
.then((str_data_url) => {
|
||||
PreviewPanel.obj_container.innerHTML = "";
|
||||
|
||||
let obj_img = document.createElement("img");
|
||||
obj_img.classList.add("preview-image");
|
||||
obj_img.src = str_data_url;
|
||||
obj_img.alt = "Rendu";
|
||||
|
||||
let obj_label = document.createElement("div");
|
||||
obj_label.classList.add("preview-label", "text-light-emphasis", "mt-1");
|
||||
|
||||
let str_filename = str_image_path.replace(/\\/g, "/");
|
||||
let list_parts = str_filename.split("/");
|
||||
obj_label.textContent = list_parts[list_parts.length - 1];
|
||||
|
||||
PreviewPanel.obj_container.appendChild(obj_img);
|
||||
PreviewPanel.obj_container.appendChild(obj_label);
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur preview : " + (obj_err.message || obj_err));
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="text-center text-warning py-4">'
|
||||
+ '<i class="mdi mdi-image-broken-variant d-block mb-2" style="font-size: 3rem;"></i>'
|
||||
+ "Impossible de charger l'image"
|
||||
+ '<div class="mt-1" style="font-size: 0.7rem;">' + str_image_path + "</div>"
|
||||
+ "</div>";
|
||||
});
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;">'
|
||||
+ '<div class="text-light-emphasis">'
|
||||
+ '<i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i>'
|
||||
+ "Aucun rendu disponible"
|
||||
+ "</div></div>";
|
||||
},
|
||||
};
|
||||
54
src/renderer/scripts/ProgressBar.js
Normal file
54
src/renderer/scripts/ProgressBar.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const ProgressBar = {
|
||||
init: () => {
|
||||
window.api.on_render_progress((obj_data) => {
|
||||
ProgressBar.update(obj_data);
|
||||
});
|
||||
},
|
||||
|
||||
update: (obj_data) => {
|
||||
let nb_current = obj_data.nb_current || 0;
|
||||
let nb_total = obj_data.nb_total || 0;
|
||||
let str_camera = obj_data.str_camera || "-";
|
||||
let nb_frame = obj_data.nb_frame || 0;
|
||||
let str_status = obj_data.str_status || "idle";
|
||||
|
||||
let nb_percent = 0;
|
||||
if (nb_total > 0) {
|
||||
nb_percent = Math.round((nb_current / nb_total) * 100);
|
||||
}
|
||||
|
||||
let obj_bar = document.getElementById("bar_progress");
|
||||
obj_bar.style.width = nb_percent + "%";
|
||||
|
||||
let obj_count = document.getElementById("label_progress_count");
|
||||
obj_count.textContent = nb_current + " / " + nb_total;
|
||||
|
||||
let obj_status = document.getElementById("label_progress_status");
|
||||
if (str_status === "running") {
|
||||
obj_status.textContent = "Rendu en cours...";
|
||||
} else if (str_status === "paused") {
|
||||
obj_status.textContent = "En pause";
|
||||
} else if (str_status === "idle" && nb_current >= nb_total && nb_total > 0) {
|
||||
obj_status.textContent = "Termine";
|
||||
} else {
|
||||
obj_status.textContent = "En attente";
|
||||
}
|
||||
|
||||
let obj_camera_label = document.getElementById("label_current_camera");
|
||||
obj_camera_label.textContent = str_camera;
|
||||
|
||||
let obj_frame_label = document.getElementById("label_current_frame");
|
||||
obj_frame_label.textContent = nb_frame > 0 ? String(nb_frame) : "-";
|
||||
|
||||
RenderQueue.update_progress(nb_current, obj_data.nb_last_render_ms || 0, obj_data.str_last_image_path || null, obj_data.list_skipped || []);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
let obj_bar = document.getElementById("bar_progress");
|
||||
obj_bar.style.width = "0%";
|
||||
document.getElementById("label_progress_count").textContent = "0 / 0";
|
||||
document.getElementById("label_progress_status").textContent = "En attente";
|
||||
document.getElementById("label_current_camera").textContent = "-";
|
||||
document.getElementById("label_current_frame").textContent = "-";
|
||||
},
|
||||
};
|
||||
234
src/renderer/scripts/RenderQueue.js
Normal file
234
src/renderer/scripts/RenderQueue.js
Normal file
@@ -0,0 +1,234 @@
|
||||
const RenderQueue = {
|
||||
list_items: [],
|
||||
nb_total_render_ms: 0,
|
||||
nb_completed_renders: 0,
|
||||
nb_last_current: 0,
|
||||
|
||||
init: () => {
|
||||
// Initialized on demand
|
||||
},
|
||||
|
||||
build_display: (str_mode, list_cameras) => {
|
||||
RenderQueue.list_items = [];
|
||||
RenderQueue.nb_total_render_ms = 0;
|
||||
RenderQueue.nb_completed_renders = 0;
|
||||
RenderQueue.nb_last_current = 0;
|
||||
|
||||
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++) {
|
||||
RenderQueue.list_items.push({
|
||||
str_camera: obj_cam.str_name,
|
||||
nb_frame: nb_frame,
|
||||
str_status: "pending",
|
||||
str_image_path: null,
|
||||
obj_dom_el: null,
|
||||
obj_dom_icon: null,
|
||||
str_dom_status: null,
|
||||
is_click_bound: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let nb_min = Infinity;
|
||||
let nb_max = -Infinity;
|
||||
for (let obj_cam of list_enabled) {
|
||||
if (obj_cam.nb_frame_start < nb_min) {
|
||||
nb_min = obj_cam.nb_frame_start;
|
||||
}
|
||||
if (obj_cam.nb_frame_end > nb_max) {
|
||||
nb_max = obj_cam.nb_frame_end;
|
||||
}
|
||||
}
|
||||
|
||||
for (let nb_frame = nb_min; nb_frame <= nb_max; nb_frame++) {
|
||||
for (let obj_cam of list_enabled) {
|
||||
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end) {
|
||||
RenderQueue.list_items.push({
|
||||
str_camera: obj_cam.str_name,
|
||||
nb_frame: nb_frame,
|
||||
str_status: "pending",
|
||||
str_image_path: null,
|
||||
obj_dom_el: null,
|
||||
obj_dom_icon: null,
|
||||
str_dom_status: null,
|
||||
is_click_bound: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let obj_badge = document.getElementById("badge_queue_count");
|
||||
obj_badge.textContent = String(RenderQueue.list_items.length);
|
||||
|
||||
RenderQueue._update_time_display();
|
||||
RenderQueue._create_dom();
|
||||
},
|
||||
|
||||
_create_dom: () => {
|
||||
let obj_container = document.getElementById("container_render_queue");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
if (RenderQueue.list_items.length === 0) {
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">File vide</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let obj_fragment = document.createDocumentFragment();
|
||||
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
let obj_el = document.createElement("div");
|
||||
obj_el.classList.add("list-group-item", "bg-dark", "text-light", "border-secondary", "py-1", "px-3", "d-flex", "align-items-center", "gap-2");
|
||||
|
||||
let obj_icon = document.createElement("i");
|
||||
obj_icon.classList.add("mdi", "mdi-clock-outline", "text-muted");
|
||||
|
||||
let obj_name = document.createElement("small");
|
||||
obj_name.classList.add("flex-grow-1");
|
||||
obj_name.textContent = obj_item.str_camera;
|
||||
|
||||
let obj_frame = document.createElement("small");
|
||||
obj_frame.classList.add("text-light-emphasis");
|
||||
obj_frame.textContent = "F" + obj_item.nb_frame;
|
||||
|
||||
obj_el.appendChild(obj_icon);
|
||||
obj_el.appendChild(obj_name);
|
||||
obj_el.appendChild(obj_frame);
|
||||
|
||||
obj_item.obj_dom_el = obj_el;
|
||||
obj_item.obj_dom_icon = obj_icon;
|
||||
obj_item.str_dom_status = "pending";
|
||||
|
||||
obj_fragment.appendChild(obj_el);
|
||||
}
|
||||
|
||||
obj_container.appendChild(obj_fragment);
|
||||
},
|
||||
|
||||
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped) => {
|
||||
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
|
||||
RenderQueue.nb_total_render_ms += nb_last_render_ms;
|
||||
RenderQueue.nb_completed_renders++;
|
||||
}
|
||||
RenderQueue.nb_last_current = nb_current;
|
||||
|
||||
if (str_last_image_path && nb_current > 0 && nb_current - 1 < RenderQueue.list_items.length) {
|
||||
RenderQueue.list_items[nb_current - 1].str_image_path = str_last_image_path;
|
||||
}
|
||||
|
||||
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||
if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
|
||||
RenderQueue.list_items[nb_i].str_status = "skipped";
|
||||
} else if (nb_i < nb_current) {
|
||||
RenderQueue.list_items[nb_i].str_status = "done";
|
||||
} else if (nb_i === nb_current) {
|
||||
RenderQueue.list_items[nb_i].str_status = "rendering";
|
||||
} else {
|
||||
RenderQueue.list_items[nb_i].str_status = "pending";
|
||||
}
|
||||
}
|
||||
|
||||
RenderQueue._update_time_display();
|
||||
RenderQueue._update_statuses();
|
||||
},
|
||||
|
||||
_update_statuses: () => {
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
if (!obj_item.obj_dom_el) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_needs_click = obj_item.str_status === "done" && obj_item.str_image_path && !obj_item.is_click_bound;
|
||||
|
||||
if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let str_icon = "mdi-clock-outline";
|
||||
let str_color = "text-muted";
|
||||
|
||||
if (obj_item.str_status === "rendering") {
|
||||
str_icon = "mdi-loading mdi-spin";
|
||||
str_color = "text-primary";
|
||||
} else if (obj_item.str_status === "done") {
|
||||
str_icon = "mdi-check-circle";
|
||||
str_color = "text-success";
|
||||
} else if (obj_item.str_status === "error") {
|
||||
str_icon = "mdi-alert-circle";
|
||||
str_color = "text-danger";
|
||||
} else if (obj_item.str_status === "skipped") {
|
||||
str_icon = "mdi-skip-next-circle";
|
||||
str_color = "text-info";
|
||||
}
|
||||
|
||||
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
|
||||
|
||||
if (is_needs_click) {
|
||||
obj_item.obj_dom_el.classList.add("queue-item-clickable");
|
||||
let str_path = obj_item.str_image_path;
|
||||
obj_item.obj_dom_el.addEventListener("click", () => {
|
||||
PreviewPanel.show_image(str_path);
|
||||
});
|
||||
obj_item.is_click_bound = true;
|
||||
}
|
||||
|
||||
obj_item.str_dom_status = obj_item.str_status;
|
||||
}
|
||||
},
|
||||
|
||||
_update_time_display: () => {
|
||||
let obj_label = document.getElementById("label_queue_time_estimate");
|
||||
if (!obj_label) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RenderQueue.nb_completed_renders === 0) {
|
||||
obj_label.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders;
|
||||
let nb_remaining_count = 0;
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") {
|
||||
nb_remaining_count++;
|
||||
}
|
||||
}
|
||||
|
||||
let nb_remaining_ms = nb_avg_ms * nb_remaining_count;
|
||||
obj_label.innerHTML = '<i class="mdi mdi-clock-outline me-1"></i>'
|
||||
+ RenderQueue._format_duration(nb_remaining_ms);
|
||||
},
|
||||
|
||||
_format_duration: (nb_ms) => {
|
||||
let nb_total_seconds = Math.ceil(nb_ms / 1000);
|
||||
let nb_days = Math.floor(nb_total_seconds / 86400);
|
||||
nb_total_seconds %= 86400;
|
||||
let nb_hours = Math.floor(nb_total_seconds / 3600);
|
||||
nb_total_seconds %= 3600;
|
||||
let nb_minutes = Math.floor(nb_total_seconds / 60);
|
||||
let nb_seconds = nb_total_seconds % 60;
|
||||
|
||||
let str_result = "";
|
||||
if (nb_days > 0) {
|
||||
str_result += nb_days + "j ";
|
||||
}
|
||||
if (nb_hours > 0 || nb_days > 0) {
|
||||
str_result += nb_hours + "h ";
|
||||
}
|
||||
if (nb_minutes > 0 || nb_hours > 0 || nb_days > 0) {
|
||||
str_result += nb_minutes + "m ";
|
||||
}
|
||||
str_result += nb_seconds + "s";
|
||||
|
||||
return str_result;
|
||||
},
|
||||
};
|
||||
207
src/renderer/styles/Main.css
Normal file
207
src/renderer/styles/Main.css
Normal file
@@ -0,0 +1,207 @@
|
||||
/* ── Global ─────────────────────────────────────────────────── */
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.col-md-4 {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Cards ──────────────────────────────────────────────────── */
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Camera list ────────────────────────────────────────────── */
|
||||
|
||||
.list-group-item-action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
background-color: rgba(13, 110, 253, 0.2) !important;
|
||||
border-color: #495057 !important;
|
||||
}
|
||||
|
||||
/* ── Preview ────────────────────────────────────────────────── */
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 350px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Console ────────────────────────────────────────────────── */
|
||||
|
||||
.console-output {
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.5;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 1px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.console-time {
|
||||
color: #6e7681;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.console-text {
|
||||
color: #c9d1d9;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Progress ───────────────────────────────────────────────── */
|
||||
|
||||
.progress {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Render queue items ─────────────────────────────────────── */
|
||||
|
||||
#container_render_queue .list-group-item {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#container_render_queue .queue-item-clickable {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
#container_render_queue .queue-item-clickable:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* ── Queue time estimate ───────────────────────────────────── */
|
||||
|
||||
.queue-time-estimate {
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Text contrast overrides ─────────────────────────────────── */
|
||||
|
||||
.text-muted {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.text-light-emphasis {
|
||||
color: #c9d1d9 !important;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #8b949e !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Form tweaks ────────────────────────────────────────────── */
|
||||
|
||||
.form-label {
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
color: #e1e4e8 !important;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.form-control:disabled,
|
||||
.form-control[readonly] {
|
||||
color: #c9d1d9 !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ──────────────────────────────────────────────── */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #495057;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
/* ── Badge ──────────────────────────────────────────────────── */
|
||||
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user