"""
Viewer Configuration
====================
Configuration dataclass for the VTK viewer and API server.
Example Usage
-------------
Basic configuration::
from yapcad.viewer import ViewerConfig
config = ViewerConfig(
stl_dir="/path/to/stl/files",
positions_file="/path/to/positions.json",
window_size=(1600, 1000),
background_color=(0.12, 0.12, 0.15)
)
Configuration with file-based commands (backward compatibility)::
config = ViewerConfig(
stl_dir="/path/to/stl/files",
command_file="/path/to/viewer_cmd.txt",
screenshot_dir="/path/to/screenshots"
)
"""
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional, Tuple, Dict, Any
[docs]
@dataclass
class ViewerConfig:
"""
Configuration for the VTK assembly viewer.
This dataclass holds all configuration options for the viewer including
file paths, window settings, rendering options, and color schemes.
Parameters
----------
stl_dir : str or Path
Directory containing STL files to load.
positions_file : str or Path, optional
JSON file containing part positions and transforms.
Expected format::
{
"PART_NAME": {
"transform_matrix": [[...], [...], [...], [...]],
"stl_file": "optional/path/to/file.stl",
"color": [r, g, b] # optional, 0-1 range
},
...
}
command_file : str or Path, optional
Path to file-based command interface for backward compatibility.
Commands are read from this file and executed.
screenshot_dir : str or Path, optional
Directory for saving screenshots. Defaults to stl_dir/renders.
window_size : tuple of int, optional
Window dimensions (width, height). Default: (1600, 1000).
window_title : str, optional
Window title. Default: "yapCAD Assembly Viewer".
background_color : tuple of float, optional
Background color as RGB (0-1 range). Default: (0.12, 0.12, 0.15).
default_opacity : float, optional
Default opacity for parts. Default: 1.0.
xray_opacity : float, optional
Opacity when x-ray mode is enabled. Default: 0.4.
default_color : tuple of float, optional
Default part color as RGB (0-1 range). Default: (0.4, 0.4, 0.4).
color_palette : dict, optional
Named color palette for automatic color assignment.
Keys are color names, values are RGB tuples (0-1 range).
enable_multi_viewport : bool, optional
Enable 4-viewport mode (ISO, TOP, FRONT, SIDE). Default: True.
command_poll_interval_ms : int, optional
Interval in milliseconds for polling command file. Default: 100.
Attributes
----------
stl_dir : Path
Resolved STL directory path.
positions_file : Path or None
Resolved positions file path.
command_file : Path or None
Resolved command file path.
screenshot_dir : Path
Resolved screenshot directory path.
Examples
--------
Minimal configuration::
config = ViewerConfig(stl_dir="/my/stls")
Full configuration::
config = ViewerConfig(
stl_dir="/my/stls",
positions_file="/my/positions.json",
window_size=(1920, 1080),
background_color=(0.1, 0.1, 0.12),
color_palette={
"primary": (0.2, 0.4, 0.8),
"secondary": (0.8, 0.4, 0.2),
"highlight": (1.0, 0.9, 0.2)
}
)
"""
stl_dir: str
positions_file: Optional[str] = None
command_file: Optional[str] = None
screenshot_dir: Optional[str] = None
# Window settings
window_size: Tuple[int, int] = (1600, 1000)
window_title: str = "yapCAD Assembly Viewer"
background_color: Tuple[float, float, float] = (0.12, 0.12, 0.15)
# Rendering settings
default_opacity: float = 1.0
xray_opacity: float = 0.4
default_color: Tuple[float, float, float] = (0.4, 0.4, 0.4)
# Color palette for automatic assignment
color_palette: Dict[str, Tuple[float, float, float]] = field(
default_factory=lambda: {
"gray": (0.4, 0.4, 0.4),
"red": (0.8, 0.2, 0.2),
"green": (0.2, 0.8, 0.2),
"blue": (0.2, 0.4, 0.8),
"yellow": (0.8, 0.8, 0.2),
"orange": (0.8, 0.5, 0.2),
"purple": (0.6, 0.2, 0.8),
"cyan": (0.2, 0.7, 0.8),
"brown": (0.5, 0.3, 0.15),
"dark": (0.15, 0.15, 0.15),
}
)
# Viewport settings
enable_multi_viewport: bool = True
# Command file polling
command_poll_interval_ms: int = 100
def __post_init__(self):
"""Validate and convert paths."""
self._stl_dir = Path(self.stl_dir)
self._positions_file = Path(self.positions_file) if self.positions_file else None
self._command_file = Path(self.command_file) if self.command_file else None
if self.screenshot_dir:
self._screenshot_dir = Path(self.screenshot_dir)
else:
self._screenshot_dir = self._stl_dir / "renders"
@property
def stl_path(self) -> Path:
"""Get the resolved STL directory path."""
return self._stl_dir
@property
def positions_path(self) -> Optional[Path]:
"""Get the resolved positions file path."""
return self._positions_file
@property
def command_path(self) -> Optional[Path]:
"""Get the resolved command file path."""
return self._command_file
@property
def screenshot_path(self) -> Path:
"""Get the resolved screenshot directory path."""
return self._screenshot_dir
[docs]
def get_color(self, name: str) -> Tuple[float, float, float]:
"""
Get a color from the palette by name.
Parameters
----------
name : str
Color name (case-insensitive).
Returns
-------
tuple of float
RGB color tuple (0-1 range).
Examples
--------
>>> config = ViewerConfig(stl_dir="/tmp")
>>> config.get_color("blue")
(0.2, 0.4, 0.8)
>>> config.get_color("unknown") # Returns default
(0.4, 0.4, 0.4)
"""
return self.color_palette.get(name.lower(), self.default_color)
[docs]
def to_dict(self) -> Dict[str, Any]:
"""
Convert configuration to dictionary.
Returns
-------
dict
Configuration as a dictionary suitable for JSON serialization.
"""
return {
"stl_dir": str(self.stl_dir),
"positions_file": str(self.positions_file) if self.positions_file else None,
"command_file": str(self.command_file) if self.command_file else None,
"screenshot_dir": str(self._screenshot_dir),
"window_size": list(self.window_size),
"window_title": self.window_title,
"background_color": list(self.background_color),
"default_opacity": self.default_opacity,
"xray_opacity": self.xray_opacity,
"default_color": list(self.default_color),
"color_palette": {k: list(v) for k, v in self.color_palette.items()},
"enable_multi_viewport": self.enable_multi_viewport,
"command_poll_interval_ms": self.command_poll_interval_ms,
}
[docs]
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "ViewerConfig":
"""
Create configuration from dictionary.
Parameters
----------
data : dict
Configuration dictionary.
Returns
-------
ViewerConfig
New configuration instance.
"""
# Convert lists back to tuples where needed
if "window_size" in data:
data["window_size"] = tuple(data["window_size"])
if "background_color" in data:
data["background_color"] = tuple(data["background_color"])
if "default_color" in data:
data["default_color"] = tuple(data["default_color"])
if "color_palette" in data:
data["color_palette"] = {
k: tuple(v) for k, v in data["color_palette"].items()
}
return cls(**data)