feat: add persistent project storage
This commit is contained in:
parent
59efdddef9
commit
2cc632c75e
9 changed files with 800 additions and 61 deletions
|
|
@ -26,3 +26,6 @@ meson compile -C build -j8
|
|||
```
|
||||
|
||||
La TUI s'affiche directement dans le terminal courant. Appuyez sur `q` ou `Q` pour quitter proprement.
|
||||
|
||||
Les projets sont enregistrés par défaut dans `~/Documents/Lardon/Projets3D`.
|
||||
La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire racine à l'exécution.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#define LARDON3D_APP_STATE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_SCREEN_HOME = 0,
|
||||
|
|
@ -16,6 +17,7 @@ typedef struct {
|
|||
bool running;
|
||||
bool project_loaded;
|
||||
char project_name[128];
|
||||
char project_path[PATH_MAX];
|
||||
char status_message[256];
|
||||
} Lardon3DAppState;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
void lardon3d_layout_draw(
|
||||
const Lardon3DAppState *state,
|
||||
const char *project_name_input,
|
||||
const char *project_input_label,
|
||||
int rows,
|
||||
int cols
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@
|
|||
|
||||
#include <lardon3d/app_state.h>
|
||||
|
||||
bool lardon3d_project_set_name(
|
||||
bool lardon3d_project_create(
|
||||
Lardon3DAppState *state,
|
||||
const char *name
|
||||
);
|
||||
|
||||
bool lardon3d_project_open(
|
||||
Lardon3DAppState *state,
|
||||
const char *directory_name
|
||||
);
|
||||
|
||||
void lardon3d_project_close(Lardon3DAppState *state);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
226
scan3d/tri_photos.py
Normal file
226
scan3d/tri_photos.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
EXTENSIONS = {".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
path: Path
|
||||
category: str
|
||||
sharpness: float
|
||||
white: float
|
||||
black: float
|
||||
brightness: float
|
||||
reason: str
|
||||
|
||||
|
||||
def natural_key(path: Path) -> list[object]:
|
||||
return [
|
||||
int(part) if part.isdigit() else part.lower()
|
||||
for part in re.split(r"(\d+)", path.name)
|
||||
]
|
||||
|
||||
|
||||
def load_gray(path: Path) -> np.ndarray:
|
||||
with Image.open(path) as source:
|
||||
source = source.convert("L")
|
||||
gray = np.asarray(source)
|
||||
|
||||
height, width = gray.shape
|
||||
maximum_dimension = 1600
|
||||
|
||||
if max(width, height) > maximum_dimension:
|
||||
scale = maximum_dimension / max(width, height)
|
||||
gray = cv2.resize(
|
||||
gray,
|
||||
(
|
||||
max(1, round(width * scale)),
|
||||
max(1, round(height * scale)),
|
||||
),
|
||||
interpolation=cv2.INTER_AREA,
|
||||
)
|
||||
|
||||
return gray
|
||||
|
||||
|
||||
def analyze(path: Path) -> tuple[dict[str, float] | None, str]:
|
||||
try:
|
||||
gray = load_gray(path)
|
||||
except Exception as error:
|
||||
return None, f"illisible: {error}"
|
||||
|
||||
return {
|
||||
"sharpness": float(cv2.Laplacian(gray, cv2.CV_64F).var()),
|
||||
"white": float(np.count_nonzero(gray >= 250) / gray.size * 100),
|
||||
"black": float(np.count_nonzero(gray <= 5) / gray.size * 100),
|
||||
"brightness": float(gray.mean()),
|
||||
}, ""
|
||||
|
||||
|
||||
def classify(path: Path, metrics: dict[str, float] | None, error: str) -> Result:
|
||||
if metrics is None:
|
||||
return Result(path, "mauvaises", 0.0, 0.0, 0.0, 0.0, error)
|
||||
|
||||
sharpness = metrics["sharpness"]
|
||||
white = metrics["white"]
|
||||
black = metrics["black"]
|
||||
brightness = metrics["brightness"]
|
||||
|
||||
reasons: list[str] = []
|
||||
|
||||
if sharpness < 100:
|
||||
reasons.append("très floue")
|
||||
elif sharpness < 170:
|
||||
reasons.append("floue possible")
|
||||
|
||||
if white > 20:
|
||||
reasons.append("très surexposée")
|
||||
elif white > 8:
|
||||
reasons.append("surexposition possible")
|
||||
|
||||
if black > 35:
|
||||
reasons.append("très sombre")
|
||||
elif black > 15:
|
||||
reasons.append("ombres bouchées possibles")
|
||||
|
||||
if brightness > 225 or brightness < 25:
|
||||
reasons.append("luminosité extrême")
|
||||
|
||||
severe = (
|
||||
sharpness < 100
|
||||
or white > 20
|
||||
or black > 35
|
||||
or brightness > 225
|
||||
or brightness < 25
|
||||
)
|
||||
|
||||
if severe:
|
||||
category = "mauvaises"
|
||||
elif reasons:
|
||||
category = "suspectes"
|
||||
else:
|
||||
category = "bonnes"
|
||||
|
||||
return Result(
|
||||
path,
|
||||
category,
|
||||
sharpness,
|
||||
white,
|
||||
black,
|
||||
brightness,
|
||||
"; ".join(reasons) or "ok",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("images", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
images_dir = args.images.expanduser().resolve()
|
||||
output_dir = args.output.expanduser().resolve()
|
||||
|
||||
excluded_roots = {
|
||||
output_dir,
|
||||
images_dir / "scan3d",
|
||||
images_dir / "tri_resultat",
|
||||
images_dir / "test_colmap",
|
||||
images_dir / "colmap_complet",
|
||||
}
|
||||
|
||||
images: list[Path] = []
|
||||
for path in images_dir.rglob("*"):
|
||||
if not path.is_file() or path.is_symlink():
|
||||
continue
|
||||
if path.suffix.lower() not in EXTENSIONS:
|
||||
continue
|
||||
if any(root == path or root in path.parents for root in excluded_roots):
|
||||
continue
|
||||
images.append(path)
|
||||
|
||||
images.sort(key=natural_key)
|
||||
|
||||
if not images:
|
||||
print("Aucune image trouvée.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"{len(images)} images trouvées.")
|
||||
|
||||
results: list[Result] = []
|
||||
for index, path in enumerate(images, 1):
|
||||
metrics, error = analyze(path)
|
||||
results.append(classify(path, metrics, error))
|
||||
print(
|
||||
f"\rAnalyse {index}/{len(images)} : {path.name[:55]:55s}",
|
||||
end="",
|
||||
flush=True,
|
||||
)
|
||||
print()
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
for category in ("bonnes", "suspectes", "mauvaises"):
|
||||
category_dir = output_dir / category
|
||||
if category_dir.exists():
|
||||
for child in category_dir.iterdir():
|
||||
if child.is_symlink() or child.is_file():
|
||||
child.unlink()
|
||||
category_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with (output_dir / "resultats.csv").open(
|
||||
"w", encoding="utf-8", newline=""
|
||||
) as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
writer.writerow(
|
||||
[
|
||||
"fichier",
|
||||
"categorie",
|
||||
"netteté",
|
||||
"blanc_pct",
|
||||
"noir_pct",
|
||||
"luminosité",
|
||||
"raison",
|
||||
]
|
||||
)
|
||||
|
||||
counters = {"bonnes": 0, "suspectes": 0, "mauvaises": 0}
|
||||
for result in results:
|
||||
counters[result.category] += 1
|
||||
link_name = f"{counters[result.category]:06d}_{result.path.name}"
|
||||
link_path = output_dir / result.category / link_name
|
||||
link_path.symlink_to(result.path.resolve())
|
||||
|
||||
writer.writerow(
|
||||
[
|
||||
str(result.path),
|
||||
result.category,
|
||||
f"{result.sharpness:.2f}",
|
||||
f"{result.white:.2f}",
|
||||
f"{result.black:.2f}",
|
||||
f"{result.brightness:.2f}",
|
||||
result.reason,
|
||||
]
|
||||
)
|
||||
|
||||
for category in ("bonnes", "suspectes", "mauvaises"):
|
||||
count = sum(result.category == category for result in results)
|
||||
print(f"{category}: {count}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -12,6 +12,7 @@ lardon3d_app_state_init(Lardon3DAppState *state)
|
|||
.running = true,
|
||||
.project_loaded = false,
|
||||
.project_name = "",
|
||||
.project_path = "",
|
||||
.status_message = "Bienvenue dans Lardon3D",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
44
src/layout.c
44
src/layout.c
|
|
@ -64,7 +64,7 @@ screen_texts(
|
|||
case LARDON3D_SCREEN_PROJECTS:
|
||||
*title = "Projets";
|
||||
*content = "Gestion des projets";
|
||||
*footer = "N Nouveau projet C Fermer le projet ESC Accueil Q Quit";
|
||||
*footer = "N Nouveau O Ouvrir C Fermer ESC Accueil Q Quit";
|
||||
break;
|
||||
case LARDON3D_SCREEN_IMPORT:
|
||||
*title = "Import";
|
||||
|
|
@ -88,20 +88,25 @@ screen_texts(
|
|||
}
|
||||
|
||||
static void
|
||||
draw_project_screen(const char *project_name_input, int columns)
|
||||
draw_project_screen(
|
||||
const char *project_name_input,
|
||||
const char *project_input_label,
|
||||
int columns
|
||||
)
|
||||
{
|
||||
draw_text(6, 4, columns - 6, "N : Nouveau projet");
|
||||
draw_text(7, 4, columns - 6, "C : Fermer le projet");
|
||||
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
||||
draw_text(9, 4, columns - 6, "Q : Quitter");
|
||||
draw_text(7, 4, columns - 6, "O : Ouvrir un projet");
|
||||
draw_text(8, 4, columns - 6, "C : Fermer le projet");
|
||||
draw_text(9, 4, columns - 6, "ESC : Accueil");
|
||||
draw_text(10, 4, columns - 6, "Q : Quitter");
|
||||
|
||||
if (!project_name_input) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw_text(10, 4, columns - 6, "Nom du nouveau projet :");
|
||||
(void)mvaddch(11, 2, '[');
|
||||
(void)mvaddch(11, columns - 3, ']');
|
||||
draw_text(11, 4, columns - 6, project_input_label);
|
||||
(void)mvaddch(12, 2, '[');
|
||||
(void)mvaddch(12, columns - 3, ']');
|
||||
|
||||
int available = columns - 8;
|
||||
size_t length = strlen(project_name_input);
|
||||
|
|
@ -110,14 +115,15 @@ draw_project_screen(const char *project_name_input, int columns)
|
|||
visible += length - (size_t)available;
|
||||
length = (size_t)available;
|
||||
}
|
||||
draw_text(11, 4, available, visible);
|
||||
(void)move(11, 4 + (int)length);
|
||||
draw_text(12, 4, available, visible);
|
||||
(void)move(12, 4 + (int)length);
|
||||
}
|
||||
|
||||
static void
|
||||
draw_content(
|
||||
const Lardon3DAppState *state,
|
||||
const char *project_name_input,
|
||||
const char *project_input_label,
|
||||
int rows,
|
||||
int columns
|
||||
)
|
||||
|
|
@ -136,8 +142,15 @@ draw_content(
|
|||
draw_text(1, (columns - title_length) / 2, title_length, title);
|
||||
draw_text(3, 2, columns - 4, "Projet");
|
||||
draw_text(4, 4, columns - 6, project);
|
||||
if (state->project_loaded) {
|
||||
draw_text(5, 4, columns - 6, state->project_path);
|
||||
}
|
||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||
draw_project_screen(project_name_input, columns);
|
||||
draw_project_screen(
|
||||
project_name_input,
|
||||
project_input_label,
|
||||
columns
|
||||
);
|
||||
} else {
|
||||
draw_text(
|
||||
(3 + journal_row) / 2,
|
||||
|
|
@ -155,6 +168,7 @@ void
|
|||
lardon3d_layout_draw(
|
||||
const Lardon3DAppState *state,
|
||||
const char *project_name_input,
|
||||
const char *project_input_label,
|
||||
int rows,
|
||||
int columns
|
||||
)
|
||||
|
|
@ -165,7 +179,13 @@ lardon3d_layout_draw(
|
|||
draw_too_small(rows, columns);
|
||||
} else {
|
||||
draw_frame(rows, columns);
|
||||
draw_content(state, project_name_input, rows, columns);
|
||||
draw_content(
|
||||
state,
|
||||
project_name_input,
|
||||
project_input_label,
|
||||
rows,
|
||||
columns
|
||||
);
|
||||
}
|
||||
|
||||
(void)refresh();
|
||||
|
|
|
|||
515
src/project.c
515
src/project.c
|
|
@ -1,57 +1,364 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/project.h>
|
||||
|
||||
bool
|
||||
lardon3d_project_set_name(Lardon3DAppState *state, const char *name)
|
||||
{
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
enum {
|
||||
MAX_CREATED_DIRECTORIES = 16,
|
||||
INI_LINE_CAPACITY = 512,
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
typedef struct {
|
||||
char paths[MAX_CREATED_DIRECTORIES][PATH_MAX];
|
||||
size_t count;
|
||||
} CreatedDirectories;
|
||||
|
||||
static void
|
||||
set_status(Lardon3DAppState *state, const char *message)
|
||||
{
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Erreur : le nom du projet est vide."
|
||||
"%s",
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
static bool
|
||||
normalize_name(
|
||||
Lardon3DAppState *state,
|
||||
const char *input,
|
||||
char *output,
|
||||
size_t output_size
|
||||
)
|
||||
{
|
||||
if (!input) {
|
||||
set_status(state, "Erreur : le nom du projet est vide.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *start = name;
|
||||
const char *start = input;
|
||||
while (*start && isspace((unsigned char)*start)) {
|
||||
++start;
|
||||
}
|
||||
|
||||
const char *end = name + strlen(name);
|
||||
const char *end = input + strlen(input);
|
||||
while (end > start && isspace((unsigned char)end[-1])) {
|
||||
--end;
|
||||
}
|
||||
|
||||
size_t length = (size_t)(end - start);
|
||||
if (length == 0) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Erreur : le nom du projet est vide."
|
||||
);
|
||||
set_status(state, "Erreur : le nom du projet est vide.");
|
||||
return false;
|
||||
}
|
||||
if (length >= output_size) {
|
||||
set_status(state, "Erreur : le nom du projet est trop long.");
|
||||
return false;
|
||||
}
|
||||
if ((length == 1 && start[0] == '.')
|
||||
|| (length == 2 && start[0] == '.' && start[1] == '.')) {
|
||||
set_status(state, "Erreur : nom de projet interdit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (length >= sizeof(state->project_name)) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Erreur : le nom du projet est trop long."
|
||||
);
|
||||
for (const char *character = start; character < end; ++character) {
|
||||
if (*character == '/' || *character == '\\'
|
||||
|| iscntrl((unsigned char)*character)) {
|
||||
set_status(state, "Erreur : nom de projet interdit.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
(void)memcpy(output, start, length);
|
||||
output[length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
copy_path(char *destination, size_t size, const char *source)
|
||||
{
|
||||
int written = snprintf(destination, size, "%s", source);
|
||||
return written >= 0 && (size_t)written < size;
|
||||
}
|
||||
|
||||
static bool
|
||||
join_path(
|
||||
char *destination,
|
||||
size_t size,
|
||||
const char *parent,
|
||||
const char *child
|
||||
)
|
||||
{
|
||||
int written = snprintf(destination, size, "%s/%s", parent, child);
|
||||
return written >= 0 && (size_t)written < size;
|
||||
}
|
||||
|
||||
static bool
|
||||
resolve_projects_root(Lardon3DAppState *state, char root[PATH_MAX])
|
||||
{
|
||||
const char *configured = getenv("LARDON3D_PROJECTS_ROOT");
|
||||
if (configured && configured[0]) {
|
||||
if (configured[0] != '/' || !copy_path(root, PATH_MAX, configured)) {
|
||||
set_status(state, "Erreur : répertoire racine invalide.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const char *home = getenv("HOME");
|
||||
if (!home || home[0] != '/') {
|
||||
set_status(state, "Erreur : HOME est absent ou invalide.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int written = snprintf(
|
||||
root,
|
||||
PATH_MAX,
|
||||
"%s/Documents/Lardon/Projets3D",
|
||||
home
|
||||
);
|
||||
if (written < 0 || (size_t)written >= PATH_MAX) {
|
||||
set_status(state, "Erreur : chemin racine trop long.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
size_t length = strlen(root);
|
||||
while (length > 1 && root[length - 1] == '/') {
|
||||
root[--length] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
cleanup_directories(CreatedDirectories *created)
|
||||
{
|
||||
while (created->count > 0) {
|
||||
--created->count;
|
||||
(void)rmdir(created->paths[created->count]);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
ensure_directory(
|
||||
Lardon3DAppState *state,
|
||||
const char *path,
|
||||
CreatedDirectories *created
|
||||
)
|
||||
{
|
||||
struct stat info;
|
||||
if (lstat(path, &info) == 0) {
|
||||
if (!S_ISDIR(info.st_mode)) {
|
||||
set_status(state, "Erreur : un élément du chemin n'est pas un dossier.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (errno != ENOENT || created->count >= MAX_CREATED_DIRECTORIES) {
|
||||
set_status(state, "Erreur : impossible de préparer le répertoire racine.");
|
||||
return false;
|
||||
}
|
||||
if (mkdir(path, 0755) != 0) {
|
||||
set_status(state, "Erreur : impossible de créer un dossier.");
|
||||
return false;
|
||||
}
|
||||
if (!copy_path(
|
||||
created->paths[created->count],
|
||||
sizeof(created->paths[created->count]),
|
||||
path
|
||||
)) {
|
||||
(void)rmdir(path);
|
||||
set_status(state, "Erreur : chemin de dossier trop long.");
|
||||
return false;
|
||||
}
|
||||
++created->count;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
ensure_directory_tree(
|
||||
Lardon3DAppState *state,
|
||||
const char *path,
|
||||
CreatedDirectories *created
|
||||
)
|
||||
{
|
||||
char partial[PATH_MAX];
|
||||
if (!copy_path(partial, sizeof(partial), path)) {
|
||||
set_status(state, "Erreur : chemin racine trop long.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char *separator = partial + 1; *separator; ++separator) {
|
||||
if (*separator != '/') {
|
||||
continue;
|
||||
}
|
||||
*separator = '\0';
|
||||
bool success = ensure_directory(state, partial, created);
|
||||
*separator = '/';
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return ensure_directory(state, partial, created);
|
||||
}
|
||||
|
||||
static bool
|
||||
write_project_ini(
|
||||
Lardon3DAppState *state,
|
||||
const char *project_path,
|
||||
const char *project_name
|
||||
)
|
||||
{
|
||||
char temporary_path[PATH_MAX];
|
||||
char final_path[PATH_MAX];
|
||||
if (!join_path(final_path, sizeof(final_path), project_path, "project.ini")
|
||||
|| !join_path(
|
||||
temporary_path,
|
||||
sizeof(temporary_path),
|
||||
project_path,
|
||||
".project.ini.tmp.XXXXXX"
|
||||
)) {
|
||||
set_status(state, "Erreur : chemin de project.ini trop long.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int descriptor = mkstemp(temporary_path);
|
||||
if (descriptor < 0) {
|
||||
set_status(state, "Erreur : impossible de créer project.ini.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *file = fdopen(descriptor, "w");
|
||||
if (!file) {
|
||||
(void)close(descriptor);
|
||||
(void)unlink(temporary_path);
|
||||
set_status(state, "Erreur : impossible d'écrire project.ini.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = fprintf(
|
||||
file,
|
||||
"[project]\nname=%s\nversion=1\n",
|
||||
project_name
|
||||
) >= 0;
|
||||
if (success) {
|
||||
success = fflush(file) == 0;
|
||||
}
|
||||
if (success) {
|
||||
success = fsync(fileno(file)) == 0;
|
||||
}
|
||||
if (fclose(file) != 0) {
|
||||
success = false;
|
||||
}
|
||||
if (success) {
|
||||
success = rename(temporary_path, final_path) == 0;
|
||||
}
|
||||
if (!success) {
|
||||
(void)unlink(temporary_path);
|
||||
set_status(state, "Erreur : impossible d'écrire project.ini.");
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool
|
||||
lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
||||
{
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char normalized_name[sizeof(state->project_name)];
|
||||
char root[PATH_MAX];
|
||||
char project_path[PATH_MAX];
|
||||
if (!normalize_name(
|
||||
state,
|
||||
name,
|
||||
normalized_name,
|
||||
sizeof(normalized_name)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
if (!resolve_projects_root(state, root)) {
|
||||
return false;
|
||||
}
|
||||
if (!join_path(
|
||||
project_path,
|
||||
sizeof(project_path),
|
||||
root,
|
||||
normalized_name
|
||||
)) {
|
||||
set_status(state, "Erreur : chemin du projet trop long.");
|
||||
return false;
|
||||
}
|
||||
|
||||
CreatedDirectories created = {0};
|
||||
if (!ensure_directory_tree(state, root, &created)) {
|
||||
cleanup_directories(&created);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat info;
|
||||
if (lstat(project_path, &info) == 0 || errno != ENOENT) {
|
||||
cleanup_directories(&created);
|
||||
set_status(state, "Erreur : ce projet existe déjà.");
|
||||
return false;
|
||||
}
|
||||
if (!ensure_directory(state, project_path, &created)) {
|
||||
cleanup_directories(&created);
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *subdirectories[] = {
|
||||
"images",
|
||||
"reconstruction",
|
||||
"exports",
|
||||
"logs",
|
||||
};
|
||||
for (size_t index = 0;
|
||||
index < sizeof(subdirectories) / sizeof(subdirectories[0]);
|
||||
++index) {
|
||||
char path[PATH_MAX];
|
||||
if (!join_path(
|
||||
path,
|
||||
sizeof(path),
|
||||
project_path,
|
||||
subdirectories[index]
|
||||
)) {
|
||||
set_status(state, "Erreur : chemin de dossier trop long.");
|
||||
cleanup_directories(&created);
|
||||
return false;
|
||||
}
|
||||
if (!ensure_directory(state, path, &created)) {
|
||||
cleanup_directories(&created);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!write_project_ini(state, project_path, normalized_name)) {
|
||||
cleanup_directories(&created);
|
||||
return false;
|
||||
}
|
||||
|
||||
(void)memcpy(state->project_name, start, length);
|
||||
state->project_name[length] = '\0';
|
||||
state->project_loaded = true;
|
||||
(void)copy_path(
|
||||
state->project_name,
|
||||
sizeof(state->project_name),
|
||||
normalized_name
|
||||
);
|
||||
(void)copy_path(
|
||||
state->project_path,
|
||||
sizeof(state->project_path),
|
||||
project_path
|
||||
);
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
|
|
@ -61,6 +368,155 @@ lardon3d_project_set_name(Lardon3DAppState *state, const char *name)
|
|||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
read_project_ini(
|
||||
Lardon3DAppState *state,
|
||||
const char *path,
|
||||
char project_name[128]
|
||||
)
|
||||
{
|
||||
int descriptor = open(path, O_RDONLY | O_NOFOLLOW);
|
||||
if (descriptor < 0) {
|
||||
set_status(state, "Erreur : project.ini est absent ou inaccessible.");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat info;
|
||||
if (fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode)) {
|
||||
(void)close(descriptor);
|
||||
set_status(state, "Erreur : project.ini n'est pas un fichier régulier.");
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *file = fdopen(descriptor, "r");
|
||||
if (!file) {
|
||||
(void)close(descriptor);
|
||||
set_status(state, "Erreur : impossible de lire project.ini.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool in_project_section = false;
|
||||
bool section_found = false;
|
||||
bool name_found = false;
|
||||
bool version_found = false;
|
||||
bool valid = true;
|
||||
char line[INI_LINE_CAPACITY];
|
||||
|
||||
while (valid && fgets(line, sizeof(line), file)) {
|
||||
size_t length = strlen(line);
|
||||
if (length > 0 && line[length - 1] == '\n') {
|
||||
line[--length] = '\0';
|
||||
} else if (!feof(file)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
if (length > 0 && line[length - 1] == '\r') {
|
||||
line[--length] = '\0';
|
||||
}
|
||||
|
||||
if (strcmp(line, "[project]") == 0) {
|
||||
in_project_section = true;
|
||||
section_found = true;
|
||||
} else if (line[0] == '[') {
|
||||
in_project_section = false;
|
||||
} else if (in_project_section && strncmp(line, "name=", 5) == 0) {
|
||||
if (name_found || !normalize_name(
|
||||
state,
|
||||
line + 5,
|
||||
project_name,
|
||||
sizeof(state->project_name)
|
||||
)) {
|
||||
valid = false;
|
||||
}
|
||||
name_found = true;
|
||||
} else if (in_project_section && strncmp(line, "version=", 8) == 0) {
|
||||
if (version_found || strcmp(line, "version=1") != 0) {
|
||||
valid = false;
|
||||
}
|
||||
version_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ferror(file)) {
|
||||
valid = false;
|
||||
}
|
||||
if (fclose(file) != 0) {
|
||||
valid = false;
|
||||
}
|
||||
if (!valid || !section_found || !name_found || !version_found) {
|
||||
set_status(state, "Erreur : project.ini invalide.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
lardon3d_project_open(
|
||||
Lardon3DAppState *state,
|
||||
const char *directory_name
|
||||
)
|
||||
{
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char normalized_directory[sizeof(state->project_name)];
|
||||
char root[PATH_MAX];
|
||||
char project_path[PATH_MAX];
|
||||
char ini_path[PATH_MAX];
|
||||
if (!normalize_name(
|
||||
state,
|
||||
directory_name,
|
||||
normalized_directory,
|
||||
sizeof(normalized_directory)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
if (!resolve_projects_root(state, root)) {
|
||||
return false;
|
||||
}
|
||||
if (!join_path(
|
||||
project_path,
|
||||
sizeof(project_path),
|
||||
root,
|
||||
normalized_directory
|
||||
)
|
||||
|| !join_path(ini_path, sizeof(ini_path), project_path, "project.ini")) {
|
||||
set_status(state, "Erreur : chemin du projet trop long.");
|
||||
return false;
|
||||
}
|
||||
|
||||
struct stat info;
|
||||
if (lstat(project_path, &info) != 0 || !S_ISDIR(info.st_mode)) {
|
||||
set_status(state, "Erreur : dossier projet absent ou invalide.");
|
||||
return false;
|
||||
}
|
||||
|
||||
char project_name[sizeof(state->project_name)];
|
||||
if (!read_project_ini(state, ini_path, project_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
state->project_loaded = true;
|
||||
(void)copy_path(
|
||||
state->project_name,
|
||||
sizeof(state->project_name),
|
||||
project_name
|
||||
);
|
||||
(void)copy_path(
|
||||
state->project_path,
|
||||
sizeof(state->project_path),
|
||||
project_path
|
||||
);
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Projet ouvert : %s",
|
||||
state->project_name
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
lardon3d_project_close(Lardon3DAppState *state)
|
||||
{
|
||||
|
|
@ -69,19 +525,12 @@ lardon3d_project_close(Lardon3DAppState *state)
|
|||
}
|
||||
|
||||
if (!state->project_loaded) {
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Aucun projet à fermer."
|
||||
);
|
||||
set_status(state, "Aucun projet à fermer.");
|
||||
return;
|
||||
}
|
||||
|
||||
state->project_loaded = false;
|
||||
state->project_name[0] = '\0';
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Projet fermé."
|
||||
);
|
||||
state->project_path[0] = '\0';
|
||||
set_status(state, "Projet fermé.");
|
||||
}
|
||||
|
|
|
|||
56
src/tui.c
56
src/tui.c
|
|
@ -15,7 +15,11 @@ enum {
|
|||
};
|
||||
|
||||
typedef struct {
|
||||
bool active;
|
||||
enum {
|
||||
PROJECT_INPUT_NONE = 0,
|
||||
PROJECT_INPUT_CREATE,
|
||||
PROJECT_INPUT_OPEN,
|
||||
} mode;
|
||||
char text[PROJECT_INPUT_CAPACITY];
|
||||
size_t length;
|
||||
} ProjectInput;
|
||||
|
|
@ -27,10 +31,15 @@ redraw(const Lardon3DAppState *state, const ProjectInput *input)
|
|||
int columns;
|
||||
getmaxyx(stdscr, rows, columns);
|
||||
|
||||
const char *text = input->active ? input->text : NULL;
|
||||
lardon3d_layout_draw(state, text, rows, columns);
|
||||
const char *text = input->mode != PROJECT_INPUT_NONE ? input->text : NULL;
|
||||
const char *label = input->mode == PROJECT_INPUT_OPEN
|
||||
? "Nom du dossier projet :"
|
||||
: "Nom du nouveau projet :";
|
||||
lardon3d_layout_draw(state, text, label, rows, columns);
|
||||
(void)curs_set(
|
||||
input->active && rows >= MINIMUM_ROWS && columns >= MINIMUM_COLUMNS
|
||||
input->mode != PROJECT_INPUT_NONE
|
||||
&& rows >= MINIMUM_ROWS
|
||||
&& columns >= MINIMUM_COLUMNS
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
|
|
@ -48,18 +57,26 @@ handle_project_input(
|
|||
}
|
||||
|
||||
if (key == 27) {
|
||||
input->active = false;
|
||||
const char *message = input->mode == PROJECT_INPUT_OPEN
|
||||
? "Ouverture du projet annulée."
|
||||
: "Création du projet annulée.";
|
||||
input->mode = PROJECT_INPUT_NONE;
|
||||
(void)snprintf(
|
||||
state->status_message,
|
||||
sizeof(state->status_message),
|
||||
"Création du projet annulée."
|
||||
"%s",
|
||||
message
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key == '\n' || key == '\r' || key == KEY_ENTER) {
|
||||
(void)lardon3d_project_set_name(state, input->text);
|
||||
input->active = false;
|
||||
if (input->mode == PROJECT_INPUT_OPEN) {
|
||||
(void)lardon3d_project_open(state, input->text);
|
||||
} else {
|
||||
(void)lardon3d_project_create(state, input->text);
|
||||
}
|
||||
input->mode = PROJECT_INPUT_NONE;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -73,8 +90,12 @@ handle_project_input(
|
|||
|
||||
if (key >= 0 && key <= UCHAR_MAX && isprint((unsigned char)key)) {
|
||||
if (input->length + 1 >= sizeof(input->text)) {
|
||||
(void)lardon3d_project_set_name(state, input->text);
|
||||
input->active = false;
|
||||
if (input->mode == PROJECT_INPUT_OPEN) {
|
||||
(void)lardon3d_project_open(state, input->text);
|
||||
} else {
|
||||
(void)lardon3d_project_create(state, input->text);
|
||||
}
|
||||
input->mode = PROJECT_INPUT_NONE;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +142,18 @@ handle_normal_input(
|
|||
case 'N':
|
||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||
*input = (ProjectInput) {
|
||||
.active = true,
|
||||
.mode = PROJECT_INPUT_CREATE,
|
||||
.text = "",
|
||||
.length = 0,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case 'o':
|
||||
case 'O':
|
||||
if (state->screen == LARDON3D_SCREEN_PROJECTS) {
|
||||
*input = (ProjectInput) {
|
||||
.mode = PROJECT_INPUT_OPEN,
|
||||
.text = "",
|
||||
.length = 0,
|
||||
};
|
||||
|
|
@ -172,7 +204,7 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool should_redraw = input.active
|
||||
bool should_redraw = input.mode != PROJECT_INPUT_NONE
|
||||
? handle_project_input(state, &input, key)
|
||||
: handle_normal_input(state, &input, key);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue