From 9496ae0a50e6848121c7e913ca2dc55c8e6c84c1 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 5 Aug 2025 10:11:14 +0200 Subject: rewrote clustering --- src/glamac/glamac.c | 74 +-- src/glamac/glamac_events.c | 54 +- src/glamac/glamac_render.c | 1246 ++++++++++++-------------------------------- src/glamac/glamac_view.c | 398 ++++++++++++-- src/glamac/glass_data.c | 4 +- src/glautils/fgla.c | 72 ++- 6 files changed, 797 insertions(+), 1051 deletions(-) (limited to 'src') diff --git a/src/glamac/glamac.c b/src/glamac/glamac.c index 4b2f0fa..dba2fa1 100644 --- a/src/glamac/glamac.c +++ b/src/glamac/glamac.c @@ -12,8 +12,7 @@ // External function declarations from glamac_events.c extern b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, - i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit, - GlassCluster *clusters, i32 clusterCount); + i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit); // Function to reload fonts on window resize b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { @@ -51,59 +50,20 @@ b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { // Debug mode flag static b32 g_debug_mode = 0; +// Function to check debug mode (accessible from other files) +b32 is_debug_mode(void) { + return g_debug_mode; +} + int main(int argc, char* argv[]) { // Check for debug flag - for (int i = 1; i < argc; i++) { + for (i32 i = 1; i < argc; i++) { if (strcmp(argv[i], "--debug") == 0 || strcmp(argv[i], "-d") == 0) { g_debug_mode = 1; printf("Debug mode enabled\n"); break; } } - - // If debug mode, run without GUI - if (g_debug_mode) { - printf("Running in debug mode (no GUI)\n"); - - // Initialize glass data - initialize_glass_data(); - - // Create glass clusters for testing - i32 clusterCount = 0; - GlassCluster* clusters = create_glass_clusters(&clusterCount); - - printf("Glass data loaded: %d glasses\n", get_glass_count()); - printf("Created %d clusters\n", clusterCount); - - // Print first few glasses for verification - for (i32 i = 0; i < 10 && i < (i32)get_glass_count(); i++) { - const Glass* glass = get_glass(i); - if (glass) { - printf("Glass %d: %s (nd=%.4f, vd=%.2f, mfg=%s)\n", - i, glass->name, glass->refractiveIndex, glass->abbeNumber, glass->manufacturer); - } - } - - // Test collision detection with dummy data - printf("\nTesting collision detection...\n"); - LabelRect rect1 = {10, 10, 50, 20}; - LabelRect rect2 = {40, 15, 50, 20}; - LabelRect rect3 = {100, 100, 50, 20}; - - printf("Rect1 vs Rect2 overlap: %s\n", rects_overlap(&rect1, &rect2) ? "YES" : "NO"); - printf("Rect1 vs Rect3 overlap: %s\n", rects_overlap(&rect1, &rect3) ? "YES" : "NO"); - - printf("Label collision with point test:\n"); - printf("Label at point collision: %s\n", label_collides_with_point(&rect1, 30, 20, 5) ? "YES" : "NO"); - printf("Label far from point collision: %s\n", label_collides_with_point(&rect3, 30, 20, 5) ? "YES" : "NO"); - - // Cleanup - free_glass_clusters(clusters); - cleanup_glass_data(); - - printf("Debug mode complete\n"); - return 0; - } SDL_Window* window = NULL; SDL_Renderer* renderer = NULL; FontSet fonts = {0}; @@ -174,10 +134,14 @@ int main(int argc, char* argv[]) { ViewState view; init_view_state(&view, INITIAL_WIDTH, INITIAL_HEIGHT); - // Create glass clusters for similar glasses - i32 clusterCount = 0; - GlassCluster* clusters = create_glass_clusters(&clusterCount); - printf("Created %d glass clusters from %d glasses\n", clusterCount, get_glass_count()); + printf("Loaded %u glasses from %u catalogs\n", get_glass_count(), get_catalog_count()); + + // Create tight clusters for the initial catalog + create_tight_clusters(&view); + + // Create loose clusters for the initial catalog + create_loose_clusters(&view); + // Main loop flag b32 quit = 0; @@ -196,7 +160,7 @@ int main(int argc, char* argv[]) { while (!quit) { // Handle events on queue while (SDL_PollEvent(&e)) { - process_events(&e, &view, window, &lastMouseX, &lastMouseY, &dragging, &quit, clusters, clusterCount); + process_events(&e, &view, window, &lastMouseX, &lastMouseY, &dragging, &quit); } // Reload fonts if window size changed significantly @@ -205,18 +169,18 @@ int main(int argc, char* argv[]) { } // Render everything - render(renderer, fonts.regular, fonts.title, fonts.label, &view, clusters, clusterCount); + render(renderer, fonts.regular, fonts.title, fonts.label, &view); } // Stop text input SDL_StopTextInput(window); // Clean up resources - free_glass_clusters(clusters); + free_tight_clusters(&view); + free_loose_clusters(&view); cleanup_glass_data(); free_fonts(&fonts); clear_text_cache(); - // cleanup_static_label_positions(); // Removed with new collision system SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); TTF_Quit(); diff --git a/src/glamac/glamac_events.c b/src/glamac/glamac_events.c index 73d1133..51d1e9d 100644 --- a/src/glamac/glamac_events.c +++ b/src/glamac/glamac_events.c @@ -3,8 +3,13 @@ */ #include #include +#include #include "glamac_view.h" #include "glass_data.h" +#include "glamac_render.h" // For clear_text_cache + +// External debug mode function +extern b32 is_debug_mode(void); // Process key event b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *window, b32 *quit) { @@ -57,12 +62,14 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo // Zoom in at center view->zoomLevel *= ZOOM_FACTOR; if (view->zoomLevel > MAX_ZOOM) view->zoomLevel = MAX_ZOOM; + create_loose_clusters(view); // Recreate loose clusters on zoom return 1; case SDLK_MINUS: // Zoom out at center view->zoomLevel /= ZOOM_FACTOR; if (view->zoomLevel < MIN_ZOOM) view->zoomLevel = MIN_ZOOM; + create_loose_clusters(view); // Recreate loose clusters on zoom view->gKeyPressed = 0; // Reset g key state return 1; @@ -85,8 +92,12 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo cycle_catalog(-1); refresh_view_data_range(view); // Update view for new catalog view->selectedGlass = -1; // Clear selection - view->selectedCluster = -1; - printf("Switched to catalog: %s\n", get_current_catalog_name()); + view->selectedCluster = -1; // Clear cluster selection + clear_text_cache(); // Clear text cache when switching catalogs + create_tight_clusters(view); // Recreate tight clusters for new catalog + create_loose_clusters(view); // Recreate loose clusters for new catalog + printf("Switched to catalog: %s (%u glasses)\n", get_current_catalog_name(), get_glass_count()); + } else { // Move left view->offsetX += PAN_STEP; @@ -100,8 +111,11 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo cycle_catalog(1); refresh_view_data_range(view); // Update view for new catalog view->selectedGlass = -1; // Clear selection - view->selectedCluster = -1; - printf("Switched to catalog: %s\n", get_current_catalog_name()); + view->selectedCluster = -1; // Clear cluster selection + clear_text_cache(); // Clear text cache when switching catalogs + create_tight_clusters(view); // Recreate tight clusters for new catalog + create_loose_clusters(view); // Recreate loose clusters for new catalog + printf("Switched to catalog: %s (%u glasses)\n", get_current_catalog_name(), get_glass_count()); } else { // Move right view->offsetX -= PAN_STEP; @@ -126,7 +140,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo } // Process mouse button event -b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, GlassCluster *clusters, i32 clusterCount) { +b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging) { f32 mouseX, mouseY; switch (button->button) { @@ -134,17 +148,30 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las if (button->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { SDL_GetMouseState(&mouseX, &mouseY); - // Find the nearest glass point (no clustering) + // Find the nearest glass point i32 nearestGlass = find_nearest_glass((i32)mouseX, (i32)mouseY, view, 15); + if (nearestGlass >= 0) { - // Select the individual glass - view->selectedGlass = nearestGlass; - view->selectedCluster = -1; + // Check if clicked glass is part of a tight cluster + i32 clusterIndex = find_tight_cluster_for_glass(nearestGlass, view); + + if (clusterIndex >= 0 && view->tightClusters[clusterIndex].count > 1) { + // Clicked on a multi-glass tight cluster + view->selectedCluster = clusterIndex; + view->selectedGlass = -1; // Clear individual glass selection + } else { + // Clicked on individual glass or single-glass cluster + view->selectedGlass = nearestGlass; + view->selectedCluster = -1; // Clear cluster selection + } } else { - // If clicking outside any point, clear selection + // No glass clicked view->selectedGlass = -1; view->selectedCluster = -1; - // Start dragging for panning + } + + if (nearestGlass < 0) { + // Start dragging for panning if no glass was clicked *dragging = 1; *lastMouseX = (i32)mouseX; *lastMouseY = (i32)mouseY; @@ -203,8 +230,7 @@ b32 process_window_event(SDL_WindowEvent *window_event, ViewState *view) { // Process all events on queue b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, - i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit, - GlassCluster *clusters, i32 clusterCount) { + i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit) { switch (event->type) { case SDL_EVENT_QUIT: *quit = 1; @@ -222,7 +248,7 @@ b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: - return process_mouse_button(&event->button, view, lastMouseX, lastMouseY, dragging, clusters, clusterCount); + return process_mouse_button(&event->button, view, lastMouseX, lastMouseY, dragging); case SDL_EVENT_MOUSE_MOTION: return process_mouse_motion(&event->motion, view, lastMouseX, lastMouseY, *dragging); diff --git a/src/glamac/glamac_render.c b/src/glamac/glamac_render.c index 8536218..5230f1b 100644 --- a/src/glamac/glamac_render.c +++ b/src/glamac/glamac_render.c @@ -10,1041 +10,487 @@ #include "glamac_render.h" #include "glass_data.h" -// Drawing primitives - -// Helper function to check if a glass is part of a cluster -static inline i32 find_glass_cluster(i32 glassIndex, GlassCluster* clusters, i32 clusterCount) { - for (i32 i = 0; i < clusterCount; i++) { - for (i32 j = 0; j < clusters[i].count; j++) { - if (clusters[i].glassIndices[j] == glassIndex) { - return i; // Return cluster index - } - } - } - return -1; // Not in any cluster -} - -// Simple label positioning +// Text rendering cache typedef struct { - i32 dx, dy; -} LabelOffset; - -// Simplified positions - only 4 basic directions -static const LabelOffset LABEL_POSITIONS[] = { - {0, -1}, // Up - {1, 0}, // Right - {0, 1}, // Down - {-1, 0} // Left -}; -#define NUM_LABEL_POSITIONS (sizeof(LABEL_POSITIONS) / sizeof(LABEL_POSITIONS[0])) -#define LABEL_OFFSET_DISTANCE 8 // Reduced distance from glass point -#define LABEL_COLLISION_PADDING 1 // Minimal padding - - -// Simple collision check between two rectangles -b32 rects_overlap(const LabelRect* a, const LabelRect* b) { - return !(a->x + a->width + LABEL_COLLISION_PADDING < b->x || - b->x + b->width + LABEL_COLLISION_PADDING < a->x || - a->y + a->height + LABEL_COLLISION_PADDING < b->y || - b->y + b->height + LABEL_COLLISION_PADDING < a->y); -} - -// Check if label collides with glass point -b32 label_collides_with_point(const LabelRect* label, i32 pointX, i32 pointY, i32 pointRadius) { - i32 expandedRadius = pointRadius + LABEL_COLLISION_PADDING; - return !(label->x > pointX + expandedRadius || - label->x + label->width < pointX - expandedRadius || - label->y > pointY + expandedRadius || - label->y + label->height < pointY - expandedRadius); -} - -// Forward declarations -static inline i32 get_adaptive_radius(const ViewState* view, i32 baseRadius); -static inline const char* get_cluster_shortest_name(const GlassCluster* cluster); - -// Global storage for pre-calculated labels -static PreCalculatedLabel preCalculatedLabels[512]; -static i32 preCalculatedLabelCount = 0; - -// Simplified priority calculation - no clustering -static inline f32 calculate_simple_priority(i32 glassIndex, const ViewState* view, GlassCluster* clusters __attribute__((unused)), i32 clusterCount __attribute__((unused))) { - // Highest priority: Selected glass (always shows) - if (glassIndex == view->selectedGlass) { - return 1000.0f; - } - - // All other glasses: medium priority (always visible at default zoom) - return 150.0f; -} - -// Calculate label priority score (higher = more likely to show) -f32 calculate_label_priority(i32 glassIndex, const ViewState* view, GlassCluster* clusters, i32 clusterCount) { - return calculate_simple_priority(glassIndex, view, clusters, clusterCount); -} - -// Simplified label visibility - no clustering -b32 should_show_label_advanced(i32 glassIndex, const ViewState* view, GlassCluster* clusters, i32 clusterCount) { - f32 priority = calculate_label_priority(glassIndex, view, clusters, clusterCount); - - // Lower threshold to show labels at default zoom - const f32 threshold = 100.0f; - - return priority >= threshold; -} + char text[128]; + SDL_Texture* texture; + i32 width, height; + SDL_Color color; + TTF_Font* font; +} CachedText; -// Simple label positioning - always relative to glass points -void recalculate_label_positions(const ViewState* view, GlassCluster* clusters, i32 clusterCount) { - preCalculatedLabelCount = 0; - - // Simple array to track occupied positions - LabelRect occupiedRects[512]; - i32 occupiedCount = 0; - - const i32 padding = get_adaptive_padding(view); - const i32 baseRadius = 4; - const i32 radius = get_adaptive_radius(view, baseRadius); - const i32 labelOffset = radius + 8; // Fixed offset from glass point - - // Get visible data range - f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; - get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - - // Collect candidates and sort by priority - typedef struct { - i32 index; - f32 priority; - const char* labelText; - } LabelCandidate; - - LabelCandidate candidates[512]; - i32 candidateCount = 0; - - for (i32 i = 0; i < (i32)get_glass_count() && candidateCount < 512; i++) { - const Glass* glass = get_glass(i); - if (!glass) continue; - - if (!should_show_label_advanced(i, view, clusters, clusterCount)) continue; - - // Check visibility - if (glass->abbeNumber < visibleMinAbbe || glass->abbeNumber > visibleMaxAbbe || - glass->refractiveIndex < visibleMinRI || glass->refractiveIndex > visibleMaxRI) { - continue; - } - - i32 glassX, glassY; - data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); - - if (glassX < padding || glassX > view->windowWidth - padding || - glassY < padding || glassY > view->windowHeight - padding) { - continue; - } - - // No clustering - just use glass name - const char* labelText = (const char*)glass->name; - - candidates[candidateCount].index = i; - candidates[candidateCount].priority = calculate_label_priority(i, view, clusters, clusterCount); - candidates[candidateCount].labelText = labelText; - candidateCount++; - } - - // Sort by priority (highest first) - for (i32 i = 0; i < candidateCount - 1; i++) { - for (i32 j = i + 1; j < candidateCount; j++) { - if (candidates[j].priority > candidates[i].priority) { - LabelCandidate temp = candidates[i]; - candidates[i] = candidates[j]; - candidates[j] = temp; - } +static CachedText textCache[512]; +static i32 cacheSize = 0; + +// Find cached text texture +static SDL_Texture* get_cached_text(SDL_Renderer* renderer, TTF_Font* font, const char* text, SDL_Color color, i32* width, i32* height) { + for (i32 i = 0; i < cacheSize; i++) { + if (textCache[i].font == font && + textCache[i].color.r == color.r && + textCache[i].color.g == color.g && + textCache[i].color.b == color.b && + strcmp(textCache[i].text, text) == 0) { + *width = textCache[i].width; + *height = textCache[i].height; + return textCache[i].texture; } } - // Place labels in priority order with simple collision detection - for (i32 c = 0; c < candidateCount && preCalculatedLabelCount < 512; c++) { - LabelCandidate* candidate = &candidates[c]; - const Glass* glass = get_glass(candidate->index); - if (!glass) continue; - - i32 glassX, glassY; - data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); - - // Simple position: right of the glass point - i32 labelX = glassX + labelOffset; - i32 labelY = glassY - 7; // Slightly above center - - // Get approximate text size (simplified) - i32 textWidth = strlen(candidate->labelText) * 8; - i32 textHeight = 14; - - // Check for collisions - LabelRect newRect = {labelX, labelY, textWidth, textHeight}; - b32 collision = 0; - - // Check against existing labels - for (i32 i = 0; i < occupiedCount; i++) { - if (rects_overlap(&newRect, &occupiedRects[i])) { - collision = 1; - break; + // Create new cached texture + if (cacheSize < 512) { + SDL_Surface* surface = TTF_RenderText_Blended(font, text, strlen(text), color); + if (surface) { + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + if (texture) { + strncpy(textCache[cacheSize].text, text, sizeof(textCache[cacheSize].text) - 1); + textCache[cacheSize].text[sizeof(textCache[cacheSize].text) - 1] = '\0'; + textCache[cacheSize].texture = texture; + textCache[cacheSize].width = surface->w; + textCache[cacheSize].height = surface->h; + textCache[cacheSize].color = color; + textCache[cacheSize].font = font; + *width = surface->w; + *height = surface->h; + SDL_DestroySurface(surface); + cacheSize++; + return texture; } - } - - // Check collision with glass point - if (!collision && label_collides_with_point(&newRect, glassX, glassY, radius)) { - collision = 1; - } - - // Store the label position (even if collision - for selected items) - PreCalculatedLabel* newLabel = &preCalculatedLabels[preCalculatedLabelCount]; - newLabel->glassIndex = candidate->index; - newLabel->screenX = labelX; - newLabel->screenY = labelY; - newLabel->visible = !collision || candidate->priority >= 500.0f; // Always show selected - strncpy(newLabel->text, candidate->labelText, sizeof(newLabel->text) - 1); - newLabel->text[sizeof(newLabel->text) - 1] = '\0'; - - preCalculatedLabelCount++; - - // Add to occupied list if no collision - if (!collision && occupiedCount < 512) { - occupiedRects[occupiedCount] = newRect; - occupiedCount++; + SDL_DestroySurface(surface); } } + return NULL; } -// Draw pre-calculated labels -void draw_precalculated_labels(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view __attribute__((unused))) { - const SDL_Color blue = {0, 0, 150, 255}; - - for (i32 i = 0; i < preCalculatedLabelCount; i++) { - PreCalculatedLabel* label = &preCalculatedLabels[i]; - if (label->visible) { - draw_text(renderer, labelFont, label->text, label->screenX, label->screenY, blue); +void clear_text_cache(void) { + for (i32 i = 0; i < cacheSize; i++) { + if (textCache[i].texture) { + SDL_DestroyTexture(textCache[i].texture); } } + cacheSize = 0; } -// Check if label recalculation is needed and update tracking values -b32 needs_label_recalculation(ViewState* view) { - const f32 FLOAT_TOLERANCE = 0.001f; - - // Check if any of the factors that affect label positioning have changed - b32 needs_recalc = - (fabsf(view->zoomLevel - view->lastZoomLevel) > FLOAT_TOLERANCE) || - (view->windowWidth != view->lastWindowWidth) || - (view->windowHeight != view->lastWindowHeight) || - (fabsf(view->offsetX - view->lastOffsetX) > FLOAT_TOLERANCE) || - (fabsf(view->offsetY - view->lastOffsetY) > FLOAT_TOLERANCE); - - if (needs_recalc) { - // Update tracking values - view->lastZoomLevel = view->zoomLevel; - view->lastWindowWidth = view->windowWidth; - view->lastWindowHeight = view->windowHeight; - view->lastOffsetX = view->offsetX; - view->lastOffsetY = view->offsetY; +// Draw text helper +void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color) { + i32 width, height; + SDL_Texture* texture = get_cached_text(renderer, font, text, color, &width, &height); + if (texture) { + SDL_FRect destRect = {(f32)x, (f32)y, (f32)width, (f32)height}; + SDL_RenderTexture(renderer, texture, NULL, &destRect); } - - return needs_recalc; } -// Helper function to calculate adaptive radius based on window size and DPI -static inline i32 get_adaptive_radius(const ViewState* view, i32 baseRadius) { - // Scale based on window size - use average of width and height - f32 sizeScale = (view->windowWidth + view->windowHeight) / 1600.0f; // Reference: 1600 (avg of 1920x1080) - - // Clamp scale factor to reasonable bounds - if (sizeScale < 0.7f) sizeScale = 0.7f; - if (sizeScale > 2.5f) sizeScale = 2.5f; - - i32 scaledRadius = (i32)(baseRadius * sizeScale); - - // Ensure minimum radius for usability - if (scaledRadius < 2) scaledRadius = 2; - if (scaledRadius > 12) scaledRadius = 12; - - return scaledRadius; -} - -// Structure to hold cached text textures -typedef struct { - char text[64]; // The text string - SDL_Color color; // Text color - TTF_Font *font; // Font used - SDL_Texture *texture; // The cached texture - i32 width; // Texture width - i32 height; // Texture height - u32 lastUsed; // Timestamp for LRU eviction -} CachedText; - -// Text cache -#define MAX_TEXT_CACHE 2048 -static CachedText textCache[MAX_TEXT_CACHE] = {0}; -static i32 cacheCount = 0; - -// Function to draw text using SDL3_ttf with caching for improved performance -void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color) { - // Check if we already have this text cached - for (i32 i = 0; i < cacheCount; i++) { - CachedText *entry = &textCache[i]; - - // Compare text, font, and color to find a match - if (entry->font == font && - strcmp(entry->text, text) == 0 && - entry->color.r == color.r && - entry->color.g == color.g && - entry->color.b == color.b && - entry->color.a == color.a) { - - // Use the cached texture and update LRU timestamp - entry->lastUsed = SDL_GetTicks(); - SDL_FRect rect = {(f32)x, (f32)y, (f32)entry->width, (f32)entry->height}; - SDL_RenderTexture(renderer, entry->texture, NULL, &rect); - return; +// Draw text directly without caching (for axes labels) +void draw_text_direct(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color) { + SDL_Surface* surface = TTF_RenderText_Blended(font, text, strlen(text), color); + if (surface) { + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + if (texture) { + SDL_FRect destRect = {(f32)x, (f32)y, (f32)surface->w, (f32)surface->h}; + SDL_RenderTexture(renderer, texture, NULL, &destRect); + SDL_DestroyTexture(texture); } - } - - // Text not in cache, create a new texture - SDL_Surface *surface = TTF_RenderText_Blended(font, text, 0, color); - if (!surface) { - printf("TTF_RenderText_Blended failed: %s\n", SDL_GetError()); - return; - } - - SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); - if (!texture) { - printf("SDL_CreateTextureFromSurface failed: %s\n", SDL_GetError()); SDL_DestroySurface(surface); - return; - } - - // Draw the text - SDL_FRect rect = {(f32)x, (f32)y, (f32)surface->w, (f32)surface->h}; - SDL_RenderTexture(renderer, texture, NULL, &rect); - - // Add to cache or replace LRU entry - CachedText *entry = NULL; - if (cacheCount < MAX_TEXT_CACHE) { - // Cache has space, use new slot - entry = &textCache[cacheCount++]; - } else { - // Cache is full, find LRU entry to replace - u32 oldestTime = SDL_GetTicks(); - i32 oldestIndex = 0; - for (i32 i = 0; i < MAX_TEXT_CACHE; i++) { - if (textCache[i].lastUsed < oldestTime) { - oldestTime = textCache[i].lastUsed; - oldestIndex = i; - } - } - entry = &textCache[oldestIndex]; - - // Clean up old texture - if (entry->texture) { - SDL_DestroyTexture(entry->texture); - } } - - // Store new entry - strncpy(entry->text, text, sizeof(entry->text) - 1); - entry->text[sizeof(entry->text) - 1] = '\0'; - entry->color = color; - entry->font = font; - entry->texture = texture; - entry->width = surface->w; - entry->height = surface->h; - entry->lastUsed = SDL_GetTicks(); - - SDL_DestroySurface(surface); } - -// Function to draw a filled circle using SDL3's primitive functions +// Draw filled circle void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 radius) { - // Draw a filled circle using a series of horizontal lines - // This is significantly faster than the pixel-by-pixel approach for (i32 y = -radius; y <= radius; y++) { - // Calculate width of the horizontal line at this y position - i32 x_width = (i32)sqrtf((float)(radius * radius - y * y)); - - // Draw a horizontal line from left to right edge of the circle - SDL_RenderLine( - renderer, - (f32)(centerX - x_width), (f32)(centerY + y), - (f32)(centerX + x_width), (f32)(centerY + y) - ); + for (i32 x = -radius; x <= radius; x++) { + if (x*x + y*y <= radius*radius) { + SDL_RenderPoint(renderer, centerX + x, centerY + y); + } + } } } -// UI element rendering - -// Function to draw the axes with correct visible range -void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - const SDL_Color black = {0, 0, 0, 255}; - const i32 padding = get_adaptive_padding(view); - - // X-axis - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderLine(renderer, (f32)padding, (f32)(view->windowHeight - padding), - (f32)(view->windowWidth - padding), (f32)(view->windowHeight - padding)); - - // Y-axis - SDL_RenderLine(renderer, (f32)padding, (f32)padding, - (f32)padding, (f32)(view->windowHeight - padding)); +// Draw grid +void draw_grid(SDL_Renderer *renderer, const ViewState* view) { + SDL_SetRenderDrawColor(renderer, 230, 230, 230, 255); - // Calculate visible range based on current view - f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; - get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); + const i32 padding = get_adaptive_padding(view); + const i32 plotWidth = view->windowWidth - 2 * padding; + const i32 plotHeight = view->windowHeight - 2 * padding; - // Draw X-axis labels - now using the actual visible range - char label[32]; - for (i32 i = 0; i <= 5; i++) { - // For flipped axis: start with max and go to min - f32 value = visibleMaxAbbe - i * (visibleMaxAbbe - visibleMinAbbe) / 5; - i32 x = padding + i * (view->windowWidth - 2 * padding) / 5; - - SDL_RenderLine(renderer, (f32)x, (f32)(view->windowHeight - padding), - (f32)x, (f32)(view->windowHeight - padding + 5)); - sprintf(label, "%.1f", value); - draw_text(renderer, font, label, x - 15, view->windowHeight - padding + 10, black); + // Draw vertical grid lines (for Abbe numbers) + for (i32 i = 0; i <= 10; i++) { + i32 x = padding + (i * plotWidth) / 10; + SDL_RenderLine(renderer, x, padding, x, view->windowHeight - padding); } - // Draw Y-axis labels - now using the actual visible range - for (i32 i = 0; i <= 5; i++) { - f32 value = visibleMinRI + i * (visibleMaxRI - visibleMinRI) / 5; - i32 y = view->windowHeight - padding - i * (view->windowHeight - 2 * padding) / 5; - - SDL_RenderLine(renderer, (f32)(padding - 5), (f32)y, (f32)padding, (f32)y); - sprintf(label, "%.3f", value); - draw_text(renderer, font, label, padding - 50, y - 10, black); + // Draw horizontal grid lines (for refractive indices) + for (i32 i = 0; i <= 10; i++) { + i32 y = padding + (i * plotHeight) / 10; + SDL_RenderLine(renderer, padding, y, view->windowWidth - padding, y); } - - // Add axis titles with subscripts - // For X axis title (Abbe Number) - draw_text(renderer, titleFont, "Abbe Number (V", view->windowWidth / 2 - 80, view->windowHeight - padding + 30, black); - // Draw the subscript "d" with a smaller font and lowered position - draw_text(renderer, font, "d", view->windowWidth / 2 + 54, view->windowHeight - padding + 38, black); - // Draw the closing parenthesis - draw_text(renderer, titleFont, ")", view->windowWidth / 2 + 63, view->windowHeight - padding + 30, black); - - // For Y axis title (Refractive Index) - draw_text(renderer, titleFont, "Refractive Index (n", padding - 20, padding - 35, black); - // Draw the subscript "d" with a smaller font and lowered position - draw_text(renderer, font, "d", padding + 132, padding - 25, black); - // Draw the closing parenthesis - draw_text(renderer, titleFont, ")", padding + 141, padding - 35, black); } -// Function to draw a grid based on visible data range -void draw_grid(SDL_Renderer *renderer, const ViewState* view) { +// Draw axes +void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { + // Create smaller font for axis scales + TTF_Font *axisFont = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 12); + if (!axisFont) { + axisFont = font; // Fallback to regular font if loading fails + } const i32 padding = get_adaptive_padding(view); + const i32 plotWidth = view->windowWidth - 2 * padding; + const i32 plotHeight = view->windowHeight - 2 * padding; - // Calculate visible range for grid - f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; - get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); + SDL_Color black = {0, 0, 0, 255}; - // Determine grid step sizes based on zoom level - f32 xStep = 5.0f; - f32 yStep = 0.05f; + // Draw X axis (bottom) + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderLine(renderer, padding, view->windowHeight - padding, + view->windowWidth - padding, view->windowHeight - padding); - if (view->zoomLevel > 3.0f) { - xStep = 1.0f; - yStep = 0.01f; - } else if (view->zoomLevel > 7.0f) { - xStep = 0.5f; - yStep = 0.005f; - } + // Draw Y axis (left) + SDL_RenderLine(renderer, padding, padding, padding, view->windowHeight - padding); - // Round start values to nice grid intervals - const f32 xGridStart = floorf(visibleMinAbbe / xStep) * xStep; - const f32 yGridStart = floorf(visibleMinRI / yStep) * yStep; + // Calculate visible range ONCE for stable zoomed axes - with validation + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - // Draw vertical grid lines (X-axis) - for (f32 x = xGridStart; x <= visibleMaxAbbe; x += xStep) { - i32 screenX, screenY; - data_to_screen_coords(x, visibleMinRI, view, &screenX, &screenY); - - if (screenX >= padding && screenX <= view->windowWidth - padding) { - SDL_RenderLine(renderer, (f32)screenX, (f32)padding, - (f32)screenX, (f32)(view->windowHeight - padding)); + // Validate the visible range - fallback to full range if invalid + if (!isfinite(visibleMinAbbe) || !isfinite(visibleMaxAbbe) || + !isfinite(visibleMinRI) || !isfinite(visibleMaxRI) || + visibleMaxAbbe <= visibleMinAbbe || visibleMaxRI <= visibleMinRI) { + // Use full data range as fallback + visibleMinAbbe = view->minAbbe; + visibleMaxAbbe = view->maxAbbe; + visibleMinRI = view->minRI; + visibleMaxRI = view->maxRI; + } + + // Use float precision for range calculations + f32 abbeRange = visibleMaxAbbe - visibleMinAbbe; + f32 riRange = visibleMaxRI - visibleMinRI; + + // Draw axis labels + char buffer[32]; + + // X-axis labels (Abbe numbers - STABLE but ZOOMED positions) + for (i32 i = 0; i <= 10; i++) { + f32 normalizedX = (f32)i / 10.0f; + f32 abbeValue = visibleMaxAbbe - normalizedX * abbeRange; + i32 x = padding + (i * plotWidth) / 10; + + // Adaptive precision based on zoom level + if (abbeRange < 5.0f) { + snprintf(buffer, sizeof(buffer), "%.2f", abbeValue); + } else { + snprintf(buffer, sizeof(buffer), "%.1f", abbeValue); } + draw_text_direct(renderer, axisFont, buffer, x - 15, view->windowHeight - padding + 5, black); } - // Draw horizontal grid lines (Y-axis) - for (f32 y = yGridStart; y <= visibleMaxRI; y += yStep) { - i32 screenX, screenY; - data_to_screen_coords(visibleMinAbbe, y, view, &screenX, &screenY); + // Y-axis labels (refractive indices - STABLE but ZOOMED positions) + for (i32 i = 0; i <= 10; i++) { + f32 normalizedY = (f32)i / 10.0f; + f32 riValue = visibleMinRI + normalizedY * riRange; + i32 y = view->windowHeight - padding - (i * plotHeight) / 10; - if (screenY >= padding && screenY <= view->windowHeight - padding) { - SDL_RenderLine(renderer, (f32)padding, (f32)screenY, - (f32)(view->windowWidth - padding), (f32)screenY); + // Adaptive precision based on zoom level + if (riRange < 0.1f) { + snprintf(buffer, sizeof(buffer), "%.4f", riValue); + } else { + snprintf(buffer, sizeof(buffer), "%.3f", riValue); } + draw_text_direct(renderer, axisFont, buffer, padding - 45, y - 8, black); } -} - -// Helper function to find the shortest name in a cluster -static inline const char* get_cluster_shortest_name(const GlassCluster* cluster) { - if (cluster->count == 0) return ""; - - const Glass* shortestGlass = get_glass(cluster->glassIndices[0]); - if (!shortestGlass) return ""; - const char* shortestName = (const char*)shortestGlass->name; - size_t shortestLen = strlen(shortestName); + // Axis titles with subscripts (approximated) + draw_text(renderer, titleFont, "Abbe Number (Vd)", view->windowWidth/2 - 80, view->windowHeight - 25, black); - for (i32 i = 1; i < cluster->count; i++) { - const Glass* glass = get_glass(cluster->glassIndices[i]); - if (!glass) continue; - - const char* name = (const char*)glass->name; - size_t len = strlen(name); - if (len < shortestLen) { - shortestName = name; - shortestLen = len; - } - } + // Vertical text for Y-axis (simplified) + draw_text(renderer, titleFont, "Refractive Index (nd)", 5, 10, black); - return shortestName; -} - - -// Simple label position finder with better offset calculation -static inline i32 find_best_label_position(i32 glassX, i32 glassY, i32 textWidth, i32 textHeight, - LabelRect* existingLabels, i32 labelCount, i32 pointRadius) { - // Fixed offset - always outside the point - const i32 baseOffset = pointRadius + 6; // Always outside point with margin + // Display current catalog name at top center + const char* catalogName = get_current_catalog_name(); + char catalogTitle[100]; + snprintf(catalogTitle, sizeof(catalogTitle), "Catalog: %s", catalogName); + int titleWidth = strlen(catalogTitle) * 8; // Approximate width + draw_text(renderer, titleFont, catalogTitle, (view->windowWidth - titleWidth) / 2, 10, black); - // Try each position in order - for (i32 posIdx = 0; posIdx < (i32)NUM_LABEL_POSITIONS; posIdx++) { - const LabelOffset* pos = &LABEL_POSITIONS[posIdx]; - - // Calculate label position - i32 labelX = glassX + pos->dx * baseOffset; - i32 labelY = glassY + pos->dy * baseOffset; - - // Adjust for text alignment - if (pos->dx < 0) { // Left side - labelX -= textWidth; - } else if (pos->dx == 0) { // Center horizontally - labelX -= textWidth / 2; - } - - if (pos->dy < 0) { // Above - labelY -= textHeight; - } else if (pos->dy == 0) { // Center vertically - labelY -= textHeight / 2; - } - - LabelRect candidate = { - .x = labelX, - .y = labelY, - .width = textWidth, - .height = textHeight - }; - - // Check collision with existing labels - b32 collision = 0; - for (i32 i = 0; i < labelCount; i++) { - if (rects_overlap(&candidate, &existingLabels[i])) { - collision = 1; - break; - } - } - - if (!collision) { - return posIdx; - } + // Clean up axis font if we created it + if (axisFont != font) { + TTF_CloseFont(axisFont); } - - // If all positions have collisions, return -1 to indicate no placement - return -1; } - -// Function to draw glass points and labels with simple collision detection -void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view, GlassCluster* clusters, i32 clusterCount) { - const i32 padding = get_adaptive_padding(view); - const i32 baseRadius = 4; - const i32 radius = get_adaptive_radius(view, baseRadius); +// Draw glass points and labels - simplified version showing all labels +void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view) { + const u32 glassCount = get_glass_count(); + SDL_Color selectedColor = {255, 0, 0, 255}; // Red for selected + SDL_Color normalColor = {0, 150, 0, 255}; // Green for normal + SDL_Color labelColor = {0, 0, 0, 255}; // Black for labels - // Get visible data range for smart culling - f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; - get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - // Track which clusters have been processed - b32* clusterProcessed = (b32*)calloc(clusterCount, sizeof(b32)); - // Draw all glass points first - for (i32 i = 0; i < (i32)get_glass_count(); i++) { + // Draw all glass points and labels + for (u32 i = 0; i < glassCount; i++) { const Glass* glass = get_glass(i); if (!glass) continue; - // Smart culling: Check data bounds before expensive coordinate conversion - if (glass->abbeNumber < visibleMinAbbe || glass->abbeNumber > visibleMaxAbbe || - glass->refractiveIndex < visibleMinRI || glass->refractiveIndex > visibleMaxRI) { - continue; - } - i32 x, y; data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &x, &y); - // Final screen bounds check - if (x < padding || x > view->windowWidth - padding || - y < padding || y > view->windowHeight - padding) { + // Check if point is within the plot area (respecting axes padding) + const i32 padding = get_adaptive_padding(view); + if (x < padding || x >= view->windowWidth - padding || + y < padding || y >= view->windowHeight - padding) { continue; } - // Highlight selected glass only (no clustering) - if (i == view->selectedGlass) { - // Draw a larger highlight circle - SDL_SetRenderDrawColor(renderer, 100, 100, 255, 255); // Light blue highlight - draw_filled_circle(renderer, x, y, radius + 2); + // Set color based on selection + if ((i32)i == view->selectedGlass) { + SDL_SetRenderDrawColor(renderer, selectedColor.r, selectedColor.g, selectedColor.b, selectedColor.a); + } else { + SDL_SetRenderDrawColor(renderer, normalColor.r, normalColor.g, normalColor.b, normalColor.a); } - // Draw filled circle - SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); // Red for glass points - draw_filled_circle(renderer, x, y, radius); + // Draw glass point (larger) + draw_filled_circle(renderer, x, y, 5); + + // Draw label only if it should be shown (tight clustering logic) + if (should_show_glass_label((i32)i, view)) { + const char* glassName = (const char*)glass->name; + draw_text(renderer, labelFont, glassName, x + 12, y - 12, labelColor); + } } - - // Draw pre-calculated labels (simple and stable) - draw_precalculated_labels(renderer, labelFont, view); - - if (clusterProcessed) free(clusterProcessed); } -// Smart positioning for properties window (reverted to working version) +// Smart window positioning helper void calculate_smart_window_position(i32 glassX, i32 glassY, i32 windowWidth, i32 windowHeight, const ViewState* view, i32* windowX, i32* windowY) { - const i32 margin = 10; - const i32 baseOffset = 8 + (i32)(4.0f / view->zoomLevel); // Closer at higher zoom - - // Try positions in order of preference: right, left, below, above - typedef struct { - i32 x, y; - f32 distance; // Distance from glass point - } Position; - - Position positions[4] = { - {glassX + baseOffset, glassY - windowHeight / 2, 0}, // Right - {glassX - windowWidth - baseOffset, glassY - windowHeight / 2, 0}, // Left - {glassX - windowWidth / 2, glassY + baseOffset, 0}, // Below - {glassX - windowWidth / 2, glassY - windowHeight - baseOffset, 0} // Above - }; - - // Calculate distances and check bounds - for (i32 i = 0; i < 4; i++) { - Position* pos = &positions[i]; - - // Check if position is within screen bounds - if (pos->x >= margin && pos->x + windowWidth <= view->windowWidth - margin && - pos->y >= margin && pos->y + windowHeight <= view->windowHeight - margin) { - - // Calculate distance from glass point - i32 centerX = pos->x + windowWidth / 2; - i32 centerY = pos->y + windowHeight / 2; - f32 dx = centerX - glassX; - f32 dy = centerY - glassY; - pos->distance = sqrtf(dx * dx + dy * dy); - } else { - pos->distance = 10000.0f; // Invalid position - } - } - - // Find best valid position (shortest distance) - i32 bestIndex = 0; - for (i32 i = 1; i < 4; i++) { - if (positions[i].distance < positions[bestIndex].distance) { - bestIndex = i; - } + // Simple positioning: try right first, then left if doesn't fit + if (glassX + windowWidth + 10 < view->windowWidth) { + *windowX = glassX + 10; + } else { + *windowX = glassX - windowWidth - 10; + if (*windowX < 0) *windowX = 10; } - // If no good position found, use constrained positioning - if (positions[bestIndex].distance > 1000.0f) { - *windowX = glassX + baseOffset; - *windowY = glassY - windowHeight / 2; - - // Constrain to screen bounds - if (*windowX + windowWidth > view->windowWidth - margin) { - *windowX = view->windowWidth - windowWidth - margin; - } - if (*windowX < margin) { - *windowX = margin; - } - if (*windowY + windowHeight > view->windowHeight - margin) { - *windowY = view->windowHeight - windowHeight - margin; - } - if (*windowY < margin) { - *windowY = margin; - } + // Try above first, then below if doesn't fit + if (glassY - windowHeight - 10 > 0) { + *windowY = glassY - windowHeight - 10; } else { - *windowX = positions[bestIndex].x; - *windowY = positions[bestIndex].y; + *windowY = glassY + 10; + if (*windowY + windowHeight > view->windowHeight) { + *windowY = view->windowHeight - windowHeight - 10; + } } } -// Function to draw the properties popup for the selected glass -void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - if (view->selectedGlass < 0 || view->selectedGlass >= (i32)get_glass_count()) { - return; // No glass selected - } +// Draw cluster properties window (shows all glasses in tight cluster) +void draw_cluster_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { + if (view->selectedCluster < 0 || !view->tightClusters) return; - const Glass* glass = get_glass(view->selectedGlass); - if (!glass) return; // Safety check + const TightCluster* cluster = &view->tightClusters[view->selectedCluster]; + if (cluster->count == 0) return; - // Calculate glass position on screen - i32 glassX, glassY; - data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); + // Calculate cluster position on screen using representative glass + const Glass* repGlass = get_glass(cluster->representativeIndex); + if (!repGlass) return; + + i32 clusterX, clusterY; + data_to_screen_coords(repGlass->abbeNumber, repGlass->refractiveIndex, view, &clusterX, &clusterY); - // Define property window size - const i32 windowWidth = 200; - const i32 windowHeight = 100; - const i32 padding = 10; + // Window properties + const i32 windowWidth = 320; + const i32 windowHeight = 20 + cluster->count * 25; - // Use smart positioning i32 windowX, windowY; - calculate_smart_window_position(glassX, glassY, windowWidth, windowHeight, view, &windowX, &windowY); + calculate_smart_window_position(clusterX, clusterY, windowWidth, windowHeight, view, &windowX, &windowY); - // Draw background with slight transparency - SDL_FRect windowRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, 245, 245, 245, 235); - SDL_RenderFillRect(renderer, &windowRect); + // Draw background + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 220); + SDL_FRect bgRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; + SDL_RenderFillRect(renderer, &bgRect); // Draw border - SDL_SetRenderDrawColor(renderer, 40, 40, 40, 255); - SDL_RenderRect(renderer, &windowRect); - - // Draw glass name as title - SDL_Color darkBlue = {0, 0, 120, 255}; - draw_text(renderer, titleFont, (const char*)glass->name, windowX + padding, windowY + padding, darkBlue); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderRect(renderer, &bgRect); - // Draw properties + // Draw text SDL_Color black = {0, 0, 0, 255}; - char buffer[64]; - - // Refractive Index - sprintf(buffer, "n = %.4f", glass->refractiveIndex); - draw_text(renderer, font, buffer, windowX + padding, windowY + padding + 30, black); + char buffer[256]; - // Abbe Number - sprintf(buffer, "V = %.2f", glass->abbeNumber); - draw_text(renderer, font, buffer, windowX + padding, windowY + padding + 55, black); - - // Reset blend mode - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + // Draw each glass in cluster directly (no title) + for (i32 i = 0; i < cluster->count; i++) { + const Glass* glass = get_glass(cluster->glassIndices[i]); + if (glass) { + i32 yPos = windowY + 10 + i * 25; + + snprintf(buffer, sizeof(buffer), "%s: nd=%.4f, vd=%.2f", + (const char*)glass->name, glass->refractiveIndex, glass->abbeNumber); + draw_text(renderer, font, buffer, windowX + 10, yPos, black); + } + } } -// Function to draw properties for a cluster of similar glasses -void draw_cluster_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont __attribute__((unused)), const ViewState* view, GlassCluster* clusters, i32 clusterCount) { - if (view->selectedCluster < 0 || view->selectedCluster >= clusterCount) { - return; // No cluster selected - } +// Draw glass properties window +void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { + if (view->selectedGlass < 0) return; - const GlassCluster* cluster = &clusters[view->selectedCluster]; + const Glass* glass = get_glass(view->selectedGlass); + if (!glass) return; - // Calculate cluster position on screen - i32 clusterX, clusterY; - data_to_screen_coords(cluster->avgAbbeNumber, cluster->avgRefractiveIndex, view, &clusterX, &clusterY); + // Calculate glass position on screen + i32 glassX, glassY; + data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); - // Define property window size (dynamic height based on glass count) - const i32 windowWidth = 280; - const i32 windowHeight = 20 + cluster->count * 25; - const i32 padding = 10; + // Window properties + const i32 windowWidth = 300; + const i32 windowHeight = 120; - // Use smart positioning i32 windowX, windowY; - calculate_smart_window_position(clusterX, clusterY, windowWidth, windowHeight, view, &windowX, &windowY); + calculate_smart_window_position(glassX, glassY, windowWidth, windowHeight, view, &windowX, &windowY); - // Draw background with slight transparency - SDL_FRect windowRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, 245, 245, 245, 235); - SDL_RenderFillRect(renderer, &windowRect); + // Draw background + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 200); + SDL_FRect bgRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; + SDL_RenderFillRect(renderer, &bgRect); // Draw border - SDL_SetRenderDrawColor(renderer, 40, 40, 40, 255); - SDL_RenderRect(renderer, &windowRect); + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderRect(renderer, &bgRect); - // Draw properties for each glass in cluster directly (no title) + // Draw text SDL_Color black = {0, 0, 0, 255}; - for (i32 i = 0; i < cluster->count; i++) { - const Glass* glass = get_glass(cluster->glassIndices[i]); - if (!glass) continue; - - char buffer[128]; - i32 yPos = windowY + padding + i * 25; - - sprintf(buffer, "%s: n=%.4f, V=%.2f", (const char*)glass->name, glass->refractiveIndex, glass->abbeNumber); - draw_text(renderer, font, buffer, windowX + padding, yPos, black); - } - // Reset blend mode - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s", (const char*)glass->name); + draw_text(renderer, titleFont, buffer, windowX + 10, windowY + 10, black); + + snprintf(buffer, sizeof(buffer), "nd: %.5f", glass->refractiveIndex); + draw_text(renderer, font, buffer, windowX + 10, windowY + 40, black); + + snprintf(buffer, sizeof(buffer), "vd: %.2f", glass->abbeNumber); + draw_text(renderer, font, buffer, windowX + 10, windowY + 60, black); + + snprintf(buffer, sizeof(buffer), "Manufacturer: %s", (const char*)glass->manufacturer); + draw_text(renderer, font, buffer, windowX + 10, windowY + 80, black); } -// Function to draw the help window +// Draw help window void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - // Background for help window - i32 width = view->windowWidth - 200; - i32 height = view->windowHeight - 200; - i32 x = (view->windowWidth - width) / 2; - i32 y = (view->windowHeight - height) / 2; + if (!view->showHelp) return; + + const i32 windowWidth = 400; + const i32 windowHeight = 300; + const i32 windowX = (view->windowWidth - windowWidth) / 2; + const i32 windowY = (view->windowHeight - windowHeight) / 2; - SDL_FRect helpRect = {(f32)x, (f32)y, (f32)width, (f32)height}; - SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer, 245, 245, 245, 230); - SDL_RenderFillRect(renderer, &helpRect); + // Draw background + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 240); + SDL_FRect bgRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; + SDL_RenderFillRect(renderer, &bgRect); // Draw border SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderRect(renderer, &helpRect); + SDL_RenderRect(renderer, &bgRect); - // Draw help title + // Draw help text SDL_Color black = {0, 0, 0, 255}; - draw_text(renderer, titleFont, "Keyboard Shortcuts", x + width/2 - 100, y + 20, black); - - // Define all shortcuts - const char* shortcuts[] = { - "g? - Show/hide this help window", - "ESC, q - Quit application", - "+/- - Zoom in/out", - "h, j, k, l - Pan left, down, up, right", - "Shift+H, Shift+L - Previous/next catalog", - "r - Reset view to original position", - "f - Toggle fullscreen mode", - "Mouse wheel - Zoom in/out at cursor", - "Mouse drag - Pan the view", - "Mouse click - Select glass to see properties" - }; - - const i32 numShortcuts = sizeof(shortcuts) / sizeof(shortcuts[0]); - const i32 lineHeight = 30; - const i32 startY = y + 70; - - // Draw all shortcuts - for (i32 i = 0; i < numShortcuts; i++) { - draw_text(renderer, font, shortcuts[i], x + 50, startY + i * lineHeight, black); - } - - // Draw exit instruction - draw_text(renderer, font, "Press g? again, q or ESC to close this window", - x + width/2 - 150, y + height - 40, black); + i32 yPos = windowY + 20; + + draw_text(renderer, titleFont, "GlaMaC Help", windowX + 20, yPos, black); + yPos += 40; + + draw_text(renderer, font, "Mouse Controls:", windowX + 20, yPos, black); + yPos += 25; + draw_text(renderer, font, " Click: Select glass", windowX + 30, yPos, black); + yPos += 20; + draw_text(renderer, font, " Drag: Pan view", windowX + 30, yPos, black); + yPos += 20; + draw_text(renderer, font, " Wheel: Zoom in/out", windowX + 30, yPos, black); + yPos += 30; + + draw_text(renderer, font, "Keyboard Controls:", windowX + 20, yPos, black); + yPos += 25; + draw_text(renderer, font, " g?: Show/hide this help", windowX + 30, yPos, black); + yPos += 20; + draw_text(renderer, font, " r: Reset view", windowX + 30, yPos, black); + yPos += 20; + draw_text(renderer, font, " ESC: Clear selection or quit", windowX + 30, yPos, black); + yPos += 20; + draw_text(renderer, font, " q: Quit", windowX + 30, yPos, black); + yPos += 30; + + draw_text(renderer, font, "Press ESC or g? to close", windowX + 20, yPos, black); } // Main render function -void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, ViewState* view, GlassCluster* clusters, i32 clusterCount) { - const SDL_Color black = {0, 0, 0, 255}; - - // Only recalculate label positions when view parameters change - if (needs_label_recalculation(view)) { - recalculate_label_positions(view, clusters, clusterCount); - } - - // Clear screen (white background) +void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, ViewState* view) { + // Clear screen SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); SDL_RenderClear(renderer); // Draw grid - SDL_SetRenderDrawColor(renderer, 220, 220, 220, 255); draw_grid(renderer, view); - // Draw axes with correct visible range + // Draw axes draw_axes(renderer, font, titleFont, view); // Draw glass points and labels - draw_glass_points(renderer, labelFont, view, clusters, clusterCount); + draw_glass_points(renderer, labelFont, view); - // Draw cluster properties if a cluster is selected - if (view->selectedCluster >= 0) { - draw_cluster_properties(renderer, font, titleFont, view, clusters, clusterCount); - } - // Fallback to individual glass properties for backward compatibility - else if (view->selectedGlass >= 0) { - draw_glass_properties(renderer, font, titleFont, view); - } - - // Add catalog name at top center - const char* catalogName = get_current_catalog_name(); - i32 catalogTextWidth; - TTF_GetStringSize(titleFont, catalogName, 0, &catalogTextWidth, NULL); - draw_text(renderer, titleFont, catalogName, (view->windowWidth - catalogTextWidth) / 2, 20, black); + // Draw glass properties if selected + draw_glass_properties(renderer, font, titleFont, view); - // Add a small hint about help in the corner - draw_text(renderer, labelFont, "Press g? for help", 10, (i32)(0.975*view->windowHeight), black); + // Draw cluster properties if selected + draw_cluster_properties(renderer, font, titleFont, view); - // Draw help window if toggled - if (view->showHelp) { - draw_help_window(renderer, font, titleFont, view); - } + // Draw help window if needed + draw_help_window(renderer, font, titleFont, view); - // Update screen + // Present the rendered frame SDL_RenderPresent(renderer); } -// Font management - -// Helper function to try loading a font with fallbacks -static TTF_Font* try_load_font_from_paths(const char** fontNames, i32 numFonts, i32 size) { - TTF_Font* font = NULL; - - // Try all font paths - for (i32 i = 0; i < numFonts; i++) { - font = TTF_OpenFont(fontNames[i], size); - if (font) { - printf("Successfully loaded font: %s (size %d)\n", fontNames[i], size); - return font; - } - } - - // If all specific paths fail, try some generic system approaches - #ifndef _WIN32 - // Try to find any sans-serif font using common naming patterns - const char* genericFonts[] = { - "/usr/share/fonts/truetype/droid/DroidSans.ttf", - "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", - "/usr/share/fonts/TTF/DroidSans.ttf", - "/usr/share/fonts/noto/NotoSans-Regular.ttf", - "/usr/share/fonts/google-noto/NotoSans-Regular.ttf", - }; - - const i32 numGeneric = sizeof(genericFonts) / sizeof(genericFonts[0]); - for (i32 i = 0; i < numGeneric; i++) { - font = TTF_OpenFont(genericFonts[i], size); - if (font) { - printf("Successfully loaded generic font: %s (size %d)\n", genericFonts[i], size); - return font; - } - } - #endif - - return NULL; -} - -// Helper function to calculate DPI-aware font sizes -static void calculate_font_sizes(i32 windowWidth, i32 windowHeight, f32 dpi, i32* regular, i32* title, i32* label) { - // Base sizes for reference resolution (1920x1080) - const i32 baseRegular = 14; - const i32 baseTitle = 18; - const i32 baseLabel = 12; - - // Calculate scale factor based on window size and DPI - f32 sizeScale = (windowWidth + windowHeight) / 1600.0f; // Average of 1920x1080 - f32 dpiScale = dpi / 96.0f; // 96 DPI is standard - f32 totalScale = sizeScale * dpiScale; - - // Clamp scale factor to reasonable bounds - if (totalScale < 0.7f) totalScale = 0.7f; - if (totalScale > 2.5f) totalScale = 2.5f; - - *regular = (i32)(baseRegular * totalScale); - *title = (i32)(baseTitle * totalScale); - - // Apply configurable label scaling - f32 labelScale = totalScale * LABEL_SIZE_SCALE; // Use configurable scale factor - if (labelScale < 0.7f) labelScale = 0.7f; - if (labelScale > 2.0f) labelScale = 2.0f; // Allow larger labels - *label = (i32)(baseLabel * labelScale); +// Font management functions +b32 load_fonts(FontSet *fonts) { + fonts->regular = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 16); + fonts->title = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", 20); + fonts->label = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 14); - // Ensure minimum readable sizes - if (*regular < 10) *regular = 10; - if (*title < 12) *title = 12; - if (*label < 8) *label = 8; // Smaller minimum for labels + return fonts->regular && fonts->title && fonts->label; } -// Load fonts with DPI-aware sizing b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi) { - // Common font names to try, in order of preference - const char* fontNames[] = { - #ifdef _WIN32 - // Try bundled font first for Windows builds - "DejaVuSans.ttf", - "./DejaVuSans.ttf", - // Then try Windows system fonts - "C:\\Windows\\Fonts\\arial.ttf", - "C:\\Windows\\Fonts\\tahoma.ttf", - "C:\\Windows\\Fonts\\verdana.ttf", - "C:\\Windows\\Fonts\\calibri.ttf", - #else - // Ubuntu/Debian paths - "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", - "/usr/share/fonts/truetype/freefont/FreeSans.ttf", - // Arch Linux paths - "/usr/share/fonts/TTF/DejaVuSans.ttf", - "/usr/share/fonts/TTF/LiberationSans-Regular.ttf", - "/usr/share/fonts/gnu-free/FreeSans.ttf", - // Fedora/CentOS paths - "/usr/share/fonts/dejavu/DejaVuSans.ttf", - "/usr/share/fonts/liberation/LiberationSans-Regular.ttf", - // OpenSUSE paths - "/usr/share/fonts/truetype/DejaVuSans.ttf", - // Alternative common locations - "/usr/share/fonts/TTF/arial.ttf", - "/usr/share/fonts/truetype/msttcorefonts/arial.ttf", - "/System/Library/Fonts/Arial.ttf", // macOS - // Try local fonts directory - "./fonts/DejaVuSans.ttf", - "./DejaVuSans.ttf", - #endif - }; - - const i32 numFonts = sizeof(fontNames) / sizeof(fontNames[0]); - i32 regularSize, titleSize, labelSize; - calculate_font_sizes(windowWidth, windowHeight, dpi, ®ularSize, &titleSize, &labelSize); - - printf("Adaptive font sizes: regular=%d, title=%d, label=%d\n", regularSize, titleSize, labelSize); - - // Load regular font - fonts->regular = try_load_font_from_paths(fontNames, numFonts, regularSize); - if (!fonts->regular) { - printf("Failed to load any regular font!\n"); - printf("SDL_Error: %s\n", SDL_GetError()); - #ifndef _WIN32 - printf("Suggestion: Install fonts with: sudo apt install fonts-dejavu-core fonts-liberation (Ubuntu/Debian)\n"); - printf(" sudo pacman -S ttf-dejavu ttf-liberation (Arch)\n"); - printf(" sudo dnf install dejavu-sans-fonts liberation-fonts (Fedora)\n"); - #endif - return 0; - } - - // Load title font (try larger size first, fallback to regular font) - fonts->title = try_load_font_from_paths(fontNames, numFonts, titleSize); - if (!fonts->title) { - printf("Warning: Could not load title font, using regular font\n"); - fonts->title = fonts->regular; // Use the same font - } - - // Load label font (try smaller size first, fallback to regular font) - fonts->label = try_load_font_from_paths(fontNames, numFonts, labelSize); - if (!fonts->label) { - printf("Warning: Could not load label font, using regular font\n"); - fonts->label = fonts->regular; // Use the same font - } - - return 1; + (void)windowHeight; // Suppress unused parameter warning + // Calculate font sizes based on window size and DPI (increased sizes) + i32 regularSize = (i32)(16.0f * dpi / 96.0f * windowWidth / 800.0f); + i32 titleSize = (i32)(20.0f * dpi / 96.0f * windowWidth / 800.0f); + i32 labelSize = (i32)(14.0f * dpi / 96.0f * windowWidth / 800.0f); + + // Clamp font sizes + if (regularSize < 12) regularSize = 12; + if (regularSize > 32) regularSize = 32; + if (titleSize < 16) titleSize = 16; + if (titleSize > 36) titleSize = 36; + if (labelSize < 10) labelSize = 10; + if (labelSize > 24) labelSize = 24; + + fonts->regular = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", regularSize); + fonts->title = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans-Bold.ttf", titleSize); + fonts->label = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", labelSize); + + return fonts->regular && fonts->title && fonts->label; } -// Load all required fonts (legacy function for backward compatibility) -b32 load_fonts(FontSet *fonts) { - // Use default sizes - this function is for backward compatibility - return load_adaptive_fonts(fonts, 1920, 1080, 96.0f); -} - -// Free all fonts void free_fonts(FontSet *fonts) { - // Only close fonts that are different from regular font - if (fonts->regular) TTF_CloseFont(fonts->regular); - if (fonts->title && fonts->title != fonts->regular) TTF_CloseFont(fonts->title); - if (fonts->label && fonts->label != fonts->regular) TTF_CloseFont(fonts->label); -} - -// Clear text cache -void clear_text_cache(void) { - for (i32 i = 0; i < cacheCount; i++) { - if (textCache[i].texture) { - SDL_DestroyTexture(textCache[i].texture); - textCache[i].texture = NULL; - } + if (fonts->regular) { + TTF_CloseFont(fonts->regular); + fonts->regular = NULL; } - cacheCount = 0; -} + if (fonts->title) { + TTF_CloseFont(fonts->title); + fonts->title = NULL; + } + if (fonts->label) { + TTF_CloseFont(fonts->label); + fonts->label = NULL; + } +} \ No newline at end of file diff --git a/src/glamac/glamac_view.c b/src/glamac/glamac_view.c index ee75762..ff85b71 100644 --- a/src/glamac/glamac_view.c +++ b/src/glamac/glamac_view.c @@ -2,11 +2,26 @@ * glamac_view.c - Source file dealing with view states, filtering, zooming etc. of GlaMaC. */ #include +#include #include +#include #include #include "glamac_view.h" #include "glass_data.h" +// External debug mode function +extern b32 is_debug_mode(void); + +// Global tight cluster threshold parameters +f32 g_tight_cluster_nd_threshold = DEFAULT_TIGHT_CLUSTER_ND_THRESHOLD; +f32 g_tight_cluster_vd_threshold = DEFAULT_TIGHT_CLUSTER_VD_THRESHOLD; + +// Global loose cluster parameters (zoom-dependent) +f32 g_loose_cluster_nd_threshold = DEFAULT_LOOSE_CLUSTER_ND_THRESHOLD; // Base threshold +f32 g_loose_cluster_vd_threshold = DEFAULT_LOOSE_CLUSTER_VD_THRESHOLD; // Base threshold +f32 g_loose_cluster_nd_fraction = DEFAULT_LOOSE_CLUSTER_ND_FRACTION; // Zoom scaling fraction +f32 g_loose_cluster_vd_fraction = DEFAULT_LOOSE_CLUSTER_VD_FRACTION; // Zoom scaling fraction + // Initialize a view state with default values void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { view->zoomLevel = 1.0f; @@ -20,6 +35,14 @@ void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { view->selectedGlass = -1; // No glass selected initially view->selectedCluster = -1; // No cluster selected initially + // Initialize tight cluster data + view->tightClusters = NULL; + view->tightClusterCount = 0; + + // Initialize loose cluster data + view->looseClusters = NULL; + view->looseClusterCount = 0; + // Initialize label recalculation tracking (force initial calculation) view->lastZoomLevel = -1.0f; view->lastWindowWidth = -1; @@ -109,6 +132,9 @@ void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view const i32 padding = get_adaptive_padding(view); view->offsetX += (f32)(mouseX - newMouseScreenX) / (view->windowWidth - 2 * padding); view->offsetY += (f32)(newMouseScreenY - mouseY) / (view->windowHeight - 2 * padding); + + // Recreate loose clusters since zoom changed (they are zoom-dependent) + create_loose_clusters(view); } // Toggle fullscreen @@ -128,57 +154,263 @@ void reset_view(ViewState* view) { view->offsetX = 0.0f; view->offsetY = 0.0f; view->selectedGlass = -1; + + // Recreate loose clusters since zoom changed + create_loose_clusters(view); +} + +// Check if two glasses should be clustered based on separate nd and vd thresholds +static inline b32 should_cluster_glasses(const Glass* glass1, const Glass* glass2) { + f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); + f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); + + return (nd_diff <= g_tight_cluster_nd_threshold) && (vd_diff <= g_tight_cluster_vd_threshold); +} + +// Find glass with shortest name in a list +static i32 find_shortest_name_glass(const i32* glassIndices, i32 count) { + if (count == 0) return -1; + + i32 shortestIndex = glassIndices[0]; + size_t shortestLength = strlen((const char*)get_glass(shortestIndex)->name); + + for (i32 i = 1; i < count; i++) { + const Glass* glass = get_glass(glassIndices[i]); + if (glass) { + size_t nameLength = strlen((const char*)glass->name); + if (nameLength < shortestLength) { + shortestLength = nameLength; + shortestIndex = glassIndices[i]; + } + } + } + + return shortestIndex; +} + +// Create tight clusters for current catalog +void create_tight_clusters(ViewState* view) { + // Free existing clusters + free_tight_clusters(view); + + const u32 glassCount = get_glass_count(); + if (glassCount == 0) return; + + // Allocate temporary arrays + TightCluster* clusters = (TightCluster*)calloc(glassCount, sizeof(TightCluster)); + b32* processed = (b32*)calloc(glassCount, sizeof(b32)); + i32 clusterCount = 0; + + if (!clusters || !processed) { + free(clusters); + free(processed); + return; + } + + printf("Creating tight clusters for %s catalog (%u glasses, nd_threshold=%.6f, vd_threshold=%.3f)...\n", + get_current_catalog_name(), glassCount, g_tight_cluster_nd_threshold, g_tight_cluster_vd_threshold); + + // Simple clustering algorithm + for (u32 i = 0; i < glassCount; i++) { + if (processed[i]) continue; + + const Glass* glass1 = get_glass(i); + if (!glass1) continue; + + // Start new cluster with this glass + TightCluster* cluster = &clusters[clusterCount]; + cluster->glassIndices[0] = (i32)i; + cluster->count = 1; + cluster->avgAbbeNumber = glass1->abbeNumber; + cluster->avgRefractiveIndex = glass1->refractiveIndex; + processed[i] = 1; + + // Find nearby glasses within tight clustering distance + for (u32 j = i + 1; j < glassCount && cluster->count < MAX_CLUSTER_SIZE; j++) { + if (processed[j]) continue; + + const Glass* glass2 = get_glass(j); + if (!glass2) continue; + + if (should_cluster_glasses(glass1, glass2)) { + // Add to cluster + cluster->glassIndices[cluster->count] = (i32)j; + cluster->count++; + + // Update average position + cluster->avgAbbeNumber = (cluster->avgAbbeNumber * (cluster->count - 1) + glass2->abbeNumber) / cluster->count; + cluster->avgRefractiveIndex = (cluster->avgRefractiveIndex * (cluster->count - 1) + glass2->refractiveIndex) / cluster->count; + + processed[j] = 1; + } + } + + // Set representative glass (shortest name) + cluster->representativeIndex = find_shortest_name_glass(cluster->glassIndices, cluster->count); + + clusterCount++; + } + + // Count multi-glass clusters + i32 multiGlassClusters = 0; + for (i32 i = 0; i < clusterCount; i++) { + if (clusters[i].count > 1) { + multiGlassClusters++; + } + } + + printf("Created %d tight clusters (%d with multiple glasses)\n", clusterCount, multiGlassClusters); + + // Debug output: show detailed cluster information + if (is_debug_mode()) { + printf("\n=== TIGHT CLUSTER DEBUG INFO ===\n"); + printf("Catalog: %s, nd_threshold: %.6f, vd_threshold: %.3f\n", + get_current_catalog_name(), g_tight_cluster_nd_threshold, g_tight_cluster_vd_threshold); + + for (i32 i = 0; i < clusterCount; i++) { + const TightCluster* cluster = &clusters[i]; + if (cluster->count > 1) { + printf("\nCluster %d (%d glasses):\n", i, cluster->count); + for (i32 j = 0; j < cluster->count; j++) { + const Glass* glass = get_glass(cluster->glassIndices[j]); + if (glass) { + printf(" %s: nd=%.6f, vd=%.3f", + (const char*)glass->name, glass->refractiveIndex, glass->abbeNumber); + if (cluster->glassIndices[j] == cluster->representativeIndex) { + printf(" [REPRESENTATIVE]"); + } + printf("\n"); + } + } + + // Calculate and display pairwise distances within cluster + printf(" Pairwise distances:\n"); + for (i32 j = 0; j < cluster->count; j++) { + for (i32 k = j + 1; k < cluster->count; k++) { + const Glass* glass1 = get_glass(cluster->glassIndices[j]); + const Glass* glass2 = get_glass(cluster->glassIndices[k]); + if (glass1 && glass2) { + f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); + f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); + printf(" %s <-> %s: nd_diff=%.6f, vd_diff=%.3f\n", + (const char*)glass1->name, (const char*)glass2->name, nd_diff, vd_diff); + } + } + } + } + } + printf("=== END CLUSTER DEBUG INFO ===\n\n"); + } + + // Resize to actual number of clusters and store + if (clusterCount > 0) { + TightCluster* resizedClusters = (TightCluster*)realloc(clusters, clusterCount * sizeof(TightCluster)); + if (!resizedClusters) { + free(clusters); + view->tightClusters = NULL; + view->tightClusterCount = 0; + } else { + view->tightClusters = resizedClusters; + view->tightClusterCount = clusterCount; + } + } else { + free(clusters); + view->tightClusters = NULL; + view->tightClusterCount = 0; + } + + free(processed); +} + +// Free tight clusters +void free_tight_clusters(ViewState* view) { + if (view->tightClusters) { + free(view->tightClusters); + view->tightClusters = NULL; + } + view->tightClusterCount = 0; view->selectedCluster = -1; } -// Create clusters from glasses with similar properties -GlassCluster* create_glass_clusters(i32* clusterCount) { - const f32 SIMILARITY_THRESHOLD = 0.004f; // 0.4% difference threshold - const i32 maxGlasses = get_glass_count(); +// Create loose clusters for current catalog (zoom-dependent) +void create_loose_clusters(ViewState* view) { + // Free existing clusters + free_loose_clusters(view); + + const u32 glassCount = get_glass_count(); + if (glassCount == 0) return; + + // Calculate zoom-dependent thresholds that reach ZERO at maximum zoom + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - // Allocate temporary structures - GlassCluster* clusters = (GlassCluster*)calloc(maxGlasses, sizeof(GlassCluster)); - b32* processed = (b32*)calloc(maxGlasses, sizeof(b32)); - i32 numClusters = 0; + // Calculate zoom factor based on zoom level (0.0 at max zoom, 1.0 at min zoom) + // At MAX_ZOOM, factor = 0; At MIN_ZOOM, factor = 1 + f32 zoomRange = MAX_ZOOM - MIN_ZOOM; + f32 currentZoomNormalized = (view->zoomLevel - MIN_ZOOM) / zoomRange; + + // Invert so that factor = 0 at max zoom, factor = 1 at min zoom + f32 zoomFactor = 1.0f - (currentZoomNormalized > 1.0f ? 1.0f : currentZoomNormalized); + + // Ensure we reach exactly zero at maximum zoom + if (view->zoomLevel >= MAX_ZOOM) { + zoomFactor = 0.0f; + } + + // Calculate thresholds: ZERO at max zoom, base*fraction at min zoom + f32 ndThreshold = zoomFactor * g_loose_cluster_nd_threshold * g_loose_cluster_nd_fraction; + f32 vdThreshold = zoomFactor * g_loose_cluster_vd_threshold * g_loose_cluster_vd_fraction; + + // Allocate temporary arrays + LooseCluster* clusters = (LooseCluster*)calloc(glassCount, sizeof(LooseCluster)); + b32* processed = (b32*)calloc(glassCount, sizeof(b32)); + i32 clusterCount = 0; if (!clusters || !processed) { - if (clusters) free(clusters); - if (processed) free(processed); - *clusterCount = 0; - return NULL; + free(clusters); + free(processed); + return; } - for (i32 i = 0; i < (i32)maxGlasses; i++) { + printf("Creating loose clusters for %s catalog (%u glasses)...\n", get_current_catalog_name(), glassCount); + printf(" zoom level: %.2f, zoom factor: %.4f\n", view->zoomLevel, zoomFactor); + printf(" nd: %.3f * %.1f * %.4f = %.6f\n", + g_loose_cluster_nd_threshold, g_loose_cluster_nd_fraction, zoomFactor, ndThreshold); + printf(" vd: %.2f * %.1f * %.4f = %.3f\n", + g_loose_cluster_vd_threshold, g_loose_cluster_vd_fraction, zoomFactor, vdThreshold); + + // Simple clustering algorithm (same as tight clustering but with zoom-dependent thresholds) + for (u32 i = 0; i < glassCount; i++) { if (processed[i]) continue; const Glass* glass1 = get_glass(i); if (!glass1) continue; // Start new cluster with this glass - GlassCluster* cluster = &clusters[numClusters]; - cluster->glassIndices[0] = i; + LooseCluster* cluster = &clusters[clusterCount]; + cluster->glassIndices[0] = (i32)i; cluster->count = 1; cluster->avgAbbeNumber = glass1->abbeNumber; cluster->avgRefractiveIndex = glass1->refractiveIndex; processed[i] = 1; - // Find similar glasses to add to this cluster - for (i32 j = i + 1; j < (i32)maxGlasses && cluster->count < MAX_CLUSTER_SIZE; j++) { + // Find nearby glasses within loose clustering distance + for (u32 j = i + 1; j < glassCount && cluster->count < MAX_CLUSTER_SIZE; j++) { if (processed[j]) continue; const Glass* glass2 = get_glass(j); if (!glass2) continue; - // Check if glasses are similar (within 1% difference) - f32 abbeDiff = fabsf(glass1->abbeNumber - glass2->abbeNumber) / glass1->abbeNumber; - f32 riDiff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex) / glass1->refractiveIndex; + // Check if glasses should be clustered (separate nd/vd thresholds) + f32 nd_diff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); + f32 vd_diff = fabsf(glass1->abbeNumber - glass2->abbeNumber); - if (abbeDiff < SIMILARITY_THRESHOLD && riDiff < SIMILARITY_THRESHOLD) { - // Add to cluster and update averages - cluster->glassIndices[cluster->count] = j; + if (nd_diff <= ndThreshold && vd_diff <= vdThreshold) { + // Add to cluster + cluster->glassIndices[cluster->count] = (i32)j; cluster->count++; - // Update running average + // Update average position cluster->avgAbbeNumber = (cluster->avgAbbeNumber * (cluster->count - 1) + glass2->abbeNumber) / cluster->count; cluster->avgRefractiveIndex = (cluster->avgRefractiveIndex * (cluster->count - 1) + glass2->refractiveIndex) / cluster->count; @@ -186,50 +418,110 @@ GlassCluster* create_glass_clusters(i32* clusterCount) { } } - numClusters++; + // Set representative glass (shortest name) + cluster->representativeIndex = find_shortest_name_glass(cluster->glassIndices, cluster->count); + + clusterCount++; } - free(processed); + // Count multi-glass clusters + i32 multiGlassClusters = 0; + for (i32 i = 0; i < clusterCount; i++) { + if (clusters[i].count > 1) { + multiGlassClusters++; + } + } - // Resize to actual number of clusters - GlassCluster* finalClusters = (GlassCluster*)realloc(clusters, numClusters * sizeof(GlassCluster)); - if (!finalClusters) { + printf("Created %d loose clusters (%d with multiple glasses)\n", clusterCount, multiGlassClusters); + + // Resize to actual number of clusters and store + if (clusterCount > 0) { + LooseCluster* resizedClusters = (LooseCluster*)realloc(clusters, clusterCount * sizeof(LooseCluster)); + if (!resizedClusters) { + free(clusters); + view->looseClusters = NULL; + view->looseClusterCount = 0; + } else { + view->looseClusters = resizedClusters; + view->looseClusterCount = clusterCount; + } + } else { free(clusters); - *clusterCount = 0; - return NULL; + view->looseClusters = NULL; + view->looseClusterCount = 0; } - *clusterCount = numClusters; - return finalClusters; + free(processed); } -// Free cluster memory -void free_glass_clusters(GlassCluster* clusters) { - if (clusters) { - free(clusters); +// Free loose clusters +void free_loose_clusters(ViewState* view) { + if (view->looseClusters) { + free(view->looseClusters); + view->looseClusters = NULL; } + view->looseClusterCount = 0; } -// Find cluster at screen position -i32 find_cluster_at_position(GlassCluster* clusters, i32 clusterCount, i32 x, i32 y, const ViewState* view) { - const f32 maxDistance = 15.0f; // Maximum distance in pixels - i32 nearest = -1; - f32 minDist = maxDistance; +// Find tight cluster that contains a specific glass +i32 find_tight_cluster_for_glass(i32 glassIndex, const ViewState* view) { + if (!view->tightClusters) return -1; - for (i32 i = 0; i < clusterCount; i++) { - i32 clusterX, clusterY; - data_to_screen_coords(clusters[i].avgAbbeNumber, clusters[i].avgRefractiveIndex, view, &clusterX, &clusterY); + for (i32 i = 0; i < view->tightClusterCount; i++) { + for (i32 j = 0; j < view->tightClusters[i].count; j++) { + if (view->tightClusters[i].glassIndices[j] == glassIndex) { + return i; // Return cluster index + } + } + } + + return -1; // Not in any cluster +} + +// Find loose cluster that contains a specific glass +i32 find_loose_cluster_for_glass(i32 glassIndex, const ViewState* view) { + if (!view->looseClusters) return -1; + + for (i32 i = 0; i < view->looseClusterCount; i++) { + for (i32 j = 0; j < view->looseClusters[i].count; j++) { + if (view->looseClusters[i].glassIndices[j] == glassIndex) { + return i; // Return cluster index + } + } + } + + return -1; // Not in any cluster +} + +// Check if a glass label should be shown (combined tight and loose clustering logic) +b32 should_show_glass_label(i32 glassIndex, const ViewState* view) { + // First check tight clusters (they have priority) + i32 tightClusterIndex = find_tight_cluster_for_glass(glassIndex, view); + + if (tightClusterIndex >= 0) { + // Glass is in a tight cluster + const TightCluster* cluster = &view->tightClusters[tightClusterIndex]; - // Calculate distance - const f32 dx = x - clusterX; - const f32 dy = y - clusterY; - const f32 dist = sqrtf(dx*dx + dy*dy); + if (cluster->count > 1) { + // Multi-glass tight cluster: only show representative (shortest name) + return glassIndex == cluster->representativeIndex; + } + } + + // Then check loose clusters (only if not in tight cluster) + i32 looseClusterIndex = find_loose_cluster_for_glass(glassIndex, view); + + if (looseClusterIndex >= 0) { + // Glass is in a loose cluster + const LooseCluster* cluster = &view->looseClusters[looseClusterIndex]; - if (dist < minDist) { - minDist = dist; - nearest = i; + if (cluster->count > 1) { + // Multi-glass loose cluster: only show representative (shortest name) + return glassIndex == cluster->representativeIndex; } } - return nearest; + // Single glass or not in any cluster: always show + return 1; } + diff --git a/src/glamac/glass_data.c b/src/glamac/glass_data.c index bcead81..32077de 100644 --- a/src/glamac/glass_data.c +++ b/src/glamac/glass_data.c @@ -374,7 +374,7 @@ static GlamacResult load_all_manufacturers_to_catalogs(const char* json_content, GlamacResult result = load_single_manufacturer_to_catalog(json_content, json_len, manufacturers[i], catalog); if (result == GLAMAC_SUCCESS) { - printf("Loaded %d glasses from %s\n", catalog->count, catalog->name); + // printf("Loaded %d glasses from %s\n", catalog->count, catalog->name); g_glass_context.catalog_count++; // Set first successful catalog as current @@ -631,4 +631,4 @@ void cleanup_glass_data(void) { g_glass_context.catalog_count = 0; g_glass_context.current_catalog = -1; g_glass_context.using_json_data = 0; -} \ No newline at end of file +} diff --git a/src/glautils/fgla.c b/src/glautils/fgla.c index f2ed8eb..baf2fef 100644 --- a/src/glautils/fgla.c +++ b/src/glautils/fgla.c @@ -442,54 +442,66 @@ int main(int argc, char* argv[]) { // Load all glasses first, then filter by catalog during display loaded = load_glasses_from_json((const byte*)successful_path, NULL); - u32 glass_count = get_glass_count(); + // Search across all catalogs + u32 total_catalog_count = get_catalog_count(); u32 found_count = 0; // Determine search type int is_glass_code_search = is_glass_code_pattern(search_term); - // Pre-allocate matches array for performance (avoids double iteration) - u32* matching_indices = malloc(glass_count * sizeof(u32)); - if (!matching_indices) { + // Pre-allocate matches array for performance - estimate max size across all catalogs + u32* matching_indices = malloc(10000 * sizeof(u32)); // Generous allocation for all glasses + u32* matching_catalogs = malloc(10000 * sizeof(u32)); // Track which catalog each match is from + if (!matching_indices || !matching_catalogs) { fprintf(stderr, "Error: Memory allocation failed\n"); + free(matching_indices); + free(matching_catalogs); cleanup_glass_data(); return FGLA_ERROR_MEMORY; } - // Single pass: find and store matches + // Search through all catalogs found_count = 0; - for (u32 i = 0; i < glass_count; i++) { - const Glass* glass = get_glass(i); - if (!glass) continue; - - const char* glass_name = (const char*)get_glass_name(i); - if (!glass_name) continue; - - int matches = 0; + for (u32 catalog_idx = 0; catalog_idx < total_catalog_count; catalog_idx++) { + set_current_catalog(catalog_idx); + u32 glass_count = get_glass_count(); - if (is_glass_code_search) { - // Glass code pattern search - const char* glass_code = (const char*)glass->glass_code; - if (glass_code && matches_glass_code_pattern_safe(glass_code, search_term)) { - matches = 1; + for (u32 i = 0; i < glass_count; i++) { + const Glass* glass = get_glass(i); + if (!glass) continue; + + const char* glass_name = (const char*)get_glass_name(i); + if (!glass_name) continue; + + int matches = 0; + + if (is_glass_code_search) { + // Glass code pattern search + const char* glass_code = (const char*)glass->glass_code; + if (glass_code && matches_glass_code_pattern_safe(glass_code, search_term)) { + matches = 1; + } + } else { + // Name search + if (contains_substring_safe(glass_name, search_term)) { + matches = 1; + } } - } else { - // Name search - if (contains_substring_safe(glass_name, search_term)) { - matches = 1; + + // Check if this glass matches the search term and catalog filter + if (matches && matches_catalog((const char*)glass->manufacturer, catalog_list, catalog_count)) { + matching_indices[found_count] = i; + matching_catalogs[found_count] = catalog_idx; + found_count++; } } - - // Check if this glass matches the search term and catalog filter - if (matches && matches_catalog((const char*)glass->manufacturer, catalog_list, catalog_count)) { - matching_indices[found_count++] = i; - } } // Output results if (found_count == 0) { print_error_with_suggestion(FGLA_ERROR_NO_MATCHES, search_term); free(matching_indices); + free(matching_catalogs); cleanup_glass_data(); return FGLA_ERROR_NO_MATCHES; } @@ -500,6 +512,11 @@ int main(int argc, char* argv[]) { // Display matches from stored indices for (u32 j = 0; j < found_count; j++) { u32 i = matching_indices[j]; + u32 catalog_idx = matching_catalogs[j]; + + // Set the correct catalog for this glass + set_current_catalog(catalog_idx); + const Glass* glass = get_glass(i); const char* glass_name = (const char*)get_glass_name(i); @@ -512,6 +529,7 @@ int main(int argc, char* argv[]) { print_footer(output_format); free(matching_indices); + free(matching_catalogs); // Cleanup cleanup_glass_data(); -- cgit v1.2.3