Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
268 changes: 260 additions & 8 deletions src/gource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#include "gource.h"
#include "core/png_writer.h"

#include <algorithm>
#include <cmath>

bool gGourceDrawBackground = true;
bool gGourceQuadTreeDebug = false;
int gGourceMaxQuadTreeDepth = 6;
Expand Down Expand Up @@ -131,6 +134,9 @@ Gource::Gource(FrameExporter* exporter) {
dirNodeTree = 0;
userTree = 0;

// Initialize parking
parked_users.clear();

selectedFile = 0;
hoverFile = 0;
selectedUser = 0;
Expand Down Expand Up @@ -1053,6 +1059,19 @@ void Gource::deleteUser(RUser* user) {
users.erase(user->getName());
tagusermap.erase(user->getTagID());

std::unordered_map<RUser*, size_t>::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;
Expand Down Expand Up @@ -1364,21 +1383,68 @@ void Gource::updateBounds() {

void Gource::updateUsers(float t, float dt) {
std::vector<RUser*> inactiveUsers;
std::vector<RUser*> usersToUnpark;

size_t idle_users = 0;

// move users
for(std::map<std::string,RUser*>::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);
}

Expand Down Expand Up @@ -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<RUser*>::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<RUser*, size_t>::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<RUser*, size_t>::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<RUser*>::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() {
Expand Down
10 changes: 10 additions & 0 deletions src/gource.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <deque>
#include <list>
#include <fstream>
#include <unordered_map>

#include "core/display.h"
#include "core/shader.h"
Expand Down Expand Up @@ -189,6 +190,10 @@ class Gource : public SDLApp {
QuadTree* dirNodeTree;
QuadTree* userTree;

// Parking area management
std::vector<RUser*> parked_users;
std::unordered_map<RUser*, size_t> parking_slot_index;

std::string message;
float message_timer;

Expand Down Expand Up @@ -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();

Expand Down
Loading