Source code for yapcad.viewer.events

"""
WebSocket Event Types
=====================

Defines event types for WebSocket communication between the viewer server
and connected clients.

Event Categories
----------------

**Client-to-Server Events:**
    Commands sent from clients to control the viewer.

**Server-to-Client Events:**
    Notifications sent from the viewer to connected clients.

Example Usage
-------------

Sending events from server::

    from yapcad.viewer.events import ServerEvent, PartLoadedEvent

    event = PartLoadedEvent(
        part_name="AXIS1_SUN_GEAR",
        stl_file="gearbox/axis1_sun_gear.stl",
        bounds=[-10, 10, -10, 10, 0, 20]
    )
    socketio.emit(event.event_type, event.to_dict())

Handling events on client::

    socket.on('part_loaded', (data) => {
        console.log(`Loaded: ${data.part_name}`);
    });
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple


[docs] class EventType(str, Enum): """ Enumeration of all WebSocket event types. Client-to-Server ---------------- COMMAND Execute a viewer command. SUBSCRIBE Subscribe to specific event types. UNSUBSCRIBE Unsubscribe from event types. Server-to-Client ---------------- STATUS Viewer status update. PART_LOADED Part was loaded into the scene. PART_REMOVED Part was removed from the scene. PARTS_CLEARED All parts were cleared. XRAY_CHANGED X-ray mode was toggled. CAMERA_CHANGED Camera position/orientation changed. SCREENSHOT_SAVED Screenshot was captured and saved. SELECTION_CHANGED Part selection changed. ERROR An error occurred. """ # Client-to-Server COMMAND = "command" SUBSCRIBE = "subscribe" UNSUBSCRIBE = "unsubscribe" # Server-to-Client STATUS = "status" PART_LOADED = "part_loaded" PART_REMOVED = "part_removed" PARTS_CLEARED = "parts_cleared" XRAY_CHANGED = "xray_changed" CAMERA_CHANGED = "camera_changed" SCREENSHOT_SAVED = "screenshot_saved" SELECTION_CHANGED = "selection_changed" ERROR = "error"
[docs] @dataclass class BaseEvent: """ Base class for all WebSocket events. Attributes ---------- event_type : str The event type identifier. """ event_type: str = field(init=False)
[docs] def to_dict(self) -> Dict[str, Any]: """ Convert event to dictionary for JSON serialization. Returns ------- dict Event data as dictionary. """ return { k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "event_type" }
# ============================================================================= # Server-to-Client Events # =============================================================================
[docs] @dataclass class StatusEvent(BaseEvent): """ Viewer status update event. Sent periodically or on request to provide current viewer state. Attributes ---------- part_count : int Number of parts currently loaded. xray_enabled : bool Whether x-ray mode is active. viewport : str Current active viewport name. camera_position : tuple of float Camera position (x, y, z). camera_focal_point : tuple of float Camera focal point (x, y, z). WebSocket Event --------------- Event name: ``status`` Payload:: { "part_count": 42, "xray_enabled": false, "viewport": "ISO", "camera_position": [100.0, 100.0, 100.0], "camera_focal_point": [0.0, 0.0, 50.0] } """ part_count: int xray_enabled: bool viewport: str = "ISO" camera_position: Tuple[float, float, float] = (0.0, 0.0, 0.0) camera_focal_point: Tuple[float, float, float] = (0.0, 0.0, 0.0) def __post_init__(self): self.event_type = EventType.STATUS.value
[docs] @dataclass class PartLoadedEvent(BaseEvent): """ Event emitted when a part is loaded. Attributes ---------- part_name : str Name/identifier of the loaded part. stl_file : str Path to the STL file. bounds : list of float Bounding box [xmin, xmax, ymin, ymax, zmin, zmax]. color : tuple of float, optional RGB color (0-1 range). WebSocket Event --------------- Event name: ``part_loaded`` Payload:: { "part_name": "AXIS1_SUN_GEAR", "stl_file": "gearbox/axis1_sun_gear.stl", "bounds": [-10.0, 10.0, -10.0, 10.0, 0.0, 20.0], "color": [0.8, 0.5, 0.2] } """ part_name: str stl_file: str bounds: List[float] = field(default_factory=list) color: Optional[Tuple[float, float, float]] = None def __post_init__(self): self.event_type = EventType.PART_LOADED.value
[docs] @dataclass class PartRemovedEvent(BaseEvent): """ Event emitted when a part is removed. Attributes ---------- part_name : str Name of the removed part. WebSocket Event --------------- Event name: ``part_removed`` Payload:: { "part_name": "AXIS1_SUN_GEAR" } """ part_name: str def __post_init__(self): self.event_type = EventType.PART_REMOVED.value
[docs] @dataclass class PartsClearedEvent(BaseEvent): """ Event emitted when all parts are cleared. Attributes ---------- previous_count : int Number of parts that were cleared. WebSocket Event --------------- Event name: ``parts_cleared`` Payload:: { "previous_count": 42 } """ previous_count: int def __post_init__(self): self.event_type = EventType.PARTS_CLEARED.value
[docs] @dataclass class XrayChangedEvent(BaseEvent): """ Event emitted when x-ray mode changes. Attributes ---------- enabled : bool Whether x-ray mode is now enabled. opacity : float Current opacity value. WebSocket Event --------------- Event name: ``xray_changed`` Payload:: { "enabled": true, "opacity": 0.4 } """ enabled: bool opacity: float def __post_init__(self): self.event_type = EventType.XRAY_CHANGED.value
[docs] @dataclass class CameraChangedEvent(BaseEvent): """ Event emitted when camera position changes. Attributes ---------- viewport : str Viewport name that changed. position : tuple of float New camera position (x, y, z). focal_point : tuple of float New focal point (x, y, z). view_up : tuple of float View up vector. WebSocket Event --------------- Event name: ``camera_changed`` Payload:: { "viewport": "ISO", "position": [100.0, 100.0, 100.0], "focal_point": [0.0, 0.0, 50.0], "view_up": [0.0, 0.0, 1.0] } """ viewport: str position: Tuple[float, float, float] focal_point: Tuple[float, float, float] view_up: Tuple[float, float, float] = (0.0, 0.0, 1.0) def __post_init__(self): self.event_type = EventType.CAMERA_CHANGED.value
[docs] @dataclass class ScreenshotSavedEvent(BaseEvent): """ Event emitted when a screenshot is saved. Attributes ---------- filename : str Name of the saved file. filepath : str Full path to the saved file. size : tuple of int Image dimensions (width, height). WebSocket Event --------------- Event name: ``screenshot_saved`` Payload:: { "filename": "screenshot_001.png", "filepath": "/path/to/renders/screenshot_001.png", "size": [1600, 1000] } """ filename: str filepath: str size: Tuple[int, int] = (0, 0) def __post_init__(self): self.event_type = EventType.SCREENSHOT_SAVED.value
[docs] @dataclass class SelectionChangedEvent(BaseEvent): """ Event emitted when part selection changes. Attributes ---------- selected_parts : list of str Names of currently selected parts. highlighted_parts : list of str Names of currently highlighted parts. WebSocket Event --------------- Event name: ``selection_changed`` Payload:: { "selected_parts": ["AXIS1_SUN_GEAR"], "highlighted_parts": ["AXIS1_SUN_GEAR", "AXIS1_RING_HOUSING"] } """ selected_parts: List[str] = field(default_factory=list) highlighted_parts: List[str] = field(default_factory=list) def __post_init__(self): self.event_type = EventType.SELECTION_CHANGED.value
[docs] @dataclass class ErrorEvent(BaseEvent): """ Event emitted when an error occurs. Attributes ---------- message : str Error message. code : str, optional Error code for programmatic handling. details : dict, optional Additional error details. WebSocket Event --------------- Event name: ``error`` Payload:: { "message": "Failed to load STL file", "code": "LOAD_ERROR", "details": {"file": "missing.stl"} } """ message: str code: Optional[str] = None details: Optional[Dict[str, Any]] = None def __post_init__(self): self.event_type = EventType.ERROR.value
# ============================================================================= # Client-to-Server Command Types # =============================================================================
[docs] class CommandType(str, Enum): """ Types of commands that can be sent to the viewer. Commands -------- LOAD Load parts into the viewer. CLEAR Clear all loaded parts. RELOAD Reload positions from JSON file. XRAY Toggle or set x-ray mode. SCREENSHOT Capture a screenshot. FOCUS Focus camera on a specific part. HIGHLIGHT Highlight specific parts. CAMERA_RESET Reset camera to default position. CAMERA_SET Set camera position directly. CAMERA_ORBIT Orbit camera around focal point. MOVE Move a part by relative offset. MOVE_TO Move a part to absolute position. """ LOAD = "load" CLEAR = "clear" RELOAD = "reload" XRAY = "xray" SCREENSHOT = "screenshot" FOCUS = "focus" HIGHLIGHT = "highlight" CAMERA_RESET = "camera_reset" CAMERA_SET = "camera_set" CAMERA_ORBIT = "camera_orbit" MOVE = "move" MOVE_TO = "move_to"
[docs] @dataclass class CommandMessage: """ Command message sent from client to server. Attributes ---------- command : str Command type (from CommandType enum). params : dict, optional Command parameters. Example ------- Load command:: { "command": "load", "params": { "parts": ["AXIS1_SUN_GEAR", "AXIS1_RING_HOUSING"], "clear": true } } Screenshot command:: { "command": "screenshot", "params": { "filename": "my_screenshot.png" } } """ command: str params: Dict[str, Any] = field(default_factory=dict)
[docs] @classmethod def from_dict(cls, data: Dict[str, Any]) -> "CommandMessage": """ Create command from dictionary. Parameters ---------- data : dict Command data with 'command' and optional 'params'. Returns ------- CommandMessage Parsed command message. """ return cls( command=data.get("command", ""), params=data.get("params", {}) )