mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
Reviewed all 12 CodeRabbit comments on PR #306, verified each against the current code, and fixed the 11 valid ones. The 12th finding is a real code bug (cache_manager.delete() calls in api_helper.py and resource_monitor.py) that's already in the planned follow-up code-fix PR, so it stays out of this docs PR. Fixed: .cursor/plugins_guide.md, .cursor/README.md, .cursorrules - I claimed "there is no --emulator flag" in 3 places. Verified in run.py:19-20 that the -e/--emulator flag is defined and functional (it sets os.environ["EMULATOR"]="true" before the display imports). Other docs I didn't touch (.cursor/plugin_templates/QUICK_START.md, docs/PLUGIN_DEVELOPMENT_GUIDE.md) already use the flag correctly. Replaced all 3 wrong statements with accurate guidance that both forms work and explains the CLI flag's relationship to the env var. .cursorrules, docs/GETTING_STARTED.md, docs/WEB_INTERFACE_GUIDE.md, docs/PLUGIN_DEVELOPMENT_GUIDE.md - Four places claimed "the plugin loader also falls back to plugins/". Verified that PluginManager.discover_plugins() (src/plugin_system/plugin_manager.py:154) only scans the configured directory — no fallback. The fallback to plugins/ exists only in two narrower places: store_manager.py:1700-1718 (store install/update/uninstall operations) and schema_manager.py:70-80 (schema lookup for the web UI form generator). Rewrote all four mentions with the precise scope. Added a recommendation to set plugin_system.plugins_directory to "plugins" for the smoothest dev workflow with dev_plugin_setup.sh symlinks. docs/FONT_MANAGER.md - The "Status" warning told plugin authors to use display_manager.font_manager.resolve_font(...) as a workaround for loading plugin fonts. Verified in src/font_manager.py that resolve_font() takes a family name, not a file path — so the workaround as written doesn't actually work. Rewrote to tell authors to load the font directly with PIL or freetype-py in their plugin. - The same section said "the user-facing font override system in the Fonts tab still works for any element that's been registered via register_manager_font()". Verified in web_interface/blueprints/api_v3.py:5404-5428 that /api/v3/fonts/overrides is a placeholder implementation that returns empty arrays and contains "would integrate with the actual font system" comments — the Fonts tab does not have functional integration with register_manager_font() or the override system. Removed the false claim and added an explicit note that the tab is a placeholder. docs/ADVANCED_FEATURES.md:523 - The on-demand section said REST/UI calls write a request "into the cache manager (display_on_demand_config key)". Wrong — verified via grep that api_v3.py:1622 and :1687 write to display_on_demand_request, and display_on_demand_config is only written by the controller during activation (display_controller.py:1195, cleared at :1221). Corrected the key name and added controller file:line references so future readers can verify. docs/ADVANCED_FEATURES.md:803 - "Plugins using the background service" paragraph listed all scoreboard plugins but an orphaned "⏳ MLB (baseball)" bullet remained below from the old version of the section. Removed the orphan and added "baseball/MLB" to the inline list for clarity. web_interface/README.md - The POST /api/v3/system/action action list was incomplete. Verified in web_interface/app.py:1383,1386 that enable_autostart and disable_autostart are valid actions. Added both. - The Plugin Store section was missing GET /api/v3/plugins/store/github-status (verified at api_v3.py:3296). Added it. - The SSE line-range reference was app.py:607-615 but line 619 contains the "Exempt SSE streams from CSRF and add rate limiting" block that's semantically part of the same feature. Extended the range to 607-619. docs/GETTING_STARTED.md - Rows/Columns step said "Columns: 64 or 96 (match your hardware)". The web UI's validation accepts any integer in 16-128. Clarified that 64 and 96 are the common bundled-hardware values but the valid range is wider. Not addressed (out of scope for docs PR): - .cursorrules:184 CodeRabbit comment flagged the non-existent cache_manager.delete() calls in src/common/api_helper.py:287 and src/plugin_system/resource_monitor.py:343. These are real CODE bugs, not doc bugs, and they're the first item in the planned post-docs-refresh code-cleanup PR (see /home/chuck/.claude/plans/warm-imagining-river.md). The docs in this PR correctly state that delete() doesn't exist on CacheManager — the fix belongs in the follow-up code PR that either adds a delete() shim or updates the two callers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
389 lines
12 KiB
Markdown
389 lines
12 KiB
Markdown
# FontManager Usage Guide
|
|
|
|
## Overview
|
|
|
|
The enhanced FontManager provides comprehensive font management for the LEDMatrix application with support for:
|
|
- Manager font registration and detection
|
|
- Plugin font management
|
|
- Manual font overrides via web interface
|
|
- Performance monitoring and caching
|
|
- Dynamic font discovery
|
|
|
|
## Architecture
|
|
|
|
### Manager-Centric Design
|
|
|
|
Managers define their own fonts, but the FontManager:
|
|
1. **Loads and caches fonts** for performance
|
|
2. **Detects font usage** for visibility
|
|
3. **Allows manual overrides** when needed
|
|
4. **Supports plugin fonts** with namespacing
|
|
|
|
### Font Resolution Flow
|
|
|
|
```
|
|
Manager requests font → Check manual overrides → Apply manager choice → Cache & return
|
|
```
|
|
|
|
## For Manager Developers
|
|
|
|
### Basic Font Usage
|
|
|
|
```python
|
|
from src.font_manager import FontManager
|
|
|
|
class MyManager:
|
|
def __init__(self, config, display_manager, cache_manager):
|
|
self.font_manager = display_manager.font_manager # Access shared FontManager
|
|
self.manager_id = "my_manager"
|
|
|
|
def display(self):
|
|
# Define your font choices
|
|
element_key = "my_manager.title"
|
|
font_family = "press_start"
|
|
font_size_px = 10
|
|
color = (255, 255, 255) # RGB white
|
|
|
|
# Register your font choice (for detection and future overrides)
|
|
self.font_manager.register_manager_font(
|
|
manager_id=self.manager_id,
|
|
element_key=element_key,
|
|
family=font_family,
|
|
size_px=font_size_px,
|
|
color=color
|
|
)
|
|
|
|
# Get the font (checks for manual overrides automatically)
|
|
font = self.font_manager.resolve_font(
|
|
element_key=element_key,
|
|
family=font_family,
|
|
size_px=font_size_px
|
|
)
|
|
|
|
# Use the font for rendering
|
|
self.display_manager.draw_text(
|
|
"Hello World",
|
|
x=10, y=10,
|
|
color=color,
|
|
font=font
|
|
)
|
|
```
|
|
|
|
### Advanced Font Usage
|
|
|
|
```python
|
|
class AdvancedManager:
|
|
def __init__(self, config, display_manager, cache_manager):
|
|
self.font_manager = display_manager.font_manager
|
|
self.manager_id = "advanced_manager"
|
|
|
|
# Define your font specifications
|
|
self.font_specs = {
|
|
"title": {"family": "press_start", "size_px": 12, "color": (255, 255, 0)},
|
|
"body": {"family": "four_by_six", "size_px": 8, "color": (255, 255, 255)},
|
|
"footer": {"family": "five_by_seven", "size_px": 7, "color": (128, 128, 128)}
|
|
}
|
|
|
|
# Register all font specs
|
|
for element_type, spec in self.font_specs.items():
|
|
element_key = f"{self.manager_id}.{element_type}"
|
|
self.font_manager.register_manager_font(
|
|
manager_id=self.manager_id,
|
|
element_key=element_key,
|
|
family=spec["family"],
|
|
size_px=spec["size_px"],
|
|
color=spec["color"]
|
|
)
|
|
|
|
def get_font(self, element_type: str):
|
|
"""Helper method to get fonts with override support."""
|
|
spec = self.font_specs[element_type]
|
|
element_key = f"{self.manager_id}.{element_type}"
|
|
|
|
return self.font_manager.resolve_font(
|
|
element_key=element_key,
|
|
family=spec["family"],
|
|
size_px=spec["size_px"]
|
|
)
|
|
|
|
def display(self):
|
|
# Get fonts (automatically checks for overrides)
|
|
title_font = self.get_font("title")
|
|
body_font = self.get_font("body")
|
|
footer_font = self.get_font("footer")
|
|
|
|
# Render with fonts
|
|
self.display_manager.draw_text("Title", font=title_font, color=self.font_specs["title"]["color"])
|
|
self.display_manager.draw_text("Body Text", font=body_font, color=self.font_specs["body"]["color"])
|
|
self.display_manager.draw_text("Footer", font=footer_font, color=self.font_specs["footer"]["color"])
|
|
```
|
|
|
|
### Using Size Tokens
|
|
|
|
```python
|
|
# Get available size tokens
|
|
tokens = self.font_manager.get_size_tokens()
|
|
# Returns: {'xs': 6, 'sm': 8, 'md': 10, 'lg': 12, 'xl': 14, 'xxl': 16}
|
|
|
|
# Use token to get size
|
|
size_px = tokens.get('md', 10) # 10px
|
|
|
|
# Then use in font resolution
|
|
font = self.font_manager.resolve_font(
|
|
element_key="my_manager.text",
|
|
family="press_start",
|
|
size_px=size_px
|
|
)
|
|
```
|
|
|
|
## For Plugin Developers
|
|
|
|
> ⚠️ **Status**: the plugin-font registration described below is
|
|
> implemented in `src/font_manager.py:150` (`register_plugin_fonts()`)
|
|
> but is **not currently wired into the plugin loader**. Adding a
|
|
> `"fonts"` block to your plugin's `manifest.json` will silently have
|
|
> no effect — the FontManager method exists but nothing calls it.
|
|
>
|
|
> Until that's connected, plugin authors who need a custom font
|
|
> should load it directly with PIL (or `freetype-py` for BDF) in
|
|
> their plugin's `manager.py` — `FontManager.resolve_font(family=…,
|
|
> size_px=…)` takes a **family name**, not a file path, so it can't
|
|
> be used to pull a font from your plugin directory. The
|
|
> `plugin://…` source URIs described below are only honored by
|
|
> `register_plugin_fonts()` itself, which isn't wired up.
|
|
>
|
|
> The `/api/v3/fonts/overrides` endpoints and the **Fonts** tab in
|
|
> the web UI are currently **placeholder implementations** — they
|
|
> return empty arrays and contain "would integrate with the actual
|
|
> font system" comments. Manually registered manager fonts do
|
|
> **not** yet flow into that tab. If you need an override today,
|
|
> load the font directly in your plugin and skip the
|
|
> override system.
|
|
|
|
### Plugin Font Registration (planned)
|
|
|
|
In your plugin's `manifest.json`:
|
|
|
|
```json
|
|
{
|
|
"id": "my-plugin",
|
|
"name": "My Plugin",
|
|
"fonts": {
|
|
"fonts": [
|
|
{
|
|
"family": "custom_font",
|
|
"source": "plugin://fonts/custom.ttf",
|
|
"metadata": {
|
|
"description": "Custom plugin font",
|
|
"license": "MIT"
|
|
}
|
|
},
|
|
{
|
|
"family": "web_font",
|
|
"source": "https://example.com/fonts/font.ttf",
|
|
"metadata": {
|
|
"description": "Downloaded font",
|
|
"checksum": "sha256:abc123..."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using Plugin Fonts
|
|
|
|
```python
|
|
class PluginManager:
|
|
def __init__(self, config, display_manager, cache_manager, plugin_id):
|
|
self.font_manager = display_manager.font_manager
|
|
self.plugin_id = plugin_id
|
|
|
|
def display(self):
|
|
# Use plugin font (automatically namespaced)
|
|
font = self.font_manager.resolve_font(
|
|
element_key=f"{self.plugin_id}.text",
|
|
family="custom_font", # Will be resolved as "my-plugin::custom_font"
|
|
size_px=10,
|
|
plugin_id=self.plugin_id
|
|
)
|
|
|
|
self.display_manager.draw_text("Plugin Text", font=font)
|
|
```
|
|
|
|
## Manual Font Overrides
|
|
|
|
Users can override any font through the web interface:
|
|
|
|
1. Navigate to **Fonts** tab
|
|
2. View **Detected Manager Fonts** to see what's currently in use
|
|
3. In **Element Overrides** section:
|
|
- Select the element (e.g., "nfl.live.score")
|
|
- Choose a different font family
|
|
- Choose a different size
|
|
- Click **Add Override**
|
|
|
|
Overrides are stored in `config/font_overrides.json` and persist across restarts.
|
|
|
|
### Programmatic Overrides
|
|
|
|
```python
|
|
# Set override
|
|
font_manager.set_override(
|
|
element_key="nfl.live.score",
|
|
family="four_by_six",
|
|
size_px=8
|
|
)
|
|
|
|
# Remove override
|
|
font_manager.remove_override("nfl.live.score")
|
|
|
|
# Get all overrides
|
|
overrides = font_manager.get_overrides()
|
|
```
|
|
|
|
## Font Discovery
|
|
|
|
### Available Fonts
|
|
|
|
The FontManager automatically scans `assets/fonts/` for TTF and BDF fonts:
|
|
|
|
```python
|
|
# Get all available fonts
|
|
fonts = font_manager.get_available_fonts()
|
|
# Returns: {'press_start': 'assets/fonts/PressStart2P-Regular.ttf', ...}
|
|
|
|
# Check if font exists
|
|
if "my_font" in fonts:
|
|
font = font_manager.get_font("my_font", 10)
|
|
```
|
|
|
|
### Adding Custom Fonts
|
|
|
|
Place font files in `assets/fonts/` directory:
|
|
- Supported formats: `.ttf`, `.bdf`
|
|
- Font family name is derived from filename (without extension)
|
|
- Will be automatically discovered on next initialization
|
|
|
|
## Performance Monitoring
|
|
|
|
```python
|
|
# Get performance stats
|
|
stats = font_manager.get_performance_stats()
|
|
|
|
print(f"Cache hit rate: {stats['cache_hit_rate']*100:.1f}%")
|
|
print(f"Total fonts cached: {stats['total_fonts_cached']}")
|
|
print(f"Failed loads: {stats['failed_loads']}")
|
|
print(f"Manager fonts: {stats['manager_fonts']}")
|
|
print(f"Plugin fonts: {stats['plugin_fonts']}")
|
|
```
|
|
|
|
## Text Measurement
|
|
|
|
```python
|
|
# Measure text dimensions
|
|
width, height, baseline = font_manager.measure_text("Hello", font)
|
|
|
|
# Get font height
|
|
font_height = font_manager.get_font_height(font)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### For Managers
|
|
|
|
1. **Register all fonts** you use for visibility
|
|
2. **Use consistent element keys** (e.g., `{manager_id}.{element_type}`)
|
|
3. **Cache font references** if using same font multiple times
|
|
4. **Use `resolve_font()`** not `get_font()` directly to support overrides
|
|
5. **Define sensible defaults** that work well on LED matrix
|
|
|
|
### For Plugins
|
|
|
|
1. **Use plugin-relative paths** (`plugin://fonts/...`)
|
|
2. **Include font metadata** (license, description)
|
|
3. **Provide fallback** fonts if custom fonts fail to load
|
|
4. **Test with different display sizes**
|
|
|
|
### General
|
|
|
|
1. **BDF fonts** are often better for small sizes on LED matrices
|
|
2. **TTF fonts** work well for larger sizes
|
|
3. **Monospace fonts** are easier to align
|
|
4. **Test on actual hardware** - what looks good on screen may not work on LED matrix
|
|
|
|
## Migration from Old System
|
|
|
|
### Old Way (Direct Font Loading)
|
|
```python
|
|
self.font = ImageFont.truetype("assets/fonts/PressStart2P-Regular.ttf", 8)
|
|
```
|
|
|
|
### New Way (FontManager)
|
|
```python
|
|
element_key = f"{self.manager_id}.text"
|
|
self.font_manager.register_manager_font(
|
|
manager_id=self.manager_id,
|
|
element_key=element_key,
|
|
family="pressstart2p-regular",
|
|
size_px=8
|
|
)
|
|
self.font = self.font_manager.resolve_font(
|
|
element_key=element_key,
|
|
family="pressstart2p-regular",
|
|
size_px=8
|
|
)
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Font Not Found
|
|
- Check font file exists in `assets/fonts/`
|
|
- Verify font family name matches filename (without extension, lowercase)
|
|
- Check logs for font discovery errors
|
|
|
|
### Override Not Working
|
|
- Verify element key matches exactly what manager registered
|
|
- Check `config/font_overrides.json` for correct syntax
|
|
- Restart application to ensure overrides are loaded
|
|
|
|
### Performance Issues
|
|
- Check cache hit rate in performance stats
|
|
- Reduce number of unique font/size combinations
|
|
- Clear cache if it grows too large: `font_manager.clear_cache()`
|
|
|
|
### Plugin Fonts Not Loading
|
|
- Verify plugin manifest syntax
|
|
- Check plugin directory structure
|
|
- Review logs for download/registration errors
|
|
- Ensure font URLs are accessible
|
|
|
|
## API Reference
|
|
|
|
### FontManager Methods
|
|
|
|
- `register_manager_font(manager_id, element_key, family, size_px, color=None)` - Register font usage
|
|
- `resolve_font(element_key, family, size_px, plugin_id=None)` - Get font with override support
|
|
- `get_font(family, size_px)` - Get font directly (bypasses overrides)
|
|
- `measure_text(text, font)` - Measure text dimensions
|
|
- `get_font_height(font)` - Get font height
|
|
- `set_override(element_key, family=None, size_px=None)` - Set manual override
|
|
- `remove_override(element_key)` - Remove override
|
|
- `get_overrides()` - Get all overrides
|
|
- `get_detected_fonts()` - Get all detected font usage
|
|
- `get_manager_fonts(manager_id=None)` - Get fonts by manager
|
|
- `get_available_fonts()` - Get font catalog
|
|
- `get_size_tokens()` - Get size token definitions
|
|
- `get_performance_stats()` - Get performance metrics
|
|
- `clear_cache()` - Clear font cache
|
|
- `register_plugin_fonts(plugin_id, font_manifest)` - Register plugin fonts
|
|
- `unregister_plugin_fonts(plugin_id)` - Unregister plugin fonts
|
|
|
|
## Example: Complete Manager Implementation
|
|
|
|
For a working example of the font manager API in use, see
|
|
`src/font_manager.py` itself and the bundled scoreboard base classes
|
|
in `src/base_classes/` (e.g., `hockey.py`, `football.py`) which
|
|
register and resolve fonts via the patterns documented above.
|
|
|