12 Commits

Author SHA1 Message Date
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
sorlinv
68815645a4 fix: auto-update corrige — tag, robustesse UpdateManager, .env securise
- Supprime .env du tracking git (token expose) + ajout .gitignore
- UpdateManager : timeout download 60s, validation root par version.json,
  erreur si fichiers requis manquants, drain response sur redirect,
  gestion redirects 3xx, protection double-clic, logging erreurs
- UpdateBanner : log erreur dans le .catch au lieu de silencieux
- release.sh : tag cree localement avant push pour eviter desync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 10:28:55 +01:00
sorlinv
725ab74d1f Update release.sh 2026-02-27 09:54:18 +01:00
sorlinv
20a016bd3a better queue render 2026-02-26 17:32:00 +01:00
sorlinv
a65700175b chore: release v1.2.0 2026-02-26 16:29:12 +01:00
sorlinv
def63c9b06 chore: release v1.2.0 2026-02-26 16:21:35 +01:00
sorlinv
447801870e chore: release v1.2.0 2026-02-26 16:19:46 +01:00
sorlinv
25fc46851c chore: release v1.2.0 2026-02-26 16:13:18 +01:00
sorlinv
d5cb63a27b v1.1.0 — chemin Blender configurable depuis l'UI
Refonte de PathResolver : auto-detection systeme (which/where + chemins courants),
persistance du chemin dans userData, validation du fichier.
Ajout d'un modal Bootstrap pour configurer le chemin manuellement.
Badge vert/rouge dans la navbar indiquant le statut Blender.
Le modal s'ouvre automatiquement si Blender n'est pas trouve au lancement.
Suppression de extraResources (plus de Blender embarque dans le build).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 16:44:04 +01:00
23 changed files with 1525 additions and 119 deletions

View File

@@ -14,7 +14,23 @@
"Bash(powershell.exe -NoProfile -Command \"Get-Process | Where-Object { $_.ProcessName -match ''electron|Blender|Multi'' } | Select-Object Id, ProcessName | Format-Table\")",
"Bash(tasklist:*)",
"Bash(\"/c/Users/pc-valentin/Documents/GitHub/multi_render_blender/dist2/win-unpacked/Multi Render Blender.exe\")",
"Bash(npm run build:*)"
"Bash(npm run build:*)",
"WebFetch(domain:git.sorlinv.fr)",
"Bash(zip:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git tag:*)",
"Bash(node:*)",
"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 \":*)"
]
}
}

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ dist/
config/saves/
blender/
*.log
.env

78
main.js
View File

@@ -5,9 +5,34 @@ const CameraParser = require("./src/main/CameraParser.js");
const QueueManager = require("./src/main/QueueManager.js");
const ConfigManager = require("./src/main/ConfigManager.js");
const UpdateManager = require("./src/main/UpdateManager.js");
const PathResolver = require("./src/main/PathResolver.js");
let obj_main_window = null;
let obj_queue_manager = null;
let obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
const STR_NOTIFICATION_CONFIG_FILE = "notification_config.json";
const _load_notification_config = () => {
let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE);
try {
if (fs.existsSync(str_config_path)) {
let str_content = fs.readFileSync(str_config_path, "utf8");
obj_notification_config = JSON.parse(str_content);
}
} catch (obj_err) {
console.error("Erreur lecture config notifications :", obj_err.message);
}
};
const _save_notification_config = () => {
let str_config_path = path.join(app.getPath("userData"), STR_NOTIFICATION_CONFIG_FILE);
try {
fs.writeFileSync(str_config_path, JSON.stringify(obj_notification_config, null, 4), "utf8");
} catch (obj_err) {
console.error("Erreur sauvegarde config notifications :", obj_err.message);
}
};
const create_window = () => {
obj_main_window = new BrowserWindow({
@@ -27,9 +52,14 @@ const create_window = () => {
obj_queue_manager = new QueueManager(obj_main_window);
PathResolver.load_saved_path();
_load_notification_config();
obj_queue_manager.set_notification_config(obj_notification_config);
UpdateManager.init(obj_main_window);
obj_main_window.webContents.on("did-finish-load", () => {
UpdateManager.check_for_updates();
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
});
};
@@ -77,6 +107,10 @@ ipcMain.handle("start-render", (event, obj_config) => {
return obj_queue_manager.start(obj_config);
});
ipcMain.handle("check-queue", (event, obj_config) => {
return obj_queue_manager.check_queue(obj_config);
});
ipcMain.handle("pause-render", () => {
return obj_queue_manager.pause();
});
@@ -159,6 +193,37 @@ ipcMain.handle("read-image", (event, str_image_path) => {
return fn_try_read(0);
});
ipcMain.handle("get-blender-path", () => {
return PathResolver.get_status();
});
ipcMain.handle("set-blender-path", (event, str_path) => {
let obj_result = PathResolver.set_blender_path(str_path);
if (obj_result.is_success) {
obj_main_window.webContents.send("blender-path-status", PathResolver.get_status());
}
return obj_result;
});
ipcMain.handle("select-blender-exe", () => {
let str_exe_name = process.platform === "win32" ? "blender.exe" : "blender";
let list_filters = process.platform === "win32"
? [{ name: "Blender", extensions: ["exe"] }]
: [{ name: "Blender", extensions: ["*"] }];
return dialog.showOpenDialog(obj_main_window, {
title: "Selectionner l'executable Blender",
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("check-for-updates", () => {
return UpdateManager.check_for_updates();
});
@@ -179,3 +244,16 @@ ipcMain.handle("select-output-folder", () => {
return obj_result.filePaths[0];
});
});
// ── Notification Config ───────────────────────────────────────
ipcMain.handle("get-notification-config", () => {
return obj_notification_config;
});
ipcMain.handle("set-notification-config", (event, obj_config) => {
obj_notification_config = obj_config;
_save_notification_config();
obj_queue_manager.set_notification_config(obj_notification_config);
return { is_success: true };
});

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "multi-render-blender",
"version": "1.0.0",
"version": "1.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "multi-render-blender",
"version": "1.0.0",
"version": "1.3.0",
"license": "MIT",
"devDependencies": {
"electron": "^34.0.0",

View File

@@ -1,11 +1,12 @@
{
"name": "multi-render-blender",
"version": "1.0.0",
"version": "1.4.0",
"description": "Application Electron pour piloter des rendus Blender multi-cameras",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder"
"build": "electron-builder --linux dir --win dir --x64",
"release": "bash release.sh"
},
"author": "",
"license": "MIT",
@@ -23,13 +24,6 @@
"src/**/*",
"version.json"
],
"extraResources": [
{
"from": "blender",
"to": "blender",
"filter": ["**/*"]
}
],
"linux": {
"target": "dir"
},

View File

@@ -13,6 +13,10 @@ contextBridge.exposeInMainWorld("api", {
return ipcRenderer.invoke("start-render", obj_config);
},
check_queue: (obj_config) => {
return ipcRenderer.invoke("check-queue", obj_config);
},
pause_render: () => {
return ipcRenderer.invoke("pause-render");
},
@@ -67,6 +71,32 @@ contextBridge.exposeInMainWorld("api", {
});
},
get_blender_path: () => {
return ipcRenderer.invoke("get-blender-path");
},
set_blender_path: (str_path) => {
return ipcRenderer.invoke("set-blender-path", str_path);
},
select_blender_exe: () => {
return ipcRenderer.invoke("select-blender-exe");
},
on_blender_path_status: (fn_callback) => {
ipcRenderer.on("blender-path-status", (event, obj_data) => {
fn_callback(obj_data);
});
},
get_notification_config: () => {
return ipcRenderer.invoke("get-notification-config");
},
set_notification_config: (obj_config) => {
return ipcRenderer.invoke("set-notification-config", obj_config);
},
check_for_updates: () => {
return ipcRenderer.invoke("check-for-updates");
},

176
release.sh Executable file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
set -euo pipefail
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
############################################
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi
if [ -z "${GITEA_TOKEN:-}" ]; then
echo "Erreur: GITEA_TOKEN non defini. Creez un fichier .env avec GITEA_TOKEN=votre_token"
exit 1
fi
############################################
# Config
############################################
GITEA_URL="https://git.sorlinv.fr"
OWNER="sorlinv"
REPO="multi_render_blender"
PRODUCT_NAME="multi-render-blender"
############################################
# Version actuelle
############################################
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "Version actuelle: v$CURRENT_VERSION"
echo ""
if [ -n "$BUMP_ARG" ]; then
case "$BUMP_ARG" in
patch) npm version patch --no-git-tag-version ;;
minor) npm version minor --no-git-tag-version ;;
major) npm version major --no-git-tag-version ;;
*) echo "Erreur: -i accepte patch, minor ou major"; exit 1 ;;
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
1) npm version patch --no-git-tag-version ;;
2) npm version minor --no-git-tag-version ;;
3) npm version major --no-git-tag-version ;;
4) echo "Version inchangee." ;;
*) echo "Choix invalide"; exit 1 ;;
esac
fi
VERSION=$(node -p "require('./package.json').version")
TAG="v$VERSION"
if [ "$CURRENT_VERSION" = "$VERSION" ]; then
echo "Aucune modification de version."
else
echo "Nouvelle version: $VERSION"
fi
############################################
# Mise a jour version.json (robuste)
############################################
echo "Synchronisation de version.json..."
if command -v jq >/dev/null 2>&1; then
tmp=$(mktemp)
jq --arg v "$VERSION" '.str_version=$v' version.json > "$tmp"
mv "$tmp" version.json
else
node <<EOF
const fs = require('fs');
const file = 'version.json';
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
data.str_version = "$VERSION";
fs.writeFileSync(file, JSON.stringify(data, null, 4) + '\n');
EOF
fi
############################################
# Commit + tag local + push
############################################
git add package.json version.json
git commit -m "chore: release ${TAG}" || true
git tag -a "${TAG}" -m "Release ${TAG}"
git push
git push origin "${TAG}"
############################################
# Build
############################################
echo ""
echo "==> Build de la version $TAG"
rm -rf dist
npm run build
############################################
# ZIP
############################################
cd dist
if [ -d "linux-unpacked" ]; then
echo "Zip linux-unpacked..."
zip -r -q "${PRODUCT_NAME}-${VERSION}-linux-x64.zip" "linux-unpacked/"
fi
if [ -d "win-unpacked" ]; then
echo "Zip win-unpacked..."
zip -r -q "${PRODUCT_NAME}-${VERSION}-win-x64.zip" "win-unpacked/"
fi
cd ..
############################################
# Release Gitea
############################################
echo ""
echo "==> Upload de la release $TAG sur Gitea..."
RELEASE_RESPONSE=$(curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"${TAG}\",
\"body\": \"Release ${TAG}\",
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id // empty')
if [ -z "$RELEASE_ID" ]; then
echo "Erreur lors de la creation de la release:"
echo "$RELEASE_RESPONSE"
exit 1
fi
echo "Release creee (id: $RELEASE_ID)"
############################################
# Upload assets
############################################
for FILE in dist/${PRODUCT_NAME}-${VERSION}-*.zip; do
[ -f "$FILE" ] || continue
FILENAME=$(basename "$FILE")
echo "Upload de $FILENAME..."
curl -s -X POST \
"${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=${FILENAME}&token=${GITEA_TOKEN}" \
-F "attachment=@${FILE}"
done
echo ""
echo "==> Release publiee : ${GITEA_URL}/${OWNER}/${REPO}/releases"
echo "Done!"

View File

@@ -13,13 +13,56 @@ const BlenderProcess = {
let str_safe_name = str_camera_name.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
let str_python_expr = [
let list_python_lines = [
"import bpy",
"scene=bpy.context.scene",
"scene.camera=bpy.data.objects['" + str_safe_name + "']",
"scene.render.resolution_x=" + nb_resolution_x,
"scene.render.resolution_y=" + nb_resolution_y,
].join(";");
];
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 = [
"-b", str_blend_path,

View File

@@ -4,12 +4,25 @@ const PathResolver = require("./PathResolver.js");
const STR_SCENE_MARKER = "SCENE_JSON:";
const STR_PYTHON_EXPR = [
"import bpy, json, sys;",
"s=bpy.context.scene;",
"cams=[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}};",
"sys.stdout.write('SCENE_JSON:' + json.dumps(info) + '\\n');",
"sys.stdout.flush()",
"import bpy, json, sys",
"\ns=bpy.context.scene",
"\ncams=[o.name for o in bpy.data.objects if o.type=='CAMERA']",
"\ncols=[]",
"\ndef _wlc(lc,d):",
"\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("");
const CameraParser = {

View File

@@ -1,49 +1,156 @@
const path = require("path");
const fs = require("fs");
const { app } = require("electron");
const { execFileSync } = require("child_process");
const STR_EXE_NAME = process.platform === "win32" ? "blender.exe" : "blender";
const STR_CONFIG_FILE = "blender_path.json";
const PathResolver = {
_str_blender_path: null,
_is_found: false,
get_blender_path: () => {
if (PathResolver._str_blender_path) {
return PathResolver._str_blender_path;
load_saved_path: () => {
let str_config_path = PathResolver._get_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_blender_path = obj_data.str_path;
PathResolver._is_found = true;
return;
}
}
} catch (obj_err) {
console.error("PathResolver: impossible de lire la config :", obj_err.message);
}
// Mode package : resources/blender/
let str_resources_dir = path.join(process.resourcesPath, "blender");
let str_found = PathResolver._find_in_dir(str_resources_dir);
if (str_found) {
PathResolver._str_blender_path = str_found;
return str_found;
}
// Mode dev : racine projet/blender/
let str_dev_dir = path.join(__dirname, "..", "..", "blender");
str_found = PathResolver._find_in_dir(str_dev_dir);
if (str_found) {
PathResolver._str_blender_path = str_found;
return str_found;
}
// Fallback : PATH systeme
let str_detected = PathResolver.auto_detect();
if (str_detected) {
PathResolver._str_blender_path = str_detected;
PathResolver._is_found = true;
} else {
PathResolver._str_blender_path = "blender";
return "blender";
PathResolver._is_found = false;
}
},
_find_in_dir: (str_dir) => {
if (!fs.existsSync(str_dir)) {
return null;
get_blender_path: () => {
if (!PathResolver._str_blender_path) {
PathResolver.load_saved_path();
}
return PathResolver._str_blender_path;
},
is_found: () => {
return PathResolver._is_found;
},
set_blender_path: (str_path) => {
if (!str_path || !fs.existsSync(str_path)) {
return { is_success: false, str_error: "Fichier introuvable : " + str_path };
}
let list_entries = fs.readdirSync(str_dir);
for (let str_entry of list_entries) {
let str_exe = path.join(str_dir, str_entry, STR_EXE_NAME);
PathResolver._str_blender_path = str_path;
PathResolver._is_found = true;
let str_config_path = PathResolver._get_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 :", obj_err.message);
}
return { is_success: true, str_path: str_path };
},
auto_detect: () => {
if (process.platform === "win32") {
return PathResolver._auto_detect_windows();
}
return PathResolver._auto_detect_linux();
},
get_status: () => {
return {
str_path: PathResolver._str_blender_path || "blender",
is_found: PathResolver._is_found,
};
},
// ── Private ──────────────────────────────────────────────
_get_config_path: () => {
return path.join(app.getPath("userData"), STR_CONFIG_FILE);
},
_auto_detect_linux: () => {
let LIST_PATHS = [
"/usr/bin/blender",
"/snap/bin/blender",
"/usr/local/bin/blender",
"/opt/blender/blender",
];
try {
let str_result = execFileSync("which", ["blender"], { encoding: "utf8", timeout: 5000 }).trim();
if (str_result && fs.existsSync(str_result)) {
return str_result;
}
} catch (obj_err) {
// which not found or blender not in PATH
}
for (let str_path of LIST_PATHS) {
if (fs.existsSync(str_path)) {
return str_path;
}
}
return null;
},
_auto_detect_windows: () => {
try {
let str_result = execFileSync("where", ["blender.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) {
// where not found or blender not in PATH
}
let LIST_BASES = [
process.env.PROGRAMFILES || "C:\\Program Files",
process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)",
];
for (let str_base of LIST_BASES) {
let str_foundation = path.join(str_base, "Blender Foundation");
if (!fs.existsSync(str_foundation)) {
continue;
}
try {
let list_dirs = fs.readdirSync(str_foundation);
for (let str_dir of list_dirs) {
let str_exe = path.join(str_foundation, str_dir, STR_EXE_NAME);
if (fs.existsSync(str_exe)) {
return str_exe;
}
}
} catch (obj_err) {
// permission denied or similar
}
}
return null;
},

View File

@@ -1,10 +1,13 @@
const BlenderProcess = require("./BlenderProcess.js");
const path = require("path");
const fs = require("fs");
const { Notification } = require("electron");
const STR_STATUS_IDLE = "idle";
const STR_STATUS_RUNNING = "running";
const STR_STATUS_PAUSED = "paused";
const STR_PLACEHOLDER_CONTENT = "RENDERING";
const NB_PLACEHOLDER_MAX_SIZE = 64;
class QueueManager {
constructor(obj_window) {
@@ -15,6 +18,11 @@ class QueueManager {
this.obj_current_process = null;
this.nb_last_render_ms = 0;
this.str_last_image_path = null;
this.obj_notification_config = { is_notify_each_image: false, is_notify_all_done: true };
}
set_notification_config(obj_config) {
this.obj_notification_config = obj_config || { is_notify_each_image: false, is_notify_all_done: true };
}
start(obj_config) {
@@ -31,6 +39,8 @@ class QueueManager {
this.nb_last_render_ms = 0;
this.str_last_image_path = null;
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;
this._send_log("File de rendu construite : " + this.list_queue.length + " elements.");
this._send_progress();
@@ -56,10 +66,39 @@ class QueueManager {
this.obj_current_process = 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";
}
}
this._send_log("Rendu arrete.");
this._send_progress();
return Promise.resolve({ is_success: true });
}
check_queue(obj_config) {
let list_queue = this._build_queue(obj_config);
let list_existing = [];
for (let nb_i = 0; nb_i < list_queue.length; nb_i++) {
let obj_item = list_queue[nb_i];
try {
if (fs.existsSync(obj_item.str_expected_file)) {
let nb_size = fs.statSync(obj_item.str_expected_file).size;
if (nb_size > 0) {
list_existing.push({ nb_index: nb_i, str_path: obj_item.str_expected_file });
}
}
} catch (obj_err) {
// Fichier inaccessible, on ignore
}
}
return Promise.resolve({ list_existing: list_existing, nb_total: list_queue.length });
}
_build_queue(obj_config) {
let list_queue = [];
let str_blend_path = obj_config.str_blend_file;
@@ -159,12 +198,21 @@ class QueueManager {
let nb_skip_count = 0;
while (this.nb_current_index < this.list_queue.length && this.str_overwrite_mode === "skip") {
let obj_check = this.list_queue[this.nb_current_index];
if (!fs.existsSync(obj_check.str_expected_file)) {
let is_exists = false;
let nb_size = 0;
try {
is_exists = fs.existsSync(obj_check.str_expected_file);
if (is_exists) {
nb_size = fs.statSync(obj_check.str_expected_file).size;
}
} catch (obj_fs_err) {
is_exists = false;
}
if (!is_exists) {
break;
}
let obj_stats = fs.statSync(obj_check.str_expected_file);
if (obj_stats.size === 0) {
this._send_log("Placeholder vide detecte, re-rendu : " + obj_check.str_camera_name + " F" + obj_check.nb_frame);
if (nb_size === 0) {
break;
}
obj_check.str_status = "skipped";
@@ -181,10 +229,33 @@ class QueueManager {
this.str_status = STR_STATUS_IDLE;
this._send_log("Tous les rendus sont termines !");
this._send_event("render-complete", { is_all_done: true });
if (this.obj_notification_config.is_notify_all_done) {
this._send_notification("Rendus termines", "Tous les rendus de la file sont termines (" + this.list_queue.length + " elements).");
}
return;
}
let obj_item = this.list_queue[this.nb_current_index];
// Re-verification avant rendu : rattrape les fichiers non detectes par le batch skip
if (this.str_overwrite_mode === "skip") {
try {
if (fs.existsSync(obj_item.str_expected_file)) {
let nb_recheck_size = fs.statSync(obj_item.str_expected_file).size;
if (nb_recheck_size > 0) {
obj_item.str_status = "skipped";
this._send_log("Skip : " + obj_item.str_camera_name + " F" + obj_item.nb_frame + " (existant)");
this.nb_current_index++;
this._send_progress();
this._process_next();
return;
}
}
} catch (obj_recheck_err) {
// Erreur fs, on continue le rendu
}
}
obj_item.str_status = "rendering";
if (this.str_overwrite_mode === "skip") {
@@ -193,7 +264,7 @@ class QueueManager {
if (!fs.existsSync(str_dir)) {
fs.mkdirSync(str_dir, { recursive: true });
}
fs.writeFileSync(obj_item.str_expected_file, "");
fs.writeFileSync(obj_item.str_expected_file, STR_PLACEHOLDER_CONTENT);
} catch (obj_file_err) {
this._send_log("ERREUR creation placeholder : " + obj_file_err.message);
}
@@ -212,6 +283,8 @@ class QueueManager {
nb_resolution_y: obj_item.nb_resolution_y,
str_format: obj_item.str_format,
str_output_path: obj_item.str_output_path,
list_collections: this.list_collections,
obj_render_settings: this.obj_render_settings,
fn_on_stdout: (str_data) => {
this._send_log(str_data.trim());
},
@@ -234,6 +307,10 @@ class QueueManager {
this._send_event("preview-update", str_image);
this._send_log("Termine : " + str_image);
if (this.obj_notification_config.is_notify_each_image) {
this._send_notification("Rendu termine", obj_item.str_camera_name + " — Frame " + obj_item.nb_frame);
}
this.nb_current_index++;
this._send_progress();
this._process_next();
@@ -244,7 +321,7 @@ class QueueManager {
if (this.str_overwrite_mode === "skip" && fs.existsSync(obj_item.str_expected_file)) {
try {
let obj_stats = fs.statSync(obj_item.str_expected_file);
if (obj_stats.size === 0) {
if (obj_stats.size <= NB_PLACEHOLDER_MAX_SIZE) {
fs.unlinkSync(obj_item.str_expected_file);
}
} catch (obj_cleanup_err) {
@@ -260,11 +337,12 @@ class QueueManager {
this.nb_last_render_ms = Date.now() - nb_start;
this.str_last_image_path = null;
this._send_log("ERREUR : " + obj_err.str_message);
let str_err_msg = obj_err.str_message || obj_err.message || String(obj_err);
this._send_log("ERREUR : " + str_err_msg);
this._send_event("render-error", {
str_camera: obj_item.str_camera_name,
nb_frame: obj_item.nb_frame,
str_error: obj_err.str_message,
str_error: str_err_msg,
});
this.nb_current_index++;
@@ -276,9 +354,14 @@ class QueueManager {
_send_progress() {
let obj_item = this.list_queue[this.nb_current_index] || {};
let list_skipped = [];
let list_stopped = [];
let list_skipped_paths = [];
for (let nb_i = 0; nb_i < this.list_queue.length; nb_i++) {
if (this.list_queue[nb_i].str_status === "skipped") {
list_skipped.push(nb_i);
list_skipped_paths.push({ nb_index: nb_i, str_path: this.list_queue[nb_i].str_expected_file });
} else if (this.list_queue[nb_i].str_status === "stopped") {
list_stopped.push(nb_i);
}
}
this._send_event("render-progress", {
@@ -290,6 +373,8 @@ class QueueManager {
nb_last_render_ms: this.nb_last_render_ms,
str_last_image_path: this.str_last_image_path,
list_skipped: list_skipped,
list_stopped: list_stopped,
list_skipped_paths: list_skipped_paths,
});
}
@@ -302,6 +387,13 @@ class QueueManager {
this.obj_window.webContents.send(str_channel, obj_data);
}
}
_send_notification(str_title, str_body) {
if (Notification.isSupported()) {
let obj_notif = new Notification({ title: str_title, body: str_body });
obj_notif.show();
}
}
}
module.exports = QueueManager;

View File

@@ -8,11 +8,15 @@ const { execFile } = require("child_process");
const GITEA_HOST = "git.sorlinv.fr";
const REPO_PATH = "/api/v1/repos/sorlinv/multi_render_blender/tags";
const ARCHIVE_URL_BASE = "https://git.sorlinv.fr/sorlinv/multi_render_blender/archive/";
const NB_TIMEOUT = 10000;
const NB_TIMEOUT_API = 10000;
const NB_TIMEOUT_DOWNLOAD = 60000;
const LIST_REQUIRED_FILES = ["main.js", "version.json"];
const LIST_TARGETS = ["main.js", "preload.js", "src", "version.json", "package.json"];
const UpdateManager = {
obj_window: null,
str_local_version: null,
is_updating: false,
init: (obj_window) => {
UpdateManager.obj_window = obj_window;
@@ -61,12 +65,18 @@ const UpdateManager = {
return null;
})
.catch(() => {
.catch((obj_err) => {
console.error("UpdateManager: check_for_updates echoue :", obj_err.message);
return null;
});
},
download_and_apply: (str_tag_name) => {
if (UpdateManager.is_updating) {
return Promise.reject(new Error("Mise a jour deja en cours"));
}
UpdateManager.is_updating = true;
let str_temp_dir = path.join(app.getPath("temp"), "mrb_update_" + Date.now());
let str_zip_path = path.join(str_temp_dir, "update.zip");
@@ -102,11 +112,14 @@ const UpdateManager = {
nb_percent: 100,
});
UpdateManager._cleanup(str_temp_dir);
UpdateManager.is_updating = false;
app.relaunch();
app.quit();
})
.catch((obj_err) => {
UpdateManager.is_updating = false;
UpdateManager._cleanup(str_temp_dir);
console.error("UpdateManager: download_and_apply echoue :", obj_err.message);
UpdateManager._send_event("update-error", {
str_message: obj_err.message || "Erreur inconnue",
str_tag_name: str_tag_name,
@@ -154,7 +167,7 @@ const UpdateManager = {
hostname: GITEA_HOST,
path: REPO_PATH,
method: "GET",
timeout: NB_TIMEOUT,
timeout: NB_TIMEOUT_API,
headers: {
"Accept": "application/json",
"User-Agent": "MultiRenderBlender",
@@ -208,10 +221,11 @@ const UpdateManager = {
let obj_module = obj_parsed.protocol === "https:" ? https : http;
let obj_req = obj_module.get(str_download_url, {
timeout: NB_TIMEOUT,
timeout: NB_TIMEOUT_DOWNLOAD,
headers: { "User-Agent": "MultiRenderBlender" },
}, (obj_res) => {
if (obj_res.statusCode === 301 || obj_res.statusCode === 302) {
if (obj_res.statusCode >= 300 && obj_res.statusCode < 400) {
obj_res.resume();
let str_redirect = obj_res.headers.location;
if (!str_redirect) {
reject(new Error("Redirection sans header Location"));
@@ -224,6 +238,7 @@ const UpdateManager = {
}
if (obj_res.statusCode !== 200) {
obj_res.resume();
reject(new Error("Telechargement echoue : HTTP " + obj_res.statusCode));
return;
}
@@ -274,13 +289,8 @@ const UpdateManager = {
_extract_zip: (str_zip_path, str_dest_dir) => {
return new Promise((resolve, reject) => {
if (process.platform === "win32") {
let str_cmd = "Expand-Archive";
let list_args = [
"-Path", str_zip_path,
"-DestinationPath", str_dest_dir,
"-Force",
];
execFile("powershell.exe", ["-Command", str_cmd + " " + list_args.join(" ")], (obj_err) => {
let str_script = "Expand-Archive -Path '" + str_zip_path + "' -DestinationPath '" + str_dest_dir + "' -Force";
execFile("powershell.exe", ["-NoProfile", "-Command", str_script], (obj_err) => {
if (obj_err) {
reject(new Error("Extraction echouee : " + obj_err.message));
return;
@@ -309,14 +319,34 @@ const UpdateManager = {
continue;
}
let str_full = path.join(str_temp_dir, str_entry);
if (fs.statSync(str_full).isDirectory()) {
if (!fs.statSync(str_full).isDirectory()) {
continue;
}
let str_version_check = path.join(str_full, "version.json");
if (fs.existsSync(str_version_check)) {
str_root = str_full;
break;
}
let list_sub = fs.readdirSync(str_full);
for (let str_sub of list_sub) {
let str_sub_full = path.join(str_full, str_sub);
if (fs.statSync(str_sub_full).isDirectory()) {
let str_nested_check = path.join(str_sub_full, "version.json");
if (fs.existsSync(str_nested_check)) {
str_root = str_sub_full;
break;
}
}
}
if (str_root) {
break;
}
}
if (!str_root) {
reject(new Error("Dossier extrait introuvable"));
reject(new Error("Dossier extrait introuvable (version.json absent)"));
return;
}
@@ -326,15 +356,23 @@ const UpdateManager = {
_replace_app_files: (str_source_dir) => {
let str_app_dir = path.join(__dirname, "..", "..");
let LIST_TARGETS = ["main.js", "preload.js", "src", "version.json", "package.json"];
return new Promise((resolve, reject) => {
try {
for (let str_required of LIST_REQUIRED_FILES) {
let str_check = path.join(str_source_dir, str_required);
if (!fs.existsSync(str_check)) {
reject(new Error("Fichier requis manquant dans la mise a jour : " + str_required));
return;
}
}
for (let str_target of LIST_TARGETS) {
let str_src = path.join(str_source_dir, str_target);
let str_dest = path.join(str_app_dir, str_target);
if (!fs.existsSync(str_src)) {
console.log("UpdateManager: cible absente dans la source, ignoree : " + str_target);
continue;
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;">
<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:;">
<title>Multi Render Blender</title>
<!-- Bootstrap 5 -->
@@ -33,8 +33,8 @@
<div class="container-fluid p-3">
<div class="row g-3">
<!-- ── Left Column : File + Cameras ─────────────── -->
<div class="col-md-4 d-flex flex-column gap-3">
<!-- ── Left Column : File + Settings ──────────── -->
<div class="col-md-3 d-flex flex-column gap-3">
<!-- Blend file selection -->
<div class="card bg-dark border-secondary">
@@ -99,6 +99,18 @@
</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 -->
<div class="card bg-dark border-secondary">
<div class="card-header border-secondary">
@@ -132,15 +144,19 @@
</div>
</div>
</div>
</div>
<!-- ── Center Column : Cameras + Config ──────────── -->
<div class="col-md-3 d-flex flex-column gap-3">
<!-- 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">
<span><i class="mdi mdi-camera-outline me-1"></i>Cameras</span>
<span id="badge_camera_count" class="badge bg-secondary">0</span>
</div>
<div class="card-body p-0">
<div id="container_camera_list" class="list-group list-group-flush overflow-auto" style="max-height: 400px;">
<div 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">
<i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>
Chargez un fichier .blend
@@ -148,13 +164,31 @@
</div>
</div>
</div>
<!-- Collections -->
<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>
<!-- ── Center Column : Camera Config + Controls ── -->
<div class="col-md-4 d-flex flex-column gap-3">
<!-- Camera config -->
<div class="card bg-dark border-secondary">
<div class="card bg-dark border-secondary flex-grow-1">
<div class="card-header border-secondary">
<i class="mdi mdi-cog-outline me-1"></i>Configuration : <span id="label_selected_camera">-</span>
</div>
@@ -164,6 +198,10 @@
</div>
</div>
</div>
</div>
<!-- ── Right Column : Controls + Queue + Preview + Console ── -->
<div class="col-md-6 d-flex flex-column gap-3">
<!-- Render controls -->
<div class="card bg-dark border-secondary">
@@ -172,6 +210,9 @@
</div>
<div class="card-body">
<div class="d-flex gap-2 mb-3">
<button id="btn_check_queue" class="btn btn-outline-info" disabled title="Verifier les fichiers existants">
<i class="mdi mdi-check-all"></i>
</button>
<button id="btn_start" class="btn btn-success flex-fill" disabled>
<i class="mdi mdi-play"></i> Start
</button>
@@ -199,6 +240,9 @@
</div>
</div>
<!-- Render queue + Preview side by side -->
<div class="row g-3 flex-grow-1">
<div class="col-6 d-flex flex-column">
<!-- Render queue -->
<div class="card bg-dark border-secondary flex-grow-1">
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
@@ -217,17 +261,14 @@
</div>
</div>
</div>
<!-- ── Right Column : Preview + Console ─────────── -->
<div class="col-md-4 d-flex flex-column gap-3">
<div class="col-6 d-flex flex-column">
<!-- Preview -->
<div class="card bg-dark border-secondary">
<div class="card bg-dark border-secondary flex-grow-1">
<div class="card-header border-secondary">
<i class="mdi mdi-image-outline me-1"></i>Preview
</div>
<div class="card-body p-2 text-center" id="container_preview">
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;">
<div class="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
@@ -235,9 +276,11 @@
</div>
</div>
</div>
</div>
</div>
<!-- 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">
<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">
@@ -245,7 +288,7 @@
</button>
</div>
<div class="card-body p-0">
<div id="container_console" class="console-output overflow-auto p-2" style="max-height: 300px; min-height: 200px;">
<div id="container_console" class="console-output overflow-auto p-2" style="max-height: 200px; min-height: 150px;">
</div>
</div>
</div>
@@ -253,14 +296,21 @@
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Scripts -->
<script src="scripts/ConsoleLog.js"></script>
<script src="scripts/RenderSettings.js"></script>
<script src="scripts/CameraList.js"></script>
<script src="scripts/CollectionList.js"></script>
<script src="scripts/CameraConfig.js"></script>
<script src="scripts/RenderQueue.js"></script>
<script src="scripts/PreviewPanel.js"></script>
<script src="scripts/ProgressBar.js"></script>
<script src="scripts/UpdateBanner.js"></script>
<script src="scripts/BlenderPath.js"></script>
<script src="scripts/NotificationConfig.js"></script>
<script src="scripts/App.js"></script>
</body>
</html>

View File

@@ -4,12 +4,16 @@ const App = {
init: () => {
ConsoleLog.init();
RenderSettings.init();
CameraList.init(App._on_camera_select);
CollectionList.init();
CameraConfig.init();
RenderQueue.init();
PreviewPanel.init();
ProgressBar.init();
UpdateBanner.init();
BlenderPath.init();
NotificationConfig.init();
App._bind_events();
App._bind_render_events();
@@ -43,6 +47,11 @@ const App = {
App._stop_render();
});
let obj_btn_check = document.getElementById("btn_check_queue");
obj_btn_check.addEventListener("click", () => {
App._check_queue();
});
let obj_btn_save = document.getElementById("btn_save_config");
obj_btn_save.addEventListener("click", () => {
App._save_config();
@@ -63,6 +72,12 @@ const App = {
obj_radio_both.addEventListener("change", () => { App._update_output_example(); });
obj_input_frame_prefix.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: () => {
@@ -122,6 +137,18 @@ const App = {
CameraList.set_cameras(obj_result.list_cameras, obj_result.obj_scene);
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).");
App._update_start_button();
})
@@ -181,6 +208,8 @@ const App = {
nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path,
list_cameras: list_cameras,
list_collections: CollectionList.get_overrides(),
obj_render_settings: RenderSettings.get_settings(),
};
RenderQueue.build_display(str_mode, list_cameras);
@@ -217,6 +246,52 @@ const App = {
});
},
_check_queue: () => {
let obj_btn_check = document.getElementById("btn_check_queue");
if (obj_btn_check.disabled) {
return;
}
if (!App.str_output_path) {
ConsoleLog.add("Veuillez selectionner un dossier de sortie.");
return;
}
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
let list_cameras = CameraList.list_cameras;
let str_frame_prefix = document.getElementById("input_frame_prefix").value || "";
let nb_frame_padding = parseInt(document.getElementById("input_frame_padding").value, 10) || 5;
let obj_config = {
str_blend_file: App.str_blend_path,
str_render_mode: str_mode,
str_output_mode: str_output_mode,
str_frame_prefix: str_frame_prefix,
nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path,
list_cameras: list_cameras,
list_collections: CollectionList.get_overrides(),
};
RenderQueue.build_display(str_mode, list_cameras);
obj_btn_check.disabled = true;
ConsoleLog.add("Verification des fichiers existants...");
window.api.check_queue(obj_config)
.then((obj_result) => {
obj_btn_check.disabled = false;
RenderQueue.mark_existing(obj_result.list_existing);
let nb_existing = obj_result.list_existing.length;
let nb_to_render = obj_result.nb_total - nb_existing;
ConsoleLog.add("Verification terminee : " + nb_existing + " fichier(s) existant(s), " + nb_to_render + " a rendre.");
})
.catch((obj_err) => {
obj_btn_check.disabled = false;
ConsoleLog.add("Erreur verification : " + obj_err.message);
});
},
_save_config: () => {
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
@@ -232,6 +307,8 @@ const App = {
nb_frame_padding: nb_frame_padding,
str_output_path: App.str_output_path,
list_cameras: CameraList.list_cameras,
list_collections: CollectionList.list_collections,
obj_render_settings: RenderSettings.get_settings(),
};
window.api.save_config(obj_config)
@@ -286,6 +363,8 @@ const App = {
App._update_output_example();
RenderSettings.set_from_config(obj_config);
if (obj_config.list_cameras && obj_config.list_cameras.length > 0) {
CameraList.list_cameras = obj_config.list_cameras;
CameraList.str_selected_camera = null;
@@ -295,6 +374,15 @@ const App = {
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();
App._update_start_button();
ConsoleLog.add("Configuration importee.");
@@ -310,15 +398,18 @@ const App = {
let obj_btn_start = document.getElementById("btn_start");
let obj_btn_pause = document.getElementById("btn_pause");
let obj_btn_stop = document.getElementById("btn_stop");
let obj_btn_check = document.getElementById("btn_check_queue");
if (str_state === "running") {
obj_btn_start.disabled = true;
obj_btn_pause.disabled = false;
obj_btn_stop.disabled = false;
obj_btn_check.disabled = true;
} else if (str_state === "paused") {
obj_btn_start.disabled = false;
obj_btn_pause.disabled = true;
obj_btn_stop.disabled = false;
obj_btn_check.disabled = true;
} else {
App._update_start_button();
obj_btn_pause.disabled = true;
@@ -328,10 +419,12 @@ const App = {
_update_start_button: () => {
let obj_btn_start = document.getElementById("btn_start");
let obj_btn_check = document.getElementById("btn_check_queue");
let is_ready = App.str_blend_path
&& App.str_output_path
&& CameraList.get_enabled_cameras().length > 0;
obj_btn_start.disabled = !is_ready;
obj_btn_check.disabled = !is_ready;
},
};

View File

@@ -0,0 +1,197 @@
const BlenderPath = {
str_current_path: null,
is_found: false,
obj_modal: null,
init: () => {
BlenderPath._create_badge();
BlenderPath._create_modal();
BlenderPath._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_blender_status";
obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Chemin Blender";
obj_badge.innerHTML = '<i class="mdi mdi-blender-software"></i>';
obj_nav_right.insertBefore(obj_badge, obj_nav_right.firstChild);
obj_badge.addEventListener("click", () => {
BlenderPath._open_modal();
});
},
_create_modal: () => {
let obj_modal_el = document.createElement("div");
obj_modal_el.id = "modal_blender_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-blender-software me-2"></i>Chemin Blender</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-3" style="font-size: 0.85rem;">' +
'Selectionnez l\'executable Blender sur votre machine.' +
'</p>' +
'<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>' +
'<button id="btn_browse_blender" 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_blender" class="btn btn-sm btn-outline-secondary">' +
'<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement' +
'</button>' +
'<span id="label_blender_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_blender" 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);
BlenderPath.obj_modal = new bootstrap.Modal(obj_modal_el);
},
_bind_events: () => {
let obj_btn_browse = document.getElementById("btn_browse_blender");
obj_btn_browse.addEventListener("click", () => {
BlenderPath._browse();
});
let obj_btn_detect = document.getElementById("btn_detect_blender");
obj_btn_detect.addEventListener("click", () => {
BlenderPath._detect();
});
let obj_btn_validate = document.getElementById("btn_validate_blender");
obj_btn_validate.addEventListener("click", () => {
BlenderPath._validate();
});
window.api.on_blender_path_status((obj_data) => {
BlenderPath.str_current_path = obj_data.str_path;
BlenderPath.is_found = obj_data.is_found;
BlenderPath._update_badge();
if (!obj_data.is_found) {
BlenderPath._open_modal();
}
});
},
_open_modal: () => {
let obj_input = document.getElementById("input_blender_path");
obj_input.value = BlenderPath.is_found ? BlenderPath.str_current_path : "";
BlenderPath._update_modal_status(BlenderPath.is_found, BlenderPath.str_current_path);
BlenderPath.obj_modal.show();
if (!BlenderPath.is_found) {
BlenderPath._detect();
}
},
_browse: () => {
window.api.select_blender_exe()
.then((str_path) => {
if (!str_path) {
return;
}
let obj_input = document.getElementById("input_blender_path");
obj_input.value = str_path;
BlenderPath._update_modal_status(true, str_path);
})
.catch((obj_err) => {
ConsoleLog.add("Erreur selection Blender : " + obj_err.message);
});
},
_detect: () => {
let obj_btn = document.getElementById("btn_detect_blender");
obj_btn.disabled = true;
obj_btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Detection...';
window.api.get_blender_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_blender_path");
obj_input.value = obj_data.str_path;
BlenderPath._update_modal_status(true, obj_data.str_path);
} else {
BlenderPath._update_modal_status(false, null);
}
})
.catch(() => {
obj_btn.disabled = false;
obj_btn.innerHTML = '<i class="mdi mdi-magnify me-1"></i>Detecter automatiquement';
BlenderPath._update_modal_status(false, null);
});
},
_validate: () => {
let str_path = document.getElementById("input_blender_path").value;
if (!str_path) {
return;
}
window.api.set_blender_path(str_path)
.then((obj_result) => {
if (obj_result.is_success) {
BlenderPath.str_current_path = obj_result.str_path;
BlenderPath.is_found = true;
BlenderPath._update_badge();
BlenderPath.obj_modal.hide();
ConsoleLog.add("Chemin Blender configure : " + obj_result.str_path);
} else {
BlenderPath._update_modal_status(false, null);
ConsoleLog.add("Chemin Blender invalide : " + (obj_result.str_error || ""));
}
})
.catch((obj_err) => {
ConsoleLog.add("Erreur configuration Blender : " + obj_err.message);
});
},
_update_badge: () => {
let obj_badge = document.getElementById("btn_blender_status");
if (BlenderPath.is_found) {
obj_badge.className = "btn btn-sm btn-outline-success";
obj_badge.title = "Blender : " + BlenderPath.str_current_path;
} else {
obj_badge.className = "btn btn-sm btn-outline-danger";
obj_badge.title = "Blender non trouve";
}
},
_update_modal_status: (is_valid, str_path) => {
let obj_label = document.getElementById("label_blender_status");
let obj_btn_validate = document.getElementById("btn_validate_blender");
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

@@ -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,132 @@
const NotificationConfig = {
is_notify_each_image: false,
is_notify_all_done: true,
obj_modal: null,
init: () => {
NotificationConfig._create_badge();
NotificationConfig._create_modal();
NotificationConfig._bind_events();
NotificationConfig._load_config();
},
_create_badge: () => {
let obj_nav_right = document.querySelector("nav .d-flex.gap-2");
let obj_badge = document.createElement("button");
obj_badge.id = "btn_notification_config";
obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Notifications";
obj_badge.innerHTML = '<i class="mdi mdi-bell-outline"></i>';
let obj_blender_btn = document.getElementById("btn_blender_status");
if (obj_blender_btn) {
obj_nav_right.insertBefore(obj_badge, obj_blender_btn.nextSibling);
} else {
obj_nav_right.insertBefore(obj_badge, obj_nav_right.firstChild);
}
obj_badge.addEventListener("click", () => {
NotificationConfig._open_modal();
});
},
_create_modal: () => {
let obj_modal_el = document.createElement("div");
obj_modal_el.id = "modal_notification_config";
obj_modal_el.className = "modal fade";
obj_modal_el.tabIndex = -1;
obj_modal_el.innerHTML =
'<div class="modal-dialog modal-dialog-centered modal-sm">' +
'<div class="modal-content bg-dark text-light border-secondary">' +
'<div class="modal-header border-secondary">' +
'<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>' +
'</div>' +
'<div class="modal-body">' +
'<p class="text-light-emphasis mb-3" style="font-size: 0.85rem;">' +
'Configurez les notifications systeme.' +
'</p>' +
'<div class="form-check mb-2">' +
'<input class="form-check-input" type="checkbox" id="check_notify_each_image">' +
'<label class="form-check-label" for="check_notify_each_image">' +
'A chaque image rendue' +
'</label>' +
'</div>' +
'<div class="form-check">' +
'<input class="form-check-input" type="checkbox" id="check_notify_all_done" checked>' +
'<label class="form-check-label" for="check_notify_all_done">' +
'Quand tous les rendus sont termines' +
'</label>' +
'</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_save_notification" type="button" class="btn btn-sm btn-primary">' +
'<i class="mdi mdi-check me-1"></i>Valider' +
'</button>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(obj_modal_el);
NotificationConfig.obj_modal = new bootstrap.Modal(obj_modal_el);
},
_bind_events: () => {
let obj_btn_save = document.getElementById("btn_save_notification");
obj_btn_save.addEventListener("click", () => {
NotificationConfig._save_config();
});
},
_open_modal: () => {
document.getElementById("check_notify_each_image").checked = NotificationConfig.is_notify_each_image;
document.getElementById("check_notify_all_done").checked = NotificationConfig.is_notify_all_done;
NotificationConfig.obj_modal.show();
},
_load_config: () => {
window.api.get_notification_config()
.then((obj_config) => {
if (obj_config) {
NotificationConfig.is_notify_each_image = obj_config.is_notify_each_image || false;
NotificationConfig.is_notify_all_done = obj_config.is_notify_all_done !== undefined ? obj_config.is_notify_all_done : true;
NotificationConfig._update_badge();
}
})
.catch(() => {
// Config non trouvee, valeurs par defaut
});
},
_save_config: () => {
NotificationConfig.is_notify_each_image = document.getElementById("check_notify_each_image").checked;
NotificationConfig.is_notify_all_done = document.getElementById("check_notify_all_done").checked;
let obj_config = {
is_notify_each_image: NotificationConfig.is_notify_each_image,
is_notify_all_done: NotificationConfig.is_notify_all_done,
};
window.api.set_notification_config(obj_config)
.then(() => {
NotificationConfig._update_badge();
NotificationConfig.obj_modal.hide();
ConsoleLog.add("Configuration notifications sauvegardee.");
})
.catch((obj_err) => {
ConsoleLog.add("Erreur sauvegarde notifications : " + obj_err.message);
});
},
_update_badge: () => {
let obj_badge = document.getElementById("btn_notification_config");
let is_any_active = NotificationConfig.is_notify_each_image || NotificationConfig.is_notify_all_done;
if (is_any_active) {
obj_badge.className = "btn btn-sm btn-outline-success";
obj_badge.title = "Notifications activees";
} else {
obj_badge.className = "btn btn-sm btn-outline-secondary";
obj_badge.title = "Notifications desactivees";
}
},
};

View File

@@ -40,7 +40,7 @@ const ProgressBar = {
let obj_frame_label = document.getElementById("label_current_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 || []);
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 || []);
},
reset: () => {

View File

@@ -115,7 +115,7 @@ const RenderQueue = {
obj_container.appendChild(obj_fragment);
},
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped) => {
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped, list_stopped, list_skipped_paths) => {
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
RenderQueue.nb_total_render_ms += nb_last_render_ms;
RenderQueue.nb_completed_renders++;
@@ -126,8 +126,18 @@ const RenderQueue = {
RenderQueue.list_items[nb_current - 1].str_image_path = str_last_image_path;
}
if (list_skipped_paths) {
for (let obj_sp of list_skipped_paths) {
if (obj_sp.nb_index < RenderQueue.list_items.length) {
RenderQueue.list_items[obj_sp.nb_index].str_image_path = obj_sp.str_path;
}
}
}
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
if (list_stopped && list_stopped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "stopped";
} else if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
RenderQueue.list_items[nb_i].str_status = "skipped";
} else if (nb_i < nb_current) {
RenderQueue.list_items[nb_i].str_status = "done";
@@ -143,14 +153,20 @@ const RenderQueue = {
},
_update_statuses: () => {
let obj_rendering_el = null;
for (let obj_item of RenderQueue.list_items) {
if (!obj_item.obj_dom_el) {
continue;
}
let is_needs_click = obj_item.str_status === "done" && obj_item.str_image_path && !obj_item.is_click_bound;
let is_needs_click = (obj_item.str_status === "done" || obj_item.str_status === "skipped")
&& obj_item.str_image_path && !obj_item.is_click_bound;
if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) {
if (obj_item.str_status === "rendering") {
obj_rendering_el = obj_item.obj_dom_el;
}
continue;
}
@@ -160,6 +176,7 @@ const RenderQueue = {
if (obj_item.str_status === "rendering") {
str_icon = "mdi-loading mdi-spin";
str_color = "text-primary";
obj_rendering_el = obj_item.obj_dom_el;
} else if (obj_item.str_status === "done") {
str_icon = "mdi-check-circle";
str_color = "text-success";
@@ -169,6 +186,9 @@ const RenderQueue = {
} else if (obj_item.str_status === "skipped") {
str_icon = "mdi-skip-next-circle";
str_color = "text-info";
} else if (obj_item.str_status === "stopped") {
str_icon = "mdi-stop-circle";
str_color = "text-warning";
}
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
@@ -184,6 +204,20 @@ const RenderQueue = {
obj_item.str_dom_status = obj_item.str_status;
}
if (obj_rendering_el) {
obj_rendering_el.scrollIntoView({ behavior: "smooth", block: "center" });
}
},
mark_existing: (list_existing) => {
for (let obj_existing of list_existing) {
if (obj_existing.nb_index < RenderQueue.list_items.length) {
RenderQueue.list_items[obj_existing.nb_index].str_status = "skipped";
RenderQueue.list_items[obj_existing.nb_index].str_image_path = obj_existing.str_path;
}
}
RenderQueue._update_statuses();
},
_update_time_display: () => {

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

@@ -92,7 +92,9 @@ const UpdateBanner = {
obj_btn_close.classList.add("d-none");
window.api.apply_update(UpdateBanner.str_pending_tag)
.catch(() => {});
.catch((obj_err) => {
console.error("UpdateBanner: apply_update echoue :", obj_err);
});
},
_show_progress: (str_step, nb_percent) => {

View File

@@ -39,7 +39,8 @@ body.has-update-banner .container-fluid {
height: 100%;
}
.col-md-4 {
.col-md-3,
.col-md-6 {
max-height: 100%;
overflow-y: auto;
}
@@ -75,6 +76,30 @@ body.has-update-banner .container-fluid {
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-image {

View File

@@ -1,3 +1,3 @@
{
"str_version": "1.0.0"
"str_version": "1.4.0"
}