From 8e1ae9c581ea51bfef6e531865f75453180f31b6 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 4 Aug 2025 12:37:12 +0200 Subject: changes to glamac rendering --- Makefile | 18 +- data/xlsx/.~lock.CDGM202409.xlsx# | 1 - ...tt-optical-glass-overview-excel-format-en.xlsx# | 1 - include/glamac_render.h | 11 +- include/glamac_view.h | 33 +- include/glamacdef.h | 5 + src/glamac/glamac.c | 70 ++- src/glamac/glamac_events.c | 49 +- src/glamac/glamac_render.c | 603 +++++++++++++++++++-- src/glamac/glamac_view.c | 114 +++- src/glamac/glass_data.c | 61 +-- src/glautils/fgla.c | 7 +- src/glautils/gla.c | 139 ----- 13 files changed, 861 insertions(+), 251 deletions(-) delete mode 100644 data/xlsx/.~lock.CDGM202409.xlsx# delete mode 100644 data/xlsx/.~lock.schott-optical-glass-overview-excel-format-en.xlsx# delete mode 100644 src/glautils/gla.c diff --git a/Makefile b/Makefile index 5bd69f9..e2f0c54 100644 --- a/Makefile +++ b/Makefile @@ -30,14 +30,18 @@ BINDIR_WIN := bin/win INCDIR := include DLL_CACHE := $(HOME)/.cache/glamac-dlls +# Base flags +CFLAGS_BASE := -I$(INCDIR) -O2 -flto # Security and warning flags SECURITY_FLAGS := -fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE WARNING_FLAGS := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wstack-protector -Wvla # Compiler flags -CFLAGS := -I$(INCDIR) -O2 -flto $(SECURITY_FLAGS) $(WARNING_FLAGS) +CFLAGS := $(CFLAGS_BASE) $(SECURITY_FLAGS) $(WARNING_FLAGS) CFLAGS_NATIVE := $(CFLAGS) -march=native -CFLAGS_CROSS := $(CFLAGS) -I$(CROSS_PREFIX)/include +# Windows cross-compilation flags (without stack protector to avoid libssp dependency) +WARNING_FLAGS_WIN := -Wall -Wextra -Wformat=2 -Wformat-security -Wnull-dereference -Wvla +CFLAGS_CROSS := $(CFLAGS_BASE) $(WARNING_FLAGS_WIN) -I$(CROSS_PREFIX)/include # Source files GLAMAC_SRCS := $(wildcard $(SRCDIR)/glamac/*.c) @@ -121,6 +125,16 @@ win-data: | $(BINDIR_WIN) else \ echo "Warning: glasses.json not found, Windows build may use fallback data"; \ fi + @echo "Copying font for Windows..." + @if [ -f "/usr/share/fonts/TTF/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/TTF/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + elif [ -f "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" ]; then \ + cp /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf $(BINDIR_WIN)/; \ + echo "Copied DejaVuSans.ttf to Windows build"; \ + else \ + echo "Warning: DejaVu font not found, Windows build may fail to start"; \ + fi # Cross-compilation setup setup-cross: diff --git a/data/xlsx/.~lock.CDGM202409.xlsx# b/data/xlsx/.~lock.CDGM202409.xlsx# deleted file mode 100644 index 0eb5b77..0000000 --- a/data/xlsx/.~lock.CDGM202409.xlsx# +++ /dev/null @@ -1 +0,0 @@ -,DESKTOP-17RBHJN/Mikayel,DESKTOP-17RBHJN,03.08.2025 19:47,file:///C:/Users/Mikayel/AppData/Roaming/LibreOffice/4; \ No newline at end of file diff --git a/data/xlsx/.~lock.schott-optical-glass-overview-excel-format-en.xlsx# b/data/xlsx/.~lock.schott-optical-glass-overview-excel-format-en.xlsx# deleted file mode 100644 index be471ef..0000000 --- a/data/xlsx/.~lock.schott-optical-glass-overview-excel-format-en.xlsx# +++ /dev/null @@ -1 +0,0 @@ -,DESKTOP-17RBHJN/Mikayel,DESKTOP-17RBHJN,03.08.2025 19:44,file:///C:/Users/Mikayel/AppData/Roaming/LibreOffice/4; \ No newline at end of file diff --git a/include/glamac_render.h b/include/glamac_render.h index f18d53f..546bafe 100644 --- a/include/glamac_render.h +++ b/include/glamac_render.h @@ -16,12 +16,12 @@ void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 ra // UI element rendering void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); void draw_grid(SDL_Renderer *renderer, const ViewState* view); -void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view); +void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view, GlassCluster* clusters, i32 clusterCount); void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); // Main render function -void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, const ViewState* view); +void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, const ViewState* view, GlassCluster* clusters, i32 clusterCount); // Font management typedef struct { @@ -33,8 +33,15 @@ typedef struct { // Load all required fonts b32 load_fonts(FontSet *fonts); +// Load fonts with DPI-aware sizing +b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi); + // Free all fonts void free_fonts(FontSet *fonts); void clear_text_cache(void); +// Static label positioning +void calculate_static_label_positions(GlassCluster* clusters, i32 clusterCount, const ViewState* view); +void cleanup_static_label_positions(void); + #endif /* GLAMAC_RENDER_H */ diff --git a/include/glamac_view.h b/include/glamac_view.h index 8f0b984..21f273f 100644 --- a/include/glamac_view.h +++ b/include/glamac_view.h @@ -8,12 +8,27 @@ #include "glamacdef.h" // Constants for view -#define PADDING_PERCENT 0.08f // Padding as percentage of window size +#define MIN_PADDING 20 // Minimum padding in pixels +#define MAX_PADDING_PERCENT 0.04f // Maximum padding as percentage of window size #define PAN_STEP 0.05f // Step size for keyboard panning #define ZOOM_FACTOR 1.2f // Zoom factor for zoom operations #define MIN_ZOOM 0.5f // Minimum zoom level #define MAX_ZOOM 10.0f // Maximum zoom level +// Glass label positioning constants (adjustable parameters) +#define LABEL_OFFSET_X 12 // Horizontal offset from glass point (pixels) +#define LABEL_OFFSET_Y -8 // Vertical offset from glass point (pixels) +#define LABEL_SIZE_SCALE 1.4f // Scale factor for glass name labels + +// Glass clustering for nearly identical glasses +#define MAX_CLUSTER_SIZE 8 +typedef struct { + i32 glassIndices[MAX_CLUSTER_SIZE]; // Indices of glasses in this cluster + i32 count; // Number of glasses in cluster + f32 avgAbbeNumber; // Average position for rendering + f32 avgRefractiveIndex; +} GlassCluster; + // State for zooming and panning typedef struct { f32 zoomLevel; @@ -29,15 +44,22 @@ typedef struct { b32 gKeyPressed; // Flag to track if 'g' was pressed u32 gKeyTime; // Time when 'g' was pressed for sequence timing i32 selectedGlass; // Index of selected glass (-1 if none) + i32 selectedCluster; // Index of selected cluster (-1 if none) } ViewState; // Initialize a view state with default values void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight); +// Helper function to calculate adaptive padding +static inline i32 get_adaptive_padding(const ViewState* view) { + i32 padding = (i32)(view->windowWidth * MAX_PADDING_PERCENT); + return padding > MIN_PADDING ? padding : MIN_PADDING; +} + // Convert glass data to screen coordinates with zoom and offset static inline void data_to_screen_coords(f32 abbeNumber, f32 refractiveIndex, const ViewState* view, i32 *x, i32 *y) { - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); // Apply zoom and offset transformation // FLIPPED: Use 1.0f - normalized to flip the Abbe number axis @@ -56,7 +78,7 @@ static inline void data_to_screen_coords(f32 abbeNumber, f32 refractiveIndex, // Convert screen coordinates to data values static inline void screen_to_data_coords(i32 x, i32 y, const ViewState* view, f32 *abbeNumber, f32 *refractiveIndex) { - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); // Convert to normalized coordinates f32 normalizedX = (f32)(x - padding) / (view->windowWidth - 2 * padding); @@ -87,4 +109,9 @@ void toggle_fullscreen(SDL_Window* window); // Reset view to default void reset_view(ViewState* view); +// Glass clustering functions +GlassCluster* create_glass_clusters(i32* clusterCount); +void free_glass_clusters(GlassCluster* clusters); +i32 find_cluster_at_position(GlassCluster* clusters, i32 clusterCount, i32 x, i32 y, const ViewState* view); + #endif /* GLAMAC_VIEW_H */ diff --git a/include/glamacdef.h b/include/glamacdef.h index 563df70..aeba926 100644 --- a/include/glamacdef.h +++ b/include/glamacdef.h @@ -26,7 +26,12 @@ typedef uint64_t u64; typedef float f32; typedef double f64; typedef uintptr_t uptr; +#ifndef _WIN32 typedef char byte; +#else +// On Windows, use unsigned char to match Windows headers +typedef unsigned char byte; +#endif typedef ptrdiff_t size; typedef size_t usize; /* Utility macros */ diff --git a/src/glamac/glamac.c b/src/glamac/glamac.c index 60d0a1e..7988cc7 100644 --- a/src/glamac/glamac.c +++ b/src/glamac/glamac.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "glamacdef.h" // Type definitions #include "glass_data.h" // Glass catalog #include "glamac_view.h" // View management @@ -11,17 +12,48 @@ // 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); + i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit, + GlassCluster *clusters, i32 clusterCount); + +// Function to reload fonts on window resize +b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { + static i32 lastWidth = 0; + static i32 lastHeight = 0; + + // Check if window size changed significantly (more than 50% change) + if (abs(view->windowWidth - lastWidth) > view->windowWidth * 0.5 || + abs(view->windowHeight - lastHeight) > view->windowHeight * 0.5) { + + // Clear text cache first + clear_text_cache(); + + // Free existing fonts + free_fonts(fonts); + + // Reload with new size + if (!load_adaptive_fonts(fonts, view->windowWidth, view->windowHeight, dpi)) { + printf("Failed to reload fonts after window resize!\n"); + return 0; + } + + lastWidth = view->windowWidth; + lastHeight = view->windowHeight; + printf("Fonts reloaded for new window size: %dx%d\n", view->windowWidth, view->windowHeight); + } + + return 1; +} // Initial window dimensions #define INITIAL_WIDTH 800 #define INITIAL_HEIGHT 600 -int main(int argc, char* argv[]) { +int main(int argc __attribute__((unused)), char* argv[] __attribute__((unused))) { SDL_Window* window = NULL; SDL_Renderer* renderer = NULL; FontSet fonts = {0}; + // Initialize SDL3 if (!SDL_Init(SDL_INIT_VIDEO)) { printf("SDL3 could not initialize! SDL_Error: %s\n", SDL_GetError()); @@ -58,8 +90,21 @@ int main(int argc, char* argv[]) { // Enable vsync SDL_SetRenderVSync(renderer, 1); - // Load fonts - if (!load_fonts(&fonts)) { + // Get display DPI (fallback to default if not available) + f32 dpi = 96.0f; // Default DPI + #if SDL_VERSION_ATLEAST(3, 0, 0) + // SDL3 uses different display API + SDL_DisplayID* displays = SDL_GetDisplays(NULL); + if (displays) { + if (SDL_GetDisplayContentScale(displays[0]) > 0) { + dpi = 96.0f * SDL_GetDisplayContentScale(displays[0]); + } + SDL_free(displays); + } + #endif + + // Load fonts with adaptive sizing + if (!load_adaptive_fonts(&fonts, INITIAL_WIDTH, INITIAL_HEIGHT, dpi)) { SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); TTF_Quit(); @@ -74,6 +119,11 @@ 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()); + // Main loop flag b32 quit = 0; @@ -81,7 +131,6 @@ int main(int argc, char* argv[]) { SDL_Event e; // Mouse state - i32 mouseX = 0, mouseY = 0; i32 lastMouseX = 0, lastMouseY = 0; b32 dragging = 0; @@ -92,20 +141,27 @@ 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); + process_events(&e, &view, window, &lastMouseX, &lastMouseY, &dragging, &quit, clusters, clusterCount); + } + + // Reload fonts if window size changed significantly + if (!reload_fonts_if_needed(&fonts, &view, dpi)) { + quit = 1; // Exit if font reloading fails } // Render everything - render(renderer, fonts.regular, fonts.title, fonts.label, &view); + render(renderer, fonts.regular, fonts.title, fonts.label, &view, clusters, clusterCount); } // Stop text input SDL_StopTextInput(window); // Clean up resources + free_glass_clusters(clusters); cleanup_glass_data(); free_fonts(&fonts); clear_text_cache(); + cleanup_static_label_positions(); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); TTF_Quit(); diff --git a/src/glamac/glamac_events.c b/src/glamac/glamac_events.c index e5c706c..63bb07c 100644 --- a/src/glamac/glamac_events.c +++ b/src/glamac/glamac_events.c @@ -28,9 +28,10 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo break; case SDLK_ESCAPE: - // ESC closes glass selection first, then help window, finally quits - if (view->selectedGlass >= 0) { + // ESC closes glass/cluster selection first, then help window, finally quits + if (view->selectedGlass >= 0 || view->selectedCluster >= 0) { view->selectedGlass = -1; // Clear selection + view->selectedCluster = -1; } else if (view->showHelp) { view->showHelp = 0; } else { @@ -105,7 +106,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) { +b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, GlassCluster *clusters, i32 clusterCount) { f32 mouseX, mouseY; switch (button->button) { @@ -113,14 +114,34 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las if (button->type == SDL_EVENT_MOUSE_BUTTON_DOWN) { SDL_GetMouseState(&mouseX, &mouseY); - // Check if click is near a glass point - i32 nearestGlass = find_nearest_glass((i32)mouseX, (i32)mouseY, view, 15); // 15 pixels max distance + // First find the nearest glass point + i32 nearestGlass = find_nearest_glass((i32)mouseX, (i32)mouseY, view, 15); if (nearestGlass >= 0) { - view->selectedGlass = nearestGlass; - // Don't start dragging when selecting a glass + // Check if this glass is part of a cluster + i32 glassClusterIndex = -1; + for (i32 i = 0; i < clusterCount; i++) { + for (i32 j = 0; j < clusters[i].count; j++) { + if (clusters[i].glassIndices[j] == nearestGlass) { + glassClusterIndex = i; + break; + } + } + if (glassClusterIndex >= 0) break; + } + + if (glassClusterIndex >= 0) { + // This glass is part of a cluster - select the cluster + view->selectedCluster = glassClusterIndex; + view->selectedGlass = -1; + } else { + // Individual glass not in any cluster + view->selectedGlass = nearestGlass; + view->selectedCluster = -1; + } } else { - // If clicking outside any glass point, clear selection + // If clicking outside any point, clear all selections view->selectedGlass = -1; + view->selectedCluster = -1; // Start dragging for panning *dragging = 1; *lastMouseX = (i32)mouseX; @@ -138,14 +159,14 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las } // Process mouse motion event -b32 process_mouse_motion(SDL_MouseMotionEvent *motion, ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 dragging) { +b32 process_mouse_motion(SDL_MouseMotionEvent *motion __attribute__((unused)), ViewState *view, i32 *lastMouseX, i32 *lastMouseY, b32 dragging) { if (dragging) { // Get current mouse position f32 mouseX, mouseY; SDL_GetMouseState(&mouseX, &mouseY); // Calculate normalized delta - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); const f32 dx = (mouseX - *lastMouseX) / (view->windowWidth - 2 * padding); const f32 dy = (mouseY - *lastMouseY) / (view->windowHeight - 2 * padding); @@ -170,6 +191,9 @@ b32 process_window_event(SDL_WindowEvent *window_event, ViewState *view) { view->windowWidth = (i32)window_event->data1; view->windowHeight = (i32)window_event->data2; return 1; + default: + // Ignore other window events + break; } return 0; @@ -177,7 +201,8 @@ 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) { + i32 *lastMouseX, i32 *lastMouseY, b32 *dragging, b32 *quit, + GlassCluster *clusters, i32 clusterCount) { switch (event->type) { case SDL_EVENT_QUIT: *quit = 1; @@ -195,7 +220,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); + return process_mouse_button(&event->button, view, lastMouseX, lastMouseY, dragging, clusters, clusterCount); 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 619a6e0..9d03645 100644 --- a/src/glamac/glamac_render.c +++ b/src/glamac/glamac_render.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "glamac_render.h" @@ -11,6 +12,111 @@ // Drawing primitives +// Smart label positioning +typedef struct { + i32 dx, dy; +} LabelOffset; + +static const LabelOffset LABEL_POSITIONS[] = { + {0, -1}, // Up - closest to point + {-1, 0}, // Left - second closest + {0, 1}, // Down - third closest + {1, 0}, // Right - fourth closest + {-1, -1}, // Top-left - further away + {1, -1}, // Top-right - further away + {-1, 1}, // Bottom-left - further away + {1, 1}, // Bottom-right - furthest away + // Additional positions for very crowded areas + {0, -2}, // Far up + {-2, 0}, // Far left + {0, 2}, // Far down + {2, 0}, // Far right + {-2, -2}, // Far top-left + {2, -2}, // Far top-right + {-2, 2}, // Far bottom-left + {2, 2}, // Far bottom-right + // Even more distant positions for extremely crowded areas + {0, -3}, // Very far up + {-3, 0}, // Very far left + {0, 3}, // Very far down + {3, 0}, // Very far right + {-3, -3}, // Very far top-left + {3, -3}, // Very far top-right + {-3, 3}, // Very far bottom-left + {3, 3} // Very far bottom-right +}; +#define NUM_LABEL_POSITIONS (sizeof(LABEL_POSITIONS) / sizeof(LABEL_POSITIONS[0])) +#define LABEL_OFFSET_DISTANCE 18 // Distance from glass point (increased to clear red dots) +#define LABEL_COLLISION_PADDING 2 // Extra padding around labels (reduced from 5 - less aggressive) + +// Structure to store static label positions (calculated once, zoom-independent) +typedef struct { + i32 glassIndex; // Index of the glass this label belongs to + i32 positionIndex; // Which position from LABEL_POSITIONS was chosen + const char* text; // Label text (pointer to glass name or cluster name) + b32 isCluster; // True if this is a cluster label + i32 clusterIndex; // Cluster index if isCluster is true +} StaticLabelPosition; + +// Global static label positions (calculated once at startup) +static StaticLabelPosition* g_staticLabels = NULL; +static i32 g_staticLabelCount = 0; + +// Helper function to calculate local glass density for label filtering +static inline f32 get_local_density(const Glass* targetGlass, const ViewState* view __attribute__((unused))) { + const f32 DENSITY_RADIUS = 0.02f; // Radius in data space (nd/vd units) + i32 nearbyCount = 0; + + for (i32 i = 0; i < (i32)get_glass_count(); i++) { + const Glass* glass = get_glass(i); + if (!glass || glass == targetGlass) continue; + + // Calculate distance in data space + f32 dx = glass->abbeNumber - targetGlass->abbeNumber; + f32 dy = glass->refractiveIndex - targetGlass->refractiveIndex; + f32 distance = sqrtf(dx*dx + dy*dy); + + if (distance < DENSITY_RADIUS) { + nearbyCount++; + } + } + + return (f32)nearbyCount; +} + +// Helper function to determine if a glass label should be shown +static inline b32 should_show_label(const Glass* glass, const ViewState* view, i32 glassIndex) { + // Always show selected glass label + if (glassIndex == view->selectedGlass) { + return 1; + } + + // Calculate density-based threshold + f32 density = get_local_density(glass, view); + const f32 DENSITY_THRESHOLD = 3.0f; // Maximum nearby glasses before hiding labels + + // Show label if density is low enough + return density < DENSITY_THRESHOLD; +} + +// 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 @@ -19,10 +125,11 @@ typedef struct { 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 128 +#define MAX_TEXT_CACHE 2048 static CachedText textCache[MAX_TEXT_CACHE] = {0}; static i32 cacheCount = 0; @@ -40,7 +147,8 @@ void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, entry->color.b == color.b && entry->color.a == color.a) { - // Use the cached texture + // 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; @@ -65,24 +173,39 @@ void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, SDL_FRect rect = {(f32)x, (f32)y, (f32)surface->w, (f32)surface->h}; SDL_RenderTexture(renderer, texture, NULL, &rect); - // Add to cache if there is space + // Add to cache or replace LRU entry + CachedText *entry = NULL; if (cacheCount < MAX_TEXT_CACHE) { - CachedText *entry = &textCache[cacheCount++]; - - // Copy text (with bounds checking) - 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; + // Cache has space, use new slot + entry = &textCache[cacheCount++]; } else { - // Cache is full, just clean up the texture - SDL_DestroyTexture(texture); + // 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); } @@ -109,7 +232,7 @@ void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 ra // 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 = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); // X-axis SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); @@ -165,7 +288,7 @@ void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, cons // Function to draw a grid based on visible data range void draw_grid(SDL_Renderer *renderer, const ViewState* view) { - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); // Calculate visible range for grid f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; @@ -210,25 +333,292 @@ void draw_grid(SDL_Renderer *renderer, const ViewState* view) { } } -// Function to draw glass points and labels -void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view) { +// 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 +} + +// 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); + + 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; + } + } + + return shortestName; +} + +// Helper function to check if two label rectangles overlap in screen space +static inline b32 labels_overlap_in_screen(i32 x1, i32 y1, i32 w1, i32 h1, i32 x2, i32 y2, i32 w2, i32 h2) { + return !(x1 + w1 + LABEL_COLLISION_PADDING < x2 || + x2 + w2 + LABEL_COLLISION_PADDING < x1 || + y1 + h1 + LABEL_COLLISION_PADDING < y2 || + y2 + h2 + LABEL_COLLISION_PADDING < y1); +} + +// Function to find best position for a label avoiding collisions with existing labels +static inline i32 find_best_static_position(i32 glassIndex, const char* labelText, + const ViewState* view, TTF_Font* labelFont) { + const Glass* glass = get_glass(glassIndex); + if (!glass) return 0; + + // Convert glass position to screen coordinates (using current view for calculation) + i32 glassX, glassY; + data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); + + // Get text dimensions + int textWidth, textHeight; + if (!TTF_GetStringSize(labelFont, labelText, 0, &textWidth, &textHeight)) { + textWidth = strlen(labelText) * 8; // Fallback estimate + textHeight = 14; + } + + // Try each position in order of preference + for (i32 posIdx = 0; posIdx < (i32)NUM_LABEL_POSITIONS; posIdx++) { + const LabelOffset* pos = &LABEL_POSITIONS[posIdx]; + i32 labelX = glassX + pos->dx * LABEL_OFFSET_DISTANCE; + i32 labelY = glassY + pos->dy * LABEL_OFFSET_DISTANCE; + + // Check for collisions with existing labels + b32 collision = 0; + for (i32 j = 0; j < g_staticLabelCount; j++) { + const StaticLabelPosition* existingLabel = &g_staticLabels[j]; + const Glass* existingGlass = get_glass(existingLabel->glassIndex); + if (!existingGlass) continue; + + // Calculate existing label position + i32 existingGlassX, existingGlassY; + data_to_screen_coords(existingGlass->abbeNumber, existingGlass->refractiveIndex, + view, &existingGlassX, &existingGlassY); + + const LabelOffset* existingPos = &LABEL_POSITIONS[existingLabel->positionIndex]; + i32 existingLabelX = existingGlassX + existingPos->dx * LABEL_OFFSET_DISTANCE; + i32 existingLabelY = existingGlassY + existingPos->dy * LABEL_OFFSET_DISTANCE; + + // Get existing label dimensions + int existingWidth, existingHeight; + if (!TTF_GetStringSize(labelFont, existingLabel->text, 0, &existingWidth, &existingHeight)) { + existingWidth = strlen(existingLabel->text) * 8; + existingHeight = 14; + } + + // Check for overlap + if (labels_overlap_in_screen(labelX, labelY, textWidth, textHeight, + existingLabelX, existingLabelY, existingWidth, existingHeight)) { + collision = 1; + break; + } + } + + if (!collision) { + return posIdx; // Found a good position + } + } + + // If we get here, all positions had collisions - implement emergency positioning + // Find which position has the least overlap and use that as a starting point + i32 bestPosition = 0; + f32 minOverlap = 1000000.0f; // Large number + + for (i32 posIdx = 0; posIdx < (i32)NUM_LABEL_POSITIONS; posIdx++) { + const LabelOffset* pos = &LABEL_POSITIONS[posIdx]; + i32 labelX = glassX + pos->dx * LABEL_OFFSET_DISTANCE; + i32 labelY = glassY + pos->dy * LABEL_OFFSET_DISTANCE; + + f32 totalOverlap = 0.0f; + + // Calculate total overlap area with all existing labels + for (i32 j = 0; j < g_staticLabelCount; j++) { + const StaticLabelPosition* existingLabel = &g_staticLabels[j]; + const Glass* existingGlass = get_glass(existingLabel->glassIndex); + if (!existingGlass) continue; + + i32 existingGlassX, existingGlassY; + data_to_screen_coords(existingGlass->abbeNumber, existingGlass->refractiveIndex, + view, &existingGlassX, &existingGlassY); + + const LabelOffset* existingPos = &LABEL_POSITIONS[existingLabel->positionIndex]; + i32 existingLabelX = existingGlassX + existingPos->dx * LABEL_OFFSET_DISTANCE; + i32 existingLabelY = existingGlassY + existingPos->dy * LABEL_OFFSET_DISTANCE; + + int existingWidth, existingHeight; + if (!TTF_GetStringSize(labelFont, existingLabel->text, 0, &existingWidth, &existingHeight)) { + existingWidth = strlen(existingLabel->text) * 8; + existingHeight = 14; + } + + // Calculate overlap area + i32 overlapLeft = (labelX > existingLabelX) ? labelX : existingLabelX; + i32 overlapRight = ((labelX + textWidth) < (existingLabelX + existingWidth)) ? + (labelX + textWidth) : (existingLabelX + existingWidth); + i32 overlapTop = (labelY > existingLabelY) ? labelY : existingLabelY; + i32 overlapBottom = ((labelY + textHeight) < (existingLabelY + existingHeight)) ? + (labelY + textHeight) : (existingLabelY + existingHeight); + + if (overlapLeft < overlapRight && overlapTop < overlapBottom) { + f32 overlapArea = (overlapRight - overlapLeft) * (overlapBottom - overlapTop); + totalOverlap += overlapArea; + } + } + + if (totalOverlap < minOverlap) { + minOverlap = totalOverlap; + bestPosition = posIdx; + } + } + + // Debug trace for problematic labels + if (strstr(labelText, "P-LASF47") || strstr(labelText, "N-LASF43")) { + printf("Label %s had collisions in all positions, using best position %d with overlap %.1f\n", + labelText, bestPosition, minOverlap); + } + + return bestPosition; +} + +// Function to calculate static label positions once at startup +void calculate_static_label_positions(GlassCluster* clusters, i32 clusterCount, const ViewState* view) { + // Clean up any existing static labels + if (g_staticLabels) { + free(g_staticLabels); + g_staticLabels = NULL; + g_staticLabelCount = 0; + } + + // Allocate for maximum possible labels + g_staticLabels = (StaticLabelPosition*)calloc(get_glass_count(), sizeof(StaticLabelPosition)); + if (!g_staticLabels) return; + + // We need a font for text size calculation - use a reasonable default size + TTF_Font* tempFont = TTF_OpenFont("/usr/share/fonts/TTF/DejaVuSans.ttf", 12); + if (!tempFont) { + // Try alternative paths + tempFont = TTF_OpenFont("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 12); + } + + // Track which clusters have been processed + b32* clusterProcessed = (b32*)calloc(clusterCount, sizeof(b32)); + if (!clusterProcessed) { + free(g_staticLabels); + g_staticLabels = NULL; + if (tempFont) TTF_CloseFont(tempFont); + return; + } + + // Process each glass and determine label positions with collision avoidance + for (i32 i = 0; i < (i32)get_glass_count(); i++) { + const Glass* glass = get_glass(i); + if (!glass) continue; + + // Skip if this glass shouldn't show a label + if (!should_show_label(glass, view, i)) continue; + + // Check if this glass is part of a cluster + i32 clusterIndex = find_glass_cluster(i, clusters, clusterCount); + + if (clusterIndex >= 0) { + // This glass is part of a cluster + if (!clusterProcessed[clusterIndex]) { + // This is the first glass from this cluster, create cluster label + const char* clusterText = get_cluster_shortest_name(&clusters[clusterIndex]); + + g_staticLabels[g_staticLabelCount].glassIndex = i; + g_staticLabels[g_staticLabelCount].positionIndex = tempFont ? + find_best_static_position(i, clusterText, view, tempFont) : 0; + g_staticLabels[g_staticLabelCount].text = clusterText; + g_staticLabels[g_staticLabelCount].isCluster = 1; + g_staticLabels[g_staticLabelCount].clusterIndex = clusterIndex; + g_staticLabelCount++; + clusterProcessed[clusterIndex] = 1; + } + } else { + // Individual glass, not part of a cluster + g_staticLabels[g_staticLabelCount].glassIndex = i; + g_staticLabels[g_staticLabelCount].positionIndex = tempFont ? + find_best_static_position(i, (const char*)glass->name, view, tempFont) : 0; + g_staticLabels[g_staticLabelCount].text = (const char*)glass->name; + g_staticLabels[g_staticLabelCount].isCluster = 0; + g_staticLabels[g_staticLabelCount].clusterIndex = -1; + g_staticLabelCount++; + } + } + + free(clusterProcessed); + if (tempFont) TTF_CloseFont(tempFont); +} + +// Function to clean up static label positions +void cleanup_static_label_positions(void) { + if (g_staticLabels) { + free(g_staticLabels); + g_staticLabels = NULL; + g_staticLabelCount = 0; + } +} + +// Function to draw glass points and labels using static positions +void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view, GlassCluster* clusters, i32 clusterCount) { const SDL_Color blue = {0, 0, 150, 255}; - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); - const i32 radius = 4; + const i32 padding = get_adaptive_padding(view); + const i32 baseRadius = 4; + const i32 radius = get_adaptive_radius(view, baseRadius); + + // Get visible data range for smart culling + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); - for (i32 i = 0; i < get_glass_count(); i++) { + // If static positions haven't been calculated yet, calculate them + if (!g_staticLabels) { + calculate_static_label_positions(clusters, clusterCount, view); + } + + // Draw all glass points + for (i32 i = 0; i < (i32)get_glass_count(); 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); - // Skip if outside visible area + // Final screen bounds check if (x < padding || x > view->windowWidth - padding || y < padding || y > view->windowHeight - padding) { continue; } - // Highlight selected glass - if (i == view->selectedGlass) { + // Check if this glass is part of a cluster + i32 clusterIndex = find_glass_cluster(i, clusters, clusterCount); + + // Highlight selected glass or cluster + if (i == view->selectedGlass || (clusterIndex >= 0 && clusterIndex == view->selectedCluster)) { // Draw a larger highlight circle SDL_SetRenderDrawColor(renderer, 100, 100, 255, 255); // Light blue highlight draw_filled_circle(renderer, x, y, radius + 2); @@ -237,19 +627,47 @@ void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewSt // Draw filled circle SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255); // Red for glass points draw_filled_circle(renderer, x, y, radius); + } + + // Draw labels using static positions + for (i32 i = 0; i < g_staticLabelCount; i++) { + const StaticLabelPosition* label = &g_staticLabels[i]; + const Glass* glass = get_glass(label->glassIndex); + if (!glass) continue; + + // Check if this glass is visible + 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); + + // Check if glass point is on screen + if (glassX < padding || glassX > view->windowWidth - padding || + glassY < padding || glassY > view->windowHeight - padding) { + continue; + } + + // Calculate label position using static offset + const LabelOffset* pos = &LABEL_POSITIONS[label->positionIndex]; + i32 labelX = glassX + pos->dx * LABEL_OFFSET_DISTANCE; + i32 labelY = glassY + pos->dy * LABEL_OFFSET_DISTANCE; - // Draw glass name - draw_text(renderer, labelFont, (const char*)glass->name, x + 6, y - 10, blue); + // Draw the label + draw_text(renderer, labelFont, label->text, labelX, labelY, blue); } } // 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 >= get_glass_count()) { + if (view->selectedGlass < 0 || view->selectedGlass >= (i32)get_glass_count()) { return; // No glass selected } const Glass* glass = get_glass(view->selectedGlass); + if (!glass) return; // Safety check // Calculate glass position on screen i32 glassX, glassY; @@ -305,6 +723,65 @@ void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *tit SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE); } +// 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 + } + + const GlassCluster* cluster = &clusters[view->selectedCluster]; + + // Calculate cluster position on screen + i32 clusterX, clusterY; + data_to_screen_coords(cluster->avgAbbeNumber, cluster->avgRefractiveIndex, view, &clusterX, &clusterY); + + // Define property window size (no title, just properties) + const i32 windowWidth = 280; + const i32 windowHeight = 20 + cluster->count * 25; // Dynamic height based on glass count (no title) + const i32 padding = 10; + + // Position the window near the cluster point but ensure it stays within screen bounds + i32 windowX = clusterX + 15; + i32 windowY = clusterY - windowHeight / 2; + + // Adjust if window would go off-screen + if (windowX + windowWidth > view->windowWidth) { + windowX = clusterX - windowWidth - 15; + } + if (windowY < 0) { + windowY = 0; + } + if (windowY + windowHeight > view->windowHeight) { + windowY = view->windowHeight - windowHeight; + } + + // 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 border + SDL_SetRenderDrawColor(renderer, 40, 40, 40, 255); + SDL_RenderRect(renderer, &windowRect); + + // Draw properties for each glass in cluster directly (no title) + 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); +} + // Function to draw the help window void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { // Background for help window @@ -354,7 +831,7 @@ void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFon } // Main render function -void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, const ViewState* view) { +void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, const ViewState* view, GlassCluster* clusters, i32 clusterCount) { const SDL_Color black = {0, 0, 0, 255}; // Clear screen (white background) @@ -369,10 +846,14 @@ void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Fon draw_axes(renderer, font, titleFont, view); // Draw glass points and labels - draw_glass_points(renderer, labelFont, view); + draw_glass_points(renderer, labelFont, view, clusters, clusterCount); - // Draw glass properties if a glass is selected - if (view->selectedGlass >= 0) { + // 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); } @@ -427,11 +908,46 @@ static TTF_Font* try_load_font_from_paths(const char** fontNames, i32 numFonts, return NULL; } -// Load all required fonts -b32 load_fonts(FontSet *fonts) { +// 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); + + // Ensure minimum readable sizes + if (*regular < 10) *regular = 10; + if (*title < 12) *title = 12; + if (*label < 8) *label = 8; // Smaller minimum for labels +} + +// 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", @@ -461,10 +977,13 @@ b32 load_fonts(FontSet *fonts) { }; const i32 numFonts = sizeof(fontNames) / sizeof(fontNames[0]); - i32 fontSizes[] = {14, 18, 12}; // regular, title, label + 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, fontSizes[0]); + 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()); @@ -477,14 +996,14 @@ b32 load_fonts(FontSet *fonts) { } // Load title font (try larger size first, fallback to regular font) - fonts->title = try_load_font_from_paths(fontNames, numFonts, fontSizes[1]); + 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, fontSizes[2]); + 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 @@ -493,6 +1012,12 @@ b32 load_fonts(FontSet *fonts) { return 1; } +// 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 diff --git a/src/glamac/glamac_view.c b/src/glamac/glamac_view.c index 22d6401..67763a8 100644 --- a/src/glamac/glamac_view.c +++ b/src/glamac/glamac_view.c @@ -2,6 +2,7 @@ * glamac_view.c - Source file dealing with view states, filtering, zooming etc. of GlaMaC. */ #include +#include #include #include "glamac_view.h" #include "glass_data.h" @@ -17,6 +18,7 @@ void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { view->gKeyPressed = 0; view->gKeyTime = 0; view->selectedGlass = -1; // No glass selected initially + view->selectedCluster = -1; // No cluster selected initially // Calculate data range find_glass_data_range(&view->minAbbe, &view->maxAbbe, &view->minRI, &view->maxRI); @@ -27,7 +29,7 @@ void get_visible_data_range(const ViewState* view, f32 *visibleMinAbbe, f32 *vis f32 *visibleMinRI, f32 *visibleMaxRI) { // Convert screen corners to data values f32 topLeftAbbe, topLeftRI, bottomRightAbbe, bottomRightRI; - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + const i32 padding = get_adaptive_padding(view); screen_to_data_coords(padding, padding, view, &topLeftAbbe, &topLeftRI); screen_to_data_coords(view->windowWidth - padding, view->windowHeight - padding, @@ -45,8 +47,9 @@ i32 find_nearest_glass(i32 x, i32 y, const ViewState* view, f32 maxDistance) { i32 nearest = -1; f32 minDist = maxDistance; - for (i32 i = 0; i < get_glass_count(); i++) { + for (i32 i = 0; i < (i32)get_glass_count(); i++) { const Glass* glass = get_glass(i); + if (!glass) continue; // Safety check i32 glassX, glassY; data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); @@ -88,7 +91,7 @@ void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view data_to_screen_coords(mouseDataX, mouseDataY, view, &newMouseScreenX, &newMouseScreenY); // Adjust offset to keep mouse position fixed - const i32 padding = (i32)(view->windowWidth * PADDING_PERCENT); + 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); } @@ -109,4 +112,109 @@ void reset_view(ViewState* view) { view->zoomLevel = 1.0f; view->offsetX = 0.0f; view->offsetY = 0.0f; + view->selectedGlass = -1; + 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(); + + // Allocate temporary structures + GlassCluster* clusters = (GlassCluster*)calloc(maxGlasses, sizeof(GlassCluster)); + b32* processed = (b32*)calloc(maxGlasses, sizeof(b32)); + i32 numClusters = 0; + + if (!clusters || !processed) { + if (clusters) free(clusters); + if (processed) free(processed); + *clusterCount = 0; + return NULL; + } + + for (i32 i = 0; i < (i32)maxGlasses; 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; + 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++) { + 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; + + if (abbeDiff < SIMILARITY_THRESHOLD && riDiff < SIMILARITY_THRESHOLD) { + // Add to cluster and update averages + cluster->glassIndices[cluster->count] = j; + cluster->count++; + + // Update running average + 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; + } + } + + numClusters++; + } + + free(processed); + + // Resize to actual number of clusters + GlassCluster* finalClusters = (GlassCluster*)realloc(clusters, numClusters * sizeof(GlassCluster)); + if (!finalClusters) { + free(clusters); + *clusterCount = 0; + return NULL; + } + + *clusterCount = numClusters; + return finalClusters; +} + +// Free cluster memory +void free_glass_clusters(GlassCluster* clusters) { + if (clusters) { + free(clusters); + } +} + +// 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; + + for (i32 i = 0; i < clusterCount; i++) { + i32 clusterX, clusterY; + data_to_screen_coords(clusters[i].avgAbbeNumber, clusters[i].avgRefractiveIndex, view, &clusterX, &clusterY); + + // Calculate distance + const f32 dx = x - clusterX; + const f32 dy = y - clusterY; + const f32 dist = sqrtf(dx*dx + dy*dy); + + if (dist < minDist) { + minDist = dist; + nearest = i; + } + } + + return nearest; } diff --git a/src/glamac/glass_data.c b/src/glamac/glass_data.c index 40f7ea3..28bd963 100644 --- a/src/glamac/glass_data.c +++ b/src/glamac/glass_data.c @@ -32,17 +32,17 @@ static GlassDataContext g_glass_context = {0}; // Fallback sample glass data (SCHOTT glasses for testing) static const Glass default_glasses[] = { - {"FK51A", 81.61f, 1.4866f}, - {"F2", 36.37f, 1.6200f}, - {"SF10", 28.41f, 1.7283f}, - {"SF11", 25.76f, 1.7847f}, - {"SK16", 60.32f, 1.6204f}, - {"LASF9", 32.17f, 1.8503f}, - {"N-SF6", 25.36f, 1.8050f}, - {"LaK9", 59.46f, 1.6910f}, - {"N-FK51A", 84.47f, 1.4875f}, - {"N-SF14", 27.38f, 1.7617f}, - {"N-BK7", 64.17f, 1.5168f} + {.name = "FK51A", .abbeNumber = 81.61f, .refractiveIndex = 1.4866f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "F2", .abbeNumber = 36.37f, .refractiveIndex = 1.6200f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "SF10", .abbeNumber = 28.41f, .refractiveIndex = 1.7283f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "SF11", .abbeNumber = 25.76f, .refractiveIndex = 1.7847f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "SK16", .abbeNumber = 60.32f, .refractiveIndex = 1.6204f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "LASF9", .abbeNumber = 32.17f, .refractiveIndex = 1.8503f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "N-SF6", .abbeNumber = 25.36f, .refractiveIndex = 1.8050f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "LaK9", .abbeNumber = 59.46f, .refractiveIndex = 1.6910f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "N-FK51A", .abbeNumber = 84.47f, .refractiveIndex = 1.4875f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "N-SF14", .abbeNumber = 27.38f, .refractiveIndex = 1.7617f, .glass_code = "", .manufacturer = "SCHOTT"}, + {.name = "N-BK7", .abbeNumber = 64.17f, .refractiveIndex = 1.5168f, .glass_code = "", .manufacturer = "SCHOTT"} }; #define DEFAULT_GLASS_COUNT (sizeof(default_glasses) / sizeof(default_glasses[0])) @@ -197,8 +197,8 @@ static void cleanup_glass_context(GlassDataContext* ctx) { static GlamacResult allocate_glasses(GlassDataContext* ctx, u32 count) { if (!ctx || count == 0) return GLAMAC_ERROR_INVALID_ARGUMENT; - // Prevent integer overflow - if (count > (SIZE_MAX / sizeof(Glass))) { + // Prevent integer overflow - only check if count is very large + if (count > UINT32_MAX / sizeof(Glass)) { return GLAMAC_ERROR_MEMORY; } @@ -347,27 +347,12 @@ static GlamacResult load_single_manufacturer(const char* json_content, size_t js return GLAMAC_ERROR_MANUFACTURER_NOT_FOUND; } - // Find the end of this manufacturer section (next manufacturer or end of object) - const char* section_end = json_content + json_len; - const char* next_mfg = mfg_section + strlen(manufacturer_key); + // For single manufacturer loading, use the entire remaining JSON + // This ensures we don't miss any glasses due to boundary detection issues + size_t section_len = json_len - (mfg_section - json_content); - // Look for the next manufacturer key or end of object - while (next_mfg < json_content + json_len) { - if (*next_mfg == '"' && next_mfg[1] != 'g') { // Not "glasses" - // Check if this looks like another manufacturer key - const char* colon = strchr(next_mfg, ':'); - if (colon && colon < next_mfg + 50) { // Reasonable manufacturer name length - section_end = next_mfg; - break; - } - } - next_mfg++; - } - - size_t section_len = section_end - mfg_section; - - // Initialize with estimated capacity - GlamacResult result = allocate_glasses(&g_glass_context, 1000); + // Initialize with full capacity to avoid realloc during parsing + GlamacResult result = allocate_glasses(&g_glass_context, 5000); if (result != GLAMAC_SUCCESS) { return result; } @@ -570,9 +555,9 @@ void find_glass_data_range(f32 *minAbbe, f32 *maxAbbe, f32 *minRI, f32 *maxRI) { if (g_glass_context.glasses[i].refractiveIndex > *maxRI) *maxRI = g_glass_context.glasses[i].refractiveIndex; } - // Add a small margin to the range - const f32 abbeMargin = (*maxAbbe - *minAbbe) * 0.1f; - const f32 riMargin = (*maxRI - *minRI) * 0.1f; + // Add exactly 5% margin to the range as requested + const f32 abbeMargin = (*maxAbbe - *minAbbe) * 0.05f; + const f32 riMargin = (*maxRI - *minRI) * 0.05f; *minAbbe -= abbeMargin; *maxAbbe += abbeMargin; *minRI -= riMargin; @@ -585,8 +570,8 @@ b32 load_glasses_from_json(const byte* json_path, const byte* manufacturer_filte } void initialize_glass_data(void) { - // Try to load all manufacturers from JSON by default - GlamacResult result = try_load_json_with_fallbacks_secure(NULL); + // Load only SCHOTT catalog for now + GlamacResult result = try_load_json_with_fallbacks_secure("SCHOTT"); if (result != GLAMAC_SUCCESS) { // Fallback to default static data diff --git a/src/glautils/fgla.c b/src/glautils/fgla.c index 68232fd..f2ed8eb 100644 --- a/src/glautils/fgla.c +++ b/src/glautils/fgla.c @@ -15,7 +15,6 @@ #include #include #include -#include #include "glass_data.h" #include "glamacdef.h" @@ -286,7 +285,7 @@ void print_header(OutputFormat format, u32 found_count, const char* search_term, } } -void print_glass_result(OutputFormat format, const Glass* glass, const char* glass_name, int is_first, int is_last) { +void print_glass_result(OutputFormat format, const Glass* glass, const char* glass_name, int is_first) { switch (format) { case OUTPUT_CSV: printf("%s,%.5f,%.2f,%s,%s\n", @@ -505,7 +504,7 @@ int main(int argc, char* argv[]) { const char* glass_name = (const char*)get_glass_name(i); if (glass && glass_name) { - print_glass_result(output_format, glass, glass_name, j == 0, j == found_count - 1); + print_glass_result(output_format, glass, glass_name, j == 0); } } @@ -518,4 +517,4 @@ int main(int argc, char* argv[]) { cleanup_glass_data(); return FGLA_SUCCESS; -} \ No newline at end of file +} diff --git a/src/glautils/gla.c b/src/glautils/gla.c deleted file mode 100644 index 7922c4f..0000000 --- a/src/glautils/gla.c +++ /dev/null @@ -1,139 +0,0 @@ -/** - * gla.c - main source program file for displaying the glass map. - * - * Copyright (C) 2025 https://optics-design.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * See the COPYING file for the full license text. - */ -#include -#include -#include - -int main(int argc, char* argv[]){ - int filecount = 0; - char rfilename[FILENAME_MAX]; - char glassname[50]; - - FILE *readfile; - - if (argc==1){ - printf("This program is designed to work with glass data from the manufacturers\n"); - return 1; - } - //Argument parsing - while (--argc>0) { - if ((strstr((++argv)[0], ".xml"))){ - filecount++; - strcpy(rfilename, argv[0]); - } - else { - strcpy(glassname, argv[0]); - for (int i =0; i<50; i++) { - if (!glassname[i]){ - break; - } - if (glassname[i] >= 'a' && glassname[i] <= 'z'){ - glassname[i] = glassname[i] - ('a'- 'A'); - } - - } - // while(*glassname){ - // if (*glassname >= 'a' && *glassname <= 'z'){ - // *glassname = *glassname - ('a'- 'A'); - // } - // glassname++; - // } - // glassname = glassname -1; - - } - - } - // printf("%s\n", rfilename); - readfile = fopen(rfilename, "rb"); - if (readfile == NULL) { - perror("Failed to open file"); - return 1; - } - - fseek(readfile, 0, SEEK_END); - - long file_size = ftell(readfile); - if (file_size == -1) { - perror("ftell failed"); - fclose(readfile); - return 1; - } - char* buffer = malloc(file_size + 1); - - fseek(readfile, 0, SEEK_SET); - - if (buffer == NULL) { - perror("Failed to allocate memory"); - fclose(readfile); - return 1; - } - // Read the file into the buffer - size_t read_size = fread(buffer, 1, file_size, readfile); - if (read_size != file_size) { - perror("Failed to read the complete file"); - free(buffer); - fclose(readfile); - return 1; - } - // Null terminate the buffer (in case it's treated as a string) - buffer[file_size] = '\0'; - fclose(readfile); - - // char *newline_ptr = NULL; - // for (int i = 0; i < file_size; i++) { - // if (buffer[i] == '\n') { - // newline_ptr = &buffer[i]; - // break; - // } - // } - // fwrite(buffer, 1, newline_ptr - buffer, stdout); - char* line_start = buffer; - char* line_end = NULL; - int line_length; - int glafound = 0; - - while ((line_end=strchr(line_start, '\n'))) { - *line_end = '\0'; - if (!glafound){ - if (strstr(line_start, glassname) && strstr(line_start, "")) { - line_length = strlen(line_start)-27; // 2*Length_of_tag+1 + 4 - printf("Name: %.*s, ", line_length, line_start+14); // Length_of_tag+3 - glafound = 1; - } - } - - else{ - if (strstr(line_start, "")) { - line_length = strlen(line_start)-31; // 2*Length_of_tag+1 + 4 - printf("Code: %.*s", line_length, line_start+16); // Length_of_tag+3 - } - // if (strstr(line_start, "")) { - // line_length = strlen(line_start)-33; // 2*Length_of_tag+1 + 4 - // printf("Equation: %.*s\n", line_length, line_start+17); // Length_of_tag+3 - // } - // if (strstr(line_start, "")) { - // line_length = strlen(line_start)-32; // 2*Length_of_tag+1 + 4 - // printf("Coefficient: %.*s\n", line_length, line_start+17); // Length_of_tag+3 - // } - if (strstr(line_start, "")) { - printf("\n"); - glafound = 0; - } - } - line_start = line_end+1; - - } - - free(buffer); - return 0; -} -- cgit v1.2.3