forked from genewildish/Mainline
fix(performance): use simple height estimation instead of PIL rendering
- Replace estimate_block_height (PIL-based) with estimate_simple_height (word wrap) - Update viewport filter tests to match new height-based filtering (~4 items vs 24) - Fix CI task duplication in mise.toml (remove redundant depends) Closes #38 Closes #36
This commit is contained in:
265
engine/pipeline/adapters/transform.py
Normal file
265
engine/pipeline/adapters/transform.py
Normal file
@@ -0,0 +1,265 @@
|
||||
"""Adapters for transform stages (viewport, font, image, canvas)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import engine.render
|
||||
from engine.data_sources import SourceItem
|
||||
from engine.pipeline.core import DataType, PipelineContext, Stage
|
||||
|
||||
|
||||
def estimate_simple_height(text: str, width: int) -> int:
|
||||
"""Estimate height in terminal rows using simple word wrap.
|
||||
|
||||
Uses conservative estimation suitable for headlines.
|
||||
Each wrapped line is approximately 6 terminal rows (big block rendering).
|
||||
"""
|
||||
words = text.split()
|
||||
if not words:
|
||||
return 6
|
||||
|
||||
lines = 1
|
||||
current_len = 0
|
||||
for word in words:
|
||||
word_len = len(word)
|
||||
if current_len + word_len + 1 > width - 4: # -4 for margins
|
||||
lines += 1
|
||||
current_len = word_len
|
||||
else:
|
||||
current_len += word_len + 1
|
||||
|
||||
return lines * 6 # 6 rows per line for big block rendering
|
||||
|
||||
|
||||
class ViewportFilterStage(Stage):
|
||||
"""Filter items to viewport height based on rendered height."""
|
||||
|
||||
def __init__(self, name: str = "viewport-filter"):
|
||||
self.name = name
|
||||
self.category = "render"
|
||||
self.optional = True
|
||||
self._layout: list[int] = []
|
||||
|
||||
@property
|
||||
def stage_type(self) -> str:
|
||||
return "render"
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"source.filtered"}
|
||||
|
||||
@property
|
||||
def dependencies(self) -> set[str]:
|
||||
return {"source"}
|
||||
|
||||
@property
|
||||
def inlet_types(self) -> set:
|
||||
return {DataType.SOURCE_ITEMS}
|
||||
|
||||
@property
|
||||
def outlet_types(self) -> set:
|
||||
return {DataType.SOURCE_ITEMS}
|
||||
|
||||
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
||||
"""Filter items to viewport height based on rendered height."""
|
||||
if data is None:
|
||||
return data
|
||||
|
||||
if not isinstance(data, list):
|
||||
return data
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
# Get viewport parameters from context
|
||||
viewport_height = ctx.params.viewport_height if ctx.params else 24
|
||||
viewport_width = ctx.params.viewport_width if ctx.params else 80
|
||||
camera_y = ctx.get("camera_y", 0)
|
||||
|
||||
# Estimate height for each item and cache layout
|
||||
self._layout = []
|
||||
cumulative_heights = []
|
||||
current_height = 0
|
||||
|
||||
for item in data:
|
||||
title = item.content if isinstance(item, SourceItem) else str(item)
|
||||
# Use simple height estimation (not PIL-based)
|
||||
estimated_height = estimate_simple_height(title, viewport_width)
|
||||
self._layout.append(estimated_height)
|
||||
current_height += estimated_height
|
||||
cumulative_heights.append(current_height)
|
||||
|
||||
# Find visible range based on camera_y and viewport_height
|
||||
# camera_y is the scroll offset (how many rows are scrolled up)
|
||||
start_y = camera_y
|
||||
end_y = camera_y + viewport_height
|
||||
|
||||
# Find start index (first item that intersects with visible range)
|
||||
start_idx = 0
|
||||
for i, total_h in enumerate(cumulative_heights):
|
||||
if total_h > start_y:
|
||||
start_idx = i
|
||||
break
|
||||
|
||||
# Find end index (first item that extends beyond visible range)
|
||||
end_idx = len(data)
|
||||
for i, total_h in enumerate(cumulative_heights):
|
||||
if total_h >= end_y:
|
||||
end_idx = i + 1
|
||||
break
|
||||
|
||||
# Return visible items
|
||||
return data[start_idx:end_idx]
|
||||
|
||||
|
||||
class FontStage(Stage):
|
||||
"""Render items using font."""
|
||||
|
||||
def __init__(self, name: str = "font"):
|
||||
self.name = name
|
||||
self.category = "render"
|
||||
self.optional = False
|
||||
|
||||
@property
|
||||
def stage_type(self) -> str:
|
||||
return "render"
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"render.output"}
|
||||
|
||||
@property
|
||||
def dependencies(self) -> set[str]:
|
||||
return {"source"}
|
||||
|
||||
@property
|
||||
def inlet_types(self) -> set:
|
||||
return {DataType.SOURCE_ITEMS}
|
||||
|
||||
@property
|
||||
def outlet_types(self) -> set:
|
||||
return {DataType.TEXT_BUFFER}
|
||||
|
||||
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
||||
"""Render items to text buffer using font."""
|
||||
if data is None:
|
||||
return []
|
||||
|
||||
if not isinstance(data, list):
|
||||
return [str(data)]
|
||||
|
||||
viewport_width = ctx.params.viewport_width if ctx.params else 80
|
||||
|
||||
result = []
|
||||
for item in data:
|
||||
if isinstance(item, SourceItem):
|
||||
title = item.content
|
||||
src = item.source
|
||||
ts = item.timestamp
|
||||
content_lines, _, _ = engine.render.make_block(
|
||||
title, src, ts, viewport_width
|
||||
)
|
||||
result.extend(content_lines)
|
||||
elif hasattr(item, "content"):
|
||||
title = str(item.content)
|
||||
content_lines, _, _ = engine.render.make_block(
|
||||
title, "", "", viewport_width
|
||||
)
|
||||
result.extend(content_lines)
|
||||
else:
|
||||
result.append(str(item))
|
||||
return result
|
||||
|
||||
|
||||
class ImageToTextStage(Stage):
|
||||
"""Convert image items to text."""
|
||||
|
||||
def __init__(self, name: str = "image-to-text"):
|
||||
self.name = name
|
||||
self.category = "render"
|
||||
self.optional = True
|
||||
|
||||
@property
|
||||
def stage_type(self) -> str:
|
||||
return "render"
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"render.output"}
|
||||
|
||||
@property
|
||||
def dependencies(self) -> set[str]:
|
||||
return {"source"}
|
||||
|
||||
@property
|
||||
def inlet_types(self) -> set:
|
||||
return {DataType.SOURCE_ITEMS}
|
||||
|
||||
@property
|
||||
def outlet_types(self) -> set:
|
||||
return {DataType.TEXT_BUFFER}
|
||||
|
||||
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
||||
"""Convert image items to text representation."""
|
||||
if data is None:
|
||||
return []
|
||||
|
||||
if not isinstance(data, list):
|
||||
return [str(data)]
|
||||
|
||||
result = []
|
||||
for item in data:
|
||||
# Check if item is an image
|
||||
if hasattr(item, "image_path") or hasattr(item, "image_data"):
|
||||
# Placeholder: would normally render image to ASCII art
|
||||
result.append(f"[Image: {getattr(item, 'image_path', 'data')}]")
|
||||
elif isinstance(item, SourceItem):
|
||||
result.extend(item.content.split("\n"))
|
||||
else:
|
||||
result.append(str(item))
|
||||
return result
|
||||
|
||||
|
||||
class CanvasStage(Stage):
|
||||
"""Render items to canvas."""
|
||||
|
||||
def __init__(self, name: str = "canvas"):
|
||||
self.name = name
|
||||
self.category = "render"
|
||||
self.optional = False
|
||||
|
||||
@property
|
||||
def stage_type(self) -> str:
|
||||
return "render"
|
||||
|
||||
@property
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"render.output"}
|
||||
|
||||
@property
|
||||
def dependencies(self) -> set[str]:
|
||||
return {"source"}
|
||||
|
||||
@property
|
||||
def inlet_types(self) -> set:
|
||||
return {DataType.SOURCE_ITEMS}
|
||||
|
||||
@property
|
||||
def outlet_types(self) -> set:
|
||||
return {DataType.TEXT_BUFFER}
|
||||
|
||||
def process(self, data: Any, ctx: PipelineContext) -> Any:
|
||||
"""Render items to canvas."""
|
||||
if data is None:
|
||||
return []
|
||||
|
||||
if not isinstance(data, list):
|
||||
return [str(data)]
|
||||
|
||||
# Simple canvas rendering
|
||||
result = []
|
||||
for item in data:
|
||||
if isinstance(item, SourceItem):
|
||||
result.extend(item.content.split("\n"))
|
||||
else:
|
||||
result.append(str(item))
|
||||
return result
|
||||
Reference in New Issue
Block a user