2025, Oct 21 00:16

Как правильно заливать фигуры из дуг в Turtle: один контур, один begin_fill

Разбираем, почему ломается заливка фигур из дуг в Turtle, и как это исправить: непрерывный контур, единые углы, один begin_fill/end_fill и примеры кода.

Заливка неровных фигур Turtle, составленных из дуг, неожиданно оказывается непростой. Если контур не полностью непрерывен или вы начинаете и заканчиваете заливку в неподходящих местах, вместо сплошного цвета появляются полосатые артефакты или пустые участки. Ниже — практическое объяснение, почему так происходит, и как это исправить, используя стандартный механизм заливки в Turtle.

Постановка задачи

Контур собран из фрагментов дуг, чтобы имитировать «живые» границы. Процедура рисования вычисляет точки на овале и перемещает перо в эти координаты. Цель — залить всю замкнутую фигуру, однако обёртывание дуг вызовами begin_fill и end_fill дало странный рисунок вместо сплошной заливки.

Код, демонстрирующий проблему

Функция ниже строит дугу, шагая по углам и ставя черепаху в вычисленные точки. Используются модули math и turtle напрямую. Логика показательного примера такая же, как в исходной настройке, которая и приводит к неожидаемому результату заливки.

import turtle
import math
def draw_arc(ax, by, deg_start=None, deg_end=None, pen_obj=None):
    if pen_obj is None:
        pen_obj = turtle.Turtle()
    ox = pen_obj.pos()[0]
    oy = pen_obj.pos()[1]
    try:
        adj_start = math.radians(deg_start) * 0.875 * (180 / math.pi)
        adj_end = math.radians(deg_end) * 0.875 * (180 / math.pi)
    except:
        adj_start = 0
        adj_end = 315
    for k in range(int(adj_start), int(adj_end) + 1):
        if k == int(adj_start):
            pen_obj.up()
        else:
            pen_obj.down()
        pen_obj.setposition(ox + ax * math.cos(k / 50), oy + by * math.sin(k / 50))

Почему заливка «ломается»

Самая частая причина — разрыв в траектории. Для заливки в Turtle нужен замкнутый, непрерывный полигональный путь. Если перо поднимается не вовремя или последняя точка не совпадает с первой, алгоритм не может надёжно определить внутреннюю область, и вы получаете неожиданные узоры.

Есть и проблема согласованности углов. Смешение градусов и радиан приводит к слегка смещённым точкам и, как следствие, к микроразрывам вдоль контурa. В этом примере переменная шага в одном месте трактуется как градусы, а в другом — как радианы, и этого уже достаточно, чтобы испортить заливку, даже если контур на вид замкнут.

Наконец, обрамлять каждый небольшой фрагмент begin_fill и end_fill — контрпродуктивно. Заливку нужно начинать один раз перед прорисовкой всего контура и завершать один раз после его полного обхода.

Решение: одна операция заливки, непрерывный контур и согласованные дуги

Самый надёжный путь — рисовать фигуру одной непрерывной линией и вызвать begin_fill ровно один раз перед началом, а end_fill — один раз после завершения замкнутого обхода. Построение дуг через turtle.circle облегчает поддержание непрерывности и исключает путаницу с единицами углов.

import turtle as tr
def ripple(radius=20, sweep=45):
    tr.circle(radius, sweep)
    tr.circle(-radius, sweep)
def shape_outline(repeat_count=2, radius=20, sweep=90):
    for _ in range(4):
        for _ in range(repeat_count):
            ripple(radius, sweep)
        tr.left(90)
tr.fillcolor("red")
tr.begin_fill()
shape_outline(repeat_count=3)
tr.end_fill()
tr.mainloop()

Этот код выводит волнистый многоугольник, похожий на прямоугольник, обеспечивает непрерывность пути и аккуратную заливку. Ключевые моменты: фигура замкнута, а begin_fill и end_fill вызываются один раз вокруг всего контура, а не для каждой дуги.

Вариант со скруглёнными углами и поворотом

Если нужны скруглённые углы и управление поворотом, всё равно сохраняйте одну общую заливку по всему пути. Этот вариант рисует те же волнистые стороны, добавляет скругления на углах и позволяет передать величину дуги через аргумент командной строки.

import turtle as tr
import sys
def ripple(radius=20, sweep=45):
    tr.circle(radius, sweep)
    tr.circle(-radius, sweep)
def shape_outline(repeat_count=2, radius=20, sweep=90, rounded=True):
    for _ in range(4):
        for _ in range(repeat_count):
            ripple(radius, sweep)
        if rounded:
            tr.circle(radius, 90)
        else:
            tr.left(90)
if len(sys.argv) > 1:
    SWEEP = int(sys.argv[1])
else:
    SWEEP = 90
tr.fillcolor("red")
tr.begin_fill()
tr.right(SWEEP // 2)
shape_outline(repeat_count=3, sweep=SWEEP)
tr.left(SWEEP // 2)
tr.end_fill()
tr.mainloop()

Почему это важно для графики Turtle

Качество заливки зависит от геометрии. Даже разрыв в один пиксель может «выпустить» заливку или сорвать вычисление внутренности. Единый непрерывный контур и одноразовый цикл begin_fill/end_fill делают поведение предсказуемым. Отказ от смешения радиан и градусов при вычислении координат предотвращает едва заметные, но критичные для многоугольной заливки смещения. Использование turtle.circle для дуг даёт согласованный, «родной» для движка путь, который остаётся непрерывным между сегментами.

Выводы

Вызывайте begin_fill один раз перед прорисовкой всей фигуры и end_fill — один раз после полного замыкания пути. Убедитесь, что в контуре нет разрывов, включая микроразрывы из‑за несогласованных единиц углов. По возможности отдавайте предпочтение встроенным дугам Turtle, чтобы сохранить непрерывность. С этими настройками сложные силуэты из повторяющихся дуг заполняются ровно и предсказуемо.

Статья основана на вопросе на StackOverflow от ونداد شاهدی и ответе от furas.