def dirty_set(self, rect=None): tick = get_current_tick() if rect is None: rect = Rect((0, 0), self.size) else: rect = Rect(rect) if not isinstance(rect, Rect) else rect self.dirty_registry.reset_to((tick, rect, None))
def dirty_update(self): tick = get_current_tick() # If there is any time-dependant image change, there is no way # to predict what changes from one frame to the next - just mark # all shape as dirty. if any("tick" in transformer.signatures for transformer in self.context.transformers): self.dirty_set() return # Collect rects from sprites if self.has_sprites: for sprite in self.sprites: if not sprite.active: continue for rect in sprite.dirty_rects: self.dirty_registry.push( (tick, sprite.owner_coords(rect), sprite.shape)) # mark dirty pixels tile_size = (DIRTY_TILE_SIZE, DIRTY_TILE_SIZE) self_rect = Rect((0, 0), self.size) for tile in self.dirty_pixels: rect = Rect(tile * DIRTY_TILE_SIZE, width_height=tile_size) rect = rect.intersection(self_rect) if not rect: continue self.dirty_registry.push((tick, rect, None)) self.dirty_pixels = set()
def rect(self): r = Rect(self.shape.size) if self.anchor == "topleft": r.left = self.pos.x r.top = self.pos.y elif self.anchor == "center": r.center = self.pos return r
def update(self, pos1=None, pos2=None): rect = Rect(pos1, pos2) if rect.c2 == (0, 0): rect.c2 = (self.width, self.height) with self.commands: for y in range(rect.top, rect.bottom): for x in range(rect.left, rect.right): self[x, y] = _REPLAY
def rect(self, pos1, pos2=(), *, rel=(), erase=False): """Draws a rectangle Args: - pos1 (Union[Rectangle, 2-tuple]): top-left coordinates - pos2 (Optional[2-tuple]): bottom-right limit coordinates. If not given, pass "rel" instead - rel (Optional[2-tuple]): (width, height) of rectangle. Ignored if "pos2" is given - fill (bool): Whether fill-in the rectangle, or only draw the outline. Defaults to False. - erase (bool): Whether to draw (set) or erase (reset) pixels. Public call to draw a rectangle using character blocks on the terminal. The color line is defined in the owner's context.color attribute. In the case of high-resolution drawing, the background color is also taken from the context. """ pos1, pos2 = Rect(pos1, pos2, width_height=rel) # Ending interval is open, just as Python works with intervals. pos2 -= (1, 1) fill = self.context.fill x1, y1 = pos1 x2, y2 = pos2 self.line(pos1, (x2, y1), erase=erase) self.line((x1, y2), pos2, erase=erase) if (fill or erase) and y2 != y1: direction = int((y2 - y1) / abs(y2 - y1)) for y in range(y1 + 1, y2, direction): self.line((x1, y), (x2, y), erase=erase) else: self.line(pos1, (x1, y2)) self.line((x2, y1), pos2)
def update(self, pos1=None, pos2=None): """Main method to update the display An interactive application or animation should call this once per frame to have th display contents updated on the terminal. It can optionally update just a part of the output screen, if pos1 or pos2 are given. As of pre-0.4.0 development an app should manually provide its "mainloop" and call this on each frame. Later development will probably have an optional higher level loop that will automate calling here. Args: - pos1, pos2: Corners of a rectangle delimitting the area to be updated. (optionally, 'pos1' can be a Rect object) """ tick_forward() if self.interactive and terminedia.input.keyboard.enabled and not self._inkey_called_since_last_update: # Ensure the dispatch of keypress events: terminedia.inkey(consume=False) self._inkey_called_since_last_update = False self.process_events() rect = Rect(pos1, pos2) if rect.c2 == (0, 0) and pos2 is None: rect.c2 = (self.width, self.height) if hasattr(self.commands, "fast_render") and self.root_context.fast_render: target = [ rect ] if pos1 is not None or self.root_context.interactive_mode else self.data.dirty_rects self.commands.fast_render(self.data, target) self.data.dirty_clear() else: with self.commands: for y in range(rect.top, rect.bottom): for x in range(rect.left, rect.right): self[x, y] = _REPLAY if self.root_context.interactive_mode: # move cursor a couple lines from the bottom to avoid scrolling for i in range(3): self.commands.up()
def new_line_start(self, index, direction): pos = V2(index) direction = V2(direction) r = Rect(self.text_plane.size ) while True: pos, direction, flow_changed, position_is_used = self.move_along_marks(pos, direction) if flow_changed: return pos, direction if not pos in r: return pos, direction
def __getitem__(self, index): roi = self.roi if isinstance(index, Rect): return ShapeView( self.original, Rect(V2.max(roi.c1, (roi.c1 + index.c1)), V2.min((roi.c1 + index.c2), roi.c2))) if not 0 <= index[0] < roi.width or not 0 <= index[1] < roi.bottom: raise IndexError(f"Value out of limits f{roi.width_height}") return self.original[roi.c1 + index]
def update(self, pos1=None, pos2=None): rect = Rect(pos1, pos2) if rect.c2 == (0, 0) and pos2 is None: rect.c2 = (self.width, self.height) if hasattr(self.commands, "fast_render") and self.root_context.fast_render: target = [ rect ] if pos1 is not None or self.root_context.interactive_mode else self.data.dirty_rects self.commands.fast_render(self.data, target) self.data.dirty_clear() else: with self.commands: for y in range(rect.top, rect.bottom): for x in range(rect.left, rect.right): self[x, y] = _REPLAY tick_forward() if self.root_context.interactive_mode: # move cursor a couple lines from the bottom to avoid scrolling for i in range(3): self.commands.up()
def __getitem__(self, pos): """Values for each pixel are: character, fg_color, bg_color, effects. """ if isinstance(pos, Rect): roi = pos elif isinstance(pos, tuple) and isinstance(pos[0], slice): if any(pos[i].step not in (None, 1) for i in (0, 1)): raise NotImplementedError( "Slice stepping not implemented for shapes") roi = Rect(*pos) else: return None return ShapeView(self, roi)
def refresh(self, clear=True, *, preserve_attrs=False, rect=None, target=None): """Render entire text buffer to the owner shape Args: - clear (bool): whether to render empty spaces. Default=True - preserve_attrs: whether to keep colors and effects on the rendered cells, or replace all attributes with those in the current context - rect (Optional[Rect]): area to render. Defaults to whole text plane - target (Optional[Shape]): where to render to. Defaults to owner shape. """ if "current_plane" not in self.__dict__: raise TypeError( "You must select a text plane to render - use .text[<size>].refresh()" ) if target is None: target = self.owner data = self.plane if not rect: rect = Rect((0, 0), data.size) elif not isinstance(rect, Rect): rect = Rect(rect) with target.context as context: if preserve_attrs: context.color = TRANSPARENT context.background = TRANSPARENT context.effects = TRANSPARENT for pos in rect.iter_cells(): self.blit(pos, target=target, clear=clear)
def __getitem__(self, pos): """Common logic to create ShapeViews from slices. Pixel data retrieving is implemented in the subclasses. """ if isinstance(pos, Rect): roi = pos elif isinstance(pos, tuple) and isinstance(pos[0], slice): if any(pos[i].step not in (None, 1) for i in (0, 1)): raise NotImplementedError( "Slice stepping not implemented for shapes") roi = Rect(*pos) else: return None return ShapeView(self, roi)
def ellipse(self, pos1, pos2=(), *, rel=()): """Draws an ellipse Args: - pos1 (Union[Rectangle, 2-tuple]): top-left coordinates - pos2 (Optional[2-tuple]): bottom-right limit coordinates. If not given, pass "rel" instead - rel (Optional[2-tuple]): (width, height) of rectangle. Ignored if "pos2" is given Public call to draw an ellipse using character blocks on the terminal. The color line is defined in the owner's context.color attribute. In the case of high-resolution drawing, the background color is also taken from the context. """ pos1, pos2 = Rect(pos1, pos2, width_height=rel) pos2 -= (1, 1) return (self._empty_ellipse(pos1, pos2) if not self.context.fill else self._filled_ellipse(pos1, pos2))
def floodfill(self, pos, threshold=None): """Fills the associated target with context values, starting at seed "pos". Any different parameters for target pixels are considered boundaries "threshold" may be passed a guard callable which will take each target pixel, and should return "False" if it should be painted and "True" if it is a boundary. Args: - pos (V2|Sequence[2]): initial position - threshold (Optional[Callable[Pixel, Pixel, Pos]): callable - takes initial value at origin, and target pixel value. Should return 'True' for boundary pixels, False if pixel is to be painted. The context attributes are used to fill the target area. """ seed = self.get(pos) if threshold is None: threshold = lambda seed, target, pos: seed != target rect = Rect(self.size) fillable = set() visited = set() to_check = { pos, } while to_check: pos = to_check.pop() if pos in visited: continue visited.add(pos) if threshold(seed, self.get(pos), pos): continue fillable.add(pos) for direction in Directions: target = pos + direction if target in rect: to_check.add(target) for pos in fillable: self.set(pos)
def owner_coords(self, rect, where=None): if not isinstance(rect, Rect): rect = Rect(rect) if not where: where = self.rect return Rect(where.c1 + rect.c1, width=rect.width, height=rect.height)
def test_rect_constructor_with_expected_result(args, kwargs, expected): r = Rect(*args, **kwargs) assert r == Rect(*expected)
def test_rect_constructor(args, kwargs): r = Rect(*args, **kwargs) assert r == Rect((10, 10), (20, 20))
a1.b = 5 assert flag1 and not flag2 and flaguniversal flaguniversal = False flag1 = False b2.b = 23 assert not flag1 and flag2 and flaguniversal @pytest.mark.parametrize(["args", "kwargs"], [ [[(10, 10), (20, 20)], {}], [[[10, 10], [20, 20]], {}], [[V2(10, 10), V2(20, 20)], {}], [[[10, 10, 20, 20]], {}], [[(10, 10, 20, 20)], {}], [[10, 10, 20, 20], {}], [Rect((10, 10), (20, 20)), {}], [[Rect((10, 10), (20, 20))], {}], [[], { "left_or_corner1": (10, 10), "top_or_corner2": (20, 20) }], [[(10, 10)], { "width_height": (10, 10) }], [[(10, 10)], { "width": 10, "height": 10 }], [[(10, 10)], { "right": 20, "bottom": 20
def __init__(self, original, roi): self.original = original self.roi = Rect(roi)
def blit(self, pos, data, *, roi=None, color_map=None, erase=False): """Blits a blocky image in the associated screen at POS Args: - pos (Union[2-sequence, Rectangle]): top-left corner of where to blit, or a rectangle with extents to be blitted. - shape (Shape/string/list): Shape object or multi-line string or list of strings with shape to be drawn - roi (Optional[Rect]): (Region of interest) delimiting rectangle in source image(data) to be blitted. Defaults to whole image. - color_map (Optional mapping): palette mapping chracters in shape to a color - erase (bool): if True white-spaces are erased, instead of being ignored. Default is False. FIXME: under the Pixel model, erase is always True. Shapes return specialized Pixel classes when iterated upon - what is set on the screen depends on the Pixel returned. As of version 0.3dev, Shape class returns a pixel that has a True or False value and a foreground color (or no color) - support for other Pixel capabilities is not yet implemented. """ from terminedia.image import Shape, PalettedShape, SKIP_LINE if not hasattr(self.context, "color_stack"): self.context.color_stack = [] if not hasattr(self.context, "background_stack"): self.context.background_stack = [] self.context.color_stack.append(self.context.color) self.context.background_stack.append(self.context.background) if isinstance(data, (str, list)): shape = PalettedShape(data, color_map) elif isinstance(data, Shape): shape = data else: raise TypeError( f"Unknown data argument passed to blit: {type(data)} instance") if isinstance(pos, Rect): extent = pos.width_height pos = pos.c1 else: extent = None if roi is not None: roi = Rect(roi) shape = shape[roi] direct_pix = len(inspect.signature(self.set).parameters) >= 2 ishape = iter(shape) while True: pixel_pos, pixel = next(ishape, (None, None)) if pixel_pos is None: break target_pos = pos + pixel_pos if extent and (target_pos.x >= extent.x or target_pos.y >= extent.y): ishape.send(SKIP_LINE) continue should_set = ( pixel.capabilities.value_type == str and (pixel.value != EMPTY or pixel.value == "." and pixel.capabilities.translate_dots) or pixel.capabilities.value_type == bool and pixel.value) if not should_set and not erase: continue if direct_pix: if (pixel.capabilities.has_foreground and pixel.foreground == CONTEXT_COLORS or pixel.capabilities.has_background and pixel.background == CONTEXT_COLORS): _cls = pixel.__class__ values = [pixel.value] if pixel.capabilities.has_foreground: values.append(pixel.foreground if pixel.foreground != CONTEXT_COLORS else self.context.color_stack[-1]) if pixel.capabilities.has_background: values.append(pixel.background if pixel.foreground != CONTEXT_COLORS else self.context.background_stack[-1]) if pixel.capabilities.has_effects: values.append(pixel.effects) pixel = _cls(*values) self.set(target_pos, pixel) else: if pixel.capabilities.has_foreground: if pixel.foreground == CONTEXT_COLORS: self.context.color = self.context.color_stack[-1] else: self.context.color = pixel.foreground if pixel.capabilities.has_background: if pixel.background == CONTEXT_COLORS: self.context.background = self.context.background_stack[ -1] else: self.context.background = pixel.background if should_set: self.set(target_pos) else: self.reset(target_pos) self.context.color = self.context.color_stack.pop() self.context.background = self.context.background_stack.pop()
def _fast_render(self, data, rects=None, file=None): if file is None: file = sys.stdout if rects is None: rects = {Rect((0,0), data.size)} CSI = "\x1b[" SGR = "m" MOVE = "H" last_pos = self.__class__.last_pos last_fg = last_bg = last_tm_effects = last_un_effects = None seen = set() for rect in sorted(rects): if not isinstance(rect, Rect): rect = Rect(rect) outstr = "" for y in range(rect.top, rect.bottom): for x in range(rect.left, rect.right): if (x, y) in seen: continue seen.add((x, y)) # Fast render just for full-4tuple values. char, fg, bg, effects = data[x, y] if effects != TRANSPARENT: tm_effects = effects & TERMINAL_EFFECTS un_effects = effects & UNICODE_EFFECTS else: tm_effects = un_effects = Effects.none csi = False if fg != last_fg and fg != TRANSPARENT: outstr += CSI csi = True if fg == DEFAULT_FG: outstr += "39" else: outstr += "38;2;{};{};{}".format(*fg) if bg != last_bg and bg != TRANSPARENT: if not csi: outstr += CSI csi = True else: outstr += ";" if bg == DEFAULT_BG: outstr += "49" else: outstr += "48;2;{};{};{}".format(*bg) if tm_effects != last_tm_effects and effects != TRANSPARENT: semic = ";" if not csi: outstr += CSI semic = "" csi = True if last_tm_effects: for effect in last_tm_effects: if effect not in tm_effects: outstr += f"{semic}{effect_off_map[effect]}" semic = ";" for effect in tm_effects: outstr += f"{semic}{effect_on_map[effect]}" semic = ";" if csi: outstr += "m" last_fg = fg; last_bg = bg; last_tm_effects = tm_effects if char is CONTINUATION: # ensure two spaces for terminedia double-width chars - # can possibly be made more efficient if run in a terminal # that treat those correctly (not the case in current era konsole) outstr += EMPTY if char not in (TRANSPARENT, CONTINUATION): if (x, y) != last_pos: # TODO: relative movement? outstr += CSI + f"{y + 1};{x + 1}H" final_char = self.apply_unicode_effects(char, un_effects) outstr += final_char last_pos = (x + 1, y) # TODO: temporarily disable 'non-blocking' for stdout file.write(outstr); file.flush() self.__class__.last_pos = last_pos