§3 · Python-DSL

DSL — маленький язык поверх Python: те же правила синтаксиса (имена, выражения, арифметика, комментарии), но зарезервированные имена — геометрические. Point возвращает точку, Polygon — кортеж, Intersect делает то, что написано. Вы пишете конструкцию, а AnimaGeo превращает её в граф зависимостей, который пересобирается при анимации.

Обзор

Любая AnimaGeo-сцена строится из элементов. В DSL каждому элементу соответствует имя (идентификатор) — AnimaGeo использует его и для построения графа, и для доступа через scene.element('имя').

КатегорияПримеры имёнЧто делает
ПримитивыPoint, Segment, Line, Ray, Vector, Circle, Polygon, AngleСоздают базовые геометрические объекты.
КонструкцииMidpoint, Intersect, OrthogonalLine, AngularBisector, LineBisector, Center, Centroid, Tangent, Touches, Mirror, RotateСтроят новый объект из уже существующих.
ИзмеренияDistance, Area, AngleMeasureВозвращают Measure — число с единицами.
КривыеConic, Function, ImplicitCurve, Ellipse, Parabola, HyperbolaПодробно в §7.
ПеременныеMeasure, AngleSize, BooleanАнимируемые значения (см. §8).
В DSL регистр важен: Point(0,0) — валидно, point(0,0) — не распознаётся. Внутри DSL — обычный Python: можно писать x = -2.5, r = (a + b) / 2 или for i in range(3): ..., и эти выражения будут выполнены как ожидается.
Три способа подключить DSL к сцене. У любого интерактивного примера ниже есть кнопка Scaffold, которая показывает host-обёртку в трёх вариантах:
  • Inline — scene.putCode(""" … """). Код едет строкой, работает полный DSL-transform: LHS-имена попадают в Construction, повтор в цикле даёт p, p_2, p_3…, поддержан сахар f(x) = x^2.
  • From file — scene.loadCode('scene.ag.py'). Тот же движок, просто DSL живёт в отдельном файле. Расширение .ag.py — чтобы IDE подсвечивала как Python и подхватывала стабы <ggb>_stubs.pyi, которые пишет loadGGB.
  • Pure Python — with dsl.scope(self.geo): …. AST-transform не запускается; фабрики вызываются как обычные функции. IDE-autocomplete работает из коробки, но за имена в графе отвечаете вы сами — передавайте name='A' явно. Loop-scope уникализации тоже нет: повтор имени = коллизия.

Во всех трёх вариантах получается один и тот же Construction и, как следствие, одна и та же картинка. Переключайте вкладки в Scaffold, чтобы увидеть ту же DSL-логику в трёх Python-обвязках.

Точки

Точка — атом. Всё остальное строится от точек. В DSL есть два основных способа их создать:

ВызовСмысл
Point(x, y)Свободная точка с заданными координатами сцены.
Point(circle) + elem.tparam = θТочка на окружности, параметр tparam — угол в радианах. Анимируется через update_tparam.
Point(segment) + elem.tparam = tТочка на сегменте, tparam — линейный параметр (0..1 для внутренней). Анимируется.
Midpoint(A, B)Середина отрезка AB.
Intersect(g1, g2)Пересечение (линии, окружности, кривые — любые пары). Возвращает точку или список точек.
Center(Circle(A, B, C))Центр описанной окружности треугольника ABC.
Centroid(A, B, C)Центроид треугольника (точка пересечения медиан).
Incenter(A, B, C)Центр вписанной окружности.
Поле .tparam — curve/locus parameter для точки, закреплённой на локусе. Для окружности это угол в радианах, для сегмента/прямой/луча — линейный параметр t.

Точки можно складывать и умножать на число — удобно для небольших конструкций без явного создания промежуточных элементов:

L = A + (B - A) * 0.3        # точка на 30% пути от A к B
E = Centroid + (A - Centroid) * 1.2  # «продлить» медиану на 20%
points.py · три способа получить точку
A = Point(-3.8, -1.6)
B = Point(3.8, -1.6)
C = Point(0.6, 2.4)
p, a, b, c = Polygon(A, B, C)

M = Midpoint(A, B)                      # середина
G = Centroid(A, B, C)                   # центроид
O = Center(Circle(A, B, C))               # описанная окружность

med = Segment(C, M)                     # медиана из C
k = Circle(O, A)                        # описанная окружность

style(A, B, C, M, G, O, label_visible=True)
Точки на треугольнике
Скачать: guide_style.json

Линии, сегменты, лучи, векторы

Четыре примитива отличаются тем, как рендерятся и участвуют в пересечениях:

ПримитивDSLРендерИспользуется для
LineLine(P, Q) или Line(P, direction_vector)Прямая, обрезанная viewport'омБесконечные построения: биссектрисы, перпендикуляры, касательные-прямые.
SegmentSegment(A, B)Отрезок от A до BСтороны полигонов, сегменты-подписи (AM = MB).
RayRay(origin, direction_point)Луч из origin через direction_point, обрезан viewport'омБиссектрисы-лучи, направления света/зрения.
VectorVector(tail, head)Стрелка от tail до headВекторы в физике/алгебре, направления сил.
Разница видна в обрезании viewport'ом: Line и Ray всегда тянутся до края, Segment — строго между концов. Intersect тоже ведёт себя по-разному: пересечение Line×Line всегда в одной точке (если не параллельны), Segment×Segment — только если реально пересекаются в границах.

Стили штрихов и меток равенства

У сегментов есть elem.style['tick_count']: 1, 2 или 3 штриха поперёк — классическая школьная нотация «эти стороны равны». Плюс 'tick_style': 'wave' даёт волнистые штрихи вместо прямых.

segments.py · AM = MB
A = Point(-3, 0)
B = Point(3, 0)
M = Midpoint(A, B)

s1 = Segment(A, M)
s2 = Segment(M, B)
# overrides снаружи добавляют равные штрихи (lines=2)
# попробуйте убрать — иллюстрация потеряет метки равенства

style(A, B, M, label_visible=True)
s1.style.tick_count = 2
s2.style.tick_count = 2
Сегменты с метками равенства
Скачать: segments.py

Углы

Угол в AnimaGeo — не просто число, а объект. У него есть вершина, две стороны и дуга, которую можно стилизовать:

Angle(P, V, Q)   # угол при вершине V, от луча VP до луча VQ

Порядок аргументов определяет направление дуги — это важно для ориентированных (CCW) полигонов.

Reflex-сектор. Если ваш полигон ориентирован против часовой стрелки (typical для CCW-координат), Angle(A, B, C) для внутреннего угла при B — правильный порядок. Если поменять местами A и C, получите >180°-дугу. При заливке fill это заметнее всего: half-plane вывернется наизнанку.

У углов есть несколько ключей стиля, специфичных именно для углов:

КлючЧто делает
tick_count1, 2 или 3 концентрических дуги — показать, что углы равны. arc_shift_px между дугами задаётся в стиле.
arc_size_pxРадиус внешней дуги в пикселях (pixel-invariant).
fill / fill_opacityЗаливка сектора.
label_textTeX или Unicode-строка, напр. $\alpha$, $60°$, $\frac{\pi}{3}$.

Прямой угол визуализируется автоматически — если Angle(A, B, C) на момент построения равен 90°, рисуется квадратик вместо дуги.

angles.py · разные дуги
A = Point(-4.5, -2)
B = Point(4.5, -2)
C = Point(-0.5, 2.8)
p, a, b, c = Polygon(A, B, C)

alpha = Angle(B, A, C)   # при A
beta  = Angle(C, B, A)   # при B
gamma = Angle(A, C, B)   # при C
# overrides: lines 1/2/3 — стандартная школьная нотация

style(A, B, C, label_visible=True)
alpha.style.label_text = r'$\alpha$'
beta.style.label_text = r'$\beta$'
gamma.style.label_text = r'$\gamma$'
alpha.style.tick_count = 1
beta.style.tick_count = 2
gamma.style.tick_count = 3
Углы с разными дугами
Скачать: angles.py guide_style.json

Окружности, дуги, секторы

DSLЧто
Circle(center, passing_point)Окружность с центром center, проходящая через passing_point.
Circle(center, r)Окружность радиуса r (число или Measure).
Circle(A, B, C)Окружность через три точки (= описанная для треугольника).
CircleArc(center, A, B)Дуга окружности от A до B (CCW).
CircleSector(center, A, B)Сектор (дуга + два радиуса).
CircumcircleArc(A, B, C)Дуга описанной окружности, проходящая через A → B → C.
circle.py
O = Point(0, 0)
R = Point(2.5, 0)
c = Circle(O, R)

# две точки на окружности и дуга между ними
P = Point(-1.9, 1.6)
Q = Point(1.9, 1.6)
arc1 = CircleArc(O, Q, P)

style(O, R, P, Q, label_visible=True)
arc1.style.stroke = '#f15b5b'
arc1.style.stroke_width_px = 2.5
Окружность и дуга
Скачать: circle.py

Полигоны

Polygon(P1, P2, ..., Pn) возвращает кортеж: сам полигон + его стороны, по порядку.

p, a, b, c = Polygon(A, B, C)
# p — полигон (для заливки)
# a = Segment(A, B), b = Segment(B, C), c = Segment(C, A)

Для n-угольника кортеж имеет n+1 элемент: полигон и n сторон (замыкающая идёт последней, от Pn к P1). Сторону можно использовать как обычный сегмент, в том числе передавать в Intersect:

A = Point(-4, -1.5); B = Point(4, -1.5); C = Point(0, 2.5)
p, sAB, sBC, sCA = Polygon(A, B, C)
M = Intersect(AngularBisector(B, A, C), sBC)   # M ∈ BC
Полигон рендерится как заливка (под всеми линиями) + его стороны рисуются поверх. Если нужен только контур — установите p.style['fill_opacity'] = 0. Если только заливка — скройте стороны через a.visible = False; b.visible = False; ....

Конструирующие команды

Шесть самых частых команд — основа любой школьной геометрии. Каждая принимает существующие точки/линии и возвращает новый объект.

КомандаСигнатураВозвращает
СерединаMidpoint(A, B) или Midpoint(segment)Точка
ПересечениеIntersect(g1, g2)Точка (или список, если их несколько — см. ниже)
ПерпендикулярPerpendicularLine(P, l)Прямая через P перпендикулярно l
ПараллельнаяParallelLine(P, l)Прямая через P параллельно l
Биссектриса углаAngularBisector(P, V, Q)Луч из V, делящий угол PVQ пополам
Серединный перпендикулярPerpendicularBisector(A, B)Прямая
КасательнаяTangent(P, circle)Пара прямых (или пустой список, если P внутри)

Несколько пересечений

Intersect для «линия × окружность» даёт две точки. Если нужна конкретная — можно передать индекс: публичный DSL использует нумерацию 1, 2, ... и принимает как позиционный аргумент, так и index=.

P1 = Intersect(line, circle, index=1)   # первая точка
P2 = Intersect(line, circle, 2)         # вторая

Без индекса Intersect отдаёт объект-контейнер; обычно удобнее деструктуризовать:

P1, P2 = Intersect(line, circle)

Порядок пересечений является частью публичного контракта: P1, P2 = Intersect(a, b) соответствует Intersect(a, b, index=1) и Intersect(a, b, index=2). Для окружностей AnimaGeo сохраняет GeoGebra-like эвристику: если точки пересечения уже участвовали во входных объектах окружности или второго объекта, они идут первыми в этом порядке; оставшиеся точки сохраняют внутренний детерминированный порядок. Один и тот же порядок используется в Python DSL, .ggb import и экспортных путях.

Составной пример: биссектриса и её пересечение со стороной

bisector.py
A = Point(-4, -1.8)
B = Point(4, -1.8)
C = Point(-0.8, 2.6)
p, a, b, c = Polygon(A, B, C)

# биссектриса угла A — прямая через f
f = AngularBisector(C, A, B)

# точка, где она пересекает противолежащую сторону BC
L = Intersect(f, b)

# видимый сегмент A→L (биссектриса как отрезок)
bis = Segment(A, L)

# два полууглa при A (для равных дуг)
beta  = Angle(C, A, L)
gamma = Angle(L, A, B)

hide(f)
style(A, B, C, L, label_visible=True)
beta.style.label_text = r'$\beta$'
gamma.style.label_text = r'$\gamma$'
bis.style.stroke = '#f15b5b'
bis.style.stroke_width_px = 1.5
Биссектриса угла A в треугольнике
Скачать: bisector.py

Переменные и измерения

Три типа «переменной» — это числовые значения, которые можно анимировать, привязывать к построениям и показывать как подписи.

ТипСозданиеПрименение
Measured = Distance(A, B), s = Area(p)Длины, площади. Автоматически пересчитывается при изменении точек.
AngleSizex = AngleMeasure(B, A, C)Размер угла (в радианах). Можно использовать в If, сравнениях.
Booleanb = Boolean(True), условия на сценеУправление видимостью (см. §8, keyframes).

Любой Measure можно показать как подпись — либо рядом с элементом, от которого измерение получено, либо в отдельной точке:

A = Point(-3, 0); B = Point(3, 0)
s = Segment(A, B)
d = Distance(A, B)              # Measure
s.style['label_visible'] = True
s.style['label_text'] = f'{d:.1f}'   # «6.0»
В анимации значение d — живое: при движении A или B подпись обновляется автоматически. Для живой подписи используйте строку с плейсхолдером {measure} — подробно в §6.
Числа построения можно использовать в формулах кривых: k = 2, затем f(x) = k x^2 (знак * можно опустить). Формула хранит ссылку на k и пересчитывается при его изменении; x(A), y(A) — координаты точки, логическое значение (b = AreCollinear(A, B, C)) — условие в If(b, x, -x), а в арифметике считается как 0 или 1 (y = x + b). Уравнение прямой — тоже формула: Line("y = k x + 1"). π и ℯ пишутся как есть: f(x) = sin(πx). Подробности синтаксиса — в §7.

Доступ из Python

После putCode(...) все имена из DSL живут в Construction. Доступ через scene.element(name) или напрямую через scene.geo.elements. Стили можно писать как через style[...], так и атрибутом style.X (StyleProxy поддерживает оба варианта).

scene.putCode("""
A = Point(-3, 0)
B = Point(3, 0)
s = Segment(A, B)
""")

s_obj = scene.element('s')
s_obj.style.stroke = '#f15b5b'        # атрибутный стиль
s_obj.style['stroke_width_px'] = 2.5     # dict-совместимый
s_obj.style.label_visible = True
s_obj.style.label_text = 'AB'

# или скрыть элемент полностью:
a_obj = scene.element('A')
a_obj.visible = False

# Прямой доступ к геометрическим полям
s_obj.start, s_obj.end, s_obj.length     # через Segment
a_obj.x, a_obj.y, a_obj.coords            # через Point

Внутри DSL (в коде, переданном в putCode) доступ к полям выглядит также: A.x, circ.center, seg.length и т. д. Полный список человечных имён полей — в docs/field_names.md.

В 2026-04 прошла финальная ревизия имён полей на data-классах. Короткие имена (.a, .c, .n, .r, .M, .b, .x у Measure, .alpha, .angle) полностью заменены на человечные (.coords, .center/.offset, .normal, .radius, .matrix, .value, .tparam, .size). Backward-compat алиасы удалены — старый код получает AttributeError.

Python-like DSL: control flow без unrestricted runtime

exec-движок исполняет DSL-строку как Python: for, if, def, **kwargs, comprehensions, f-строки, tuple-unpack — всё работает.

Это не unrestricted Python. Импорты в DSL-строке удаляются как IDE-only, global, nonlocal, +=, walrus :=, chained assignment и annotated assignment с value запрещены. В runtime недоступны open, eval, exec, __import__. Lowercase unknown name создаёт forward-ref Var(None), а CamelCase-опечатки должны падать как ошибка.
self.putCode("""
# Цикл создаёт p, p_2, p_3 (авто-уникализация имён)
for i in range(3):
    p = Point(i, 0)

# Функция с explicit names через kwarg
def triangle(prefix, side):
    A = Point(0, 0, name=f'{prefix}_A')
    B = Point(side, 0, name=f'{prefix}_B')
    C = Point(side/2, side*0.866, name=f'{prefix}_C')
    return A, B, C

triangle('t1', 3)
triangle('t2', 5)

# Арифметика на прокси (регистрирует Add/Sub/Mult-команды)
D = A + B                    # Add(A, B)
v = B - A                    # Sub(B, A)
E = 2 * A                    # Mult(2, A)
""")

Полный гайд по возможностям DSL — docs/python_dsl.md.

Dispatch convention

Под капотом каждая DSL-команда становится вызовом функции name_shortcut, где shortcut — конкатенация однобуквенных кодов типов аргументов. AnimaGeo выбирает конкретную реализацию по фактическим типам переданных аргументов.

КодТип
pPoint
lLine
sSegment
rRay
cCircle
CArc
aAngle
PPolygon
vVector
iint/float
mMeasure
KConic
FFunction
IImplicitCurve
Tстрока (DSL-литерал, для Conic("..."), Line("..."))

Например, Midpoint(A, B) → midpoint_pp, Intersect(l, c) → intersect_lc, Circle(O, r) → circle_pi (если r — число), Circle(O, P) → circle_pp (если P — точка).

Весь список реализаций — в animageo/geo/lib_commands.py. Там 150+ функций; знать их не обязательно — DSL-уровня достаточно для всех школьных задач.

Если нужно расширить язык — добавить, скажем, GoldenRatio(A, B) — достаточно написать golden_ratio_pp(p1, p2) в lib_commands.py и перезапустить скрипт. Диспетчер подхватит новую функцию автоматически; никаких регистраций не требуется.