16 Commits

Author SHA1 Message Date
sorlinv
5d83212761 chore: release v1.6.4 2026-03-05 16:10:13 +01:00
sorlinv
34306a3d05 fix update 2026-03-05 16:10:02 +01:00
sorlinv
cbd2f676e2 chore: release v1.6.3 2026-03-05 16:09:31 +01:00
sorlinv
ada5c05aa7 chore: release v1.6.2 2026-03-05 16:00:52 +01:00
sorlinv
eae2c68f29 Update package-lock.json 2026-03-05 15:59:18 +01:00
sorlinv
54d7d41eb6 chore: release v1.6.1 2026-03-05 15:55:54 +01:00
sorlinv
88ad17164c fix nodemailer 2026-03-05 15:55:43 +01:00
sorlinv
40b9e29f80 chore: release v1.6.0 2026-03-05 15:43:45 +01:00
sorlinv
b169e69b24 temps restant + video + download link + left click + show next frame done 2026-03-05 15:43:28 +01:00
sorlinv
9ab59373df Update package-lock.json 2026-03-04 11:38:21 +01:00
sorlinv
02d2e9ed1d chore: release v1.5.0 2026-03-04 09:52:30 +01:00
sorlinv
b1c66f055a better render queue multi 2026-03-04 09:52:17 +01:00
sorlinv
7670707d0d chore: release v1.4.0 2026-02-27 18:02:43 +01:00
sorlinv
bf05c62feb better queue render 2026-02-27 18:01:00 +01:00
sorlinv
de40d2b6af chore: release v1.3.0 2026-02-27 16:39:23 +01:00
sorlinv
19fab8ec65 fix release.sh 2026-02-27 10:36:58 +01:00
27 changed files with 3793 additions and 1293 deletions

View File

@@ -21,7 +21,18 @@
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git tag:*)", "Bash(git tag:*)",
"Bash(node:*)", "Bash(node:*)",
"Bash(chmod +x /home/valentin/Documents/GitHub/multi_render_blender/release.sh)" "Bash(chmod +x /home/valentin/Documents/GitHub/multi_render_blender/release.sh)",
"Bash(git rm --cached .env)",
"Bash(git status -u)",
"Bash(git push)",
"Bash(git push origin v1.2.0)",
"Bash(python3 -c \"import json,sys; data=json.load\\(sys.stdin\\); [print\\(f''{t[\"\"name\"\"]} -> {t[\"\"id\"\"][:12]}''\\) for t in data]\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''Release creee: id={d.get\\(\"\"id\"\", \"\"ERREUR\"\"\\)} tag={d.get\\(\"\"tag_name\"\", \"\"?\"\"\\)}''\\); print\\(d.get\\(''message'',''''\\)\\) if ''message'' in d else None\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f'' OK: {d.get\\(\"\"name\"\",\"\"?\"\"\\)} \\({d.get\\(\"\"size\"\",0\\)//1024//1024}MB\\)''\\)\")",
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(f''OK: {d.get\\(\"\"name\"\",d.get\\(\"\"message\"\",\"\"?\"\"\\)\\)} size={d.get\\(\"\"size\"\",0\\)//1024//1024}MB''\\)\")",
"Bash(python3 -c \":*)",
"mcp__ide__getDiagnostics",
"Bash(ffmpeg:*)"
] ]
} }
} }

161
main.js
View File

@@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, dialog } = require("electron"); const { app, BrowserWindow, ipcMain, dialog, shell, clipboard, nativeImage } = require("electron");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const CameraParser = require("./src/main/CameraParser.js"); const CameraParser = require("./src/main/CameraParser.js");
@@ -6,12 +6,16 @@ const QueueManager = require("./src/main/QueueManager.js");
const ConfigManager = require("./src/main/ConfigManager.js"); const ConfigManager = require("./src/main/ConfigManager.js");
const UpdateManager = require("./src/main/UpdateManager.js"); const UpdateManager = require("./src/main/UpdateManager.js");
const PathResolver = require("./src/main/PathResolver.js"); const PathResolver = require("./src/main/PathResolver.js");
const VideoGenerator = require("./src/main/VideoGenerator.js");
const EmailNotifier = require("./src/main/EmailNotifier.js");
let obj_main_window = null; let obj_main_window = null;
let obj_queue_manager = null; let obj_queue_manager = null;
let obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true }; let obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
let obj_email_config = { is_enabled: false, str_to: "" };
const STR_NOTIFICATION_CONFIG_FILE = "notification_config.json"; const STR_NOTIFICATION_CONFIG_FILE = "notification_config.json";
const STR_EMAIL_CONFIG_FILE = "email_config.json";
const _load_notification_config = () => { const _load_notification_config = () => {
let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE); let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE);
@@ -34,6 +38,28 @@ const _save_notification_config = () => {
} }
}; };
const _load_email_config = () => {
let str_config_path = path.join(app.getPath("userData"), STR_EMAIL_CONFIG_FILE);
try {
if (fs.existsSync(str_config_path)) {
let str_content = fs.readFileSync(str_config_path, "utf8");
obj_email_config = JSON.parse(str_content);
EmailNotifier.set_config(obj_email_config);
}
} catch (obj_err) {
console.error("Erreur lecture config email :", obj_err.message);
}
};
const _save_email_config = () => {
let str_config_path = path.join(app.getPath("userData"), STR_EMAIL_CONFIG_FILE);
try {
fs.writeFileSync(str_config_path, JSON.stringify(obj_email_config, null, 4), "utf8");
} catch (obj_err) {
console.error("Erreur sauvegarde config email :", obj_err.message);
}
};
const create_window = () => { const create_window = () => {
obj_main_window = new BrowserWindow({ obj_main_window = new BrowserWindow({
width: 1400, width: 1400,
@@ -53,13 +79,17 @@ const create_window = () => {
obj_queue_manager = new QueueManager(obj_main_window); obj_queue_manager = new QueueManager(obj_main_window);
PathResolver.load_saved_path(); PathResolver.load_saved_path();
PathResolver.load_saved_ffmpeg_path();
_load_notification_config(); _load_notification_config();
_load_email_config();
obj_queue_manager.set_notification_config(obj_notification_config); obj_queue_manager.set_notification_config(obj_notification_config);
obj_queue_manager.set_email_notifier(EmailNotifier);
UpdateManager.init(obj_main_window); UpdateManager.init(obj_main_window);
obj_main_window.webContents.on("did-finish-load", () => { obj_main_window.webContents.on("did-finish-load", () => {
UpdateManager.check_for_updates(); UpdateManager.check_for_updates();
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status()); obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
obj_main_window.webContents.send("ffmpeg-path-status", PathResolver.get_ffmpeg_status());
}); });
}; };
@@ -205,6 +235,37 @@ ipcMain.handle("set-blender-path", (event, str_path) => {
return obj_result; return obj_result;
}); });
ipcMain.handle("get-ffmpeg-path", () => {
return PathResolver.get_ffmpeg_status();
});
ipcMain.handle("set-ffmpeg-path", (event, str_path) => {
let obj_result = PathResolver.set_ffmpeg_path(str_path);
if (obj_result.is_success) {
obj_main_window.webContents.send("ffmpeg-path-status", PathResolver.get_ffmpeg_status());
}
return obj_result;
});
ipcMain.handle("select-ffmpeg-exe", () => {
let str_exe_name = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
let list_filters = process.platform === "win32"
? [{ name: "FFmpeg", extensions: ["exe"] }]
: [{ name: "FFmpeg", extensions: ["*"] }];
return dialog.showOpenDialog(obj_main_window, {
title: "Selectionner l'executable FFmpeg",
filters: list_filters,
properties: ["openFile"],
})
.then((obj_result) => {
if (obj_result.canceled || obj_result.filePaths.length === 0) {
return null;
}
return obj_result.filePaths[0];
});
});
ipcMain.handle("select-blender-exe", () => { ipcMain.handle("select-blender-exe", () => {
let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender"; let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender";
let list_filters = process.platform === "win32" let list_filters = process.platform === "win32"
@@ -232,6 +293,15 @@ ipcMain.handle("apply-update", (event, str_tag_name) => {
return UpdateManager.download_and_apply(str_tag_name); return UpdateManager.download_and_apply(str_tag_name);
}); });
ipcMain.handle("generate-preview-video", (event, obj_params) => {
let fn_on_log = (str_msg) => {
if (obj_main_window && !obj_main_window.isDestroyed()) {
obj_main_window.webContents.send("log", "[ffmpeg] " + str_msg);
}
};
return VideoGenerator.generate(obj_params, fn_on_log);
});
ipcMain.handle("select-output-folder", () => { ipcMain.handle("select-output-folder", () => {
return dialog.showOpenDialog(obj_main_window, { return dialog.showOpenDialog(obj_main_window, {
title: "Selectionner le dossier de sortie", title: "Selectionner le dossier de sortie",
@@ -245,6 +315,78 @@ ipcMain.handle("select-output-folder", () => {
}); });
}); });
// ── File actions (context menu) ───────────────────────────────
ipcMain.handle("show-item-in-folder", (event, str_path) => {
shell.showItemInFolder(str_path);
return { is_success: true };
});
ipcMain.handle("open-file-default", (event, str_path) => {
return shell.openPath(str_path)
.then((str_error) => {
if (str_error) {
return { is_success: false, str_error: str_error };
}
return { is_success: true };
});
});
ipcMain.handle("delete-rendered-file", (event, str_path) => {
try {
if (fs.existsSync(str_path)) {
fs.unlinkSync(str_path);
return { is_success: true };
}
return { is_success: false, str_error: "Fichier introuvable" };
} catch (obj_err) {
return { is_success: false, str_error: obj_err.message };
}
});
ipcMain.handle("delete-rendered-files", (event, list_paths) => {
let nb_deleted = 0;
let list_errors = [];
for (let str_p of list_paths) {
try {
if (fs.existsSync(str_p)) {
fs.unlinkSync(str_p);
nb_deleted++;
}
} catch (obj_err) {
list_errors.push(str_p + ": " + obj_err.message);
}
}
return { is_success: true, nb_deleted: nb_deleted, list_errors: list_errors };
});
ipcMain.handle("get-file-info", (event, str_path) => {
try {
let obj_stats = fs.statSync(str_path);
return {
is_success: true,
nb_size: obj_stats.size,
str_modified: obj_stats.mtime.toISOString(),
str_created: obj_stats.birthtime.toISOString(),
};
} catch (obj_err) {
return { is_success: false, str_error: obj_err.message };
}
});
ipcMain.handle("copy-image-to-clipboard", (event, str_path) => {
try {
let obj_image = nativeImage.createFromPath(str_path);
if (obj_image.isEmpty()) {
return { is_success: false, str_error: "Image vide ou format non supporte" };
}
clipboard.writeImage(obj_image);
return { is_success: true };
} catch (obj_err) {
return { is_success: false, str_error: obj_err.message };
}
});
// ── Notification Config ─────────────────────────────────────── // ── Notification Config ───────────────────────────────────────
ipcMain.handle("get-notification-config", () => { ipcMain.handle("get-notification-config", () => {
@@ -257,3 +399,20 @@ ipcMain.handle("set-notification-config", (event, obj_config) => {
obj_queue_manager.set_notification_config(obj_notification_config); obj_queue_manager.set_notification_config(obj_notification_config);
return { is_success: true }; return { is_success: true };
}); });
// ── Email Config ──────────────────────────────────────────────
ipcMain.handle("get-email-config", () => {
return obj_email_config;
});
ipcMain.handle("set-email-config", (event, obj_config) => {
obj_email_config = obj_config;
_save_email_config();
EmailNotifier.set_config(obj_email_config);
return { is_success: true };
});
ipcMain.handle("test-email", (event, obj_config) => {
return EmailNotifier.test(obj_config);
});

1988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "multi-render-blender", "name": "multi-render-blender",
"version": "1.2.0", "version": "1.6.4",
"description": "Application Electron pour piloter des rendus Blender multi-cameras", "description": "Application Electron pour piloter des rendus Blender multi-cameras",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@@ -11,8 +11,8 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"electron": "^34.0.0", "electron": "^40.7.0",
"electron-builder": "^25.1.8" "electron-builder": "^26.8.1"
}, },
"build": { "build": {
"appId": "com.multirender.blender", "appId": "com.multirender.blender",
@@ -22,7 +22,8 @@
"main.js", "main.js",
"preload.js", "preload.js",
"src/**/*", "src/**/*",
"version.json" "version.json",
"node_modules/nodemailer/**/*"
], ],
"linux": { "linux": {
"target": "dir" "target": "dir"
@@ -31,5 +32,8 @@
"target": "dir", "target": "dir",
"signAndEditExecutable": false "signAndEditExecutable": false
} }
},
"dependencies": {
"nodemailer": "^8.0.1"
} }
} }

View File

@@ -41,6 +41,34 @@ contextBridge.exposeInMainWorld("api", {
return ipcRenderer.invoke("read-image", str_path); return ipcRenderer.invoke("read-image", str_path);
}, },
show_item_in_folder: (str_path) => {
return ipcRenderer.invoke("show-item-in-folder", str_path);
},
open_file_default: (str_path) => {
return ipcRenderer.invoke("open-file-default", str_path);
},
delete_rendered_file: (str_path) => {
return ipcRenderer.invoke("delete-rendered-file", str_path);
},
delete_rendered_files: (list_paths) => {
return ipcRenderer.invoke("delete-rendered-files", list_paths);
},
get_file_info: (str_path) => {
return ipcRenderer.invoke("get-file-info", str_path);
},
copy_image_to_clipboard: (str_path) => {
return ipcRenderer.invoke("copy-image-to-clipboard", str_path);
},
generate_preview_video: (obj_params) => {
return ipcRenderer.invoke("generate-preview-video", obj_params);
},
on_render_progress: (fn_callback) => { on_render_progress: (fn_callback) => {
ipcRenderer.on("render-progress", (event, obj_data) => { ipcRenderer.on("render-progress", (event, obj_data) => {
fn_callback(obj_data); fn_callback(obj_data);
@@ -89,6 +117,24 @@ contextBridge.exposeInMainWorld("api", {
}); });
}, },
get_ffmpeg_path: () => {
return ipcRenderer.invoke("get-ffmpeg-path");
},
set_ffmpeg_path: (str_path) => {
return ipcRenderer.invoke("set-ffmpeg-path", str_path);
},
select_ffmpeg_exe: () => {
return ipcRenderer.invoke("select-ffmpeg-exe");
},
on_ffmpeg_path_status: (fn_callback) => {
ipcRenderer.on("ffmpeg-path-status", (event, obj_data) => {
fn_callback(obj_data);
});
},
get_notification_config: () => { get_notification_config: () => {
return ipcRenderer.invoke("get-notification-config"); return ipcRenderer.invoke("get-notification-config");
}, },
@@ -97,6 +143,18 @@ contextBridge.exposeInMainWorld("api", {
return ipcRenderer.invoke("set-notification-config", obj_config); return ipcRenderer.invoke("set-notification-config", obj_config);
}, },
get_email_config: () => {
return ipcRenderer.invoke("get-email-config");
},
set_email_config: (obj_config) => {
return ipcRenderer.invoke("set-email-config", obj_config);
},
test_email: (obj_config) => {
return ipcRenderer.invoke("test-email", obj_config);
},
check_for_updates: () => { check_for_updates: () => {
return ipcRenderer.invoke("check-for-updates"); return ipcRenderer.invoke("check-for-updates");
}, },

View File

@@ -3,6 +3,18 @@ set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
############################################
# Options CLI
############################################
BUMP_ARG=""
while getopts "i:" opt; do
case "$opt" in
i) BUMP_ARG="$OPTARG" ;;
*) echo "Usage: $0 [-i patch|minor|major]"; exit 1 ;;
esac
done
############################################ ############################################
# Chargement du token # Chargement du token
############################################ ############################################
@@ -30,21 +42,30 @@ CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Version actuelle: v$CURRENT_VERSION" echo "Version actuelle: v$CURRENT_VERSION"
echo "" echo ""
echo "Comment incrementer la version ?" if [ -n "$BUMP_ARG" ]; then
echo " 1) patch" case "$BUMP_ARG" in
echo " 2) minor" patch) npm version patch --no-git-tag-version ;;
echo " 3) major" minor) npm version minor --no-git-tag-version ;;
echo " 4) garder ($CURRENT_VERSION)" major) npm version major --no-git-tag-version ;;
echo "" *) echo "Erreur: -i accepte patch, minor ou major"; exit 1 ;;
read -p "Choix [1/2/3/4]: " BUMP_CHOICE esac
else
echo "Comment incrementer la version ?"
echo " 1) patch"
echo " 2) minor"
echo " 3) major"
echo " 4) garder ($CURRENT_VERSION)"
echo ""
read -p "Choix [1/2/3/4]: " BUMP_CHOICE
case "$BUMP_CHOICE" in case "$BUMP_CHOICE" in
1) npm version patch --no-git-tag-version ;; 1) npm version patch --no-git-tag-version ;;
2) npm version minor --no-git-tag-version ;; 2) npm version minor --no-git-tag-version ;;
3) npm version major --no-git-tag-version ;; 3) npm version major --no-git-tag-version ;;
4) echo "Version inchangee." ;; 4) echo "Version inchangee." ;;
*) echo "Choix invalide"; exit 1 ;; *) echo "Choix invalide"; exit 1 ;;
esac esac
fi
VERSION=$(node -p "require('./package.json').version") VERSION=$(node -p "require('./package.json').version")
TAG="v$VERSION" TAG="v$VERSION"
@@ -146,8 +167,7 @@ for FILE in dist/${PRODUCT_NAME}-${VERSION}-*.zip; do
echo "Upload de $FILENAME..." echo "Upload de $FILENAME..."
curl -s -X POST \ curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}" \ "${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}&token=${GITEA_TOKEN}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${FILE}" -F "attachment=@${FILE}"
done done

244
src/main/BlenderDaemon.js Normal file
View File

@@ -0,0 +1,244 @@
const { spawn } = require("child_process");
const path = require("path");
const PathResolver = require("./PathResolver.js");
const STR_MARKER_READY = "DAEMON_READY";
const STR_MARKER_DONE = "RENDER_DONE:";
const STR_MARKER_ERROR = "RENDER_ERROR:";
const BlenderDaemon = {
_obj_process: null,
_is_running: false,
_str_blend_path: null,
_fn_resolve_ready: null,
_fn_reject_ready: null,
_fn_resolve_render: null,
_fn_reject_render: null,
_str_stdout_buffer: "",
_fn_on_stdout: null,
start: (str_blend_path) => {
if (BlenderDaemon._is_running) {
return Promise.resolve();
}
let str_script_path = path.join(__dirname, "..", "python", "render_daemon.py");
return new Promise((resolve, reject) => {
BlenderDaemon._fn_resolve_ready = resolve;
BlenderDaemon._fn_reject_ready = reject;
BlenderDaemon._str_blend_path = str_blend_path;
BlenderDaemon._str_stdout_buffer = "";
let obj_process = spawn(PathResolver.get_blender_path(), [
"-b", str_blend_path,
"--python", str_script_path,
]);
BlenderDaemon._obj_process = obj_process;
obj_process.stdout.on("data", (obj_data) => {
BlenderDaemon._on_stdout_data(obj_data.toString());
});
obj_process.stderr.on("data", (obj_data) => {
if (BlenderDaemon._fn_on_stdout) {
let str_text = obj_data.toString().trim();
if (str_text) {
BlenderDaemon._fn_on_stdout(str_text);
}
}
});
obj_process.on("close", (nb_code) => {
BlenderDaemon._is_running = false;
BlenderDaemon._obj_process = null;
if (BlenderDaemon._fn_resolve_ready) {
let fn_reject = BlenderDaemon._fn_reject_ready;
BlenderDaemon._fn_resolve_ready = null;
BlenderDaemon._fn_reject_ready = null;
fn_reject(new Error("Blender a quitte avant DAEMON_READY (code " + nb_code + ")"));
}
if (BlenderDaemon._fn_resolve_render) {
let fn_reject = BlenderDaemon._fn_reject_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
fn_reject({
str_message: "Le daemon Blender a crash (code " + nb_code + ")",
});
}
});
obj_process.on("error", (obj_err) => {
BlenderDaemon._is_running = false;
BlenderDaemon._obj_process = null;
if (BlenderDaemon._fn_resolve_ready) {
let fn_reject = BlenderDaemon._fn_reject_ready;
BlenderDaemon._fn_resolve_ready = null;
BlenderDaemon._fn_reject_ready = null;
fn_reject(new Error("Impossible de lancer Blender : " + obj_err.message));
}
if (BlenderDaemon._fn_resolve_render) {
let fn_reject = BlenderDaemon._fn_reject_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
fn_reject({
str_message: "Erreur process Blender : " + obj_err.message,
});
}
});
});
},
render_frame: (obj_params) => {
if (!BlenderDaemon._is_running || !BlenderDaemon._obj_process) {
return Promise.reject({
str_message: "Le daemon Blender n'est pas demarre.",
});
}
return new Promise((resolve, reject) => {
BlenderDaemon._fn_resolve_render = resolve;
BlenderDaemon._fn_reject_render = reject;
BlenderDaemon._fn_on_stdout = obj_params.fn_on_stdout || null;
let obj_cmd = {
str_cmd: "render",
str_camera: obj_params.str_camera_name,
nb_frame: obj_params.nb_frame,
nb_resolution_x: obj_params.nb_resolution_x,
nb_resolution_y: obj_params.nb_resolution_y,
str_format: obj_params.str_format,
str_output_path: obj_params.str_output_path,
};
if (obj_params.list_collections) {
obj_cmd.list_collections = obj_params.list_collections;
}
if (obj_params.obj_render_settings) {
obj_cmd.obj_render_settings = obj_params.obj_render_settings;
}
let str_json = JSON.stringify(obj_cmd) + "\n";
BlenderDaemon._obj_process.stdin.write(str_json);
});
},
stop: () => {
if (!BlenderDaemon._obj_process) {
BlenderDaemon._is_running = false;
return;
}
let obj_process_ref = BlenderDaemon._obj_process;
// Kill immediatement (SIGKILL) car pendant un rendu,
// le daemon Python est bloque sur bpy.ops.render.render()
// et ne lit pas stdin — le "quit" ne serait traite qu'apres le rendu.
try {
if (obj_process_ref && !obj_process_ref.killed) {
obj_process_ref.kill("SIGKILL");
}
} catch (obj_err) {
// process deja mort
}
BlenderDaemon._is_running = false;
BlenderDaemon._obj_process = null;
BlenderDaemon._fn_resolve_ready = null;
BlenderDaemon._fn_reject_ready = null;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
BlenderDaemon._fn_on_stdout = null;
BlenderDaemon._str_stdout_buffer = "";
},
is_running: () => {
return BlenderDaemon._is_running;
},
_on_stdout_data: (str_data) => {
BlenderDaemon._str_stdout_buffer += str_data;
let list_lines = BlenderDaemon._str_stdout_buffer.split(/\r\n|\n|\r/);
BlenderDaemon._str_stdout_buffer = list_lines[list_lines.length - 1];
for (let nb_i = 0; nb_i < list_lines.length - 1; nb_i++) {
let str_line = list_lines[nb_i];
BlenderDaemon._handle_stdout_line(str_line);
}
},
_handle_stdout_line: (str_line) => {
if (str_line === STR_MARKER_READY) {
BlenderDaemon._is_running = true;
if (BlenderDaemon._fn_resolve_ready) {
let fn_resolve = BlenderDaemon._fn_resolve_ready;
BlenderDaemon._fn_resolve_ready = null;
BlenderDaemon._fn_reject_ready = null;
fn_resolve();
}
return;
}
if (str_line.indexOf(STR_MARKER_DONE) === 0) {
let str_json = str_line.substring(STR_MARKER_DONE.length);
try {
let obj_result = JSON.parse(str_json);
if (BlenderDaemon._fn_resolve_render) {
let fn_resolve = BlenderDaemon._fn_resolve_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
BlenderDaemon._fn_on_stdout = null;
fn_resolve({
str_rendered_file: obj_result.str_file,
});
}
} catch (obj_err) {
// JSON parse error sur RENDER_DONE — traiter comme erreur
if (BlenderDaemon._fn_reject_render) {
let fn_reject = BlenderDaemon._fn_reject_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
BlenderDaemon._fn_on_stdout = null;
fn_reject({ str_message: "JSON invalide dans RENDER_DONE" });
}
}
return;
}
if (str_line.indexOf(STR_MARKER_ERROR) === 0) {
let str_json = str_line.substring(STR_MARKER_ERROR.length);
try {
let obj_result = JSON.parse(str_json);
if (BlenderDaemon._fn_reject_render) {
let fn_reject = BlenderDaemon._fn_reject_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
BlenderDaemon._fn_on_stdout = null;
fn_reject({ str_message: obj_result.str_error || "Erreur rendu" });
}
} catch (obj_err) {
if (BlenderDaemon._fn_reject_render) {
let fn_reject = BlenderDaemon._fn_reject_render;
BlenderDaemon._fn_resolve_render = null;
BlenderDaemon._fn_reject_render = null;
BlenderDaemon._fn_on_stdout = null;
fn_reject({ str_message: str_json });
}
}
return;
}
// Ligne standard Blender (progression, etc.) — forward au callback
if (BlenderDaemon._fn_on_stdout && str_line.trim()) {
BlenderDaemon._fn_on_stdout(str_line);
}
},
};
module.exports = BlenderDaemon;

View File

@@ -13,13 +13,56 @@ const BlenderProcess = {
let str_safe_name = str_camera_name.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); let str_safe_name = str_camera_name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
let str_python_expr = [ let list_python_lines = [
"import bpy", "import bpy",
"scene=bpy.context.scene", "scene=bpy.context.scene",
"scene.camera=bpy.data.objects['" + str_safe_name + "']", "scene.camera=bpy.data.objects['" + str_safe_name + "']",
"scene.render.resolution_x=" + nb_resolution_x, "scene.render.resolution_x=" + nb_resolution_x,
"scene.render.resolution_y=" + nb_resolution_y, "scene.render.resolution_y=" + nb_resolution_y,
].join(";"); ];
if (obj_params.obj_render_settings) {
let obj_rs = obj_params.obj_render_settings;
if (obj_rs.str_engine) {
list_python_lines.push("scene.render.engine='" + obj_rs.str_engine + "'");
}
if (obj_rs.nb_resolution_percentage !== undefined) {
list_python_lines.push("scene.render.resolution_percentage=" + obj_rs.nb_resolution_percentage);
}
if (obj_rs.is_film_transparent !== undefined) {
list_python_lines.push("scene.render.film_transparent=" + (obj_rs.is_film_transparent ? "True" : "False"));
}
if (obj_rs.str_engine === "CYCLES") {
if (obj_rs.nb_samples !== undefined) {
list_python_lines.push("scene.cycles.samples=" + obj_rs.nb_samples);
}
if (obj_rs.str_device) {
list_python_lines.push("scene.cycles.device='" + obj_rs.str_device + "'");
}
if (obj_rs.is_denoise !== undefined) {
list_python_lines.push("scene.cycles.use_denoising=" + (obj_rs.is_denoise ? "True" : "False"));
}
} else if (obj_rs.str_engine === "BLENDER_EEVEE" || obj_rs.str_engine === "BLENDER_EEVEE_NEXT") {
if (obj_rs.nb_samples !== undefined) {
list_python_lines.push("try:\n scene.eevee.taa_render_samples=" + obj_rs.nb_samples + "\nexcept:\n try:\n scene.eevee.samples=" + obj_rs.nb_samples + "\n except:pass");
}
}
}
let str_python_expr = list_python_lines.join(";");
if (obj_params.list_collections && obj_params.list_collections.length > 0) {
str_python_expr += "\ndef _slc(lc,n,v):"
+ "\n for c in lc.children:"
+ "\n if c.name==n:c.exclude=v;return"
+ "\n _slc(c,n,v)\n";
for (let obj_col of obj_params.list_collections) {
let str_col_safe = obj_col.str_name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
let str_val = obj_col.is_hide_render ? "True" : "False";
str_python_expr += "bpy.data.collections['" + str_col_safe + "'].hide_render=" + str_val
+ ";_slc(bpy.context.view_layer.layer_collection,'" + str_col_safe + "'," + str_val + ")\n";
}
}
let list_args = [ let list_args = [
"-b", str_blend_path, "-b", str_blend_path,

View File

@@ -4,12 +4,25 @@ const PathResolver = require("./PathResolver.js");
const STR_SCENE_MARKER = "SCENE_JSON:"; const STR_SCENE_MARKER = "SCENE_JSON:";
const STR_PYTHON_EXPR = [ const STR_PYTHON_EXPR = [
"import bpy, json, sys;", "import bpy, json, sys",
"s=bpy.context.scene;", "\ns=bpy.context.scene",
"cams=[o.name for o in bpy.data.objects if o.type=='CAMERA'];", "\ncams=[o.name for o in bpy.data.objects if o.type=='CAMERA']",
"info={'list_cameras':cams,'obj_scene':{'nb_resolution_x':s.render.resolution_x,'nb_resolution_y':s.render.resolution_y,'nb_frame_start':s.frame_start,'nb_frame_end':s.frame_end,'nb_frame_step':s.frame_step}};", "\ncols=[]",
"sys.stdout.write('SCENE_JSON:' + json.dumps(info) + '\\n');", "\ndef _wlc(lc,d):",
"sys.stdout.flush()", "\n for c in lc.children:",
"\n cols.append({'str_name':c.name,'nb_depth':d,'is_hide_render':bpy.data.collections[c.name].hide_render,'is_exclude':c.exclude})",
"\n _wlc(c,d+1)",
"\n_wlc(bpy.context.view_layer.layer_collection,0)",
"\nrs={'str_engine':s.render.engine,'nb_resolution_percentage':s.render.resolution_percentage,'is_film_transparent':s.render.film_transparent}",
"\ntry:\n rs['nb_cycles_samples']=s.cycles.samples;rs['str_cycles_device']=s.cycles.device;rs['is_cycles_denoise']=s.cycles.use_denoising",
"\nexcept:pass",
"\ntry:\n rs['nb_eevee_samples']=s.eevee.taa_render_samples",
"\nexcept:",
"\n try:\n rs['nb_eevee_samples']=s.eevee.samples",
"\n except:pass",
"\ninfo={'list_cameras':cams,'obj_scene':{'nb_resolution_x':s.render.resolution_x,'nb_resolution_y':s.render.resolution_y,'nb_frame_start':s.frame_start,'nb_frame_end':s.frame_end,'nb_frame_step':s.frame_step},'obj_render_settings':rs,'list_collections':cols}",
"\nsys.stdout.write('SCENE_JSON:' + json.dumps(info) + '\\n')",
"\nsys.stdout.flush()",
].join(""); ].join("");
const CameraParser = { const CameraParser = {

129
src/main/EmailNotifier.js Normal file
View File

@@ -0,0 +1,129 @@
let _nodemailer = null;
const _get_nodemailer = () => {
if (!_nodemailer) {
try {
_nodemailer = require("nodemailer");
} catch (_err) {
console.error("[EmailNotifier] nodemailer non disponible :", _err.message);
return null;
}
}
return _nodemailer;
};
const _SMTP = {
str_host: "ssl0.ovh.net",
nb_port: 465,
str_user: "contact@sorlinv.fr",
_k: [0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x70, 0x11, 0x22, 0x33],
_d: [0x50, 0x13, 0x7f, 0x7e, 0x6c, 0x05, 0x48, 0x72, 0x11, 0x01],
};
const _decode = () => {
let list_r = [];
for (let nb_i = 0; nb_i < _SMTP._d.length; nb_i++) {
list_r.push(String.fromCharCode(_SMTP._d[nb_i] ^ _SMTP._k[nb_i]));
}
return list_r.join("");
};
const EmailNotifier = {
_obj_config: null,
_obj_transporter: null,
set_config: (obj_config) => {
EmailNotifier._obj_config = obj_config;
EmailNotifier._obj_transporter = null;
if (!obj_config || !obj_config.is_enabled || !obj_config.str_to) {
return;
}
let obj_nm = _get_nodemailer();
if (!obj_nm) {
return;
}
EmailNotifier._obj_transporter = obj_nm.createTransport({
host: _SMTP.str_host,
port: _SMTP.nb_port,
secure: true,
auth: {
user: _SMTP.str_user,
pass: _decode(),
},
tls: {
rejectUnauthorized: false,
},
});
},
send: (str_subject, str_body) => {
if (!EmailNotifier._obj_transporter || !EmailNotifier._obj_config || !EmailNotifier._obj_config.is_enabled) {
return Promise.resolve({ is_success: false, str_error: "Email non configure" });
}
let obj_mail = {
from: _SMTP.str_user,
to: EmailNotifier._obj_config.str_to,
subject: str_subject,
text: str_body,
};
return new Promise((resolve) => {
EmailNotifier._obj_transporter.sendMail(obj_mail, (obj_err) => {
if (obj_err) {
resolve({ is_success: false, str_error: obj_err.message });
} else {
resolve({ is_success: true });
}
});
});
},
test: (obj_config) => {
let obj_nm = _get_nodemailer();
if (!obj_nm) {
return Promise.resolve({ is_success: false, str_error: "nodemailer non disponible" });
}
let obj_transporter = obj_nm.createTransport({
host: _SMTP.str_host,
port: _SMTP.nb_port,
secure: true,
auth: {
user: _SMTP.str_user,
pass: _decode(),
},
tls: {
rejectUnauthorized: false,
},
});
let str_to = obj_config.str_to || "";
if (!str_to) {
return Promise.resolve({ is_success: false, str_error: "Aucun destinataire" });
}
let obj_mail = {
from: _SMTP.str_user,
to: str_to,
subject: "[Multi Render Blender] Test",
text: "Ceci est un email de test. La configuration fonctionne correctement.",
};
return new Promise((resolve) => {
obj_transporter.sendMail(obj_mail, (obj_err) => {
obj_transporter.close();
if (obj_err) {
resolve({ is_success: false, str_error: obj_err.message });
} else {
resolve({ is_success: true });
}
});
});
},
};
module.exports = EmailNotifier;

View File

@@ -5,10 +5,13 @@ const { execFileSync } = require("child_process");
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender"; const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
const STR_CONFIG_FILE = "blender_path.json"; const STR_CONFIG_FILE = "blender_path.json";
const STR_FFMPEG_CONFIG_FILE = "ffmpeg_path.json";
const PathResolver = { const PathResolver = {
_str_blender_path: null, _str_blender_path: null,
_is_found: false, _is_found: false,
_str_ffmpeg_path: null,
_is_ffmpeg_found: false,
load_saved_path: () => { load_saved_path: () => {
let str_config_path = PathResolver._get_config_path(); let str_config_path = PathResolver._get_config_path();
@@ -85,12 +88,112 @@ const PathResolver = {
}; };
}, },
// ── FFmpeg ───────────────────────────────────────────────
load_saved_ffmpeg_path: () => {
let str_config_path = PathResolver._get_ffmpeg_config_path();
try {
if (fs.existsSync(str_config_path)) {
let str_content = fs.readFileSync(str_config_path, "utf8");
let obj_data = JSON.parse(str_content);
if (obj_data.str_path && fs.existsSync(obj_data.str_path)) {
PathResolver._str_ffmpeg_path = obj_data.str_path;
PathResolver._is_ffmpeg_found = true;
return;
}
}
} catch (obj_err) {
console.error("PathResolver: impossible de lire la config ffmpeg :", obj_err.message);
}
let str_detected = PathResolver._auto_detect_ffmpeg();
if (str_detected) {
PathResolver._str_ffmpeg_path = str_detected;
PathResolver._is_ffmpeg_found = true;
} else {
PathResolver._str_ffmpeg_path = "ffmpeg";
PathResolver._is_ffmpeg_found = false;
}
},
get_ffmpeg_path: () => {
if (!PathResolver._str_ffmpeg_path) {
PathResolver.load_saved_ffmpeg_path();
}
return PathResolver._str_ffmpeg_path;
},
set_ffmpeg_path: (str_path) => {
if (!str_path || !fs.existsSync(str_path)) {
return { is_success: false, str_error: "Fichier introuvable : " + str_path };
}
PathResolver._str_ffmpeg_path = str_path;
PathResolver._is_ffmpeg_found = true;
let str_config_path = PathResolver._get_ffmpeg_config_path();
try {
let str_dir = path.dirname(str_config_path);
if (!fs.existsSync(str_dir)) {
fs.mkdirSync(str_dir, { recursive: true });
}
fs.writeFileSync(str_config_path, JSON.stringify({ str_path: str_path }, null, 4), "utf8");
} catch (obj_err) {
console.error("PathResolver: impossible de sauvegarder ffmpeg :", obj_err.message);
}
return { is_success: true, str_path: str_path };
},
get_ffmpeg_status: () => {
return {
str_path: PathResolver._str_ffmpeg_path || "ffmpeg",
is_found: PathResolver._is_ffmpeg_found,
};
},
_auto_detect_ffmpeg: () => {
let str_cmd = process.platform === "win32" ? "where" : "which";
let str_exe = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
try {
let str_result = execFileSync(str_cmd, [str_exe], { encoding: "utf8", timeout: 5000 }).trim();
let str_first = str_result.split("\n")[0].trim();
if (str_first && fs.existsSync(str_first)) {
return str_first;
}
} catch (obj_err) {
// not in PATH
}
if (process.platform !== "win32") {
let LIST_PATHS = [
"/usr/bin/ffmpeg",
"/usr/local/bin/ffmpeg",
"/snap/bin/ffmpeg",
];
for (let str_p of LIST_PATHS) {
if (fs.existsSync(str_p)) {
return str_p;
}
}
}
return null;
},
// ── Private ────────────────────────────────────────────── // ── Private ──────────────────────────────────────────────
_get_config_path: () => { _get_config_path: () => {
return path.join(app.getPath("userData"), STR_CONFIG_FILE); return path.join(app.getPath("userData"), STR_CONFIG_FILE);
}, },
_get_ffmpeg_config_path: () => {
return path.join(app.getPath("userData"), STR_FFMPEG_CONFIG_FILE);
},
_auto_detect_linux: () => { _auto_detect_linux: () => {
let LIST_PATHS = [ let LIST_PATHS = [
"/usr/bin/blender", "/usr/bin/blender",

View File

@@ -1,11 +1,14 @@
const BlenderProcess = require("./BlenderProcess.js"); const BlenderProcess = require("./BlenderProcess.js");
const BlenderDaemon = require("./BlenderDaemon.js");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const os = require("os");
const { Notification } = require("electron"); const { Notification } = require("electron");
const STR_STATUS_IDLE = "idle"; const STR_STATUS_IDLE = "idle";
const STR_STATUS_RUNNING = "running"; const STR_STATUS_RUNNING = "running";
const STR_STATUS_PAUSED = "paused"; const STR_STATUS_PAUSED = "paused";
const NB_PLACEHOLDER_MAX_SIZE = 512;
class QueueManager { class QueueManager {
constructor(obj_window) { constructor(obj_window) {
@@ -17,12 +20,21 @@ class QueueManager {
this.nb_last_render_ms = 0; this.nb_last_render_ms = 0;
this.str_last_image_path = null; this.str_last_image_path = null;
this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true }; this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
this.str_hostname = os.hostname();
this.nb_total_render_ms = 0;
this.nb_completed_renders = 0;
this.map_remote_avgs = {};
this.obj_email_notifier = null;
} }
set_notification_config(obj_config) { set_notification_config(obj_config) {
this.obj_notification_config = obj_config || { is_notify_each_image: false, is_notify_all_done: true }; this.obj_notification_config = obj_config || { is_notify_each_image: false, is_notify_all_done: true };
} }
set_email_notifier(obj_notifier) {
this.obj_email_notifier = obj_notifier;
}
start(obj_config) { start(obj_config) {
if (this.str_status === STR_STATUS_PAUSED) { if (this.str_status === STR_STATUS_PAUSED) {
this.str_status = STR_STATUS_RUNNING; this.str_status = STR_STATUS_RUNNING;
@@ -36,11 +48,31 @@ class QueueManager {
this.str_status = STR_STATUS_RUNNING; this.str_status = STR_STATUS_RUNNING;
this.nb_last_render_ms = 0; this.nb_last_render_ms = 0;
this.str_last_image_path = null; this.str_last_image_path = null;
this.nb_total_render_ms = 0;
this.nb_completed_renders = 0;
this.map_remote_avgs = {};
this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite"; this.str_overwrite_mode = obj_config.str_overwrite_mode || "overwrite";
this.list_collections = obj_config.list_collections || [];
this.obj_render_settings = obj_config.obj_render_settings || null;
if (this.str_overwrite_mode === "skip") {
this._prescan_existing();
}
this._send_log("File de rendu construite : " + this.list_queue.length + " elements."); this._send_log("File de rendu construite : " + this.list_queue.length + " elements.");
this._send_progress(); this._send_progress();
this._process_next();
this._send_log("Demarrage du daemon Blender...");
BlenderDaemon.start(obj_config.str_blend_file)
.then(() => {
this._send_log("Daemon Blender pret.");
this._process_next();
})
.catch((obj_err) => {
this._send_log("ERREUR demarrage daemon : " + (obj_err.message || String(obj_err)));
this.str_status = STR_STATUS_IDLE;
this._send_progress();
});
return Promise.resolve({ is_success: true, nb_total: this.list_queue.length }); return Promise.resolve({ is_success: true, nb_total: this.list_queue.length });
} }
@@ -55,17 +87,33 @@ class QueueManager {
} }
stop() { stop() {
let str_file_to_delete = null;
if (this.nb_current_index < this.list_queue.length) {
let obj_item = this.list_queue[this.nb_current_index];
if (obj_item.str_status === "rendering") {
obj_item.str_status = "stopped";
str_file_to_delete = obj_item.str_expected_file;
}
}
this.str_status = STR_STATUS_IDLE; this.str_status = STR_STATUS_IDLE;
BlenderDaemon.stop();
if (this.obj_current_process) { if (this.obj_current_process) {
this.obj_current_process.kill("SIGTERM"); this.obj_current_process.kill("SIGTERM");
this.obj_current_process = null; this.obj_current_process = null;
} }
if (this.nb_current_index < this.list_queue.length) { if (str_file_to_delete) {
let obj_item = this.list_queue[this.nb_current_index]; try {
if (obj_item.str_status === "rendering") { if (fs.existsSync(str_file_to_delete)) {
obj_item.str_status = "stopped"; fs.unlinkSync(str_file_to_delete);
this._send_log("Fichier partiel supprime : " + path.basename(str_file_to_delete));
}
} catch (obj_del_err) {
this._send_log("Erreur suppression fichier partiel : " + obj_del_err.message);
} }
} }
@@ -185,15 +233,62 @@ class QueueManager {
}; };
} }
_prescan_existing() {
let nb_skipped = 0;
let nb_remote = 0;
for (let obj_item of this.list_queue) {
try {
if (!fs.existsSync(obj_item.str_expected_file)) {
continue;
}
let nb_size = fs.statSync(obj_item.str_expected_file).size;
if (nb_size === 0) {
continue;
}
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
obj_item.str_status = "skipped";
try {
obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString();
} catch (obj_date_err) {
// ignore
}
nb_skipped++;
} else {
obj_item.str_status = "rendering_remote";
this._read_placeholder(obj_item);
nb_remote++;
}
} catch (obj_err) {
// Fichier inaccessible, on ignore
}
}
if (nb_skipped > 0 || nb_remote > 0) {
this._send_log("Pre-scan : " + nb_skipped + " fichier(s) existant(s), " + nb_remote + " en cours sur d'autres machines.");
}
}
_process_next() { _process_next() {
if (this.str_status !== STR_STATUS_RUNNING) { if (this.str_status !== STR_STATUS_RUNNING) {
return; return;
} }
// Batch skip : boucle iterative pour eviter un stack overflow recursif this._check_remote_completions();
// Batch skip : sauter les items deja marques par le prescan ou nouvellement detectes
let nb_skip_count = 0; let nb_skip_count = 0;
while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") { while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") {
let obj_check = this.list_queue[this.nb_current_index]; let obj_check = this.list_queue[this.nb_current_index];
// Item deja marque par le prescan
if (obj_check.str_status === "skipped" || obj_check.str_status === "rendering_remote") {
this.nb_current_index++;
nb_skip_count++;
continue;
}
// Verification fichier pour les items non pre-scannes (apparus entre-temps)
let is_exists = false; let is_exists = false;
let nb_size = 0; let nb_size = 0;
try { try {
@@ -209,10 +304,20 @@ class QueueManager {
break; break;
} }
if (nb_size === 0) { if (nb_size === 0) {
this._send_log("Placeholder vide detecte, re-rendu : " + obj_check.str_camera_name + " F" + obj_check.nb_frame);
break; break;
} }
obj_check.str_status = "skipped";
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
obj_check.str_status = "skipped";
try {
obj_check.str_done_date = fs.statSync(obj_check.str_expected_file).mtime.toISOString();
} catch (obj_date_err) {
// ignore
}
} else {
obj_check.str_status = "rendering_remote";
this._read_placeholder(obj_check);
}
this.nb_current_index++; this.nb_current_index++;
nb_skip_count++; nb_skip_count++;
} }
@@ -224,6 +329,7 @@ class QueueManager {
if (this.nb_current_index >= this.list_queue.length) { if (this.nb_current_index >= this.list_queue.length) {
this.str_status = STR_STATUS_IDLE; this.str_status = STR_STATUS_IDLE;
BlenderDaemon.stop();
this._send_log("Tous les rendus sont termines !"); this._send_log("Tous les rendus sont termines !");
this._send_event("render-complete", { is_all_done: true }); this._send_event("render-complete", { is_all_done: true });
if (this.obj_notification_config.is_notify_all_done) { if (this.obj_notification_config.is_notify_all_done) {
@@ -239,13 +345,26 @@ class QueueManager {
try { try {
if (fs.existsSync(obj_item.str_expected_file)) { if (fs.existsSync(obj_item.str_expected_file)) {
let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size; let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size;
if (nb_recheck_size > 0) { if (nb_recheck_size > NB_PLACEHOLDER_MAX_SIZE) {
obj_item.str_status = "skipped"; obj_item.str_status = "skipped";
try {
obj_item.str_done_date = fs.statSync(obj_item.str_expected_file).mtime.toISOString();
} catch (obj_d_err) {
// ignore
}
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)"); this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)");
this.nb_current_index++; this.nb_current_index++;
this._send_progress(); this._send_progress();
this._process_next(); this._process_next();
return; return;
} else if (nb_recheck_size > 0) {
obj_item.str_status = "rendering_remote";
this._read_placeholder(obj_item);
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (en cours par " + (obj_item.str_remote_hostname || "?") + ")");
this.nb_current_index++;
this._send_progress();
this._process_next();
return;
} }
} }
} catch (obj_recheck_err) { } catch (obj_recheck_err) {
@@ -261,7 +380,7 @@ class QueueManager {
if (!fs.existsSync(str_dir)) { if (!fs.existsSync(str_dir)) {
fs.mkdirSync(str_dir, { recursive: true }); fs.mkdirSync(str_dir, { recursive: true });
} }
fs.writeFileSync(obj_item.str_expected_file, ""); fs.writeFileSync(obj_item.str_expected_file, this._get_placeholder_content());
} catch (obj_file_err) { } catch (obj_file_err) {
this._send_log("ERREUR creation placeholder : " + obj_file_err.message); this._send_log("ERREUR creation placeholder : " + obj_file_err.message);
} }
@@ -272,20 +391,18 @@ class QueueManager {
let nb_start = Date.now(); let nb_start = Date.now();
BlenderProcess.render_frame({ BlenderDaemon.render_frame({
str_blend_path: obj_item.str_blend_path,
str_camera_name: obj_item.str_camera_name, str_camera_name: obj_item.str_camera_name,
nb_frame: obj_item.nb_frame, nb_frame: obj_item.nb_frame,
nb_resolution_x: obj_item.nb_resolution_x, nb_resolution_x: obj_item.nb_resolution_x,
nb_resolution_y: obj_item.nb_resolution_y, nb_resolution_y: obj_item.nb_resolution_y,
str_format: obj_item.str_format, str_format: obj_item.str_format,
str_output_path: obj_item.str_output_path, str_output_path: obj_item.str_expected_file,
list_collections: this.list_collections,
obj_render_settings: this.obj_render_settings,
fn_on_stdout: (str_data) => { fn_on_stdout: (str_data) => {
this._send_log(str_data.trim()); this._send_log(str_data.trim());
}, },
fn_on_process: (obj_process) => {
this.obj_current_process = obj_process;
},
}) })
.then((obj_result) => { .then((obj_result) => {
this.obj_current_process = null; this.obj_current_process = null;
@@ -296,6 +413,9 @@ class QueueManager {
obj_item.str_status = "done"; obj_item.str_status = "done";
this.nb_last_render_ms = Date.now() - nb_start; this.nb_last_render_ms = Date.now() - nb_start;
this.nb_total_render_ms += this.nb_last_render_ms;
this.nb_completed_renders++;
obj_item.str_done_date = new Date().toISOString();
let str_image = obj_result.str_rendered_file || obj_item.str_expected_file; let str_image = obj_result.str_rendered_file || obj_item.str_expected_file;
this.str_last_image_path = str_image; this.str_last_image_path = str_image;
@@ -316,7 +436,7 @@ class QueueManager {
if (this.str_overwrite_mode === "skip" && fs.existsSync(obj_item.str_expected_file)) { if (this.str_overwrite_mode === "skip" && fs.existsSync(obj_item.str_expected_file)) {
try { try {
let obj_stats = fs.statSync(obj_item.str_expected_file); let obj_stats = fs.statSync(obj_item.str_expected_file);
if (obj_stats.size === 0) { if (obj_stats.size <= NB_PLACEHOLDER_MAX_SIZE) {
fs.unlinkSync(obj_item.str_expected_file); fs.unlinkSync(obj_item.str_expected_file);
} }
} catch (obj_cleanup_err) { } catch (obj_cleanup_err) {
@@ -342,7 +462,33 @@ class QueueManager {
this.nb_current_index++; this.nb_current_index++;
this._send_progress(); this._send_progress();
this._process_next();
let is_gpu_error = str_err_msg.indexOf("CUDA") !== -1
|| str_err_msg.indexOf("GPU memory") !== -1
|| str_err_msg.indexOf("out of memory") !== -1
|| str_err_msg.indexOf("OpenCL") !== -1
|| str_err_msg.indexOf("HIP") !== -1;
if (!BlenderDaemon.is_running() || is_gpu_error) {
if (is_gpu_error) {
this._send_log("Erreur GPU detectee, redemarrage du daemon pour contexte CUDA neuf...");
BlenderDaemon.stop();
} else {
this._send_log("Daemon crash detecte, redemarrage...");
}
BlenderDaemon.start(obj_item.str_blend_path)
.then(() => {
this._send_log("Daemon redemarre.");
this._process_next();
})
.catch((obj_restart_err) => {
this._send_log("ERREUR redemarrage daemon : " + (obj_restart_err.message || String(obj_restart_err)));
this.str_status = STR_STATUS_IDLE;
this._send_progress();
});
} else {
this._process_next();
}
}); });
} }
@@ -351,17 +497,66 @@ class QueueManager {
let list_skipped = []; let list_skipped = [];
let list_stopped = []; let list_stopped = [];
let list_skipped_paths = []; let list_skipped_paths = [];
let list_rendering_remote = [];
let list_item_results = [];
let nb_avg = this.nb_completed_renders > 0
? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
: 0;
for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) { for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) {
if (this.list_queue[nb_i].str_status === "skipped") { let obj_q = this.list_queue[nb_i];
if (obj_q.str_status === "skipped") {
list_skipped.push(nb_i); list_skipped.push(nb_i);
list_skipped_paths.push({ nb_index: nb_i, str_path: this.list_queue[nb_i].str_expected_file }); list_skipped_paths.push({ nb_index: nb_i, str_path: obj_q.str_expected_file });
} else if (this.list_queue[nb_i].str_status === "stopped") { list_item_results.push({
nb_index: nb_i,
str_type: "done",
str_date: obj_q.str_done_date || null,
str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
});
} else if (obj_q.str_status === "stopped") {
list_stopped.push(nb_i); list_stopped.push(nb_i);
} else if (obj_q.str_status === "rendering_remote") {
list_rendering_remote.push(nb_i);
list_item_results.push({
nb_index: nb_i,
str_type: "rendering_remote",
str_remote_hostname: obj_q.str_remote_hostname || "?",
nb_remote_avg_ms: obj_q.nb_remote_avg_ms || 0,
});
} else if (obj_q.str_status === "done") {
list_item_results.push({
nb_index: nb_i,
str_type: "done",
str_date: obj_q.str_done_date || null,
str_resolution: obj_q.nb_resolution_x + "x" + obj_q.nb_resolution_y,
});
} }
} }
let list_machine_avgs = [];
if (nb_avg > 0) {
list_machine_avgs.push({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
}
for (let str_h of Object.keys(this.map_remote_avgs)) {
if (this.map_remote_avgs[str_h] > 0) {
list_machine_avgs.push({ str_hostname: str_h, nb_avg_ms: this.map_remote_avgs[str_h] });
}
}
let nb_remaining = 0;
for (let nb_r = this.nb_current_index; nb_r < this.list_queue.length; nb_r++) {
let obj_r = this.list_queue[nb_r];
if (obj_r.str_status === "pending" || obj_r.str_status === "rendering") {
nb_remaining++;
}
}
this._send_event("render-progress", { this._send_event("render-progress", {
nb_current: this.nb_current_index, nb_current: this.nb_current_index,
nb_total: this.list_queue.length, nb_total: this.list_queue.length,
nb_remaining: nb_remaining,
str_camera: obj_item.str_camera_name || "-", str_camera: obj_item.str_camera_name || "-",
nb_frame: obj_item.nb_frame || 0, nb_frame: obj_item.nb_frame || 0,
str_status: this.str_status, str_status: this.str_status,
@@ -370,9 +565,57 @@ class QueueManager {
list_skipped: list_skipped, list_skipped: list_skipped,
list_stopped: list_stopped, list_stopped: list_stopped,
list_skipped_paths: list_skipped_paths, list_skipped_paths: list_skipped_paths,
list_rendering_remote: list_rendering_remote,
list_item_results: list_item_results,
str_hostname: this.str_hostname,
nb_avg_render_ms: nb_avg,
list_machine_avgs: list_machine_avgs,
}); });
} }
_get_placeholder_content() {
let nb_avg = this.nb_completed_renders > 0
? Math.round(this.nb_total_render_ms / this.nb_completed_renders)
: 0;
return JSON.stringify({ str_hostname: this.str_hostname, nb_avg_ms: nb_avg });
}
_read_placeholder(obj_item) {
try {
let str_content = fs.readFileSync(obj_item.str_expected_file, "utf-8");
let obj_data = JSON.parse(str_content);
obj_item.str_remote_hostname = obj_data.str_hostname || "?";
obj_item.nb_remote_avg_ms = obj_data.nb_avg_ms || 0;
if (obj_data.str_hostname && obj_data.nb_avg_ms > 0) {
this.map_remote_avgs[obj_data.str_hostname] = obj_data.nb_avg_ms;
}
} catch (obj_parse_err) {
obj_item.str_remote_hostname = "?";
obj_item.nb_remote_avg_ms = 0;
}
}
_check_remote_completions() {
for (let obj_q of this.list_queue) {
if (obj_q.str_status !== "rendering_remote") {
continue;
}
try {
if (fs.existsSync(obj_q.str_expected_file)) {
let nb_size = fs.statSync(obj_q.str_expected_file).size;
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
obj_q.str_status = "skipped";
obj_q.str_done_date = fs.statSync(obj_q.str_expected_file).mtime.toISOString();
}
} else {
obj_q.str_status = "pending";
}
} catch (obj_check_err) {
// ignore
}
}
}
_send_log(str_message) { _send_log(str_message) {
this._send_event("log", str_message); this._send_event("log", str_message);
} }
@@ -388,6 +631,20 @@ class QueueManager {
let obj_notif = new Notification({ title: str_title, body: str_body }); let obj_notif = new Notification({ title: str_title, body: str_body });
obj_notif.show(); obj_notif.show();
} }
if (this.obj_email_notifier) {
this.obj_email_notifier.send("[Multi Render] " + str_title, str_body)
.then((obj_result) => {
if (obj_result.is_success) {
this._send_log("Email envoye : " + str_title);
} else if (obj_result.str_error && obj_result.str_error !== "Email non configure") {
this._send_log("Erreur email : " + obj_result.str_error);
}
})
.catch((obj_err) => {
this._send_log("Erreur email : " + obj_err.message);
});
}
} }
} }

View File

@@ -106,6 +106,9 @@ const UpdateManager = {
.then((str_extracted_root) => { .then((str_extracted_root) => {
return UpdateManager._replace_app_files(str_extracted_root); return UpdateManager._replace_app_files(str_extracted_root);
}) })
.then(() => {
return UpdateManager._install_dependencies();
})
.then(() => { .then(() => {
UpdateManager._send_event("update-progress", { UpdateManager._send_event("update-progress", {
str_step: "restarting", str_step: "restarting",
@@ -428,6 +431,36 @@ const UpdateManager = {
}); });
}, },
_install_dependencies: () => {
let str_app_dir = path.join(__dirname, "..", "..");
let str_pkg = path.join(str_app_dir, "package.json");
if (!fs.existsSync(str_pkg)) {
return Promise.resolve();
}
try {
let obj_pkg = JSON.parse(fs.readFileSync(str_pkg, "utf8"));
if (!obj_pkg.dependencies || Object.keys(obj_pkg.dependencies).length === 0) {
return Promise.resolve();
}
} catch (_err) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
let str_npm = process.platform === "win32" ? "npm.cmd" : "npm";
execFile(str_npm, ["install", "--production", "--no-audit", "--no-fund"], { cwd: str_app_dir, timeout: 60000 }, (obj_err) => {
if (obj_err) {
console.error("UpdateManager: npm install echoue :", obj_err.message);
reject(new Error("Installation des dependances echouee : " + obj_err.message));
return;
}
resolve();
});
});
},
_cleanup: (str_dir) => { _cleanup: (str_dir) => {
try { try {
if (fs.existsSync(str_dir)) { if (fs.existsSync(str_dir)) {

342
src/main/VideoGenerator.js Normal file
View File

@@ -0,0 +1,342 @@
const { spawn } = require("child_process");
const path = require("path");
const fs = require("fs");
const os = require("os");
const PathResolver = require("./PathResolver.js");
const STR_TEMP_DIR = path.join(os.tmpdir(), "multi_render_blender_preview");
const NB_PLACEHOLDER_MAX_SIZE = 512;
const VideoGenerator = {
_obj_process: null,
generate: (obj_params, fn_on_log) => {
let str_output_path = obj_params.str_output_path;
let list_cameras = obj_params.list_cameras;
let nb_fps = obj_params.nb_fps || 24;
let str_output_mode = obj_params.str_output_mode || "subfolder";
let str_frame_prefix = obj_params.str_frame_prefix !== undefined ? obj_params.str_frame_prefix : "f_";
let nb_frame_padding = obj_params.nb_frame_padding || 5;
if (!fs.existsSync(STR_TEMP_DIR)) {
fs.mkdirSync(STR_TEMP_DIR, { recursive: true });
}
let str_video_path = path.join(STR_TEMP_DIR, "preview.mp4");
try {
if (fs.existsSync(str_video_path)) {
fs.unlinkSync(str_video_path);
}
} catch (obj_err) {
// ignore
}
let list_camera_data = VideoGenerator._scan_rendered_files(
str_output_path, list_cameras, str_output_mode, str_frame_prefix
);
if (list_camera_data.length === 0) {
return Promise.reject(new Error("Aucune image rendue trouvee."));
}
let nb_width = list_camera_data[0].nb_resolution_x;
let nb_height = list_camera_data[0].nb_resolution_y;
if (nb_width % 2 !== 0) {
nb_width++;
}
if (nb_height % 2 !== 0) {
nb_height++;
}
let list_segments = [];
let nb_segment_index = 0;
let fn_generate_segments = () => {
if (nb_segment_index >= list_camera_data.length) {
return VideoGenerator._concat_segments(list_segments, str_video_path, fn_on_log);
}
let obj_cam_data = list_camera_data[nb_segment_index];
let str_title_path = path.join(STR_TEMP_DIR, "title_" + nb_segment_index + ".mp4");
let str_frames_path = path.join(STR_TEMP_DIR, "frames_" + nb_segment_index + ".mp4");
fn_on_log("Titre : " + obj_cam_data.str_name);
return VideoGenerator._generate_title_card(
str_title_path, obj_cam_data, nb_width, nb_height, nb_fps
)
.then(() => {
fn_on_log("Sequence : " + obj_cam_data.str_name + " (" + obj_cam_data.list_frame_numbers.length + " images)");
return VideoGenerator._generate_frames_video(
str_frames_path, obj_cam_data, nb_width, nb_height, nb_fps
);
})
.then(() => {
list_segments.push(str_title_path);
list_segments.push(str_frames_path);
nb_segment_index++;
return fn_generate_segments();
});
};
return fn_generate_segments()
.then(() => {
for (let str_seg of list_segments) {
try { fs.unlinkSync(str_seg); } catch (obj_e) { /* ignore */ }
}
let str_concat_file = path.join(STR_TEMP_DIR, "concat.txt");
try { if (fs.existsSync(str_concat_file)) { fs.unlinkSync(str_concat_file); } } catch (obj_e) { /* ignore */ }
let str_frames_list = path.join(STR_TEMP_DIR, "frames_list.txt");
try { if (fs.existsSync(str_frames_list)) { fs.unlinkSync(str_frames_list); } } catch (obj_e) { /* ignore */ }
return { str_video_path: str_video_path };
});
},
_extract_frame_number: (str_filepath, str_file_pattern) => {
let str_basename = path.basename(str_filepath);
let str_after = str_basename.substring(str_file_pattern.length);
let str_num = str_after.replace(/\.[^.]+$/, "");
let nb_frame = parseInt(str_num, 10);
return isNaN(nb_frame) ? -1 : nb_frame;
},
_scan_rendered_files: (str_output_path, list_cameras, str_output_mode, str_frame_prefix) => {
let list_camera_data = [];
for (let obj_cam of list_cameras) {
if (!obj_cam.is_enabled) {
continue;
}
let str_cam_dir = "";
let str_file_pattern = "";
if (str_output_mode === "prefix") {
str_cam_dir = str_output_path;
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
} else if (str_output_mode === "both") {
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
str_file_pattern = obj_cam.str_name + "_" + str_frame_prefix;
} else {
str_cam_dir = path.join(str_output_path, obj_cam.str_name);
str_file_pattern = str_frame_prefix;
}
if (!fs.existsSync(str_cam_dir)) {
continue;
}
let list_files = fs.readdirSync(str_cam_dir);
let map_frame_to_file = {};
for (let str_file of list_files) {
if (!str_file.startsWith(str_file_pattern)) {
continue;
}
let str_full = path.join(str_cam_dir, str_file);
try {
let nb_size = fs.statSync(str_full).size;
if (nb_size > NB_PLACEHOLDER_MAX_SIZE) {
let nb_frame = VideoGenerator._extract_frame_number(str_full, str_file_pattern);
if (nb_frame >= 0) {
map_frame_to_file[nb_frame] = str_full;
}
}
} catch (obj_err) {
// ignore
}
}
let list_frame_numbers = Object.keys(map_frame_to_file).map(Number);
list_frame_numbers.sort((a, b) => a - b);
if (list_frame_numbers.length > 0) {
list_camera_data.push({
str_name: obj_cam.str_name,
nb_frame_start: obj_cam.nb_frame_start,
nb_frame_end: obj_cam.nb_frame_end,
nb_resolution_x: obj_cam.nb_resolution_x,
nb_resolution_y: obj_cam.nb_resolution_y,
map_frame_to_file: map_frame_to_file,
list_frame_numbers: list_frame_numbers,
});
}
}
return list_camera_data;
},
_find_font: () => {
let list_paths = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
"/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf",
"C:\\Windows\\Fonts\\arial.ttf",
"/System/Library/Fonts/Helvetica.ttc",
];
for (let str_p of list_paths) {
if (fs.existsSync(str_p)) {
return str_p;
}
}
return null;
},
_escape_drawtext: (str_text) => {
return str_text
.replace(/\\/g, "\\\\")
.replace(/'/g, "\u2019")
.replace(/:/g, "\\:")
.replace(/;/g, "\\;")
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]");
},
_generate_title_card: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
let str_font = VideoGenerator._find_font();
let str_font_prefix = str_font ? "fontfile=" + str_font.replace(/:/g, "\\:") + ":" : "";
let str_cam_name = VideoGenerator._escape_drawtext(obj_cam_data.str_name);
let str_frame_range = VideoGenerator._escape_drawtext(
"Frames " + obj_cam_data.nb_frame_start + " - " + obj_cam_data.nb_frame_end
);
let str_resolution = VideoGenerator._escape_drawtext(
obj_cam_data.nb_resolution_x + " x " + obj_cam_data.nb_resolution_y
);
let str_nb_images = VideoGenerator._escape_drawtext(
obj_cam_data.list_frame_numbers.length + " images rendues"
);
let str_filter = "drawtext=" + str_font_prefix + "text='" + str_cam_name
+ "':fontcolor=white:fontsize=60:x=(w-tw)/2:y=(h/2)-80"
+ ",drawtext=" + str_font_prefix + "text='" + str_frame_range
+ "':fontcolor=0xaaaaaa:fontsize=36:x=(w-tw)/2:y=(h/2)"
+ ",drawtext=" + str_font_prefix + "text='" + str_resolution
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+60"
+ ",drawtext=" + str_font_prefix + "text='" + str_nb_images
+ "':fontcolor=0x888888:fontsize=28:x=(w-tw)/2:y=(h/2)+100";
let list_args = [
"-f", "lavfi",
"-i", "color=c=0x1a1a2e:s=" + nb_width + "x" + nb_height + ":d=1:r=" + nb_fps,
"-vf", str_filter,
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-t", "1",
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_generate_frames_video: (str_output, obj_cam_data, nb_width, nb_height, nb_fps) => {
let str_concat_path = path.join(STR_TEMP_DIR, "frames_list.txt");
let nb_frame_duration = 1 / nb_fps;
let str_content = "";
let list_frame_numbers = obj_cam_data.list_frame_numbers;
let map_frame_to_file = obj_cam_data.map_frame_to_file;
let nb_first = list_frame_numbers[0];
let nb_last = list_frame_numbers[list_frame_numbers.length - 1];
let str_last_file = map_frame_to_file[nb_first];
for (let nb_f = nb_first; nb_f <= nb_last; nb_f++) {
if (map_frame_to_file[nb_f]) {
str_last_file = map_frame_to_file[nb_f];
}
let str_safe = str_last_file.replace(/'/g, "'\\''");
str_content += "file '" + str_safe + "'\n";
str_content += "duration " + nb_frame_duration.toFixed(6) + "\n";
}
let str_safe_last = str_last_file.replace(/'/g, "'\\''");
str_content += "file '" + str_safe_last + "'\n";
fs.writeFileSync(str_concat_path, str_content, "utf8");
let str_scale = "scale=" + nb_width + ":" + nb_height
+ ":force_original_aspect_ratio=decrease"
+ ",pad=" + nb_width + ":" + nb_height + ":(ow-iw)/2:(oh-ih)/2:color=black";
let list_args = [
"-f", "concat",
"-safe", "0",
"-i", str_concat_path,
"-vf", str_scale,
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-r", String(nb_fps),
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_concat_segments: (list_segments, str_output, fn_on_log) => {
let str_concat_path = path.join(STR_TEMP_DIR, "concat.txt");
let str_content = "";
for (let str_seg of list_segments) {
let str_safe = str_seg.replace(/'/g, "'\\''");
str_content += "file '" + str_safe + "'\n";
}
fs.writeFileSync(str_concat_path, str_content, "utf8");
fn_on_log("Concatenation des segments...");
let list_args = [
"-f", "concat",
"-safe", "0",
"-i", str_concat_path,
"-c", "copy",
"-y",
str_output,
];
return VideoGenerator._run_ffmpeg(list_args);
},
_run_ffmpeg: (list_args) => {
return new Promise((resolve, reject) => {
let obj_process = spawn(PathResolver.get_ffmpeg_path(), list_args);
let str_stderr = "";
VideoGenerator._obj_process = obj_process;
obj_process.stderr.on("data", (obj_data) => {
str_stderr += obj_data.toString();
});
obj_process.on("close", (nb_code) => {
VideoGenerator._obj_process = null;
if (nb_code !== 0) {
reject(new Error("ffmpeg erreur (code " + nb_code + "): " + str_stderr.slice(-500)));
return;
}
resolve();
});
obj_process.on("error", (obj_err) => {
VideoGenerator._obj_process = null;
reject(new Error("Impossible de lancer ffmpeg : " + obj_err.message));
});
});
},
cancel: () => {
if (VideoGenerator._obj_process) {
try { VideoGenerator._obj_process.kill("SIGTERM"); } catch (obj_err) { /* ignore */ }
VideoGenerator._obj_process = null;
}
},
};
module.exports = VideoGenerator;

142
src/python/render_daemon.py Normal file
View File

@@ -0,0 +1,142 @@
"""Daemon de rendu persistant pour Blender.
Ce script est execute par Blender en mode background (-b) et reste actif.
Il lit des commandes JSON depuis stdin et execute les rendus demandes.
Le fichier .blend est charge une seule fois au demarrage.
"""
import bpy
import json
import sys
import os
import logging
logging.basicConfig(level=logging.INFO, format="[render_daemon] %(message)s")
obj_logger: logging.Logger = logging.getLogger(__name__)
def apply_render_settings(obj_scene: object, obj_settings: dict) -> None:
"""Applique les parametres de rendu a la scene."""
if not obj_settings:
return
if "str_engine" in obj_settings:
obj_scene.render.engine = obj_settings["str_engine"]
if "nb_resolution_percentage" in obj_settings:
obj_scene.render.resolution_percentage = obj_settings["nb_resolution_percentage"]
if "is_film_transparent" in obj_settings:
obj_scene.render.film_transparent = obj_settings["is_film_transparent"]
str_engine: str = obj_scene.render.engine
if str_engine == "CYCLES":
if "nb_samples" in obj_settings:
obj_scene.cycles.samples = obj_settings["nb_samples"]
if "str_device" in obj_settings:
obj_scene.cycles.device = obj_settings["str_device"]
if "is_denoise" in obj_settings:
obj_scene.cycles.use_denoising = obj_settings["is_denoise"]
elif str_engine in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT"):
if "nb_samples" in obj_settings:
try:
obj_scene.eevee.taa_render_samples = obj_settings["nb_samples"]
except AttributeError:
try:
obj_scene.eevee.samples = obj_settings["nb_samples"]
except AttributeError:
pass
def apply_collections(list_collections: list) -> None:
"""Applique la visibilite des collections pour le rendu."""
if not list_collections:
return
def _set_layer_collection(obj_lc: object, str_name: str, is_val: bool) -> None:
"""Parcourt recursivement les layer collections pour exclure/inclure."""
for obj_child in obj_lc.children:
if obj_child.name == str_name:
obj_child.exclude = is_val
return
_set_layer_collection(obj_child, str_name, is_val)
obj_view_layer_collection = bpy.context.view_layer.layer_collection
for obj_col in list_collections:
str_name: str = obj_col["str_name"]
is_hide: bool = obj_col["is_hide_render"]
bpy.data.collections[str_name].hide_render = is_hide
_set_layer_collection(obj_view_layer_collection, str_name, is_hide)
def process_render(obj_cmd: dict) -> None:
"""Execute le rendu d'une frame selon la commande recue."""
obj_scene = bpy.context.scene
str_camera: str = obj_cmd["str_camera"]
nb_frame: int = obj_cmd["nb_frame"]
nb_resolution_x: int = obj_cmd["nb_resolution_x"]
nb_resolution_y: int = obj_cmd["nb_resolution_y"]
str_format: str = obj_cmd["str_format"]
str_output_path: str = obj_cmd["str_output_path"]
obj_scene.camera = bpy.data.objects[str_camera]
obj_scene.render.resolution_x = nb_resolution_x
obj_scene.render.resolution_y = nb_resolution_y
obj_scene.render.image_settings.file_format = str_format
obj_scene.frame_set(nb_frame)
str_dir: str = os.path.dirname(str_output_path)
if str_dir and not os.path.exists(str_dir):
os.makedirs(str_dir, exist_ok=True)
obj_scene.render.filepath = str_output_path
apply_render_settings(obj_scene, obj_cmd.get("obj_render_settings"))
apply_collections(obj_cmd.get("list_collections"))
try:
bpy.ops.render.render(write_still=True)
str_result: str = json.dumps({"str_file": str_output_path})
sys.stdout.write("RENDER_DONE:" + str_result + "\n")
sys.stdout.flush()
except Exception as obj_err:
str_error: str = json.dumps({"str_error": str(obj_err)})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
obj_logger.error("Erreur rendu : %s", str(obj_err))
def main() -> None:
"""Boucle principale du daemon de rendu."""
sys.stdout.write("DAEMON_READY\n")
sys.stdout.flush()
for str_line in sys.stdin:
str_line = str_line.strip()
if not str_line:
continue
try:
obj_cmd: dict = json.loads(str_line)
except json.JSONDecodeError as obj_err:
str_error: str = json.dumps({"str_error": "JSON invalide : " + str(obj_err)})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
continue
str_cmd: str = obj_cmd.get("str_cmd", "")
if str_cmd == "quit":
break
elif str_cmd == "render":
process_render(obj_cmd)
else:
str_error = json.dumps({"str_error": "Commande inconnue : " + str_cmd})
sys.stdout.write("RENDER_ERROR:" + str_error + "\n")
sys.stdout.flush()
main()

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:; media-src 'self' file: data:;">
<title>Multi Render Blender</title> <title>Multi Render Blender</title>
<!-- Bootstrap 5 --> <!-- Bootstrap 5 -->
@@ -33,8 +33,8 @@
<div class="container-fluid p-3"> <div class="container-fluid p-3">
<div class="row g-3"> <div class="row g-3">
<!-- ── Left Column : File + Cameras ─────────────── --> <!-- ── Left Column : File + Settings ──────────── -->
<div class="col-md-4 d-flex flex-column gap-3"> <div class="col-md-3 d-flex flex-column gap-3">
<!-- Blend file selection --> <!-- Blend file selection -->
<div class="card bg-dark border-secondary"> <div class="card bg-dark border-secondary">
@@ -99,6 +99,18 @@
</div> </div>
</div> </div>
<!-- Render settings -->
<div class="card bg-dark border-secondary">
<div class="card-header border-secondary">
<i class="mdi mdi-tune-vertical me-1"></i>Proprietes de rendu
</div>
<div class="card-body" id="container_render_settings">
<div class="text-center text-light-emphasis py-2">
Chargez un fichier .blend
</div>
</div>
</div>
<!-- Render mode --> <!-- Render mode -->
<div class="card bg-dark border-secondary"> <div class="card bg-dark border-secondary">
<div class="card-header border-secondary"> <div class="card-header border-secondary">
@@ -132,15 +144,19 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ── Center Column : Cameras + Config ──────────── -->
<div class="col-md-3 d-flex flex-column gap-3">
<!-- Camera list --> <!-- Camera list -->
<div class="card bg-dark border-secondary flex-grow-1"> <div class="card bg-dark border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center"> <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><i class="mdi mdi-camera-outline me-1"></i>Cameras</span>
<span id="badge_camera_count" class="badge bg-secondary">0</span> <span id="badge_camera_count" class="badge bg-secondary">0</span>
</div> </div>
<div class="card-body p-0"> <div class="card-body p-0">
<div id="container_camera_list" class="list-group list-group-flush overflow-auto" style="max-height: 400px;"> <div id="container_camera_list" class="list-group list-group-flush overflow-auto" style="max-height: 300px;">
<div class="text-center text-light-emphasis py-4"> <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> <i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>
Chargez un fichier .blend Chargez un fichier .blend
@@ -148,13 +164,31 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ── Center Column : Camera Config + Controls ── --> <!-- Collections -->
<div class="col-md-4 d-flex flex-column gap-3"> <div class="card bg-dark border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
<span><i class="mdi mdi-folder-multiple-outline me-1"></i>Collections</span>
<div class="d-flex align-items-center gap-2">
<button id="btn_reset_collections" class="btn btn-sm btn-outline-secondary py-0 px-1"
title="Restaurer les valeurs originales">
<i class="mdi mdi-undo-variant" style="font-size: 0.75rem;"></i>
</button>
<span id="badge_collection_count" class="badge bg-secondary">0</span>
</div>
</div>
<div class="card-body p-0">
<div id="container_collection_list" class="list-group list-group-flush overflow-auto" style="max-height: 250px;">
<div class="text-center text-light-emphasis py-3">
<i class="mdi mdi-folder-off-outline d-block mb-2" style="font-size: 1.5rem;"></i>
Chargez un fichier .blend
</div>
</div>
</div>
</div>
<!-- Camera config --> <!-- Camera config -->
<div class="card bg-dark border-secondary"> <div class="card bg-dark border-secondary flex-grow-1">
<div class="card-header border-secondary"> <div class="card-header border-secondary">
<i class="mdi mdi-cog-outline me-1"></i>Configuration : <span id="label_selected_camera">-</span> <i class="mdi mdi-cog-outline me-1"></i>Configuration : <span id="label_selected_camera">-</span>
</div> </div>
@@ -164,6 +198,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- ── Right Column : Controls + Queue + Preview + Console ── -->
<div class="col-md-6 d-flex flex-column gap-3">
<!-- Render controls --> <!-- Render controls -->
<div class="card bg-dark border-secondary"> <div class="card bg-dark border-secondary">
@@ -191,8 +229,9 @@
<small id="label_progress_status">En attente</small> <small id="label_progress_status">En attente</small>
<small id="label_progress_count">0 / 0</small> <small id="label_progress_count">0 / 0</small>
</div> </div>
<div class="progress bg-secondary" style="height: 8px;"> <div class="progress bg-secondary position-relative" style="height: 20px;">
<div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div> <div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div>
<small id="label_progress_time" class="position-absolute w-100 text-center" style="line-height: 20px; font-size: 0.7rem; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.9);"></small>
</div> </div>
</div> </div>
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
@@ -202,45 +241,63 @@
</div> </div>
</div> </div>
<!-- Render queue --> <!-- Render queue + Preview side by side -->
<div class="card bg-dark border-secondary flex-grow-1"> <div class="row g-3 flex-grow-1">
<div class="card-header border-secondary d-flex justify-content-between align-items-center"> <div class="col-6 d-flex flex-column">
<span><i class="mdi mdi-format-list-numbered me-1"></i>File de rendu</span> <!-- Render queue -->
<div class="d-flex align-items-center gap-2"> <div class="card bg-dark border-secondary flex-grow-1">
<small id="label_queue_time_estimate" class="queue-time-estimate"></small> <div class="card-header border-secondary d-flex justify-content-between align-items-center">
<span id="badge_queue_count" class="badge bg-secondary">0</span> <span><i class="mdi mdi-format-list-numbered me-1"></i>File de rendu</span>
</div> <div class="d-flex align-items-center gap-2">
</div> <small id="label_queue_time_estimate" class="queue-time-estimate"></small>
<div class="card-body p-0"> <span id="badge_queue_count" class="badge bg-secondary">0</span>
<div id="container_render_queue" class="list-group list-group-flush overflow-auto" style="max-height: 300px;"> </div>
<div class="text-center text-light-emphasis py-4"> </div>
File vide <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> </div>
</div> </div>
</div> <div class="col-6 d-flex flex-column">
</div> <!-- Preview -->
<div class="card bg-dark border-secondary flex-grow-1">
<!-- ── Right Column : Preview + Console ─────────── --> <div class="card-header border-secondary d-flex justify-content-between align-items-center">
<div class="col-md-4 d-flex flex-column gap-3"> <span><i class="mdi mdi-image-outline me-1"></i>Preview</span>
<div class="d-flex align-items-center gap-1">
<!-- Preview --> <div id="container_ffmpeg_progress" class="d-none d-flex align-items-center gap-1">
<div class="card bg-dark border-secondary"> <div class="progress bg-secondary position-relative" style="height: 14px; width: 120px;">
<div class="card-header border-secondary"> <div id="bar_ffmpeg_progress" class="progress-bar progress-bar-striped progress-bar-animated bg-info" role="progressbar" style="width: 100%;"></div>
<i class="mdi mdi-image-outline me-1"></i>Preview <small id="label_ffmpeg_time" class="position-absolute w-100 text-center" style="line-height: 14px; font-size: 0.6rem; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.9);"></small>
</div> </div>
<div class="card-body p-2 text-center" id="container_preview"> </div>
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;"> <select id="select_video_fps" class="form-select form-select-sm bg-dark text-light border-secondary py-0" style="width: 70px; font-size: 0.7rem;">
<div class="text-light-emphasis"> <option value="24">24 fps</option>
<i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i> <option value="30" selected>30 fps</option>
Aucun rendu disponible <option value="60">60 fps</option>
</select>
<button id="btn_generate_video" class="btn btn-sm btn-outline-info py-0 px-1" title="Generer une video preview (ffmpeg)">
<i class="mdi mdi-video-outline"></i>
</button>
</div>
</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: 200px;">
<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>
</div> </div>
</div> </div>
</div> </div>
<!-- Console logs --> <!-- Console logs -->
<div class="card bg-dark border-secondary flex-grow-1"> <div class="card bg-dark border-secondary">
<div class="card-header border-secondary d-flex justify-content-between align-items-center"> <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> <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"> <button id="btn_clear_console" class="btn btn-sm btn-outline-secondary" title="Vider">
@@ -248,7 +305,7 @@
</button> </button>
</div> </div>
<div class="card-body p-0"> <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 id="container_console" class="console-output overflow-auto p-2" style="max-height: 200px; min-height: 150px;">
</div> </div>
</div> </div>
</div> </div>
@@ -256,18 +313,38 @@
</div> </div>
</div> </div>
<!-- Video Preview Modal -->
<div class="modal fade" id="modal_video_preview" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark border-secondary">
<div class="modal-header border-secondary py-2">
<h6 class="modal-title text-light mb-0">
<i class="mdi mdi-video-outline me-1"></i>Preview Video
</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0 text-center bg-black">
<video id="video_preview_player" controls style="max-width: 100%; max-height: 75vh;"></video>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS --> <!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/ConsoleLog.js"></script> <script src="scripts/ConsoleLog.js"></script>
<script src="scripts/RenderSettings.js"></script>
<script src="scripts/CameraList.js"></script> <script src="scripts/CameraList.js"></script>
<script src="scripts/CollectionList.js"></script>
<script src="scripts/CameraConfig.js"></script> <script src="scripts/CameraConfig.js"></script>
<script src="scripts/RenderQueue.js"></script> <script src="scripts/RenderQueue.js"></script>
<script src="scripts/PreviewPanel.js"></script> <script src="scripts/PreviewPanel.js"></script>
<script src="scripts/ProgressBar.js"></script> <script src="scripts/ProgressBar.js"></script>
<script src="scripts/UpdateBanner.js"></script> <script src="scripts/UpdateBanner.js"></script>
<script src="scripts/BlenderPath.js"></script> <script src="scripts/BlenderPath.js"></script>
<script src="scripts/FfmpegPath.js"></script>
<script src="scripts/NotificationConfig.js"></script> <script src="scripts/NotificationConfig.js"></script>
<script src="scripts/App.js"></script> <script src="scripts/App.js"></script>
</body> </body>

View File

@@ -4,13 +4,16 @@ const App = {
init: () => { init: () => {
ConsoleLog.init(); ConsoleLog.init();
RenderSettings.init();
CameraList.init(App._on_camera_select); CameraList.init(App._on_camera_select);
CollectionList.init();
CameraConfig.init(); CameraConfig.init();
RenderQueue.init(); RenderQueue.init();
PreviewPanel.init(); PreviewPanel.init();
ProgressBar.init(); ProgressBar.init();
UpdateBanner.init(); UpdateBanner.init();
BlenderPath.init(); BlenderPath.init();
FfmpegPath.init();
NotificationConfig.init(); NotificationConfig.init();
App._bind_events(); App._bind_events();
@@ -70,6 +73,12 @@ const App = {
obj_radio_both.addEventListener("change", () => { App._update_output_example(); }); obj_radio_both.addEventListener("change", () => { App._update_output_example(); });
obj_input_frame_prefix.addEventListener("input", () => { App._update_output_example(); }); obj_input_frame_prefix.addEventListener("input", () => { App._update_output_example(); });
obj_input_frame_padding.addEventListener("input", () => { App._update_output_example(); }); obj_input_frame_padding.addEventListener("input", () => { App._update_output_example(); });
let obj_btn_reset_cols = document.getElementById("btn_reset_collections");
obj_btn_reset_cols.addEventListener("click", () => {
CollectionList.reset_to_original();
ConsoleLog.add("Collections restaurees aux valeurs originales.");
});
}, },
_bind_render_events: () => { _bind_render_events: () => {
@@ -129,6 +138,18 @@ const App = {
CameraList.set_cameras(obj_result.list_cameras, obj_result.obj_scene); CameraList.set_cameras(obj_result.list_cameras, obj_result.obj_scene);
CameraConfig.clear(); CameraConfig.clear();
if (obj_result.obj_render_settings) {
RenderSettings.set_from_blend(obj_result.obj_render_settings);
}
if (obj_result.list_collections) {
CollectionList.set_collections(obj_result.list_collections);
ConsoleLog.add(obj_result.list_collections.length + " collection(s) trouvee(s).");
} else {
CollectionList.clear();
}
ConsoleLog.add(obj_result.list_cameras.length + " camera(s) trouvee(s)."); ConsoleLog.add(obj_result.list_cameras.length + " camera(s) trouvee(s).");
App._update_start_button(); App._update_start_button();
}) })
@@ -188,6 +209,8 @@ const App = {
nb_frame_padding: nb_frame_padding, nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path, str_output_path: App.str_output_path,
list_cameras: list_cameras, list_cameras: list_cameras,
list_collections: CollectionList.get_overrides(),
obj_render_settings: RenderSettings.get_settings(),
}; };
RenderQueue.build_display(str_mode, list_cameras); RenderQueue.build_display(str_mode, list_cameras);
@@ -249,6 +272,7 @@ const App = {
nb_frame_padding: nb_frame_padding, nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path, str_output_path: App.str_output_path,
list_cameras: list_cameras, list_cameras: list_cameras,
list_collections: CollectionList.get_overrides(),
}; };
RenderQueue.build_display(str_mode, list_cameras); RenderQueue.build_display(str_mode, list_cameras);
@@ -284,6 +308,8 @@ const App = {
nb_frame_padding: nb_frame_padding, nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path, str_output_path: App.str_output_path,
list_cameras: CameraList.list_cameras, list_cameras: CameraList.list_cameras,
list_collections: CollectionList.list_collections,
obj_render_settings: RenderSettings.get_settings(),
}; };
window.api.save_config(obj_config) window.api.save_config(obj_config)
@@ -338,6 +364,8 @@ const App = {
App._update_output_example(); App._update_output_example();
RenderSettings.set_from_config(obj_config);
if (obj_config.list_cameras && obj_config.list_cameras.length > 0) { if (obj_config.list_cameras && obj_config.list_cameras.length > 0) {
CameraList.list_cameras = obj_config.list_cameras; CameraList.list_cameras = obj_config.list_cameras;
CameraList.str_selected_camera = null; CameraList.str_selected_camera = null;
@@ -347,6 +375,15 @@ const App = {
obj_badge.textContent = String(obj_config.list_cameras.length); obj_badge.textContent = String(obj_config.list_cameras.length);
} }
if (obj_config.list_collections && obj_config.list_collections.length > 0) {
CollectionList.list_collections = obj_config.list_collections;
CollectionList.render();
let obj_badge_col = document.getElementById("badge_collection_count");
obj_badge_col.textContent = String(obj_config.list_collections.length);
} else {
CollectionList.clear();
}
CameraConfig.clear(); CameraConfig.clear();
App._update_start_button(); App._update_start_button();
ConsoleLog.add("Configuration importee."); ConsoleLog.add("Configuration importee.");

View File

@@ -36,9 +36,14 @@ const BlenderPath = {
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' + '<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
'</div>' + '</div>' +
'<div class="modal-body">' + '<div class="modal-body">' +
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' + '<p class="text-light-emphasis mb-2" style="font-size: 0.85rem;">' +
'Selectionnez l\'executable Blender sur votre machine.' + 'Selectionnez l\'executable Blender sur votre machine.' +
'</p>' + '</p>' +
'<p class="mb-3" style="font-size: 0.8rem;">' +
'<a href="https://www.blender.org/download/" target="_blank" class="text-info">' +
'<i class="mdi mdi-download me-1"></i>Telecharger Blender' +
'</a>' +
'</p>' +
'<div class="input-group input-group-sm mb-3">' + '<div class="input-group input-group-sm mb-3">' +
'<input type="text" id="input_blender_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun chemin configure" readonly>' + '<input type="text" id="input_blender_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun chemin configure" readonly>' +
'<button id="btn_browse_blender" class="btn btn-outline-primary" type="button">' + '<button id="btn_browse_blender" class="btn btn-outline-primary" type="button">' +

View File

@@ -0,0 +1,133 @@
const CollectionList = {
list_collections: [],
init: () => {
// Peuple lors du chargement du .blend
},
set_collections: (list_raw) => {
CollectionList.list_collections = [];
if (!list_raw || list_raw.length === 0) {
CollectionList.render();
let obj_badge = document.getElementById("badge_collection_count");
obj_badge.textContent = "0";
return;
}
for (let obj_raw of list_raw) {
let is_hidden = obj_raw.is_hide_render || obj_raw.is_exclude;
CollectionList.list_collections.push({
str_name: obj_raw.str_name,
nb_depth: obj_raw.nb_depth,
is_original_hide_render: is_hidden,
is_original_exclude: obj_raw.is_exclude,
is_hide_render: is_hidden,
has_override: false,
});
}
CollectionList.render();
let obj_badge = document.getElementById("badge_collection_count");
obj_badge.textContent = String(CollectionList.list_collections.length);
},
get_overrides: () => {
let list_overrides = [];
for (let obj_col of CollectionList.list_collections) {
list_overrides.push({
str_name: obj_col.str_name,
is_hide_render: obj_col.is_hide_render,
});
}
return list_overrides;
},
render: () => {
let obj_container = document.getElementById("container_collection_list");
obj_container.innerHTML = "";
if (CollectionList.list_collections.length === 0) {
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-3">'
+ '<i class="mdi mdi-folder-off-outline d-block mb-2" style="font-size: 1.5rem;"></i>'
+ "Chargez un fichier .blend"
+ "</div>";
return;
}
for (let obj_col of CollectionList.list_collections) {
let obj_item = document.createElement("div");
obj_item.classList.add("list-group-item", "bg-dark", "text-light",
"border-secondary", "d-flex", "align-items-center", "gap-2", "py-1");
let nb_padding = 0.75 + obj_col.nb_depth * 1.2;
obj_item.style.paddingLeft = nb_padding + "rem";
let obj_checkbox = document.createElement("input");
obj_checkbox.type = "checkbox";
obj_checkbox.classList.add("form-check-input");
obj_checkbox.checked = !obj_col.is_hide_render;
obj_checkbox.addEventListener("change", () => {
obj_col.is_hide_render = !obj_checkbox.checked;
obj_col.has_override = (obj_col.is_hide_render !== obj_col.is_original_hide_render);
CollectionList.render();
});
let obj_icon = document.createElement("i");
obj_icon.classList.add("mdi");
if (obj_col.is_hide_render) {
obj_icon.classList.add("mdi-folder-off-outline", "text-muted");
} else {
obj_icon.classList.add("mdi-folder-outline");
}
let obj_label = document.createElement("span");
obj_label.classList.add("flex-grow-1", "collection-name");
obj_label.textContent = obj_col.str_name;
if (obj_col.is_hide_render) {
obj_label.classList.add("text-muted");
}
let obj_indicator = document.createElement("small");
obj_indicator.classList.add("collection-original-badge");
if (obj_col.has_override) {
obj_indicator.classList.add("text-warning");
obj_indicator.innerHTML = '<i class="mdi mdi-pencil-outline"></i>';
obj_indicator.title = "Modifie (original : "
+ (obj_col.is_original_hide_render ? "masque" : "visible") + ")";
} else if (obj_col.is_original_hide_render) {
obj_indicator.classList.add("text-muted");
obj_indicator.innerHTML = '<i class="mdi mdi-eye-off-outline"></i>';
obj_indicator.title = "Masque dans le .blend";
}
obj_item.appendChild(obj_checkbox);
obj_item.appendChild(obj_icon);
obj_item.appendChild(obj_label);
obj_item.appendChild(obj_indicator);
obj_container.appendChild(obj_item);
}
},
reset_to_original: () => {
for (let obj_col of CollectionList.list_collections) {
obj_col.is_hide_render = obj_col.is_original_hide_render;
obj_col.has_override = false;
}
CollectionList.render();
},
clear: () => {
CollectionList.list_collections = [];
let obj_container = document.getElementById("container_collection_list");
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-3">'
+ '<i class="mdi mdi-folder-off-outline d-block mb-2" style="font-size: 1.5rem;"></i>'
+ "Chargez un fichier .blend"
+ "</div>";
let obj_badge = document.getElementById("badge_collection_count");
obj_badge.textContent = "0";
},
};

View File

@@ -0,0 +1,198 @@
const FfmpegPath = {
str_current_path: null,
is_found: false,
obj_modal: null,
init: () => {
FfmpegPath._create_badge();
FfmpegPath._create_modal();
FfmpegPath._bind_events();
},
_create_badge: () => {
let obj_nav_right = document.querySelector("nav .d-flex.gap-2");
let obj_badge = document.createElement("button");
obj_badge.id = "btn_ffmpeg_status";
obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Chemin FFmpeg";
obj_badge.innerHTML = '<i class="mdi mdi-video-outline"></i>';
obj_nav_right.insertBefore(obj_badge, obj_nav_right.firstChild);
obj_badge.addEventListener("click", () => {
FfmpegPath._open_modal();
});
},
_create_modal: () => {
let obj_modal_el = document.createElement("div");
obj_modal_el.id = "modal_ffmpeg_path";
obj_modal_el.className = "modal fade";
obj_modal_el.tabIndex = -1;
obj_modal_el.innerHTML =
'<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content bg-dark text-light border-secondary">' +
'<div class="modal-header border-secondary">' +
'<h6 class="modal-title"><i class="mdi mdi-video-outline me-2"></i>Chemin FFmpeg</h6>' +
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
'</div>' +
'<div class="modal-body">' +
'<p class="text-light-emphasis mb-2" style="font-size: 0.85rem;">' +
'Selectionnez l\'executable FFmpeg sur votre machine.' +
'</p>' +
'<p class="mb-3" style="font-size: 0.8rem;">' +
'<a href="https://ffmpeg.org/download.html" target="_blank" class="text-info">' +
'<i class="mdi mdi-download me-1"></i>Telecharger FFmpeg' +
'</a>' +
'</p>' +
'<div class="input-group input-group-sm mb-3">' +
'<input type="text" id="input_ffmpeg_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun chemin configure" readonly>' +
'<button id="btn_browse_ffmpeg" class="btn btn-outline-primary" type="button">' +
'<i class="mdi mdi-folder-search-outline"></i> Parcourir' +
'</button>' +
'</div>' +
'<div class="d-flex justify-content-between align-items-center">' +
'<button id="btn_detect_ffmpeg" class="btn btn-sm btn-outline-secondary">' +
'<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement' +
'</button>' +
'<span id="label_ffmpeg_status" class="badge bg-danger">' +
'<i class="mdi mdi-close-circle me-1"></i>Non trouve' +
'</span>' +
'</div>' +
'</div>' +
'<div class="modal-footer border-secondary">' +
'<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' +
'<button id="btn_validate_ffmpeg" type="button" class="btn btn-sm btn-primary" disabled>' +
'<i class="mdi mdi-check me-1"></i>Valider' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(obj_modal_el);
FfmpegPath.obj_modal = new bootstrap.Modal(obj_modal_el);
},
_bind_events: () => {
let obj_btn_browse = document.getElementById("btn_browse_ffmpeg");
obj_btn_browse.addEventListener("click", () => {
FfmpegPath._browse();
});
let obj_btn_detect = document.getElementById("btn_detect_ffmpeg");
obj_btn_detect.addEventListener("click", () => {
FfmpegPath._detect();
});
let obj_btn_validate = document.getElementById("btn_validate_ffmpeg");
obj_btn_validate.addEventListener("click", () => {
FfmpegPath._validate();
});
window.api.on_ffmpeg_path_status((obj_data) => {
FfmpegPath.str_current_path = obj_data.str_path;
FfmpegPath.is_found = obj_data.is_found;
FfmpegPath._update_badge();
});
},
_open_modal: () => {
let obj_input = document.getElementById("input_ffmpeg_path");
obj_input.value = FfmpegPath.is_found ? FfmpegPath.str_current_path : "";
FfmpegPath._update_modal_status(FfmpegPath.is_found, FfmpegPath.str_current_path);
FfmpegPath.obj_modal.show();
if (!FfmpegPath.is_found) {
FfmpegPath._detect();
}
},
_browse: () => {
window.api.select_ffmpeg_exe()
.then((str_path) => {
if (!str_path) {
return;
}
let obj_input = document.getElementById("input_ffmpeg_path");
obj_input.value = str_path;
FfmpegPath._update_modal_status(true, str_path);
})
.catch((obj_err) => {
ConsoleLog.add("Erreur selection FFmpeg : " + obj_err.message);
});
},
_detect: () => {
let obj_btn = document.getElementById("btn_detect_ffmpeg");
obj_btn.disabled = true;
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Detection...';
window.api.get_ffmpeg_path()
.then((obj_data) => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
if (obj_data.is_found) {
let obj_input = document.getElementById("input_ffmpeg_path");
obj_input.value = obj_data.str_path;
FfmpegPath._update_modal_status(true, obj_data.str_path);
} else {
FfmpegPath._update_modal_status(false, null);
}
})
.catch(() => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
FfmpegPath._update_modal_status(false, null);
});
},
_validate: () => {
let str_path = document.getElementById("input_ffmpeg_path").value;
if (!str_path) {
return;
}
window.api.set_ffmpeg_path(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
FfmpegPath.str_current_path = obj_result.str_path;
FfmpegPath.is_found = true;
FfmpegPath._update_badge();
FfmpegPath.obj_modal.hide();
ConsoleLog.add("Chemin FFmpeg configure : " + obj_result.str_path);
} else {
FfmpegPath._update_modal_status(false, null);
ConsoleLog.add("Chemin FFmpeg invalide : " + (obj_result.str_error || ""));
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur configuration FFmpeg : " + obj_err.message);
});
},
_update_badge: () => {
let obj_badge = document.getElementById("btn_ffmpeg_status");
if (FfmpegPath.is_found) {
obj_badge.className = "btn btn-sm btn-outline-success";
obj_badge.title = "FFmpeg : " + FfmpegPath.str_current_path;
} else {
obj_badge.className = "btn btn-sm btn-outline-danger";
obj_badge.title = "FFmpeg non trouve";
}
},
_update_modal_status: (is_valid, str_path) => {
let obj_label = document.getElementById("label_ffmpeg_status");
let obj_btn_validate = document.getElementById("btn_validate_ffmpeg");
if (is_valid && str_path) {
obj_label.className = "badge bg-success";
obj_label.innerHTML = '<i class="mdi mdi-check-circle me-1"></i>Trouve';
obj_btn_validate.disabled = false;
} else {
obj_label.className = "badge bg-danger";
obj_label.innerHTML = '<i class="mdi mdi-close-circle me-1"></i>Non trouve';
obj_btn_validate.disabled = true;
}
},
};

View File

@@ -1,6 +1,7 @@
const NotificationConfig = { const NotificationConfig = {
is_notify_each_image: false, is_notify_each_image: false,
is_notify_all_done: true, is_notify_all_done: true,
obj_email_config: null,
obj_modal: null, obj_modal: null,
init: () => { init: () => {
@@ -8,6 +9,7 @@ const NotificationConfig = {
NotificationConfig._create_modal(); NotificationConfig._create_modal();
NotificationConfig._bind_events(); NotificationConfig._bind_events();
NotificationConfig._load_config(); NotificationConfig._load_config();
NotificationConfig._load_email_config();
}, },
_create_badge: () => { _create_badge: () => {
@@ -35,28 +37,55 @@ const NotificationConfig = {
obj_modal_el.className = "modal fade"; obj_modal_el.className = "modal fade";
obj_modal_el.tabIndex = -1; obj_modal_el.tabIndex = -1;
obj_modal_el.innerHTML = obj_modal_el.innerHTML =
'<div class="modal-dialog modal-dialog-centered modal-sm">' + '<div class="modal-dialog modal-dialog-centered">' +
'<div class="modal-content bg-dark text-light border-secondary">' + '<div class="modal-content bg-dark text-light border-secondary">' +
'<div class="modal-header border-secondary">' + '<div class="modal-header border-secondary">' +
'<h6 class="modal-title"><i class="mdi mdi-bell-outline me-2"></i>Notifications</h6>' + '<h6 class="modal-title"><i class="mdi mdi-bell-outline me-2"></i>Notifications</h6>' +
'<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' + '<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>' +
'</div>' + '</div>' +
'<div class="modal-body">' + '<div class="modal-body">' +
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' +
'Configurez les notifications systeme.' + '<p class="text-light-emphasis mb-2" style="font-size: 0.8rem; font-weight: 600;">Notifications systeme</p>' +
'</p>' +
'<div class="form-check mb-2">' + '<div class="form-check mb-2">' +
'<input class="form-check-input" type="checkbox" id="check_notify_each_image">' + '<input class="form-check-input" type="checkbox" id="check_notify_each_image">' +
'<label class="form-check-label" for="check_notify_each_image">' + '<label class="form-check-label" for="check_notify_each_image">' +
'A chaque image rendue' + 'A chaque image rendue' +
'</label>' + '</label>' +
'</div>' + '</div>' +
'<div class="form-check">' + '<div class="form-check mb-3">' +
'<input class="form-check-input" type="checkbox" id="check_notify_all_done" checked>' + '<input class="form-check-input" type="checkbox" id="check_notify_all_done" checked>' +
'<label class="form-check-label" for="check_notify_all_done">' + '<label class="form-check-label" for="check_notify_all_done">' +
'Quand tous les rendus sont termines' + 'Quand tous les rendus sont termines' +
'</label>' + '</label>' +
'</div>' + '</div>' +
'<hr class="border-secondary">' +
'<div class="d-flex justify-content-between align-items-center mb-2">' +
'<p class="text-light-emphasis mb-0" style="font-size: 0.8rem; font-weight: 600;">' +
'<i class="mdi mdi-email-outline me-1"></i>Notification par email' +
'</p>' +
'<div class="form-check form-switch mb-0">' +
'<input class="form-check-input" type="checkbox" id="check_email_enabled">' +
'</div>' +
'</div>' +
'<div id="container_email_fields">' +
'<p class="text-light-emphasis mb-2" style="font-size: 0.75rem;">' +
'Un email sera envoye lors des notifications. Separez les adresses par une virgule.' +
'</p>' +
'<div class="mb-2">' +
'<label class="form-label form-label-sm mb-0">Destinataire(s)</label>' +
'<input type="text" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_email_to" placeholder="email@example.com, autre@example.com">' +
'</div>' +
'<div class="d-flex justify-content-between align-items-center">' +
'<button id="btn_test_email" class="btn btn-sm btn-outline-info">' +
'<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test' +
'</button>' +
'<span id="label_email_test_status" class="badge d-none"></span>' +
'</div>' +
'</div>' +
'</div>' + '</div>' +
'<div class="modal-footer border-secondary">' + '<div class="modal-footer border-secondary">' +
'<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' + '<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Annuler</button>' +
@@ -76,11 +105,46 @@ const NotificationConfig = {
obj_btn_save.addEventListener("click", () => { obj_btn_save.addEventListener("click", () => {
NotificationConfig._save_config(); NotificationConfig._save_config();
}); });
let obj_check_email = document.getElementById("check_email_enabled");
obj_check_email.addEventListener("change", () => {
NotificationConfig._toggle_email_fields();
});
let obj_btn_test = document.getElementById("btn_test_email");
obj_btn_test.addEventListener("click", () => {
NotificationConfig._test_email();
});
},
_toggle_email_fields: () => {
let is_enabled = document.getElementById("check_email_enabled").checked;
let obj_fields = document.getElementById("container_email_fields");
let list_inputs = obj_fields.querySelectorAll("input, button");
for (let obj_input of list_inputs) {
obj_input.disabled = !is_enabled;
}
obj_fields.style.opacity = is_enabled ? "1" : "0.4";
}, },
_open_modal: () => { _open_modal: () => {
document.getElementById("check_notify_each_image").checked = NotificationConfig.is_notify_each_image; document.getElementById("check_notify_each_image").checked = NotificationConfig.is_notify_each_image;
document.getElementById("check_notify_all_done").checked = NotificationConfig.is_notify_all_done; document.getElementById("check_notify_all_done").checked = NotificationConfig.is_notify_all_done;
let obj_email = NotificationConfig.obj_email_config;
if (obj_email) {
document.getElementById("check_email_enabled").checked = obj_email.is_enabled || false;
document.getElementById("input_email_to").value = obj_email.str_to || "";
} else {
document.getElementById("check_email_enabled").checked = false;
document.getElementById("input_email_to").value = "";
}
NotificationConfig._toggle_email_fields();
let obj_status = document.getElementById("label_email_test_status");
obj_status.classList.add("d-none");
NotificationConfig.obj_modal.show(); NotificationConfig.obj_modal.show();
}, },
@@ -98,16 +162,39 @@ const NotificationConfig = {
}); });
}, },
_load_email_config: () => {
window.api.get_email_config()
.then((obj_config) => {
if (obj_config) {
NotificationConfig.obj_email_config = obj_config;
NotificationConfig._update_badge();
}
})
.catch(() => {
// Config non trouvee
});
},
_save_config: () => { _save_config: () => {
NotificationConfig.is_notify_each_image = document.getElementById("check_notify_each_image").checked; NotificationConfig.is_notify_each_image = document.getElementById("check_notify_each_image").checked;
NotificationConfig.is_notify_all_done = document.getElementById("check_notify_all_done").checked; NotificationConfig.is_notify_all_done = document.getElementById("check_notify_all_done").checked;
let obj_config = { let obj_notif_config = {
is_notify_each_image: NotificationConfig.is_notify_each_image, is_notify_each_image: NotificationConfig.is_notify_each_image,
is_notify_all_done: NotificationConfig.is_notify_all_done, is_notify_all_done: NotificationConfig.is_notify_all_done,
}; };
window.api.set_notification_config(obj_config) let obj_email_config = {
is_enabled: document.getElementById("check_email_enabled").checked,
str_to: document.getElementById("input_email_to").value.trim(),
};
NotificationConfig.obj_email_config = obj_email_config;
window.api.set_notification_config(obj_notif_config)
.then(() => {
return window.api.set_email_config(obj_email_config);
})
.then(() => { .then(() => {
NotificationConfig._update_badge(); NotificationConfig._update_badge();
NotificationConfig.obj_modal.hide(); NotificationConfig.obj_modal.hide();
@@ -118,12 +205,47 @@ const NotificationConfig = {
}); });
}, },
_test_email: () => {
let obj_btn = document.getElementById("btn_test_email");
let obj_status = document.getElementById("label_email_test_status");
obj_btn.disabled = true;
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Envoi...';
obj_status.classList.add("d-none");
let obj_config = {
str_to: document.getElementById("input_email_to").value.trim(),
};
window.api.test_email(obj_config)
.then((obj_result) => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test';
obj_status.classList.remove("d-none");
if (obj_result.is_success) {
obj_status.className = "badge bg-success";
obj_status.innerHTML = '<i class="mdi mdi-check-circle me-1"></i>Envoye';
} else {
obj_status.className = "badge bg-danger";
obj_status.textContent = obj_result.str_error || "Erreur";
}
})
.catch((obj_err) => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-email-send-outline me-1"></i>Envoyer un test';
obj_status.classList.remove("d-none");
obj_status.className = "badge bg-danger";
obj_status.textContent = obj_err.message || "Erreur";
});
},
_update_badge: () => { _update_badge: () => {
let obj_badge = document.getElementById("btn_notification_config"); let obj_badge = document.getElementById("btn_notification_config");
let is_any_active = NotificationConfig.is_notify_each_image || NotificationConfig.is_notify_all_done; let is_any_active = NotificationConfig.is_notify_each_image || NotificationConfig.is_notify_all_done;
if (is_any_active) { let is_email = NotificationConfig.obj_email_config && NotificationConfig.obj_email_config.is_enabled;
if (is_any_active || is_email) {
obj_badge.className = "btn btn-sm btn-outline-success"; obj_badge.className = "btn btn-sm btn-outline-success";
obj_badge.title = "Notifications activees"; obj_badge.title = "Notifications activees" + (is_email ? " + email" : "");
} else { } else {
obj_badge.className = "btn btn-sm btn-outline-secondary"; obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Notifications desactivees"; obj_badge.title = "Notifications desactivees";

View File

@@ -7,6 +7,18 @@ const PreviewPanel = {
window.api.on_preview_update((str_image_path) => { window.api.on_preview_update((str_image_path) => {
PreviewPanel.show_image(str_image_path); PreviewPanel.show_image(str_image_path);
}); });
let obj_btn_video = document.getElementById("btn_generate_video");
obj_btn_video.addEventListener("click", () => {
PreviewPanel._generate_video();
});
let obj_modal = document.getElementById("modal_video_preview");
obj_modal.addEventListener("hidden.bs.modal", () => {
let obj_video = document.getElementById("video_preview_player");
obj_video.pause();
obj_video.src = "";
});
}, },
show_image: (str_image_path) => { show_image: (str_image_path) => {
@@ -55,4 +67,88 @@ const PreviewPanel = {
+ "Aucun rendu disponible" + "Aucun rendu disponible"
+ "</div></div>"; + "</div></div>";
}, },
_generate_video: () => {
if (!App.str_output_path) {
ConsoleLog.add("Veuillez selectionner un dossier de sortie.");
return;
}
let list_cameras = CameraList.list_cameras;
let is_has_enabled = false;
for (let obj_cam of list_cameras) {
if (obj_cam.is_enabled) {
is_has_enabled = true;
break;
}
}
if (!is_has_enabled) {
ConsoleLog.add("Aucune camera activee.");
return;
}
let obj_btn = document.getElementById("btn_generate_video");
obj_btn.disabled = true;
obj_btn.innerHTML = '<i class="mdi mdi-loading mdi-spin"></i>';
ConsoleLog.add("Generation de la video preview...");
let obj_ffmpeg_container = document.getElementById("container_ffmpeg_progress");
let obj_ffmpeg_label = document.getElementById("label_ffmpeg_time");
obj_ffmpeg_container.classList.remove("d-none");
obj_ffmpeg_label.textContent = "0s";
let nb_start_ms = Date.now();
let nb_timer = setInterval(() => {
let nb_elapsed = Math.round((Date.now() - nb_start_ms) / 1000);
obj_ffmpeg_label.textContent = PreviewPanel._format_elapsed(nb_elapsed);
}, 1000);
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
let str_frame_prefix = document.getElementById("input_frame_prefix").value || "";
let nb_frame_padding = parseInt(document.getElementById("input_frame_padding").value, 10) || 5;
let nb_fps = parseInt(document.getElementById("select_video_fps").value, 10) || 30;
let obj_params = {
str_output_path: App.str_output_path,
list_cameras: list_cameras,
nb_fps: nb_fps,
str_output_mode: str_output_mode,
str_frame_prefix: str_frame_prefix,
nb_frame_padding: nb_frame_padding,
};
window.api.generate_preview_video(obj_params)
.then((obj_result) => {
clearInterval(nb_timer);
obj_ffmpeg_container.classList.add("d-none");
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-video-outline"></i>';
ConsoleLog.add("Video generee : " + obj_result.str_video_path);
let obj_video = document.getElementById("video_preview_player");
obj_video.src = "file://" + obj_result.str_video_path;
obj_video.load();
let obj_modal = new bootstrap.Modal(document.getElementById("modal_video_preview"));
obj_modal.show();
obj_video.play();
})
.catch((obj_err) => {
clearInterval(nb_timer);
obj_ffmpeg_container.classList.add("d-none");
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-video-outline"></i>';
ConsoleLog.add("Erreur generation video : " + (obj_err.message || obj_err));
});
},
_format_elapsed: (nb_seconds) => {
let nb_m = Math.floor(nb_seconds / 60);
let nb_s = nb_seconds % 60;
if (nb_m > 0) {
return nb_m + "m " + nb_s + "s";
}
return nb_s + "s";
},
}; };

View File

@@ -11,6 +11,8 @@ const ProgressBar = {
let str_camera = obj_data.str_camera || "-"; let str_camera = obj_data.str_camera || "-";
let nb_frame = obj_data.nb_frame || 0; let nb_frame = obj_data.nb_frame || 0;
let str_status = obj_data.str_status || "idle"; let str_status = obj_data.str_status || "idle";
let nb_avg_render_ms = obj_data.nb_avg_render_ms || 0;
let nb_remaining = obj_data.nb_remaining || 0;
let nb_percent = 0; let nb_percent = 0;
if (nb_total > 0) { if (nb_total > 0) {
@@ -40,7 +42,21 @@ const ProgressBar = {
let obj_frame_label = document.getElementById("label_current_frame"); let obj_frame_label = document.getElementById("label_current_frame");
obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? String(nb_frame) : "-"; obj_frame_label.textContent = nb_frame !== null && nb_frame !== undefined ? 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 || [], obj_data.list_stopped || [], obj_data.list_skipped_paths || []); let obj_time_label = document.getElementById("label_progress_time");
if (nb_avg_render_ms > 0 && nb_remaining > 0 && str_status === "running") {
let nb_remaining_ms = nb_avg_render_ms * nb_remaining;
let str_remaining = ProgressBar._format_duration(nb_remaining_ms);
let obj_eta = new Date(Date.now() + nb_remaining_ms);
let str_eta_hours = String(obj_eta.getHours()).padStart(2, "0");
let str_eta_minutes = String(obj_eta.getMinutes()).padStart(2, "0");
obj_time_label.textContent = "Restant : " + str_remaining + " — Fin : " + str_eta_hours + ":" + str_eta_minutes;
} else if (str_status === "idle" && nb_current >= nb_total && nb_total > 0) {
obj_time_label.textContent = "Termine";
} else {
obj_time_label.textContent = "";
}
RenderQueue.update_progress(obj_data);
}, },
reset: () => { reset: () => {
@@ -50,5 +66,30 @@ const ProgressBar = {
document.getElementById("label_progress_status").textContent = "En attente"; document.getElementById("label_progress_status").textContent = "En attente";
document.getElementById("label_current_camera").textContent = "-"; document.getElementById("label_current_camera").textContent = "-";
document.getElementById("label_current_frame").textContent = "-"; document.getElementById("label_current_frame").textContent = "-";
document.getElementById("label_progress_time").textContent = "";
},
_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;
}, },
}; };

View File

@@ -3,9 +3,14 @@ const RenderQueue = {
nb_total_render_ms: 0, nb_total_render_ms: 0,
nb_completed_renders: 0, nb_completed_renders: 0,
nb_last_current: 0, nb_last_current: 0,
list_machine_avgs: [],
nb_local_avg_ms: 0,
str_hostname: "",
_obj_context_menu: null,
_nb_context_index: -1,
init: () => { init: () => {
// Initialized on demand RenderQueue._create_context_menu();
}, },
build_display: (str_mode, list_cameras) => { build_display: (str_mode, list_cameras) => {
@@ -13,6 +18,9 @@ const RenderQueue = {
RenderQueue.nb_total_render_ms = 0; RenderQueue.nb_total_render_ms = 0;
RenderQueue.nb_completed_renders = 0; RenderQueue.nb_completed_renders = 0;
RenderQueue.nb_last_current = 0; RenderQueue.nb_last_current = 0;
RenderQueue.list_machine_avgs = [];
RenderQueue.nb_local_avg_ms = 0;
RenderQueue.str_hostname = "";
let list_enabled = []; let list_enabled = [];
for (let obj_cam of list_cameras) { for (let obj_cam of list_cameras) {
@@ -32,6 +40,7 @@ const RenderQueue = {
str_image_path: null, str_image_path: null,
obj_dom_el: null, obj_dom_el: null,
obj_dom_icon: null, obj_dom_icon: null,
obj_dom_result: null,
str_dom_status: null, str_dom_status: null,
is_click_bound: false, is_click_bound: false,
}); });
@@ -60,6 +69,7 @@ const RenderQueue = {
str_image_path: null, str_image_path: null,
obj_dom_el: null, obj_dom_el: null,
obj_dom_icon: null, obj_dom_icon: null,
obj_dom_result: null,
str_dom_status: null, str_dom_status: null,
is_click_bound: false, is_click_bound: false,
}); });
@@ -101,12 +111,20 @@ const RenderQueue = {
obj_frame.classList.add("text-light-emphasis"); obj_frame.classList.add("text-light-emphasis");
obj_frame.textContent = "F" + obj_item.nb_frame; obj_frame.textContent = "F" + obj_item.nb_frame;
let obj_result = document.createElement("small");
obj_result.classList.add("text-light-emphasis", "queue-item-result");
obj_result.style.minWidth = "140px";
obj_result.style.textAlign = "right";
obj_result.style.fontSize = "0.75em";
obj_el.appendChild(obj_icon); obj_el.appendChild(obj_icon);
obj_el.appendChild(obj_name); obj_el.appendChild(obj_name);
obj_el.appendChild(obj_frame); obj_el.appendChild(obj_frame);
obj_el.appendChild(obj_result);
obj_item.obj_dom_el = obj_el; obj_item.obj_dom_el = obj_el;
obj_item.obj_dom_icon = obj_icon; obj_item.obj_dom_icon = obj_icon;
obj_item.obj_dom_result = obj_result;
obj_item.str_dom_status = "pending"; obj_item.str_dom_status = "pending";
obj_fragment.appendChild(obj_el); obj_fragment.appendChild(obj_el);
@@ -115,7 +133,21 @@ const RenderQueue = {
obj_container.appendChild(obj_fragment); obj_container.appendChild(obj_fragment);
}, },
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped, list_stopped, list_skipped_paths) => { update_progress: (obj_data) => {
let nb_current = obj_data.nb_current || 0;
let nb_last_render_ms = obj_data.nb_last_render_ms || 0;
let str_last_image_path = obj_data.str_last_image_path || null;
let list_skipped = obj_data.list_skipped || [];
let list_stopped = obj_data.list_stopped || [];
let list_skipped_paths = obj_data.list_skipped_paths || [];
let list_rendering_remote = obj_data.list_rendering_remote || [];
let list_item_results = obj_data.list_item_results || [];
let list_machine_avgs = obj_data.list_machine_avgs || [];
RenderQueue.str_hostname = obj_data.str_hostname || "";
RenderQueue.nb_local_avg_ms = obj_data.nb_avg_render_ms || 0;
RenderQueue.list_machine_avgs = list_machine_avgs;
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) { if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
RenderQueue.nb_total_render_ms += nb_last_render_ms; RenderQueue.nb_total_render_ms += nb_last_render_ms;
RenderQueue.nb_completed_renders++; RenderQueue.nb_completed_renders++;
@@ -134,9 +166,17 @@ const RenderQueue = {
} }
} }
// Build result map for quick lookup
let map_results = {};
for (let obj_r of list_item_results) {
map_results[obj_r.nb_index] = obj_r;
}
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) { for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
if (list_stopped && list_stopped.indexOf(nb_i) !== -1) { if (list_stopped && list_stopped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "stopped"; RenderQueue.list_items[nb_i].str_status = "stopped";
} else if (list_rendering_remote && list_rendering_remote.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "rendering_remote";
} else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) { } else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "skipped"; RenderQueue.list_items[nb_i].str_status = "skipped";
} else if (nb_i < nb_current) { } else if (nb_i < nb_current) {
@@ -146,6 +186,11 @@ const RenderQueue = {
} else { } else {
RenderQueue.list_items[nb_i].str_status = "pending"; RenderQueue.list_items[nb_i].str_status = "pending";
} }
// Store result info on item
if (map_results[nb_i]) {
RenderQueue.list_items[nb_i].obj_result = map_results[nb_i];
}
} }
RenderQueue._update_time_display(); RenderQueue._update_time_display();
@@ -163,7 +208,9 @@ const RenderQueue = {
let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped") let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped")
&& obj_item.str_image_path && !obj_item.is_click_bound; && obj_item.str_image_path && !obj_item.is_click_bound;
if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) { let is_status_changed = obj_item.str_status !== obj_item.str_dom_status;
if (!is_status_changed && !is_needs_click) {
if (obj_item.str_status === "rendering") { if (obj_item.str_status === "rendering") {
obj_rendering_el = obj_item.obj_dom_el; obj_rendering_el = obj_item.obj_dom_el;
} }
@@ -189,10 +236,18 @@ const RenderQueue = {
} else if (obj_item.str_status === "stopped") { } else if (obj_item.str_status === "stopped") {
str_icon = "mdi-stop-circle"; str_icon = "mdi-stop-circle";
str_color = "text-warning"; str_color = "text-warning";
} else if (obj_item.str_status === "rendering_remote") {
str_icon = "mdi-desktop-classic";
str_color = "text-warning";
} }
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color; obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
// Update result column
if (obj_item.obj_dom_result) {
RenderQueue._update_result_cell(obj_item);
}
if (is_needs_click) { if (is_needs_click) {
obj_item.obj_dom_el.classList.add("queue-item-clickable"); obj_item.obj_dom_el.classList.add("queue-item-clickable");
let str_path = obj_item.str_image_path; let str_path = obj_item.str_image_path;
@@ -210,6 +265,45 @@ const RenderQueue = {
} }
}, },
_update_result_cell: (obj_item) => {
let obj_r = obj_item.obj_result;
if (!obj_r) {
obj_item.obj_dom_result.textContent = "";
return;
}
if (obj_r.str_type === "done") {
let str_date = "";
if (obj_r.str_date) {
let obj_d = new Date(obj_r.str_date);
let str_day = String(obj_d.getDate()).padStart(2, "0");
let str_month = String(obj_d.getMonth() + 1).padStart(2, "0");
let str_hours = String(obj_d.getHours()).padStart(2, "0");
let str_minutes = String(obj_d.getMinutes()).padStart(2, "0");
str_date = str_day + "/" + str_month + " " + str_hours + ":" + str_minutes;
}
let str_res = obj_r.str_resolution || "";
obj_item.obj_dom_result.textContent = str_date + (str_date && str_res ? " | " : "") + str_res;
obj_item.obj_dom_result.className = "text-success queue-item-result";
obj_item.obj_dom_result.style.minWidth = "140px";
obj_item.obj_dom_result.style.textAlign = "right";
obj_item.obj_dom_result.style.fontSize = "0.75em";
} else if (obj_r.str_type === "rendering_remote") {
let str_host = obj_r.str_remote_hostname || "?";
let str_avg = "";
if (obj_r.nb_remote_avg_ms > 0) {
str_avg = " ~" + RenderQueue._format_duration(obj_r.nb_remote_avg_ms) + "/i";
}
obj_item.obj_dom_result.textContent = str_host + str_avg;
obj_item.obj_dom_result.className = "text-warning queue-item-result";
obj_item.obj_dom_result.style.minWidth = "140px";
obj_item.obj_dom_result.style.textAlign = "right";
obj_item.obj_dom_result.style.fontSize = "0.75em";
} else {
obj_item.obj_dom_result.textContent = "";
}
},
mark_existing: (list_existing) => { mark_existing: (list_existing) => {
for (let obj_existing of list_existing) { for (let obj_existing of list_existing) {
if (obj_existing.nb_index < RenderQueue.list_items.length) { if (obj_existing.nb_index < RenderQueue.list_items.length) {
@@ -226,22 +320,55 @@ const RenderQueue = {
return; return;
} }
if (RenderQueue.nb_completed_renders === 0) { let nb_local_avg = RenderQueue.nb_local_avg_ms;
if (nb_local_avg === 0 && RenderQueue.nb_completed_renders > 0) {
nb_local_avg = Math.round(RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders);
}
if (nb_local_avg === 0) {
obj_label.innerHTML = ""; obj_label.innerHTML = "";
return; return;
} }
let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders;
let nb_remaining_count = 0; let nb_remaining_count = 0;
for (let obj_item of RenderQueue.list_items) { for (let obj_item of RenderQueue.list_items) {
if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") { if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped" && obj_item.str_status !== "rendering_remote") {
nb_remaining_count++; nb_remaining_count++;
} }
} }
let nb_remaining_ms = nb_avg_ms * nb_remaining_count; // Part 1 : temps moyen machine actuelle /i
obj_label.innerHTML = '<i class="mdi mdi-clock-outline me-1"></i>' let str_local_avg = RenderQueue._format_duration(nb_local_avg) + "/i";
+ RenderQueue._format_duration(nb_remaining_ms);
// Part 2 : temps restant si machine seule
let nb_remaining_solo_ms = nb_local_avg * nb_remaining_count;
let str_remaining_solo = RenderQueue._format_duration(nb_remaining_solo_ms);
// Part 3 : temps restant multi-machines
let list_avgs = RenderQueue.list_machine_avgs;
let nb_machines_with_data = 0;
let nb_sum_avgs = 0;
for (let obj_m of list_avgs) {
if (obj_m.nb_avg_ms > 0) {
nb_sum_avgs += obj_m.nb_avg_ms;
nb_machines_with_data++;
}
}
let str_remaining_multi = "";
if (nb_machines_with_data > 1) {
let nb_avg_of_avgs = nb_sum_avgs / nb_machines_with_data;
let nb_remaining_multi_ms = (nb_avg_of_avgs * nb_remaining_count) / nb_machines_with_data;
str_remaining_multi = RenderQueue._format_duration(nb_remaining_multi_ms);
}
let str_display = '<i class="mdi mdi-clock-outline me-1"></i>'
+ str_local_avg + " | " + str_remaining_solo;
if (str_remaining_multi) {
str_display += " | " + str_remaining_multi;
}
obj_label.innerHTML = str_display;
}, },
_format_duration: (nb_ms) => { _format_duration: (nb_ms) => {
@@ -267,4 +394,262 @@ const RenderQueue = {
return str_result; return str_result;
}, },
// ── Context menu ─────────────────────────────────────────
_create_context_menu: () => {
let obj_menu = document.createElement("div");
obj_menu.id = "context_menu_queue";
obj_menu.className = "context-menu d-none";
obj_menu.innerHTML =
'<div class="context-menu-item" data-action="preview"><i class="mdi mdi-eye-outline me-2"></i>Voir dans le preview</div>' +
'<div class="context-menu-item" data-action="open-folder"><i class="mdi mdi-folder-open-outline me-2"></i>Ouvrir dans l\'explorateur</div>' +
'<div class="context-menu-item" data-action="open-default"><i class="mdi mdi-open-in-new me-2"></i>Ouvrir (app par defaut)</div>' +
'<div class="context-menu-separator"></div>' +
'<div class="context-menu-item" data-action="copy-path"><i class="mdi mdi-content-copy me-2"></i>Copier le chemin</div>' +
'<div class="context-menu-item" data-action="copy-image"><i class="mdi mdi-image-outline me-2"></i>Copier l\'image</div>' +
'<div class="context-menu-separator"></div>' +
'<div class="context-menu-item" data-action="file-info"><i class="mdi mdi-information-outline me-2"></i>Infos fichier</div>' +
'<div class="context-menu-separator"></div>' +
'<div class="context-menu-item" data-action="delete"><i class="mdi mdi-delete-outline me-2"></i>Supprimer l\'image</div>' +
'<div class="context-menu-item context-menu-danger" data-action="delete-from-here"><i class="mdi mdi-delete-sweep-outline me-2"></i>Supprimer depuis ici</div>';
document.body.appendChild(obj_menu);
RenderQueue._obj_context_menu = obj_menu;
obj_menu.addEventListener("click", (obj_event) => {
let obj_target = obj_event.target.closest(".context-menu-item");
if (!obj_target) {
return;
}
let str_action = obj_target.dataset.action;
RenderQueue._hide_context_menu();
RenderQueue._handle_context_action(str_action);
});
document.addEventListener("click", () => {
RenderQueue._hide_context_menu();
});
document.addEventListener("keydown", (obj_event) => {
if (obj_event.key === "Escape") {
RenderQueue._hide_context_menu();
}
});
let obj_container = document.getElementById("container_render_queue");
obj_container.addEventListener("contextmenu", (obj_event) => {
let obj_el = obj_event.target.closest(".list-group-item");
if (!obj_el) {
return;
}
let nb_index = -1;
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
if (RenderQueue.list_items[nb_i].obj_dom_el === obj_el) {
nb_index = nb_i;
break;
}
}
if (nb_index === -1) {
return;
}
let obj_item = RenderQueue.list_items[nb_index];
if (!obj_item.str_image_path) {
return;
}
obj_event.preventDefault();
RenderQueue._nb_context_index = nb_index;
RenderQueue._show_context_menu(obj_event.clientX, obj_event.clientY);
});
obj_container.addEventListener("scroll", () => {
RenderQueue._hide_context_menu();
});
},
_show_context_menu: (nb_x, nb_y) => {
let obj_menu = RenderQueue._obj_context_menu;
obj_menu.classList.remove("d-none");
obj_menu.style.left = nb_x + "px";
obj_menu.style.top = nb_y + "px";
let obj_rect = obj_menu.getBoundingClientRect();
if (obj_rect.right > window.innerWidth) {
obj_menu.style.left = (nb_x - obj_rect.width) + "px";
}
if (obj_rect.bottom > window.innerHeight) {
obj_menu.style.top = (nb_y - obj_rect.height) + "px";
}
},
_hide_context_menu: () => {
if (RenderQueue._obj_context_menu) {
RenderQueue._obj_context_menu.classList.add("d-none");
}
},
_handle_context_action: (str_action) => {
let nb_index = RenderQueue._nb_context_index;
if (nb_index < 0 || nb_index >= RenderQueue.list_items.length) {
return;
}
let obj_item = RenderQueue.list_items[nb_index];
let str_path = obj_item.str_image_path;
if (!str_path) {
return;
}
if (str_action === "preview") {
PreviewPanel.show_image(str_path);
} else if (str_action === "open-folder") {
window.api.show_item_in_folder(str_path)
.catch((obj_err) => {
ConsoleLog.add("Erreur ouverture dossier : " + (obj_err.message || obj_err));
});
} else if (str_action === "open-default") {
window.api.open_file_default(str_path)
.then((obj_result) => {
if (!obj_result.is_success) {
ConsoleLog.add("Erreur ouverture : " + obj_result.str_error);
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur ouverture : " + (obj_err.message || obj_err));
});
} else if (str_action === "copy-path") {
navigator.clipboard.writeText(str_path)
.then(() => {
ConsoleLog.add("Chemin copie : " + str_path);
})
.catch(() => {
ConsoleLog.add("Erreur copie chemin.");
});
} else if (str_action === "copy-image") {
window.api.copy_image_to_clipboard(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
ConsoleLog.add("Image copiee dans le presse-papier.");
} else {
ConsoleLog.add("Erreur copie image : " + obj_result.str_error);
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur copie image : " + (obj_err.message || obj_err));
});
} else if (str_action === "file-info") {
window.api.get_file_info(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
let nb_size_kb = Math.round(obj_result.nb_size / 1024);
let str_size = nb_size_kb > 1024
? (nb_size_kb / 1024).toFixed(1) + " Mo"
: nb_size_kb + " Ko";
let obj_date = new Date(obj_result.str_modified);
let str_date = String(obj_date.getDate()).padStart(2, "0") + "/"
+ String(obj_date.getMonth() + 1).padStart(2, "0") + "/"
+ obj_date.getFullYear() + " "
+ String(obj_date.getHours()).padStart(2, "0") + ":"
+ String(obj_date.getMinutes()).padStart(2, "0") + ":"
+ String(obj_date.getSeconds()).padStart(2, "0");
ConsoleLog.add("Infos : " + str_path);
ConsoleLog.add(" Taille : " + str_size + " — Modifie : " + str_date);
} else {
ConsoleLog.add("Erreur infos : " + obj_result.str_error);
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur infos : " + (obj_err.message || obj_err));
});
} else if (str_action === "delete") {
RenderQueue._delete_single(nb_index);
} else if (str_action === "delete-from-here") {
RenderQueue._delete_from_here(nb_index);
}
},
_delete_single: (nb_index) => {
let obj_item = RenderQueue.list_items[nb_index];
let str_path = obj_item.str_image_path;
window.api.delete_rendered_file(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
obj_item.str_image_path = null;
obj_item.str_status = "deleted";
obj_item.is_click_bound = false;
RenderQueue._update_deleted_item(obj_item);
ConsoleLog.add("Supprime : " + str_path);
} else {
ConsoleLog.add("Erreur suppression : " + obj_result.str_error);
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur suppression : " + (obj_err.message || obj_err));
});
},
_delete_from_here: (nb_index) => {
let list_paths = [];
for (let nb_i = nb_index; nb_i < RenderQueue.list_items.length; nb_i++) {
let obj_it = RenderQueue.list_items[nb_i];
if (obj_it.str_image_path) {
list_paths.push(obj_it.str_image_path);
}
}
if (list_paths.length === 0) {
ConsoleLog.add("Aucun fichier a supprimer.");
return;
}
let is_confirm = confirm("Supprimer " + list_paths.length + " fichier(s) depuis cet element ?\n\nCette action est irreversible.");
if (!is_confirm) {
return;
}
window.api.delete_rendered_files(list_paths)
.then((obj_result) => {
if (obj_result.is_success) {
for (let nb_i = nb_index; nb_i < RenderQueue.list_items.length; nb_i++) {
let obj_it = RenderQueue.list_items[nb_i];
if (obj_it.str_image_path) {
obj_it.str_image_path = null;
obj_it.str_status = "deleted";
obj_it.is_click_bound = false;
RenderQueue._update_deleted_item(obj_it);
}
}
ConsoleLog.add(obj_result.nb_deleted + " fichier(s) supprime(s).");
if (obj_result.list_errors.length > 0) {
for (let str_err of obj_result.list_errors) {
ConsoleLog.add("Erreur : " + str_err);
}
}
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur suppression : " + (obj_err.message || obj_err));
});
},
_update_deleted_item: (obj_item) => {
if (!obj_item.obj_dom_el) {
return;
}
obj_item.obj_dom_icon.className = "mdi mdi-file-remove-outline text-muted";
obj_item.obj_dom_el.classList.remove("queue-item-clickable");
if (obj_item.obj_dom_result) {
obj_item.obj_dom_result.textContent = "supprime";
obj_item.obj_dom_result.className = "text-muted queue-item-result";
obj_item.obj_dom_result.style.minWidth = "140px";
obj_item.obj_dom_result.style.textAlign = "right";
obj_item.obj_dom_result.style.fontSize = "0.75em";
}
obj_item.str_dom_status = "deleted";
},
}; };

View File

@@ -0,0 +1,152 @@
const RenderSettings = {
obj_settings: null,
init: () => {
RenderSettings.obj_settings = {
str_engine: "CYCLES",
nb_samples: 128,
str_device: "GPU",
is_denoise: true,
is_film_transparent: false,
nb_resolution_percentage: 100,
};
},
set_from_blend: (obj_render) => {
if (!obj_render) {
return;
}
let obj_s = RenderSettings.obj_settings;
obj_s.str_engine = obj_render.str_engine || "CYCLES";
obj_s.nb_resolution_percentage = obj_render.nb_resolution_percentage || 100;
obj_s.is_film_transparent = !!obj_render.is_film_transparent;
if (obj_render.str_cycles_device) {
obj_s.str_device = obj_render.str_cycles_device;
}
if (obj_render.is_cycles_denoise !== undefined) {
obj_s.is_denoise = obj_render.is_cycles_denoise;
}
if (obj_render.nb_cycles_samples !== undefined) {
obj_s.nb_samples = obj_render.nb_cycles_samples;
} else if (obj_render.nb_eevee_samples !== undefined) {
obj_s.nb_samples = obj_render.nb_eevee_samples;
}
RenderSettings.render();
},
set_from_config: (obj_config) => {
if (!obj_config || !obj_config.obj_render_settings) {
return;
}
let obj_src = obj_config.obj_render_settings;
let obj_s = RenderSettings.obj_settings;
if (obj_src.str_engine !== undefined) { obj_s.str_engine = obj_src.str_engine; }
if (obj_src.nb_samples !== undefined) { obj_s.nb_samples = obj_src.nb_samples; }
if (obj_src.str_device !== undefined) { obj_s.str_device = obj_src.str_device; }
if (obj_src.is_denoise !== undefined) { obj_s.is_denoise = obj_src.is_denoise; }
if (obj_src.is_film_transparent !== undefined) { obj_s.is_film_transparent = obj_src.is_film_transparent; }
if (obj_src.nb_resolution_percentage !== undefined) { obj_s.nb_resolution_percentage = obj_src.nb_resolution_percentage; }
RenderSettings.render();
},
get_settings: () => {
return Object.assign({}, RenderSettings.obj_settings);
},
render: () => {
let obj_container = document.getElementById("container_render_settings");
if (!obj_container) {
return;
}
let obj_s = RenderSettings.obj_settings;
let is_cycles = obj_s.str_engine === "CYCLES";
let str_html = '<div class="row g-2">'
+ ' <div class="col-12">'
+ ' <label class="form-label form-label-sm">Moteur de rendu</label>'
+ ' <select class="form-select form-select-sm bg-dark text-light border-secondary" id="select_engine">'
+ ' <option value="CYCLES"' + (obj_s.str_engine === "CYCLES" ? " selected" : "") + ">Cycles</option>"
+ ' <option value="BLENDER_EEVEE_NEXT"' + (obj_s.str_engine === "BLENDER_EEVEE_NEXT" ? " selected" : "") + ">EEVEE</option>"
+ ' <option value="BLENDER_EEVEE"' + (obj_s.str_engine === "BLENDER_EEVEE" ? " selected" : "") + ">EEVEE (Legacy)</option>"
+ ' <option value="BLENDER_WORKBENCH"' + (obj_s.str_engine === "BLENDER_WORKBENCH" ? " selected" : "") + ">Workbench</option>"
+ " </select>"
+ " </div>"
+ ' <div class="col-6">'
+ ' <label class="form-label form-label-sm">Samples</label>'
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_samples" value="' + obj_s.nb_samples + '" min="1">'
+ " </div>"
+ ' <div class="col-6">'
+ ' <label class="form-label form-label-sm">Resolution %</label>'
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_res_percent" value="' + obj_s.nb_resolution_percentage + '" min="1" max="1000">'
+ " </div>"
+ ' <div class="col-12' + (is_cycles ? "" : " d-none") + '" id="container_device">'
+ ' <label class="form-label form-label-sm">Device</label>'
+ ' <select class="form-select form-select-sm bg-dark text-light border-secondary" id="select_device">'
+ ' <option value="GPU"' + (obj_s.str_device === "GPU" ? " selected" : "") + ">GPU</option>"
+ ' <option value="CPU"' + (obj_s.str_device === "CPU" ? " selected" : "") + ">CPU</option>"
+ " </select>"
+ " </div>"
+ ' <div class="col-6">'
+ ' <div class="form-check form-check-sm mt-1">'
+ ' <input class="form-check-input" type="checkbox" id="check_denoise"' + (obj_s.is_denoise ? " checked" : "") + ">"
+ ' <label class="form-check-label" for="check_denoise">Denoising</label>'
+ " </div>"
+ " </div>"
+ ' <div class="col-6">'
+ ' <div class="form-check form-check-sm mt-1">'
+ ' <input class="form-check-input" type="checkbox" id="check_film_transparent"' + (obj_s.is_film_transparent ? " checked" : "") + ">"
+ ' <label class="form-check-label" for="check_film_transparent">Transparent</label>'
+ " </div>"
+ " </div>"
+ "</div>";
obj_container.innerHTML = str_html;
RenderSettings._bind_events();
},
_bind_events: () => {
let obj_select_engine = document.getElementById("select_engine");
let obj_input_samples = document.getElementById("input_samples");
let obj_input_res_percent = document.getElementById("input_res_percent");
let obj_select_device = document.getElementById("select_device");
let obj_check_denoise = document.getElementById("check_denoise");
let obj_check_transparent = document.getElementById("check_film_transparent");
obj_select_engine.addEventListener("change", () => {
RenderSettings.obj_settings.str_engine = obj_select_engine.value;
let obj_device_container = document.getElementById("container_device");
if (obj_select_engine.value === "CYCLES") {
obj_device_container.classList.remove("d-none");
} else {
obj_device_container.classList.add("d-none");
}
});
obj_input_samples.addEventListener("change", () => {
RenderSettings.obj_settings.nb_samples = parseInt(obj_input_samples.value, 10) || 128;
});
obj_input_res_percent.addEventListener("change", () => {
RenderSettings.obj_settings.nb_resolution_percentage = parseInt(obj_input_res_percent.value, 10) || 100;
});
obj_select_device.addEventListener("change", () => {
RenderSettings.obj_settings.str_device = obj_select_device.value;
});
obj_check_denoise.addEventListener("change", () => {
RenderSettings.obj_settings.is_denoise = obj_check_denoise.checked;
});
obj_check_transparent.addEventListener("change", () => {
RenderSettings.obj_settings.is_film_transparent = obj_check_transparent.checked;
});
},
};

View File

@@ -39,7 +39,8 @@ body.has-update-banner .container-fluid {
height: 100%; height: 100%;
} }
.col-md-4 { .col-md-3,
.col-md-6 {
max-height: 100%; max-height: 100%;
overflow-y: auto; overflow-y: auto;
} }
@@ -75,6 +76,30 @@ body.has-update-banner .container-fluid {
border-color: #495057 !important; border-color: #495057 !important;
} }
/* ── Collection list ────────────────────────────────────────── */
#container_collection_list .list-group-item {
font-size: 0.8rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.collection-name {
font-size: 0.8rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.collection-original-badge {
font-size: 0.7rem;
flex-shrink: 0;
}
.collection-original-badge .mdi {
font-size: 0.85rem;
}
/* ── Preview ────────────────────────────────────────────────── */ /* ── Preview ────────────────────────────────────────────────── */
.preview-image { .preview-image {
@@ -221,6 +246,47 @@ body.has-update-banner .container-fluid {
background-color: #6c757d; background-color: #6c757d;
} }
/* ── Context menu ──────────────────────────────────────────── */
.context-menu {
position: fixed;
z-index: 9999;
background-color: #1e1e2e;
border: 1px solid #444;
border-radius: 6px;
padding: 4px 0;
min-width: 220px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}
.context-menu-item {
padding: 5px 12px;
font-size: 0.78rem;
color: #e1e4e8;
cursor: pointer;
display: flex;
align-items: center;
white-space: nowrap;
}
.context-menu-item:hover {
background-color: rgba(13, 110, 253, 0.25);
}
.context-menu-danger {
color: #f87171;
}
.context-menu-danger:hover {
background-color: rgba(248, 113, 113, 0.2);
}
.context-menu-separator {
height: 1px;
background-color: #444;
margin: 3px 8px;
}
/* ── Badge ──────────────────────────────────────────────────── */ /* ── Badge ──────────────────────────────────────────────────── */
.badge { .badge {

View File

@@ -1,3 +1,3 @@
{ {
"str_version": "1.2.0" "str_version": "1.6.4"
} }