From e3ee7ed7f74fa8408a80149153c5565fd6e39aa4 Mon Sep 17 00:00:00 2001 From: sokripon Date: Tue, 30 Dec 2025 11:20:21 +0100 Subject: [PATCH] Implement user parking feature with configurable settings --- README.md | 41 ++++++ src/gource.cpp | 268 ++++++++++++++++++++++++++++++++++++++-- src/gource.h | 10 ++ src/gource_settings.cpp | 155 +++++++++++++++++++++++ src/gource_settings.h | 14 +++ src/user.cpp | 97 ++++++++++++++- src/user.h | 23 ++++ 7 files changed, 599 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b1e5c2ec..aa20ef98 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,47 @@ options: --user-scale SCALE Change scale of user avatars. + --park-idle-users + Park inactive users at the screen edge instead of removing them. + + --park-immediate + Park users as soon as they become idle (implies --park-idle-users). + + --park-lock-slots + Prevent parked users from shifting when a slot before them becomes + free. Empty slots can be reused by any user. + + --park-position POSITION + Position of parking area (bottom, top, left, right). + Default: bottom. + + --park-direction DIRECTION + Fill direction for parking slots (forward, reverse). + Default: forward. + + --park-rows ROWS + Number of rows for parked users. Use 0 for auto. Default: 1. + + --park-round-robin + Distribute users across rows in round-robin order instead of + filling each row completely before starting the next. + + --park-y-offset PIXELS + Distance from the screen edge for the parking area. Default: 0. + + --park-spacing PIXELS + Spacing between parked users. Default: 0 (auto). + + --park-scale SCALE + Scale of parked user avatars. Default: 0.5. + + --park-opacity FLOAT + Opacity of parked users (0.0 to 1.0). Default: 0.5. + + --park-speed-factor FLOAT + Speed multiplier for users traveling to parking spots. + Default: 0.5. + --camera-mode MODE Camera mode (overview,track). diff --git a/src/gource.cpp b/src/gource.cpp index cf86c4f9..c5307d18 100644 --- a/src/gource.cpp +++ b/src/gource.cpp @@ -18,6 +18,9 @@ #include "gource.h" #include "core/png_writer.h" +#include +#include + bool gGourceDrawBackground = true; bool gGourceQuadTreeDebug = false; int gGourceMaxQuadTreeDepth = 6; @@ -131,6 +134,9 @@ Gource::Gource(FrameExporter* exporter) { dirNodeTree = 0; userTree = 0; + // Initialize parking + parked_users.clear(); + selectedFile = 0; hoverFile = 0; selectedUser = 0; @@ -1053,6 +1059,19 @@ void Gource::deleteUser(RUser* user) { users.erase(user->getName()); tagusermap.erase(user->getTagID()); + std::unordered_map::iterator it = parking_slot_index.find(user); + if(it != parking_slot_index.end()) { + size_t slot = it->second; + if(slot < parked_users.size() && parked_users[slot] == user) { + parked_users[slot] = nullptr; + } + parking_slot_index.erase(it); + } + + if(!gGourceSettings.park_lock_slots && !parked_users.empty()) { + parked_users.erase(std::remove(parked_users.begin(), parked_users.end(), nullptr), parked_users.end()); + } + //debugLog("deleted user %s, tagid = %d\n", user->getName().c_str(), user->getTagID()); delete user; @@ -1364,21 +1383,68 @@ void Gource::updateBounds() { void Gource::updateUsers(float t, float dt) { std::vector inactiveUsers; + std::vector usersToUnpark; size_t idle_users = 0; - // move users - for(std::map::iterator it = users.begin(); it!=users.end(); it++) { - RUser* u = it->second; + // If slot locking is disabled, drop any reserved slots and mapping + if(!gGourceSettings.park_lock_slots && !parking_slot_index.empty()) { + parking_slot_index.clear(); + parked_users.erase(std::remove(parked_users.begin(), parked_users.end(), (RUser*)0), parked_users.end()); + } + + for(RUser* u : parked_users) { + if(u == 0) continue; + if(!u->isIdle()) { + usersToUnpark.push_back(u); + } + } + + for(RUser* u : usersToUnpark) { + unparkUser(u); + } + + // Update parked user positions to follow camera (screen-fixed) + for(size_t i = 0; i < parked_users.size(); i++) { + RUser* u = parked_users[i]; + if(u == 0) continue; + + vec2 screen_pos = calcParkingPosition(i); + + if(u->isParked()) { + u->snapToParkingTarget(screen_pos); + } else if(u->isParking()) { + u->updateParkingTarget(screen_pos); + float snap_thresh = std::max(RUser::parkingArrivalThreshold() * 2.0f, + 10.0f * gGourceSettings.park_scale); + snap_thresh = std::max(snap_thresh, gGourceSettings.park_spacing * gGourceSettings.park_scale * 0.75f); + if(u->parkingDistance() <= snap_thresh) { + u->forceParkSnap(); + } + } + } + // move users + for(auto& kv : users) { + RUser* u = kv.second; u->logic(t, dt); - //deselect user if fading out from inactivity - if(u->isFading() && selectedUser == u) { + //deselect user if fading out from inactivity (only when not using parking) + if(!gGourceSettings.park_idle_users && u->isFading() && selectedUser == u) { selectUser(0); } - if(u->isInactive()) { + // Mark inactive users for parking or deletion + bool mark_inactive = false; + if(!u->isParked() && !u->isParking()) { + if(gGourceSettings.park_idle_users && gGourceSettings.park_immediate && u->isIdle()) { + mark_inactive = true; + } else if(u->isInactive()) { + mark_inactive = true; + } + } + + if(mark_inactive) { inactiveUsers.push_back(u); } @@ -1409,10 +1475,196 @@ void Gource::updateUsers(float t, float dt) { idle_time = 0; } - // delete inactive users + // delete inactive users or park them for(std::vector::iterator it = inactiveUsers.begin(); it != inactiveUsers.end(); it++) { - deleteUser(*it); + if(gGourceSettings.park_idle_users) { + parkUser(*it); + } else { + deleteUser(*it); + } + } +} + +vec2 Gource::calcParkingPosition(int slot) { + vec3 campos = camera.getPos(); + float cam_z = -campos.z; + + float fov_rad = camera.getFOV() * PI / 180.0f; + float half_height = cam_z * tan(fov_rad / 2.0f); + float aspect_ratio = display.width / (float)display.height; + float half_width = half_height * aspect_ratio; + + // Y increases upward, so max_y is visual top of screen + float view_max_y = campos.y + half_height; + float view_min_y = campos.y - half_height; + float view_left = campos.x - half_width; + float view_right = campos.x + half_width; + + bool horizontal = (gGourceSettings.park_position == GourceSettings::PARK_BOTTOM || gGourceSettings.park_position == GourceSettings::PARK_TOP); + float primary_length = horizontal ? (half_width * 2.0f) : (half_height * 2.0f); + + // Calculate spacing - auto if 0 (based on parked user size + minimal gap) + float scaled_spacing; + if(gGourceSettings.park_spacing <= 0.0f) { + float parked_user_size = 20.0f * gGourceSettings.user_scale * gGourceSettings.park_scale; + float min_gap = 5.0f * gGourceSettings.park_scale; + scaled_spacing = parked_user_size + min_gap; + } else { + scaled_spacing = gGourceSettings.park_spacing * gGourceSettings.park_scale; + } + float scaled_offset = gGourceSettings.park_y_offset * gGourceSettings.park_scale; + + int slots_per_row = std::max(1, (int)(primary_length / scaled_spacing)); + float margin = scaled_spacing * 0.5f; + + size_t max_slot = (size_t) slot; + for(size_t i = 0; i < parked_users.size(); i++) { + if(parked_users[i] != 0 && i > max_slot) { + max_slot = i; + } } + + int total_slots = (int) (max_slot + 1); + int effective_rows = gGourceSettings.park_rows; + if(effective_rows == 0) { + effective_rows = std::max(1, (int) std::ceil((float) total_slots / (float) slots_per_row)); + } + + int row, col; + + if(gGourceSettings.park_round_robin && effective_rows > 1) { + row = slot % effective_rows; + col = slot / effective_rows; + } else { + row = slot / slots_per_row; + col = slot % slots_per_row; + + if(effective_rows > 1 && row >= effective_rows) { + row = row % effective_rows; + } + } + + float x, y; + + if(gGourceSettings.park_position == GourceSettings::PARK_BOTTOM) { + y = view_max_y - scaled_offset - margin - row * scaled_spacing; + if(gGourceSettings.park_direction_reverse) { + x = view_right - margin - col * scaled_spacing; + } else { + x = view_left + margin + col * scaled_spacing; + } + } else if(gGourceSettings.park_position == GourceSettings::PARK_TOP) { + y = view_min_y + scaled_offset + margin + row * scaled_spacing; + if(gGourceSettings.park_direction_reverse) { + x = view_right - margin - col * scaled_spacing; + } else { + x = view_left + margin + col * scaled_spacing; + } + } else if(gGourceSettings.park_position == GourceSettings::PARK_LEFT) { + x = view_left + scaled_offset + margin + row * scaled_spacing; + if(gGourceSettings.park_direction_reverse) { + y = view_min_y + margin + col * scaled_spacing; + } else { + y = view_max_y - margin - col * scaled_spacing; + } + } else { + x = view_right - scaled_offset - margin - row * scaled_spacing; + if(gGourceSettings.park_direction_reverse) { + y = view_min_y + margin + col * scaled_spacing; + } else { + y = view_max_y - margin - col * scaled_spacing; + } + } + + return vec2(x, y); +} + +void Gource::recalcParkingPositions() { + for(size_t i = 0; i < parked_users.size(); i++) { + RUser* u = parked_users[i]; + if(u == 0) continue; + vec2 new_target = calcParkingPosition(i); + if(u->isParked()) { + u->snapToParkingTarget(new_target); + } else if(u->isParking()) { + u->updateParkingTarget(new_target); + } + } +} + +void Gource::parkUser(RUser* user) { + if(user->isParked() || user->isParking()) return; + + size_t slot = parked_users.size(); + + if(gGourceSettings.park_lock_slots) { + std::unordered_map::iterator it = parking_slot_index.find(user); + + if(it != parking_slot_index.end()) { + slot = it->second; + } else { + for(size_t i = 0; i < parked_users.size(); i++) { + if(parked_users[i] == 0) { + slot = i; + break; + } + } + if(slot == parked_users.size()) { + parked_users.push_back(0); + } + parking_slot_index[user] = slot; + } + + if(slot < parked_users.size() && parked_users[slot] != 0 && parked_users[slot] != user) { + size_t new_slot = parked_users.size(); + for(size_t i = 0; i < parked_users.size(); i++) { + if(parked_users[i] == 0) { + new_slot = i; + break; + } + } + if(new_slot == parked_users.size()) { + parked_users.push_back(0); + } + slot = new_slot; + parking_slot_index[user] = slot; + } + + if(slot >= parked_users.size()) { + parked_users.resize(slot + 1, 0); + } + + parked_users[slot] = user; + } else { + slot = parked_users.size(); + parked_users.push_back(user); + } + + vec2 target = calcParkingPosition((int) slot); + user->park(target); +} + +void Gource::unparkUser(RUser* user) { + if(!user->isParked() && !user->isParking()) return; + + if(gGourceSettings.park_lock_slots) { + std::unordered_map::iterator it = parking_slot_index.find(user); + if(it != parking_slot_index.end()) { + size_t slot = it->second; + if(slot < parked_users.size() && parked_users[slot] == user) { + parked_users[slot] = 0; + } + } + } else { + std::vector::iterator it = std::find(parked_users.begin(), parked_users.end(), user); + if(it != parked_users.end()) { + parked_users.erase(it); + } + parking_slot_index.erase(user); + } + + user->unpark(); + recalcParkingPositions(); } void Gource::interactDirs() { diff --git a/src/gource.h b/src/gource.h index 338e7ac5..fbf25387 100644 --- a/src/gource.h +++ b/src/gource.h @@ -21,6 +21,7 @@ #include #include #include +#include #include "core/display.h" #include "core/shader.h" @@ -189,6 +190,10 @@ class Gource : public SDLApp { QuadTree* dirNodeTree; QuadTree* userTree; + // Parking area management + std::vector parked_users; + std::unordered_map parking_slot_index; + std::string message; float message_timer; @@ -225,6 +230,11 @@ class Gource : public SDLApp { void updateUsers(float t, float dt); void updateDirs(float dt); + void parkUser(RUser* user); + void unparkUser(RUser* user); + vec2 calcParkingPosition(int slot); + void recalcParkingPositions(); + void interactUsers(); void interactDirs(); diff --git a/src/gource_settings.cpp b/src/gource_settings.cpp index 76ec9479..3dcf8fe7 100644 --- a/src/gource_settings.cpp +++ b/src/gource_settings.cpp @@ -163,6 +163,19 @@ if(extended_help) { printf(" --user-scale SCALE Change scale of users (default: 1.0)\n"); printf(" --max-user-speed UNITS Speed users can travel per second (default: 500)\n\n"); + printf(" --park-idle-users Park inactive users at screen edge instead of removing\n"); + printf(" --park-immediate Park users as soon as they go idle\n"); + printf(" --park-lock-slots Keep parking slots reserved once claimed (not per-user)\n"); + printf(" --park-y-offset PIXELS Distance from edge for parking area (default: 0)\n"); + printf(" --park-spacing PIXELS Spacing between parked users (default: 0 = auto)\n"); + printf(" --park-opacity FLOAT Opacity of parked users (default: 0.5)\n"); + printf(" --park-scale SCALE Scale of parked users (default: 0.5)\n"); + printf(" --park-speed-factor F Speed multiplier while parking (default: 0.5)\n"); + printf(" --park-rows ROWS Number of rows for parked users (0 = auto, default: 1)\n"); + printf(" --park-round-robin Distribute users across rows in round-robin order\n"); + printf(" --park-position POS Position of parking area: bottom, top, left, right\n"); + printf(" --park-direction DIR Fill direction: forward or reverse (default: forward)\n\n"); + printf(" --follow-user USER Camera will automatically follow this user\n"); printf(" --highlight-dirs Highlight the names of all directories\n"); printf(" --highlight-user USER Highlight the names of a particular user\n"); @@ -363,6 +376,19 @@ GourceSettings::GourceSettings() { arg_types["filename-time"] = "float"; arg_types["dir-name-depth"] = "int"; + + arg_types["park-idle-users"] = "bool"; + arg_types["park-immediate"] = "bool"; + arg_types["park-lock-slots"] = "bool"; + arg_types["park-y-offset"] = "float"; + arg_types["park-spacing"] = "float"; + arg_types["park-opacity"] = "float"; + arg_types["park-scale"] = "float"; + arg_types["park-speed-factor"] = "float"; + arg_types["park-rows"] = "int"; + arg_types["park-round-robin"] = "bool"; + arg_types["park-position"] = "string"; + arg_types["park-direction"] = "string"; } void GourceSettings::setGourceDefaults() { @@ -472,6 +498,19 @@ void GourceSettings::setGourceDefaults() { user_friction = 1.0f; user_scale = 1.0f; + park_idle_users = false; + park_immediate = false; + park_lock_slots = false; + park_y_offset = 0.0f; + park_spacing = 0.0f; + park_opacity = 0.5f; + park_scale = 0.5f; + park_speed_factor = 0.5f; + park_rows = 1; + park_round_robin = false; + park_position = PARK_BOTTOM; + park_direction_reverse = false; + follow_users.clear(); highlight_users.clear(); highlight_all_users = false; @@ -1432,6 +1471,122 @@ void GourceSettings::importGourceSettings(ConfFile& conffile, ConfSection* gourc } } + if(gource_settings->getBool("park-idle-users")) { + park_idle_users = true; + } + + if(gource_settings->getBool("park-immediate")) { + park_immediate = true; + // park-immediate implies park-idle-users + park_idle_users = true; + } + + if(gource_settings->getBool("park-lock-slots")) { + park_lock_slots = true; + } + + if((entry = gource_settings->getEntry("park-y-offset")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-y-offset (pixels)"); + + park_y_offset = entry->getFloat(); + + if(park_y_offset < 0.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-spacing")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-spacing (pixels)"); + + park_spacing = entry->getFloat(); + + if(park_spacing < 0.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-opacity")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-opacity (0.0-1.0)"); + + park_opacity = entry->getFloat(); + + if(park_opacity < 0.0f || park_opacity > 1.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-scale")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-scale (scale)"); + + park_scale = entry->getFloat(); + + if(park_scale <= 0.0f || park_scale > 2.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-speed-factor")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-speed-factor (multiplier)"); + + park_speed_factor = entry->getFloat(); + + if(park_speed_factor <= 0.0f || park_speed_factor > 5.0f) { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-rows")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-rows (number)"); + + park_rows = entry->getInt(); + + if(park_rows < 0 || park_rows > 10) { + conffile.invalidValueException(entry); + } + } + + if(gource_settings->getBool("park-round-robin")) { + park_round_robin = true; + } + + if((entry = gource_settings->getEntry("park-position")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-position (bottom, top, left, right)"); + + std::string pos = entry->getString(); + + if(pos == "bottom") { + park_position = PARK_BOTTOM; + } else if(pos == "top") { + park_position = PARK_TOP; + } else if(pos == "left") { + park_position = PARK_LEFT; + } else if(pos == "right") { + park_position = PARK_RIGHT; + } else { + conffile.invalidValueException(entry); + } + } + + if((entry = gource_settings->getEntry("park-direction")) != 0) { + + if(!entry->hasValue()) conffile.entryException(entry, "specify park-direction (forward, reverse)"); + + std::string dir = entry->getString(); + + if(dir == "reverse") { + park_direction_reverse = true; + } else if(dir != "forward") { + conffile.invalidValueException(entry); + } + } + if((entry = gource_settings->getEntry("max-user-speed")) != 0) { if(!entry->hasValue()) conffile.entryException(entry, "specify max-user-speed (units)"); diff --git a/src/gource_settings.h b/src/gource_settings.h index 1975ec59..659a2c62 100644 --- a/src/gource_settings.h +++ b/src/gource_settings.h @@ -136,6 +136,20 @@ class GourceSettings : public SDLAppSettings { float user_scale; float time_scale; + bool park_idle_users; + bool park_immediate; + bool park_lock_slots; + float park_y_offset; + float park_spacing; + float park_opacity; + float park_scale; + float park_speed_factor; + int park_rows; + bool park_round_robin; + enum ParkPosition { PARK_BOTTOM, PARK_TOP, PARK_LEFT, PARK_RIGHT }; + ParkPosition park_position; + bool park_direction_reverse; + bool highlight_dirs; bool highlight_all_users; diff --git a/src/user.cpp b/src/user.cpp index 9611f488..752b5855 100644 --- a/src/user.cpp +++ b/src/user.cpp @@ -34,6 +34,14 @@ RUser::RUser(const std::string& name, vec2 pos, int tagid) : Pawn(name,pos,tagid highlighted=false; + // Initialize parking state + parked = false; + parking = false; + parking_target = vec2(0.0f, 0.0f); + pre_park_pos = vec2(0.0f, 0.0f); + parked_size = size * gGourceSettings.park_scale; + parking_start_dist = 0.0f; + assignUserImage(); setSelected(false); @@ -296,7 +304,32 @@ void RUser::logic(float t, float dt) { accel = normalise(accel) * speed; } - pos += accel * dt; + if(parking) { + vec2 dir = parking_target - pos; + float dist = glm::length(dir); + float arrival_threshold = PARKING_ARRIVAL_THRESHOLD * gGourceSettings.park_scale; + + if(dist < arrival_threshold) { + pos = parking_target; + parking = false; + parked = true; + accel = vec2(0.0f, 0.0f); + size = parked_size; + dims = vec2(size, size * graphic_ratio); + } else { + float park_speed = speed * gGourceSettings.park_speed_factor; + vec2 move_dir = normalise(dir) * std::min(park_speed * dt, dist); + pos += move_dir; + + float progress = (parking_start_dist > 0.001f) ? (1.0f - (dist / parking_start_dist)) : 1.0f; + progress = glm::clamp(progress, 0.0f, 1.0f); + float target_size = BASE_USER_SIZE * gGourceSettings.user_scale; + size = glm::mix(target_size, parked_size, progress); + dims = vec2(size, size * graphic_ratio); + } + } else if(!parked) { + pos += accel * dt; + } accel = accel * std::max(0.0f, (1.0f - gGourceSettings.user_friction*dt)); } @@ -342,6 +375,15 @@ const std::string& RUser::getName() const { float RUser::getAlpha() const { float alpha = Pawn::getAlpha(); + + if(parked || parking) { + return gGourceSettings.park_opacity; + } + + if(gGourceSettings.park_idle_users) { + return alpha; + } + //user fades out if not doing anything if(elapsed - last_action > gGourceSettings.user_idle_time) { alpha = 1.0 - std::min(elapsed - last_action - gGourceSettings.user_idle_time, 1.0f); @@ -362,6 +404,59 @@ bool RUser::isInactive() { return isIdle() && (elapsed - last_action) > 10.0; } +void RUser::park(const vec2& target) { + if(parked) { + parked = false; + pre_park_pos = pos; + } else if(!parking) { + pre_park_pos = pos; + } + + parking_target = target; + parking = true; + parked_size = BASE_USER_SIZE * gGourceSettings.user_scale * gGourceSettings.park_scale; + parking_start_dist = glm::length(target - pre_park_pos); +} + +void RUser::unpark() { + if(!parked && !parking) return; + + parking = false; + parked = false; + size = BASE_USER_SIZE * gGourceSettings.user_scale; + dims = vec2(size, size * graphic_ratio); + last_action = elapsed; +} + +void RUser::updateParkingTarget(const vec2& target) { + float current_dist = glm::length(parking_target - pos); + float new_dist = glm::length(target - pos); + if(parking_start_dist > 0.001f && current_dist > 0.001f) { + parking_start_dist = parking_start_dist * (new_dist / current_dist); + } + parking_target = target; +} + +void RUser::snapToParkingTarget(const vec2& target) { + parking_target = target; + if(parked) { + setPos(target); + } +} + +float RUser::parkingDistance() const { + return glm::length(parking_target - pos); +} + +void RUser::forceParkSnap() { + parking = false; + parked = true; + pos = parking_target; + accel = vec2(0.0f, 0.0f); + size = parked_size; + dims = vec2(size, size * graphic_ratio); +} + bool RUser::nameVisible() const { return (Pawn::nameVisible() || gGourceSettings.highlight_all_users || highlighted) ? true : false; } diff --git a/src/user.h b/src/user.h index d0dbdd24..67ad99e5 100644 --- a/src/user.h +++ b/src/user.h @@ -52,6 +52,18 @@ class RUser : public Pawn { bool highlighted; + // Parking constants + static constexpr float PARKING_ARRIVAL_THRESHOLD = 5.0f; + static constexpr float BASE_USER_SIZE = 20.0f; + + // Parking state + bool parked; + bool parking; + vec2 parking_target; + vec2 pre_park_pos; + float parked_size; + float parking_start_dist; + bool nameVisible() const; void updateFont(); @@ -72,6 +84,17 @@ class RUser : public Pawn { bool isFading(); bool isInactive(); + bool isParked() const { return parked; } + bool isParking() const { return parking; } + static float parkingArrivalThreshold() { return PARKING_ARRIVAL_THRESHOLD; } + void park(const vec2& target); + void unpark(); + void updateParkingTarget(const vec2& target); + void snapToParkingTarget(const vec2& target); + float parkingDistance() const; + void forceParkSnap(); + vec2 getParkingTarget() const { return parking_target; } + void setSelected(bool selected); void setHighlighted(bool highlighted);