diff --git a/apps/camera_lidar_calibration/App.cpp b/apps/camera_lidar_calibration/App.cpp index ba4a1c40..b0ccc47d 100644 --- a/apps/camera_lidar_calibration/App.cpp +++ b/apps/camera_lidar_calibration/App.cpp @@ -601,6 +601,38 @@ static bool shiftHeld() return IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); } +// Loads one dropped file by extension -- the same extension -> action mapping as main.cpp's +// preloadByExt(), but applied immediately (the window is already running) instead of queued for +// App::run()'s startup pass. `sawCloud` tracks whether a *.laz/*.las has already landed in this +// drop batch, so the first one replaces the cloud (like UI::actionOpenPointCloud) and later ones +// merge into it (like UI::actionAddPointCloud) -- mirroring how a multi-file drop of point clouds +// mixes "open" and "add" semantics for a natural one-shot drop. +static void handleDroppedFile(AppState& state, const std::string& path, bool& sawCloud) +{ + auto pos = path.rfind('.'); + std::string e = pos == std::string::npos ? "" : path.substr(pos + 1); + std::transform(e.begin(), e.end(), e.begin(), ::tolower); + + if (e == "jpg" || e == "jpeg" || e == "png" || e == "bmp") + state.loadImage(path.c_str()); + else if (e == "laz" || e == "las") + { + if (sawCloud) + state.addCloud(path.c_str()); + else + { + state.loadCloud(path.c_str()); + sawCloud = true; + } + } + else if (e == "yml" || e == "yaml") + state.loadIntrinsics(path.c_str()); + else if (e == "json") + state.loadCalibration(path.c_str()); + else + state.statusMsg = "Unsupported dropped file: " + path; +} + // ── App::update ─────────────────────────────────────────────────────────────── void App::update() { @@ -610,6 +642,19 @@ void App::update() // to pick". bool allowOrbit = !imguiWantMouse && !shiftHeld(); state.orbit.update(allowOrbit); + + // Drag & drop images (*.jpg/*.jpeg/*.png/*.bmp), point clouds (*.laz/*.las), intrinsics + // (*.yml/*.yaml) or a calibration (*.json) onto the window to load them -- raylib's GLFW + // backend surfaces OS drag & drop the same way on Windows, Linux and macOS, so no + // platform-specific code is needed here. + if (IsFileDropped()) + { + FilePathList dropped = LoadDroppedFiles(); + bool sawCloud = false; + for (unsigned int i = 0; i < dropped.count; i++) + handleDroppedFile(state, dropped.paths[i], sawCloud); + UnloadDroppedFiles(dropped); + } } // ── 3D point picking + correspondence markers ─────────────────────────────── diff --git a/apps/camera_lidar_intrinsics_calib/IntrinsicsCalib.cpp b/apps/camera_lidar_intrinsics_calib/IntrinsicsCalib.cpp index 299701e2..fb693925 100644 --- a/apps/camera_lidar_intrinsics_calib/IntrinsicsCalib.cpp +++ b/apps/camera_lidar_intrinsics_calib/IntrinsicsCalib.cpp @@ -89,6 +89,20 @@ struct State }; // ── helpers ─────────────────────────────────────────────────────────────────── +// Reads one image file and appends it to s.images if it decodes; used by both loadDir() (a whole +// folder) and the drag & drop handler in main()'s loop below (individual dropped photos). +static bool appendImage(State& s, const std::string& path) +{ + cv::Mat bgr = cv::imread(path, cv::IMREAD_COLOR); + if (bgr.empty()) + return false; + CalibImage ci; + ci.path = path; + cv::cvtColor(bgr, ci.rgb, cv::COLOR_BGR2RGB); + s.images.push_back(std::move(ci)); + return true; +} + static void loadDir(State& s) { s.images.clear(); @@ -121,16 +135,32 @@ static void loadDir(State& s) std::sort(paths.begin(), paths.end()); for (auto& p : paths) + appendImage(s, p); + s.statusMsg = "Loaded " + std::to_string(s.images.size()) + " images"; +} + +// Drops a directory in as if Browse+Load had picked it (replaces the current image set); +// individual photo files are appended instead (accumulating shots from wherever they came from, +// since there's no single directory to re-scan for them). Used by the drag & drop handler in +// main()'s loop below. +static void handleDroppedPath(State& s, const std::string& path) +{ + if (fs::is_directory(path)) { - cv::Mat bgr = cv::imread(p, cv::IMREAD_COLOR); - if (bgr.empty()) - continue; - CalibImage ci; - ci.path = p; - cv::cvtColor(bgr, ci.rgb, cv::COLOR_BGR2RGB); - s.images.push_back(std::move(ci)); + setBuf(s.dirBuf, sizeof(s.dirBuf), path); + loadDir(s); + return; + } + + if (appendImage(s, path)) + { + s.calibrated = false; + s.statusMsg = "Added " + path + " (" + std::to_string(s.images.size()) + " images total)"; + } + else + { + s.statusMsg = "Unsupported dropped file: " + path; } - s.statusMsg = "Loaded " + std::to_string(s.images.size()) + " images"; } static void detectAll(State& s) @@ -335,6 +365,18 @@ int main(int argc, char* argv[]) while (!WindowShouldClose()) { + // Drag & drop a folder of checkerboard photos (replaces the current set, like Browse+Load) + // or individual photo files (appended to the current set) onto the window to load them -- + // raylib's GLFW backend surfaces OS drag & drop the same way on Windows, Linux and macOS, + // so no platform-specific code is needed here. + if (IsFileDropped()) + { + FilePathList dropped = LoadDroppedFiles(); + for (unsigned int i = 0; i < dropped.count; i++) + handleDroppedPath(state, dropped.paths[i]); + UnloadDroppedFiles(dropped); + } + refreshTex(state); BeginDrawing(); diff --git a/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp b/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp index 63fde7a5..a7a26c1f 100644 --- a/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp +++ b/apps/camera_lidar_trajectory_viewer/TrajectoryViewer.cpp @@ -982,6 +982,33 @@ static void actionOpenCalibration(AppState& s) } } +// Drag & drop equivalent of actionSelectLioResultDir()/actionOpenCalibration(): a dropped +// directory is this app's session (LIO result dir), and unlike the menu action it loads +// immediately instead of waiting for the "Load session" button, since a drop is already an +// explicit "load this" gesture. A dropped *.json is treated as a calibration file. Used by the +// drag & drop handler in main()'s loop below. +static void handleDroppedPath(AppState& s, const std::string& path) +{ + if (fs::is_directory(path)) + { + setBuf(s.sessionBuf, sizeof(s.sessionBuf), path); + loadSession(s); + return; + } + + std::string ext = fs::path(path).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + if (ext == ".json") + { + setBuf(s.calibBuf, sizeof(s.calibBuf), path); + loadCalib(s); + } + else + { + s.status = "Unsupported dropped file: " + path; + } +} + static void actionExportColoredPointCloud(AppState& s) { std::string defaultName = fs::path(s.exportBuf).filename().string(); @@ -1398,6 +1425,17 @@ int main(int argc, char* argv[]) bool imguiWants = ImGui::GetIO().WantCaptureMouse; s.orbit.updateEulerTransition(GetFrameTime()); + // Drag & drop the LIO result directory (this app's session) or a calibration *.json onto + // the window to load it -- raylib's GLFW backend surfaces OS drag & drop the same way on + // Windows, Linux and macOS, so no platform-specific code is needed here. + if (IsFileDropped()) + { + FilePathList dropped = LoadDroppedFiles(); + if (dropped.count > 0) + handleDroppedPath(s, dropped.paths[0]); + UnloadDroppedFiles(dropped); + } + // pick up the ROS export result from the worker thread (if any) { std::lock_guard lk(s.rosMtx); diff --git a/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp b/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp index 204940ab..ddbf5046 100644 --- a/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp +++ b/apps/multi_view_tls_registration/multi_view_tls_registration_gui.cpp @@ -2361,6 +2361,27 @@ void loadSession(const std::string& session_file_name) } } +// Accepts a Mandeye JSON Session file (*.mjs/*.json) -- shared by the drag & drop handler in main()'s loop and +// the CLI argv handling below, so both accept the same input and report unsupported drops the same way. +void loadSessionFromPath(const std::string& path) +{ + std::string ext = fs::path(path).extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext != ".mjs" && ext != ".json") + { + spdlog::error("Unsupported file dropped: '{}'", path); + + [[maybe_unused]] pfd::message message( + "Load session", "Unsupported file:\n" + path + "\n\nDrop a session file (*.mjs/*.json).", pfd::choice::ok, pfd::icon::error); + message.result(); + return; + } + + session_file_name = path; + loadSession(path); +} + void openSession() { session_file_name = mandeye::fd::OpenFileDialogOneFile("Open session", mandeye::fd::Session_filter); @@ -6043,8 +6064,7 @@ int main(int argc, char* argv[]) if (ext == ".mjs" || ext == ".json") { - loadSession(argv[i]); - + loadSessionFromPath(argv[i]); break; } } @@ -6081,6 +6101,20 @@ int main(int argc, char* argv[]) if (wheelMove != 0.0f) wheel(0, wheelMove > 0.0f ? 1 : -1, mx, my); + // Drag & drop a session file (*.mjs/*.json) onto the window to load it. raylib's GLFW backend + // surfaces OS drag & drop the same way on Windows, Linux and macOS, so no platform-specific code is + // needed here. Only the first dropped path is used; loadSessionFromPath() reports unsupported drops + // via a message box instead of silently ignoring them. + if (IsFileDropped()) + { + FilePathList dropped_files = LoadDroppedFiles(); + if (dropped_files.count > 0) + { + loadSessionFromPath(dropped_files.paths[0]); + } + UnloadDroppedFiles(dropped_files); + } + BeginDrawing(); display(); EndDrawing();