Biblioteka kształtów – programowanie funkcyjne

W tym ćwiczeniu przepiszesz bibliotekę kształtów SVG zgodnie z paradygmatem programowania funkcyjnego. W przeciwieństwie do poprzedniego zadania:

  • nie używamy klas

  • kształty są reprezentowane jako dane (słowniki)

  • renderowanie odbywa się przez funkcje

Efektem końcowym ma być poprawne wygenerowanie wykresu funkcji sinus i cosinus za pomocą dostarczonego programu plot.py.

Reprezentacja kształtu

Każdy element SVG jest reprezentowany jako słownik zawierający m.in.:

  • tag – typ elementu (np. circle, line)

  • id – identyfikator

  • inne atrybuty (np. cx, cy)

  • opcjonalnie style

Dla przykładu poniżej podano implementację funkcji tworzącej element <circle>:

1def Circle(shape_id, cx, cy, r, style=None):
2    return {
3        "tag": "circle",
4        "id": shape_id,
5        "cx": cx,
6        "cy": cy,
7        "r": r,
8        "style": style,
9    }

Renderowanie elementów

Każdy typ elementu ma odpowiadającą funkcję renderującą. Dla przykładu element <circle> narysujemy:

1def render_circle(obj):
2    return f'<circle id="{obj["id"]}"{render_style(obj["style"])} cx="{obj["cx"]}" cy="{obj["cy"]}" r="{obj["r"]}" />'

Zwróć uwagę, że funkcja:

  • przyjmuje słownik (dane)

  • zwraca tekst SVG

  • nie modyfikuje stanu

Funkcja render_circle() pobiera styl elementu, zapisany w słowniku pod kluczem "style" i wywołuje dla niego render_style().

Funkcja główna renderująca

Funkcja render_element() wybiera odpowiednią funkcję renderującą na podstawie pola tag, korzystając z dyspozytora:

1def render_element(obj):
2    try:
3        return _RENDERERS[obj["tag"]](obj)
4    except KeyError:
5        raise ValueError(f'Unknown SVG tag: {obj["tag"]}') from None

Zadanie

Uzupełnij plik svg_shapes_func.py tak, aby program plot.py działał poprawnie.

W szczególności:

  1. Zdefiniuj brakujące funkcje konstruktorów kształtów:

    • Line()

    • Rect()

    • Group()

    • Square()

    Każda z nich powinna zwracać słownik opisujący element SVG.

2. Zaimplementuj Style, która przyjmuje wymagane argumenty: fill, stroke oraz stroke_width a zwraca odpowiedni słownik. Dodatkowo, napisz render_style().

  1. Zaimplementuj brakujące funkcje renderujące:

    • _render_line()

    • _render_rect()

    • _render_group()

  2. Uzupełnij słownik dyspozytora:

    • _RENDERERS = { ... }

    który mapuje tag → funkcja renderująca

Dodatkowe elementy funkcyjne

Zwróć uwagę na elementy programowania funkcyjnego użyte w zadaniu:

  • funkcje jako argumenty (make_points(..., math.sin, ...))

  • brak mutowalnego stanu

  • transformacje danych (with_style())

  • list comprehensions zamiast pętli z .add()

Funkcja with_style() (już zaimplementowana):

1def with_style(style, element):
2    return {**element, "style": style}

Ostatecznie, obraz tworzony jest funkcją svg_drawing:

 1def render_svg(width, height, elements, file_obj=None):
 2    inner = "\n".join(render_element(el) for el in elements)
 3    txt = (
 4        f'<svg width="{width}" height="{height}" '
 5        f'xmlns="http://www.w3.org/2000/svg">\n'
 6        f'{inner}\n'
 7        f'</svg>'
 8    )
 9
10    if file_obj is None:
11        print(txt)
12    elif isinstance(file_obj, str):
13        with open(file_obj, "w", encoding="utf-8") as f:
14            f.write(txt)
15    else:
16        file_obj.write(txt)

Efekt końcowy / ocena ćwiczenia

Poniżej znajduje się kompletny program plot.py, który korzysta z Twojej biblioteki.

Twoim celem jest uzyskanie poprawnego obrazu SVG.

 1import math
 2from svg_shapes_func import *
 3from itertools import chain
 4
 5
 6# Coordinate transforms as lambdas
 7to_data_x = lambda screen_x: (screen_x - 10) / 100
 8to_data_y = lambda screen_y: (150 - screen_y) / 100
 9to_screen_x = lambda data_x: 10 + 100 * data_x
10to_screen_y = lambda data_y: 150 - 100 * data_y
11
12
13# Returns iterable to markers rather than markers themselves
14def make_points(sample_points, fun, marker, prefix, size):
15    def make_one(i):
16        return marker(
17            f"{prefix}{i}",
18            to_screen_x(to_data_x(i + 10)),
19            to_screen_y(fun(to_data_x(i + 10))),
20            size,
21        )
22    return map(make_one, sample_points)
23
24
25# Styles
26border_style = Style(stroke="black", fill="white", stroke_width="3")
27grid_style = Style(stroke="gray", stroke_width="0.5")
28sin_style = Style(fill="#a6cee3", stroke_width=1.5, stroke="#1f78b4")
29cos_style = Style(fill="#fdbf6f", stroke_width=1.5, stroke="#ff7f00")
30
31# Border
32border = Rect("border", 10, 10, 628, 280, style=border_style)
33
34# Iterable to vertical grid lines
35vertical_grid = map(lambda x:
36        Line(f"lx{x}",to_screen_x(x * math.pi / 4),10, to_screen_x(x * math.pi / 4),290),
37            range(1, 8)
38    )
39
40# Iterable to horizontal grid lines
41horizontal_grid = map(lambda y:
42        Line(f"ly{y}", 10, to_screen_y(y), 638, to_screen_y(y)),
43            [1.0, 0.75, 0.5, 0.25, 0.0, -0.25, -0.5, -0.75, -1.0]
44    )
45
46# a grid holds a chained iterables over grid lines
47grid_group = Group("grid", chain(vertical_grid, horizontal_grid), style=grid_style)
48
49# Sample points iterates over approximately every pi/10
50sample_points = range(0, 628, 31)
51
52# Marker sets created by a higher-order function
53sin_points = make_points(sample_points, math.sin, Circle, "s", 5)
54cos_points = make_points(sample_points, math.cos, Square, "c", 8)
55
56# Apply style functionally
57sin_group = with_style(sin_style, Group("sin", sin_points))
58cos_group = with_style(cos_style, Group("cos", cos_points))
59
60# Final drawing
61render_svg(640, 300, [border, grid_group, sin_group, cos_group], "pltf.svg")

Po poprawnej implementacji uruchomienie plot.py powinno wygenerować poprawny wykres funkcji sinus i cosinus w formacie SVG.