svg2ild/ilda/animation.py

139 lines
5.1 KiB
Python

# ilda/animation.py
from pathlib import Path
from typing import List, Tuple, Optional
from .frame import Frame
class Animation:
"""
A class representing an ILDA animation consisting of multiple frames (SVG files).
It stores the default ILDA format (e.g. 0), company_name, and projector,
which are used when creating sections for each frame.
"""
def __init__(self,
ilda_format: int = 0,
company_name: str = "MIKLOBIT",
projector: int = 0) -> None:
self.ilda_format: int = ilda_format
self.company_name: str = company_name
self.projector: int = projector
self.frames: List[Frame] = []
self.frame_names: List[str] = []
def set_ilda_format(self, fmt: int) -> None:
self.ilda_format = fmt
def set_company_name(self, name: str) -> None:
self.company_name = name
def set_projector(self, projector: int) -> None:
self.projector = projector
def create_from_file(self,
svg_path: str,
simplify: bool = False,
tol: float = 1.0,
clear_existing: bool = True) -> None:
"""
Load a single SVG file and add a Frame object to the animation.
The frame name is the file name (stem) truncated to 8 characters.
The simplify and tol parameters are passed to Frame.read_svg.
"""
p = Path(svg_path)
if clear_existing:
self.frames = []
self.frame_names = []
if not p.exists():
raise FileNotFoundError(f"SVG file not found: {svg_path}")
frame = Frame()
frame.read_svg(str(p), simplify=simplify, tol=tol)
name8 = p.stem[:8]
self.frames.append(frame)
self.frame_names.append(name8)
def create_from_folder(self,
folder_path: str,
simplify: bool = False,
tol: float = 1.0,
recursive: bool = False,
clear_existing: bool = True) -> None:
"""
Load all .svg files from the folder (alphabetically) and create Frame objects.
If recursive=True, it searches subdirectories.
"""
p = Path(folder_path)
if not p.exists() or not p.is_dir():
raise NotADirectoryError(f"Folder not found: {folder_path}")
if clear_existing:
self.frames = []
self.frame_names = []
if recursive:
glob_iter = p.rglob("*.svg")
else:
glob_iter = p.glob("*.svg")
files = sorted(list(glob_iter), key=lambda x: x.name.lower())
for fp in files:
try:
self.create_from_file(str(fp), simplify=simplify, tol=tol, clear_existing=False)
except Exception as e:
print(f"Warning: failed to read SVG {fp}: {e}")
def _make_eof_header(self) -> bytes:
"""
Returns a 32-byte ILDA header indicating EOF (NumberOfRecords = 0).
Uses the ilda_format/company_name/projector settings from the instance.
"""
b = bytearray(32)
b[0:4] = b'ILDA'
b[7] = int(self.ilda_format) & 0xFF
name_enc = self.company_name.encode('ascii', errors='ignore')[:8]
b[16:16+len(name_enc)] = name_enc # company name field (optional)
b[24:26] = (0).to_bytes(2, byteorder='big', signed=False) # NumberOfRecords = 0 -> EOF
b[30] = int(self.projector) & 0xFF
return bytes(b)
def write_ild(self, out_path: str, overwrite: bool = True) -> None:
"""
Save the compiled .ilda file: concatenate all sections obtained from Frame.get_ilda
and add a single EOF header.
- Skip frames for which get_ilda() will throw a ValueError (e.g., no points).
- FrameNumber and TotalFrames are set automatically.
"""
if not self.frames:
raise RuntimeError("No frames to write. Use create_from_* first.")
outp = Path(out_path)
if outp.exists() and not overwrite:
raise FileExistsError(f"File exists: {out_path}")
total_frames = len(self.frames)
bin_stream = bytearray()
for idx, frame in enumerate(self.frames):
frame_name = self.frame_names[idx] if idx < len(self.frame_names) else f"{idx:08d}"
try:
section = frame.get_ilda(format_code=self.ilda_format,
frame_name=frame_name,
company_name=self.company_name,
frame_number=idx,
total_frames=total_frames,
projector=self.projector)
except ValueError as ve:
# return ValueError if frame empty (--> header of 0 records = EOF)
print(f"Info: skipping empty frame '{frame_name}': {ve}")
continue
bin_stream += section
bin_stream += self._make_eof_header()
with open(outp, 'wb') as f:
f.write(bin_stream)