Skip to content

Conversation

@Lavodan
Copy link
Contributor

@Lavodan Lavodan commented Oct 28, 2025

Description:

Fixes #2257 (well, a part of it)
This PR implements a new async helper function in client Utils.ts ( getSvgAspectRatio), which caches and then retrieves aspect ratios for Svg files. (I am not married to it being in Utils.ts, I just couldn't find a better place to put it, and it seemed like something that should be accessible to many files).
It then implements this helper in RadialMenu.ts to fix incorrect aspect ratios. It uses the default value for the smaller side, and the computed value for the larger side.
Before fixing the stretching in EventsDisplay.ts, this PR first consolidates the hard coded html for toggle buttons into a helper function (renderToggleButton). Afterwards, it adds width calculation to this helper function using getSvgAspectRatio.

Potential flaws

  • getSvgAspectRatio might potentially be in the wrong file
  • EventsDisplay.ts consolidation may be out of scope, but it seemed necessary to make clean changes. It also introduces a slight delay before toggle buttons are loaded once a player enters the match
  • If the icon is not cached yet, the RadialMenu implementation shows the old stretched icon for a split second. In regular gameplay this should probably not be noticeable,
  • For some reason (I guess because of slightly too many characters) prettier split up exactly one of the renderToggleButton calls which makes it ugly, but thats what prettier wants lol
  • Not sure if I should add any tests? If so feel free to tell me, but I didn't so far.

Screenshots

Before
image

After
image

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

Lavodan

@Lavodan Lavodan requested a review from a team as a code owner October 28, 2025 17:49
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

Walkthrough

Adds exported async utility getSvgAspectRatio(src) with per-src caching and timeout-based fetch + SVG parsing. EventsDisplay gains a renderToggleButton helper for consistent icon sizing. RadialMenu uses the util to asynchronously adjust icon dimensions and recenters images when aspect ≠ 1.

Changes

Cohort / File(s) Summary
SVG Aspect Ratio Utility
src/client/Utils.ts
Added `getSvgAspectRatio(src: string): Promise<number
Event Toggle UI
src/client/graphics/layers/EventsDisplay.ts
Added defaultButtonSize and renderToggleButton helper; replaced repetitive inline toggle rendering for categories; applies size mapping and visual filter when active.
Radial Menu Icon Resizing
src/client/graphics/layers/RadialMenu.ts
Imported getSvgAspectRatio; capture appended image selection as imgSel and call util; when non-1 ratio returned, compute adjusted width/height and update x/y to center icon for main and submenu icon rendering flows.

Sequence Diagram(s)

sequenceDiagram
    participant UI as RadialMenu / EventsDisplay
    participant Util as Utils.getSvgAspectRatio
    participant Cache as Cache (on function)
    participant Net as network/fetch

    UI->>Util: getSvgAspectRatio(src)
    Util->>Cache: lookup(src)
    alt cache hit
        Cache-->>Util: ratio
    else cache miss
        Util->>Net: fetch(src) with timeout
        Net-->>Util: svg text / error
        alt fetched SVG
            Util->>Util: parse viewBox or width/height -> compute ratio
            Util->>Cache: store(src, ratio or null)
        else fetch/parse failed
            Util->>Cache: store(src, null)
        end
    end
    Util-->>UI: ratio or null
    UI->>UI: if ratio != 1 -> compute w/h and x/y -> update <img> attributes (center)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review focus:
    • async post-render adjustments in RadialMenu (race/flicker)
    • correctness of regex/unit stripping and numeric parsing in Utils
    • caching semantics (identical src strings, query params, data URIs)
    • centering math and DOM attribute updates for <img> elements

Suggested labels

Bug Fix, UI/UX

Suggested reviewers

  • evanpelle

Poem

🖼️ Small icons stretched, then found their grace,
A fetch, a viewBox—each in the right place.
Buttons tidy, menus gently align,
Pixels settle, images center fine. 🎨

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The title "Fix stretched icons" directly and clearly relates to the main change in the pull request. It accurately summarizes the primary objective of the changeset, which is to resolve the stretched icon rendering issue reported in #2257. The title is concise, specific enough for a teammate scanning history to understand the purpose, and avoids generic terminology.
Linked Issues Check ✅ Passed The code changes directly address the requirements from linked issue #2257. The PR implements getSvgAspectRatio to compute and cache SVG aspect ratios, then applies this utility in RadialMenu.ts and EventsDisplay.ts to fix the stretched icon rendering issue. This approach ensures icons maintain their correct aspect ratios instead of being distorted, which is the core requirement of the issue. The implementation handles both the release notes icon and the alliance icon mentioned in the reproduction steps.
Out of Scope Changes Check ✅ Passed All code changes are directly related to fixing the stretched icon issue. While the EventsDisplay.ts changes include refactoring of toggle button rendering into a helper function (renderToggleButton), this refactoring enables clean implementation of the aspect ratio fix without code duplication across five render paths. The author explicitly acknowledged this consolidation in the PR description as necessary for clean implementation. The utility function, RadialMenu modifications, and EventsDisplay modifications all serve the primary objective of correcting icon aspect ratios.
Description Check ✅ Passed The pull request description is well-related to the changeset, providing clear context about what was changed and why. It explains the implementation of the getSvgAspectRatio helper function, its use in RadialMenu.ts and EventsDisplay.ts, and acknowledges potential concerns about the approach. The description includes before/after screenshots and references the linked issue, making it substantive and meaningful rather than vague or off-topic.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Lavodan Lavodan marked this pull request as draft October 28, 2025 17:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/client/Utils.ts (2)

262-262: Consider supporting single-quoted viewBox attributes.

The regex /viewBox="([^"]+)"/i only matches double-quoted attributes. While double quotes are standard, some SVG files use single quotes (viewBox='...').

If needed, update the regex to handle both:

-    const vbMatch = text.match(/viewBox="([^"]+)"/i);
+    const vbMatch = text.match(/viewBox=["']([^"']+)["']/i);

Similarly for width/height attributes at lines 276-277.


250-251: Function-level cache is unconventional but acceptable.

Storing the cache on the function object itself (self.svgAspectRatioCache) is an unconventional pattern in TypeScript. A module-level Map or WeakMap would be more idiomatic, but this approach works and avoids polluting the module scope.

If you prefer a more conventional approach:

+const svgAspectRatioCache = new Map<string, number | null>();
+
 export async function getSvgAspectRatio(src: string): Promise<number | null> {
-  const self = getSvgAspectRatio as any;
-  self.svgAspectRatioCache ??= new Map();
-
-  const cached = self.svgAspectRatioCache.get(src);
+  const cached = svgAspectRatioCache.get(src);
   if (cached !== undefined) return cached;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd45e8 and 0b2adea.

📒 Files selected for processing (3)
  • src/client/Utils.ts (1 hunks)
  • src/client/graphics/layers/EventsDisplay.ts (3 hunks)
  • src/client/graphics/layers/RadialMenu.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/client/graphics/layers/RadialMenu.ts (1)
src/client/Utils.ts (1)
  • getSvgAspectRatio (249-294)
src/client/graphics/layers/EventsDisplay.ts (1)
src/client/Utils.ts (1)
  • getSvgAspectRatio (249-294)
🪛 GitHub Check: 🔍 ESLint
src/client/graphics/layers/EventsDisplay.ts

[failure] 912-912:
'width' is assigned a value but never used.

🔇 Additional comments (3)
src/client/Utils.ts (1)

253-254: Caching null results prevents retry on transient failures.

When getSvgAspectRatio returns null due to a fetch error or parse failure, that null is not cached (line 254 checks cached !== undefined, and the function only caches when it successfully computes a ratio). This means transient failures will cause repeated fetches.

However, if a malformed SVG is fetched successfully but produces null (lines 288-293), the function will re-fetch it on every call. Consider whether caching null after successful fetch but failed parse would improve performance.

src/client/graphics/layers/RadialMenu.ts (2)

555-572: Icons may briefly display stretched before aspect ratio loads.

The implementation creates the image with iconSize × iconSize dimensions (lines 549-553), then asynchronously fetches the aspect ratio and adjusts dimensions (lines 555-572). If the aspect ratio is not yet cached, users may see a brief stretched icon before the correction applies.

This is likely acceptable since:

  1. Subsequent loads will use the cached value
  2. The visual glitch is brief and self-correcting

However, if this is noticeable, consider pre-loading aspect ratios for known icons during init(), similar to the suggestion for EventsDisplay.ts.


546-572: Async icon sizing logic is correctly implemented.

The use of .then() to handle the async aspect ratio adjustment is appropriate here. The icon is created with default dimensions and then updated once the aspect ratio is available. This pattern works well for this use case.

@Lavodan
Copy link
Contributor Author

Lavodan commented Oct 28, 2025

I was able to remove the async aspect ratio completely in EventDisplay.ts simply by not defining the class w-X in the buttons, and letting CSS do the hard work of finding the width. This approach won't work for RadialMenu.ts, because we need to find the largest side in the case. It could technically be hard coded to just use width too, as both the non square icons have a positive aspect ratio (they are wider than taller), but this seems quite short sighted.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b2adea and a80697a.

📒 Files selected for processing (2)
  • src/client/graphics/layers/EventsDisplay.ts (2 hunks)
  • src/client/graphics/layers/RadialMenu.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/RadialMenu.ts
🔇 Additional comments (1)
src/client/graphics/layers/EventsDisplay.ts (1)

954-961: Good consolidation of duplicate code.

The use of renderToggleButton successfully eliminates repetitive inline HTML for the five category filter buttons. This makes the code cleaner and easier to maintain.

Note: This approval is for the consolidation approach itself. See comments above regarding the dynamic Tailwind class issue and missing aspect ratio logic.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 28, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/client/Utils.ts (2)

249-254: Consider adding a cache size limit.

The cache grows unbounded, which could accumulate memory if the app uses many unique SVG URLs over time. For a game with a fixed set of icons this is likely fine, but consider adding an LRU eviction policy or size limit if the icon set is dynamic or user-generated.

Example approach using a simple size limit:

const MAX_CACHE_SIZE = 100;
const cache = self.svgAspectRatioCache;
if (cache.size >= MAX_CACHE_SIZE) {
  const firstKey = cache.keys().next().value;
  cache.delete(firstKey);
}
cache.set(src, ratio);

292-294: Consider logging errors during development.

Silent error handling could make it harder to debug issues with SVG files (CORS problems, malformed URLs, invalid SVG format). Consider adding a console.warn for development builds to help identify problems.

  } catch (e) {
-   // fetch may fail due to CORS or non-SVG..
+   // fetch may fail due to CORS or non-SVG
+   if (process.env.NODE_ENV === 'development') {
+     console.warn(`Failed to get SVG aspect ratio for ${src}:`, e);
+   }
  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8abe6b2 and 0b460a9.

📒 Files selected for processing (1)
  • src/client/Utils.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: 🧪 CI
src/client/Utils.ts

[warning] 1-1: Code style issues found in the above file. Run 'npx prettier --write' to fix.

🔇 Additional comments (1)
src/client/Utils.ts (1)

256-262: Timeout implementation looks good.

The AbortController with timeout correctly addresses the previous review concern about hanging requests. The cleanup with clearTimeout is handled properly.

coderabbitai[bot]
coderabbitai bot previously approved these changes Oct 28, 2025
@Lavodan Lavodan changed the title Fix allianceico Fix stretched icons Oct 28, 2025
@Lavodan Lavodan marked this pull request as ready for review October 28, 2025 20:51
@jrouillard
Copy link
Contributor

jrouillard commented Oct 28, 2025

Thanks for noticing the bug
I am not sure though that computing size after loading is the right solution, it would be better ux and a simpler solution to have icons have the same ratios when in the same menu

evanpelle
evanpelle previously approved these changes Oct 29, 2025
Copy link
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@evanpelle
Copy link
Collaborator

Looks like you may need to run npm run format

@Lavodan Lavodan dismissed stale reviews from evanpelle and coderabbitai[bot] via f9a2e45 October 29, 2025 06:01
@Lavodan
Copy link
Contributor Author

Lavodan commented Oct 29, 2025

Sorry for the format issue again >.<

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/client/Utils.ts (1)

281-293: Fallback parsing handles typical SVG width/height formats.

The parseNum helper correctly strips units (e.g., "px") and the validation catches any malformed values. The implementation works well for standard SVG files.

Optional improvement: Check the response Content-Type header to confirm it's an SVG before parsing, which would provide clearer errors if non-SVG URLs are passed.

Optional: Add Content-Type validation
   const resp = await fetch(src, { signal: controller.signal });
   clearTimeout(timeoutId);
   if (!resp.ok) throw new Error(`Fetch failed: ${resp.status}`);
+  const contentType = resp.headers.get('content-type');
+  if (contentType && !contentType.includes('svg')) {
+    throw new Error('Not an SVG file');
+  }
   const text = await resp.text();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8713f26 and f9a2e45.

📒 Files selected for processing (1)
  • src/client/Utils.ts (1 hunks)
🔇 Additional comments (4)
src/client/Utils.ts (4)

249-254: Clean cache pattern that matches the codebase style.

The function-scoped cache approach is consistent with translateText above and provides good performance for repeated lookups of the same SVG. The early return keeps the code clean.


256-262: Timeout implementation looks solid.

The AbortController pattern with a 5-second timeout prevents hanging requests and the timeout is properly cleared after completion. This addresses the reliability concern from previous reviews.


264-279: ViewBox parsing correctly handles SVG spec variations.

The regex split pattern /[\s,]+/ properly supports both space and comma separators per the SVG specification, and the validation ensures positive numeric dimensions before computing the ratio.


295-300: Error handling supports graceful degradation.

Returning null on any fetch or parse failure allows callers to apply fallback styling when aspect ratios can't be determined, which aligns with the progressive enhancement approach for icon rendering.

@Lavodan
Copy link
Contributor Author

Lavodan commented Oct 29, 2025

@jrouillard

Thanks for noticing the bug I am not sure though that computing size after loading is the right solution, it would be better ux and a simpler solution to have icons have the same ratios when in the same menu

Good shout!
I did consider this (it would be a lot simpler to code), but I feel like the icons shouldn't be limited to a strict (and silent) square aspect ratio. For example, the alliance icon is something that I think benefits quite nicely from being able to be a bit longer. Of course, it is possible to design new icons but I feel like we would be sacrificing UI freedom for uniformity.
I'm not totally against it but I don't really see how having uniform aspect ratios would be better UX

@TheGiraffe3 TheGiraffe3 added this to the v26 milestone Oct 29, 2025
@evanpelle evanpelle merged commit 7fe3b03 into openfrontio:main Oct 29, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some icons appeared stretched

4 participants