Прикладная геометрия и головоломки

Построение треугольников из командной строки

Я создаю головоломки для твиттер-канала PragProg. В этой недавней головоломке вас просят посчитать треугольники, которые появляются на картинке. Это сложно, так как некоторые треугольники могут сочетаться. Это был популярный твит, и наши подписчики с удовольствием выясняли ответ. В этом посте на Medium я расскажу о геометрии, используемой для построения этой фигуры с использованием экосистемы macOS.

И macOS, и iOS предлагают обширные и выразительные инструменты рендеринга. Для рисования я использую базовые кривые Безье. Поскольку инструменты рисования в iOS на поколение лучше, чем в macOS, я написал библиотеки, расширяющие инструменты macOS для эмуляции вызовов iOS. Это помогает мне создавать головоломки в командной строке macOS.

Например, в macOS пока нет GraphicsImageRenderer, как в iOS. Средство визуализации создает холст, на котором вы можете рисовать, и возвращает изображение. Инструменты были доступны для iOS в течение нескольких лет, но не для Mac. Введение Catalyst в macOS, по-видимому, убрало приоритеты синхронизации этих двух функций. Поэтому я потратил время на заполнение пробелов, чтобы лучше рисовать на Mac.

Это позволяет мне обмениваться кодом между платформами. Мне не нужно писать одну библиотеку для Mac и другую для iOS и tvOS. Моя условная компиляция скрыта, и я использую один набор вызовов для выполнения всего моего рисования, независимо от цели.

Чтобы создать эту головоломку, я создал пространство размером 800 x 600 (контекст) для рисования и залил его белым фоном. Одно из странных исторических различий между iOS и macOS заключается в том, что они используют разные источники. iOS находит (0,0) вверху слева, а macOS внизу слева. Нижний левый устанавливает положительный квадрант XY в математическом пространстве, с осью x, идущей вправо, и осью y, идущей вверх. Это более традиционная система координат, используемая для построения графиков. iOS приняла движение оси Y вниз, ближе к тому, как люди думают о массивах и графической памяти.

Вы можете применить матрицу преобразования, как это делаю я, которая меняет ориентацию координат. Применяя аффинное преобразование при обнаружении развертывания Mac, мой вертикально зеркальный источник становится согласованным в iOS и macOS.

В основе этой головоломки лежит равносторонний треугольник, поэтому я начал с круга. Я разделил 360° круга на 3, по 120° на каждую сторону. Я установил свою первую точку на 90° (верхняя), затем на 210° и 330°. С этими углами я использовал r•cos(θ) и r•sin(θ) для вычисления координат (x, y) для каждой вершины моего основного треугольника.

return CGPoint(x: r * CGFloat(cos(theta)), 
               y: r * CGFloat(sin(theta)))

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

Затем я хотел сгенерировать линии, соответствующие этому треугольнику. Три случайные строки — это как раз то, что нужно, чтобы головоломка была простой, но интересной. Я установил изменяемый массив точек, заполнив его тремя конечными точками треугольника. Мутация позволяет массиву расти и изменяться по мере добавления новых значений.

Для каждой из трех линий я случайным образом выбрал три отдельные точки из своего массива точек. Самый простой способ выбрать n элементов за раз — перетасовать (рандомизировать) массив, а затем обратиться к префиксу n-элемента массива. В первый раз это точно соответствовало трем внешним точкам.

Я построил вектор между randomPoint1 и randomPoint2, выбрав случайный коэффициент от 0,33 до 0,7 для масштабирования результирующего вектора. Это создало новую точку где-то в середине линии, которую я затем добавил в свою коллекцию точек.

// create and scale the vector
let vector = (randomPoint1 — randomPoint2) * 
             Double.random(in: 0.3 … 0.7)
// establish the new point
let newPoint = randomPoint1 — vector
points.append(newPoint)

Помните, я начал с трех пунктов. Я использовал третью точку (randomPoint3), новую точку (newPoint) и одну из первых двух (выбрав randomPoint1 или randomPoint2), чтобы нарисовать треугольник. Затем я повторил этот процесс еще дважды, создав в общей сложности три точки. В моей головоломке это q1, q2 и q3 на следующем изображении.

Поскольку я добавляю сгенерированный newPoint в свой массив очков, каждая последующая точка участвует в розыгрыше 3 очков. Вот как q3 находится между p1 и q2. Последние два были выбраны в качестве одного из источников randomPoint1 и randomPoint2 с q1 в качестве randomPoint3 во время создания головоломки.

Чтобы сэкономить усилия при написании кода, я просмотрел каждый вывод, чтобы понять, «хорошая ли это головоломка?» Я хотел головоломку, в которой линии не пересекались бы. Хотя это легко вычислить, я не тратил время на его добавление. Это пересечение (извините за каламбур) создания инструментов для решения проблемы и использования человеческого фактора для соблюдения сроков. Я был рад, что два треугольника ▵p1q1q3 и ▵q1q2q3 объединились в еще один треугольник. Так что это цифра, которую я выбрал и опубликовал.

Применение этой простой геометрии кругов, точек, векторов и линий автоматизировало создание и рисование этого изображения-головоломки. Лучше всего то, что командная строка позволила избежать накладных расходов на создание всего приложения с графическим интерфейсом.

Кстати, следующий рисунок — это мой ответ на исходную головоломку, так что вам не нужно выполнять работу. Я нашел восемь треугольников. Вот они.

Если вам понравилась эта статья, вам также могут понравиться Числовые головоломки от Эрики Садун, бета-версия из The Pragmatic Bookshelf: