3 Commits

Author SHA1 Message Date
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
13 changed files with 550 additions and 72 deletions

View File

@@ -21,7 +21,16 @@
"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 \":*)"
] ]
} }
} }

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "multi-render-blender", "name": "multi-render-blender",
"version": "1.2.0", "version": "1.3.0",
"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": {

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

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 = {

View File

@@ -6,6 +6,8 @@ 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 STR_PLACEHOLDER_CONTENT = "RENDERING";
const NB_PLACEHOLDER_MAX_SIZE = 64;
class QueueManager { class QueueManager {
constructor(obj_window) { constructor(obj_window) {
@@ -37,6 +39,8 @@ 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.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;
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();
@@ -209,7 +213,6 @@ 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"; obj_check.str_status = "skipped";
@@ -261,7 +264,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, STR_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);
} }
@@ -280,6 +283,8 @@ class QueueManager {
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_output_path,
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());
}, },
@@ -316,7 +321,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) {

View File

@@ -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">
@@ -202,45 +240,47 @@
</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">
<div class="col-md-4 d-flex flex-column gap-3"> <i class="mdi mdi-image-outline me-1"></i>Preview
</div>
<!-- Preview --> <div class="card-body p-2 text-center" id="container_preview">
<div class="card bg-dark border-secondary"> <div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 200px;">
<div class="card-header border-secondary"> <div class="text-light-emphasis">
<i class="mdi mdi-image-outline me-1"></i>Preview <i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i>
</div> Aucun rendu disponible
<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;"> </div>
<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>
<!-- 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 +288,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>
@@ -261,7 +301,9 @@
<!-- 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>

View File

@@ -4,7 +4,9 @@ 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();
@@ -70,6 +72,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 +137,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 +208,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 +271,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 +307,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 +363,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 +374,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

@@ -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,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 {

View File

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