From a3389f25f786be7139feaca837c86442f7745745 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 6 Aug 2025 11:31:21 +0200 Subject: changed gui stuff --- Makefile.backup | 247 ---------------- include/glamac/graphics/glamac_render.h | 26 +- include/glamac/graphics/glamac_view.h | 66 ++++- src/glamac/glamac.c | 97 ++++++- src/glamac/glamac_events.c | 20 +- src/glamac/glamac_render.c | 501 ++++++++++++++++++++++++++------ src/glamac/glamac_view.c | 281 +++++++++++++++++- 7 files changed, 865 insertions(+), 373 deletions(-) delete mode 100644 Makefile.backup diff --git a/Makefile.backup b/Makefile.backup deleted file mode 100644 index e2f0c54..0000000 --- a/Makefile.backup +++ /dev/null @@ -1,247 +0,0 @@ -# Simplified Makefile for glamac - SDL3 version -# Supports: Linux native, Windows cross-compilation - -# Detect OS and set platform-specific variables -UNAME_S := $(shell uname -s 2>/dev/null || echo Windows_NT) -ifeq ($(UNAME_S),Linux) - PLATFORM := linux - EXE_EXT := - MKDIR := mkdir -p - RM := rm -f - RMDIR := rm -rf - CC := gcc - MINGW_CC := x86_64-w64-mingw32-gcc - SDL3_LIBS := $(shell pkg-config --libs sdl3 SDL3_ttf 2>/dev/null || echo -lSDL3 -lSDL3_ttf) -lm - CROSS_PREFIX := /usr/x86_64-w64-mingw32 -else - PLATFORM := windows - EXE_EXT := .exe - MKDIR := mkdir - RM := del /Q - RMDIR := rmdir /s /q - CC := gcc - SDL3_LIBS := -lSDL3 -lSDL3_ttf -mwindows -endif - -# Directories -SRCDIR := src -BINDIR := bin -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 := $(CFLAGS_BASE) $(SECURITY_FLAGS) $(WARNING_FLAGS) -CFLAGS_NATIVE := $(CFLAGS) -march=native -# 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) -GLAUTILS_SRCS := $(wildcard $(SRCDIR)/glautils/*.c) -GLAUTILS_BINS := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR)/%$(EXE_EXT), $(GLAUTILS_SRCS)) -GLAUTILS_BINS_WIN := $(patsubst $(SRCDIR)/glautils/%.c, $(BINDIR_WIN)/%.exe, $(GLAUTILS_SRCS)) - -# Glass data dependencies for fgla -GLASS_DATA_SRCS := $(SRCDIR)/glamac/glass_data.c $(SRCDIR)/glamac/glamac_errors.c - -# Default target -all: glamac glautils - -# Native build targets -glamac: $(BINDIR)/glamac$(EXE_EXT) - -glautils: $(GLAUTILS_BINS) - -$(BINDIR)/glamac$(EXE_EXT): $(GLAMAC_SRCS) | $(BINDIR) - @echo "Building glamac..." - $(CC) $^ $(CFLAGS_NATIVE) $(SDL3_LIBS) -o $@ - -# Special rule for fgla which needs glass_data dependencies -$(BINDIR)/fgla$(EXE_EXT): $(SRCDIR)/glautils/fgla.c $(GLASS_DATA_SRCS) | $(BINDIR) - $(CC) $^ $(CFLAGS_NATIVE) -o $@ - -# General rule for other glautils (excluding fgla) -$(BINDIR)/%$(EXE_EXT): $(SRCDIR)/glautils/%.c | $(BINDIR) - $(CC) $< $(CFLAGS_NATIVE) -o $@ - -$(BINDIR): - $(MKDIR) $(BINDIR) - -# Windows cross-compilation (Linux only) -ifeq ($(PLATFORM),linux) - -win: $(BINDIR_WIN)/glamac.exe win-dlls win-data - -win-all: win $(GLAUTILS_BINS_WIN) - -$(BINDIR_WIN)/glamac.exe: $(GLAMAC_SRCS) | $(BINDIR_WIN) - @echo "Cross-compiling glamac for Windows..." - @which $(MINGW_CC) >/dev/null 2>&1 || (echo "ERROR: Install mingw-w64-gcc first" && exit 1) - $(MINGW_CC) $^ $(CFLAGS_CROSS) -L$(CROSS_PREFIX)/lib -lmingw32 -lSDL3 -lSDL3_ttf -mwindows -static-libgcc -o $@ - -$(BINDIR_WIN)/%.exe: $(SRCDIR)/glautils/%.c | $(BINDIR_WIN) - $(MINGW_CC) $< $(CFLAGS_CROSS) -static-libgcc -o $@ - -$(BINDIR_WIN): - $(MKDIR) $(BINDIR_WIN) - -# Windows DLL management -win-dlls: | $(BINDIR_WIN) - @echo "Getting Windows DLLs..." - @$(MKDIR) $(DLL_CACHE) - @if [ ! -f "$(DLL_CACHE)/SDL3.dll" ]; then \ - echo "Downloading SDL3.dll..."; \ - cd $(DLL_CACHE) && \ - wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-3.2.10-win32-x64.zip && \ - unzip -j SDL3-3.2.10-win32-x64.zip SDL3.dll && \ - $(RM) SDL3-3.2.10-win32-x64.zip; \ - fi - @if [ ! -f "$(DLL_CACHE)/SDL3_ttf.dll" ]; then \ - echo "Downloading SDL3_ttf.dll..."; \ - cd $(DLL_CACHE) && \ - wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-3.2.2-win32-x64.zip && \ - unzip -j SDL3_ttf-3.2.2-win32-x64.zip SDL3_ttf.dll && \ - $(RM) SDL3_ttf-3.2.2-win32-x64.zip; \ - fi - @cp $(DLL_CACHE)/*.dll $(BINDIR_WIN)/ - @echo "Windows build ready in $(BINDIR_WIN)/" - -# Copy data files for Windows build -win-data: | $(BINDIR_WIN) - @echo "Copying data files for Windows..." - @$(MKDIR) $(BINDIR_WIN)/data/json - @if [ -f "data/json/glasses.json" ]; then \ - cp data/json/glasses.json $(BINDIR_WIN)/data/json/; \ - cp data/json/glasses.json $(BINDIR_WIN)/; \ - echo "Copied glasses.json to Windows build"; \ - 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: - @echo "Setting up cross-compilation..." - @which wget >/dev/null 2>&1 || (echo "Install wget first: sudo pacman -S wget" && exit 1) - @which $(MINGW_CC) >/dev/null 2>&1 || (echo "Install mingw-w64-gcc first: sudo pacman -S mingw-w64-gcc" && exit 1) - sudo $(MKDIR) $(CROSS_PREFIX)/include $(CROSS_PREFIX)/lib - @echo "Downloading SDL3 development libraries..." - cd /tmp && \ - wget -q https://github.com/libsdl-org/SDL/releases/download/release-3.2.10/SDL3-devel-3.2.10-mingw.tar.gz && \ - wget -q https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ - tar -xzf SDL3-devel-3.2.10-mingw.tar.gz && \ - tar -xzf SDL3_ttf-devel-3.2.2-mingw.tar.gz && \ - sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ - sudo cp -r SDL3-3.2.10/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ - sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/include/* $(CROSS_PREFIX)/include/ && \ - sudo cp -r SDL3_ttf-3.2.2/x86_64-w64-mingw32/lib/* $(CROSS_PREFIX)/lib/ && \ - $(RM) -rf SDL3-3.2.10* SDL3_ttf-3.2.2* - @echo "Cross-compilation setup complete!" - -else -# Windows host - disable cross-compilation -win win-all win-dlls setup-cross: - @echo "Cross-compilation not available on Windows. Use 'make all' instead." -endif - -# Excel to JSON conversion -EXCEL_FILES := $(wildcard data/Excel/*.xlsx) -JSON_FILES := $(patsubst data/Excel/%.xlsx, data/JSON/%.json, $(EXCEL_FILES)) - -convert-catalogs: $(JSON_FILES) - -data/JSON/%.json: data/Excel/%.xlsx scripts/excel_to_json.py - @echo "Converting $< to JSON..." - @$(MKDIR) data/JSON - @python3 scripts/excel_to_json.py $< -o $@ - -# Dependency management -deps: -ifeq ($(PLATFORM),linux) - @echo "Installing dependencies..." - sudo pacman -S --needed sdl3 git cmake pkgconf freetype2 python python-pandas python-openpyxl - @echo "Building SDL3_ttf from source..." - cd /tmp && $(RM) -rf SDL_ttf && \ - git clone https://github.com/libsdl-org/SDL_ttf.git && \ - cd SDL_ttf && git checkout release-3.2.2 && \ - cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && \ - make -C build -j$$(nproc) && sudo make -C build install && \ - sudo ldconfig - @echo "Dependencies installed!" -else - @echo "Install SDL3 development libraries manually on Windows." - @echo "Install Python with pandas and openpyxl for Excel conversion." -endif - -# Cleanup -clean: - $(RMDIR) $(BINDIR) 2>/dev/null || true - -clean-deps: -ifeq ($(PLATFORM),linux) - @echo "Removing SDL3 dependencies..." - sudo pacman -R sdl3 --noconfirm 2>/dev/null || true - sudo $(RM) -f /usr/lib/libSDL3_ttf.so* /usr/lib/libSDL3_ttf.a /usr/lib/pkgconfig/SDL3_ttf.pc - sudo $(RMDIR) /usr/include/SDL3_ttf/ 2>/dev/null || true - sudo ldconfig - @echo "Dependencies removed!" -endif - -clean-cache: - $(RMDIR) $(DLL_CACHE) 2>/dev/null || true - -clean-all: clean clean-cache - -rebuild: clean all - -# Help -help: - @echo "GlaMaC Build System" - @echo "" - @echo "Build targets:" - @echo " all - Build for current platform" - @echo " glamac - Build main application only" - @echo " glautils - Build utilities only" - @echo " clean - Clean build files" - @echo " rebuild - Clean and rebuild" -ifeq ($(PLATFORM),linux) - @echo "" - @echo "Cross-compilation (Linux → Windows):" - @echo " win - Build glamac for Windows" - @echo " win-all - Build everything for Windows" - @echo " setup-cross - Setup cross-compilation" -endif - @echo "" - @echo "Dependencies:" - @echo " deps - Install SDL3 dependencies" - @echo " clean-deps - Remove SDL3 dependencies" - @echo "" - @echo "Glass catalog conversion:" - @echo " convert-catalogs - Convert Excel files to JSON" - @echo "" - @echo "Quick start:" -ifeq ($(PLATFORM),linux) - @echo " make deps && make all # Native build" - @echo " make setup-cross && make win # Windows build" -else - @echo " Install SDL3, then: make all" -endif - -.PHONY: all glamac glautils win win-all win-dlls setup-cross convert-catalogs deps clean clean-deps clean-cache clean-all rebuild help \ No newline at end of file diff --git a/include/glamac/graphics/glamac_render.h b/include/glamac/graphics/glamac_render.h index 4c20751..29e7613 100644 --- a/include/glamac/graphics/glamac_render.h +++ b/include/glamac/graphics/glamac_render.h @@ -9,26 +9,28 @@ #include "../core/glamacdef.h" #include "glamac_view.h" +// Font management +typedef struct { + TTF_Font *regular; + TTF_Font *title; + TTF_Font *label; + TTF_Font *axis; // Smaller font for axis labels + TTF_Font *label_bold; // Bold font for cluster representatives +} FontSet; + // Drawing primitives void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, i32 y, SDL_Color color); void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 radius); // UI element rendering -void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view); +void draw_axes(SDL_Renderer *renderer, const FontSet *fonts, 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_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); +void draw_glass_points(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view); +void draw_glass_properties(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view); +void draw_help_window(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view); // Main render function -void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Font *labelFont, ViewState* view); - -// Font management -typedef struct { - TTF_Font *regular; - TTF_Font *title; - TTF_Font *label; -} FontSet; +void render(SDL_Renderer *renderer, const FontSet *fonts, ViewState* view); // Load all required fonts b32 load_fonts(FontSet *fonts); diff --git a/include/glamac/graphics/glamac_view.h b/include/glamac/graphics/glamac_view.h index 58de2b1..a15aaf1 100644 --- a/include/glamac/graphics/glamac_view.h +++ b/include/glamac/graphics/glamac_view.h @@ -8,8 +8,8 @@ #include "../core/glamacdef.h" // Constants for view -#define MIN_PADDING 20 // Minimum padding in pixels -#define MAX_PADDING_PERCENT 0.04f // Maximum padding as percentage of window size +#define MIN_PADDING 15 // Minimum padding in pixels (reduced for small windows) +#define MAX_PADDING_PERCENT 0.06f // Maximum padding as percentage of window size (increased for responsiveness) #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 @@ -20,7 +20,28 @@ #define LABEL_OFFSET_Y -8 // Vertical offset from glass point (pixels) #define LABEL_SIZE_SCALE 1.4f // Scale factor for glass name labels +// Performance optimization constants +#define LABEL_VISIBILITY_THRESHOLD 2.0f // Minimum zoom level to show labels +#define VIEWPORT_CULL_MARGIN 50 // Extra margin for viewport culling (pixels) +// Tight clustering constants (easily adjustable thresholds) +#define TIGHT_CLUSTER_ND_THRESHOLD 0.0001f // Maximum nd difference for tight clustering +#define TIGHT_CLUSTER_VD_THRESHOLD 0.1f // Maximum vd difference for tight clustering +#define MAX_CLUSTER_SIZE 8 // Maximum glasses per tight cluster + + + +// Forward declaration for tight cluster +typedef struct TightCluster TightCluster; + +// Tight cluster structure for glasses with similar optical properties +struct TightCluster { + u32 glassIndices[MAX_CLUSTER_SIZE]; // Indices of glasses in this cluster + u32 count; // Number of glasses in cluster + u32 representativeIndex; // Index of glass with shortest name (for display) + f32 centerNd; // Average nd of cluster + f32 centerVd; // Average vd of cluster +}; // State for zooming and panning typedef struct { @@ -37,6 +58,13 @@ 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) + b32 viewDirty; // Flag to track if view needs re-rendering + + // Tight clustering data + TightCluster* tightClusters; // Array of tight clusters + u32 tightClusterCount; // Number of tight clusters + i32* glassToCluster; // Map from glass index to cluster index (-1 if not clustered) + u32 clusteringGlassCount; // Glass count when clustering was created (for bounds checking) } ViewState; // Initialize a view state with default values @@ -95,15 +123,45 @@ i32 find_nearest_glass(i32 x, i32 y, const ViewState* view, f32 maxDistance); void get_visible_data_range(const ViewState* view, f32 *visibleMinAbbe, f32 *visibleMaxAbbe, f32 *visibleMinRI, f32 *visibleMaxRI); +// Check if a glass point is visible in the current viewport (optimized for data-space culling) +static inline b32 is_glass_visible_fast(f32 abbeNumber, f32 refractiveIndex, + f32 visibleMinAbbe, f32 visibleMaxAbbe, + f32 visibleMinRI, f32 visibleMaxRI) { + // Simple bounds check in data space (no coordinate transformation needed) + return (abbeNumber >= visibleMinAbbe && abbeNumber <= visibleMaxAbbe && + refractiveIndex >= visibleMinRI && refractiveIndex <= visibleMaxRI); +} + +// Legacy function for compatibility - use is_glass_visible_fast for better performance +static inline b32 is_glass_visible(f32 abbeNumber, f32 refractiveIndex, const ViewState* view) { + i32 x, y; + data_to_screen_coords(abbeNumber, refractiveIndex, view, &x, &y); + + const i32 margin = VIEWPORT_CULL_MARGIN; + return (x >= -margin && x < view->windowWidth + margin && + y >= -margin && y < view->windowHeight + margin); +} + +// Mark view as dirty (needs re-rendering) +static inline void mark_view_dirty(ViewState* view) { + view->viewDirty = 1; +} + // Handle mouse wheel zoom void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view); -// Toggle fullscreen -void toggle_fullscreen(SDL_Window* window); +// Toggle fullscreen with proper state synchronization +void toggle_fullscreen(SDL_Window* window, ViewState* view); // Reset view to default void reset_view(ViewState* view); +// Tight clustering functions +void create_tight_clusters(ViewState* view); +void free_tight_clusters(ViewState* view); +i32 find_tight_cluster_for_glass(i32 glassIndex, const ViewState* view); +b32 should_show_glass_label_tight(i32 glassIndex, const ViewState* view); + #endif /* GLAMAC_VIEW_H */ diff --git a/src/glamac/glamac.c b/src/glamac/glamac.c index dd06626..a26c7e6 100644 --- a/src/glamac/glamac.c +++ b/src/glamac/glamac.c @@ -35,10 +35,10 @@ b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { return 0; } - // Hysteresis parameters - const f32 threshold = 0.25f; // 25% change threshold - const u32 minReloadInterval = 500; // Minimum 500ms between font reloads - const u32 maxPendingTime = 2000; // Maximum time to wait for pending reload + // Hysteresis parameters - reduced for better responsiveness + const f32 threshold = 0.15f; // 15% change threshold (more sensitive for fullscreen) + const u32 minReloadInterval = 50; // Minimum 50ms between font reloads (faster response) + const u32 maxPendingTime = 300; // Maximum time to wait for pending reload u32 currentTime = SDL_GetTicks(); @@ -48,15 +48,24 @@ b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { b32 significantChange = (widthDiff > (i32)(view->windowWidth * threshold) || heightDiff > (i32)(view->windowHeight * threshold)); - if (significantChange) { + // Detect major changes (like fullscreen transitions) - force immediate reload + b32 majorChange = (widthDiff > 200 || heightDiff > 200 || + (lastWidth > 0 && lastHeight > 0 && + (abs(view->windowWidth - lastWidth) > (view->windowWidth / 2) || + abs(view->windowHeight - lastHeight) > (view->windowHeight / 2)))); + + if (significantChange || majorChange) { // Update pending dimensions pendingWidth = view->windowWidth; pendingHeight = view->windowHeight; - // Check if enough time has passed since last reload - if (currentTime - lastReloadTime >= minReloadInterval) { - printf("Window size changed significantly: %dx%d -> %dx%d, reloading fonts...\n", - lastWidth, lastHeight, view->windowWidth, view->windowHeight); + // For major changes, force immediate reload regardless of timing + // For normal changes, check if enough time has passed since last reload + if (majorChange || currentTime - lastReloadTime >= minReloadInterval) { + if (is_debug_mode()) { + printf("Window size changed significantly: %dx%d -> %dx%d, reloading fonts...\n", + lastWidth, lastHeight, view->windowWidth, view->windowHeight); + } // Clear text cache first clear_text_cache(); @@ -101,17 +110,24 @@ b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { lastReloadTime = currentTime; pendingWidth = pendingHeight = 0; // Clear pending state - printf("Fonts successfully reloaded for window size: %dx%d\n", - view->windowWidth, view->windowHeight); + // Mark view as dirty to re-render with new fonts + mark_view_dirty(view); + + if (is_debug_mode()) { + printf("Fonts successfully reloaded for window size: %dx%d\n", + view->windowWidth, view->windowHeight); + } } else { // Can't reload yet due to rate limiting, check if we should force it if (pendingWidth != 0 && pendingHeight != 0 && currentTime - lastReloadTime >= maxPendingTime) { - printf("Forcing delayed font reload after %ums\n", maxPendingTime); + if (is_debug_mode()) { + printf("Forcing delayed font reload after %ums\n", maxPendingTime); + } // Recursive call will now pass the time check return reload_fonts_if_needed(fonts, view, dpi); } - printf("Font reload rate limited, pending: %dx%d\n", pendingWidth, pendingHeight); + // Silently rate limit font reloads } } else if (pendingWidth != 0 && pendingHeight != 0 && currentTime - lastReloadTime >= minReloadInterval) { @@ -122,6 +138,58 @@ b32 reload_fonts_if_needed(FontSet *fonts, ViewState *view, f32 dpi) { return reload_fonts_if_needed(fonts, view, dpi); } + // If no reload needed and no pending changes, clear stale state + if (widthDiff == 0 && heightDiff == 0 && pendingWidth == 0 && pendingHeight == 0) { + // Window size is stable, no action needed + } + + return 1; +} + +// Force font reload (for fullscreen transitions and other critical updates) +b32 force_font_reload(FontSet *fonts, ViewState *view, f32 dpi) { + if (!fonts || !view) { + printf("Error: Invalid parameters for forced font reload\n"); + return 0; + } + + if (is_debug_mode()) { + printf("Force reloading fonts for window size: %dx%d\n", + view->windowWidth, view->windowHeight); + } + + // Clear text cache first + clear_text_cache(); + + // Store current fonts as backup + FontSet backup_fonts = *fonts; + + // Try to reload with new size + if (!load_adaptive_fonts(fonts, view->windowWidth, view->windowHeight, dpi)) { + printf("Error: Failed to force reload fonts, restoring backup\n"); + + // Restore backup fonts + *fonts = backup_fonts; + return 0; + } + + // Free backup fonts if new loading succeeded + if (backup_fonts.regular != fonts->regular && backup_fonts.regular) { + TTF_CloseFont(backup_fonts.regular); + } + if (backup_fonts.title != fonts->title && backup_fonts.title) { + TTF_CloseFont(backup_fonts.title); + } + if (backup_fonts.label != fonts->label && backup_fonts.label) { + TTF_CloseFont(backup_fonts.label); + } + if (backup_fonts.axis != fonts->axis && backup_fonts.axis) { + TTF_CloseFont(backup_fonts.axis); + } + if (backup_fonts.label_bold != fonts->label_bold && backup_fonts.label_bold) { + TTF_CloseFont(backup_fonts.label_bold); + } + return 1; } @@ -253,13 +321,14 @@ int main(int argc, char* argv[]) { } // Render everything - render(renderer, fonts.regular, fonts.title, fonts.label, &view); + render(renderer, &fonts, &view); } // Stop text input SDL_StopTextInput(window); // Clean up resources + free_tight_clusters(&view); cleanup_glass_data(); free_fonts(&fonts); clear_text_cache(); diff --git a/src/glamac/glamac_events.c b/src/glamac/glamac_events.c index 5966efa..8adefa5 100644 --- a/src/glamac/glamac_events.c +++ b/src/glamac/glamac_events.c @@ -28,6 +28,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo // If Shift is held (for '?' on most layouts) or it's a direct '?' key if ((key->mod & SDL_KMOD_SHIFT) || key->key == SDLK_QUESTION) { view->showHelp = !view->showHelp; + mark_view_dirty(view); } view->gKeyPressed = 0; return 1; @@ -38,8 +39,10 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo // ESC closes glass selection first, then help window, finally quits if (view->selectedGlass >= 0) { view->selectedGlass = -1; // Clear selection + mark_view_dirty(view); } else if (view->showHelp) { view->showHelp = 0; + mark_view_dirty(view); } else { *quit = 1; } @@ -49,6 +52,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo case SDLK_Q: if (view->showHelp) { view->showHelp = 0; + mark_view_dirty(view); } else { *quit = 1; } @@ -61,6 +65,7 @@ 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; + mark_view_dirty(view); return 1; case SDLK_MINUS: @@ -68,6 +73,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo view->zoomLevel /= ZOOM_FACTOR; if (view->zoomLevel < MIN_ZOOM) view->zoomLevel = MIN_ZOOM; view->gKeyPressed = 0; // Reset g key state + mark_view_dirty(view); return 1; case SDLK_R: @@ -78,8 +84,8 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo case SDLK_F: // Toggle fullscreen - toggle_fullscreen(window); - view->gKeyPressed = 0; // Reset g key state + toggle_fullscreen(window, view); + view->gKeyPressed = 0; // Reset g key state return 1; // Vim-like navigation @@ -95,6 +101,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo } else { // Move left view->offsetX += PAN_STEP; + mark_view_dirty(view); } view->gKeyPressed = 0; // Reset g key state return 1; @@ -110,6 +117,7 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo } else { // Move right view->offsetX -= PAN_STEP; + mark_view_dirty(view); } view->gKeyPressed = 0; // Reset g key state return 1; @@ -118,12 +126,14 @@ b32 process_key_event(SDL_KeyboardEvent *key, ViewState *view, SDL_Window *windo // Move up view->offsetY -= PAN_STEP; view->gKeyPressed = 0; // Reset g key state + mark_view_dirty(view); return 1; case SDLK_J: // Move down view->offsetY += PAN_STEP; view->gKeyPressed = 0; // Reset g key state + mark_view_dirty(view); return 1; } @@ -145,9 +155,11 @@ b32 process_mouse_button(SDL_MouseButtonEvent *button, ViewState *view, i32 *las if (nearestGlass >= 0) { // Found a glass within click tolerance - select it view->selectedGlass = nearestGlass; + mark_view_dirty(view); } else { // No glass clicked - clear selection view->selectedGlass = -1; + mark_view_dirty(view); } if (nearestGlass < 0) { @@ -214,6 +226,9 @@ b32 process_mouse_motion(SDL_MouseMotionEvent *motion __attribute__((unused)), V view->offsetX += dx; view->offsetY -= dy; + // Mark view as dirty for re-rendering + mark_view_dirty(view); + // Update last mouse position *lastMouseX = (i32)mouseX; *lastMouseY = (i32)mouseY; @@ -319,6 +334,7 @@ b32 process_events(SDL_Event *event, ViewState *view, SDL_Window *window, view->windowWidth = newWidth; view->windowHeight = newHeight; + mark_view_dirty(view); return 1; } } diff --git a/src/glamac/glamac_render.c b/src/glamac/glamac_render.c index 1fdaf30..9743e7f 100644 --- a/src/glamac/glamac_render.c +++ b/src/glamac/glamac_render.c @@ -16,7 +16,7 @@ #define MIN_FONT_SIZE 8 #define MAX_FONT_SIZE 72 #define DEFAULT_FONT_SIZE 16 -#define AXIS_FONT_SIZE 12 +#define AXIS_FONT_SIZE 8 #define TITLE_FONT_SIZE 20 #define LABEL_FONT_SIZE 11 @@ -105,12 +105,18 @@ static SDL_Texture* get_cached_text(SDL_Renderer* renderer, TTF_Font* font, cons return NULL; } - // Validate text length - size_t text_len = strlen(text); + // Security: Validate text length and ensure null termination + size_t text_len = strnlen(text, MAX_TEXT_LENGTH); if (text_len == 0 || text_len >= MAX_TEXT_LENGTH) { return NULL; } + // Security: Ensure text is properly null-terminated + if (text[text_len] != '\0') { + printf("Warning: Cached text not properly null-terminated\n"); + return NULL; + } + cache_access_counter++; // Search for existing cache entry @@ -154,9 +160,11 @@ static SDL_Texture* get_cached_text(SDL_Renderer* renderer, TTF_Font* font, cons evict_cache_entry(cache_index); } - // Store in cache - strncpy(textCache[cache_index].text, text, sizeof(textCache[cache_index].text) - 1); - textCache[cache_index].text[sizeof(textCache[cache_index].text) - 1] = '\0'; + // Store in cache with safe string copying + size_t max_copy = sizeof(textCache[cache_index].text) - 1; + size_t copy_len = (text_len < max_copy) ? text_len : max_copy; + memcpy(textCache[cache_index].text, text, copy_len); + textCache[cache_index].text[copy_len] = '\0'; textCache[cache_index].texture = texture; textCache[cache_index].width = surface->w; textCache[cache_index].height = surface->h; @@ -199,7 +207,24 @@ void draw_text(SDL_Renderer *renderer, TTF_Font *font, const char *text, i32 x, // 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); + // Security: Validate input parameters + if (!renderer || !font || !text) { + return; + } + + // Security: Validate text length and content + size_t text_len = strnlen(text, 1024); // Max text length check + if (text_len == 0 || text_len >= 1024) { + return; // Reject empty or overly long text + } + + // Security: Ensure text is properly null-terminated + if (text[text_len] != '\0') { + printf("Warning: Text not properly null-terminated, skipping render\n"); + return; + } + + SDL_Surface* surface = TTF_RenderText_Blended(font, text, (int)text_len, color); if (surface) { SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); if (texture) { @@ -211,13 +236,22 @@ void draw_text_direct(SDL_Renderer *renderer, TTF_Font *font, const char *text, } } -// Draw filled circle +// Draw filled circle using optimized scan-line algorithm void draw_filled_circle(SDL_Renderer *renderer, i32 centerX, i32 centerY, i32 radius) { + if (radius <= 0) return; + + // Use scan-line algorithm for filled circle - much faster than pixel-by-pixel + i32 radiusSquared = radius * radius; + for (i32 y = -radius; y <= radius; y++) { - for (i32 x = -radius; x <= radius; x++) { - if (x*x + y*y <= radius*radius) { - SDL_RenderPoint(renderer, centerX + x, centerY + y); - } + i32 ySquared = y * y; + i32 halfWidth = (i32)(sqrtf((f32)(radiusSquared - ySquared)) + 0.5f); + + if (halfWidth > 0) { + // Draw horizontal line from -halfWidth to +halfWidth + SDL_RenderLine(renderer, + centerX - halfWidth, centerY + y, + centerX + halfWidth, centerY + y); } } } @@ -261,31 +295,59 @@ static TTF_Font* load_font_safe(const char* const* font_paths, int size) { continue; } - // Check for directory traversal attempts - if (strstr(path, "..") || strstr(path, "//")) { - printf("Warning: Skipping potentially unsafe font path: %s\n", path); + // Enhanced security: Check for directory traversal and other unsafe patterns + if (strstr(path, "..") || strstr(path, "//") || + strstr(path, "%2E%2E") || // URL-encoded .. + strstr(path, "%2F%2F") || // URL-encoded // + strstr(path, "\\\\") || // Windows double backslash + strstr(path, "\\.\\")) { // Windows .\ pattern + if (is_debug_mode()) { + printf("Warning: Skipping potentially unsafe font path: %s\n", path); + } + continue; + } + + // Additional validation: ensure path is within safe directories + b32 safe_path = 0; + if (strncmp(path, "/usr/share/fonts/", 17) == 0 || + strncmp(path, "/System/Library/Fonts/", 22) == 0 || + strncmp(path, "C:\\Windows\\Fonts\\", 17) == 0 || + strncmp(path, "fonts/", 6) == 0 || + (strlen(path) < 50 && strchr(path, '/') == NULL)) { // Simple filename only + safe_path = 1; + } + + if (!safe_path) { + if (is_debug_mode()) { + printf("Warning: Font path not in safe directory list: %s\n", path); + } continue; } TTF_Font* font = TTF_OpenFont(path, size); if (font) { - printf("Successfully loaded font: %s (size %d)\n", path, size); + if (is_debug_mode()) { + printf("Successfully loaded font: %s (size %d)\n", path, size); + } return font; } } - printf("Warning: Failed to load any font from provided paths\n"); + if (is_debug_mode()) { + printf("Warning: Failed to load any font from provided paths\n"); + } return NULL; } // Draw axes -void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { - // Create smaller font for axis scales with safe loading - TTF_Font *axisFont = load_font_safe(SAFE_FONT_PATHS, 12); - if (!axisFont) { - axisFont = font; // Fallback to regular font if loading fails - printf("Warning: Using fallback font for axes\n"); +void draw_axes(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view) { + if (!fonts || !fonts->axis || !fonts->title) { + printf("Error: Invalid fonts for draw_axes\n"); + return; } + + // Use preloaded axis font (no runtime font loading) + TTF_Font *axisFont = fonts->axis; const i32 padding = get_adaptive_padding(view); const i32 plotWidth = view->windowWidth - 2 * padding; const i32 plotHeight = view->windowHeight - 2 * padding; @@ -349,46 +411,83 @@ void draw_axes(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, cons } else { snprintf(buffer, sizeof(buffer), "%.3f", riValue); } - draw_text_direct(renderer, axisFont, buffer, padding - 45, y - 8, black); + // Move the bottom-most number up slightly to avoid overlap with X-axis labels + i32 yOffset = (i == 0) ? -15 : -8; + // Adaptive positioning based on window size + i32 axisLabelOffset = (view->windowWidth < 600) ? padding - 35 : padding - 45; + draw_text_direct(renderer, axisFont, buffer, axisLabelOffset, y + yOffset, black); } - // Axis titles with subscripts (approximated) - draw_text(renderer, titleFont, "Abbe Number (Vd)", view->windowWidth/2 - 80, view->windowHeight - 25, black); + // Axis titles with subscripts - using regular font (2pt smaller than title) + // Adaptive positioning based on window size + i32 abbeNumberY = (view->windowHeight < 500) ? view->windowHeight - 25 : view->windowHeight - 35; + draw_text(renderer, fonts->regular, "Abbe Number (Vd)", view->windowWidth/2 - 80, abbeNumberY, black); // Vertical text for Y-axis (simplified) - draw_text(renderer, titleFont, "Refractive Index (nd)", 5, 10, black); + draw_text(renderer, fonts->regular, "Refractive Index (nd)", 5, 10, black); // 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); + draw_text(renderer, fonts->title, catalogTitle, (view->windowWidth - titleWidth) / 2, 10, black); - // Clean up axis font if we created it - if (axisFont != font) { - TTF_CloseFont(axisFont); - } + // No font cleanup needed - fonts are managed externally } -// Draw glass points and labels - simplified version showing all labels -void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewState* view) { +// Draw glass points and labels with viewport culling and zoom-based visibility +void draw_glass_points(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view) { + if (!fonts || !fonts->label || !fonts->label_bold) { + printf("Error: Invalid fonts for draw_glass_points\n"); + return; + } + + // Use preloaded fonts (no runtime font loading) + TTF_Font *regularFont = fonts->label; + TTF_Font *boldFontSameSize = fonts->label_bold; 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 + SDL_Color labelColor = {0, 0, 0, 255}; // Black for all labels + + // Performance counters for debugging + u32 visibleCount = 0; + u32 labelCount = 0; + const b32 showLabels = (view->zoomLevel >= LABEL_VISIBILITY_THRESHOLD); + + // Calculate visible range once for efficient viewport culling + f32 visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI; + get_visible_data_range(view, &visibleMinAbbe, &visibleMaxAbbe, &visibleMinRI, &visibleMaxRI); + // Add some margin for better culling (in data space) + const f32 abbeRange = visibleMaxAbbe - visibleMinAbbe; + const f32 riRange = visibleMaxRI - visibleMinRI; + const f32 abbeMargin = abbeRange * 0.1f; // 10% margin + const f32 riMargin = riRange * 0.1f; + visibleMinAbbe -= abbeMargin; + visibleMaxAbbe += abbeMargin; + visibleMinRI -= riMargin; + visibleMaxRI += riMargin; - // Draw all glass points and labels + // Draw all visible glass points and labels for (u32 i = 0; i < glassCount; i++) { const Glass* glass = get_glass(i); if (!glass) continue; + // Optimized viewport culling - data space bounds check (much faster) + if (!is_glass_visible_fast(glass->abbeNumber, glass->refractiveIndex, + visibleMinAbbe, visibleMaxAbbe, visibleMinRI, visibleMaxRI)) { + continue; + } + + visibleCount++; + i32 x, y; data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &x, &y); - // Check if point is within the plot area (respecting axes padding) + // Additional padding check for plot area const i32 padding = get_adaptive_padding(view); if (x < padding || x >= view->windowWidth - padding || y < padding || y >= view->windowHeight - padding) { @@ -405,9 +504,43 @@ void draw_glass_points(SDL_Renderer *renderer, TTF_Font *labelFont, const ViewSt // Draw glass point (larger) draw_filled_circle(renderer, x, y, 5); - // Show all glass labels (clustering removed) - const char* glassName = (const char*)glass->name; - draw_text(renderer, labelFont, glassName, x + 12, y - 12, labelColor); + // Show labels with tight clustering logic + if (showLabels && should_show_glass_label_tight(i, view)) { + labelCount++; + const char* glassName = (const char*)glass->name; + + // Font selection: Bold for cluster representatives, Regular for single glasses (same size) + i32 clusterIndex = find_tight_cluster_for_glass(i, view); + TTF_Font* fontToUse; + + if (clusterIndex >= 0) { + // Glass is in a cluster - use bold font (same size as regular) + const TightCluster* cluster = &view->tightClusters[clusterIndex]; + if ((u32)i == cluster->representativeIndex) { + // This is a cluster representative - use bold font + fontToUse = boldFontSameSize; + } else { + // This shouldn't happen since non-representatives shouldn't show labels + fontToUse = regularFont; + } + } else { + // Single glass (not in any cluster) - use regular font (same size as bold) + fontToUse = regularFont; + } + + draw_text(renderer, fontToUse, glassName, x + LABEL_OFFSET_X, y + LABEL_OFFSET_Y, labelColor); + } + } + + // Debug output if enabled + if (is_debug_mode()) { + static u32 lastReportTime = 0; + u32 currentTime = SDL_GetTicks(); + if (currentTime - lastReportTime > 1000) { // Report every second + printf("Rendering: %u/%u glasses visible, %u labels drawn (zoom: %.2f)\n", + visibleCount, glassCount, labelCount, view->zoomLevel); + lastReportTime = currentTime; + } } } @@ -434,26 +567,53 @@ void calculate_smart_window_position(i32 glassX, i32 glassY, i32 windowWidth, i3 } -// Draw glass properties window -void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { +// Draw glass properties window with tight cluster support +void draw_glass_properties(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view) { + if (!fonts || !fonts->regular || !fonts->title) { + printf("Error: Invalid fonts for draw_glass_properties\n"); + return; + } if (view->selectedGlass < 0) return; - const Glass* glass = get_glass(view->selectedGlass); - if (!glass) return; + const Glass* selectedGlass = get_glass(view->selectedGlass); + if (!selectedGlass) return; + + // Check if selected glass is part of a tight cluster + i32 clusterIndex = find_tight_cluster_for_glass(view->selectedGlass, view); // Calculate glass position on screen i32 glassX, glassY; - data_to_screen_coords(glass->abbeNumber, glass->refractiveIndex, view, &glassX, &glassY); + data_to_screen_coords(selectedGlass->abbeNumber, selectedGlass->refractiveIndex, view, &glassX, &glassY); + + // Compact window sizing - just slightly larger than content + const i32 windowWidth = 220; // Fixed compact width (slightly larger) - // Window properties - const i32 windowWidth = 300; - const i32 windowHeight = 120; + // Minimal line heights and spacing for label font size + const i32 lineHeight = 15; + const i32 glassSpacing = 17; + const i32 basePadding = 12; + + i32 windowHeight; + u32 glassesToShow; + + if (clusterIndex >= 0) { + // Show all glasses in cluster - minimal padding + const TightCluster* cluster = &view->tightClusters[clusterIndex]; + glassesToShow = cluster->count; + // Each glass: title + 2 detail lines + small gap + windowHeight = basePadding + (glassesToShow * (glassSpacing + 2 * lineHeight + 8)) + basePadding; + } else { + // Show single glass - minimal padding + glassesToShow = 1; + // Title + 3 detail lines + windowHeight = basePadding + glassSpacing + 3 * lineHeight + basePadding; + } i32 windowX, windowY; calculate_smart_window_position(glassX, glassY, windowWidth, windowHeight, view, &windowX, &windowY); // Draw background - SDL_SetRenderDrawColor(renderer, 255, 255, 255, 200); + SDL_SetRenderDrawColor(renderer, 255, 255, 255, 220); SDL_FRect bgRect = {(f32)windowX, (f32)windowY, (f32)windowWidth, (f32)windowHeight}; SDL_RenderFillRect(renderer, &bgRect); @@ -461,25 +621,65 @@ void draw_glass_properties(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *tit SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderRect(renderer, &bgRect); - // Draw text + // Draw content SDL_Color black = {0, 0, 0, 255}; 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); + i32 yPos = windowY + 10; - snprintf(buffer, sizeof(buffer), "Manufacturer: %s", (const char*)glass->manufacturer); - draw_text(renderer, font, buffer, windowX + 10, windowY + 80, black); + if (clusterIndex >= 0) { + // Clustered glass - show all glasses in cluster without cluster text + const TightCluster* cluster = &view->tightClusters[clusterIndex]; + + // Show all glasses in cluster + for (u32 i = 0; i < cluster->count; i++) { + const Glass* glass = get_glass(cluster->glassIndices[i]); + if (!glass) continue; + + // Use same color for all glasses + SDL_Color textColor = black; + // Use bold font for representative glass (shortest name) - but smaller size + TTF_Font* fontToUse = (cluster->glassIndices[i] == cluster->representativeIndex) ? fonts->label_bold : fonts->label; + + // Show glass name as title + snprintf(buffer, sizeof(buffer), "%s", (const char*)glass->name); + draw_text(renderer, fontToUse, buffer, windowX + 10, yPos, textColor); + yPos += glassSpacing; + + // Show details + snprintf(buffer, sizeof(buffer), "nd: %.5f, vd: %.2f", glass->refractiveIndex, glass->abbeNumber); + draw_text(renderer, fonts->label, buffer, windowX + 10, yPos, textColor); + yPos += lineHeight; + + snprintf(buffer, sizeof(buffer), "Manufacturer: %s", (const char*)glass->manufacturer); + draw_text(renderer, fonts->label, buffer, windowX + 10, yPos, textColor); + yPos += glassSpacing; // Extra space between glasses + } + } else { + // Single glass - traditional display with scaled spacing + snprintf(buffer, sizeof(buffer), "%s", (const char*)selectedGlass->name); + draw_text(renderer, fonts->label_bold, buffer, windowX + 10, yPos, black); + yPos += glassSpacing; + + snprintf(buffer, sizeof(buffer), "nd: %.5f", selectedGlass->refractiveIndex); + draw_text(renderer, fonts->label, buffer, windowX + 10, yPos, black); + yPos += lineHeight; + + snprintf(buffer, sizeof(buffer), "vd: %.2f", selectedGlass->abbeNumber); + draw_text(renderer, fonts->label, buffer, windowX + 10, yPos, black); + yPos += lineHeight; + + snprintf(buffer, sizeof(buffer), "Manufacturer: %s", (const char*)selectedGlass->manufacturer); + draw_text(renderer, fonts->label, buffer, windowX + 10, yPos, black); + } } // Draw help window -void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, const ViewState* view) { +void draw_help_window(SDL_Renderer *renderer, const FontSet *fonts, const ViewState* view) { + if (!fonts || !fonts->regular || !fonts->title) { + printf("Error: Invalid fonts for draw_help_window\n"); + return; + } if (!view->showHelp) return; const i32 windowWidth = 400; @@ -500,34 +700,43 @@ void draw_help_window(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFon SDL_Color black = {0, 0, 0, 255}; i32 yPos = windowY + 20; - draw_text(renderer, titleFont, "GlaMaC Help", windowX + 20, yPos, black); + draw_text(renderer, fonts->title, "GlaMaC Help", windowX + 20, yPos, black); yPos += 40; - draw_text(renderer, font, "Mouse Controls:", windowX + 20, yPos, black); + draw_text(renderer, fonts->regular, "Mouse Controls:", windowX + 20, yPos, black); yPos += 25; - draw_text(renderer, font, " Click: Select glass", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " Click: Select glass", windowX + 30, yPos, black); yPos += 20; - draw_text(renderer, font, " Drag: Pan view", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " Drag: Pan view", windowX + 30, yPos, black); yPos += 20; - draw_text(renderer, font, " Wheel: Zoom in/out", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " Wheel: Zoom in/out", windowX + 30, yPos, black); yPos += 30; - draw_text(renderer, font, "Keyboard Controls:", windowX + 20, yPos, black); + draw_text(renderer, fonts->regular, "Keyboard Controls:", windowX + 20, yPos, black); yPos += 25; - draw_text(renderer, font, " g?: Show/hide this help", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " g?: Show/hide this help", windowX + 30, yPos, black); yPos += 20; - draw_text(renderer, font, " r: Reset view", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " r: Reset view", windowX + 30, yPos, black); yPos += 20; - draw_text(renderer, font, " ESC: Clear selection or quit", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " ESC: Clear selection or quit", windowX + 30, yPos, black); yPos += 20; - draw_text(renderer, font, " q: Quit", windowX + 30, yPos, black); + draw_text(renderer, fonts->regular, " q: Quit", windowX + 30, yPos, black); yPos += 30; - draw_text(renderer, font, "Press ESC or g? to close", windowX + 20, yPos, black); + draw_text(renderer, fonts->regular, "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) { +void render(SDL_Renderer *renderer, const FontSet *fonts, ViewState* view) { + if (!fonts) { + printf("Error: Invalid fonts for render\n"); + return; + } + // Only render if view is dirty (needs update) + if (!view->viewDirty) { + return; + } + // Clear screen SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); SDL_RenderClear(renderer); @@ -536,20 +745,22 @@ void render(SDL_Renderer *renderer, TTF_Font *font, TTF_Font *titleFont, TTF_Fon draw_grid(renderer, view); // Draw axes - draw_axes(renderer, font, titleFont, view); + draw_axes(renderer, fonts, view); // Draw glass points and labels - draw_glass_points(renderer, labelFont, view); + draw_glass_points(renderer, fonts, view); // Draw glass properties if selected - draw_glass_properties(renderer, font, titleFont, view); - + draw_glass_properties(renderer, fonts, view); // Draw help window if needed - draw_help_window(renderer, font, titleFont, view); + draw_help_window(renderer, fonts, view); // Present the rendered frame SDL_RenderPresent(renderer); + + // Mark view as clean after rendering + view->viewDirty = 0; } // Font management functions @@ -560,10 +771,24 @@ b32 load_fonts(FontSet *fonts) { fonts->regular = NULL; fonts->title = NULL; fonts->label = NULL; + fonts->axis = NULL; + fonts->label_bold = NULL; fonts->regular = load_font_safe(SAFE_FONT_PATHS, 16); fonts->title = load_font_safe(SAFE_BOLD_FONT_PATHS, 20); fonts->label = load_font_safe(SAFE_FONT_PATHS, 11); + fonts->axis = load_font_safe(SAFE_FONT_PATHS, 8); + fonts->label_bold = load_font_safe(SAFE_BOLD_FONT_PATHS, 12); + + // Debug output to check font loading + if (is_debug_mode()) { + printf("Font loading results:\n"); + printf(" Regular font: %s\n", fonts->regular ? "loaded" : "failed"); + printf(" Title/Bold font: %s\n", fonts->title ? "loaded" : "failed"); + printf(" Label font: %s\n", fonts->label ? "loaded" : "failed"); + printf(" Axis font: %s\n", fonts->axis ? "loaded" : "failed"); + printf(" Label bold font: %s\n", fonts->label_bold ? "loaded" : "failed"); + } // Check if at least one font loaded successfully if (!fonts->regular && !fonts->title && !fonts->label) { @@ -574,18 +799,41 @@ b32 load_fonts(FontSet *fonts) { // Use fallbacks if specific fonts failed if (!fonts->title && fonts->regular) { fonts->title = fonts->regular; - printf("Warning: Using regular font as title font fallback\n"); + if (is_debug_mode()) { + printf("Warning: Using regular font as title font fallback - bold/regular distinction will be lost!\n"); + } } if (!fonts->label && fonts->regular) { fonts->label = fonts->regular; - printf("Warning: Using regular font as label font fallback\n"); + if (is_debug_mode()) { + printf("Warning: Using regular font as label font fallback\n"); + } } if (!fonts->regular && fonts->title) { fonts->regular = fonts->title; - printf("Warning: Using title font as regular font fallback\n"); + if (is_debug_mode()) { + printf("Warning: Using title font as regular font fallback\n"); + } + } + if (!fonts->axis && fonts->regular) { + fonts->axis = fonts->regular; + if (is_debug_mode()) { + printf("Warning: Using regular font as axis font fallback\n"); + } + } + if (!fonts->label_bold && fonts->title) { + fonts->label_bold = fonts->title; + if (is_debug_mode()) { + printf("Warning: Using title font as label bold font fallback\n"); + } + } else if (!fonts->label_bold && fonts->label) { + fonts->label_bold = fonts->label; + if (is_debug_mode()) { + printf("Warning: Using label font as label bold font fallback\n"); + } } - return (fonts->regular && fonts->title && fonts->label) ? 1 : 0; + return (fonts->regular && fonts->title && fonts->label && fonts->axis && fonts->label_bold) ? 1 : 0; } b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 dpi) { @@ -605,23 +853,44 @@ b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 d i32 titleSize = (i32)(20.0f * dpi / 96.0f * windowWidth / 800.0f); // Reduce label size for better overview visibility (was 14.0f) i32 labelSize = (i32)(11.0f * dpi / 96.0f * windowWidth / 800.0f); + i32 axisSize = (i32)(8.0f * dpi / 96.0f * windowWidth / 800.0f); + i32 labelBoldSize = labelSize; // Same size as regular labels for consistency - // Clamp font sizes to safe ranges - if (regularSize < 12) regularSize = 12; + // Clamp font sizes to safe ranges - allow smaller sizes for very small windows + if (regularSize < 10) regularSize = 10; if (regularSize > 32) regularSize = 32; - if (titleSize < 16) titleSize = 16; + if (titleSize < 12) titleSize = 12; if (titleSize > 36) titleSize = 36; - if (labelSize < 8) labelSize = 8; + if (labelSize < 6) labelSize = 6; if (labelSize > 20) labelSize = 20; + if (axisSize < 6) axisSize = 6; // Allow smaller axis fonts for small windows + if (axisSize > 18) axisSize = 18; + if (labelBoldSize < 6) labelBoldSize = 6; + if (labelBoldSize > 20) labelBoldSize = 20; // Initialize to NULL for safe cleanup fonts->regular = NULL; fonts->title = NULL; fonts->label = NULL; + fonts->axis = NULL; + fonts->label_bold = NULL; fonts->regular = load_font_safe(SAFE_FONT_PATHS, regularSize); fonts->title = load_font_safe(SAFE_BOLD_FONT_PATHS, titleSize); fonts->label = load_font_safe(SAFE_FONT_PATHS, labelSize); + fonts->axis = load_font_safe(SAFE_FONT_PATHS, axisSize); + fonts->label_bold = load_font_safe(SAFE_BOLD_FONT_PATHS, labelBoldSize); + + // Debug output to check adaptive font loading + if (is_debug_mode()) { + printf("Adaptive font loading results (sizes: regular=%d, title=%d, label=%d, axis=%d, label_bold=%d):\n", + regularSize, titleSize, labelSize, axisSize, labelBoldSize); + printf(" Regular font: %s\n", fonts->regular ? "loaded" : "failed"); + printf(" Title/Bold font: %s\n", fonts->title ? "loaded" : "failed"); + printf(" Label font: %s\n", fonts->label ? "loaded" : "failed"); + printf(" Axis font: %s\n", fonts->axis ? "loaded" : "failed"); + printf(" Label bold font: %s\n", fonts->label_bold ? "loaded" : "failed"); + } // Check if at least one font loaded successfully if (!fonts->regular && !fonts->title && !fonts->label) { @@ -629,21 +898,61 @@ b32 load_adaptive_fonts(FontSet *fonts, i32 windowWidth, i32 windowHeight, f32 d return 0; } - // Use fallbacks if specific fonts failed + // Enhanced fallback logic that preserves font distinction when possible if (!fonts->title && fonts->regular) { - fonts->title = fonts->regular; - printf("Warning: Using regular font as title font fallback\n"); + // Load bold font at different size to maintain some visual distinction + fonts->title = load_font_safe(SAFE_BOLD_FONT_PATHS, titleSize + 2); + if (!fonts->title) { + // If still no bold font, try regular font at larger size for distinction + fonts->title = load_font_safe(SAFE_FONT_PATHS, titleSize + 2); + if (!fonts->title) { + // Last resort: use regular font but warn about loss of distinction + fonts->title = fonts->regular; + if (is_debug_mode()) { + printf("Warning: Using regular font as title font fallback - bold/regular distinction will be lost!\n"); + } + } else { + if (is_debug_mode()) { + printf("Info: Using larger regular font as title font for visual distinction\n"); + } + } + } else { + if (is_debug_mode()) { + printf("Info: Successfully loaded bold font on second attempt\n"); + } + } } if (!fonts->label && fonts->regular) { fonts->label = fonts->regular; - printf("Warning: Using regular font as label font fallback\n"); + if (is_debug_mode()) { + printf("Warning: Using regular font as label font fallback\n"); + } } if (!fonts->regular && fonts->title) { fonts->regular = fonts->title; - printf("Warning: Using title font as regular font fallback\n"); + if (is_debug_mode()) { + printf("Warning: Using title font as regular font fallback\n"); + } + } + if (!fonts->axis && fonts->regular) { + fonts->axis = fonts->regular; + if (is_debug_mode()) { + printf("Warning: Using regular font as axis font fallback\n"); + } + } + if (!fonts->label_bold && fonts->title) { + fonts->label_bold = fonts->title; + if (is_debug_mode()) { + printf("Info: Using title font as label bold font\n"); + } + } else if (!fonts->label_bold && fonts->label) { + fonts->label_bold = fonts->label; + if (is_debug_mode()) { + printf("Warning: Using label font as label bold font fallback - bold/regular distinction will be lost!\n"); + } } - return (fonts->regular && fonts->title && fonts->label) ? 1 : 0; + return (fonts->regular && fonts->title && fonts->label && fonts->axis && fonts->label_bold) ? 1 : 0; } void free_fonts(FontSet *fonts) { @@ -659,4 +968,12 @@ void free_fonts(FontSet *fonts) { TTF_CloseFont(fonts->label); fonts->label = NULL; } + if (fonts->axis) { + TTF_CloseFont(fonts->axis); + fonts->axis = NULL; + } + if (fonts->label_bold) { + TTF_CloseFont(fonts->label_bold); + fonts->label_bold = NULL; + } } \ No newline at end of file diff --git a/src/glamac/glamac_view.c b/src/glamac/glamac_view.c index 244523e..35c5806 100644 --- a/src/glamac/glamac_view.c +++ b/src/glamac/glamac_view.c @@ -24,15 +24,30 @@ void init_view_state(ViewState* view, i32 windowWidth, i32 windowHeight) { view->gKeyPressed = 0; view->gKeyTime = 0; view->selectedGlass = -1; // No glass selected initially + view->viewDirty = 1; // Initial render needed + + // Initialize tight clustering data + view->tightClusters = NULL; + view->tightClusterCount = 0; + view->glassToCluster = NULL; + view->clusteringGlassCount = 0; // Calculate data range find_glass_data_range(&view->minAbbe, &view->maxAbbe, &view->minRI, &view->maxRI); + + // Create tight clusters for the initial catalog + create_tight_clusters(view); } // Refresh view state data range when catalog changes void refresh_view_data_range(ViewState* view) { find_glass_data_range(&view->minAbbe, &view->maxAbbe, &view->minRI, &view->maxRI); + // Recreate tight clusters for new catalog + free_tight_clusters(view); + create_tight_clusters(view); + + mark_view_dirty(view); } // Calculate visible data range @@ -97,6 +112,9 @@ void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view // If zoom didn't change, don't recalculate if (oldZoom == view->zoomLevel) return; + // Mark view as dirty for re-rendering + mark_view_dirty(view); + // Calculate where the mouse point would be after zoom i32 newMouseScreenX, newMouseScreenY; data_to_screen_coords(mouseDataX, mouseDataY, view, &newMouseScreenX, &newMouseScreenY); @@ -108,15 +126,43 @@ void handle_mouse_wheel_zoom(i32 wheelY, i32 mouseX, i32 mouseY, ViewState* view } -// Toggle fullscreen -void toggle_fullscreen(SDL_Window* window) { +// Toggle fullscreen with proper state synchronization +void toggle_fullscreen(SDL_Window* window, ViewState* view) { + if (!window || !view) return; + bool isFullscreen = SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN; if (!isFullscreen) { + // Entering fullscreen SDL_SetWindowFullscreen(window, true); } else { + // Exiting fullscreen SDL_SetWindowFullscreen(window, false); } + + // Wait a bit longer for the window system to process the state change + SDL_Delay(100); + + // Get updated window dimensions after fullscreen toggle + i32 newWidth, newHeight; + SDL_GetWindowSize(window, &newWidth, &newHeight); + + // Validate new dimensions + if (newWidth > 0 && newHeight > 0) { + view->windowWidth = newWidth; + view->windowHeight = newHeight; + + // Force a complete refresh after fullscreen toggle + mark_view_dirty(view); + + if (is_debug_mode()) { + printf("Fullscreen toggled: %s, new size: %dx%d\n", + isFullscreen ? "OFF" : "ON", newWidth, newHeight); + } + } else { + printf("Warning: Invalid window dimensions after fullscreen toggle: %dx%d\n", + newWidth, newHeight); + } } // Reset view to default @@ -126,9 +172,240 @@ void reset_view(ViewState* view) { view->offsetY = 0.0f; view->selectedGlass = -1; + // Force complete refresh - this ensures fonts and layout are recalculated + mark_view_dirty(view); +} + +// Helper function to calculate string length for name comparison +static size_t safe_glass_name_length(const char* name) { + if (!name) return SIZE_MAX; // Invalid name gets maximum length + return strnlen(name, 50); // Glass names are max 50 chars +} + +// Free tight clustering data +void free_tight_clusters(ViewState* view) { + if (!view) return; + + if (is_debug_mode()) { + printf("Freeing tight clusters: %u clusters, %u glass count\n", + view->tightClusterCount, view->clusteringGlassCount); + } + + // Safe cleanup - use free() directly since data was allocated with safe_malloc/safe_calloc + if (view->tightClusters) { + free(view->tightClusters); + view->tightClusters = NULL; + } + + if (view->glassToCluster) { + free(view->glassToCluster); + view->glassToCluster = NULL; + } + + // Reset counters + view->tightClusterCount = 0; + view->clusteringGlassCount = 0; +} + +// Check if two glasses are close enough for tight clustering +static b32 glasses_are_close(const Glass* glass1, const Glass* glass2) { + if (!glass1 || !glass2) return 0; + + f32 ndDiff = fabsf(glass1->refractiveIndex - glass2->refractiveIndex); + f32 vdDiff = fabsf(glass1->abbeNumber - glass2->abbeNumber); + + return (ndDiff <= TIGHT_CLUSTER_ND_THRESHOLD && vdDiff <= TIGHT_CLUSTER_VD_THRESHOLD); +} + + +// Find the glass with the shortest name in a cluster +static u32 find_representative_glass(const u32* glassIndices, u32 count) { + if (count == 0) return 0; + + u32 representative = glassIndices[0]; + size_t shortestLength = SIZE_MAX; + + for (u32 i = 0; i < count; i++) { + const Glass* glass = get_glass(glassIndices[i]); + if (!glass) continue; + + size_t nameLength = safe_glass_name_length((const char*)glass->name); + if (nameLength < shortestLength) { + shortestLength = nameLength; + representative = glassIndices[i]; + } + } + + return representative; +} + +// Create tight clusters for glasses with similar optical properties +void create_tight_clusters(ViewState* view) { + if (!view) return; + + const u32 glassCount = get_glass_count(); + if (glassCount == 0) { + if (is_debug_mode()) { + printf("No glasses available for clustering\n"); + } + return; + } + + if (is_debug_mode()) { + printf("Starting clustering for %u glasses\n", glassCount); + } + + // Free existing clustering data + free_tight_clusters(view); + + // Allocate glass-to-cluster mapping using safe allocation + view->glassToCluster = new_zero(i32, glassCount); + if (!view->glassToCluster) { + printf("Error: Failed to allocate glass-to-cluster mapping for %u glasses\n", glassCount); + return; + } + + // Initialize all glasses as unclustered (-1) + for (u32 i = 0; i < glassCount; i++) { + view->glassToCluster[i] = -1; + } + + // Allocate clusters array - worst case is every glass is its own cluster + view->tightClusters = new_zero(TightCluster, glassCount); + if (!view->tightClusters) { + printf("Error: Failed to allocate tight clusters array\n"); + free(view->glassToCluster); + view->glassToCluster = NULL; + return; + } + + view->tightClusterCount = 0; + view->clusteringGlassCount = glassCount; + + // Create clusters using simple greedy algorithm + for (u32 i = 0; i < glassCount; i++) { + // Skip if glass is already in a cluster + if (view->glassToCluster[i] != -1) continue; + + const Glass* baseGlass = get_glass(i); + if (!baseGlass) continue; + + // First, find all similar glasses to this one + u32 similarGlasses[MAX_CLUSTER_SIZE]; + u32 similarCount = 1; + similarGlasses[0] = i; // Include the base glass + + for (u32 j = i + 1; j < glassCount && similarCount < MAX_CLUSTER_SIZE; j++) { + if (view->glassToCluster[j] != -1) continue; // Already clustered + + const Glass* candidate = get_glass(j); + if (!candidate) continue; + + // Check if glasses are similar enough to cluster + if (glasses_are_close(baseGlass, candidate)) { + similarGlasses[similarCount] = j; + similarCount++; + } + } + + // Only create a cluster if we have MORE than one glass (i.e., actual clustering) + if (similarCount > 1) { + // Start new cluster with all similar glasses + TightCluster* cluster = &view->tightClusters[view->tightClusterCount]; + cluster->count = similarCount; + + for (u32 k = 0; k < similarCount; k++) { + cluster->glassIndices[k] = similarGlasses[k]; + view->glassToCluster[similarGlasses[k]] = (i32)view->tightClusterCount; + } + + view->tightClusterCount++; + + // Calculate cluster center (average properties) + f32 totalNd = 0.0f, totalVd = 0.0f; + for (u32 k = 0; k < cluster->count; k++) { + const Glass* glass = get_glass(cluster->glassIndices[k]); + if (glass) { + totalNd += glass->refractiveIndex; + totalVd += glass->abbeNumber; + } + } + + cluster->centerNd = totalNd / cluster->count; + cluster->centerVd = totalVd / cluster->count; + + // Find representative glass (shortest name) + cluster->representativeIndex = find_representative_glass(cluster->glassIndices, cluster->count); + } + // If similarCount == 1, leave the glass unclustered (glassToCluster[i] remains -1) + } + + // Debug output + if (is_debug_mode()) { + u32 clusteredGlasses = 0; + u32 multiGlassClusters = 0; + + for (u32 i = 0; i < view->tightClusterCount; i++) { + clusteredGlasses += view->tightClusters[i].count; + if (view->tightClusters[i].count > 1) { + multiGlassClusters++; + } + } + + printf("Created %u clusters (%u multi-glass) containing %u/%u glasses\n", + view->tightClusterCount, multiGlassClusters, clusteredGlasses, glassCount); + } } +// Find the tight cluster index for a given glass (-1 if not clustered) +i32 find_tight_cluster_for_glass(i32 glassIndex, const ViewState* view) { + if (!view || !view->glassToCluster || glassIndex < 0) return -1; + + // Bounds check against clustering glass count + if (view->clusteringGlassCount == 0 || (u32)glassIndex >= view->clusteringGlassCount) { + return -1; + } + + // Additional safety check against current glass count + const u32 currentGlassCount = get_glass_count(); + if ((u32)glassIndex >= currentGlassCount) { + return -1; + } + + i32 clusterIndex = view->glassToCluster[glassIndex]; + + // Validate cluster index + if (clusterIndex >= 0 && (u32)clusterIndex >= view->tightClusterCount) { + return -1; // Invalid cluster index + } + + return clusterIndex; +} +// Check if a glass label should be shown (only representatives in tight clusters) +b32 should_show_glass_label_tight(i32 glassIndex, const ViewState* view) { + if (!view || glassIndex < 0) return 0; + + // If clustering is not initialized, show all labels + if (!view->tightClusters || !view->glassToCluster || view->tightClusterCount == 0) { + return 1; + } + + // Bounds check + if ((u32)glassIndex >= view->clusteringGlassCount) return 0; + + // Find cluster for this glass + i32 clusterIndex = view->glassToCluster[glassIndex]; + if (clusterIndex < 0 || (u32)clusterIndex >= view->tightClusterCount) { + // Glass is not in any cluster, show its label + return 1; + } + + const TightCluster* cluster = &view->tightClusters[clusterIndex]; + + // Only show cluster representatives (shortest name in multi-glass clusters) + return ((u32)glassIndex == cluster->representativeIndex); +} -- cgit v1.2.3