The Annotation System allows users to add visual annotations (arrows, squares, circles, text) to chessboard positions. Annotations are stored per move (ply) and can be saved to PGN tags for persistence. The system supports multiple annotation types with customizable colors, sizes, and styling options. Annotations are displayed on the chessboard and can be toggled on/off.
The annotation system follows a Model-Controller-Service-View pattern with per-ply storage:
AnnotationController (app/controllers/annotation_controller.py):
- Orchestrates annotation operations
- Manages
AnnotationModelandAnnotationStorageService - Handles annotation creation (arrow, square, circle, text)
- Implements toggle behavior (add/remove identical annotations)
- Manages annotation visibility
- Handles game and move changes (loads annotations per game)
- Saves annotations to PGN tags
- Marks databases as unsaved when annotations are saved
AnnotationModel (app/models/annotation_model.py):
- Stores annotations per ply:
{ply_index: [Annotation, ...]} - Emits signals when annotations change
- Manages annotation layer visibility
- Provides methods to add, remove, clear annotations
- QObject-based model for signal/slot communication
AnnotationStorageService (app/services/annotation_storage_service.py):
- Serializes annotations to JSON
- Compresses and stores data in PGN tag
[CARAAnnotations "..."] - Stores metadata in
[CARAAnnotationsInfo "..."]and checksum in[CARAAnnotationsChecksum "..."] - Loads and validates stored annotations
- Handles corrupted data cleanup
ChessBoardWidget (app/views/chessboard_widget.py):
- Renders annotations on chessboard
- Observes
AnnotationModelsignals for updates - Draws arrows, squares, circles, text based on annotation data
- Handles annotation layer visibility toggle
Annotation Creation Flow:
- User interacts with chessboard (e.g., drags to create arrow)
- View calls controller method (e.g.,
add_arrow()) - Controller gets current ply index from
GameModel - Controller checks if identical annotation exists (toggle behavior)
- If exists, controller removes it (toggle off)
- If not exists, controller creates new
Annotationwith unique ID - Controller calls
AnnotationModel.add_annotation() - Model stores annotation and emits
annotation_addedsignal - View observes signal and redraws chessboard
- Database is marked as unsaved (if auto-save enabled)
Game Change Flow:
- User switches to different game
GameModelemitsactive_game_changedsignal- Controller observes signal and calls
_on_active_game_changed() - Controller calls
AnnotationStorageService.load_annotations() - Service loads annotations from game's PGN tag
- Controller calls
AnnotationModel.set_all_annotations() - Model stores annotations and emits signals for all affected plies
- View observes signals and updates chessboard display
Move Navigation Flow:
- User navigates to different move
GameModelemitsactive_move_changedsignal- Controller observes signal (annotations are automatically shown for current ply)
- View observes
AnnotationModeland displays annotations for current ply - No explicit controller action needed (model/view handle it)
Annotation Save Flow:
- User saves annotations (or auto-save triggers)
- Controller calls
save_annotations() - Controller gets all annotations from model
- Controller calls
AnnotationStorageService.store_annotations() - Service serializes, compresses, and stores in PGN tag
- Service updates game's PGN text
- Controller emits
metadata_updatedsignal onGameModel - Controller updates database model and marks as unsaved
- Progress service displays success message
Annotation Visibility Toggle Flow:
- User toggles annotation layer visibility
- Controller calls
toggle_annotations_visibility() - Controller calls
AnnotationModel.toggle_annotations_visibility() - Model emits
annotations_visibility_changedsignal - View observes signal and shows/hides annotation layer
- Purpose: Show move directions or piece trajectories
- Properties:
from_square: Starting square (e.g., "e2")to_square: Ending square (e.g., "e4")color: RGB color [r, g, b]color_index: Index into color palettesize: Size multiplier (0.5-2.0, default: 1.0)shadow: Whether to add black shadow for readability
- Purpose: Highlight squares
- Properties:
square: Square to highlight (e.g., "e4")color: RGB color [r, g, b]color_index: Index into color palettesize: Size multiplier (0.5-2.0, default: 1.0)shadow: Whether to add black shadow for readability
- Purpose: Circle squares
- Properties:
square: Square to circle (e.g., "e4")color: RGB color [r, g, b]color_index: Index into color palettesize: Size multiplier (0.5-2.0, default: 1.0)shadow: Whether to add black shadow for readability
- Purpose: Add text labels to squares
- Properties:
square: Square to place text on (e.g., "e4")text: Text contentcolor: RGB color [r, g, b]color_index: Index into color palettetext_x: X position relative to square (0-1, default: 0.5)text_y: Y position relative to square (0-1, default: 0.5)text_size: Text size in points (default: 12.0)text_rotation: Text rotation in degrees (default: 0.0)size: Size multiplier (0.5-2.0, default: 1.0)shadow: Whether to add black shadow for readability
Annotations support toggle behavior:
- Adding identical annotation: If an annotation with same type, squares/position, and color exists, it is removed (toggle off)
- Adding different annotation: Creates new annotation (toggle on)
- Purpose: Allows users to easily add/remove annotations by repeating the same action
Toggle behavior is implemented in controller methods:
_find_existing_arrow(): Checks for identical arrow_find_existing_square(): Checks for identical square_find_existing_circle(): Checks for identical circle
Annotations are stored in PGN tags for persistence:
- Tag:
[CARAAnnotations "..."]- Compressed, base64-encoded JSON - Metadata:
[CARAAnnotationsInfo "..."]- App version and creation datetime - Checksum:
[CARAAnnotationsChecksum "..."]- SHA256 hash for data integrity
Storage format:
- JSON serialization:
{ply_index: [annotation_dict, ...]} - Each annotation dict includes: id, type, color, color_index, and type-specific properties
- Gzip compression (level 9)
- Base64 encoding for PGN tag compatibility
- Checksum validation on load
If annotations cannot be decompressed or checksum validation fails, corrupted tags are automatically removed from the game's PGN.
Annotations are stored per ply (move):
- Ply index 0: Starting position
- Ply index 1: After white's first move
- Ply index 2: After black's first move
- Ply index N: After N-th half-move
This allows:
- Different annotations for each position in the game
- Annotations persist when navigating through moves
- Annotations are automatically shown/hidden based on current move
Annotation layer visibility can be toggled:
- Show annotations: All annotations are visible on chessboard
- Hide annotations: All annotations are hidden (but not deleted)
- Toggle: Users can show/hide annotations without losing them
- Per-game: Visibility state is managed per game session
- Controller:
app/controllers/annotation_controller.py - Model:
app/models/annotation_model.py - Storage:
app/services/annotation_storage_service.py - View:
app/views/chessboard_widget.py(annotation rendering) - Data Classes:
app/models/annotation_model.py(Annotation, AnnotationType)