Remove trailing spaces (#2)

Co-authored-by: Alexey Vazhnov <vazhnov@boot-keys.org>
Co-committed-by: Alexey Vazhnov <vazhnov@boot-keys.org>
This commit is contained in:
Alexey Vazhnov 2025-12-21 16:52:58 +00:00 committed by miklo
parent a939774852
commit 28d25381e2
2 changed files with 58 additions and 60 deletions

View File

@ -9,7 +9,7 @@ class Animation:
""" """
A class representing an ILDA animation consisting of multiple frames (SVG files). 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, It stores the default ILDA format (e.g. 0), company_name, and projector,
which are used when creating sections for each frame. which are used when creating sections for each frame.
""" """
def __init__(self, def __init__(self,

View File

@ -49,16 +49,16 @@ class Frame:
# Public instance state: only normalized ILDA-format points and colors plus original svg size # Public instance state: only normalized ILDA-format points and colors plus original svg size
points_list: List[List[PointI]] # each path -> list of (x,y,z) int16 points_list: List[List[PointI]] # each path -> list of (x,y,z) int16
colors_rgb: List[RGB] # per-path colors as 8-bit rgb colors_rgb: List[RGB] # per-path colors as 8-bit rgb
colors_indexed: List[int] # per-path palette index (0-255) colors_indexed: List[int] # per-path palette index (0-255)
svg_size: Tuple[float, float] # (width, height) of original SVG in user units (floats) svg_size: Tuple[float, float] # (width, height) of original SVG in user units (floats)
point_cnt: int # total points of all paths point_cnt: int # total points of all paths
point_cnt_simpl: int # total points of all paths simplified point_cnt_simpl: int # total points of all paths simplified
def __init__(self) -> None: def __init__(self) -> None:
self.points_list = [] self.points_list = []
self.colors_rgb = [] self.colors_rgb = []
self.colors_indexed = [] self.colors_indexed = []
self.svg_size = (0.0, 0.0) self.svg_size = (0.0, 0.0)
self.point_cnt = 0 self.point_cnt = 0
self.point_cnt_simpl = 0 self.point_cnt_simpl = 0
@ -76,11 +76,11 @@ class Frame:
self.points_list: list of paths; each path is list of (x,y,z) int16 with z=0 self.points_list: list of paths; each path is list of (x,y,z) int16 with z=0
self.colors_rgb: per-path (r,g,b) tuples self.colors_rgb: per-path (r,g,b) tuples
self.colors_indexed: per-path 8bit color index from DEFAULT_ilda_palette self.colors_indexed: per-path 8bit color index from DEFAULT_ilda_palette
self.svg_size: (width, height) from svg attributes (floats) self.svg_size: (width, height) from svg attributes (floats)
Notes: Notes:
the viewbox passed to _path_to_points is (xmin, ymin, xmax, ymax) derived from svg size. the viewbox passed to _path_to_points is (xmin, ymin, xmax, ymax) derived from svg size.
""" """
# Helper: compare two polylines with tolerance # Helper: compare two polylines with tolerance
def _poly_equal(a: List[Tuple[float, float]], b: List[Tuple[float, float]], eps: float = 1e-6) -> bool: def _poly_equal(a: List[Tuple[float, float]], b: List[Tuple[float, float]], eps: float = 1e-6) -> bool:
if len(a) != len(b): if len(a) != len(b):
@ -89,7 +89,7 @@ class Frame:
if abs(x1 - x2) > eps or abs(y1 - y2) > eps: if abs(x1 - x2) > eps or abs(y1 - y2) > eps:
return False return False
return True return True
# Helper: append one polyline (list of (x,y) floats) and its attributes to internal lists # Helper: append one polyline (list of (x,y) floats) and its attributes to internal lists
def _append_poly_and_attr(poly: List[Tuple[float, float]], attr: Dict[str, str]) -> None: def _append_poly_and_attr(poly: List[Tuple[float, float]], attr: Dict[str, str]) -> None:
# If poly too short, append empty placeholder and still record color info # If poly too short, append empty placeholder and still record color info
@ -101,13 +101,13 @@ class Frame:
idx = self._find_best_palette_index(rgb) idx = self._find_best_palette_index(rgb)
self.colors_indexed.append(idx) self.colors_indexed.append(idx)
return return
# Simplify polyline using class RDP if requested # Simplify polyline using class RDP if requested
if simplify: if simplify:
processed = self._rdp(poly, tol) processed = self._rdp(poly, tol)
else: else:
processed = poly processed = poly
if not processed or len(processed) < 2: if not processed or len(processed) < 2:
self.points_list.append([]) self.points_list.append([])
color = self._extract_stroke(attr) color = self._extract_stroke(attr)
@ -116,28 +116,28 @@ class Frame:
idx = self._find_best_palette_index(rgb) idx = self._find_best_palette_index(rgb)
self.colors_indexed.append(idx) self.colors_indexed.append(idx)
return return
# Normalize to ILDA coordinates (use svg_size width/height) # Normalize to ILDA coordinates (use svg_size width/height)
width, height = self.svg_size width, height = self.svg_size
ptsi = [self._normalize_point_to_ilda((x, y), width, height) for (x, y) in processed] ptsi = [self._normalize_point_to_ilda((x, y), width, height) for (x, y) in processed]
ptsi3 = [(int(x), int(y), 0) for (x, y) in ptsi] ptsi3 = [(int(x), int(y), 0) for (x, y) in ptsi]
self.points_list.append(ptsi3) self.points_list.append(ptsi3)
# Color handling (duplicate original attr for this fragment) # Color handling (duplicate original attr for this fragment)
color = self._extract_stroke(attr) color = self._extract_stroke(attr)
rgb = self._parse_color_to_rgb(color) rgb = self._parse_color_to_rgb(color)
self.colors_rgb.append(rgb) self.colors_rgb.append(rgb)
idx = self._find_best_palette_index(rgb) idx = self._find_best_palette_index(rgb)
self.colors_indexed.append(idx) self.colors_indexed.append(idx)
# Update simplified point counter # Update simplified point counter
self.point_cnt_simpl += len(processed) self.point_cnt_simpl += len(processed)
# ---- main body ---- # ---- main body ----
paths, attribs, svg_attrib = svg2paths2(infile) paths, attribs, svg_attrib = svg2paths2(infile)
width, height = self._extract_svg_size(svg_attrib) width, height = self._extract_svg_size(svg_attrib)
self.svg_size = (width, height) self.svg_size = (width, height)
# Reset internal lists and counters # Reset internal lists and counters
self.points_list = [] self.points_list = []
self.colors_rgb = [] self.colors_rgb = []
@ -145,11 +145,11 @@ class Frame:
sample_step = max(0.5, tol / 3.0) sample_step = max(0.5, tol / 3.0)
self.point_cnt = 0 self.point_cnt = 0
self.point_cnt_simpl = 0 self.point_cnt_simpl = 0
# Collect all polylines (after sampling+clipping) and their attributes # Collect all polylines (after sampling+clipping) and their attributes
all_polylines: List[List[Tuple[float, float]]] = [] all_polylines: List[List[Tuple[float, float]]] = []
all_attrs: List[Dict[str, str]] = [] all_attrs: List[Dict[str, str]] = []
for i, path_obj in enumerate(paths): for i, path_obj in enumerate(paths):
# Pass viewbox as (xmin, ymin, xmax, ymax) to _path_to_points # Pass viewbox as (xmin, ymin, xmax, ymax) to _path_to_points
xmin, ymin = 0.0, 0.0 xmin, ymin = 0.0, 0.0
@ -161,19 +161,19 @@ class Frame:
for part in ptsf_parts: for part in ptsf_parts:
all_polylines.append(part) all_polylines.append(part)
all_attrs.append(attr) all_attrs.append(attr)
# If nothing found, keep behavior: print and return # If nothing found, keep behavior: print and return
if not all_polylines: if not all_polylines:
print(f"Processing file: {infile} Points: {self.point_cnt} Points (simplified): {self.point_cnt_simpl}") print(f"Processing file: {infile} Points: {self.point_cnt} Points (simplified): {self.point_cnt_simpl}")
return return
# optional order polylines to minimize blank travel # optional order polylines to minimize blank travel
if mintravel: if mintravel:
ordered_result = order_paths_greedy_with_2opt(all_polylines, start_point=None, do_2opt=True) ordered_result = order_paths_greedy_with_2opt(all_polylines, start_point=None, do_2opt=True)
ordered_polylines = ordered_result.paths ordered_polylines = ordered_result.paths
else: else:
ordered_polylines = list(all_polylines) ordered_polylines = list(all_polylines)
# Map ordered polylines back to original attributes by matching geometry (or reversed) # Map ordered polylines back to original attributes by matching geometry (or reversed)
used_indices = set() used_indices = set()
for poly in ordered_polylines: for poly in ordered_polylines:
@ -191,7 +191,7 @@ class Frame:
# If no exact match found, use empty attr (rare) # If no exact match found, use empty attr (rare)
matched_attr = {} matched_attr = {}
_append_poly_and_attr(poly, matched_attr) _append_poly_and_attr(poly, matched_attr)
print(f"Processing file: {infile} Points: {self.point_cnt} Points (simplified): {self.point_cnt_simpl}") print(f"Processing file: {infile} Points: {self.point_cnt} Points (simplified): {self.point_cnt_simpl}")
@ -217,7 +217,7 @@ class Frame:
""" """
if format_code != 0: if format_code != 0:
raise NotImplementedError("Only Format 0 (3D Indexed) is implemented") raise NotImplementedError("Only Format 0 (3D Indexed) is implemented")
# helper creating a 32-byte header # helper creating a 32-byte header
def _make_header(fmt: int, name: str, company: str, num_records: int, def _make_header(fmt: int, name: str, company: str, num_records: int,
frame_number: int = 0, total_frames: int = 1, projector: int = 0) -> bytes: frame_number: int = 0, total_frames: int = 1, projector: int = 0) -> bytes:
@ -233,17 +233,17 @@ class Frame:
b[28:30] = total_frames.to_bytes(2, byteorder='big', signed=False) b[28:30] = total_frames.to_bytes(2, byteorder='big', signed=False)
b[30] = projector & 0xFF b[30] = projector & 0xFF
return bytes(b) return bytes(b)
# total number of records (each record = 8 bytes) # total number of records (each record = 8 bytes)
total_points = 0 total_points = 0
for path in self.points_list: for path in self.points_list:
total_points += max(0, len(path)) total_points += max(0, len(path))
if total_points == 0: if total_points == 0:
# Note: a header with num_records=0 is treated as EOF — we do not want to include this # Note: a header with num_records=0 is treated as EOF — we do not want to include this
# in the middle of the animation; we return an error so that Animation can skip this frame. # in the middle of the animation; we return an error so that Animation can skip this frame.
raise ValueError("Frame contains no points; get_ilda() would produce EOF header") raise ValueError("Frame contains no points; get_ilda() would produce EOF header")
records = bytearray() records = bytearray()
# determine the index of the last point in this frame -> set the LastPoint bit at the last point of this section # determine the index of the last point in this frame -> set the LastPoint bit at the last point of this section
last_path_idx = -1 last_path_idx = -1
@ -252,7 +252,7 @@ class Frame:
if path: if path:
last_path_idx = pi last_path_idx = pi
last_point_idx = len(path) - 1 last_point_idx = len(path) - 1
for pi, path in enumerate(self.points_list): for pi, path in enumerate(self.points_list):
if not path: if not path:
continue continue
@ -267,7 +267,7 @@ class Frame:
records += int(fy).to_bytes(2, byteorder='big', signed=True) records += int(fy).to_bytes(2, byteorder='big', signed=True)
records += int(fz).to_bytes(2, byteorder='big', signed=True) records += int(fz).to_bytes(2, byteorder='big', signed=True)
records += bytes([status, ci]) records += bytes([status, ci])
# subsequent points (draw), set LastPoint to the last point of this frame/section # subsequent points (draw), set LastPoint to the last point of this frame/section
for pti, (x, y, z) in enumerate(path): for pti, (x, y, z) in enumerate(path):
status = 0 status = 0
@ -280,11 +280,11 @@ class Frame:
records += int(y).to_bytes(2, byteorder='big', signed=True) records += int(y).to_bytes(2, byteorder='big', signed=True)
records += int(z).to_bytes(2, byteorder='big', signed=True) records += int(z).to_bytes(2, byteorder='big', signed=True)
records += bytes([status, ci]) records += bytes([status, ci])
header = _make_header(format_code, frame_name, company_name, len(records) // 8, header = _make_header(format_code, frame_name, company_name, len(records) // 8,
frame_number=frame_number, total_frames=total_frames, projector=projector) frame_number=frame_number, total_frames=total_frames, projector=projector)
return header + bytes(records) return header + bytes(records)
def write_ild(self, filename: str, format_code: int = 0, def write_ild(self, filename: str, format_code: int = 0,
frame_name: str = '00000001', company_name: str = 'MIKLO', frame_name: str = '00000001', company_name: str = 'MIKLO',
frame_number: int = 0, total_frames: int = 1, projector: int = 0) -> None: frame_number: int = 0, total_frames: int = 1, projector: int = 0) -> None:
@ -314,13 +314,13 @@ class Frame:
b[28:30] = total_frames.to_bytes(2, byteorder='big', signed=False) b[28:30] = total_frames.to_bytes(2, byteorder='big', signed=False)
b[30] = projector & 0xFF b[30] = projector & 0xFF
return bytes(b) return bytes(b)
eof_header = _make_eof_header(format_code, frame_name, company_name, frame_number=0, total_frames=0, projector=projector) eof_header = _make_eof_header(format_code, frame_name, company_name, frame_number=0, total_frames=0, projector=projector)
with open(filename, 'wb') as f: with open(filename, 'wb') as f:
f.write(section) f.write(section)
f.write(eof_header) f.write(eof_header)
def write_svg(self, outfile: str, canvas_size: Optional[Tuple[float, float]] = None) -> None: def write_svg(self, outfile: str, canvas_size: Optional[Tuple[float, float]] = None) -> None:
""" """
Build an SVG with paths from internal ILDA-normalized integer coordinates. Build an SVG with paths from internal ILDA-normalized integer coordinates.
@ -333,7 +333,7 @@ class Frame:
target_w, target_h = (ILDA_CANVAS, ILDA_CANVAS) target_w, target_h = (ILDA_CANVAS, ILDA_CANVAS)
else: else:
target_w, target_h = canvas_size target_w, target_h = canvas_size
svg_attrs = { svg_attrs = {
'xmlns': 'http://www.w3.org/2000/svg', 'xmlns': 'http://www.w3.org/2000/svg',
'width': str(target_w), 'width': str(target_w),
@ -341,10 +341,10 @@ class Frame:
'viewBox': f"0 0 {target_w} {target_h}" 'viewBox': f"0 0 {target_w} {target_h}"
} }
root = Element('svg', svg_attrs) root = Element('svg', svg_attrs)
scale_x = target_w / float(ILDA_CANVAS) scale_x = target_w / float(ILDA_CANVAS)
scale_y = target_h / float(ILDA_CANVAS) scale_y = target_h / float(ILDA_CANVAS)
for idx, pts in enumerate(self.points_list): for idx, pts in enumerate(self.points_list):
if not pts: if not pts:
continue continue
@ -363,12 +363,12 @@ class Frame:
el.set('stroke', f"rgb({rgb[0]},{rgb[1]},{rgb[2]})") el.set('stroke', f"rgb({rgb[0]},{rgb[1]},{rgb[2]})")
el.set('fill', 'none') el.set('fill', 'none')
el.set('stroke-width', '1.0') el.set('stroke-width', '1.0')
tree = ElementTree(root) tree = ElementTree(root)
tree.write(outfile, encoding='utf-8', xml_declaration=True) tree.write(outfile, encoding='utf-8', xml_declaration=True)
def _extract_svg_size(self, svg_attrib: Dict[str, str]) -> Tuple[float, float]: def _extract_svg_size(self, svg_attrib: Dict[str, str]) -> Tuple[float, float]:
vb = svg_attrib.get('viewBox') or svg_attrib.get('viewbox') vb = svg_attrib.get('viewBox') or svg_attrib.get('viewbox')
if vb: if vb:
@ -396,7 +396,7 @@ class Frame:
if w and h: if w and h:
return (w, h) return (w, h)
return (ILDA_CANVAS, ILDA_CANVAS) return (ILDA_CANVAS, ILDA_CANVAS)
def _extract_stroke(self, attr: Dict[str, str]) -> str: def _extract_stroke(self, attr: Dict[str, str]) -> str:
if 'stroke' in attr and attr['stroke'].strip(): if 'stroke' in attr and attr['stroke'].strip():
return attr['stroke'].strip() return attr['stroke'].strip()
@ -410,8 +410,8 @@ class Frame:
if k.strip() == 'stroke' and v.strip(): if k.strip() == 'stroke' and v.strip():
return v.strip() return v.strip()
return '' return ''
def _parse_color_to_rgb(self, color_str: str) -> RGB: def _parse_color_to_rgb(self, color_str: str) -> RGB:
if not color_str: if not color_str:
return (0, 0, 0) return (0, 0, 0)
@ -451,7 +451,7 @@ class Frame:
if lc in named: if lc in named:
return named[lc] return named[lc]
return (0, 0, 0) return (0, 0, 0)
def _path_to_points(self, path: Path, max_segment_length: float, viewbox: Optional[Tuple[float, float, float, float]] = None) -> List[List[PointF]]: def _path_to_points(self, path: Path, max_segment_length: float, viewbox: Optional[Tuple[float, float, float, float]] = None) -> List[List[PointF]]:
""" """
Sample an svgpathtools Path into one or more polylines (list of point lists). Sample an svgpathtools Path into one or more polylines (list of point lists).
@ -481,22 +481,22 @@ class Frame:
pts.append(endpt) pts.append(endpt)
except Exception: except Exception:
pass pass
# If no clipping requested, return single polyline as single-item list (or empty list if too short) # If no clipping requested, return single polyline as single-item list (or empty list if too short)
if not viewbox: if not viewbox:
return [pts] if len(pts) >= 2 else [] return [pts] if len(pts) >= 2 else []
# Convert to (xmin,ymin,xmax,ymax) exactly (read_svg should provide this form) # Convert to (xmin,ymin,xmax,ymax) exactly (read_svg should provide this form)
xmin, ymin, xmax, ymax = viewbox xmin, ymin, xmax, ymax = viewbox
if len(pts) < 2: if len(pts) < 2:
return [] return []
pieces = self._clip_polyline_to_rect(pts, (xmin, ymin, xmax, ymax)) pieces = self._clip_polyline_to_rect(pts, (xmin, ymin, xmax, ymax))
return pieces return pieces
def _sample_segment(self, seg: Any, max_segment_length: float) -> List[PointF]: def _sample_segment(self, seg: Any, max_segment_length: float) -> List[PointF]:
try: try:
seg_len = seg.length(error=1e-5) seg_len = seg.length(error=1e-5)
@ -514,8 +514,8 @@ class Frame:
c = seg.point(t) c = seg.point(t)
pts.append((c.real, c.imag)) pts.append((c.real, c.imag))
return pts return pts
def _rdp(self, points: List[PointF], eps: float) -> List[PointF]: def _rdp(self, points: List[PointF], eps: float) -> List[PointF]:
if len(points) < 3: if len(points) < 3:
return points[:] return points[:]
@ -541,8 +541,8 @@ class Frame:
return left[:-1] + right return left[:-1] + right
else: else:
return [points[0], points[-1]] return [points[0], points[-1]]
def _normalize_point_to_ilda(self, p: PointF, svg_w: float, svg_h: float) -> Tuple[int, int]: def _normalize_point_to_ilda(self, p: PointF, svg_w: float, svg_h: float) -> Tuple[int, int]:
x, y = p x, y = p
if svg_w <= 0 or svg_h <= 0: if svg_w <= 0 or svg_h <= 0:
@ -554,8 +554,8 @@ class Frame:
ix = max(INT16_MIN, min(INT16_MAX, ix)) ix = max(INT16_MIN, min(INT16_MAX, ix))
iy = max(INT16_MIN, min(INT16_MAX, iy)) iy = max(INT16_MIN, min(INT16_MAX, iy))
return (ix, iy) return (ix, iy)
def _ilda_to_canvas_float(self, p3: PointI) -> Tuple[float, float]: def _ilda_to_canvas_float(self, p3: PointI) -> Tuple[float, float]:
x_int, y_int, _ = p3 x_int, y_int, _ = p3
nx = (x_int / ILDA_CANVAS) + 0.5 nx = (x_int / ILDA_CANVAS) + 0.5
@ -563,8 +563,8 @@ class Frame:
fx = nx * ILDA_CANVAS fx = nx * ILDA_CANVAS
fy = ny * ILDA_CANVAS fy = ny * ILDA_CANVAS
return (float(fx), float(fy)) return (float(fx), float(fy))
def _points_to_path_d(self, points: List[Tuple[float, float]], closed: bool) -> str: def _points_to_path_d(self, points: List[Tuple[float, float]], closed: bool) -> str:
if not points: if not points:
return "" return ""
@ -594,8 +594,8 @@ class Frame:
best_idx = idx best_idx = idx
# Ensure 0-255 range # Ensure 0-255 range
return max(0, min(255, int(best_idx))) return max(0, min(255, int(best_idx)))
def _clip_polyline_to_rect(self, def _clip_polyline_to_rect(self,
poly: List[PointF], poly: List[PointF],
rect: Tuple[float, float, float, float], rect: Tuple[float, float, float, float],
@ -611,7 +611,7 @@ class Frame:
xmin, ymin, xmax, ymax = rect xmin, ymin, xmax, ymax = rect
if not poly or xmin >= xmax or ymin >= ymax: if not poly or xmin >= xmax or ymin >= ymax:
return [] return []
if LineString is not None: if LineString is not None:
# robust, uses geometric intersection # robust, uses geometric intersection
ls = LineString(poly) ls = LineString(poly)
@ -633,5 +633,3 @@ class Frame:
# ignore points etc. # ignore points etc.
_extract_lines(inter) _extract_lines(inter)
return parts return parts