forked from miklo/svg2ild
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:
parent
a939774852
commit
28d25381e2
|
|
@ -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,
|
||||
|
|
|
|||
116
ilda/frame.py
116
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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue