diff --git a/package-lock.json b/package-lock.json index 3b3cc1f..457f638 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "multi-render-blender", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "multi-render-blender", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "devDependencies": { "electron": "^34.0.0", diff --git a/release.sh b/release.sh index e61d03a..04b07c4 100755 --- a/release.sh +++ b/release.sh @@ -3,6 +3,18 @@ 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 ############################################ @@ -30,21 +42,30 @@ CURRENT_VERSION=$(node -p "require('./package.json').version") echo "Version actuelle: v$CURRENT_VERSION" echo "" -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 +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 + 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" diff --git a/src/main/BlenderProcess.js b/src/main/BlenderProcess.js index ac864e7..5b99bc1 100644 --- a/src/main/BlenderProcess.js +++ b/src/main/BlenderProcess.js @@ -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, diff --git a/src/main/CameraParser.js b/src/main/CameraParser.js index eafc4b9..d193b98 100644 --- a/src/main/CameraParser.js +++ b/src/main/CameraParser.js @@ -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 = { diff --git a/src/main/QueueManager.js b/src/main/QueueManager.js index 10e7849..ad30904 100644 --- a/src/main/QueueManager.js +++ b/src/main/QueueManager.js @@ -6,6 +6,8 @@ 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) { @@ -37,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(); @@ -209,7 +213,6 @@ class QueueManager { break; } if (nb_size === 0) { - this._send_log("Placeholder vide detecte, re-rendu : " + obj_check.str_camera_name + " F" + obj_check.nb_frame); break; } obj_check.str_status = "skipped"; @@ -261,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); } @@ -280,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()); }, @@ -316,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) { diff --git a/src/renderer/index.html b/src/renderer/index.html index 783e322..4201522 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -33,8 +33,8 @@
- -
+ +
@@ -99,6 +99,18 @@
+ +
+
+ Proprietes de rendu +
+
+
+ Chargez un fichier .blend +
+
+
+
@@ -132,15 +144,19 @@
+
+ + +
-
+
Cameras 0
-
+
Chargez un fichier .blend @@ -148,13 +164,31 @@
-
- -
+ +
+
+ Collections +
+ + 0 +
+
+
+
+
+ + Chargez un fichier .blend +
+
+
+
-
+
Configuration : -
@@ -164,6 +198,10 @@
+
+ + +
@@ -202,45 +240,47 @@
- -
-
- File de rendu -
- - 0 -
-
-
-
-
- File vide + +
+
+ +
+
+ File de rendu +
+ + 0 +
+
+
+
+
+ File vide +
+
-
-
- - -
- - -
-
- Preview -
-
-
-
- - Aucun rendu disponible +
+ +
+
+ Preview +
+
+
+
+ + Aucun rendu disponible +
+
-
+
Console
-
+
@@ -261,7 +301,9 @@ + + diff --git a/src/renderer/scripts/App.js b/src/renderer/scripts/App.js index f5ff147..99e7478 100644 --- a/src/renderer/scripts/App.js +++ b/src/renderer/scripts/App.js @@ -4,7 +4,9 @@ const App = { init: () => { ConsoleLog.init(); + RenderSettings.init(); CameraList.init(App._on_camera_select); + CollectionList.init(); CameraConfig.init(); RenderQueue.init(); PreviewPanel.init(); @@ -70,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: () => { @@ -129,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(); }) @@ -188,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); @@ -249,6 +271,7 @@ const App = { 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); @@ -284,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) @@ -338,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; @@ -347,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."); diff --git a/src/renderer/scripts/CollectionList.js b/src/renderer/scripts/CollectionList.js new file mode 100644 index 0000000..32c9a5b --- /dev/null +++ b/src/renderer/scripts/CollectionList.js @@ -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 = '
' + + '' + + "Chargez un fichier .blend" + + "
"; + 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 = ''; + 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 = ''; + 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 = '
' + + '' + + "Chargez un fichier .blend" + + "
"; + let obj_badge = document.getElementById("badge_collection_count"); + obj_badge.textContent = "0"; + }, +}; diff --git a/src/renderer/scripts/RenderSettings.js b/src/renderer/scripts/RenderSettings.js new file mode 100644 index 0000000..4a3e982 --- /dev/null +++ b/src/renderer/scripts/RenderSettings.js @@ -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 = '
' + + '
' + + ' ' + + ' " + + "
" + + '
' + + ' ' + + ' ' + + "
" + + '
' + + ' ' + + ' ' + + "
" + + '
' + + ' ' + + ' " + + "
" + + '
' + + '
' + + ' " + + ' ' + + "
" + + "
" + + '
' + + '
' + + ' " + + ' ' + + "
" + + "
" + + "
"; + + 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; + }); + }, +}; diff --git a/src/renderer/styles/Main.css b/src/renderer/styles/Main.css index 464b512..08ab28a 100644 --- a/src/renderer/styles/Main.css +++ b/src/renderer/styles/Main.css @@ -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 {