Katkat

Design a Layered ASCII Canvas Command Processor

AsanaSoftware Engineer · Onsite

Last updated

Design a clean, object-oriented command processor for a fixed 10x6 ASCII canvas that supports drawing, layering, and rendering overlapping filled rectangles.

Problem Statement

Design an ASCII canvas system with a fixed width of 10 columns and height of 6 rows. The canvas allows users to place, remove, and reorder rectangular shapes on different layers. Each rectangle is filled with a single ASCII character. When rendered, higher-layer shapes occlude lower-layer shapes. Pixels with no shapes display a default background character '.'. Coordinates use (x, y) where x represents horizontal position (0 <= x < 10, left to right) and y represents vertical position (0 <= y < 6, top to bottom).

Implement a class AsciiCanvas with the following interface:

class AsciiCanvas:
    def __init__(self, width: int = 10, height: int = 6):
        """Initializes the canvas with the given dimensions."""
        pass

    def add_rectangle(self, shape_id: str, x: int, y: int, width: int, height: int, fill: str) -> bool:
        """Adds a new filled rectangle at the top of the layer stack.
        Returns False if shape_id already exists or dimensions are invalid (width  bool:
        """Removes the rectangle with shape_id. Returns False if shape_id does not exist."""
        pass

    def bring_to_front(self, shape_id: str) -> bool:
        """Moves shape_id to the topmost layer. Returns False if shape_id does not exist."""
        pass

    def send_to_back(self, shape_id: str) -> bool:
        """Moves shape_id to the bottommost layer. Returns False if shape_id does not exist."""
        pass

    def render(self) -> list[str]:
        """Returns the rendered 10x6 grid as a list of 6 strings, each of length 10."""
        pass

Example

canvas = AsciiCanvas(10, 6)
canvas.add_rectangle("A", x=1, y=1, width=4, height=3, fill="A")
canvas.add_rectangle("B", x=3, y=2, width=4, height=3, fill="B")

# B is on top of A
print("\n".join(canvas.render()))
# Output:
# ..........
# .AAAA.....
# .AABBBB...
# .AABBBB...
# ...BBBB...
# ..........

canvas.bring_to_front("A")
print("\n".join(canvas.render()))
# Output:
# ..........
# .AAAA.....
# .AAAABB...
# .AAAABB...
# ...BBBB...
# ..........

Expectations

  • Separate domain models (e.g., Rectangle, LayerStack, CanvasRenderer) cleanly.
  • Explicitly handle edge cases: shapes partially or fully out of bounds, duplicate IDs, non-existent shape operations.
  • Discuss the trade-offs between rendering lazily (rasterizing on render()) versus eagerly (maintaining a pixel buffer per operation).

Hints

Hint 1

Consider what data structure enables fast layer reordering (`bring_to_front`, `send_to_back`) as well as quick lookups by `shape_id`.

Hint 2

Decide whether to store pixels in a persistent 2D buffer or reconstruct the visible grid during `render()` by iterating through shapes bottom-to-top or top-to-bottom.

Hint 3

Ensure coordinates are clipped against the `[0, width)` and `[0, height)` boundaries so that out-of-canvas shapes do not throw indexing errors.

Follow-up questions

  • How would you optimize rendering if the canvas size were 4K (3840x2160) with thousands of overlapping rectangles, most of which are fully occluded?
  • How would you extend this system to support undo/redo operations using the Command pattern?
  • If multiple clients were issuing canvas commands concurrently, how would you ensure consistency and prevent race conditions?