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