v1
This commit is contained in:
249
src/renderer/index.html
Normal file
249
src/renderer/index.html
Normal file
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src https://cdn.jsdelivr.net; img-src 'self' file: data:;">
|
||||
<title>Multi Render Blender</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Material Design Icons -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<!-- App CSS -->
|
||||
<link rel="stylesheet" href="styles/Main.css">
|
||||
</head>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<!-- ── Top Bar ──────────────────────────────────────────── -->
|
||||
<nav class="navbar navbar-dark bg-dark border-bottom border-secondary px-3">
|
||||
<span class="navbar-brand mb-0 h1">
|
||||
<i class="mdi mdi-blender-software me-2"></i>Multi Render Blender
|
||||
</span>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btn_load_config" class="btn btn-sm btn-outline-secondary" title="Charger config">
|
||||
<i class="mdi mdi-folder-open-outline"></i>
|
||||
</button>
|
||||
<button id="btn_save_config" class="btn btn-sm btn-outline-secondary" title="Sauvegarder config">
|
||||
<i class="mdi mdi-content-save-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid p-3">
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ── Left Column : File + Cameras ─────────────── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Blend file selection -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-file-outline me-1"></i>Fichier Blender
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="input_blend_path" class="form-control bg-dark text-light border-secondary" placeholder="Aucun fichier selectionne" readonly>
|
||||
<button id="btn_select_blend" class="btn btn-outline-primary" type="button">
|
||||
<i class="mdi mdi-folder-search-outline"></i> Parcourir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output folder -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-folder-outline me-1"></i>Dossier de sortie
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="input_output_path" class="form-control bg-dark text-light border-secondary" placeholder="Selectionnez un dossier" readonly>
|
||||
<button id="btn_select_output" class="btn btn-outline-primary" type="button">
|
||||
<i class="mdi mdi-folder-search-outline"></i> Parcourir
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_subfolder" value="subfolder" checked>
|
||||
<label class="form-check-label" for="radio_output_subfolder">
|
||||
Sous-dossier par camera
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="radio" name="output_mode" id="radio_output_prefix" value="prefix">
|
||||
<label class="form-check-label" for="radio_output_prefix">
|
||||
Nom camera dans le fichier
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<small id="label_output_example" class="text-light-emphasis">Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render mode -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-swap-horizontal me-1"></i>Mode de rendu
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="render_mode" id="radio_camera_by_camera" value="camera_by_camera" checked>
|
||||
<label class="form-check-label" for="radio_camera_by_camera">
|
||||
Camera par camera
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="render_mode" id="radio_frame_by_frame" value="frame_by_frame">
|
||||
<label class="form-check-label" for="radio_frame_by_frame">
|
||||
Frame par frame
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-2 border-secondary">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="overwrite_mode" id="radio_overwrite" value="overwrite" checked>
|
||||
<label class="form-check-label" for="radio_overwrite">
|
||||
Ecraser les fichiers existants
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="overwrite_mode" id="radio_skip" value="skip">
|
||||
<label class="form-check-label" for="radio_skip">
|
||||
Passer si le fichier existe (multi-PC)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Camera list -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-camera-outline me-1"></i>Cameras</span>
|
||||
<span id="badge_camera_count" class="badge bg-secondary">0</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_camera_list" class="list-group list-group-flush overflow-auto" style="max-height: 400px;">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
<i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>
|
||||
Chargez un fichier .blend
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Center Column : Camera Config + Controls ── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Camera config -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-cog-outline me-1"></i>Configuration : <span id="label_selected_camera">-</span>
|
||||
</div>
|
||||
<div class="card-body" id="container_camera_config">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
Selectionnez une camera
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render controls -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-play-circle-outline me-1"></i>Controles
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 mb-3">
|
||||
<button id="btn_start" class="btn btn-success flex-fill" disabled>
|
||||
<i class="mdi mdi-play"></i> Start
|
||||
</button>
|
||||
<button id="btn_pause" class="btn btn-warning flex-fill" disabled>
|
||||
<i class="mdi mdi-pause"></i> Pause
|
||||
</button>
|
||||
<button id="btn_stop" class="btn btn-danger flex-fill" disabled>
|
||||
<i class="mdi mdi-stop"></i> Stop
|
||||
</button>
|
||||
</div>
|
||||
<!-- Progress -->
|
||||
<div id="container_progress" class="mb-2">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<small id="label_progress_status">En attente</small>
|
||||
<small id="label_progress_count">0 / 0</small>
|
||||
</div>
|
||||
<div class="progress bg-secondary" style="height: 8px;">
|
||||
<div id="bar_progress" class="progress-bar bg-primary" role="progressbar" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-light-emphasis">Camera : <span id="label_current_camera">-</span></small>
|
||||
<small class="text-light-emphasis">Frame : <span id="label_current_frame">-</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Render queue -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-format-list-numbered me-1"></i>File de rendu</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<small id="label_queue_time_estimate" class="queue-time-estimate"></small>
|
||||
<span id="badge_queue_count" class="badge bg-secondary">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_render_queue" class="list-group list-group-flush overflow-auto" style="max-height: 300px;">
|
||||
<div class="text-center text-light-emphasis py-4">
|
||||
File vide
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Right Column : Preview + Console ─────────── -->
|
||||
<div class="col-md-4 d-flex flex-column gap-3">
|
||||
|
||||
<!-- Preview -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-header border-secondary">
|
||||
<i class="mdi mdi-image-outline me-1"></i>Preview
|
||||
</div>
|
||||
<div class="card-body p-2 text-center" id="container_preview">
|
||||
<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;">
|
||||
<div class="text-light-emphasis">
|
||||
<i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i>
|
||||
Aucun rendu disponible
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Console logs -->
|
||||
<div class="card bg-dark border-secondary flex-grow-1">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span><i class="mdi mdi-console me-1"></i>Console</span>
|
||||
<button id="btn_clear_console" class="btn btn-sm btn-outline-secondary" title="Vider">
|
||||
<i class="mdi mdi-delete-outline"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div id="container_console" class="console-output overflow-auto p-2" style="max-height: 300px; min-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/ConsoleLog.js"></script>
|
||||
<script src="scripts/CameraList.js"></script>
|
||||
<script src="scripts/CameraConfig.js"></script>
|
||||
<script src="scripts/RenderQueue.js"></script>
|
||||
<script src="scripts/PreviewPanel.js"></script>
|
||||
<script src="scripts/ProgressBar.js"></script>
|
||||
<script src="scripts/App.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
311
src/renderer/scripts/App.js
Normal file
311
src/renderer/scripts/App.js
Normal file
@@ -0,0 +1,311 @@
|
||||
const App = {
|
||||
str_blend_path: null,
|
||||
str_output_path: null,
|
||||
|
||||
init: () => {
|
||||
ConsoleLog.init();
|
||||
CameraList.init(App._on_camera_select);
|
||||
CameraConfig.init();
|
||||
RenderQueue.init();
|
||||
PreviewPanel.init();
|
||||
ProgressBar.init();
|
||||
|
||||
App._bind_events();
|
||||
App._bind_render_events();
|
||||
|
||||
ConsoleLog.add("Application prete.");
|
||||
},
|
||||
|
||||
_bind_events: () => {
|
||||
let obj_btn_blend = document.getElementById("btn_select_blend");
|
||||
obj_btn_blend.addEventListener("click", () => {
|
||||
App._select_blend_file();
|
||||
});
|
||||
|
||||
let obj_btn_output = document.getElementById("btn_select_output");
|
||||
obj_btn_output.addEventListener("click", () => {
|
||||
App._select_output_folder();
|
||||
});
|
||||
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
obj_btn_start.addEventListener("click", () => {
|
||||
App._start_render();
|
||||
});
|
||||
|
||||
let obj_btn_pause = document.getElementById("btn_pause");
|
||||
obj_btn_pause.addEventListener("click", () => {
|
||||
App._pause_render();
|
||||
});
|
||||
|
||||
let obj_btn_stop = document.getElementById("btn_stop");
|
||||
obj_btn_stop.addEventListener("click", () => {
|
||||
App._stop_render();
|
||||
});
|
||||
|
||||
let obj_btn_save = document.getElementById("btn_save_config");
|
||||
obj_btn_save.addEventListener("click", () => {
|
||||
App._save_config();
|
||||
});
|
||||
|
||||
let obj_btn_load = document.getElementById("btn_load_config");
|
||||
obj_btn_load.addEventListener("click", () => {
|
||||
App._load_config();
|
||||
});
|
||||
|
||||
let obj_radio_subfolder = document.getElementById("radio_output_subfolder");
|
||||
let obj_radio_prefix = document.getElementById("radio_output_prefix");
|
||||
obj_radio_subfolder.addEventListener("change", () => { App._update_output_example(); });
|
||||
obj_radio_prefix.addEventListener("change", () => { App._update_output_example(); });
|
||||
},
|
||||
|
||||
_bind_render_events: () => {
|
||||
window.api.on_render_complete((obj_data) => {
|
||||
if (obj_data.is_all_done) {
|
||||
ConsoleLog.add("Tous les rendus sont termines !");
|
||||
App._set_controls_state("idle");
|
||||
}
|
||||
});
|
||||
|
||||
window.api.on_render_error((obj_data) => {
|
||||
ConsoleLog.add("ERREUR sur " + obj_data.str_camera + " frame " + obj_data.nb_frame + " : " + obj_data.str_error);
|
||||
});
|
||||
},
|
||||
|
||||
_update_output_example: () => {
|
||||
let str_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let obj_label = document.getElementById("label_output_example");
|
||||
if (str_mode === "subfolder") {
|
||||
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>/frame_00001.png';
|
||||
} else {
|
||||
obj_label.innerHTML = 'Ex: /sortie/<strong>Camera.001</strong>_frame_00001.png';
|
||||
}
|
||||
},
|
||||
|
||||
// ── Actions ────────────────────────────────────────────
|
||||
|
||||
_select_blend_file: () => {
|
||||
let obj_btn_blend = document.getElementById("btn_select_blend");
|
||||
|
||||
window.api.select_blend_file()
|
||||
.then((str_path) => {
|
||||
if (!str_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_blend_path = str_path;
|
||||
document.getElementById("input_blend_path").value = str_path;
|
||||
ConsoleLog.add("Fichier charge : " + str_path);
|
||||
|
||||
obj_btn_blend.disabled = true;
|
||||
CameraList.show_loading();
|
||||
|
||||
return window.api.get_cameras(str_path);
|
||||
})
|
||||
.then((list_cameras) => {
|
||||
obj_btn_blend.disabled = false;
|
||||
|
||||
if (!list_cameras) {
|
||||
return;
|
||||
}
|
||||
|
||||
CameraList.set_cameras(list_cameras);
|
||||
CameraConfig.clear();
|
||||
ConsoleLog.add(list_cameras.length + " camera(s) trouvee(s).");
|
||||
App._update_start_button();
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
obj_btn_blend.disabled = false;
|
||||
ConsoleLog.add("Erreur chargement : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_select_output_folder: () => {
|
||||
window.api.select_output_folder()
|
||||
.then((str_path) => {
|
||||
if (!str_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_output_path = str_path;
|
||||
document.getElementById("input_output_path").value = str_path;
|
||||
ConsoleLog.add("Dossier de sortie : " + str_path);
|
||||
App._update_start_button();
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur selection dossier : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_on_camera_select: (obj_camera) => {
|
||||
CameraConfig.show(obj_camera);
|
||||
},
|
||||
|
||||
_start_render: () => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
if (obj_btn_start.disabled) {
|
||||
return;
|
||||
}
|
||||
obj_btn_start.disabled = true;
|
||||
|
||||
if (!App.str_output_path) {
|
||||
ConsoleLog.add("Veuillez selectionner un dossier de sortie.");
|
||||
App._set_controls_state("idle");
|
||||
return;
|
||||
}
|
||||
|
||||
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
|
||||
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let str_overwrite_mode = document.querySelector('input[name="overwrite_mode"]:checked').value;
|
||||
let list_cameras = CameraList.list_cameras;
|
||||
|
||||
let obj_config = {
|
||||
str_blend_file: App.str_blend_path,
|
||||
str_render_mode: str_mode,
|
||||
str_output_mode: str_output_mode,
|
||||
str_overwrite_mode: str_overwrite_mode,
|
||||
str_output_path: App.str_output_path,
|
||||
list_cameras: list_cameras,
|
||||
};
|
||||
|
||||
RenderQueue.build_display(str_mode, list_cameras);
|
||||
ProgressBar.reset();
|
||||
|
||||
window.api.start_render(obj_config)
|
||||
.then(() => {
|
||||
App._set_controls_state("running");
|
||||
ConsoleLog.add("Rendu lance en mode : " + str_mode);
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
App._set_controls_state("idle");
|
||||
ConsoleLog.add("Erreur lancement rendu : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_pause_render: () => {
|
||||
window.api.pause_render()
|
||||
.then(() => {
|
||||
App._set_controls_state("paused");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur pause : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_stop_render: () => {
|
||||
window.api.stop_render()
|
||||
.then(() => {
|
||||
App._set_controls_state("idle");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur arret : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_save_config: () => {
|
||||
let str_mode = document.querySelector('input[name="render_mode"]:checked').value;
|
||||
let str_output_mode = document.querySelector('input[name="output_mode"]:checked').value;
|
||||
let str_overwrite_mode = document.querySelector('input[name="overwrite_mode"]:checked').value;
|
||||
let obj_config = {
|
||||
str_blend_file: App.str_blend_path,
|
||||
str_render_mode: str_mode,
|
||||
str_output_mode: str_output_mode,
|
||||
str_overwrite_mode: str_overwrite_mode,
|
||||
str_output_path: App.str_output_path,
|
||||
list_cameras: CameraList.list_cameras,
|
||||
};
|
||||
|
||||
window.api.save_config(obj_config)
|
||||
.then((obj_result) => {
|
||||
if (obj_result && obj_result.is_success) {
|
||||
ConsoleLog.add("Configuration exportee : " + obj_result.str_path);
|
||||
}
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur sauvegarde : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
_load_config: () => {
|
||||
window.api.load_config()
|
||||
.then((obj_config) => {
|
||||
if (!obj_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
App.str_blend_path = obj_config.str_blend_file;
|
||||
App.str_output_path = obj_config.str_output_path || null;
|
||||
document.getElementById("input_blend_path").value = obj_config.str_blend_file || "";
|
||||
document.getElementById("input_output_path").value = obj_config.str_output_path || "";
|
||||
|
||||
if (obj_config.str_render_mode === "frame_by_frame") {
|
||||
document.getElementById("radio_frame_by_frame").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_camera_by_camera").checked = true;
|
||||
}
|
||||
|
||||
if (obj_config.str_output_mode === "prefix") {
|
||||
document.getElementById("radio_output_prefix").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_output_subfolder").checked = true;
|
||||
}
|
||||
|
||||
if (obj_config.str_overwrite_mode === "skip") {
|
||||
document.getElementById("radio_skip").checked = true;
|
||||
} else {
|
||||
document.getElementById("radio_overwrite").checked = true;
|
||||
}
|
||||
|
||||
App._update_output_example();
|
||||
|
||||
if (obj_config.list_cameras && obj_config.list_cameras.length > 0) {
|
||||
CameraList.list_cameras = obj_config.list_cameras;
|
||||
CameraList.str_selected_camera = null;
|
||||
CameraList.render();
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = String(obj_config.list_cameras.length);
|
||||
}
|
||||
|
||||
CameraConfig.clear();
|
||||
App._update_start_button();
|
||||
ConsoleLog.add("Configuration importee.");
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur chargement config : " + obj_err.message);
|
||||
});
|
||||
},
|
||||
|
||||
// ── UI State ───────────────────────────────────────────
|
||||
|
||||
_set_controls_state: (str_state) => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
let obj_btn_pause = document.getElementById("btn_pause");
|
||||
let obj_btn_stop = document.getElementById("btn_stop");
|
||||
|
||||
if (str_state === "running") {
|
||||
obj_btn_start.disabled = true;
|
||||
obj_btn_pause.disabled = false;
|
||||
obj_btn_stop.disabled = false;
|
||||
} else if (str_state === "paused") {
|
||||
obj_btn_start.disabled = false;
|
||||
obj_btn_pause.disabled = true;
|
||||
obj_btn_stop.disabled = false;
|
||||
} else {
|
||||
App._update_start_button();
|
||||
obj_btn_pause.disabled = true;
|
||||
obj_btn_stop.disabled = true;
|
||||
}
|
||||
},
|
||||
|
||||
_update_start_button: () => {
|
||||
let obj_btn_start = document.getElementById("btn_start");
|
||||
let is_ready = App.str_blend_path
|
||||
&& App.str_output_path
|
||||
&& CameraList.get_enabled_cameras().length > 0;
|
||||
obj_btn_start.disabled = !is_ready;
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
App.init();
|
||||
});
|
||||
86
src/renderer/scripts/CameraConfig.js
Normal file
86
src/renderer/scripts/CameraConfig.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const CameraConfig = {
|
||||
obj_current_camera: null,
|
||||
|
||||
init: () => {
|
||||
// Initialized on demand when a camera is selected
|
||||
},
|
||||
|
||||
show: (obj_camera) => {
|
||||
CameraConfig.obj_current_camera = obj_camera;
|
||||
|
||||
let obj_label = document.getElementById("label_selected_camera");
|
||||
obj_label.textContent = obj_camera.str_name;
|
||||
|
||||
let obj_container = document.getElementById("container_camera_config");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
let str_html = ""
|
||||
+ '<div class="row g-2">'
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Resolution X</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_res_x" value="' + obj_camera.nb_resolution_x + '" min="1">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Resolution Y</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_res_y" value="' + obj_camera.nb_resolution_y + '" min="1">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Frame start</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_start" value="' + obj_camera.nb_frame_start + '" min="0">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-6">'
|
||||
+ ' <label class="form-label form-label-sm">Frame end</label>'
|
||||
+ ' <input type="number" class="form-control form-control-sm bg-dark text-light border-secondary" id="input_frame_end" value="' + obj_camera.nb_frame_end + '" min="0">'
|
||||
+ " </div>"
|
||||
+ ' <div class="col-12">'
|
||||
+ ' <label class="form-label form-label-sm">Format</label>'
|
||||
+ ' <select class="form-select form-select-sm bg-dark text-light border-secondary" id="select_format">'
|
||||
+ ' <option value="PNG"' + (obj_camera.str_format === "PNG" ? " selected" : "") + ">PNG</option>"
|
||||
+ ' <option value="JPEG"' + (obj_camera.str_format === "JPEG" ? " selected" : "") + ">JPEG</option>"
|
||||
+ ' <option value="OPEN_EXR"' + (obj_camera.str_format === "OPEN_EXR" ? " selected" : "") + ">EXR</option>"
|
||||
+ ' <option value="BMP"' + (obj_camera.str_format === "BMP" ? " selected" : "") + ">BMP</option>"
|
||||
+ ' <option value="TIFF"' + (obj_camera.str_format === "TIFF" ? " selected" : "") + ">TIFF</option>"
|
||||
+ " </select>"
|
||||
+ " </div>"
|
||||
+ ' <div class="col-12 mt-3">'
|
||||
+ ' <button class="btn btn-sm btn-primary w-100" id="btn_apply_config">'
|
||||
+ ' <i class="mdi mdi-check me-1"></i>Appliquer'
|
||||
+ " </button>"
|
||||
+ " </div>"
|
||||
+ "</div>";
|
||||
|
||||
obj_container.innerHTML = str_html;
|
||||
|
||||
CameraConfig._bind_events();
|
||||
},
|
||||
|
||||
_bind_events: () => {
|
||||
let obj_btn_apply = document.getElementById("btn_apply_config");
|
||||
obj_btn_apply.addEventListener("click", () => {
|
||||
CameraConfig._apply();
|
||||
});
|
||||
},
|
||||
|
||||
_apply: () => {
|
||||
let obj_cam = CameraConfig.obj_current_camera;
|
||||
if (!obj_cam) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj_cam.nb_resolution_x = parseInt(document.getElementById("input_res_x").value, 10) || 1920;
|
||||
obj_cam.nb_resolution_y = parseInt(document.getElementById("input_res_y").value, 10) || 1080;
|
||||
obj_cam.nb_frame_start = parseInt(document.getElementById("input_frame_start").value, 10) || 1;
|
||||
obj_cam.nb_frame_end = parseInt(document.getElementById("input_frame_end").value, 10) || 250;
|
||||
obj_cam.str_format = document.getElementById("select_format").value;
|
||||
|
||||
ConsoleLog.add("Config appliquee pour " + obj_cam.str_name);
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
CameraConfig.obj_current_camera = null;
|
||||
let obj_label = document.getElementById("label_selected_camera");
|
||||
obj_label.textContent = "-";
|
||||
let obj_container = document.getElementById("container_camera_config");
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">Selectionnez une camera</div>';
|
||||
},
|
||||
};
|
||||
116
src/renderer/scripts/CameraList.js
Normal file
116
src/renderer/scripts/CameraList.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const CameraList = {
|
||||
list_cameras: [],
|
||||
str_selected_camera: null,
|
||||
fn_on_select: null,
|
||||
|
||||
init: (fn_on_select) => {
|
||||
CameraList.fn_on_select = fn_on_select;
|
||||
},
|
||||
|
||||
set_cameras: (list_names) => {
|
||||
CameraList.list_cameras = [];
|
||||
|
||||
for (let str_name of list_names) {
|
||||
CameraList.list_cameras.push({
|
||||
str_name: str_name,
|
||||
is_enabled: true,
|
||||
nb_resolution_x: 1920,
|
||||
nb_resolution_y: 1080,
|
||||
nb_frame_start: 1,
|
||||
nb_frame_end: 250,
|
||||
str_format: "PNG",
|
||||
});
|
||||
}
|
||||
|
||||
CameraList.str_selected_camera = null;
|
||||
CameraList.render();
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = String(list_names.length);
|
||||
},
|
||||
|
||||
get_camera_by_name: (str_name) => {
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
if (obj_cam.str_name === str_name) {
|
||||
return obj_cam;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
get_enabled_cameras: () => {
|
||||
let list_enabled = [];
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
if (obj_cam.is_enabled) {
|
||||
list_enabled.push(obj_cam);
|
||||
}
|
||||
}
|
||||
return list_enabled;
|
||||
},
|
||||
|
||||
show_loading: () => {
|
||||
let obj_container = document.getElementById("container_camera_list");
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-loading mdi-spin d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargement des cameras..."
|
||||
+ "</div>";
|
||||
|
||||
let obj_badge = document.getElementById("badge_camera_count");
|
||||
obj_badge.textContent = "0";
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let obj_container = document.getElementById("container_camera_list");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
if (CameraList.list_cameras.length === 0) {
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-camera-off-outline d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargez un fichier .blend"
|
||||
+ "</div>";
|
||||
return;
|
||||
}
|
||||
|
||||
for (let obj_cam of CameraList.list_cameras) {
|
||||
let obj_item = document.createElement("div");
|
||||
obj_item.classList.add("list-group-item", "list-group-item-action", "bg-dark", "text-light", "border-secondary", "d-flex", "align-items-center", "gap-2");
|
||||
|
||||
if (obj_cam.str_name === CameraList.str_selected_camera) {
|
||||
obj_item.classList.add("active");
|
||||
}
|
||||
|
||||
let obj_checkbox = document.createElement("input");
|
||||
obj_checkbox.type = "checkbox";
|
||||
obj_checkbox.classList.add("form-check-input");
|
||||
obj_checkbox.checked = obj_cam.is_enabled;
|
||||
obj_checkbox.addEventListener("change", (event) => {
|
||||
event.stopPropagation();
|
||||
obj_cam.is_enabled = obj_checkbox.checked;
|
||||
});
|
||||
|
||||
let obj_icon = document.createElement("i");
|
||||
obj_icon.classList.add("mdi", "mdi-camera-outline");
|
||||
|
||||
let obj_label = document.createElement("span");
|
||||
obj_label.classList.add("flex-grow-1");
|
||||
obj_label.textContent = obj_cam.str_name;
|
||||
|
||||
obj_item.appendChild(obj_checkbox);
|
||||
obj_item.appendChild(obj_icon);
|
||||
obj_item.appendChild(obj_label);
|
||||
|
||||
obj_item.addEventListener("click", (event) => {
|
||||
if (event.target === obj_checkbox) {
|
||||
return;
|
||||
}
|
||||
CameraList.str_selected_camera = obj_cam.str_name;
|
||||
CameraList.render();
|
||||
if (CameraList.fn_on_select) {
|
||||
CameraList.fn_on_select(obj_cam);
|
||||
}
|
||||
});
|
||||
|
||||
obj_container.appendChild(obj_item);
|
||||
}
|
||||
},
|
||||
};
|
||||
78
src/renderer/scripts/ConsoleLog.js
Normal file
78
src/renderer/scripts/ConsoleLog.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const NB_MAX_CONSOLE_LINES = 500;
|
||||
const NB_FLUSH_INTERVAL = 100;
|
||||
|
||||
const ConsoleLog = {
|
||||
obj_container: null,
|
||||
_list_buffer: [],
|
||||
_nb_flush_timer: null,
|
||||
|
||||
init: () => {
|
||||
ConsoleLog.obj_container = document.getElementById("container_console");
|
||||
|
||||
let obj_btn_clear = document.getElementById("btn_clear_console");
|
||||
obj_btn_clear.addEventListener("click", () => {
|
||||
ConsoleLog.clear();
|
||||
});
|
||||
|
||||
window.api.on_log((str_message) => {
|
||||
ConsoleLog.add(str_message);
|
||||
});
|
||||
},
|
||||
|
||||
add: (str_message) => {
|
||||
ConsoleLog._list_buffer.push(str_message);
|
||||
|
||||
if (ConsoleLog._nb_flush_timer === null) {
|
||||
ConsoleLog._nb_flush_timer = setTimeout(() => {
|
||||
ConsoleLog._flush();
|
||||
}, NB_FLUSH_INTERVAL);
|
||||
}
|
||||
},
|
||||
|
||||
_flush: () => {
|
||||
ConsoleLog._nb_flush_timer = null;
|
||||
|
||||
let list_messages = ConsoleLog._list_buffer;
|
||||
ConsoleLog._list_buffer = [];
|
||||
|
||||
let obj_fragment = document.createDocumentFragment();
|
||||
|
||||
for (let str_message of list_messages) {
|
||||
let obj_line = document.createElement("div");
|
||||
obj_line.classList.add("console-line");
|
||||
|
||||
let obj_time = document.createElement("span");
|
||||
obj_time.classList.add("console-time");
|
||||
let obj_date = new Date();
|
||||
let str_time = String(obj_date.getHours()).padStart(2, "0")
|
||||
+ ":" + String(obj_date.getMinutes()).padStart(2, "0")
|
||||
+ ":" + String(obj_date.getSeconds()).padStart(2, "0");
|
||||
obj_time.textContent = str_time;
|
||||
|
||||
let obj_text = document.createElement("span");
|
||||
obj_text.classList.add("console-text");
|
||||
obj_text.textContent = str_message;
|
||||
|
||||
obj_line.appendChild(obj_time);
|
||||
obj_line.appendChild(obj_text);
|
||||
obj_fragment.appendChild(obj_line);
|
||||
}
|
||||
|
||||
ConsoleLog.obj_container.appendChild(obj_fragment);
|
||||
|
||||
while (ConsoleLog.obj_container.childElementCount > NB_MAX_CONSOLE_LINES) {
|
||||
ConsoleLog.obj_container.removeChild(ConsoleLog.obj_container.firstChild);
|
||||
}
|
||||
|
||||
ConsoleLog.obj_container.scrollTop = ConsoleLog.obj_container.scrollHeight;
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
ConsoleLog._list_buffer = [];
|
||||
if (ConsoleLog._nb_flush_timer !== null) {
|
||||
clearTimeout(ConsoleLog._nb_flush_timer);
|
||||
ConsoleLog._nb_flush_timer = null;
|
||||
}
|
||||
ConsoleLog.obj_container.innerHTML = "";
|
||||
},
|
||||
};
|
||||
58
src/renderer/scripts/PreviewPanel.js
Normal file
58
src/renderer/scripts/PreviewPanel.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const PreviewPanel = {
|
||||
obj_container: null,
|
||||
|
||||
init: () => {
|
||||
PreviewPanel.obj_container = document.getElementById("container_preview");
|
||||
|
||||
window.api.on_preview_update((str_image_path) => {
|
||||
PreviewPanel.show_image(str_image_path);
|
||||
});
|
||||
},
|
||||
|
||||
show_image: (str_image_path) => {
|
||||
if (!str_image_path) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">'
|
||||
+ '<i class="mdi mdi-loading mdi-spin d-block mb-2" style="font-size: 2rem;"></i>'
|
||||
+ "Chargement..."
|
||||
+ "</div>";
|
||||
|
||||
window.api.read_image(str_image_path)
|
||||
.then((str_data_url) => {
|
||||
PreviewPanel.obj_container.innerHTML = "";
|
||||
|
||||
let obj_img = document.createElement("img");
|
||||
obj_img.classList.add("preview-image");
|
||||
obj_img.src = str_data_url;
|
||||
obj_img.alt = "Rendu";
|
||||
|
||||
let obj_label = document.createElement("div");
|
||||
obj_label.classList.add("preview-label", "text-light-emphasis", "mt-1");
|
||||
|
||||
let str_filename = str_image_path.replace(/\\/g, "/");
|
||||
let list_parts = str_filename.split("/");
|
||||
obj_label.textContent = list_parts[list_parts.length - 1];
|
||||
|
||||
PreviewPanel.obj_container.appendChild(obj_img);
|
||||
PreviewPanel.obj_container.appendChild(obj_label);
|
||||
})
|
||||
.catch((obj_err) => {
|
||||
ConsoleLog.add("Erreur preview : " + (obj_err.message || obj_err));
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="text-center text-warning py-4">'
|
||||
+ '<i class="mdi mdi-image-broken-variant d-block mb-2" style="font-size: 3rem;"></i>'
|
||||
+ "Impossible de charger l'image"
|
||||
+ '<div class="mt-1" style="font-size: 0.7rem;">' + str_image_path + "</div>"
|
||||
+ "</div>";
|
||||
});
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
PreviewPanel.obj_container.innerHTML = '<div class="preview-placeholder d-flex align-items-center justify-content-center" style="min-height: 300px;">'
|
||||
+ '<div class="text-light-emphasis">'
|
||||
+ '<i class="mdi mdi-image-off-outline d-block mb-2" style="font-size: 3rem;"></i>'
|
||||
+ "Aucun rendu disponible"
|
||||
+ "</div></div>";
|
||||
},
|
||||
};
|
||||
54
src/renderer/scripts/ProgressBar.js
Normal file
54
src/renderer/scripts/ProgressBar.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const ProgressBar = {
|
||||
init: () => {
|
||||
window.api.on_render_progress((obj_data) => {
|
||||
ProgressBar.update(obj_data);
|
||||
});
|
||||
},
|
||||
|
||||
update: (obj_data) => {
|
||||
let nb_current = obj_data.nb_current || 0;
|
||||
let nb_total = obj_data.nb_total || 0;
|
||||
let str_camera = obj_data.str_camera || "-";
|
||||
let nb_frame = obj_data.nb_frame || 0;
|
||||
let str_status = obj_data.str_status || "idle";
|
||||
|
||||
let nb_percent = 0;
|
||||
if (nb_total > 0) {
|
||||
nb_percent = Math.round((nb_current / nb_total) * 100);
|
||||
}
|
||||
|
||||
let obj_bar = document.getElementById("bar_progress");
|
||||
obj_bar.style.width = nb_percent + "%";
|
||||
|
||||
let obj_count = document.getElementById("label_progress_count");
|
||||
obj_count.textContent = nb_current + " / " + nb_total;
|
||||
|
||||
let obj_status = document.getElementById("label_progress_status");
|
||||
if (str_status === "running") {
|
||||
obj_status.textContent = "Rendu en cours...";
|
||||
} else if (str_status === "paused") {
|
||||
obj_status.textContent = "En pause";
|
||||
} else if (str_status === "idle" && nb_current >= nb_total && nb_total > 0) {
|
||||
obj_status.textContent = "Termine";
|
||||
} else {
|
||||
obj_status.textContent = "En attente";
|
||||
}
|
||||
|
||||
let obj_camera_label = document.getElementById("label_current_camera");
|
||||
obj_camera_label.textContent = str_camera;
|
||||
|
||||
let obj_frame_label = document.getElementById("label_current_frame");
|
||||
obj_frame_label.textContent = nb_frame > 0 ? String(nb_frame) : "-";
|
||||
|
||||
RenderQueue.update_progress(nb_current, obj_data.nb_last_render_ms || 0, obj_data.str_last_image_path || null, obj_data.list_skipped || []);
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
let obj_bar = document.getElementById("bar_progress");
|
||||
obj_bar.style.width = "0%";
|
||||
document.getElementById("label_progress_count").textContent = "0 / 0";
|
||||
document.getElementById("label_progress_status").textContent = "En attente";
|
||||
document.getElementById("label_current_camera").textContent = "-";
|
||||
document.getElementById("label_current_frame").textContent = "-";
|
||||
},
|
||||
};
|
||||
234
src/renderer/scripts/RenderQueue.js
Normal file
234
src/renderer/scripts/RenderQueue.js
Normal file
@@ -0,0 +1,234 @@
|
||||
const RenderQueue = {
|
||||
list_items: [],
|
||||
nb_total_render_ms: 0,
|
||||
nb_completed_renders: 0,
|
||||
nb_last_current: 0,
|
||||
|
||||
init: () => {
|
||||
// Initialized on demand
|
||||
},
|
||||
|
||||
build_display: (str_mode, list_cameras) => {
|
||||
RenderQueue.list_items = [];
|
||||
RenderQueue.nb_total_render_ms = 0;
|
||||
RenderQueue.nb_completed_renders = 0;
|
||||
RenderQueue.nb_last_current = 0;
|
||||
|
||||
let list_enabled = [];
|
||||
for (let obj_cam of list_cameras) {
|
||||
if (obj_cam.is_enabled) {
|
||||
list_enabled.push(obj_cam);
|
||||
}
|
||||
}
|
||||
|
||||
if (str_mode === "camera_by_camera") {
|
||||
for (let obj_cam of list_enabled) {
|
||||
for (let nb_frame = obj_cam.nb_frame_start; nb_frame <= obj_cam.nb_frame_end; nb_frame++) {
|
||||
RenderQueue.list_items.push({
|
||||
str_camera: obj_cam.str_name,
|
||||
nb_frame: nb_frame,
|
||||
str_status: "pending",
|
||||
str_image_path: null,
|
||||
obj_dom_el: null,
|
||||
obj_dom_icon: null,
|
||||
str_dom_status: null,
|
||||
is_click_bound: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let nb_min = Infinity;
|
||||
let nb_max = -Infinity;
|
||||
for (let obj_cam of list_enabled) {
|
||||
if (obj_cam.nb_frame_start < nb_min) {
|
||||
nb_min = obj_cam.nb_frame_start;
|
||||
}
|
||||
if (obj_cam.nb_frame_end > nb_max) {
|
||||
nb_max = obj_cam.nb_frame_end;
|
||||
}
|
||||
}
|
||||
|
||||
for (let nb_frame = nb_min; nb_frame <= nb_max; nb_frame++) {
|
||||
for (let obj_cam of list_enabled) {
|
||||
if (nb_frame >= obj_cam.nb_frame_start && nb_frame <= obj_cam.nb_frame_end) {
|
||||
RenderQueue.list_items.push({
|
||||
str_camera: obj_cam.str_name,
|
||||
nb_frame: nb_frame,
|
||||
str_status: "pending",
|
||||
str_image_path: null,
|
||||
obj_dom_el: null,
|
||||
obj_dom_icon: null,
|
||||
str_dom_status: null,
|
||||
is_click_bound: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let obj_badge = document.getElementById("badge_queue_count");
|
||||
obj_badge.textContent = String(RenderQueue.list_items.length);
|
||||
|
||||
RenderQueue._update_time_display();
|
||||
RenderQueue._create_dom();
|
||||
},
|
||||
|
||||
_create_dom: () => {
|
||||
let obj_container = document.getElementById("container_render_queue");
|
||||
obj_container.innerHTML = "";
|
||||
|
||||
if (RenderQueue.list_items.length === 0) {
|
||||
obj_container.innerHTML = '<div class="text-center text-light-emphasis py-4">File vide</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let obj_fragment = document.createDocumentFragment();
|
||||
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
let obj_el = document.createElement("div");
|
||||
obj_el.classList.add("list-group-item", "bg-dark", "text-light", "border-secondary", "py-1", "px-3", "d-flex", "align-items-center", "gap-2");
|
||||
|
||||
let obj_icon = document.createElement("i");
|
||||
obj_icon.classList.add("mdi", "mdi-clock-outline", "text-muted");
|
||||
|
||||
let obj_name = document.createElement("small");
|
||||
obj_name.classList.add("flex-grow-1");
|
||||
obj_name.textContent = obj_item.str_camera;
|
||||
|
||||
let obj_frame = document.createElement("small");
|
||||
obj_frame.classList.add("text-light-emphasis");
|
||||
obj_frame.textContent = "F" + obj_item.nb_frame;
|
||||
|
||||
obj_el.appendChild(obj_icon);
|
||||
obj_el.appendChild(obj_name);
|
||||
obj_el.appendChild(obj_frame);
|
||||
|
||||
obj_item.obj_dom_el = obj_el;
|
||||
obj_item.obj_dom_icon = obj_icon;
|
||||
obj_item.str_dom_status = "pending";
|
||||
|
||||
obj_fragment.appendChild(obj_el);
|
||||
}
|
||||
|
||||
obj_container.appendChild(obj_fragment);
|
||||
},
|
||||
|
||||
update_progress: (nb_current, nb_last_render_ms, str_last_image_path, list_skipped) => {
|
||||
if (nb_current > RenderQueue.nb_last_current && nb_last_render_ms > 0) {
|
||||
RenderQueue.nb_total_render_ms += nb_last_render_ms;
|
||||
RenderQueue.nb_completed_renders++;
|
||||
}
|
||||
RenderQueue.nb_last_current = nb_current;
|
||||
|
||||
if (str_last_image_path && nb_current > 0 && nb_current - 1 < RenderQueue.list_items.length) {
|
||||
RenderQueue.list_items[nb_current - 1].str_image_path = str_last_image_path;
|
||||
}
|
||||
|
||||
for (let nb_i = 0; nb_i < RenderQueue.list_items.length; nb_i++) {
|
||||
if (list_skipped && list_skipped.indexOf(nb_i) !== -1) {
|
||||
RenderQueue.list_items[nb_i].str_status = "skipped";
|
||||
} else if (nb_i < nb_current) {
|
||||
RenderQueue.list_items[nb_i].str_status = "done";
|
||||
} else if (nb_i === nb_current) {
|
||||
RenderQueue.list_items[nb_i].str_status = "rendering";
|
||||
} else {
|
||||
RenderQueue.list_items[nb_i].str_status = "pending";
|
||||
}
|
||||
}
|
||||
|
||||
RenderQueue._update_time_display();
|
||||
RenderQueue._update_statuses();
|
||||
},
|
||||
|
||||
_update_statuses: () => {
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
if (!obj_item.obj_dom_el) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_needs_click = obj_item.str_status === "done" && obj_item.str_image_path && !obj_item.is_click_bound;
|
||||
|
||||
if (obj_item.str_status === obj_item.str_dom_status && !is_needs_click) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let str_icon = "mdi-clock-outline";
|
||||
let str_color = "text-muted";
|
||||
|
||||
if (obj_item.str_status === "rendering") {
|
||||
str_icon = "mdi-loading mdi-spin";
|
||||
str_color = "text-primary";
|
||||
} else if (obj_item.str_status === "done") {
|
||||
str_icon = "mdi-check-circle";
|
||||
str_color = "text-success";
|
||||
} else if (obj_item.str_status === "error") {
|
||||
str_icon = "mdi-alert-circle";
|
||||
str_color = "text-danger";
|
||||
} else if (obj_item.str_status === "skipped") {
|
||||
str_icon = "mdi-skip-next-circle";
|
||||
str_color = "text-info";
|
||||
}
|
||||
|
||||
obj_item.obj_dom_icon.className = "mdi " + str_icon + " " + str_color;
|
||||
|
||||
if (is_needs_click) {
|
||||
obj_item.obj_dom_el.classList.add("queue-item-clickable");
|
||||
let str_path = obj_item.str_image_path;
|
||||
obj_item.obj_dom_el.addEventListener("click", () => {
|
||||
PreviewPanel.show_image(str_path);
|
||||
});
|
||||
obj_item.is_click_bound = true;
|
||||
}
|
||||
|
||||
obj_item.str_dom_status = obj_item.str_status;
|
||||
}
|
||||
},
|
||||
|
||||
_update_time_display: () => {
|
||||
let obj_label = document.getElementById("label_queue_time_estimate");
|
||||
if (!obj_label) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (RenderQueue.nb_completed_renders === 0) {
|
||||
obj_label.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
let nb_avg_ms = RenderQueue.nb_total_render_ms / RenderQueue.nb_completed_renders;
|
||||
let nb_remaining_count = 0;
|
||||
for (let obj_item of RenderQueue.list_items) {
|
||||
if (obj_item.str_status !== "done" && obj_item.str_status !== "skipped") {
|
||||
nb_remaining_count++;
|
||||
}
|
||||
}
|
||||
|
||||
let nb_remaining_ms = nb_avg_ms * nb_remaining_count;
|
||||
obj_label.innerHTML = '<i class="mdi mdi-clock-outline me-1"></i>'
|
||||
+ RenderQueue._format_duration(nb_remaining_ms);
|
||||
},
|
||||
|
||||
_format_duration: (nb_ms) => {
|
||||
let nb_total_seconds = Math.ceil(nb_ms / 1000);
|
||||
let nb_days = Math.floor(nb_total_seconds / 86400);
|
||||
nb_total_seconds %= 86400;
|
||||
let nb_hours = Math.floor(nb_total_seconds / 3600);
|
||||
nb_total_seconds %= 3600;
|
||||
let nb_minutes = Math.floor(nb_total_seconds / 60);
|
||||
let nb_seconds = nb_total_seconds % 60;
|
||||
|
||||
let str_result = "";
|
||||
if (nb_days > 0) {
|
||||
str_result += nb_days + "j ";
|
||||
}
|
||||
if (nb_hours > 0 || nb_days > 0) {
|
||||
str_result += nb_hours + "h ";
|
||||
}
|
||||
if (nb_minutes > 0 || nb_hours > 0 || nb_days > 0) {
|
||||
str_result += nb_minutes + "m ";
|
||||
}
|
||||
str_result += nb_seconds + "s";
|
||||
|
||||
return str_result;
|
||||
},
|
||||
};
|
||||
207
src/renderer/styles/Main.css
Normal file
207
src/renderer/styles/Main.css
Normal file
@@ -0,0 +1,207 @@
|
||||
/* ── Global ─────────────────────────────────────────────────── */
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: calc(100vh - 56px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.col-md-4 {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ── Cards ──────────────────────────────────────────────────── */
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Camera list ────────────────────────────────────────────── */
|
||||
|
||||
.list-group-item-action:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
background-color: rgba(13, 110, 253, 0.2) !important;
|
||||
border-color: #495057 !important;
|
||||
}
|
||||
|
||||
/* ── Preview ────────────────────────────────────────────────── */
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 350px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* ── Console ────────────────────────────────────────────────── */
|
||||
|
||||
.console-output {
|
||||
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.5;
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 1px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.console-time {
|
||||
color: #6e7681;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.console-text {
|
||||
color: #c9d1d9;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ── Progress ───────────────────────────────────────────────── */
|
||||
|
||||
.progress {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Render queue items ─────────────────────────────────────── */
|
||||
|
||||
#container_render_queue .list-group-item {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#container_render_queue .queue-item-clickable {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
#container_render_queue .queue-item-clickable:hover {
|
||||
background-color: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* ── Queue time estimate ───────────────────────────────────── */
|
||||
|
||||
.queue-time-estimate {
|
||||
font-size: 0.7rem;
|
||||
color: #8b949e;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ── Text contrast overrides ─────────────────────────────────── */
|
||||
|
||||
.text-muted {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.text-light-emphasis {
|
||||
color: #c9d1d9 !important;
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: #8b949e !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Form tweaks ────────────────────────────────────────────── */
|
||||
|
||||
.form-label {
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
color: #e1e4e8 !important;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.form-control:disabled,
|
||||
.form-control[readonly] {
|
||||
color: #c9d1d9 !important;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* ── Scrollbar ──────────────────────────────────────────────── */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #495057;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
/* ── Badge ──────────────────────────────────────────────────── */
|
||||
|
||||
.badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user