Научная графика на языке Asymptote Обзорная монография Ю.М. Волченко 14.02.2018 Îãëàâëåíèå I Предисловие к третьему изданию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Предисловие ко второму изданию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Предисловие к первому изданию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 5 5 ПРОГРАММИРОВАНИЕ 9 1 Базовые типы данных 10 2 Сложные типы данных 2.1 Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 Срезы массивов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3 Структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 19 21 3 Операторы 3.1 Арифметические и логические операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Префиксные и постфиксные операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Операторы, определяемые пользователем . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4 Условные операторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5 Циклы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6 Безусловный оператор . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 25 26 26 27 28 28 4 Функции 4.1 Аргументы по умолчанию . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Именованные аргументы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Аргументы . . . (rest) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4 Математические функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.5 Функции, определенные для пар . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.6 Функции, определенные для троек . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.7 Строковые функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.8 Функции для путей path . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9 Функции для путей guide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.10 Системные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.11 Приведение типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.12 Импорт . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.13 Статические переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 33 34 35 36 38 39 40 41 44 44 45 47 49 5 Модули и их возможности 52 1 6 Основные инструменты 6.1 Установки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.1.1 Формат выходного файла . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.1.2 Размеры и процедура size . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Перья и опция pen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.1 Цвет . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.2 Тип линии . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.2.3 Толщина линии . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3 Преобразования и процедура transform . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.4 Заполнение области и опция filltype . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.5 Положение и направление . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6 Фреймы и картинки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.1 Фреймы frame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.6.2 Картинки picture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 58 58 58 58 59 61 62 62 63 64 65 65 65 II 68 РИСОВАНИЕ НА ПЛОСКОСТИ 7 Модуль plain 7.1 Процедуры dot и label . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2 Кривые Безье . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 Процедуры path, guide и draw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.4 Процедуры unitcircle, circle, ellipse и arc . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5 Процедуры unitsquare, box и polygon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.6 Заполнение, градиентная заливка, обрезка . . . . . . . . . . . . . . . . . . . . . . . . . . 7.7 Картинки picture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.8 Подпути и пересечения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.9 Фрагменты пути . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.10 Создание замкнутых путей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.11 Модули geometry и olympiad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.12 Модуль LSystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 69 71 72 77 80 80 83 84 87 88 90 92 8 Модуль graph 96 8.1 Оси координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 8.1.1 Процедура xaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 8.1.2 Процедура yaxis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 8.1.3 Процедуры xlimits, ylimits и limits . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 8.1.4 Процедуры xequals и yequals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 8.1.5 Процедура axes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 8.1.6 Процедура axis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 8.2 Построение графиков . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 8.2.1 Сплайны . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104 8.2.2 Декартова система координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 8.2.3 Параметрическое задание функции . . . . . . . . . . . . . . . . . . . . . . . . . . 114 8.2.4 Полярная система координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 8.2.5 Изображение векторных полей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 9 Модуль palette 118 10 Модуль contour 122 2 11 Модули markers, labelpath и patterns 127 11.1 Модуль markers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 11.2 Модуль labelpath . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 11.3 Модуль patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 12 Пакет flowchart 134 13 Пакет asy-circ 138 13.1 Элементы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 13.2 Функция join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 13.3 Методы элементов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 13.3.1 Метод setPosition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 13.3.2 Метод addTension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 13.3.3 Метод addLabel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 13.3.4 Метод variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 13.3.5 Метод connect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 13.4 Электрические цепи . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 13.4.1 Компоновочная сетка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147 13.4.2 Примеры электрических схем . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 13.5 Комплексные элементы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 13.5.1 Последовательные и параллельные цепи . . . . . . . . . . . . . . . . . . . . . . . 149 13.5.2 Соединения суперэлементов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 III РИСОВАНИЕ В ПРОСТРАНСТВЕ 153 14 Проекции, перспектива, освещение 154 14.1 Настройки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 14.1.1 Использование формата PRC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 14.1.2 Разрешение изображения . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 14.1.3 Размеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 14.2 Косоугольная проекция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 14.3 Ортогональная проекция . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156 14.4 Перспектива . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158 14.5 Освещение поверхности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 14.6 Создание интерактивного рисунка . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 15 Модуль three 162 15.1 Контуры и поверхности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162 15.2 Стрелки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 15.3 Окружности, дуги, прямоугольники и боксы . . . . . . . . . . . . . . . . . . . . . . . . . 168 15.4 Трехмерные преобразования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172 16 Модуль graph3 179 16.1 Оси координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179 16.2 Координатные сетки и модуль grid3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 16.3 Графики функций как поверхности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 16.3.1 Декартова система координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 16.3.2 Параметрическое задание поверхности . . . . . . . . . . . . . . . . . . . . . . . . 189 16.3.3 Цилиндрическая система координат . . . . . . . . . . . . . . . . . . . . . . . . . . 191 16.3.4 Сферическая система координат . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 16.3.5 Прозрачность и зачерчивание . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 3 16.3.6 Построение поверхностей по точкам . . . . . . . . . . . . . . . . . . . . . . . . . . 192 16.3.7 Проецирование, процедура lift . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195 16.3.8 Изображение векторных полей . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 16.3.9 Раскрашивание с помощью палитр . . . . . . . . . . . . . . . . . . . . . . . . . . 198 16.3.10Рельефные надписи на поверхности . . . . . . . . . . . . . . . . . . . . . . . . . . 199 17 Модули solids и contour3 203 18 Модули labelpath3 и tube 208 A План квартиры 213 B Зачерчивание I 216 C Зачерчивание II 220 D Чашка с напитком 224 Литература 226 4 ÏÐÅÄÈÑËÎÂÈÅ Ê ÒÐÅÒÜÅÌÓ ÈÇÄÀÍÈÞ В части «Рисование на плоскости» добавлен раздел «Модуль flowchart», в котором описано применение Asymptote к изображению различного рода схем. Рассмотрены использование оператора break и преобразование блок-схем программ в программы без goto (раздел «Безусловный оператор»). В разделе «Функции» появились незначительные добавления по поводу использования оператора return и вложенных функций. ÏÐÅÄÈÑËÎÂÈÅ ÊÎ ÂÒÎÐÎÌÓ ÈÇÄÀÍÈÞ За время, прошедшее с момента опубликования первого издания, в нем был замечен ряд неточностей и несуразностей, которые в настоящем издании устранены. Впрочем, это не гарантирует отсутствия недочетов после сделанных исправлений. Кроме того, автор познакомился с некоторыми пакетами и дополнительными возможностями Asymptote, которые могут оказаться полезными и другим ее пользователям. В результате объем настоящего руководства увеличился за счет следующих изменений. Добавлено упоминание о подпрограммах, применяемых для генерации случайных чисел: srand, rand, unitrand и Gaussrand, раздел 4.4. Добавлен раздел 7.11 с изложением основных особенностей пакетов geometry и olympiad, созданных для иллюстрации постановок и решения задач плоской евклидовой геометрии. Добавлен раздел 7.12 с описанием модуля LSystem, служащего для создания так называемых L-фракталов на плоскости. Добавлена глава 13 с руководством по использованию пакета asy-circ, предназначенного для изображения электрических схем. Добавлен раздел 14.6, в котором описывается создание рисунков со встроенной интерактивностью, которая дает возможность изучать полученное изображение в различных ракурсах и масштабах. Добавлен раздел 16.3.6, посвященный возможности построения поверхностей по точкам, в частности, поверхностей NURBS. ÏÐÅÄÈÑËÎÂÈÅ Ê ÏÅÐÂÎÌÓ ÈÇÄÀÍÈÞ Если пользователь совершенно добровольно работает в такой «жуткой» среде как LATEX, значит, ему не все равно, как выглядит создаваемый им документ с эстетической точки зрения. С эстетикой в упомянутой издательской системе все в порядке. Но вот рисунки! Их надо создавать с помощью отдельных программ, из коих достойны употребления, по-видимому, только две (и обе бесплатные!): PGF/TikZ и Asymptote. Они, заботясь о едином стиле документа и рисунков в нем, обеспечили проникновение LATEX’а в свои операторы, команды и процедуры. В большей мере это свойственно PGF/TikZ и в меньшей – Asymptote. Но первая слабо справляется с трехмерной графикой (даже с учетом пакета PGFPLOTS), не имеет достаточно развитого языка программирования и довольно часто отказывается компилировать рисунки из-за нехватки памяти. Asymptote же представляет собой язык программирования столь высокого уровня, что создание с ее помощью самых сложных трехмерных рисунков превращается в изысканное удовольствие. Проще говоря, там, где в PGF/TikZ скрежет зубовный и «танцы с бубном», в Asymptote сочиняется само собой, как песня. Нехватка памяти случается и у нее, о чем в некоторых примерах этой книги пару раз упоминается. Но этот сбой встречается довольно редко и в действительно сложных ситуациях. 5 Однако есть другие неприятные моменты. Надо хорошо постараться, чтобы в Asymptote вывести на рисунок русский текст, поэтому этим вопросом мы заниматься не будем. Что касается TEX’овских формул, Asymptote их понимает и выводит, но при корректировке размера шрифта в документе соответствующей корректировки на рисунках не произойдет. Кроме того, встраивание программы на Asymptote в TEX’овский текст неэффективно, так как требует трех проходов компиляции, скорость которой остается за пределами желаемого. Следовательно, рисунки лучше компилировать отдельно, а затем вставлять в текст. Если рисунок сохранить в pdf-формате, потери качества не произойдет. Что же в итоге? Для плоской графики следует предпочесть пакет PGF/TikZ, принимая во внимание его полную интеграцию с LATEX’ом, что в свою очередь обеспечивает вывод на рисунки русского текста. Собственно говоря, все примеры данного документа оформлены (не путать с созданием рисунков в Asymptote!) с помощью пакета tcolorbox, активно использующего пакет PGF/TikZ. А вот трехмерную графику лучше делать в Asymptote, поскольку лишь она достойно справится с этой задачей. Вот и ладушки? Ан нет! Последняя неприятность состоит в том, что достаточно полного руководства по Asymptote в природе не существует, что́ было не раз отмечено различными авторами. Руководство, которое можно скачать с официального сайта, довольно сухое, малоинформативное, совершенно не страдающее избытком примеров и даже использующее нигде в тексте не определенные ключевые слова. Возможно, создатели придерживались мнения, что пользователи хорошо знакомы с языками C, C++, Java и графической программой MetaPost, то есть со всем тем, что унаследовала Asymptote. Да и модули-исходники ее все открыты, примеры в виде текстов программ имеются, читай и разбирайся! Короче, Asymptote – не для простых смертных! Конечно, грешно предъявлять претензии к авторам бесплатной программы – могли бы и программу не писать, не то, что руководство. Но простым смертным тоже некуда деваться, если позарез требуется трехмерная графика! Так что приходится по кусочкам собирать то, что имеется на английском, французском, чешском и русском, и стряпать собственные опусы. Так возникла и эта книга: просто мне захотелось вставить в свои лекции, выложенные на двух сайтах, красивые трехмерные графики да еще с анимацией. Поэтому сей труд написан в основном для себя. Наверняка в нем есть опечатки, ошибки, ляпы, недопонимание и непонимание того, о чем здесь написано и т. д. Короче, все в лучших традициях бесплатных разработок! Итак, с чего же начать? Начнем с сайтов и литературы. Официальный сайт Asymptote такой: http://asymptote.sourceforge.net. Здесь можно скачать ее дистрибутив, руководство и получить адреса ссылок на страницы Интернет с примерами программ и изображений. В конце этой книги можно найти список литературы, которую я безжалостно проэксплутировал, сочиняя свою обзорную монографию. На то она и обзорная! Я не стал делать ссылки на источники по поводу каждого использованного мной примера, как и не стал специально отмечать собственные примеры. Не надо думать, что в книге дается достаточное полное изложение работы в Asymptote. За бортом остались многие вещи. Например, ничего не сказано о создании анимаций и интерактивных картинок. Не раскрыта работа с модулями, обеспечивающими статистические вычисления, рисование блок-схем, использование бинарных деревьев и т. д. Впрочем, один из них, модуль geometry, уже описан на русском языке [6]. Инсталлировать Asymptote лучше на диск C, так как у автора был случай, когда инсталлированная на диск D Asymptote отказалась делать надписи в рисунках не только на русском, но и на английском языке. В принципе программы для Asymptote можно писать в любом текстовом редакторе, но наиболее удобно это делать в блокноте notepad++. Он обеспечивает расцветку синтаксиса в 6 стиле Asymptote, и в нем возможно проводить компиляцию программ и получать сообщения об ошибках. Кроме того, этот блокнот имеет свойство накапливать информацию об используемых пользователем словах и в дальнейшем подсовывать их в виде подсказок. Остается воспользоваться автозавершением и скорость набора команд существенно увеличится. Таким образом, вы получаете полноценную среду разработки программ. Особенно это подойдет тем пользователям, которые не собираются размещать рисунки в документах TEX’а, а планируют их вставлять, например, в документ Word’а или хранить в отдельном файле для демонстрации. Для полноценной работы Asymptote необходимо установить программу Ghostscript, систему MikTEX и просмотрщики картинок; например, для просмотра рисунков в формате pdf – программу SumatraPDF. Возможны и другие варианты. Для предотвращения возможных сбоев не следует создавать программу в блокноте, используя формат txt. Asymptote может отказаться работать с таким файлом. Лучше взять какой-нибудь файл примера из директории Asymptote с расширением asy, перенести его в свою рабочую директорию, удалить содержимое и набрать в нем свою программу. Теперь о настройке блокнота notepad++ для работы с Asymptote. Необходимо выполнить следующие действия. • Вызвать notepad++ и открыть в нем какой-нибудь asy-файл с каким-нибудь текстом (неважно, будет ли он правильным с точки зрения синтаксиса Asymptote). • Подключить плагин NppExec, для чего войти в главное меню блокнота и выполнить цепочку Плагины → Plugin Manager → Show Plugin Manager. Найти в появившемся списке NppExec и отметить ее галочкой. • Щелкнуть мышкой по кнопке Install и на оба вопроса в двух диалоговых окнах ответить Да. • Войти в главное меню блокнота и выполнить Плагины → NppExec, после чего поставить галочки в позициях Show Console Dialog Console Commands History Save All Files on Execute Follow $(CURRENT_DIRECTORY) • Нажать F6 и после появления диалогового окна в поле Command(s) ввести код asy -f pdf -render 4 $(FILE_NAME) Нажать Save... и в поле Script name для сохранения pdf-файла ввести asy_pdf_render 4 Нажать Save и OK. В результате последнего действия произойдет компиляция программы. Если в ней нет ошибок, в pdf-просмотрщике появится откомпилированный рисунок; в противном случае в нижней части блокнота в разделе Console появится сообщение об ошибке. Дальнейшие компиляции выполняются при нажатии клавиш Ctrl+F6. Для красоты жизни можно добавить расцветку программ в соответствии с синтаксисом Asymptote. Для этого сначала надо в таблице сайта http://svn.gmaths.net найти файл расцветки UserDefineLang.xml и скачать его, щелкнув в таблице по Télécharger. Далее в блокноте 7 зайти в главное меню и выполнить Синтаксисы → Задать свой синтаксис... В диалоговом окне щелкнуть Импортир... и вызвать скачанный файл. Затем закрыть окно и закрыть и снова открыть notepad++. Зайти в Синтаксисы и выбрать asy. Файл программы раскрасится яркими (даже чересчур!) красками. Постоянно работающим в системе LATEX можно рекомендовать TeXStudio – бесплатный редактор, в котором предусмотрена полная поддержка Asymptote, включая расцветку синтаксиса. Так что в этом редакторе можно и документы набирать, и рисунки делать. Для вызова Asymptote надо войти в главное меню TeXStudio и выполнить Инструменты → Команды → Asymptote. Единственное, что можно добавить для удобства, так это клавишный вызов Asymptote. С этой целью надо перейти в главное меню и активировать следующую цепочку: Параметры → Конфигурация TeXstudio → Горячие клавиши. Затем раскрыть узлы дерева справа: Меню → Инструменты → Команды → Asymptote. Дважды щелкнуть мышью в графе Текущая и создать подходящий клавишный набор, например, Ctrl+Left. Естественно, все рассказанное проделывалось автором только в ОС Windows. И последнее. Если вас здорово перепугала первая часть книги, пропустите ее. Я так и сделал, когда самообучался Asymptote. Начните со второй части. Первую часть можно освоить потом, если понадобится создать какой-нибудь сложный рисунок. А в остальных случаях достаточно пользоваться ею как справочником по терминам, которые встречаются в остальных частях книги. 8 ×àñòü I Ïðîãðàììèðîâàíèå 9 Ãëàâà 1 Áàçîâûå òèïû äàííûõ Язык программирования в Asymptote подобен языкам программирования C, C++ и Java. Например, короткие комментарии обозначаются символами //, а более пространные – символами /* и */: // Ýòî - êîììåíòàðèé. x = x + 1; // Óâåëè÷èâàåì çíà÷åíèå ïåðåìåííîé íà 1. /* Çäåñü ïðèâîäèòñÿ îïèñàíèå ðèñóíêà ïîâåðõíîñòè, âûïîëíåííîãî â ñèñòåìå Asymptote */ Asymptote поддерживает следующие типы данных. void Тип функции, не имеющей аргументов. bool Обычный булевский тип со значениями true и false. Описание bool b = true; задает булевскую переменную b и инициализирует ее значением true. Если инициализация отсутствует, например, bool b;, то значением по умолчанию предполагается false. bool3 Расширенный булевский тип, принимающий значения true, default и false. По умолчанию инициализируется значением default. int Целое число, которое хранится в 64-битовом формате и принимает значения от −9 223 372 036 854 775 808 до 9 223 372 036 854 775 805 (т. е. от −263 до 263 − 3). Эти граничные значения хранятся в переменных intMin и intMax. real Число с плавающей точкой. По умолчанию инициализируется числом 0.0. Действительные числа имеют точность realEpsilon с realDigits значащими цифрами. Наименьшее положительное действительное число хранится в realMin, а наименьшее – в realMax. Могут быть полезны переменная inf и булевская функция isnan(real x), когда необходима маскировка исключений действительной арифметики с помощью опции командной строки -mask (по умолчанию в интерактивном режиме). pair Комплексное число, представляемое упорядоченной парой действительных компонент (x,y). Действительная и мнимая части пары z записываются как z.x и z.y. Их называют виртуальными членами пары; они не могут быть модифицированы напрямую. По умолчанию пара инициализируется значением (0.0,0.0). Комплексно сопряженное число можно получить несколькими способами: 10 Глава 1. Базовые типы данных 11 pair z = (3,4); z = (z.x,-z.y); z = z.x - I*z.y; z = conj(z); Здесь I – пара (0,1), conj – функция комплексного сопряжения. triple Упорядоченная тройка действительных чисел (x,y,z), используемая для изображений в трехмерном пространстве. Компоненты тройки v записываются как v.x, v.y и v.z. По умолчанию инициализируется значением (0.0,0.0,0.0). string Последовательность символов, соответствующих классу STL string. Строки заключаются в двойные кавычки ("), причем, должны удовлетворять следующим соответствиям, которые обеспечивают использование двойных кавычек в ТеХ’е: • \" соответствует " • \\ соответствует \\ Строки, заключенные в одинарные кавычки (’) должны отвечать тем же соответствиям, что и строки символов в ANSI C. • \' соответствует ’ • \" соответствует " • \? соответствует ? • \\ соответствует backslash • \a соответствует alert (ошибка) • \b соответствует пробел • \f соответствует прогон страницы • \n соответствует переход на новую строку • \r соответствует возврат каретки • \t соответствует табуляция • \v соответствует вертикальная табуляция • \0-\377 соответствует один из 8-ричных байтов • \x0-\xFF соответствует один из 16-ричных байтов По умолчанию строка считается пустой: "". Строки можно соединять с помощью оператора +. path Кубический сплайн, представляющий собой путь. Неявным представителем пути является nullpath. guide Нерешенный кубический сплайн, (список узлов и опорных точек кубического сплайна). Неявным инициализатором guide является nullguide. Тип guide похож на path, за исключением того, что вычисление кубического сплайна откладывается до времени рисования (когда он будет преобразован в путь). pen Перо. В Asymptote перьями задают стиль рисования. Перо, используемое по умолчанию, называется currentpen. Глава 1. Базовые типы данных 12 transform Преобразование. К этому типу данных относятся частные виды аффинных преобразований. picture Рисунок, картинка. Является основой рисования в пользовательских координатах и служит для манипуляций с частями изображения. По умолчанию рисование выполняется на картинке currentpicture. frame Полотно (холст). Холсты являются основой для рисования в PostScript-координатах. Ãëàâà 2 Ñëîæíûå òèïû äàííûõ 2.1 Ìàññèâû Добавление пары скобок [ ] к встроенному или пользовательскому типу делает последний массивом. Доступ к элементу i массива A может быть получен в виде A[i]. Задание отрицательного индекса массива считается ошибкой. Попытка получить элемент массива с индексом за границей индексов массива также вызывает ошибку. Тем не менее, введение нового элемента с таким индексом приводит к расширению массива с целью разместить в нем новый элемент. Возможна индексация массива A другим массивом B: массив A[B] формируется индексированием массива A элементами массива B. Декларация real[] A; создает пустой массив A нулевой длины. Пустые массивы следует отличать от null-массивов. Если мы набираем real[] A = null; то A вообще не может быть разыменован (null-массивы не имеют длины, из них нельзя читать и в них нельзя писать). Инициализируется массив следующим образом: real[] A={0,1,2}; Присвоение массива в Asymptote использует неглубокое копирование: копируется лишь указатель (если одна из копий модифицируется, то остальные также модифицируются). Функция copy, которая приводится ниже, применяет полное копирование. Каждый массив A типа T[ ] имеет следующие свойства. • int length • int cyclic • int[ ] keys • T push(T x) • void append(T[ ] a) • T pop() 13 Глава 2. Сложные типы данных 14 • void insert(int i ... T[ ] x) • void delete(int i, int j = i) • void delete() • bool initialized(int n) Свойство A.length означает длину массива. Установка A.cyclic = true указывает на то, что индексы массива должны быть приведены к текущей длине массива. Чтение из такого массива или запись в него не должны сопровождаться ошибками выхода за диапазон индекса или изменением длины массива. Свойство A.keys представляет собой массив целых, содержащий индексы инициализированных элементов массива A в возрастающем порядке. Таким образом, для массива длины n, у которого все элементы инициализированы, A.keys имеет вид {0, 1, . . . , n − 1}. Функции A.push и A.append добавляют свои аргументы в конец массива, в то время как A.insert(int i ... T[] x) вставляет x в массив в позицию i. Для удобства A.push возвращает вставленный элемент. Функция A.pop() выталкивает и возвращает последний элемент, в то время как A.delete(int i, int j = i) удаляет элементы с индексами из диапазона i ÷ j, уменьшая номера позиций всех элементов с бо́льшими номерами индексов. Если аргументы отсутствуют, A.delete() дает удобную возможность удаления всех элементов A. Процедура A.initialized(int n) может использоваться для проверки инициализации элемента с индексом n. Как и другие функции Asymptote, функции push, append, pop, insert, delete и initialized могут быть «отсоединены» от массивов и использоваться сами по себе. Рассмотрим примеры. • int[] A={1}; • A.push(2); // A = {1,2} • A.append(A); // A = {1,2,1,2} • f(3); // A = {1,2,1,2,3}. • write(g()); // Ïå÷àòàåò 3 • A.delete(0,1); // A = {2} • A.insert(1 ... A); // A = {2,2,3,3} • int f(int) = A.push; • int g() = A.pop; • A.delete(0); • A.insert(1,3); • A.insert(2,4,5); // A = {2,1,2} // A = {2,3} // A = {2,2,4,5,3,3} Суффикс [] может появляться и после имени переменной; иногда это удобно для объявления списка переменных и массивов одного типа. Например, real a,A[]; Глава 2. Сложные типы данных 15 объявляет a переменной типа real, а A – массивом типа real[]. В следующем списке встроенных функций T является обобщенным типом. Заметим, что встроенные функции alias, array, copy, concat, sequence, map и transpose, которые зависят от типа T[], определяются только после первого объявления переменной типа T[]. new T[] Создает новый пустой массив типа T[]. new T[] list Создает новый массив типа T[], заполненный элементами списка list (элементы последнего должны быть разделены запятыми). new T[n] Создает новый массив из n элементов типа T[]. Эти n элементов не инициализируются, если только сами не являются массивами (в этом случае они инициализируются как пустые массивы). T[] array(int n, T value, int depth = intMax) Возвращает массив, содержащий n копий value. Если value – массив, то для каждого элемента создается полная копия value. Если глубина копирования указана, то оно рекурсивно проводится для указанного числа уровней. int[] sequence(int n) Если n ≥ 1, возвращает массив {0, 1, . . . , n−1} (в противном случае возвращается null-массив). int[] sequence(int n, int m) Если m ≥ n возвращает массив {n, n + 1, . . . , m} (в противном случае возвращается null-массив). T[] sequence(T f(int), int n) Если n ≥ 1, формирует массив {fi : i = 0, 1, . . . , n − 1}, определяемый функцией T f(int) и целым n (в противном случае возвращается nullмассив). T[] map(T f(T), T[] a) Формирует массив применением функции f к каждому элементу массива a. Эквивалентно sequence(new T(int i) {return f(a[i]);},a.length). int[] reverse(int n) Если n ≥ 1, возвращает массив {n−1, n−2, . . . , 0} (в противном случае возвращается null-массив). int[] complement(int[] a, int n) Производит дополнение целочисленного массива a во множестве {0, 1, 2, . . . , n − 1}, так что b[complement(a,b.length)] производит дополнение b[a]. real[] uniform(real a, real b, int n) Если n ≥ 1, возвращает разбиение отрезка [a,b] на n одинаковых подынтервалов (в противном случае возвращается null-массив). int find(bool[], int n = 1) Возвращает индекс n-го значения true в массиве или −1, если ни одного такого значения нет. Если n отрицательно, поиск производится от конца массива до (−n)-го значения. int search(T[] a, T key) Для встроенных порядковых типов T отыскивает в отсортированном массиве a из n элементов такой индекс i, что a[i] ≤ key < a[i+1]. В случае невыполнения неравенства возвращает −1, если key меньше всех элементов a, и возвращает n-1, если key больше последнего элемента a или равен ему. int search(T[] a, T key, bool less(T i, T j)) В массиве a, отсортированном по возрастанию элементов, отыскивает такой элемент i, что i предшествует j, если less(i,j) принимает значение true. Глава 2. Сложные типы данных 16 T[] copy(T[] a) Делает полную копию массива a. T[] concat(... T[][] a) Формирует новый массив конкатенацией заданных одномерных массивов-аргументов. bool alias(T[] a, T[] b) Возвращает true, если массивы a и b идентичны. T[] sort(T[] a) Для встроенного порядкового типа T возвращает копию массива a, отсортированного в порядке возрастания элементов. T[][] sort(T[][] a) Для встроенного порядкового типа T возвращает копию массива a со строками, отсортированными по первому столбцу. Например: string[][] a={{"bob","9"},{"alice","5"},{"pete","7"},{"alice","4"}}; // Ñòðîêè ñîðòèðóþòñÿ ïî íóëåâîìó ñòîëáöó: write(sort(a)); дает alice 4 alice 5 bob 9 pete 7 T[] sort(T[] a, bool less(T i, T j)) Возвращает копию массива a, отсортированного в порядке возрастания таким образом, что элемент i предшествует элементу j, если less(i,j) принимает значение true. T[][] transpose(T[][] a) Транспонирует a. T[][][] transpose(T[][][] a, int[] perm) Возвращает транспозицию a, получаемую применением перестановки perm типа int[]{0,1,2} к индексам каждого элемента массива. T sum(T[] a) Для арифметического типа T возвращает сумму элементов массива a. Если T имеет булевский тип, подсчитывается число элементов true в массиве a. T min(T[] a) T min(T[][] a) T min(T[][][] a) Для встроенного порядкового типа T находит минимальный элемент массива a. T max(T[] a) T max(T[][] a) T max(T[][][] a) Для встроенного порядкового типа T находит максимальный элемент массива a. T[] min(T[] a, T[] b) Для встроенного порядкового типа T из массивов a и b одинаковой длины формирует массив, составленный из минимумов соответствующих элементов a и b. T[] max(T[] a, T[] b) Для встроенного порядкового типа T из массивов a и b одинаковой длины формирует массив, составленный из максимумов соответствующих элементов a и b. Глава 2. Сложные типы данных 17 pair[] pairs(real[] x, real[] y); Для массивов x и y одинаковой длины возвращает массив пар sequence(new pair(int i) {return (x[i],y[i]);},x.length). pair[] fft(pair[] a, int sign=1) Возвращает результат быстрого преобразования Фурье, выполненного над массивом a (если установлен опциональный пакет FFTW), используя заданный sign. Вот несколько простых примеров: int n=4; pair[] f=sequence(n); write(f); pair[] g=fft(f,-1); write(); write(g); f=fft(g,1); write(); write(f/n); real dot(real[] a, real[] b) Вычисляет скалярное произведение векторов a и b. pair dot(pair[] a, pair[] b) Возвращает скалярное произведение sum(a*conj(b)) комплексных чисел a и b. real[] tridiagonal(real[] a, real[] b, real[] c, real[] f) Решает периодическую трехдиагональную задачу Lx = f , получая решение x, где f – вектор размерности n, а L – матрица n × n: [ b[0] c[0] a[0] ] [ a[1] b[1] c[1] ] [ a[2] b[2] c[2] ] [ ... ] [ c[n-1] a[n-1] b[n-1] ] Для условий граничной задачи Дирихле (задаваемых u[-1] и u[n]) следует заменить f[0] на f[0]-a[0]u[-1] и f[n-1]-c[n-1]u[n], а затем положить a[0]=c[n-1]=0. real[] solve(real[][] a, real[] b, bool warn=true) Методом LU-декомпозиции находит решение x линейной системы ax = b, где a – матрица n × n, а b – вектор размерности n. Например: import math; real[][] a={{1,-2,3,0},{4,-5,6,2},{-7,-8,10,5},{1,50,1,-2}}; real[] b={7,19,33,3}; real[] x=solve(a,b); write(a); write(); write(b); write(); write(x); write(); write(a*x); Если a вырождена и warn имеет значение false, возвращается пустой массив. Если матрица a трехдиагональна, эффективнее использовать процедуру tridiagonal. Глава 2. Сложные типы данных 18 real[][] solve(real[][] a, real[][] b, bool warn=true) Решает систему ax = b, возвращая решение x, где a – матрица n×n, а b – матрица n×m. Если матрица a вырождена и warn имеет значение false, возвращается пустой массив. real[][] identity(int n); Возвращает единичную матрицу размера n × n. real[][] diagonal(... real[] a) Возвращает диагональную матрицу с диагональными элементами a. real[][] inverse(real[][] a) Вычисляет матрицу, обратную квадратной матрице a. real[] quadraticroots(real a, real b, real c); Находит действительные корни квадратного уравнения ax2 + bx + c = 0 и располагает их в порядке возрастания. Кратные корни заносятся в отдельный список. pair[] quadraticroots(explicit pair a, explicit pair b, explicit pair c); Находит комплексные корни квадратного уравнения ax2 + bx + c = 0. real[] cubicroots(real a, real b, real c, real d); Находит действительные корни кубического уравнения ax3 + bx2 + cx + d = 0. Кратные корни заносятся в отдельный список. В Asymptote имеется полный набор инструкций по выполнению арифметических и логических операторов над массивами векторного типа. Для ускорения вычислений эти инструкции реализованы на C++. Пусть заданы два массива real[] a={1,2}; real[] b={3,2}; тогда результатом сравнений a == b и a >= 2 в обоих случаях будет вектор {false, true}. Если требуется проверить выполнение сравнения для всех компонентов a и b, используют булевскую функцию all(a == b). Можно также применить условие вида (a >= 2) ? a : b, которое возвращает массив {3,2}, или write((a >= 2) ? a : null), возвращающее массив {2}. Все стандартные функции, встроенные в библиотеку libm† вида real(real) могут в качестве аргумента иметь и массив типа real. Это похоже на применение функции map. Как и другие встроенные функции, массивы могут быть прочитаны из файлов с помощью операции присваивания. В следующем примере программа file fin=input("test.txt"); real[] A=fin; читает переменные типа real в массив A, пока не будет достигнут конец файла (или не возникнет ошибка ввода-вывода). Такие свойства файлов как dimension, line, csv, word и read могут быть полезны и при чтении массивов. Например, если с помощью file line(bool b=true) установлен режим строки, то чтение будет прекращено при достижении ее конца: file fin=input("test.txt"); real[] A=fin.line(); Так как строка по умолчанию читается, пока не встретится ее конец, то режим строки не имеет никакого влияния на чтение массива строк. В то же время для чтения строк имеется режим пробелов file word(bool b=true), при котором чтение строки выполняется до появления пробела, а не конца строки: † Подключаемая в языке С библиотека стандартных математических функций. Глава 2. Сложные типы данных 19 file fin=input("test.txt").line().word(); real[] A=fin; Еще одним полезным режимом является режим запятой, file csv(bool b=true), при котором чтение строки происходит до встречи с разделителем-запятой: file fin=csv(input("test.txt")); real[] A=fin; Функцию file dimension(int) используют, чтобы ограничить количество прочитываемых значений : file fin=input("test.txt"); real[] A=dimension(fin,10); В массив A прочитывается 10 значений, если прежде не встретится конец файла Попытка прочитать за концом файла приводит к выводу сообщения об ошибке выполнения. Если количество прочитываемых значений ограничено нулем, это равносильно чтению до конца файла (или до конца строки в режиме строки). Дву- и трехмерные массивы базовых типов данных могут быть считаны подобно этому: file fin=input("test.txt"); real[][] A=fin.dimension(2,3); real[][][] B=fin.dimension(2,3,4); Опять-таки задание нулевого ограничения снимает ограничения на количество прочитываемых значений. Иногда размеры массива хранятся вместе с данными в целочисленных полях в начале массива. Поэтому одно-, дву- и трехмерные массивы могут быть прочитаны с помощью свойств read(1), read(2), read(3), соответственно: file fin=input("test.txt"); real[] A=fin.read(1); real[][] B=fin.read(2); real[][][] C=fin.read(3); Вывод одно-, дву- и трехмерных массивов базовых типов данных осуществляют функции write(file,T[]), write(file,T[][]), write(file,T[][][]), соответственно. 2.2 Ñðåçû ìàññèâîâ Asymptote позволяет использовать срезы массивов, синтаксис которых подобен синтаксису аналогичных структур в языке Python. Если A – массив, выражение A[m:n] возвращает новый массив, содержащий элементы A от элемента с номером m до элемента n−1. Например, int[] x={0,1,2,3,4,5,6,7,8,9}; int[] y=x[2:6]; // y={2,3,4,5}; int[] z=x[5:10]; // z={5,6,7,8,9}; Если левый индекс отсутствует, то он считается равным 0. Если отсутствует правый индекс, он берется равным длине массива. Если отсутствуют оба индекса, срез выполняется от начала до конца массива посредством глубокого копирования последнего. Примеры: Глава 2. Сложные типы данных 20 int[] x={0,1,2,3,4,5,6,7,8,9}; int[] y=x[:4]; // y={0,1,2,3} int[] z=x[5:]; // z={5,6,7,8,9} int[] w=x[:]; // w={0,1,2,3,4,5,6,7,8,9} - ìàññèâ, îòëè÷íûé îò ìàññèâà x. Для нециклического массива ни для одного из индексов нельзя задавать отрицательные значения. Если индексы превосходят длину массива, они усекаются до этой величины. Для циклических массивов срез A[m:n] по-прежнему означает элементы, индексированные числами из [m,n), но теперь допускаются как отрицательные индексы, так и индексы, превосходящие длину массива. Просто индексы идут по кругу: int[] x={0,1,2,3,4,5,6,7,8,9}; x.cyclic=true; int[] y=x[8:15]; // y={8,9,0,1,2,3,4}. int[] z=x[-5:5]; // z={5,6,7,8,9,0,1,2,3,4} int[] w=x[-3:17]; // w={7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6} Отметим, что для циклических массивов можно один и тот же элемент исходного массива включить в срез несколько раз. Независимо от цикличности или нецикличности исходного массива срез всегда нецикличен. Если левый и правый индексы среза одинаковы, в результате получится пустой массив. Если исходный массив пуст, то пустым будет и срез. Срез, у которого левый индекс превышает правый, вызовет ошибку. Для срезов можно выполнять присвоения, изменяя значения исходного массива. Если массив, присваиваемый срезу, имеет длину, меньшую, чем сам срез, для формирования нового массива некоторые элементы могут быть удалены или вставлены. Примеры: string[] toppings={"mayo", "salt", "ham", "lettuce"}; toppings[0:2]=new string[] {"mustard", "pepper"}; // Òåïåðü toppings={"mustard", "pepper", "ham", "lettuce"} toppings[2:3]=new string[] {"turkey", "bacon" }; // Òåïåðü toppings={"mustard", "pepper", "turkey", "bacon", "lettuce"} toppings[0:3]=new string[] {"tomato"}; // Òåïåðü toppings={"tomato", "bacon", "lettuce"} Если массив получают присвоением среза, взятого от этого массива, то копия исходного массива присваивается срезу. Т. о. запись x[m:n]=x равносильна записи x[m:n]=copy(x). Можно назначить x[m:m]=y, чтобы вставить содержимое массива y в массив x, начиная с позиции, непосредственно предшествующей x[m]. Для циклического массива срез является сочлененным, если он адресуется к элементам массива, номера которых идут до конца массива, а затем переходят на его начало. Например, если A – циклический массив длины 10, то A[8:12], A[-3:1] и A[5:25] являются сочлененными срезами, в то время как A[3:7], A[7:10], A[-3:0] и A[103:107] таковыми не являются. Для сочлененных срезов можно выполнять присвоения только тогда, когда число их элементов в точности равно числу присваиваемых им элементов, в противном случае возникает неопределенность, какой из новых элементов должен быть A[0], в результате чего возникает ошибка. Несочлененным срезам могут присваиваться массивы любой длины. Для циклического массива A выражение вида A[A.length:A.length] равносильно выражению A[0:0], так что в результате присвоения элементы будут вставлены в начало массива. В конец массива элементы вставляются с помощью A.append(). Не разрешается присваивать срезу циклический массив, у которого повторяются все его элементы. Глава 2. Сложные типы данных 21 2.3 Ñòðóêòóðû Пользователи могут создавать свои собственные типы данных вроде структур, так же как и собственные операторы, подобно тому, как это делается в C++. По умолчанию структуры имеют статус public (могут быть считаны и изменены в любом месте программы), но могут быть опционально объявлены как restricted (считываются, но писать в них можно лишь внутри структуры, где они определены) или private (считывают из них и пишут в них только внутри структуры). При определении структуры ключевое слово this может быть использовано как указатель на созданный экземпляр структуры. При инициализации выполняются все коды верхнего уровня структуры. Переменные содержат указатели на структуры. Рассмотрим пример: struct T { int x; } T foo=new T; T bar=foo; bar.x=5 Переменная foo содержит ссылку на экземпляр структуры T. Когда значение foo присваивается переменной bar, последняя тоже получает указатель на тот же экземпляр, что и foo. Присваивание bar.x=5 изменяет значение поля x и в экземпляре foo, так что foo.x тоже будет равно 5. Выражение new T создает новый экземпляр структуры T и возвращает указатель на этот экземпляр. При создании нового экземпляра выполнится код в теле структуры. Пример: int Tcount=0; struct T { int x; ++Tcount; } T foo=new T; Выражение new T не только сгенерирует новый экземпляр класса, но и увеличит Tcount. Выражение null может быть приведено к любому структурному типу, чтобы получить указатель, не указывающий ни на один экземпляр структуры. Попытка использовать поле с таким указателем ведет к ошибке. Функция bool alias(T,T) проверяет, не указывают ли два указателя структуры на один и тот же ее экземпляр (или оба – на null). В примере кода в начале раздела при создании нового экземпляра с помощью new T функция alias(foo,bar) дала бы значение true, а alias(foo,new T) вернула бы false. Булевские операторы == и != по определению равносильны alias и !alias, соответственно, но могут быть перекрыты для некоторых типов (например, при глубоком сравнении). Если структура T определена, переменная типа T автоматически инициализируется для создания нового экземпляра (с помощью new T). Тем не менее в момент определения структуры переменные типа T по умолчанию инициализируются значением null. Подобное поведение позволяет избежать бесконечной рекурсии при создании новых экземпляров. Пример: struct tree { int value; Глава 2. Сложные типы данных } 22 tree left; tree right; Следующий пример демонстрирует использование структур. struct S { real a=1; real f(real a) {return a+this.a;} } S s; // Èíèöèàëèçèðóåò s ñ ïîìîùüþ new S write(s.f(2)); // Âûâîäèòñÿ 3 S operator + (S s1, S s2) { S result; result.a=s1.a+s2.a; return result; } write((s+s).f(0)); // Âûâîäèòñÿ 2 Часто удобно иметь функции, конструирующие новые экземпляры структуры. Пусть определена структура Person: struct Person { string firstname; string lastname; } Person joe=new Person; joe.firstname="Joe"; joe.lastname="Jones"; Создание новой персоны довольно громоздко: потребовалось три строки для создания нового экземпляра и инициализации его полей (впрочем, при создании реальной особы требуется намного больше усилий). Мы можем упростить дело, определив конструктор Person(string,string): struct Person { string firstname; string lastname; } static Person Person(string firstname, string lastname) { Person p=new Person; p.firstname=firstname; p.lastname=lastname; return p; } Person joe=Person.Person("Joe", "Jones"); Глава 2. Сложные типы данных 23 Стало проще, но пришлось использовать идентификатор с расширением Person.Person. Если сразу после определения структуры добавить строку from Person unravel Person; можно обойтись без расширения: Person joe=Person("Joe", "Jones") Теперь конструктор стало использовать еще легче, но все эти определения требуют дополнительной работы. Если у вас в ходу несколько конструкторов, вы заметите, что приходится много писать повторяющегося кода. К счастью, разработчики Asymptote нашли способ автоматизировать большую часть процесса. Если в теле структуры встречается определение функции вида void operator init(args), Asymptote неявно создает конструктор с аргументами args, использующий упомянутую функцию void operator init для инициализации нового экземпляра структуры. То есть при этом фактически определяется следующий конструктор (пусть структура называется Foo): } static Foo Foo(args) { Foo instance=new Foo; instance.operator init(args); return instance; Этот конструктор передается в область видимости за пределами определения структуры, так что может быть задействован в дальнейшем без использования расширения в виде имени структуры. Наша персона теперь может быть реализована так: struct Person { string firstname; string lastname; } void operator init(string firstname, string lastname) { this.firstname=firstname; this.lastname=lastname; } Person joe=Person("Joe", "Jones"); Использование operator init при таком неявном определении конструкторов не следует путать с его использованием при определении значений по умолчанию для переменных. Действительно, в первом случае возвращаемый тип operator init должен быть void, а во втором, он должен быть (не void) типом переменной. Функция cputime() возвращает структуру процессорного времени с кумулятивными временами CPU, разбитыми на поля parent.user, parent.system, child.user и child.system. Для удобства инкрементные поля change.user и change.system фиксируют изменения в соответствующих общих родительских и дочерних временах CPU, начиная с момента последнего обращения к cputime(). Функция void write(file file=stdout, string s="", cputime c, string format=cputimeformat, suffix suffix=none); Глава 2. Сложные типы данных 24 выводит инкрементное пользовательское время CPU, следующее за “u”; инкрементное системное время CPU, следующее за s”; общее пользовательское время CPU, следующее за “U”; и общее системное время CPU, следующее за “S”. Приведение типов, во многом подобное тому, что имеется в C++, обеспечивает элегантную реализацию наследования структур, включая виртуальные функции: struct parent { real x; void operator init(int x) {this.x=x;} void virtual(int) {write(0);} void f() {virtual(1);} } void write(parent p) {write(p.x);} struct child { parent parent; real y=3; void operator init(int x) {parent.operator init(x);} void virtual(int x) {write(x);} parent.virtual=virtual; void f()=parent.f; } parent operator cast(child child) {return child.parent;} parent p=parent(1); child c=child(2); write(c); // Âûâîäèòñÿ 2 p.f(); c.f(); // Âûâîäèòñÿ 0 // Âûâîäèòñÿ 1 write(c.parent.x); // Âûâîäèòñÿ 2 write(c.y); // Âûâîäèòñÿ 3 Ãëàâà 3 Îïåðàòîðû 3.1 Àðèôìåòè÷åñêèå è ëîãè÷åñêèå îïåðàòîðû В Asymptote используются стандартные бинарные арифметические операторы. Тем не менее, если одно целое число делится на другое, оба аргумента перед делением преобразуются в действительные числа и результатом будет действительное частное (как оно обычно и подразумевается). Функция int quotient(int x, int y) возвращает наибольшее целое, меньшее или равное x/y. Во всех остальных случаях оба операнда приводятся к одному типу, который будет и типом результата: + Сложение. - Вычитание. * Умножение. / Деление. % Деление по модулю; знак результата совпадает со знаком делителя; в частности, выполняется q*quotient(p,q)+p%q == p для любого целого p и ненулевого целого q. Возведение в степень; если показатель степени (второй аргумент) – целое, используется рекурсивное умножение; в противном случае используется экспонента и логарифм; ** является синонимом . Определены также обычные логические операторы: == Равно. != Не равно. < Меньше. <= Меньше или равно. > Больше. >= Больше или равно. && Логическое «и» (правая часть не проверяется, если левая равна false). & Логическое «и» (правая часть всегда проверяется). 25 Глава 3. Операторы 26 || Логическое «или» (правая часть не проверяется, если левая равна true). | Логическое «или» (правая часть всегда проверяется). Исключающее «или», или операция сложения по модулю 2, или операция несовпадения: x^y = (x̄ ∧ y) ∨ (x ∧ ȳ) = (x̄ ∨ ȳ) ∧ (x ∨ y); возвращает значение true, если только один из операндов имеет значение true. ! Логическое «не». Еще имеются составные операторы присваивания +=, -=, *=, /=, %=, <<=, >>=, &=, |=, ^= Например, *= означает a=a*b. Функция T interp(T a, T b, real t) дает (1-t)*a+t*b для неинтегральных† встроенных арифметических типов T. Если a и b – перья, они сначала переводятся в одно и то же цветовое пространство. В Asymptote также определены битовые функции int AND(int,int), int OR(int,int), int XOR(int,int), int NOT(int), int CLZ(int) (подсчет количества лидирующих нулей) и int CTZ(int) (подсчет хвостовых нулей). 3.2 Ïðåôèêñíûå è ïîñòôèêñíûå îïåðàòîðû Как и в C, каждый из арифметических операторов +, -, *, /, % и может использоваться в самоприсваивании. Определены также префиксные операторы ++ (увеличить на 1) и -- (уменьшить на 1). Например, код int i=1; i += 2; int j=++i; равносилен коду int i=1; i=i+2; int j=i=i+1; Тем не менее постфиксные операторы, подобные i++ и i-- не определены (в силу возникновения неопределенности при использовании оператора сочленения путей --). В тех редких случаях, когда i++ и i-- действительно необходимы, можно применить выражения (++i-1) и (--i+1), соответственно. 3.3 Îïåðàòîðû, îïðåäåëÿåìûå ïîëüçîâàòåëåì Следующие символы могут быть использованы с именем operator для определения и переопределения операторов на структурах и встроенных типах: † Данное, описываемое как одно целое, называется интегральным, а описываемое с помощью нескольких понятий (например, мантиссы и экспоненты) – неинтегральным. В языке C к интегральным числовым данным относят char, int, short, long, signed, unsigned и enum, а к неинтегральным – float, double и long double. Глава 3. Операторы 27 - + / % ^ ! < > == != <= >= & | ^^ .. :: -- --- ++ << >> $ $$ @ @@ Операторы во второй строке имеют приоритет перед булевскими операторами <, >, <= и >=. Операторы пути типа .. могут быть перекрыты для написания функции, создающей новый путь из данного пути: guide dots(... guide[] g)=operator ..; guide operator ..(... guide[] g) { guide G; if(g.length > 0) { write(g[0]); G=g[0]; } for(int i=1; i < g.length; ++i) { write(g[i]); write(); G=dots(G,g[i]); } return G; } guide g=(0,0){up}..{SW}(100,100){NE}..{curl 3}(50,50)..(10,10); write("g=",g); 3.4 Óñëîâíûå îïåðàòîðû Asymptote поддерживает C-подобный условный оператор: real y = (x < 0) ? x+1 : 2.5; Применяется также оператор if как в упрощенном виде: if (<óñëîâèå>) <îïåðàòîð 1>; else <îïåðàòîð 2>; так и в более общем: if (<óñëîâèå>) {<îïåðàòîð 1>; ... <îïåðàòîð m>;} else {<îïåðàòîð m+1>; ... <îïåðàòîð n>;} Пример: if(x == 1.0) { write("x equals 1.0"); } else { write("x is not equal to 1.0"); } Глава 3. Операторы 28 3.5 Öèêëû Цикл for можно записать так: for (int i = 0; i < 10; ++i) { write(i); } Имеются также циклы while: while(k <= 5){ write(k); ++k; } и do-while: do {write(i); ++i;} while (i < 0); Цикл while выполняется до тех пор, пока логическое условие истинно. Цикл do-while выполняется по крайней мере один раз, даже если логическое условие не выполняется. Asymptote поддерживает такие же, как в C/C++, операторы while, do, break, continue, а также цикл, принятый в языке Java и организованный по элементам массива: int[] array={1,1,2,3,5}; for(int k : array) { write(k); } В случае двух вложенных циклов оператор break вызывает выход только из внутреннего цикла. Чтобы выйти сразу из обоих циклов, обычно добавляют булеву переменную и условный оператор: bool out = false; for (int i = 1; i <= 5; ++i){ for (int j = 1; j<=6; ++j){ write("Hello!"); // Hello! out = true; break; } if (out) break; write("Goodbye!"); } Поэтому Goodbye! не будет напечатано. 3.6 Áåçóñëîâíûé îïåðàòîð А вот безусловного-то оператора и нет! То бишь оператора типа goto! Ну, тех, кто привык писать коды в стиле структурного программирования, это сообщение не огорчит, а вот, как быть тем, кто привык к goto и отказаться от него – как отказаться от самого себя? Или есть еще такой вариант: имеется надежный, проверенный временем и эксплуатацией, алгоритм в виде программы с goto и очень хочется его использовать в Asymptote. А вот вникать в алгоритм Глава 3. Операторы 29 не хочется, лучше его как-то так, в автоматическом режиме, не задумываясь, переписать без goto. Для этого существуют разные некрасивые приемы, и с некоторыми мы сейчас познакомимся. Всяким там гуру этот раздел читать не рекомендуется – дабы не испортить себе настроение. Хуже всего, если оператор goto передает управление «назад», то есть оператору, который находится позади goto, часто за много операторов до goto по тексту программы. Предположим, имеется старинная программа на незнакомом языке, которая в общем виде выглядит так: start; M: A; IF out THEN [B; GOTO M]; F; halt; Можно догадаться, что start и halt, означают, соответственно, начало и конец программы; A, B и F – ряд операторов, что-то вычисляющих, в частности, параметры условия out, которое проверяет условный оператор, принимающий решение либо продолжить циклический процесс, либо завершить выполнение программы. Последняя, кстати, изображена в виде блоксхемы на рис. 3.1. Да, и еще, конечно, оператор GOTO, от которого следует избавиться, если мы хотим переписать данный код в языке Asymptote. start out A B No F halt Yes Рис. 3.1. Эта задача не является такой уж трудной, достаточно применить оператор do-while: do { A; if (out) B;} while (out); F; Более сложную задачу своего преобразования предлагает программа (см. также блоксхему на рис. 3.2) start; M1: A; M2: B; IF (comp) [D; GOTO M1]; C; IF (out) THEN GOTO M2; F; halt; Глава 3. Операторы 30 Yes start A comp B D No C out No F halt Yes Рис. 3.2. Если бы циклы, организованные с помощью GOTO, были вложены друг в друга, можно было бы дважды применить предыдущий прием. Но, поскольку этого нет, можно искусственно перетянуть стрелку возврата к блоку B к блоку A, но при этом придется проверять, откуда пришла стрелка: от comp или от out. Значит, придется ввести дополнительную логическую переменную. Зато получатся два теперь уже вложенных цикла, которые в Asymptote примут вид циклов do-while: bool test = false; do { do { if (! test) A; B; test = false; if (comp) D;} while (comp); C; if (out) test = true;} while (out); F; Более общие подходы к преобразованию блок-схем программ с goto в программы без этих операторов предлагались многими авторами. Введение дополнительных переменных, в том числе логических, рассматривалось, например, в [2] и [3]. Ãëàâà 4 Ôóíêöèè Функции Asymptote рассматриваются как переменные с сигнатурой† (нефункциональные переменные имеют null-сигнатуру). Допустимы переменные с одинаковыми именами, если они имеют различные сигнатуры. Аргументы передаются функции по значению. Чтобы передать аргумент по ссылке, надо включить его в структуру. Вот некоторые существенные черты функций Asymptote. 1. Переменные с сигнатурой и без нее различаются: int x, x(); x=5; x=new int() {return 17;}; x=x(); // Âûçûâàåòñÿ ôóíêöèÿ x() è ðåçóëüòàò åå ðàáîòû, 17, // ïðèñâàèâàåòñÿ ñêàëÿðó x. 2. Допустимы традиционные определения функции: int sqr(int x) { return x*x; } sqr=null; // Ôóíêöèÿ åùå ÿâëÿåòñÿ è ïåðåìåííîé. 3. Для разрешения неопределенности используется механизм приведения типов: int a, a(), b, b(); // Ïðàâèëüíî: îïðåäåëÿþòñÿ 4 ïåðåìåííûå. a=b; // Íåïðàâèëüíî: ïðèñâàèâàíèå äâóñìûñëåííî. a=(int) b; // Ïðàâèëüíî: íåîïðåäåëåííîñòü óñòðàíÿåòñÿ. (int) (a=b); // Ïðàâèëüíî: íåîïðåäåëåííîñòü óñòðàíÿåòñÿ. (int) a=b; // Íåïðàâèëüíî: Âûðàæåíèÿ ïðè ïðèâåäåíèè òèïîâ // íå ìîãóò áûòü L-çíà÷åíèÿìè, òî åñòü çíà÷åíèÿìè, // êîòîðûå ìîãóò áûòü äîñòóïíû ïðîãðàììíî ïðè // âûïîëíåíèè ïðîãðàììû (â íåêîòîðîì ðîäå // óêàçàòåëè). int c(); c=a; // Ïðàâèëüíî: ëèøü îäèí âîçìîæíûé àðãóìåíò. † В C++ сигнатура простой функции – это ее имя и последовательность типов ее аргументов; если функция является методом некоторого класса, то в сигнатуре участвует и имя класса. 31 Глава 4. Функции 32 4. Разрешены также анонимные, или, так называемые, функции высшего порядка† : typedef int intop(int); intop adder(int m) { return new int(int n) {return m+n;}; } intop addby7=adder(7); write(addby7(1)); // Âûâîäèòñÿ 8. 5. Можно переопределить функцию f, даже в случае, если f вызывается в предварительно объявленных функциях, назначив для этого другую (анонимную или именованную) функцию. Тем не менее, если f перегружается с помощью нового определения функции, предварительно объявленные функции будут по-прежнему обращаться к исходной версии f, как показано в следующем примере: void f() { write("hi"); } void g() { f(); } g(); // Âûâîäèò "hi" f=new void() {write("bye");}; g(); // Âûâîäèò "bye" void f() {write("overloaded");}; f(); // Âûâîäèò "overloaded" g(); // Âûâîäèò "bye" 6. Анонимные функции можно использовать для переопределения функциональных переменных, которые уже объявлены (и инициализированы как null-функции), но еще до конца не определены: void f(bool b); void g(bool b) { if(b) f(b); else write(b); } f=new void(bool b) { write(b); g(false); † Принимает в качестве параметра другую функцию или возвращает функцию в качестве результата. Глава 4. Функции 33 }; g(true); // Âûâîäèòñÿ true, çàòåì - false. Видимо, Asymptote является единственным языком программирования, который обрабатывает функции как переменные, но допускает перегрузку переменных, отличающихся лишь сигнатурой. Внутри функций допускается определение других функций, использующих внутренние переменные внешней функции: real f(real x){ real a = 2; write(a); // 2 void g(){ a = a + 3; write(a); // 5 return; } g(); return a + 5; } write(f(4)); // 10 Оператор return обязательно ставится в конце тела функции, даже если такой оператор в теле функции уже имеется: real f(real x){ if (x > 3) return 7777; return 3333; } Если return 3333 будет отсутствовать, Asymptote выдаст ошибку. Так что при обращении write(f(1)) получим 3333, а при write(f(5)) получим 7777. Asymptote разрешает рекурсивный вызов функции. Как и в С++, бесконечная рекурсия вызывает переполнение стека. 4.1 Àðãóìåíòû ïî óìîë÷àíèþ Асимптота поддерживает более гибкий механизм умолчания для аргументов функций, чем C++: аргументы могут появляться в любом месте прототипа функции† . Поскольку некоторые типы данных неявно приводятся к более сложным типам, часто можно избежать двусмысленности, упорядочивая аргументы функции от простейших к самым сложным. Например, функция real f(int a=1, real b=0) {return a+b;} для f(1) возвращает 1, а для f(1.0) возвращает 2.0. Значение аргумента по умолчанию определяется вычислением заданного выражения в том окружении, в котором определена вызываемая функция. † Прототипом функции в языке Си или C++ называется объявление функции, которое не содержит тела функции, но указывает ее имя, арность, типы аргументов и возвращаемый тип данных. Глава 4. Функции 34 4.2 Èìåíîâàííûå àðãóìåíòû Не всегда бывает легко запомнить порядок, в котором аргументы появляются в объявлении функции. Именованные (ключевое слово) аргументы упрощают вызов функций с несколькими аргументами. В отличие от C и C++ языков, присваивание аргументу функции интерпретируется как присваивание параметра с таким же именем в сигнатуре функции, но не в области локальной видимости. Параметр командной строки -d может быть использован для проверки программы в случае, если именованный аргумент может стать причиной ошибки локального присваивания. При сопоставлении аргументов сигнатурам прежде всего сравниваются ключевые слова, а затем неименованные аргументы сравниваются с неименованными формальными параметрами, как обычно. Например, int f(int x, int y) { return 10x+y; } write(f(4,x=3)); выводит 34, так как x уже присвоено значение, когда происходит сопоставление неименованного аргумента 4, и, таким образом, это значение получает y. В тех редких случаях, когда желательно присвоить значение локальной переменной внутри аргумента функции (как правило, это не является хорошим стилем программирования), лучше заключить присвоение в скобки. Например, для данного в предыдущем примере определения функции f код int x; write(f(4,(x=3))); равносилен командам int x; x=3; write(f(4,3)); которые дают на выходе 43. Помещением keyword перед именем параметра последний помечается как «только с ключевым словом», например, int f(int keyword x) или int f(int keyword x=77). Это приводит к необходимости при вызове функции, чтобы задать значение этого параметра, использовать именованный аргумент. Таким образом, f(x=42) – правильно, а f(25) – нет. Параметры с обязательным ключевым словом при определении функции должны располагаться после обычных параметров. Отметим еще одну техническую деталь. Поскольку допустимы переменные с одинаковыми именами, но разными сигнатурами, то следующий код int f(int x, int x()) { return x+x(); } int seven() {return 7;} допустим для f(2,seven) и результатом будет 9. Именованный аргумент сопоставляется первому неименованному формальному параметру с таким же именем, поэтому f(x=2,x=seven) является равносильным вызовом функции, а f(x=seven,2) таковым не является, так как первый аргумент ставится в соответствие первому формальному параметру, но int () не может быть преобразовано в int. Параметры по умолчанию не работают при сопоставлении с именованными аргументами, так что для функции Глава 4. Функции 35 int f(int x=3, int x()) { return x+x(); } обращение f(x=seven) будет неправильным, хотя, очевидно, f(seven) сработает. 4.3 Àðãóìåíòû . . . (rest) Rest-аргументы (предваряются многоточием) позволяют использовать функции с неопределенным числом аргументов: // Ýòà ôóíêöèÿ ñóììèðóåò ñâîè àðãóìåíòû. int sum(... int[] nums) { int total=0; for(int i=0; i < nums.length; ++i) total += nums[i]; return total; } sum(1,2,3,4); // Âîçâðàùàåò 10 sum(); // Âîçâðàùàåò 0 // Ýòà ôóíêöèÿ èç ïåðâîãî àðãóìåíòà âû÷èòàåò âñå îñòàëüíûå. int subtract(int start ... int[] subs) { for(int i=0; i < subs.length; ++i) start -= subs[i]; return start; } subtract(10,1,2); // Âîçâðàùàåò 7 subtract(10); // Âîçâðàùàåò 10 subtract(); // Íåïðàâèëüíî Помещение аргументов в rest-массив называется упаковкой. Можно задать rest-аргумент как список, так что функция subtract может быть переписана и так: int subtract(int start ... int[] subs) { return start - sum(... subs); } Можно сочетать rest-аргументы с обычными аргументами: sum(1,2,3 ... new int[] {4,5,6}); // Âîçâðàùàåò 21 Здесь создается шестиэлементный массив, который воспринимается sum как nums. Обратная операция, распаковка: subtract(... new int[] {10, 1, 2}); недопустима, так как формальному параметру start ничего не сопоставлено. Если ни один аргумент не упакован, то с rest-параметром связывается массив нулевой длины (но не null). Заметим, что аргументы по умолчанию игнорируются для формальных параметров rest, а rest-аргумент не связывается с ключевым словом. Глава 4. Функции 36 Иногда бывают полезны параметры, используемые лишь с ключевыми словами. Это дает возможность избежать присвоения rest-аргументов другим параметрам. Например, в следующем примере применение keyword позволяет в pnorm (1.0,2.0,0.3) предотвратить сопоставление 1.0 с ð. real pnorm(real keyword p=2.0 ... real[] v) { return sum(v^p)^(1/p); } Метод перегрузки в Asymptote похож на правила сопоставления функций, используемые в C++. При сопоставлении аргументов предпочтение отдается точному соответствию. Затем в порядке предпочтения идут преобразование типов и упаковка в массив. Если соответствию удовлетворяют два или больше кандидатов-аргументов, возникает ошибка неоднозначности. int f(path g); int f(guide g); f((0,0)--(100,100)); // Îáðàùåíèå ê ôóíêöèè ñîîòâåòñòâóåò âòîðîìó åå // îáúÿâëåíèþ; àðãóìåíòîì áóäåò guide int g(int x, real y); int g(real x, int x); g(3,4); // Íåîäíîçíà÷íîñòü; ïåðâîå îáúÿâëåíèå ôóíêöèè // ïðåäïî÷òèòåëüíåå äëÿ ïåðâîãî àðãóìåíòà, âòîðîå // äëÿ âòîðîãî int h(... int[] rest); int h(real x ... int[] rest); h(1,2); // Âûáèðàåòñÿ ñîîòâåòñòâèå âòîðîìó îáúÿâëåíèþ ôóíêöèè, òàê êàê // õîòÿ îíî òðåáóåò ïðåîáðàçîâàíèÿ òèïà, íî ýòî ïðåäïî÷òèòåëüíåå, // ÷åì óïàêîâêà â ìàññèâ int i(int x ... int[] rest); int i(real x, real y ... int[] rest); i(3,4); // Íåîäíîçíà÷íîñòü; ïåðâîå îáúÿâëåíèå ôóíêöèè ïðåäïî÷òèòåëüíåå // äëÿ ïåðâîãî àðãóìåíòà, âòîðîå - äëÿ âòîðîãî 4.4 Ìàòåìàòè÷åñêèå ôóíêöèè Asymptote располагает встроеннными версиями действительных математических функций типа real (real) из библиотеки libm; имеются в виду sin, cos, tan, asin, acos, atan, exp, log, pow10, log10, sinh, cosh, tanh, asinh, acosh, atanh, sqrt, cbrt (кубический корень), fabs, expm1 (ex − 1), log1p (ln(1 + x)), как и тождественной функцией identity. Определены также функции Бесселя порядка n первого рода Jn(int n,real) и второго рода Yn(int n,real), гамма-функция gamma, функция ошибок erf, Z x 2 2 erf(x) = √ e−t dt, π 0 и дополнительная функция ошибок erfc, 2 erfc(x) = √ π Z ∞ x 2 e−t dt = 1 − erf(x). Глава 4. Функции 37 Включены в обиход также стандартные типа real (real,real) функции atan2 (обращение вида atan2(y,x) возвращает y/x в радианах), hypot (возвращает длину гипотенузы при заданных двух катетах), fmod, remainder (обращения с аргументами (x,y) к этим функциям возвращает остаток от деления x/y). Функции degrees(real radians) и radians(real degrees) переводят радианы в градусы и обратно. Функция Degrees(real radians) выдает угол в градусах из интервала [0;360). Для удобства в Asymptote имеются разновидности стандартных тригонометрических функций Sin, Cos, Tan, aSin, aCos, aTan, которые работают не с радианами, а с градусами. Имеются также комплексные версии функций sqrt, sin, cos, exp, log, gamma. Функции floor, ceil и round отличаются от своих обычных аналогов тем, что возвращают целое, а не действительное число (обычно именно это и требуется). Функции Floor, Ceil и Round, соответственно, действуют аналогично, за исключением того, что при невозможности преобразовать результат в подходящее целое они возвращают intMax для положительного аргумента и intMin – для отрицательного, не генерируя переполнения. Определена также функция sgn, которая возвращает знак своего действительного аргумента в виде одного из целых чисел −1, 0 или 1. Имеется функция abs(int) и функция abs(real), аналогичная fabs(real), и функция abs(pair), аналогичная length(pair). Случайные числа инициализируются процедурой srand(int) и генерируются функцией rand(), которая возвращает случайное целое из диапазона от 0 до randMax – наибольшего целого. Функция unitrand() производит случайное число, равномерно распределенное на отрезке [0; 1]. Для получения гауссовских случайных чисел служит функция Gaussrand и ряд статистических процедур, включенных в файл stats.asy. В этом же файле находится функция вычисления факториала factorial(int n) и функция choose(int n, int k), возвращающая n!/(k!(n-k)!). Если Asymptote конфигурирована с библиотекой GNU Scientific Library† (GSL), доступной на сайте http://www .gnu .org /software /gsl /, то можно подключить модуль gsl, содержащий функции Эйри Ai(real), Bi(real), Ai_deriv(real), Bi_deriv(real), zero_Ai(int), zero_Bi(int), zero_Ai_deriv(int), zero_Bi_deriv(int); функции Бесселя I(int, real), K(int, real), j(int, real), y(int, real), i_scaled(int, real), k_scaled(int, real), J(real, real), Y(real, real), I(real, real), K(real, real), zero_J(real, int); эллиптические функции F(real, real), E(real, real), P(real, real); эллиптические функции Якоби real[] sncndn(real,real); экспоненциальные/тригонометрические интегралы Ei, Si, Ci; полиномы Лежандра Pl(int, real); дзета-функцию Римана zeta(real). Например, для вычисления интегрального синуса от 1.0 следует набрать import gsl; write(Si(1.0)); Asymptote также предоставляет пользователю несколько численных процедур общего назначения. real newton(int iterations=100, real f(real), real fprime(real), real x, bool verbose=false); Для поиска корня действительной дифференцируемой функции f используется метод Ньютона-Рафсона, где fprime – производная заданной функции, x – начальная точка поиска. Если verbose=true, выводится диагностика каждой итерации. Если число итераций превзошло разрешенный максимум (iterations), возвращается realMax. † Библиотека численного анализа для C и C++. Глава 4. Функции 38 real newton(int iterations=100, real f(real), real fprime(real), real x1, real x2, bool verbose=false); Для поиска корня действительной дифференцируемой функции f на отрезке [x1,x2] (на концах которого функция имеет значения противоположных знаков) применяется комбинированный метод Ньютона-Рафсона и бисекции, где fprime – производная заданной функции. Если verbose=true, выводится диагностика каждой итерации. Если число итераций превзошло разрешенный максимум (iterations), возвращается realMax. real simpson(real f(real), real a, real b, real acc=realEpsilon, real dxmax=b-a) Вычисляет интеграл от f в пределах от a до b методом Симпсона с адаптацией. 4.5 Ôóíêöèè, îïðåäåëåííûå äëÿ ïàð pair conj(pair z) Возвращает число, комплексно сопряженное числу z. real length(pair z) Вычисляет модуль комплексного числа z. Синонимом length(pair) является функция abs(pair). real angle(pair z, bool warn = true) Возвращает аргумент комплексного числа z в радианах в интервале [-pi,pi] или 0, если warn имеет значение false, а z = (0,0) (а не генерирует ошибку). real degrees(pair z, bool warn = true) Возвращает аргумент комплексного числа z в градусах в интервале [0,360) или 0, если warn имеет значение false, а z = (0,0) (а не генерирует ошибку). pair unit(pair z) Возвращает единичный вектор направления, определяемого парой z. pair expi(real angle) Возвращает единичный вектор направления, определяемого углом angle, заданным в радианах. pair dir(real degrees) Возвращает единичный вектор направления, определяемого углом angle, заданным в градусах. real xpart(pair z) Возвращает z.x. real ypart(pair z) Возвращает z.y. pair realmult(pair z, pair w) Выполняет покомпонентное умножение пар z и w, формируя пару (z.x*w.x,z.y*w.y). real dot(explicit pair z, explicit pair w) Вычисляет скалярное произведение пар z и w по правилу z.x*w.x+z.y*w.y. pair minbound(pair z, pair w) Возвращает (min(z.x,w.x),min(z.y,w.y)). pair maxbound(pair z, pair w) Возвращает (max(z.x,w.x),max(z.y,w.y)). Глава 4. Функции 39 4.6 Ôóíêöèè, îïðåäåëåííûå äëÿ òðîåê real length(triple v) Находит модуль вектора v. Синонимом length(triple) является функция abs(triple). real polar(triple v, bool warn = true) Возвращает кошироту† вектора v в радианах или 0, если warn имеет значение false, а v = O (а не генерирует ошибку). real azimuth(triple v, bool warn = true) Возвращает долготу‡ вектора v в радианах или 0, если warn имеет значение false и v.x = v.y = 0 (а не генерирует ошибку). real colatitude(triple v, bool warn = true) Возвращает кошироту вектора v в градусах или 0, если warn имеет значение false, а v = O (а не генерирует ошибку). real latitude(triple v, bool warn = true) Возвращает широту†† вектора v в градусах или 0, если warn имеет значение false, а v = O (а не генерирует ошибку). real longitude(triple v, bool warn = true) Возвращает долготу вектора v в градусах или 0, если warn имеет значение false и v.x = v.y = 0 (а не генерирует ошибку). triple unit(triple v) Возвращает единичный вектор направления, определяемого тройкой v. triple expi(real polar, real azimuth) Находит единичный вектор направления, определяемого парой (polar,azimuth), заданной в радианах. triple dir(real colatitude, real longitude) Возвращает единичный вектор направления, определяемого парой (colatitude,longitude), заданной в градусах. real xpart(triple v) Возвращает v.x. real ypart(triple v) Возвращает v.y. real zpart(triple v) Возвращает v.z. real dot(triple u, triple v) Вычисляет скалярное произведение троек u и v по правилу u.x*v.x+u.y*v.y+u.z*v.z. triple cross(triple u, triple v) Вычисляет векторное произведение троек u и v по правилу (u.y*v.z-u.z*v.y,u.z*v.x-u.x*v.z,u.x*v.y-v.x*u.y). triple minbound(triple u, triple v) Возвращает (min(u.x,v.x),min(u.y,v.y),min(u.z,v.z)). triple maxbound(triple u, triple v) Возвращает (max(u.x,v.x),max(u.y,v.y),max(u.z,v.z)). † Угол между осью Oz и радиус-вектором точки в диапазоне от 0◦ до 180◦ . Угол между осью Ox и проекцией радиус-вектора точки на плоскость xOy в диапазоне от −180◦ до 180◦ . †† Угол между радиус-вектором точки и плоскостью xOy. ‡ Глава 4. Функции 40 4.7 Ñòðîêîâûå ôóíêöèè int length(string s) Возвращает длину строки s. int find(string s, string t, int pos = 0) Возвращает позицию первого вхождения строки t в строку s после позиции pos (включительно), или −1, если t не является подстрокой строки s. int rfind(string s, string t, int pos = -1) Возвращает позицию последнего вхождения строки t в строку s до позиции pos (включительно, причем, если pos=-1, то это – конец строки s), или −1, если t не является подстрокой строки s. string insert(string s, int pos, string t) Вставляет строку t в строку s в позицию pos. string erase(string s, int pos, int n) Удаляет строку длины n (если n = −1, то до конца строки s) из строки s, начиная с позиции pos. string substr(string s, int pos, int n = -1) Возвращает подстроку строки s длины n (если n=-1, то до конца строки s), начиная с позиции pos. string reverse(string s) Обращает порядок символов в строке s на противоположный. string replace(string s, string before, string after) Заменяет в строке s все вхождения строки before на строку after. string replace(string s, string[][] table) По таблице-массиву table, состоящему из пар {before,after}, вхождения строк before заменяются соответствующими строками after. string[] split(string s, string delimiter = " ") Формирует массив строк, расщепляя строку s c помощью разделителя delimiter (пустой delimiter означает пробел, а сдвоенные разделители отбрасываются). string format(string s, int n, string locale = " ") Возвращает строку, содержащую n, отформатированное в соответствии со стилем С, заданным в строке s при использовании локаля† locale (или использовании текущего локаля, если заданная строка пуста). string format(string s = defaultformat, string s = defaultseparator, real x, x string lokale = " ") Возвращает строку, содержащую x, отформатированной в соответствии со стилем С, заданным в строке s при использовании локаля locale (или использовании текущего локаля, если заданная строка пуста, повторяя поведение С-функции fprintf), за исключением случая, когда допускается лишь одно поле. Финальные нули по умолчанию не удаляются (если не указан #), а (если формат строки определен математическим режимом) для верстки математических выражений используется TEX, если defaultseparator = "\!\times\!";. int hex(string s) Преобразует 16-ричную строку s в целое число. int ascii(string s) Возвращает ASCII-код первого символа строки s. string string(real x, int digits = realDigits) Преобразует x в строку, используя точность digits и С-локаль. † Локаль – набор параметров, включая набор символов, язык пользователя, страну, часовой пояс, а также другие предустановки. Глава 4. Функции 41 4.8 Ôóíêöèè äëÿ ïóòåé path В простейшем случае path (путь) представляет собой полностью рассчитанный кубический сплайн. Его неявным инициализатором является nullpath. pair accel(path p, int t, int sign=0); Если sign < 0, возвращает входящее ускорение для узла t пути p; если sign > 0, возвращает выходящее ускорение. Если sign = 0, возвращает среднее значение этих ускорений. pair accel(path p, real t); Возвращает ускорение в точке t пути p. real arclength(path p); Возвращает длину (в пользовательских координатах) кусочно линейной или кубической кривой, представляющей путь p. real arcpoint(path p, real L); Возвращает point(p,arctime(p,L)). real arctime(path p, real L); Возвращает значение параметра пути, действительное число между 0 и длиной пути, в смысле point(path p, real t), для которого длина части пути от его начала равна L. path buildcycle(... path[] p); Возвращает путь, окружающий область, ограниченную не менее, чем двумя, последовательно пересекающимися путями. Пути вводятся списком p. slice cut(path p, path knife, int n); Возвращает в виде структуры struct slice { path before,after; } части пути p до и после его n-го пересечения с путем knife. В случае отсутствия пересечения весь путь считается before. Аргумент n трактуется как модуль числа пересечений. bool cyclic(path p); Возвращает true, если путь p цикличен. pair dir(path p, int t, int sign=0, bool normalize=true); Если sign < 0, возвращается направление (в виде пары) касательной, входящей в узел t пути p; если sign > 0, возвращается направление выходящей касательной. Если sign = 0, возвращается среднее значение этих двух направлений. pair dir(path p, real t, bool normalize=true); Дает направление касательной к пути p в точке между узлами floor(t) и floor(t)+1, отвечающей на кубическом сплайне параметру t-floor(t). pair dir(path p); Возвращает dir(p,length(p)). pair dir(path p, path q); Возвращает unit(dir(p)+dir(q)). real dirtime(path p, pair z); Возвращает первое значение параметра пути, действительное число между 0 и длиной пути, в смысле point(path p, real t), для которого касательная к пути имеет направление пары z. Если последнее не происходит, возвращает −1. pair extension(pair P, pair Q, pair p, pair q); Возвращает точку пересечения продолжений отрезков P--Q и p--q или (infinity,infinity), если отрезки не пересекаются. Глава 4. Функции 42 slice firstcut(path p, path knife); Равносильна cut(p,knife,0); bool interior(int windingnumber, pen fillrule); Выдает true, если windingnumber соответствует внутренней точке области действия fillrule. bool inside(path p, pair z, pen fillrule=currentpen); Возвращает true, если точка z лежит внутри области, ограниченной циклическим путем p и соответствующей действию fillrule, или на ее границе. int inside(path p, path q, pen fillrule=currentpen); Возвращает 1, если циклический путь p полностью охватывает путь q с учетом fillrule; возвращает −1, если циклический путь q полностью охватывает p; и возвращает 0 в противном случае. pair inside(path p, pen fillrule=currentpen); Возвращает точку, которую охватывает циклический путь p, с учетом fillrule. real[]intersect(path p, path q, real fuzz=-1); Если p и q имеют хотя бы одну точку пересечения, возвращает действительный массив длины 2, элемент которого [0] содержит значение параметра для пути p, а элемент [1] – значение параметра для пути q, при которых эти пути пересекаются (в смысле point(path, real)). Абсолютная погрешность вычислений определяется параметром fuzz, но, если fuzz < 0, – машинной точностью. Если пути не пересекаются, возвращает действительный массив длины 0. pair intersectionpoint(path p, path q, real fuzz=-1); Возвращает точку пересечения путей point(p,intersect(p,q,fuzz)[0]). pair[] intersectionpoints(path p, path q, real fuzz=-1); Возвращает массив, который содержит все точки пересечения путей p и q. real[][]intersections(path p, path q, real fuzz=-1); Возвращает значения параметров для всех (если только их не бесконечно много) точек пересечения путей p и q в виде отсортированного массива действительных массивов длины 2. Абсолютная погрешность вычислений определяется параметром fuzz, но, если fuzz < 0, – машинной точностью. real[]intersections(path p, explicit pair a, explicit pair b, real fuzz=-1); x Возвращает значения параметра для всех (если только их не бесконечно много) точек пересечения пути p с (бесконечной) прямой, проходящей через точки a и b, в виде отсортированного массива. Абсолютная погрешность вычислений определяется параметром fuzz, но, если fuzz < 0, – машинной точностью. slice lastcut(path p, path knife); Равносильна cut(p,knife,-1); int length(path p); Число сегментов (линейных или сплайновых) в пути p. Если путь p – циклический, то это то же самое, что и число узлов в p. pair max(path p); Возвращает пару (right, top), которая является правой верхней вершиной минимального прямоугольника, заключающего в себе путь p. real[] maxtimes(path p); Возвращает массив длины 2, содержащий значения параметра для пути p, при которых последний достигает максимальных горизонтальных и вертикальных уровней. pair midpoint(path p); Возвращает точку в середине пути p. pair min(path p); Возвращает пару (left, bottom), которая является левой нижней вершиной минимального прямоугольника, заключающего в себе путь p. Глава 4. Функции 43 real[] mintimes(path p); Возвращает массив длины 2, содержащий значения параметра для пути p, при которых последний достигает минимальных горизонтальных и вертикальных уровней. bool piecewisestraight(path p) Возвращает true, если путь p является кусочно постоянным. pair point(path p, int t); Если p представляет собой циклический путь, возвращаются координаты узла t mod length(p). В противном случае – координаты узла t, за исключением двух случаев: если t < 0, возвращается point(0), а при t > length(p) возвращается point(length(p). pair point(path p, real t); Возвращает координаты точки, расположенной между узлами floor(t) и floor(t)+1, отвечающей на кубическом сплайне параметру t-floor(t). Если t лежит вне диапазона [0,length(p)], то вначале производится редукция по модулю length(p), на чем для циклического пути вычисления заканчиваются; в противном случае еще выполняется преобразование к соответствующей концевой точке. pair precontrol(path p, int t); Возвращает предопорную точку узла t пути p. pair precontrol(path p, real t); Возвращает эффективную предопорную точку пути p для значения параметра t. pair postcontrol(path p, int t); Возвращает постопорную точку узла t пути p. pair postcontrol(path p, real t); Возвращает эффективную постопорную точку пути p для значения параметра t. real radius(path p, real t); Возвращает радиус кривизны в точке t пути p. pair relpoint(path p, real l); Возвращает точку на пути p, соответствующую относительной доле l длины пути arclength. real reltime(path p, real l); Возвращает значение параметра пути p, для которого относительная доля длины пути по сравнению с arclength равна l. path reverse(path p); Возвращает путь, который проходится в обратном направлении. int size(path p); Число узлов пути p. Если p цикличен, то это то же, что и length(p). bool straight(path p, int i); Возвращает true, если часть пути между узлами i и i+1 является отрезком прямой. path[] strokepath(path g, pen p=currentpen); Возвращает массив путей, который заполняет PostScript, вычерчивая путь g пером p. path subpath(path p, int a, int b); Возвращает подпуть пути p от узла a до узла b. Если a > b, то направление пути будет обратным. path subpath(path p, real a, real b); Возвращает подпуть пути p от значения параметра a до значения параметра b в смысле point(path, real). Если a > b, то направление пути будет обратным. real[] times(path p, real x); Возвращает все значения параметра для пути p, при которых последний пересекает вертикальную прямую, проходящую через точку (x,0). Глава 4. Функции 44 real[] times(path p, explicit pair z); Возвращает все значения параметра для пути p, при которых последний пересекает горизонтальную прямую, проходящую через точку (0,z.y). int windingnumber(path p, pair z); Возвращает порядок (число обходов, винтовое число) циклического пути p относительно точки z. Порядок пути положителен, если если путь обходит z против часовой стрелки. Если z принадлежит p, возвращается константа undefined (представляемая наибольшим нечетным целым). 4.9 Ôóíêöèè äëÿ ïóòåé guide Путь guide представляет собой нерешенный кубический сплайн (список узлов и опорных точек кубического сплайна). Неявным инициализатором для guide является nullpath; он полезен при построении guide с помощью цикла. pair[] controlSpecifier(guide g, int i); Если сегмент g между узлами i и i+1 имеет входящую и выходящую опорные точки, они возвращаются в качестве, соответственно, элементов 0 и 1 двухэлементного массива. В противном случае возвращается пустой массив. real[] curlSpecifier(guide g); Возвращает массив, содержащий начальный (как элемент 0) и конечный (как элемент 1) загибы (раздел 7.2) для g. bool cyclic(guide p); Аналог cyclic(path p). pair[] dirSpecifier(guide g, int i) Возвращает массив пар длины 2, содержащий выходящее (элемент 0) и входящее (элемент1) направления, определенные для сегмента g между узлами i и i+1. Возвращает (0,0), если ни одно из направлений не определено. int length(guide g); Аналог length(path p). pair point(guide g, int t); Аналог point(path p). guide reverse(guide g); Аналог reverse(path p). Если g цикличен и содержит еще один цикл, он сначала преобразуется в path, а затем выполняется обращение обхода. Если g не цикличен, но содержит внутренний цикл, только последний превращается в path, а затем выполняется обращение. Если внутренние циклы отсутствуют, превращение в path не происходит. int size(guide g); Аналог size(path p). tensionSpecifier tensionSpecifier(guide g, int i); Для сегмента g возвращает натяжение (раздел 7.2) между узлами i и i+1. Компоненты типа tensionSpecifier доступны в качестве виртуальных членов in, out и atLeast. 4.10 Ñèñòåìíûå ôóíêöèè string locale(string s = " ") Назначает локаль по данной строке, если она непуста, и возвращает текущий локаль. Глава 4. Функции 45 string time(string format = "%a %b %d %T %Z %Y") Возвращает текущее время, отформатированное процедурой strftime в ANSI в соответствии со строкой format и с использованием текущего локаля. Т. о. time(); time("\%a \%b \%d \%H:\%M:\%S \%Z \%Y"); равносильно получению текущего времени с помощью команды date в системе UNIX в формате, принятом по умолчанию. int seconds(string t = " ", string format = " ") Возвращает время в секундах после «эры Unix»‡ (Thu Jan 01 00:00:00 UTC 1970), определяемое в ANSI С-процедурой strptime в формате, задаваемом строкой format и текущим локалем, или текущее время, если строка t пуста. Заметим, что расширение "%Z"для POSIX-спецификации strptime игнорируется текущей библиотекой GNU C. При возникновении ошибки возвращается значение −1 . Вот несколько примеров: seconds("Mar 02 11:12:36 AM PST 2007","\%b \%d \%r PST \%Y"); seconds(time("\%b \%d \%r \%z \%Y"),"\%b \%d \%r \%z \%Y"); seconds(time("\%b \%d \%r \%Z \%Y"),"\%b \%d \%r "+time("\%Z")+" \%Y"); 1+(seconds()-seconds("Jan 1","\%b \%d"))/(24*60*60); В последнем примере возвращается текущее время, отсчитываемое от начала года. string time(int seconds, string format ="%a %b %d %T %Z %Y") Возвращает время в секундах от «Эры UNIX», определяемое в ANSI С-процедурой strptime в формате, задаваемом строкой format и текущим локалем. Например, чтобы получить момент времени, случившийся 24 часа назад, надо выполнить time(seconds()-24*60*60); int system(string s) x int system(string[] s) Если safe установлено в false, вызывается подходящая системная команда s. void asy(string format, bool overwrite = false ...string[] s) Обрабатывает имена файлов, помещенные в массив s, используя формат format, перекрывая выходной файл, только если overwriting имеет значение true. void exit() Осуществляет выход (с нулевым кодом возврата в пакетном режиме). void sleep(int seconds) Вызывает паузу в течение заданного числа секунд. void usleep(int microseconds) Вызывает паузу в течение заданного числа микросекунд. void beep() Генерирует звуковой сигнал. 4.11 Ïðèâåäåíèå òèïîâ Asymptote неявно преобразует int в real, int в pair, real в pair, pair в path, pair в guide, path в guide, guide в path, real в pen, pair[] в guide[], pair[] в path[], path в path[] и guide в path[] наряду с различными трехмерными преобразованиями типов, определенных в модуле three.asy. Неявное приведение типов применяется автоматически при выполнения ‡ «Эра UNIX» (англ. Unix Epoch) отсчитывается от полночи (по UTC) с 31 декабря 1969 г. на 1 января 1970 г. Глава 4. Функции 46 присваивания и при сопоставлении аргументов вызываемой функции с ее сигнатурой. Неявное приведение можно запретить, объявив explicit для некоторых переменных в сигнатуре функции. Например, чтобы избежать неоднозначности при вызове функции, возвращающей 0: int f(pair a) {return 0;} int f(explicit real x) {return 1;} write(f(0)); Другие преобразования, скажем, real в int или real в string, требуют явного приведения: int i=(int) 2.5; string s=(string) 2.5; real[] a={2.5,-3.5}; int[] b=(int []) a; write(stdout,b); // Âûâîäèòñÿ 2,-3 Для типов, определенных пользователем, преобразование типов выполняется с помощью operator cast: struct rpair { real radius; real angle; } pair operator cast(rpair x) { return (x.radius*cos(x.angle),x.radius*sin(x.angle)); } rpair x; x.radius=1; x.angle=pi/6; write(x); // Âûâîäèòñÿ (0.866025403784439,0.5) При определении новых операторов приведения, следует проявлять осторожность. Допустим, в программе необходимо все целые числа представлять кратными 100. Предположим, что вначале их преобразуют в действительные числа, а затем умножают на 100. Однако решение «в лоб» real operator cast(int x) {return x*100;} приводит к бесконечной рекурсии, так как результат x*100 вызывает сам себя для перевода из целого в действительное число. Вместо этого можно использовать стандартное приведение int к real: real convert(int x) {return x*100;} real operator cast(int x)=convert; Явное приведение выполняется аналогично с использованием operator ecast. Глава 4. Функции 47 4.12 Èìïîðò Хотя Asymptote поддерживает множество возможностей по умолчанию, для некоторых приложений требуются специфические возможности, предоставляемые в Asymptote внешними модулями. Например, строки access graph; graph.axes(); рисуют оси x и y на плоскости. Первая команда ищет модуль graph в глобальном словаре модулей и помещает его в новую переменную под именем graph. Модуль является структурой, так что к его полям можно обращаться, как к полям обычной структуры. Часто удобнее использовать имена функций модуля, не обращаясь к его имени. Код from graph access axes; добавляет поле axes модуля graph в локальное пространство имен, после чего можно писать просто axes(). Если данное имя перекрыто, добавляются все типы и переменные этого имени. Для добавления более одного имени используется список с разделителями-запятыми: from graph access axes, xaxis, yaxis; Чтобы добавить в локальное пространство имен все типы и поля из разделов модуля, отличных от private, можно использовать wildcard-нотацию† : from graph access *; Аналогично можно добавить в локальную область типы структуры и ее поля из разделов, отличных от private, используя ключевое слово unravel: struct matrix { real a,b,c,d; } real det(matrix m) { unravel m; return a*d-b*c; } Вместо этого можно определить отдельные поля: } real det(matrix m) { from m unravel a,b,c as C,d; return a*d-b*C; Команда import graph; является удобной заменой команд access graph; unravel graph; † Wildcard-нотацией называется метод описания поискового запроса с использованием метасимволов (символовджокеров). Глава 4. Функции 48 т е. import graph сначала загружает модуль в структуру под названием graph, а затем добавляет его типы и поля из разделов, отличных от private, в локальную область. Таким образом, если имя переменной (или функции) из этой области используется какой-нибудь локальной переменной (или функцией такой же сигнатуры) изначальная переменная (или функция) остается доступной с помощью дополнения ее именем модуля. В большинстве случаев wildcard-нотация ведет себя нормально, но обычно пользователю не известны имена всех внутренних типов и переменных модуля, которые можно ненароком изменить. По этой причине благоразумнее ставить команды импорта в начале файла Asymptote, чтобы импортируемые имена не оказались перекрытыми именами локально определенных функций. Кроме того, импортируемые названия могут перекрыть другие импортируемые имена, в зависимости от порядка, в котором они импортировались, а импортированные функции могут привести к возникновению проблемы выбора требуемой функции, если названные функции имеют то же имя, что и локальные функции, определяемые далее. Для переименования или для полей, добавляемых в локальную область, применяют as: access graph as graph2d; from graph access xaxis as xline, yaxis as yline; Команда import graph as graph2d; является удобной заменой команд access graph as graph2d; unravel graph2d; За исключением нескольких встроенных модулей, таких, как settings, все модули реализованы в виде файлов Asymptote. При поиске еще не загруженного модуля, Асимптота обходит определенную последовательность директорий с целью сопоставления имени модуля имени файла. При состоявшемся сопоставлении, соответствующий код будет прочитан и будет трактоваться как тело структуры, определяющее модуль. Если имя файла содержит символы, не являющиеся буквами или цифрами, его следует взять в кавычки: access "/usr/local/share/asymptote/graph.asy" as graph; from "/usr/local/share/asymptote/graph.asy" access axes; import "/usr/local/share/asymptote/graph.asy" as graph; Если модуль импортирует сам себя, или модули в цикле импортируют друг друга, это считается ошибкой. В режиме реального времени можно импортировать модуль, определяемый строкой s: eval("import "+s,true); Для исполнения массива asy-файлов применяют void asy(string format, bool overwrite ... string[] s); Файл будет обработан с использованием формата format, если overwrite есть true или отсутствует выходной файл. Любое выражение Asymptote можно вычислить (без возврата результата), записав его в строку s и применив оператор void eval(string s, bool embedded=false); Глава 4. Функции 49 Нет необходимости заканчивать строку точкой с запятой. Если embedded есть true, строка будет вычислена на верхнем уровне текущего состояния. В противном случае (предполагаемом по умолчанию) строка будет вычислена в независимой среде с теми же самыми установками settings модуля. Можно вычислить произвольный код Asymptote (который может содержать немаскированные кавычки) с помощью команды void eval(code s, bool embedded=false); Вот код специального типа, в котором используется quote{}, чтобы вставить в Asymptote такой код: real a=1; code s=quote { write(a); }; eval(s,true); // Âûâîäèòñÿ 1 Чтобы вставить содержимое файла graph дословно (как если бы содержимое файла было просто перенесено в данное место), можно использовать один из вариантов вида include graph; include "/usr/local/share/asymptote/graph.asy"; Чтобы составить список всех глобальных функций и переменных, определенных в модуле, озаглавив список содержимым строки s, используется функция void list(string s, bool imports=false); Если переменная imports имеет значение true, то в список добавляются также импортируемые глобальные функции и переменные. 4.13 Ñòàòè÷åñêèå ïåðåìåííûå Для статической переменной адрес памяти выделяется в объемлющем блоке. В теле функции переменная размещается в том блоке, в котором определена функция; т. о. в коде struct s { int count() { static int c=0; ++c; return c; } } имеется один экземпляр переменной c для каждого объекта s (а не для каждого вызова count). Аналогично в следующем фрагменте int factorial(int n) { int helper(int k) { static int x=1; x *= k; return k == 1 ? x : helper(k-1); Глава 4. Функции } 50 } return helper(n); имеется один экземпляр x для каждого вызова factorial (но не для каждого вызова helper), так что это – хоть и правильная, но безобразная реализация вычисления факториала. Точно так же статическая переменная, объявленная внутри структуры, размещается в блоке, в котором определена структура. Т. о. struct A { struct B { static pair z; } } создает объект z для каждого созданного объекта типа A. В следующем примере int pow(int n, int k) { struct A { static int x=1; void helper() { x *= n; } } for(int i=0; i < k; ++i) { A a; a.helper(); } return A.x; } создается один экземпляр x для каждого вызова pow, так что теперь имеем и уродливую реализацию степенной функции. Циклы выполняют размещение на каждой итерации. Это сделано для того, чтобы высокого порядка функции могли обращаться к переменным, определенным для конкретных итераций цикла: void f(); for(int i=0; i < 10; ++i) { int x=i; if(x==5) { f=new void () { write(x); } } } f() Здесь каждая итерация цикла имеет собственную переменную x, так что f() напечатает 5. Если переменная в цикле объявлена статической, она будет размещена там же, где объявлена объемлющая функция или структура (как если бы статическая переменная была объявлена вне цикла). Например, во фрагменте: Глава 4. Функции 51 void f() { static int x; for(int i=0; i < 10; ++i) { static int y; } } и x, и y будут размещены в том же месте, в котором будет размещена f. Операторы также могут быть объявлены статическими, в этом случае они выполняются в том месте, в котором объявлена объемлющая функция или структура. Переменные или операторы, определенные вне функций или структур, объявлять статическими бессмысленно, так как они уже находятся на самом объемлющем уровне. В этом случае выдается предупреждение. Поскольку структуры могут иметь статические поля, то не всегда ясно, относится ли квалифицируемое имя к переменной или типу. Например, в коде struct A { static int x; } pair A; int y=A.x; возникает вопрос, к чему обращается A в A.x: к структуре или к переменной типа pair. В Asymptote действует соглашение о том, что, если нефункциональная переменная имеет то же имя, что и квалификатор, то последний обращается к переменной, а не к типу. Это не зависит от того, какие поля имеет переменная. Ãëàâà 5 Ìîäóëè è èõ âîçìîæíîñòè Asymptote содержит следующие модули. • Модуль animation служит для создания анимаций. В подкаталоге animations каталога examples имеются файлы примеров wheel.asy, wavelet.asy и cube.asy. Анимации используют программу ImageMagick convert для объединения серии изображений в gifили mpeg-клипы. Родственный пакет animate, производный от модуля animation, генерирует высокого качества компактные интерактивные pdf-клипы с дополнительными элементами управления. Требуется установка пакета http://www.ctan.org/tex-archive/macros/latex/contrib/animate/ animate.sty (версии 2007/11/30 или более поздней) в новый каталог animate локального каталога LATEX’а (например, в /usr/local/share/texmf/tex/latex/animate). В системах UNIX затем следует выполнить команду texhash. Возможности пакета иллюстрируют файлы примеров pdfmovie.asy и slidemovies.asy, intro.asy (слайд-презентации) из каталога animations. Примеры inlinemovie.tex и inlinemovie3.tex показывают, как создавать и вставлять pdf-клипы непосредственно в файл LATEX. Функция-член† string pdf(fit fit=NoBox, real delay=animationdelay, string options="", bool keep=settings.keep, bool multipage=true); структуры animate делает доступной любую из опций файла animate.sty, как это описано в http://www.ctan.org/tex-archive/macros/latex/contrib/animate/ doc/animate.pdf • Модуль annotate создает pdf-аннотации для просмотра с помощью Adobe Reader, используя функцию void annotate(picture pic=currentpicture, string title, string text, pair position) Аннотации приведены в файле annotation.asy. В настоящее время аннотации внедряются с помощью LATEX’а и TEX-движков. † Функция, определенная в какой-либо структуре. 52 Глава 5. Модули и их возможности 53 • Модуль babel вводит в Asymptote LATEX-пакет babel: import babel; babel("german"); • Модуль binarytree может быть использован для рисования различных бинарных деревьев и включает в себя процедуру для особого случая двоичного дерева поиска, как показано в примере binarytreetest.asy. • Модуль CAD (автор – Mark Henning) содержит основные определения перьев и функции измерений для простого CAD-черчения (не трехмерного) в соответствии с DIN 15. Документация к модулю находится в файле CAD.pdf. • Модуль contour обеспечивает построение контурных графиков (линий уровня функций двух переменных). • Модуль contour3 служит для построения поверхностей уровня функций трех переменных. • Модуль drawtree предназначен для рисования деревьев, см. пример treetest.asy. • Модуль embed предоставляет интерфейс для LATEX’а (в комплексе с MikTeX’ом) http://www.ctan.org/tex-archive/macros/latex/contrib/movie15 чтобы встраивать клипы, звук и 3D-объекты в pdf-документы. Пользователи XeLaTeX’а должны переименовать модифицированную версию movie15_dvipdfmx.sty http://asymptote.svn.sourceforge.net/viewvc/asymptote/trunk/ asymptote/patches/ в movie15.sty и поместить ее в путь LATEX’а. Последняя версия пакета movie15 требует версии 1.20 или более поздней программы pdflatex и файла http://www.ctan.org/tex-archive/macros/latex/contrib/oberdiek/ ifdraft.dtx который должен быть установлен в каталоге ifdraft локальной директории LATEX’а (например, в /usr/local/share/texmf/tex/latex/ifdraft), в которой должны быть затем выполнены команды tex ifdraft.dtx texhas • Модуль feynman (автор – Martin Wiebusch) обеспечивает рисование диаграмм Фейнмана, см. примеры eetomumu.asy и fermi.asy. • Модуль flowchart служит для вычерчивания блок-схем. • Модуль geometry, написанный Филиппом Ивальди, предоставляет обширный набор геометрических процедур. Документация к модулю: http://asymptote.sourceforge.net/links.html множество примеров: http://www.piprime.fr/files/asymptote/geometry/ оглавление: http://www.piprime.fr/files/asymptote/geometry/modules/geometry.asy. index.type.html Глава 5. Модули и их возможности 54 • Модуль graph обеспечивает построение линейных и логарифмических графиков на плоскости, включая автоматическое масштабирование и выбор отметок (с возможностью переопределения вручную). График является guide (то есть может быть нарисован процедурой draw и снабжен обозначениями). • Модуль graph3 содержит трехмерные версии функций модуля graph. Имеются специальные процедуры для построения координатных осей, поверхностей и векторных полей в пространстве. • Модуль grid3 (автор – Филипп Ивальди) обеспечивает черчение 3D-сеток. Примеры см. в файле grid3.asy и на странице http://www.piprime.fr/files/asymptote/grid3/ • Модуль interpolate позволяет использовать интерполяции Лагранжа, Эрмита и стандартную кубическую сплайн-интерполяцию, применяемую в Asymptote, что́ и показано в примере interpolate1.asy. • Модуль labelpath использует макрос pstextpath из пакета PSTricks, чтобы подогнать метки вдоль пути (правильно и с кернингом, как показано в примере curvedlabel.asy) c помощью процедуры void labelpath(picture pic=currentpicture, Label L, path g, string justify=Centered, pen p=currentpen); Опция justify принимает значения LeftJustified, Centered или RightJustified. Применительно к метке компонента x в преобразовании shift интерпретируется как сдвиг вдоль кривой, а компонента y – как сдвиг в сторону от кривой. Все другие преобразования метки игнорируются. Этот пакет требует LATEX’а или TEX-движка и наследует ограничения макро \pstextpath пакета PSTricks. • Модуль labelpath3 (автор – Jens Schwaiger) расширяет возможности labelpath на трехмерное пространство и не требует пакета PSTricks. См. пример curvedlabel3.asy. • Модуль latin1. Если в данной версии LATEX нет поддержки unicode, импортируя модуль latin1, можно включить поддержку западноевропейских языков (ISO 8859-1). Этот модуль может быть использован в качестве шаблона для поддержки других алфавитов ISO 8859. • Модуль markers реализует специализированные программы для маркировки путей и углов. Основной процедурой маркировки является markroutine markinterval(int n=1, frame f, bool rotated=false); которая центрирует n копий кадра f и размещает их равномерно вдоль пути с интервалом arclength при необходимости поворачивая на некоторый угол относительно локальной касательной. Примеры предопределенных маркеров можно посмотреть в markers1 и в markers2. Предусмотрены процедуры создания новых маркеров. • Модуль math расширяет математические возможности Asymptote с помощью некоторых полезных функций вроде следующих. void drawline(picture pic=currentpicture, pair P, pair Q, x pen p=currentpen); Не изменяя размеров рисунка pic и используя перо pen, рисует видимую часть (бесконечной) прямой, проходящей через точки P и Q. Глава 5. Модули и их возможности 55 real intersect(triple P, triple Q, triple n, triple Z); Возвращает параметр точки пересечения продолжения отрезка PQ (заданного в параметрической форме) с плоскостью, имеющей нормаль n и проходящей через точку Z. triple intersectionpoint(triple n0, triple P0, triple n1, triple P1); Возвращает произвольную точку пресечения двух плоскостей с нормалями n0 и n1 и проходящими через точки P0 и P1, соответственно. Если плоскости параллельны, возвращается (infinity,infinity,infinity). pair[] quarticroots(real a, real b, real c, real d, real e); Находит четыре комплексных корня уравнения четвертой степени ax4 + bx3 + cx2 + dx + e = 0. pair[][] fft(pair[][] a, int sign=1) Возвращает двумерное преобразование Фурье, используя заданное значение sign. real time(path g, real x, int n=0) Возвращает n-е значение параметра, при котором путь g пересекается вертикальной прямой, имеющей абсциссу x. real time(path g, explicit pair z, int n=0 Возвращает n-е значение параметра, при котором путь g пересекается горизонтальной прямой, имеющей ординату z.y. real value(path g, real x, int n=0) Вычисляет n-е значение ординаты g в точке x. real value(path g, explicit pair z, int n=0) Вычисляет n-е значение абсциссы g для ординаты z.y. real slope(path g, real x, int n=0) Возвращает n-й наклон g в точке x. real slope(path g, explicit pair z, int n=0 Возвращает n-й наклон g для ординаты z.y. int[][] segment(bool[] b) Возвращает индексы последовательностей из элементов true в массиве b. real[] partialsum(real[] a) Подсчитывает частичные суммы в действительном массиве a. real[] partialsum(real[] a, real[] dx) Подсчитывает частичные dx-взвешенные суммы в действительном массиве a. bool increasing(real[] a, bool strict=false) При strict=false возвращает true, если элементы a расположены в нестрого возрастающем порядке; если же strict=true возвращает true, если элементы a расположены в строго возрастающем порядке. int unique(real[] a, real x Если отсортированный массив a не содержит x, вставляет последний, возвращая индекс x в полученном массиве. bool lexorder(pair a, pair b) Возвращает true, если a.x < b.x || (a.x == b.x && a.y < b.y). bool lexorder(triple a, triple b) Возвращает true, если a.x < b.x || (a.x == b.x && (a.y < b.y || (a.y == b.y && a.z < b.z))). • Модуль MetaPost содержит несколько полезных процедур, облегчающих пользователям MetaPost’а преобразование старого MetaPost-кода в код Asymptote. Asymptote, в отличие от MetaPost’а, не решает неявно заданных линейных уравнений и поэтому не имеет команды whatever. Процедура extension является полезной заменой whatever: она находит точки пересечения линий, проходящих через p, q и P, Q. Менее употребительна замена whatever встроенным оператором solve. Глава 5. Модули и их возможности 56 • Модуль obj реализует построение поверхностей, опираясь на простые obj-файлы, см. примеры galleon.asy и triceratops.asy. • Модуль ode реализует ряд схем явного численного интегрирования для обыкновенных дифференциальных уравнений, см. пример odetest.asy. • Модуль palette позволяет генерировать плотность цвета изображения и палитры. Последние определены в файле palette.asy. • Модуль patterns реализует модели штриховки Postscript и включает несколько процедур генерация шаблонов штриховки. • Модуль plain по умолчанию является базовым модулем, определяющим основные составляющие языка рисования (например, структуру picture). Подключение этого модуля private import plain; происходит неявно до трансляции файла и перед первой командой, заданной в интерактивном режиме. Это относится также к трансляции файлов, определяющих модули. Сказанное означает, что типы и функции, определенные в plain, доступны почти во всех кодах Asymptote. • Модуль roundedpath (автор – Стефан Кнорр) скругляет острые углы путей, см. примеры в файле roundpath.asy. • Модуль simplex решает задачи линейного программирования двух переменных симплекс-методом. Используется в модуле plain для автоматической установки размеров рисунка. • Модуль slide предоставляет простое, но качественное средство для изготовления слайдов презентаций, в том числе переносных и со встроенной pdf-анимацией (см. пример slidemovies.asy). Простой пример приведен в файле slidedemo.asy. • Модуль slopefield предназначен для построения поля касательных к интегральным кривым дифференциального уравнения dy = f (x, y). dx • Модуль solids содержит структуру revolution, с помощью которой можно рисовать поверхности вращения. • Модуль stats реализует генерацию гауссовских случайных чисел и содержит набор статистических процедур, включая построение гистограмм и метод наименьших квадратов leastsquares. • Модуль syzygy автоматизирует процесс рисования кос, отношений и узлов, см. пример knots.asy. • Модуль tree реализует пример динамического двоичного дерева поиска. • Модуль three расширяет понятия path и guide на трехмерное пространство. Он вводит новые типы данных path3, guide3 и surface (поверхность). Синтаксис остается тем же, что и для случая двух измерений, только вместо пар (x,y) для узлов и спецификаторов направлений используются тройки (x,y,z). В модуле применяется обобщение сплайн-алгоритма Джона Хобби, которое shape-инвариантно при трехмерном вращении, масштабировании и сдвиге, и в плоском случае сводится к двумерному алгоритму, имеющемуся в Asymptote, MetaPost’е и MetaFont’е. Глава 5. Модули и их возможности 57 • Модуль trembling, также написанный Филиппом Ивальди, позволяет рисовать волнистыми линиями, имитирующими рисование от руки, см. пример floatingdisk.asy. Другие примеры размещены на странице http://www.piprime.fr/files/asymptote/trembling/ • Модуль tube предназначен для рисования трубчатых поверхностей, представленных в файле three_arrows.asy. • Модуль unicode. Команда import unicode в начале файла предписывает LATEX’у принять стандартизованные международные символы unicode (UTF-8). Для использования кириллических шрифтов необходимо изменить кодировку: import unicode; texpreamble("\usepackage{mathtext}\usepackage[russian]{babel}"); defaultpen(font("T2A","cmr","m","n")); При установке Ubuntu 12.04 этот способ не дает результата. Вместо него следует применить texpreamble("\usepackage[T2A]{fontenc}\usepackage[utf8]{inputenc} \usepackage{mathtext}\usepackage[russian]{babel}"); Два замечания. 1. Загрузка \usepackage{mathtext} и строка defaultpen(font("T2A "cmr "m "n"); не обязательны. 2. В математической моде (например, между знаками $$) русский текст не воспроизводится. Ãëàâà 6 Îñíîâíûå èíñòðóìåíòû 6.1 Óñòàíîâêè 6.1.1 Ôîðìàò âûõîäíîãî ôàéëà Устанавливается командой settings.outformat. Допустимы следующие форматы выходных файлов: eps (по умолчанию), png, pdf. Типичная установка формата выглядит так: settings.outformat = "pdf"; 6.1.2 Ðàçìåðû è ïðîöåäóðà size Компоненты пар выражаются в «больших пунктах» PostScript (1 bp = 1/72 дюйма). Например, линия по умолчанию имеет ширину 0.5 bp. Кроме того, размеры можно выражать в пунктах (1 pt = 1/72.27 inch), сантиметрах (cm), миллиметрах (mm) или дюймах (inch). Процедура size(real,real) имеет два аргумента-размера: максимальную ширину и максимальную высоту. Например, size(2cm,3cm); продуцирует рис., который будет иметь либо 2 см в ширину (и ≤ 3 см в высоту), либо 3 см в высоту (и ≤ 2 см в ширину). В любом случае отношение высоты к ширине сохраняется. Если какой-либо аргумент в size(real,real) равен нулю, он игнорируется. Таким образом, процедура size(4cm,0); масштабирует рис. таким образом, что его ширина будет точно равна 4 см. Высота же будет «естественной» высотой. Процедура size(real,real,keepAspect=false); масштабирует ширину и высоту независимо друг от друга, что приводит к тому, что рис. будет иметь заданные ширину и высоту, но отношение высоты к ширине может изменяться. Так, например, если окружность изображается на рис. с использованием этого типа процедуры size, то она рискует превратиться в эллипс. 6.2 Ïåðüÿ è îïöèÿ pen Эта опция используется для указания следующих атрибутов рисования: цвета (color), типа линии (line type), ее толщины (line width), формы ее концов (line cap), типа соединения (line join), правила заполнения (заливки) (fill rule), выравнивания текста (text alignment), вида шрифта (font), его размера (font size), штриховки (pattern), режима замены (overwrite mode), каллиграфического преобразования острия пера (calligraphic 58 Глава 6. Основные инструменты 59 transforms on the pen nib). По умолчанию используется перо currentpen. Неявным инициализатором является defaultpen. Различные перья могут объединяться в одно целое с помощью неассоциативного бинарного оператора +. Так получают сложение цветов. Например, можно получить желтое пунктирное перо, задав dashed+red+green, или red+green+dashed, или red+dashed+green. Бинарный оператор * можно использовать для получения оттенков цвета. Для этого надо умножить одну или нескольких цветовых компонент на действительное число, меньшее 1. 6.2.1 Öâåò По умолчанию рисование выполняется черным цветом, black; его можно изменить с помощью оператора defaultpen(pen). Цвета задаются в одном из следующих цветовых пространств. pen gray(real g); Дает оттенки серого цвета, интенсивность которого g лежит в промежутке [0; 1]; при этом 0.0 соответствует черному цвету, а 1.0 – белому. Предопределенный цвет gray имеет интенсивность 0.5. pen rgb(real r,real g,real b); Формирует RGB-цвет с интенсивностью составляющих цветов r (red, красного), g (green, зеленого), b (blue, синего) в пределах от 0 до 1. pen cmyk(real c,real m,real y,real k); создает CMYK-цвет с интенсивностью составляющих цветов c (cyan, голубого), m (magenta, пурпурного), y (yellow, желтого), b (black, черного) в пределах от 0 до 1. pen invisible; рисует невидимыми чернилами, создавая в то же время ограничивающий бокс, как и при обычном рисовании (аналогично команде \phantom в TEX’е). Может быть использована функция bool invisible(pen) для проверки того, является ли перо невидимым. Функция real[] colors(pen) возвращает цвета компонентов пера. Тройка других функций, pen gray(pen), pen rgb(pen) и pen cmyk(pen), создает новые перья, получаемые преобразованием аргументов этих функций в соответствующие цветовые пространства. Шестисимвольная шестнадцатеричная строка RGB может быть преобразована в перо процедурой pen rgb(string s); обратное преобразование пера в указанную строку выполняется процедурой string hex(pen p); Различные оттенки и смеси основных цветов серой шкалы (black и white, основных RGBцветов (red, green, blue), вторичных RGB-цветов (cyan, magenta, yellow), основных CMYKцветов (Cyan, Magenta, Yellow, Black) находятся в модуле plain (рис. 6.1). Команда import x11colors; импортирует стандартные 140 цветов RGB X11, а стандартные 68 CMYK-цветов TEX’а импортирует команда import texcolors; Отметим, что RGB- и CMYK-цвета частично совпадают, но некоторые значительно отличаются (например, green). В составе Asymptote имеется стилевой файл asycolors.sty в виде пакета LaTeX, в котором определены LaTeX’овские CMYK-версии предопределенных в Asymptote цветов. Так что они могут напрямую использоваться в строках документа LaTeX. Обычно для этого служат аргументы текущего пера, но иногда, чтобы изменить цвет только части строки, скажем, при создании презентации, бывает желательным определить цвет непосредственно в документе LaTeX. Этот файл может быть передан командой Asymptote usepackage("asycolors"); Структура hsv, определенная в файле plain_pens.asy, может быть использована для конвертации из HSV в RGB и обратно. Здесь цветовой оттенок h (hue) представляет собой угол из интервала (0; 360), а насыщенность s (saturation) и яркость v (value или brightness) принимают значения в диапазоне [0; 1]. Например, Глава 6. Основные инструменты 60 palered lightred mediumred red heavyred brown darkbrown palecyan lightcyan mediumcyan cyan heavycyan deepcyan darkcyan palegreen lightgreen mediumgreen green heavygreen deepgreen darkgreen pink lightmagenta mediummagenta magenta heavymagenta deepmagenta darkmagenta paleblue lightblue mediumblue blue heavyblue deepblue darkblue paleyellow lightyellow mediumyellow yellow lightolive olive darkolive black white orange fuchsia chartreuse springgreen purple royalblue Cyan Magenta Yellow Black cmyk(red) cmyk(blue) cmyk(green) palegray lightgray mediumgray gray heavygray deepgray darkgray Рис. 6.1. Наименования оттенков цветов. pen p=hsv(180,0.5,0.75); write(p); // ([default], red=0.375, green=0.75, blue=0.75) hsv q=p; write(q.h,q.s,q.v); // 180 0.5 0.75 В Asymptote есть предопределенные палитры цветов, используемые для закрашивания и хранящиеся в модуле palette.asy: pen[] Grayscale(int NColors = 256) // Ïàëèòðà ñåðîãî öâåòà pen[] Rainbow(int NColors=32766) // Ïàëèòðà-ðàäóãà pen[] BWRainbow(int NColors=32761) // Ïàëèòðà-ðàäóãà ñ äîáàâëåíèåì áåëîãî // è ÷åðíîãî öâåòîâ íà êîíöàõ ñïåêòðà pen[] BWRainbow2(int NColors=32761) // Äâîéíàÿ ïàëèòðà-ðàäóãà ñ äîáàâëåíèåì // áåëîãî è ÷åðíîãî öâåòîâ íà êîíöàõ ñïåêòðà è ëèíåéíî èçìåíÿþùåéñÿ // èíòåíñèâíîñòüþ öâåòà pen[] Wheel(int NColors=32766) // Ïîëíûé íàáîð öâåòîâ pen[] Gradient(int NColors=256 ... pen[] p) // Ïàëèòðà ëèíåéíî èçìåíÿþùèõñÿ // öâåòîâ, çàäàííûõ ìàññèâîì ïåðüåâ (pen) èç NColors Эти палитры показаны на рис. 6.2. Причем, на последней картинке показана палитра типа палитры violet из системы pgfplots, выполненная с помощью следующих команд: Глава 6. Основные инструменты 61 pen p1=rgb(.1,.1,.48); pen p2=rgb(.93,.55,.93); Gradient(p2,white,p1); Grayscale Rainbow BWRainbow BWRainbow2 Wheel Пользовательская Рис. 6.2. Предопределенные палитры. 6.2.2 Òèï ëèíèè Этот параметр задает процедура pen linetype(real[] a,real offset=0,bool scale=true,bool adjust = true) Аргумент a означает массив действительных чисел. Первое число массива указывает длину первого штриха, второе число – длину первого пробела и т. д. Если scale=true, то единицей длины считается единица длины пера, в противном случае – PostScript-единица. Если adjust=true, то эти расстояния регулируются Asymptote автоматически в зависимости от длины пути. Необязательный аргумент offset определяет смещение в шаблоне штриховки. Имеются предопределенные типы линий: pen solid=linetype(new real[]); pen dotted=linetype(new real[] {0,4}); pen dashed=linetype(new real[] {8,8}); pen longdashed=linetype(new real[] {24,8}); // Ñïëîøíàÿ ëèíèÿ // Ïóíêòèðíàÿ ëèíèÿ // Øòðèõîâàÿ ëèíèÿ // Äëèííàÿ øòðèõîâàÿ ëèíèÿ Глава 6. Основные инструменты 62 pen dashdotted=linetype(new real[] {8,8,0,8}); // Øòðèõïóíêòèðíàÿ ëèíèÿ // Äëèííàÿ øòðèõïóíêòèðíàÿ ëèíèÿ pen longdashdotted=linetype(new real[] {24,8,0,8}); pen Dotted(pen p=currentpen) {return linetype(new real[] {0,3})+2*linewidth(p);} pen Dotted=Dotted(); // Ïóíêòèðíàÿ ëèíèÿ Типом линии по умолчанию считается solid. Впрочем, его несложно изменить с помощью определения defaultpen(pen). Тип линии, производимой пером может быть задан функциями real[] linetype(pen p = currentpen), real offset(pen p), bool scale(pen p) и bool adjust(pen p) 6.2.3 Òîëùèíà ëèíèè Толщина линии определяется толщиной пера и указывается как pen linewidth(real) (в PostScript-единицах). По умолчанию толщина линии составляет 0.5 bp. Это значение изменяет процедура defaultpen(pen). Функция real linewidth(pen p=currentpen) возвращает толщину линии. Для удобства в модуль plain включены следующие определения. static void defaultpen(real w) defaultpen(linewidth(w)); static pen operator +(pen p,real w) return p+linewidth(w); static pen operator +(real w,pen p) return linewidth(w)+p; Так что допустимы такие определения толщины линии defaultpen(2); pen p=red+0.5; 6.3 Ïðåîáðàçîâàíèÿ è ïðîöåäóðà transform В Asymptote широко применяются аффинные преобразования. Пара (x,y) с помощью оператора t=(t.x,t.y,t.xx,t.xy,t.yx,t.yy) преобразуется в пару (x',y'), где x' = t.x + t.xx * x + t.xy * y y' = t.y + t.yx * x + t.yy * y Это равносильно PostScript-преобразованию [t.xx t.yx t.xy t.yy t.x t.y]. Преобразования применяются к парам (pair), путям (path и guide), строкам (string), холстам (frame), картинкам (picture) и другим преобразованиям (transform) с помощью левого умножения (бинарный оператор *). Преобразования могут составлять композиции и обращаться с помощью функции transform inverse(transform t); они также могут возводиться в любую целую степень оператором ^. Имеются следующие встроенные преобразования. transform identity(); Тождественное преобразование. transform shift(pair z); Сдвиг на вектор z. Глава 6. Основные инструменты 63 transform shift(real x, real y); Сдвиг на вектор (x,y). transform xscale(real x); Масштабирование в x раз в направлении оси Ox. transform yscale(real y); Масштабирование в y раз в направлении оси Oy. transform scale(real s); Масштабирование в s раз в направлении обеих координатных осей. transform scale(real x, real y); Масштабирование в x раз в направлении оси Ox и в y раз в направлении оси Oy. transform slant(real s) Наклон: точка (x,y) переводится в точку (x+s*y,y). transform rotate(real angle, pair z=(0,0)) Поворот вокруг точки z на угол angle (в градусах). transform reflect(pair a, pair b); Отражение относительно прямой a--b. Неявной инициализацией преобразований является identity(). Процедуры shift(transform t) è shiftless(transform t) возвращают преобразования (t.x,t.y,0,0,0,0) и (0,0,t.xx,t.xy,t.yx,t.yy), соответственно. 6.4 Çàïîëíåíèå îáëàñòè è îïöèÿ lltype Для заполнения областей цветом в Asymptote имеется опция filltype. Она может принимать следующие значения. FillDraw Заполняет внутреннюю часть области и рисует ее границу. FillDraw(real xmargin=0, real ymargin=xmargin, pen fillpen=nullpen, pen drawpen=nullpen) Если fillpen равно nullpen, заполнение выполняется пером drawpen; в противном случае – пером fillpen. Если drawpen=nullpen, то граница рисуется пером fillpen; в противном случае – пером drawpen. Можно задать ширину заполнения с помощью xmargin и ymargin. Fill Заполняет внутренность области. Fill(real xmargin=0, real ymargin=xmargin, pen p=nullpen) Если p=nullpen, заполнение выполняется пером drawpen; в противном случае – пером p. Можно задать ширину заполнения с помощью xmargin и ymargin. NoFill Заполнение не происходит. Draw Рисуется только граница. Draw(real xmargin=0, real ymargin=xmargin, pen p=nullpen) Если p=nullpen, граница рисуется пером drawpen; в противном случае – пером p. Можно задать ширину заполнения с помощью xmargin и ymargin. UnFill Обрезка области. UnFill(real xmargin=0, real ymargin=xmargin) Обрезает область, окружая ее границами xmargin и ymargin. Глава 6. Основные инструменты 64 RadialShade(pen penc, pen penr) Заполнение идет радиально от центра ограничивающего прямоугольника пером penc до границы области пером penr. RadialShadeDraw(real xmargin=0, real ymargin=xmargin, pen penc,pen penr, pen drawpen=nullpen) Сначала выполняется заполнение с помощью RadialShade, а затем рисуется граница. Перо с PostScript’овским правилом заполнения возвращается с одним из двух целых значений: pen zerowinding=fillrule(0); pen evenodd=fillrule(1); Правило заливки оказывает влияние только на функции clip, fill и inside и использует алгоритм, который применяется для определения внутренней части области, ограниченной путем или массивом путей. Для правила zerowinding (принятого по умолчанию) точка z лежит вне области, ограниченной путем, если число пересечений снизу вверх пути и горизонтальной линии z--z+infinity минус число нисходящих пересечений равно нулю. Для правила evenodd точка z считается расположенной вне указанной области, если общее число таких пересечений четно. Правило, действующее по умолчанию может быть изменено с помощью defaultpen(pen). Функция int fillrule(pen p=currentpen) возвращает правило заполнения. 6.5 Ïîëîæåíèå è íàïðàâëåíèå Позиция Relative(real) определяет место объекта по отношению к общей длине (arclength) пути. Имеются предопределенные положения объектов: position BeginPoint=Relative(0); position MidPoint=Relative(0.5); position EndPoint=Relative(1); Направление на рисунках удобно задавать с помощью как абсолютного направления по компасу, так и относительного направления Relative(pair), отсчитываемого от северной оси до локального направления пути. Предопределенные «компасные» направления показаны на рис. 6.3. NNW N NNE NW NE WNW ENE W E WSW ESE SW SE SSW S SSE Рис. 6.3. Предопределенные «компасные» направления. Эти компасные направления имеют следующий смысл: E=(1,0), N=(0,1), NE=unit(N+E), ENE=unit(E+NE). Они вместе с направлениями up, down, right и left определены в виде пар Глава 6. Основные инструменты 65 в основном модуле Asymptote plain (пользователь, который ввел локальную переменную E может использовать компасное направление E, предваряя его именем модуля, в котором оно определено: plain.E). Для удобства введены направления LeftSide, Center и RightSide как Relative(W), Relative((0,0)) и Relative(E), соответственно. Умножение LeftSide, Center, RightSide на коэффициент смещает объект относительно пути. 6.6 Ôðåéìû è êàðòèíêè 6.6.1 Ôðåéìû frame Фрейм frame – это холст для рисования в координатах PostScript. Впрочем, работа с фреймами по созданию отложенных процедур рисования не так удобна, как с картинками (picture), поэтому последние используются чаще. Неявным инициализатором фрейма является newframe. Функция bool empty(frame f) возвращает true только для пустого фрейма f. Процедура erase(frame) удаляет фрейм. Функции pair min(frame) и pair max(frame) возвращают координаты левого нижнего и правого верхнего углов, соответственно, ограничивающего фрейм прямоугольника. Содержимое фрейма src может быть нарисовано поверх фрейма dest процедурой void add(frame dest, frame src); или нарисовано под фреймом dest с помощью процедуры void prepend(frame dest, frame src); Смещение фрейма f в направлении align, подобно выравниванию метки (о чем будет идти речь дальше), достигается применением функции frame align(frame f, pair align); Чтобы нарисовать заполненный прямоугольник или эллипс вокруг фрейма или метки, и получить границу в виде пути, используют одну из предопределенных envelope-процедур: path box(frame f, Label L="", real xmargin=0, real ymargin=xmargin, pen p=currentpen, filltype filltype=NoFill, bool above=true); path roundbox(frame f, Label L="", real xmargin=0, real ymargin=xmargin, pen p=currentpen, filltype filltype=NoFill, bool above=true); path ellipse(frame f, Label L="", real xmargin=0, real ymargin=xmargin, pen p=currentpen, filltype filltype=NoFill, bool above=true); 6.6.2 Êàðòèíêè picture Картинка picture является структурой высокого уровня, определенной в модуле plain, и обеспечивает рисование на холсте в пользовательских координатах. По умолчанию рисование осуществляется на картинке currentpicture. Новая картинка создается так: picture pic; Анонимные картинки вводят с помощью выражения new picture. Процедура size задает требуемые размеры картинки: Глава 6. Основные инструменты 66 void size(picture pic=currentpicture, real x, real y=x, bool keepAspect=Aspect); Если оба размера x и y равны 0, пользовательские координаты будут интерпретированы как PostScript-координаты. В этом случае преобразованием, переводящим картинку в выходной формат, будет identity(). Если равен нулю только один из размеров, x или y, то в этом направлении никакие ограничения на размер не накладываются; это направление будет масштабироваться так же, как и другое направление. Если keepAspect равен Aspect или true, то картинка будет масштабироваться с сохранением пропорций, причем результирующая ширина не будет превышать x, а результирующая высота – y. Если keepAspect равно IgnoreAspect или false, картинка будет масштабироваться в обоих направлениях без сохранения пропорций, а ее результирующие ширина и высота будут равны, соответственно, x и y. Чтобы сделать пользовательские координаты для картинки pic кратными x в направлении Ox и кратными y в направлении Oy, применяется функция void unitsize(picture pic=currentpicture, real x, real y=x); При ненулевых значениях эти x и y перекрывают соответствующие размерные параметры картинки pic. Процедура void size(picture pic=currentpicture, real xsize, real ysize, pair min, pair max); преобразует картинку, масштабируя ее к пользовательскому координатному прямоугольнику box(min,max) в область ширины xsize и высоты ysize (если эти параметры ненулевые). Вместо этого, вызывая процедуру transform fixedscaling(picture pic=currentpicture, pair min, pair max, pen p=nullpen, bool warn=false); можно привести картинку к фиксированному масштабу в пользовательском координатном прямоугольнике box(min,max) и к уже заданному размеру картинки, учитывая толщину пера p. Будет выдано сообщение, если результирующий размер картинки превысит заданный размер. Чтобы картинку pic заключить в рамку и вывести в файл prefix.format, где format – один из форматов изображений, следует обратиться к функции shipout: void shipout(string prefix=defaultfilename, picture pic=currentpicture, orientation orientation=orientation, string format="", bool wait=false, bool view=true, string options="", string script="", light light=currentlight, projection P=currentprojection) Параметры options, script и projection имеют смысл только для трехмерных изображений. Если defaultfilename является пустой строкой, системой будет использован префикс outprefix(). Вообще говоря, процедура shipout() всегда неявно добавляется в файловый вывод, если только эта процедура не была указана явно. По умолчанию ориентация страницы предполагается Portrait, но это можно изменить с помощью переменной orientation. Чтобы вывести Глава 6. Основные инструменты 67 страницу в альбомной ориентации, достаточно указать orientation=Landscape или выполнить процедуру shipout(Landscape); Чтобы повернуть страницу на −90◦ , следует использовать ориентацию Seascape. Ориентация UpsideDown поворачивает страницу на 180◦ . Картинку можно явным образом заключить в рамку, если сделать вызов frame pic.fit(real xsize=pic.xsize, real ysize=pic.ysize, bool keepAspect=pic.keepAspect); Размеры и пропорции по умолчанию берутся такими, какими их задает процедура size (для которой по умолчанию размеры есть 0 и 0, а сохранение пропорций имеет значение true). Преобразование, которое будет выполняться над картинкой pic с целью заключить ее в рамку, возвращается функцией-членом pic.calculateTransform(). Чтобы нарисовать вокруг картинки рамку с полями, следует поместить картинку во фрейм bbox с помощью функции frame bbox(picture pic=currentpicture, real xmargin=0, real ymargin=xmargin, pen p=currentpen, filltype filltype=NoFill); Например, чтобы нарисовать рамку с полями величиной 0,25 см, достаточно применить процедуру shipout(bbox(0.25cm));. Рамка может быть снабжена фоном цвета p, если вызвать функцию function bbox(p,Fill). Функции pair min(picture pic, user=false); pair max(picture pic, user=false); pair size(picture pic, user=false); вычисляют границы и размер картинки pic, которые бы она имела, если бы была вставлена во фрейм со своими размерами по умолчанию. Если user=false, то результат возвращается в координатах PostScript, в противном случае – в пользовательских координатах. Функция pair point(picture pic=currentpicture, pair dir, bool user=true); предоставляет удобный способ определения точки на рамке картинки pic в направлении dir относительно ее центра при игнорировании поправок, вносимых объектами фиксированного размера (типа меток или стрелок). Если user=false, то результат возвращается в координатах PostScript, в противном случае – в пользовательских координатах. Функция pair truepoint(picture pic=currentpicture, pair dir, bool user=true); идентична функции point, только теперь в расчет берутся и все объекты фиксированных размеров. Если user=false, то результат возвращается в координатах PostScript, в противном случае – в пользовательских координатах. ×àñòü II Ðèñîâàíèå íà ïëîñêîñòè 68 Ãëàâà 7 Ìîäóëü plain Этот модуль является встроенным и не требует специального подключения при работе. Он содержит все основные процедуры и опции рисования на плоскости. 7.1 Ïðîöåäóðû dot è label Процедура label выполняет надписи на рисунке. Ее прототип имеет следующий вид: void label(picture pic = currentpicture, Label L, pair position, align align = NoAlign, pen p = nullpen, filltype filltype = NoFill); Если align=NoAlign, то метка будет центрирована в позиции position пользовательских координат, в противном случае она будет сдвинута в направлении align от точки position и смещена на PostScript-расстояние align*labelmargin(p). Label L означает метку, которая может быть строкой или структурой, полученной одной из функций: Label Label(string s="",pair position, align align=NoAlign, pen p=nullpen, embed embed=Rotate, filltype filltype=NoFill); Label Label(string s="", align align=NoAlign, pen p=nullpen, embed embed=Rotate, filltype filltype=NoFill); Label Label(Label L, pair position, align align=NoAlign, pen p=nullpen, embed embed=L.embed, filltype filltype=NoFill); Label Label(Label L, align align=NoAlign, pen p=nullpen, embed embed=L.embed, filltype filltype=NoFill); Параметр s задает текст метки. Параметр position указывает точку привязки метки. Равенство pen=nullpen означает, что используется перо, которое объявлено в Label; по умолчанию – currentpen. Текст метки может быть масштабирован, наклонен, сдвинут или повернут с помощью произведения соответствующих аффинных операторов. Например, rotate(45)*xscale(2)*L сначала масштабирует L по x, а затем поворачивает против часовой стрелки на 45 градусов. Окончательная позиция метки может перемещаться с помощью PostScript-преобразования координат: shift(10,0)*L. Аргумент embed определяет, как будет преобразовываться метка вместе со встраиваемой картинкой: Shift – только сдвиг вместе со встраиваемой картинкой; 69 Глава 7. Модуль plain Rotate 70 – лишь сдвиг и вращение (по умолчанию); Rotate(pair z) – поворот с вектором z; Slant – только сдвиг, поворот, наклон и отражение; Scale – сдвиг, поворот, наклон, отражение и масштабирование. Параметр filltype указывает тип заполнения прямоугольника, содержащего метку, (фона метки). Снабжение метками путей см. в п. 7.3 Процедура dot из модуля plain рисует точку в виде круга диаметром, равным толщине пера, используя указанный filltype (при этом толщина линии умножается на dotfactor, по умолчанию равный 6): void dot(picture pic=currentpicture, pair z, pen p=currentpen, filltype filltype=Fill); Обязательным является лишь параметр z, задающий координаты точки. Если к точке требуется добавить метку, используется процедура dot(picture pic=currentpicture, Label L, pair z, align align=NoAlign, string format=defaultformat, pen p=currentpen, filltype filltype=Fill); О рисовании точки в каждом узле пути см. п. 7.3. В примере 7.1 показаны различные способы изображения точек; точек, снабженных метками; и просто меток. Наипростейший оператор изображает черную точку. Если в аргументы добавить слово Label, то рядом с точкой появится метка с координатами точки. Красная точка снабжена красной же меткой, расположенной в направлении W (на запад). Синяя точка увеличена в размере и снабжена укрупненной сиреневой меткой, сдвинутой на северо-восток в 3 раза дальше, чем обычно. В правом столбце расположены метки, нарисованные сами по себе, без привязки к dot. Метки изображены разными цветами, одна из них повернута на −30◦ , для других использовалась опция filltype, которая создавала для метки фон, или рамку, или то и другое. Последняя метка, D, помечает точку, созданную отдельно. Ïðèìåð 7.1. Òî÷êè è ìåòêè. settings.outformat="pdf"; settings.prc=false; size(0,5.5cm); dot((0,0)); dot(Label,(0,1)); pair A=(0,2); dot(A,L=Label("A"),W,red); pair B=(0,3); dot(B,L=Label(scale(2)*"B",magenta),3*NE,blue+8bp); label("This is a label",(4,3.5),filltype=Fill(green)); label(rotate(-30)*"$A_3$",(4,2.5),brown); label("This is a label too",(4,1),filltype=FillDraw(2mm,2mm,yellow,red)); label("This is a label else",(2.7,-0.5),filltype=Draw(1mm,1mm,red),blue); dot((3,-1.5)); label("$D$",(3,-1.5),E,deepgreen); shipout(bbox(0.2cm+invisible)); B This is a label A3 A (0,1) This is a label too This is a label else D В дальнейших текстах программ первая, вторая и последняя строчка будут опускаться. Глава 7. Модуль plain 71 7.2 Êðèâûå Áåçüå Для плавного соединения точек Asymptote использует кривые Безье. Для каждого внутреннего узла кубического сплайна может быть указано два направления (dir), входное и выходное: пара dir определяет направление входящей или исходящей касательной, соответственно, к кривой в этом узле. Для концевых узлов такое направление задается только с внутренней стороны. Кубический сплайн между узлом z0 с постопорной точкой c0 и узлом z1 с предопорной точкой c1 имеет вид кривой Безье (1 − t)3 z0 + 3t(1 − t)2 c0 + 3t2 (1 − t)c1 + t3 z1 , 0 ≤ t ≤ 1. Как показано на рис. 7.1, средняяя точка третьего порядка m5 , полученная из двух концевых точек z0 и z1 и двух опорных точек c0 и c1 , является точкой, которой соответствует t = 1/2 на кривой Безье, сформированной четверкой (z0 , c0 , c1 , z1 ). Это позволяет рекурсивно строить требуемую кривую, используя полученную среднюю точку 3-го порядка как концевую и соответствующие средние точки 2-го и 1-го порядков в качестве опорных. c0 m3 m1 m5 m0 c1 m4 m2 z0 z1 Рис. 7.1. Кривая Безье. На рис. 7.1 m0 , m1 и m2 – средние точки 1-го порядка, m3 и m4 – 2-го порядка, а m5 – средняя точка 3-го порядка. Кривая строится рекурсивно с применением алгоритма к четверке (z0 , m0 , m3 , m5 ) и (m5 , m4 , m2 , z1 ). На самом деле аналогичное свойство имеет место и для точек, расположенных в любой доле сегмента [0; 1], не только для средних точек (t = 1/2). Сконструированная таким образом кривая Безье обладает следующими свойствами: • целиком содержится в выпуклой оболочке заданных четырех точек; • начинается из первой концевой точки и идет по направлению к первой опорной, а заканчивается во второй концевой точке, идя по направлению от второй опорной точки. Тем не менее гораздо удобнее использовать оператор .. , который разрешает Asymptote самой выбрать опорные точки, используя алгоритм, описанный Дональдом Кнутом в 14 главе его книги «Все про METAFONT». Впрочем, пользователь может модифицировать такие параметры кривой как направление (direction), натяжение (tension) и загиб (curl). Чем больше натяжение, тем прямее кривая, тем лучше она аппроксимируется прямой линией. Натяжение можно изменять от 1, его значения по умолчанию, до 0.75. (см. John D. Hobby, Discrete and Computational Geometry 1, 1986). Параметр curl определяет загиб в конечных точках кривой: 0 означает прямую, а 1, значению по умолчанию, приближенно соответствует окружность. Глава 7. Модуль plain 72 Коннектор MetaPost’а ... , который отличается тем, что, когда это возможно, пытается ограничить путь некоторым треугольником, определяемым конечными точками и направлениями, представлен в Asymptote аббревиатурой :: for ..tension atleast 1 .. (многоточие занято в Asymptote под rest-аргументы). Коннектор --- является сокращением для ..tension atleast infinity.., а коннектор & соединяет два пути, начиная с последнего узла первого пути (он, как правило, должен присоединиться к первому узлу второго пути). 7.3 Ïðîöåäóðû path, guide è draw Путь path может быть элементарным объектом, например, точкой, а может быть совокупностью объектов, в том числе других путей, соединенных между собой различными способами. Неявным представителем пути является nullpath. Пусть, p и q – два пути. Тогда p--q – пути соединяются отрезком прямой, p..q – пути соединяются кубическим сплайном, p^^q – пути рассматриваются как один путь (например, в случае разрывных путей), p::q – пути соединяются отрезком прямой, плавно переходящим в следующий путь, p---q – «натянутая» линия, p & q – объединение двух независимых путей. Команда cycle соединяет конечную и начальную точки пути. Для присоединения метки к пути используют процедуру label следующей структуры: void label(picture pic=currentpicture, Label L, path g, align align=NoAlign, pen p=currentpen, filltype filltype=NoFill); Как уже указывалось, процедура guide строит нерешенный кубический сплайн (список узлов и опорных точек сплайна). Неявным инициализатором является nullpath.guide. Процедура похожа на path, но вычисление кубического сплайна откладывается до времени рисования (когда он будет преобразован в path). Процедура draw собственно и выполняет рисование. Ее определение таково: draw(picture pic=currentpicture, Label L="", path g, align align=NoAlign, pen p=currentpen, arrowbar arrow=None, arrowbar bar=None, margin margin=NoMargin, Label legend="", marker marker=nomarker); Она рисует путь g на картинке pic, пользуясь пером pen. Путь может сопровождаться меткой L, выровненной с помощью align; стрелками, снабженными черточками на концах; полями, обозначениями и маркерами. Обязательным параметром является лишь путь, path. Черточки могут быть полезны для указания размеров. Они принимают значения None, BeginBar, EndBar (что равносильно Bar) и Bars (черточки изображаются на обоих концах пути). Каждая черточка (за исключением None) может иметь дополнительный действительный аргумент, который имеет смысл длины черточки в PostScript-координатах. По умолчанию эта длина равна barsize(pen). Возможные значения стрелки (arrow): None, Blank (стрелки на пути не изображаются), BeginArrow, MidArrow, EndArrow (что равносильно Arrow) и Arrows (стрелки изображаются на обоих концах пути). Для каждого вида стрелок, кроме None и Blank можно указать тип острия (DefaultHead, SimpleHead, HookHead, TeXHead), размер size (в PosrScript-координатах), угол angle (в градусах), тип заполнения filltype (FillDraw, Fill, NoFill, UnFill, Глава 7. Модуль plain 73 Draw) и (кроме MidArrow и Arrows) позицию position на пути в смысле значения функции point(path p,real t). По умолчанию размер стрелки, которая создается пером p, равен arrowsize(p). Можно использовать стрелки для дуг: BeginArcArrow, EndArcArrow (что равносильно ArcArrow), MidArcArrow и ArcArrows). Поля margin применяют для сокращения видимой части пути labelmargin(p), чтобы избежать наложений на другие объекты. Значениями margin являются NoMargin, BeginMargin, EndMargin (что равносильно Margin) и Margins (поля возникают на обоих концах пути). Описание и применение опций legend и marker рассматривается в главе о модуле graph при описании построения графиков функций. Процедуру draw можно использовать для рисования точки. Для этого строят путь, состоящий из точки. Рассмотрим рисунки, демонстрирующие некоторые из перечисленных возможностей. На рис. примера 7.2 показаны все виды соединения путей, кроме ^^. Ïðèìåð 7.2. Ñîåäèíåíèÿ ïóòåé. z0 size(2.7cm,0); pair z0=(2,12),z1=(0,10),z2=(0,9),z3=(2,8),z4=(0,7),z5=(0,6), z6=(2,4),z7=(0,3),z8=(0,2),z9=(2,0); path connections = z0..z1---z2::{right}z3 & z3..z4--z5::{right}z6 & z6::z7---z8..{right}z9; z1 draw(connections,blue+bp); z4 dot(z0,L=Label("$z_0$",black),E,red); dot(z1,L=Label("$z_1$",black),W,red); dot(z2,L=Label("$z_2$",black),W,red); dot(z3,L=Label("$z_3$",black),E,red); dot(z4,L=Label("$z_4$",black),W,red); dot(z5,L=Label("$z_5$",black),W,red); dot(z6,L=Label("$z_6$",black),E,red); dot(z7,L=Label("$z_7$",black),W,red); dot(z8,L=Label("$z_8$",black),W,red); dot(z9,L=Label("$z_9$",black),E,red); z5 z2 z3 z6 z7 z8 z9 Соединив определенные точки коннектором .. в путь и использовав cycle, можно получить единичную окружность, на которой можно нарисовать эти точки и снабдить их метками. Рисунок примера 7.3, кроме этого, показывает, что Asymptote автоматически расставляет метки вдоль окружности, сама выбирая их расположение так, чтобы они не попадали на окружность и вообще располагались вдоль нее регулярным образом. В примере 7.22 представлен вариант автоматической расстановки меток, когда метки и точки окрашиваются в разные цвета. В примере 7.3 был использован вариант процедуры dot, который рисует точку в каждом узле пути: void dot(picture pic=currentpicture, Label[] L=new Label[], path g, align align=RightSide, string format=defaultformat, pen p=currentpen, filltype filltype=Fill); Глава 7. Модуль plain 74 Ïðèìåð 7.3. Àâòîìàòè÷åñêàÿ ðàññòàíîâêà òî÷åê è ìåòîê íà êðèâîé. size(3.5cm,0); pair A=(1,0),B=(0,1),C=(-1,0),D=(0,-1); path unitcircle = A..B..C..D..cycle; draw(unitcircle,blue); Label[] Labels=new Label[4]; Labels[0]="$A$"; Labels[1]="$B$"; Labels[2]="$C$"; Labels[3]="$D$"; dot(Labels,unitcircle,red); B C A D Снабдить меткой можно не только точку, но и путь (path или guide) в полном соответствии с определением оператора draw. По умолчанию метка размещается в середине пути. Альтернативое положение метки (в смысле point(path p, real t)) может быть указано с помощью position в конструкции Label. Позиция Relative(real) определяет относительное положение метки по отношению к длине всего пути. Для удобства предопределены следующие аббревиатуры: position BeginPoint=Relative(0); position MidPoint=Relative(0.5); position EndPoint=Relative(1); Метки пути выравниваются вдоль направления align, которое может определяться абсолютным компасным направлением (pair) или направлением Relative(pair), отсчитываемым от северной оси до локального направления пути. Для удобства LeftSide, Center и RightSide определяются как Relative(W), Relative((0,0)) и Relative(E), соответственно. Умножение LeftSide, Center, RightSide слева на масштабирующий множитель передвигает метку ближе к пути или дальше от него. Рассмотрим пример 7.4. Верхняя кривая нарисована оператором draw, фактическими аргументами которого являются только метка и путь, поэтому остальные атрибуты назначены по умолчанию. В результате путь нарисован черной сплошной линией стандартной толщины, а метка помещена ниже кривой возле ее середины. Следующая ниже кривая уже начерчена синим цветом и имеет увеличенную толщину, метка же помещена в конце пути. Так что понятно, что ее положение по умолчанию – справа от кривой. Третья кривая, красного цвета, помечена также в конце пути, но метка расположена под кривой. Наконец, метка четвертой кривой находится на относительном расстоянии 0,2 от начала пути. Еще один пример, 7.5, на эту тему демонстрирует, как использовать векторы направления сторон треугольника, чтобы желательным образом расположить и ориентировать их метки. Стрелка фиксированного размера длины arrowlength, указывающая на b в направлении dir, может быть помечена применением процедуры void arrow(picture pic=currentpicture, Label L="", pair b, pair dir, real length=arrowlength, align align=NoAlign, pen p=currentpen, arrowbar arrow=Arrow, margin margin=EndMargin); Если выравнивание не указано (в Label или в виде явного аргумента), опциональная Label будет выровнена в направлении dir с использованием границы margin. Как уже было сказано, в арсенале Asymptote имеются различного типа стрелки, форму и размещение которых пользователь может варьировать в зависимости от своих целей. На рис. примера 7.6 на синей кривой изображена стрелка по умолчанию; чтобы ее задать, надо только написать Arrow. Для зеленой стрелки задана ее величина в мм, а ее внутренность Глава 7. Модуль plain 75 Ïðèìåð 7.4. Ðàññòàíîâêà ìåòîê íà êðèâîé ïðîöåäóðîé draw. size(6cm,0); path p=(0,0.2)..(1,-0.3)..(2,0.2)..(3,0.5); draw(Label("$f(t)$"),p); draw(Label("$f(t)$",EndPoint), shift(0,-0.7)*p,blue+bp); draw(Label("$f(t)$",EndPoint), shift(0,-1.4)*p,S,red+bp); draw(Label("$f(t)$",position=Relative(0.2)), shift(0,-2.1)*p,heavygreen+bp); f (t) f (t) f (t) f (t) Ïðèìåð 7.5. Ìåòêè ïèôàãîðîâà òðåóãîëüíèêà. size(3.5cm,0); pair A=(0,0), B=(2,3), C=(2,0); path treug= A--B--C--cycle, f=A--C, g=C--B, h=A--B; label("$A$",A,dir(A-C)); label("$B$",B,dir(B-A)); label("$C$",C,dir(C-A)); A b=2 a=3 c= √ 13 draw(Label("$b=2$",position=MidPoint,brown),f,blue); draw(Label("$a=3$",embed=Rotate(dir(g)),brown),g,blue); draw(Label("$c=\sqrt{13}$",embed=Rotate(dir(h)), align=Center,brown,filltype=UnFill),h,blue); B C ничем не заполнена. Красная кривая снабжена двумя стрелками на концах (Arrows) и имеет отличный от стандартного тип TeXHead. На розовой кривой в ее начале и конце имеются черточки, а стрелка помещена в середине пути, изображена синим цветом, отличным от цвета кривой, и имеет тип HookHead. Оливковая кривая заканчивается стрелкой SimpleHead, а на сиреневой кривой расстояние от ее начала до стрелки составляет 0.8 от длины кривой; причем, стрелка имеет двойной тип: с одной стороны, она заявлена как HookHead, а с другой, относится к семейству стрелок, предназначенных для снабжения ими дуг окружностей. Как видим, это не мешает ее использовать для произвольных кривых. Следующие семь кривых демонстрируют типы линий в Asymptote: сплошные, штриховые, пунктирные и штрих-пунктирные с различной величины штрихами. Первая кривая снабжена метками L1 и L2 . Тем самым показано, что путь может быть помечен и оператором label в соответствии с еще одним его вариантом: void label(picture pic=currentpicture, Label L, path g, align align=NoAlign, pen p=nullpen, filltype filltype=NoFill); На базе рассмотренных операторов можно сделать несложный геометрический чертеж, рис. примера 7.7, или план квартиры, см. Приложение A. В последнем примере можно познакомиться с тем, как используется коннектор ^^ и чем его использование отличается от применения коннектора &. Типы path и guide похожи, но последний путь окончательно вычисляется непосредственно перед рисованием, что дает возможность провести более гладкую кривую. В примере 7.8 Глава 7. Модуль plain 76 Ïðèìåð 7.6. Òèïû ñòðåëîê è ëèíèé. L2 L1 size(5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); path curv=(0,0)..(1,0.5)..(2,0)..(3,0.5); label(Label("$L_1$",Relative(0.25)),curv,S); label("$L_2$",curv,NE); draw(curv,blue,Arrow); draw(shift(0,-0.6)*curv,heavygreen,BeginArrow(5mm,Draw), Bar); draw(shift(0,-1.2)*curv,8*red,Arrows(TeXHead)); draw(shift(0,-1.8)*curv,bp+magenta,MidArrow(HookHead, Fill(blue)),Bars(2mm)); draw(shift(0,-2.4)*curv,lightolive,Arrow(SimpleHead)); draw(shift(0,-3)*curv,purple,ArcArrow(HookHead, Relative(0.8))); draw(shift(0,-3.6)*curv,heavycyan); draw(shift(0,-4.2)*curv,blue+dashed); draw(shift(0,-4.8)*curv,darkgreen+black+dotted); draw(shift(0,-5.4)*curv,red+longdashed); draw(shift(0,-6)*curv,heavymagenta+dashdotted); draw(shift(0,-6.6)*curv,olive+longdashdotted); pen Dotted(pen p=currentpen) {return linetype(new real[] {0,3})+2*linewidth(p);} pen Dotted=Dotted(); draw(shift(0,-7.2)*curv,purple+Dotted); Ïðèìåð 7.7. Ïðèçìà. size(6.5cm,0); pair A=(-2,-0.5), B=(-0.8,0.5), C=(2,0.5), D=(0.8,-0.5), SS=(0,3), O=(0,0); draw(A--SS--C--D--cycle,blue); draw(SS--D,blue); draw(A--C--B--cycle,blue+dashed); draw(D--B--SS--O--B,blue+dashed); dot(O,L=Label("$O$",black),1.5*S,red); label("$A$",A,W); label("$B$",B,NW); label("$C$",C,E); label("$D$",D,ESE); label("$S$",SS,N); label("$l$",SS--C,NE); label("$a$",A--D,S); label(L=Label("$h$",Relative(0.6)),SS--O); S l h B A C O a D сплошная кривая как guide формирует в цикле свои узлы и опорные точки, но рисуется лишь по окончании цикла. Штриховая кривая как path в цикле делает то же самое, но дополнительно еще пристыковывает каждый новый сегмент пути к предыдущему, так что по окончании цикла получаем сформированную кривую, которая, однако, выглядит менее гладко, чем сплошная кривая. Можно сказать, что path строит кривую локально, а guide – глобально. Еще одна особенность отличает path от guide. Фрагмент программы Глава 7. Модуль plain 77 Ïðèìåð 7.8. Ðàçëè÷èÿ ìåæäó path è guide. size(9cm,0); real mexican(real x) {return (1-8x^2)*exp(-(4x^2));} int n=30; real a=1.5; real width=2a/n; guide hat; path solved; for(int i=0; i < n; ++i) { real t=-a+i*width; pair z=(t,mexican(t)); hat=hat..z; solved=solved..z; } draw(hat); dot(hat,red); draw(solved,dashed); guide g; for(int i=0; i < 10; ++i) g=g--(i,i); path p=g; выполняется за линейное время, а следующий фрагмент – за квадратичное. path p; for(int i=0; i < 10; ++i) p=p--(i,i); Рассмотрим влияние параметров direction, tension и curl на форму кривых. В примере 7.9 показано, как зависит форма кривой от параметра direction. Этот параметр определяет направление касательной в заданной точке кривой, причем, его значение записывается в фигурных скобках. Если параметр стоит перед точкой, он указывает направление левой касательной, если после точки, то правой касательной. В примере такому заданию направления касательной соответствует кривая, проходящая через точки A, B и C. Для следующей кривой, проходящей через точки D, E и F, направления касательных задаются с помощью векторов up = (0,1), down = (-1,0), left = (-1,0) и right = (1,0). Еще одна возможность – задавать упомянутые направления в градусах, как это сделано для третьей кривой, проходящей через точки G, H и I. Параметр tension (натяжение) регулирует кривизну линии. Чем он больше, тем сильнее кривая похожа на прямую. Значение натяжения по умолчанию равно 1, минимальное значение равно 0.75. В примере 7.10 первая кривая регулируется одним значением натяжения между узлами сплайна, вторая – двумя. Параметр curl (загиб) регулирует кривизну кривой в конечных точках пути. Значение загиба 0 приближает кривизну линии к прямой, значение 1, принятое по умолчанию, – к окружности. В примере 7.11 показано использование этого параметра. 7.4 Ïðîöåäóðû unitcircle, circle, ellipse è arc Путь path unitcircle; Глава 7. Модуль plain Ïðèìåð 7.9. Ïàðàìåòð direction. size(6cm,0); pair A=(0,0), B=(2,1), C=(3,0); path p1=A{(1,1)}..{(1,0)}B{0,-1}..{(1,.5)}C; draw(p1,blue); dot(p1,red); pair D=(0,-1.5), E=(1.5,-0.5), F=(2.5,-1.5); path p2=D{up}..{right}E{down}..{left}F; draw(p2,blue); dot(p2,red); pair G=(0,-3), H=(2,-2), I=(3,-3); path p3=G{dir(75)}..{dir(45)}H{dir(0)}..{dir(-90)}I; draw(p3,blue); dot(p3,red); Ïðèìåð 7.10. Ïàðàìåòð tension. size(5cm,0); pair A=(0,0), B=(2,1), C=(3,0); path p1=A..tension 0.75 ..B..tension 2 ..C; draw(p1,blue); dot(p1,red); pair D=(0,-1.5), E=(2,-0.5), F=(3,-1.5); path p2=D..tension 2 and 0.75 ..E..tension 0.9 and 13 ..F; draw(p2,blue); dot(p2,red); Ïðèìåð 7.11. Ïàðàìåòð curl. size(5cm,0); pair A=(0,0), B=(2,1), C=(3,0); path p1=A{curl 0}..B..{curl 0}C; draw(p1,blue); dot(p1,red); pair D=(0,-1.5), E=(2,-0.5), F=(3,-1.5); path p2=D{curl 1}..E..{curl 0}F; draw(p2,blue); dot(p2,red); представляет собой окружность с центром в начале координат. Процедура path circle(pair c, real r); 78 Глава 7. Модуль plain 79 возвращает окружность радиуса r с центром в точке c. Процедура path ellipse(pair c, real a, real b); производит эллипс с центром в точке c, имеющий горизонтальную полуось a и вертикальную полуось b. Построение окружностей и эллипса приведено в примере 7.12. Ïðèìåð 7.12. Îêðóæíîñòü è ýëëèïñ. size(4cm,0); dot((0,0)); draw(unitcircle,red); draw(circle((0,0),2),blue); draw(ellipse((0,0),2,1),green); Процедура path arc(pair c, explicit pair z1, explicit pair z2, bool direction=CCW); создает дугу с центром в точке c, которая идет из точки z1 в точку z2. По умолчанию дуга проводится против часовой стрелки: direction=CCW (по умолчанию). Если требуется провести дугу в противоположном направлении, следует набрать direction=CW. Рисунок примера 7.13 Ïðèìåð 7.13. Äóãà èç òî÷êè â òî÷êó. size(5.5cm,0); pair M=(-1,-1), N=(-1,1), P=(1,1), Q=(1,-1), A=(M+N)/2, B=(N+P)/2, C=(P+Q)/2, D=(Q+M)/2; draw(M--N--P--Q--cycle,blue); draw(unitcircle,green); draw(A--C,dashed+blue); draw(B--D,dashed+blue); draw(arc(D,Q,M),red); draw(arc(A,M,N),red); draw(arc(B,N,P),red); draw(arc(C,P,Q),red); dot(A,L=Label("$A$",black),left,red); dot(B,L=Label("$B$",black),up,red); dot(C,L=Label("$C$",black),right,red); dot(D,L=Label("$D$",black),down,red); B A C D выполнен с помощью именно этой процедуры. Еще один вариант задания дуги реализует процедура path arc(pair c, real r, real angle1, real angle2); которая формирует дугу радиуса r с центром в точке c, заключенную между углами angle1 и angle2. Если angle1 < angle2, дуга проводится против часовой стрелки, в противном случае – по часовой стрелке. Пример 7.14 демонстрирует применение этой процедуры, а заодно и рисование не простых дуг, а направленных, снабженных на концах стрелками. Глава 7. Модуль plain 80 Ïðèìåð 7.14. Äóãà îò óãëà äî óãëà. y 120 ◦ ◦ 90 x O − 90 ◦ size(5.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw((-1.3,0)--(1.3,0),orange,Arrow(size=2mm), L=Label("$x$",position=EndPoint, align=right,mahogany)); draw((0,-1.3)--(0,1.3),orange,Arrow(size=2mm), L=Label("$y$",position=EndPoint, align=up,mahogany)); draw(circle((0,0),1.1),blue); draw(arc((0,0),0.3,0,90),arrow=Arrow(2mm), L=Label(rotate(-45)*"$90^\circ$", position=MidPoint,black),green); draw(arc((0,0),0.7,0,120),arrow=Arrow(2mm), L=Label(rotate(-27)*"$120^\circ$", position=MidPoint,black),green); draw(arc((0,0),0.5,0,-90),arrow=Arrow(2mm), L=Label(rotate(45)*"$-90^\circ$", position=MidPoint,align=SE,black),green); label("$O$",(0,0),SW,mahogany); draw((0,0)--(-0.55,sqrt(1.1^2-0.55^2)),mahogany+dashed); 7.5 Ïðîöåäóðû unitsquare, box è polygon Чтобы упростить вычерчивание многоугольников, в Asymptote имеются специальные процедуры. Одна з них path unitsquare; создает единичный квадрат, левая нижняя вершина которого находится в начале координат. Процедура path box(pair a, pair b); возвращает прямоугольник, в котором противоположными вершинами являются a и b. Наконец, процедура path polygon(int n); формирует правильный n-угольник, вписывающийся в единичный круг. Пример 7.15 демонстрирует эти возможности. 7.6 Çàïîëíåíèå, ãðàäèåíòíàÿ çàëèâêà, îáðåçêà Внутренность циклического пути может быть заполнена каким-нибудь цветом. Это оформление выполняет процедура void fill(picture pic=currentpicture, path g, pen p=currentpen); Более гибкая процедура void filldraw(picture pic=currentpicture, path g, pen fillpen=currentpen, pen drawpen=currentpen); Глава 7. Модуль plain 81 Ïðèìåð 7.15. Ìíîãîóãîëüíèêè. size(7.3cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitsquare,blue); draw(box((-0.2,-1.2),(5.8,5.66)),red); dot((0,0),red); for (int n = 3; n <= 8; ++n){ real s = (n < 6) ? 2.3 : 4.6; int m = (n < 6) ? n : n-3; draw(shift((s,(m-3)*2.2))*polygon(n),blue); draw(shift((s,(m-3)*2.2))*unitcircle,green); } позволяет не только заполнить область пером fillpen, но и обвести ее контуром, цвет которого задает drawpen. Так что область и ее граница могут быть разного цвета. Следующая процедура void filloutside(picture pic=currentpicture, path g, pen p=currentpen); заполняет область вне циклического пути p, между ним и границами рисунка. Результат использования процедур можно видеть на рис. примера 7.16. Ïðèìåð 7.16. Çàïîëíåíèå. size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); filloutside(rotate(14)*scale(1.5)*polygon(9),green); filldraw(unitcircle,fillpen=pink,drawpen=blue+bp); fill((0,-0.5){curl 0}..(-0.5,0.25)..(-0.25,0.5).. {dir(-45)}(0,0.25){dir(45)}..(0.25,0.5)..(0.5,0.25).. {curl 0}cycle,red); Теперь рассмотрим градиентные заливки. Процедура void latticeshade(picture pic=currentpicture, path g, bool stroke=false, pen fillrule=currentpen, pen[][] p) обеспечивает плавный переход цветов, заданных массивом p, для заливки области, ограниченной контуром g, с учетом правила заполнения fillrule. Если параметр stroke=true, заливка производится точно так же, как при выполнении процедуры draw(pic,g,fillrule+zerowinding); в этом случае цикличность пути g не обязательна. Цвета в p должны быть из одного и того же цветового пространства. Чтобы перевести цвета в подходящее цветовое пространство, можно использовать функции rgb(pen) или cmyk(pen). Такой тип заливки показан в примере 7.17. Глава 7. Модуль plain 82 Ïðèìåð 7.17. Ãðàäèåíòíàÿ çàëèâêà íàáîðîì öâåòîâ. size(4.5cm,0); pen[][] p={{white,grey,black}, {red,green,blue}, {cyan,magenta,yellow}}; latticeshade(unitsquare,p); Осевая градиентная заливка означает плавное перетекание одного цвета в другой в определенном направлении. Процедурв void axialshade(picture pic=currentpicture, path g, bool stroke=false, pen pena, pair a, bool extenda=true, pen penb, pair b, bool extendb=true); создает такую заливку, причем, цвет pena постепенно переходит в цвет penb в направлении от точки a к точке b. Параметры extenda и extendb указывают на необходимость осуществлять процесс заливки за пределами концов оси a--b. Демонстрацию осевой заливки см. в примере 7.18. Ïðèìåð 7.18. Îñåâàÿ ãðàäèåíòíàÿ çàëèâêà. size(4.5cm,0); path g=box((0,0),(4,3)); axialshade(g,red,(0,1),yellow,(4,1)); Радиальная градиентная заливка осуществляет плавный переход одного цвета в другой вдоль отрезков радиусов, лежащих между двумя окружностями. Для этого имеется процедура void radialshade(picture pic=currentpicture, path g, bool stroke=false, pen pena, pair a, real ra, bool extenda=true, pen penb, pair b, real rb, bool extendb=true); которая начинает рисование цветом pena на окружности радиуса ra с центром в точке a, а заканчивает – цветом penb на окружности радиуса rb с центром в точке b. Радиальная заливка представлена в примере 7.19. Переходим к выполнению обрезки изображений. Для этого служит процедура void clip(picture pic=currentpicture, path g, stroke=false, pen fillrule=currentpen); которая обрезает текущее изображение по границе пути g, используя правило заполнения fillrule. Если параметр stroke=true, заливка равносильна рисованию области процедурой Глава 7. Модуль plain 83 Ïðèìåð 7.19. Ðàäèàëüíàÿ ãðàäèåíòíàÿ çàëèâêà. size(5cm,0); path g=scale(2)*unitcircle; label("$a \le r \le b$"); a≤r≤b radialshade(unitcircle^^g,yellow+evenodd,(0,0),1.0, yellow+brown,(0,0),2); draw(pic,g,fillrule+zerowinding); в этом случае цикличность пути g не обязательна. Отметим, что данная процедура обрезает все, что было нарисовано до ее применения, оставляя без обрезки то, что будет нарисовано после. Так, в примере 7.20 обрезается розовый пятиугольник и не обрезается голубой, поскольку первый был вычерчен до применения процедуры clip, а второй – после. Ïðèìåð 7.20. Ïðèìåíåíèå ïðîöåäóðû clip. size(5cm,0); filldraw(scale(1.5)*polygon(5),pink,red); clip(box((-1.5,-0.5),(4.7,1.6))); filldraw(shift((3.2,0))*scale(1.5)*polygon(5),cyan,blue); 7.7 Êàðòèíêè picture Все картинки, выполненные в примерах, были картинкой currentpicture. Но можно создавать и другие картинки, помещая их на текущей картинке в различных местах. Для этого следует объявить картинку как переменную типа picture, а затем рисовать на ней, не забывая добавлять в каждый из операторов название этой картинки. После завершения рисунка полученную картинку (pic1) можно разместить на текущей картинке currentpicture с помощью процедуры void add(picture pic1, bool group=true, filltype filltype=NoFill, bool above=true); или на любой другой картинке (pic2) – с помощью процедуры void add(picture pic2, picture pic1, bool group=true, filltype filltype=NoFill, bool above=true); Параметр group определяет, будет ли графический интерфейс xasy обрабатывать все элементы pic1 как одно целое. Параметр filltype назначает заполнение или обрезание картинки. Наконец, параметр above отвечает за рисование картинки поверх имеющихся объектов или под ними. Глава 7. Модуль plain 84 Ïðèìåð 7.21. Êàðòèíêè íà êàðòèíêå. size(5.5cm,0); pen p=blue+3bp; draw((-2,1)--(2,1)--(0,-3)--cycle,p); draw((0,-3)--(0,1),p); picture pic; filldraw(pic,unitcircle,paleyellow,blue); fill(pic,shift(-0.4,0.3)*scale(0.12)*unitcircle,red); fill(pic,shift(0.4,0.3)*scale(0.12)*unitcircle,red); fill(pic,shift(0,-0.1)*scale(0.2)*polygon(3),brown); draw(pic,arc((0,0),0.6,-45,-135),brown+2bp); add(shift(0,-0.4)*scale(0.6)*pic); add(rotate(180,(0,-3))*shift(0,-3)*scale(0.8)*pic); add(rotate(45,(-2,1))*shift(-2,1)*scale(0.5)*pic); add(rotate(-45,(2,1))*shift(2,1)*scale(0.5)*pic); Рисунок примера 7.21 демонстрирует создание рожицы в виде отдельной картинки, а затем ее размещение в различных местах основной картинки с масштабированием и поворотами. Другим способом добавления картинки является процедура attach, которая автоматически увеличивает размер картинки dest, чтобы добавить к ней фрейм src в позиции, определяемой параметром position: void attach(picture dest=currentpicture, frame src, pair position=0, bool group=true, filltype filltype=NoFill, bool above=true); void attach(picture dest=currentpicture, frame src, pair position, pair align, bool group=true, filltype filltype=NoFill, bool above=true); Фрейм можно располагать относительно точки position с помощью параметра align, заполнять цветом, рисовать поверх основной картинки или под ней. Таким добавляемым к картинке фреймом является список обозначений legend к графикам функций. С его добавлением к этим графикам можно познакомиться на примерах 8.14 и 8.15. Иногда возникает необходимость удалить содержимое картинки. Для этого следует применить функцию void erase(picture pic=currentpicture); Отметим, что при таком удалении размеры картинки сохраняются. 7.8 Ïîäïóòè è ïåðåñå÷åíèÿ Подпути вместе с массивами и точками пересечения являются важным инструментом для получения новых геометрических объектов на основе уже имеющихся. Имеется два варианта формирования подпути. Первый задается функцией path subpath(path p, int a, int b); которая возвращает подпуть пути p от узла сплайна a до узла b. Если a > b, то направление пути будет обратным. Использование этой функции показано в примере 7.22: красный подпуть является частью синего пути между вторым и четвертым узлом сплайна. Стрелка показывает направление прохождения подпути. Второй способ выделения подпути осуществляет функция Глава 7. Модуль plain 85 Ïðèìåð 7.22. Ïîäïóòü ìåæäó óçëàìè ñïëàéíà. size(5cm,0); path g=(-2,-2)..(0,-1)..(2,-1.5)..(2,1.5).. (-2,2.5)..(0,0.5); 4 3 draw(g,blue); Label[] Labels=new Label[4]; 5 for (int i; i<=length(g); ++i) Labels[i]=Label(string(i),black); 1 dot(Labels,g,green+5bp); draw(subpath(g,2,4),red+bp,Arrow(3mm)); 2 0 path subpath(path p, real a, real b); у которой аргументы a и b уже являются действительными числами и имеют смысл значений параметра, определяющего положение точки на пути p в смысле point(path, real). Если a > b, то направление подпути опять-таки будет обратным. Этот второй способ продемонстрирован на рисунке примера 7.23, в котором выделяется подпуть красного цвета от точки, которой отвечает значение упомянутого параметра, равное 0.7, до точки, которой соответствует значение параметра 3.5. Значение 0.7 означает 70% пути между узлами 0 и 1, а 3.5 отсчитывает 50% кривой между узлами 3 и 4. Ïðèìåð 7.23. Ïîäïóòü ìåæäó ïðîèçâîëüíûìè òî÷êàìè. size(5cm,0); path g=(-2,-2)..(0,-1)..(2,-1.5)..(2,1.5)..(-2,2.5)..(0,0.5); draw(g,blue); Label[] Labels=new Label[4]; for (int i; i<=length(g); ++i) Labels[i]=Label(string(i),black); dot(Labels,g,green+5bp); path gg=subpath(g,0.7,3.5); draw(gg,red+bp,Arrow(3mm)); dot(relpoint(gg,0),red+5bp); 4 3 5 1 0 2 Рассмотрим процедуры, осуществляющие поиск точек пересечения путей. Простейшая из них pair extension(pair P, pair Q, pair p, pair q); находит точку пересечения прямых, на которых находятся отрезки P--Q и p--q. На рисунке примера 7.24 красным цветом показана точка пересечения продолжения двух отрезков, найденная с помощью этой процедуры. Однако и для пересекающихся отрезков точка пересечения может быть получена таким же способом, см. рис. примера 7.25. Для произвольных кривых какую-либо точку пересечения путей p и q отыскивает процедура Глава 7. Модуль plain 86 Ïðèìåð 7.24. Ïåðåñå÷åíèå ïðîäîëæåíèé îòðåçêîâ. size(5cm,0); pair E=(-2,3), F=(0,3), G=(-2,5), H=(0,4); draw(E--F^^G--H,blue); pair N=extension(E,F,G,H); dot(E--F^^G--H); draw(F--N,dashed+blue); draw(H--N,dashed+blue); dot(N,red+4bp); Ïðèìåð 7.25. Ïåðåñå÷åíèå îòðåçêîâ. size(5cm,0); pair A=(-2,-1), B=(2,1), C=(-2,2), D=(2,-1.5); draw(A--B^^C--D,blue); pair M=extension(A,B,C,D); dot(M,red+4bp); pair intersectionpoint(path p, path q, real fuzz=-1); В примере 7.26 эта процедура находит точку пересечения синего и красного путей. Ïðèìåð 7.26. Ïåðåñå÷åíèå ïðîèçâîëüíûõ êðèâûõ. size(6.5cm,0); path g=(-2,-1)..(0,1)..(2,-1)..(1,2)..(-1,2)..cycle; draw(g,blue); path h=(-3,2)..(0,-0.5)..(3,2); draw(h,red); path s=(-2.5,2)..(0,0.5)..(2.5,2); draw(s,green); pair GH=intersectionpoint(g,h); dot(GH,brown+3bp); pair[] GS=intersectionpoints(g,s); dot(GS,magenta+3bp); Все точки пересечения путей p и q находит процедура pair[] intersectionpoints(path p, path q, real fuzz=-1); В примере 7.26 результатом ее работы являются точки пересечения синей и зеленой кривой. С помощью этой же процедуры можно найти и точки самопересечения кривой, при этом в их список попадают и узлы сплайна, см. пример 7.27. Поскольку точки пересечения кривых заносятся в одномерный массив, из него можно извлекать только необходимые нам точки. В примере 7.28 из массива GS извлекаются и изображаются на рисунке только точки с индексами [0] и [2]. Еще одна процедура real[] intersect(path p, path q, real fuzz=-1); формирует двухиндексный массив, элемент которого [0] содержит значение параметра для пути p, а элемент [1] – значение параметра для пути q, при которых эти пути пересекаются Глава 7. Модуль plain 87 Ïðèìåð 7.27. Ñàìîïåðåñå÷åíèå êðèâîé. size(6.5cm,0); path p=(-2.5,-4)..(0,-1)..(2.5,-4)..(1,-2).. (-1,-2)..cycle; draw(p,blue); pair[] PP=intersectionpoints(p,p); dot(PP,red+3bp); Ïðèìåð 7.28. Èçâëå÷åíèå òî÷åê ïåðåñå÷åíèÿ èç îäíîìåðíîãî ìàññèâà. size(6.5cm,0); path g=(-2,-1)..(0,1)..(2,-1)..(1,2)..(-1,2)..cycle; path s=(-2.5,2)..(0,0.5)..(2.5,2); draw(g,blue); draw(s,green); pair[] GS=intersectionpoints(g,s); dot(Label("GS[0]",black),GS[0],3*WSW,magenta+3bp); dot(Label("GS[2]",black),GS[2],2*E,magenta+3bp); GS[2] GS[0] (в смысле point(path, real)). На рис. примера 7.29 точка P является точкой пересечения с индексом [1] для кривой p, причем, значение параметра для этой кривой равно PQ[1][0] (индекс [0] нумерует «первую» кривую, p). Точка Q является точкой пересечения с индексом [3] для кривой q, причем, значение параметра для этой кривой равно PQ[3][1] (индекс [1] нумерует «вторую» кривую, q). Ïðèìåð 7.29. Èçâëå÷åíèå òî÷åê ïåðåñå÷åíèÿ èç äâóìåðíîãî ìàññèâà. size(6.5cm,0); path g=(-2,-1)..(0,1)..(2,-1)..(1,2).. (-1,2)..cycle; path s=(-2.5,2)..(0,0.5)..(2.5,2); path p=shift(0,-3.5)*g; path q=shift(0,-3.5)*s; draw(p,blue); draw(q,green); real[][] PQ=intersections(p,q); pair P=point(p,PQ[1][0]); dot(Label("P",black),P,1.5*SSW,red+3bp); pair Q=point(q,PQ[3][1]); dot(Label("Q",black),Q,2*dir(170),red+3bp); Q P 7.9 Ôðàãìåíòû ïóòè Для выделения фрагментов (slice) путей используются их точки пересечения, структура struct slice { Глава 7. Модуль plain } 88 path before,after; и описанные в первой части документа функции путей: slice cut(path p, path knife, int n); slice firstcut(path p, path knife); slice lastcut(path p, path knife); В примере 7.30 путь-нож (knife) оранжевого цвета пересекает основной путь p четыре раза. В результате применения функции cut создается зеленый фрагмент пути p до третьей точки пересечения и синий фрагмент – после четвертой точки пересечения. Ïðèìåð 7.30. Âûäåëåíèå ôðàãìåíòîâ ïóòè. size(6.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); pair A=(-4,-1), B=(-2,1.5), C=(0,1.5), D=(2,0), E=(4,3.5); path p=(-4,.2){(1,0)}..{(1,2)}(4,3), knife=A{dir(0)}..B..(-1,-1.5)..C..D..{dir(0)}E; path bf=cut(p,knife,2).before, af=cut(p,knife,3).after; draw(p,mahogany); draw(knife,orange); draw(bf,2bp+green); draw(af,2bp+blue); Если из текста программы убрать оператор draw(p,mahogany);, то будут нарисованы лишь фрагменты пути p. В следующем примере 7.31 показано, как нарисовать фрагмент пути между двумя точками пересечения. Ïðèìåð 7.31. Âûðåçàíèå ôðàãìåíòà ïóòè. size(6.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); pair A=(-4,-1), B=(-2,1.5), C=(0,1.5), D=(2,0), E=(4,3.5); path p=(-4,.2){(1,0)}..{(1,2)}(4,3), knife=A{dir(0)}..B..(-1,-1.5)..C..D..{dir(0)}E; path af1=cut(p,knife,2).after, af2=cut(p,knife,3).after; draw(af1,green); draw(af2,white+bp); draw(knife,orange); 7.10 Ñîçäàíèå çàìêíóòûõ ïóòåé Чтобы создать замкнутый путь из кусочков уже имеющихся путей, в Asymptote имеется специальная функция buildcycle. В примере 7.32 незамкнутый путь зеленого цвета разбивает область, ограниченную замкнутым путем синего цвета, на две области. На рис. мы видим один из возможных замкнутых путей, красного цвета, построенный с помощью buildcycle. Глава 7. Модуль plain 89 Ïðèìåð 7.32. Ïåðâûé âàðèàíò çàìêíóòîãî ïóòè. size(6.5cm,0); pair [] A; A[4]=(0,2); A[1]=(1,3); A[2]=(3,2); A[3]=(3,-1.5); pair [] B; B[1]=(-2,0); B[2]=(1,1.5); B[3]=(2,0); B[4]=(1,-2.5); path [] c; c[1] = A[1]..{(1,0)}A[2]..{(-1,0)}A[3]..A[4]..cycle; c[2] = B[1]..{(1,0)}B[2]..B[3]..B[4]; c[3] = buildcycle(c[1],c[2]); draw(c[1],green^^c[2],blue); draw(c[3], 2bp+red); Ïðèìåð 7.33. Ôîðìèðîâàíèå ïåðâîãî âàðèàíòà çàìêíóòîãî ïóòè. pair [] A; A[4]=(0,2); A[1]=(1,3); A[2]=(3,2); A[3]=(3,-1.5); pair [] B; B[1]=(-2,0); B[2]=(1,1.5); B[3]=(2,0); B[4]=(1,-2.5); path [] c; c[1] = A[1]..{(1,0)}A[2]..{(-1,0)}A[3]..A[4]..cycle; c[2] = B[1]..{(1,0)}B[2]..B[3]..B[4]; c[3] = buildcycle(c[1],c[2]); draw(c[1],blue); draw(c[2],green); draw(c[3], 1bp+red); for(int i=0; i<length(c[1]); ++i) dot(string(i),point(c[1],i),NW,blue); for(int i=0; i<=length(c[2]); ++i) dot(string(i),point(c[2],i),SW,green); for(int i=1; i<length(c[3]); ++i) dot(string(i),point(c[3],i),NW,red); dot(string(0),point(c[3],0),SSE,red); 0 3 1 1 2 1 3 2 0 0 2 3 Чтобы понять механизм действия buildcycle, добавим к рисунку номера узлов, обозначив их тем же цветом, что и соответствующие им пути (рис. примера 7.33). Направление на кривых указывается возрастанием номеров узлов. В примере у функции buildcycle имеется два аргумента c[1] и c[2], причем, их порядок имеет значение. Движение пера начинается с нулевого узла, который определяется следующим образом. Поскольку первым аргументом buildcycle является c[1], то движение начинается из его нулевой синей точки до первой точки его пересечения с зеленым путем c[2]. Эта точка пересечения (начальная точка формируемого замкнутого пути) обозначена на рис. нулем красного цвета. От нее перо движется вдоль синего пути до второй точки пересечения путей, которая обозначена красной единицей. Далее перо перемещается вдоль зеленого пути, проходя через узлы 1 и 2 зеленого цвета. Эти узлы становятся узлами нового замкнутого пути, почему и получают номера 2 и 3 красного цвета. Движение пера заканчивается в начальной точке с номером 0 (красного цвета). В следующем примере 7.34 вместо пути c[1] используется путь c[3], который отличается от первого только направлением обхода. Порядок аргументов buildcycle тоже другой: пер- Глава 7. Модуль plain 90 вым аргументом является c[2], а вторым – c[3]. Мы видим, что теперь получается совсем другой замкнутый путь красного цвета. Ïðèìåð 7.34. Âòîðîé âàðèàíò çàìêíóòîãî ïóòè. size(6.5cm,0); pair [] A; A[4]=(0,2); A[1]=(1,3); A[2]=(3,2); A[3]=(3,-1.5); pair [] B; B[1]=(-2,0); B[2]=(1,1.5); B[3]=(2,0); B[4]=(1,-2.5); path [] c; c[1] = A[1]..{(1,0)}A[2]..{(-1,0)}A[3]..A[4]..cycle; c[2] = B[1]..{(1,0)}B[2]..B[3]..B[4]; c[3] = reverse(c[1]); c[4] = buildcycle(c[2],c[3]); draw(c[3],blue); draw(c[2],green); draw(c[4],bp+red); Функция buildcycle может иметь более двух аргументов, как в примере 7.35, в котором эта функция находит новый замкнутый путь, используя части трех кривых. Ïðèìåð 7.35. Çàìêíóòûé ïóòü èç òðåõ êðèâûõ size(5cm,0); path [] c; c[1] = (-2,-.5){up}..tension 1.2..(2,-.5){down}; c[2] = rotate(60)*c[1]; c[3] = rotate(-60)*c[1]; c[4] = rotate(180)*c[1]; draw(c[2]^^c[3]^^c[4],blue); c[5] = buildcycle(c[2],c[3],c[4]); fill(c[5],lightyellow); draw(c[5],red+2bp); Пример 7.36 демонстрирует возможности использования замыкания путей для закрашивания областей плоскости. Вначале каждый круг закрашивается одним цветом: красным, зеленым и синим, затем с помощью процедуры buildcycle находятся замкнутые пути, возникающие при пересечении пар окружностей, и получаемые таким образом области закрашиваются цветами, равными сумме цветов, которыми были закрашены соответствующие круги. Затем отыскивается замкнутый путь, возникающий при пересечении всех трех окружностей, и закрашивается белым цветом. 7.11 Ìîäóëè geometry è olympiad Для иллюстрации и решения задач геометрии на плоскости Asymptote предоставляет пользователю мощный модуль geometry, который входит в ее стандартную поставку. Модуль включает в себя • возможность создания и использования различных систем координат на плоскости; • введение и применение точек, обладающих массой; Глава 7. Модуль plain 91 Ïðèìåð 7.36. Èñïîëüçîâàíèå çàìêíóòûõ ïóòåé èç äâóõ è òðåõ êðèâûõ. size(5cm,0); path a,b,c; a = shift(1,0)*scale(2)*unitcircle; b = rotate(120)*a; c = rotate(120)*b; fill(a, red); fill(b, green); fill(c, blue); fill(buildcycle(a,b), red + green); fill(buildcycle(b,c), green + blue); fill(buildcycle(c,a), blue + red); fill(buildcycle(a,b,c), white); draw(a^^b^^c); • задание преобразований плоскости, отличных от аффинных; • создание, изображение и обработку коник (эллипсов, парабол и гипербол); • дотошный и разнообразный расчет треугольников, включая введение специальных трилинейных координат; • определение и изображение различных инверсий. Поскольку существует достаточно полное описание geometry [6], то за разъяснениями и примерами следует обратиться к нему. Иллюстрацию задач школьной геометрии (включая олимпиадные) способен также выполнить другой, более простой и приземленный модуль olympiad. По крайней мере, для этой цели его создала автор Мария Монкс. Этот модуль не входит в поставку Asymptote, поэтому его при желании надо скачать из Интернета в виде, например, содержимого интернетной страницы, а затем вставить в пустой asy-файл, сохранив затем как olympiad.asy в основной директории Asymptote (там, где находятся и остальные модули). Модуль имеет в своем составе следующие функции. origin Начало координат на плоскости в виде pair(0,0). waypoint(path p, real r) Возвращает точку, находящуюся от начала пути p на относительном расстоянии r, измеряемом вдоль p, 0 ≤ r ≤ 1. foot(pair P, A, B) Определяет основание перпендикуляра, опущенного из точки P на прямую AB. bisectorpoint(pair A, B, C) Находит точку на биссектрисе ∠ABC, расположенную на единичном расстоянии от B. bisectorpoint(pair A, B) Возвращает точку на перпендикуляре к отрезку AB, находящуюся на единичном расстоянии от AB. circumcenter(pair A, B, C) Находит центр описанной вокруг 4ABC окружности. circumradius(pair A, B, C) Возвращает радиус описанной вокруг 4ABC окружности. circumcircle(pair A, B, C) Создает описанную вокруг 4ABC окружность. incenter(pair A, B, C) Находит центр вписанной в 4ABC окружности. Глава 7. Модуль plain 92 inradius(pair A, B, C) Возвращает радиус вписанной в 4ABC окружности. incircle(pair A, B, C) Создает вписанную в 4ABC окружность. tangent(pair P, pair O, real r, int n=1) Возвращает одну из двух точек касания (при n = 1 и n = 2, соответственно) для касательной, проведенной из точки P к окружности с центром в точке O радиуса r. cyclic(pair A, B, C, D) Возвращает true, если ABCD – четырехугольник. concurrent(pair A, B, C, D, E, F) Возвращает true, если AB, CD и EF совпадают или взаимно параллельны. collinear(pair A, B, C) Возвращает true, если A, B и C коллинеарны. centroid(pair A, B, C) Находит центр масс 4ABC. orthocenter(pair A, B, C) Определяет ортоцентр (точку пересечения высот) 4ABC. rightanglemark(pair A, B, C, real s=8) Обозначает прямой угол ABC общепринятой для таких углов отметкой размера s. Впрочем, отмечает не только прямые углы. anglemark(pair A, B, C, real t=8 ... real [] s) Обозначает угол ABC несколькими круговыми дугами радиусов, определяемых последним аргументом, произвольным массивом действительных чисел. pathticks(path g, int n=1, real r=.5, spacing=6, s=8, pen p=currentpen) Создает картинку picture, которая помечает путь g с помощью n черточек длины s, расположенных на расстоянии spacing друг от друга. Черточки строятся вокруг точки, находящейся на относительном расстоянии r от начала пути g. Расстояние измеряется вдоль этого пути. На рис. примера 7.37 представлен результат работы модуля olympiad. Вначале задаются вершины треугольника ABC, затем вычисляются центры описанной (O) и вписанной (I) окружностей. Далее перечисленные точки изображаются на рисунке вместе с указанными окружностями. Центры окружностей обозначаются с помощью меток. После этого программа находит основание перпендикуляра D, опущенного из точки I на сторону AB, и строит отрезок ID, изображая таким образом радиус вписанной окружности. Следующим шагом является определение точки E пересечения описанной окружности с лучом, выходящим из ее центра, и рисование радиуса OE описанной окружности. Показана также пометка прямого угла, другого угла – двумя дугами окружностей и стороны – двумя черточками. 7.12 Ìîäóëü LSystem Филиппом Ивальди для системы Asymptote написан модуль Lsystem, позволяющий строить и изображать так называемые L-фракталы, то есть фракталы, которые конструируются с помощью L-систем [8]. Основной структурой модуля является структура Lsystem, в которую входит множество данных, включая различные процедуры и функции. Процедура init инициализирует структуру и ее прототип можно описать как Глава 7. Модуль plain 93 Ïðèìåð 7.37. Ïðèìåíåíèå ìîäóëÿ olympiad. import olympiad; unitsize(70); pair A,B,C,O,I; A=origin; B=2*right; C=1.5*dir(70); O=circumcenter(A,B,C); I=incenter(A,B,C); draw(A--B--C--cycle,red); dot(O); dot(I); draw(circumcircle(A,B,C),blue); draw(incircle(A,B,C),heavygreen); label("$I$",I,W); label("$O$",O,S); pair D=foot(I,A,B); draw(I--D); pair E=(2,1.3); pair OCE=intersectionpoint(O--E,circumcircle(A,B,C)); draw(O--OCE); draw(anglemark(A,C,B,8,6.5),brown); draw(rightanglemark(A,D,I,3)); add(pathticks(A--B,2,.5,1,3,purple)); I O void Lsystem(string Lstart="F", string[][] Lrule=new string[][]{{"F"},{"F-F+F+F-F"}}, real La=90,real Lai=0, real Llength=1); Первым параметром процедуры является аксиома L-системы в виде строки Lstart. Набор порождающих правил задает двумерный массив Lrule; причем число правил не ограничено. Каждое правило состоит из двух строк: первая содержит заменяемый символ, а вторая – заменяющую его строку. Угол θ обозначен La, а угол α – La1. Параметр Llength означает длину шага, которая по умолчанию равна 1. Как видно из определения функции, ее параметры полностью инициализированы, так что ее можно сразу запускать, не задавая никаких аргументов. Для запуска служит процедура iterate структуры Lsystem, для которой следует задать число итераций построения фрактала. При нулевом значении параметра будет сгенерирована фигура, соответствующая строке Lstart. При выполнении iterate внутри структуры Lsystem создается фрактал в виде массива путей и процедура paths, способная предоставить полученный массив пользователю. Например, этот массив можно будет нарисовать. Пример 7.38 демонстрирует фрактал, который по умолчанию строит Asymptote. Ïðèìåð 7.38. Ôðàêòàë ïî óìîë÷àíèþ. import Lsystem; size(0,5cm); Lsystem proba=Lsystem(); proba.iterate(5); draw(proba.paths(),brown); В качестве основных в порождающих правилах применяются символы F (шаг вперед с рисованием), f (холостой шаг без рисования, символы [, ] (для ветвления фрактала) и символ |, Глава 7. Модуль plain 94 вызывающий поворот на 180◦ . В строки правил можно включать любые другие буквы и даже цифры, на которые программа реагировать не будет, а смысл их введения заключается в возможности формирования хитроумных управляющих слов. Для получения так называемой Мозаики Пенроуза в качестве таких символов выбраны цифры 6, 7, 8 и 9, пример 7.39. Ïðèìåð 7.39. Ìîçàèêà Ïåíðîóçà. import Lsystem; size(6cm,0); string[][] rules= {{"6","8F++9F----7F[-8F----6F]++"}, {"7","+8F--9F[---6F--7F]+"}, {"8","-6F++7F[+++8F++9F]-"}, {"9","--8F++++6F[+9F++++7F]--7F"}, {"F",""}}; Lsystem penrose= Lsystem("[7]++[7]++[7]++[7]++[7]", rules,La=36); penrose.iterate(4); draw(penrose.paths(),lightcyan+0.7bp); shipout(bbox(}3mm,Fill(heavyblue))); Изображение выполнено на синем фоне, но раскрасить можно не только фон, но и сам фрактал. Дело в том, что созданный процедурой iterate массив путей можно замкнуть с помощью конструкции &cycle. После этого он превращается в фигуру, которую можно закрасить и обвести контуром. В Asymptote это, конечно, выполняет оператор filldraw. Пример 7.40 демонстрирует раскрашивание фрактала серым цветом с желтой обводкой. Ïðèìåð 7.40. Ôðàêòàë â öâåòå. import Lsystem; size(6cm,0); string[][] rules={{"L","+R-F-R+"}, {"R","-L+F+L-"}}; Lsystem sqCurve=Lsystem("L--F--L--F", rules,La=45,Lai=45); sqCurve.iterate(9); filldraw(sqCurve.paths()[0]&cycle,grey,yellow); shipout(bbox(3mm,Fill(black))); Для раскраски фракталов-деревьев используют бинарное дерево tree, которое также формирует процедура iterate. Это дерево содержит массив индексов своих ветвей keys и для каждой ветви ее глубину depth. Организуя цикл по ветвям дерева, можно в зависимости от глубины ветви устанавливать те или иные свойства рисующего ее пера (цвет, прозрачность, Глава 7. Модуль plain 95 толщину). Раскрашенное таким образом дерево иллюстрирует пример 7.41. Ïðèìåð 7.41. Äåðåâî-ôðàêòàë. import Lsystem; size(8cm,0); string[][] rules= {{"X","F[+X][-X]FX"},{"F","FF"}}; Lsystem plant=Lsystem("X",rules,La=25,Lai=90); plant.iterate(8); path[] g=plant.paths(); tree g=plant.tree(); for (int i:g.keys){ if(g[i].depth<=10) draw(g[i].g,heavygreen); } for (int i:g.keys){ int m=g[i].depth; if (m>=11) draw(g[i].g, brown+(1+0.5*(m-12))*bp+opacity(0.3)); } С построением с помощью Asymptote фракталов других типов можно познакомиться по [4]. В частности, там показано использование Asymptote для изображения • L-фракталов, конструируемых не только с помощью рассматриваемого модуля, но и по другим алгоритмам, включая специальные методы расцвечивания фракталов; • СИФ-фракталов (создаваемых с помощью систем итерированных функций), в том числе со сгущением и с параметрами; • раскрашенных СИФ-фракталов; • фракталов комплексной динамики, множеств Жюлиа и Мандельброта, получаемых по разнообразным алгоритмам. Ãëàâà 8 Ìîäóëü graph Модуль graph реализует двумерные графики, включая линейное, логарифмическое и произвольное масштабирование, выбор отметок (с возможностью переопределения вручную) и присоединение списка обозначений. График представляет собой guide. 8.1 Îñè êîîðäèíàò 8.1.1 Ïðîöåäóðà xaxis Ось абсцисс создается процедурой void xaxis(picture pic=currentpicture, Label L="", axis axis=YZero, real xmin=-infinity, real xmax=infinity, pen p=currentpen, ticks ticks=NoTicks, arrowbar arrow=None, bool above=false); которая рисует ось Ox на картинке pic от абсциссы õ=Xmin до абсциссы õ=Xmax пером ð, снабжая ось, если требуется, меткой L. Относительное расположение метки вдоль оси регулируется вещественным числом из диапазона [0,1], которое по умолчанию равно 1, так что в этом случае метка располагается в конце оси. Бесконечное значение Xmin или Xmax означает, что ось будет автоматически ограничиваться теми ограничениями, которые накладываются на картинку в целом. Необязательный аргумент arrow принимает те же значения, что и в процедуре draw. Ось рисуется поверх любых имеющихся на рисунке объектов, если above=true. В примере 8.1 вначале ось абсцисс нарисована такой, какой она предполагается по умолчанию: черного цвета, без стрелки и название переменной расположено внизу. Вторая ось уже имеет стрелку. Наконец, третья координатная ось изображена разными цветами, а ее метка помещена справа от стрелки. Расположение оси определяется одной из следующих процедур. YZero(bool extend = true) Рисует ось, начиная с y = 0 (или y = 1 для логарифмической оси) до самого края картинки, если не установлено extend=false. YEquals(real Y, bool extend=true) Рисует ось, начиная с y = Y до края картинки, если не установлено extend=false. Bottom(bool extend=false) Ось будет внизу рисунка. Top(bool extend=false) Ось будет вверху рисунка. BottomTop(bool extend=false) Оси будут и внизу, и вверху рисунка. 96 Глава 8. Модуль graph 97 Ïðèìåð 8.1. Ïðîñòûå âàðèàíòû îñè àáñöèññ. import graph; size(6.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); picture pic1,pic2; xaxis("$x$"); xaxis(pic1,"$x$",arrow=Arrow); add(shift(0,-1)*pic1); xaxis(pic2,L=Label("$x$",align=2*right,mahogany), xmin=-3,xmax=4.5,p=orange, arrow=Arrow(2mm,Fill(mahogany))); add(shift(0,-2)*pic2); x x x Пользователь может построить требуемую ось, просто модифицируя стандартную процедуру вида import graph; YZero=new axis(bool extend=true) { return new void(picture pic, axisT axis) { real y=pic.scale.x.scale.logarithmic ? 1 : 0; axis.value=I*pic.scale.y.T(y); axis.position=1; axis.side=right; axis.align=2.5E; axis.value2=Infinity; axis.extend=extend; }; }; YZero=YZero(); По умолчанию черточки-отметки (ticks) на оси не изображаются, что́ выражается равенством ticks=NoTicks в определении процедуры xaxis. Применяя опции LeftTicks: ticks LeftTicks(Label format="", ticklabel ticklabel=null, bool beginlabel=true, bool endlabel=true, int N=0, int n=0, real Step=0, real step=0, bool begin=true, bool end=true, tickmodifier modify=None, real Size=0, real size=0, bool extend=false, pen pTick=nullpen, pen ptick=nullpen); RightTicks или Ticks, можно рисовать черточки слева, справа или с обеих сторон пути по направлению его прохождения. При отсутствии в процедуре рисования черточек каких-либо необязательных аргументов, по умолчанию будут использованы подходящие значения. Параметры имеют следующий смысл. Label format Перекрывает формат метки для черточки (defaultformat, "$%.4g$"), ее вращение, перо и выравнивание относительно оси (например, LeftSide, Center, RightSide). Чтобы обеспечить возможность использования математической моды LATEX, строка должна начинаться и заканчиваться символами $. Если строка содержит нули в младших разрядах, эти нули будут добавлены к меткам; вывод метки будет подавлен, если строка имеет вид "%". Глава 8. Модуль graph 98 ticklabel Функция вида string(real x) возвращает метку (format(format.s,x) по определению) для каждой крупной черточки, отвечающей значению x. bool beginlabel Определяет, рисовать ли первую черточку. bool endlabel Определяет, рисовать ли последнюю черточку. int N Задает число интервалов, при автоматическом масштабировании равномерно расположенных на оси и разделенных крупными черточками; для логарифмической оси это означает число десятков между черточками. int n Определяет число интервалов, разделенных мелкими черточками, на которые делится каждый интервал, ограниченный крупными черточками. real Step Задает расстояние между крупными черточками (если N=0). real step Задает расстояние между мелкими черточками (если n=0). bool begin Определяет, рисовать ли первую крупную черточку. bool end Определяет, рисовать ли последнюю крупную черточку. tickmodifier modify; Необязательная функция, которая принимает и возвращает структуру tickvalue типа real [], элементами которой являются major и minor, состоящие из значений черточек (чтобы разрешить модификацию автоматически генерируемых значений черточек). real Size Устанавливает длину крупных черточек (в PostScript-координатах). real size Устанавливает длину мелких черточек (в PostScript-координатах). bool extend Рисует черточки между двумя осями (полезно для изображения сетки). pen pTick Определяет перо, используемое для рисования крупных черточек. pen ptick Определяет перо, используемое для рисования мелких черточек. На рис. примера 8.2 оси координат снабжены отметками. Для первой оси использовалась опция ticks=LeftTicks, для второй – ticks=RightTicks, для третьей – ticks=Ticks. Первая и последняя длинные черточки не рисовались, короткие и длинные черточки изображались разными цветами. Для удаления части автоматически генерируемые черточек и их меток используют предопределенные модификации OmitTick (... real[] x) è OmitTickIntervals(real[] a, real[]b). Модификация OmitFormat(string s=defaultformat ... real[] x) может быть применена для удаления некоторых меток, но не соответствующих им черточек. Модификация NoZero является аббревиатурой для OmitTick(0), а модификация NoZeroFormat является сокращением для OmitFormat(0). Применение этих модификаций показано в примере 8.3. Некоторые черточки удалены вместе с их метками. Можно задать собственную локализацию черточек, используя LeftTicks, RightTicks и Ticks и заполняя массив Ticks и необязательный массив ticks, содержащие, соответственно, крупные и мелкие черточки: Глава 8. Модуль graph 99 Ïðèìåð 8.2. Ñòàíäàðòíûå îòìåòêè íà îñÿõ. import graph; size(7.5cm,0); picture pic1,pic2; xaxis("$x$",xmin=-3,xmax=3, ticks=LeftTicks(N=6,n=5,begin=false, end=false,pTick=blue,ptick=green)); xaxis(pic1,"$x$",xmin=-3,xmax=3, ticks=RightTicks(N=6,n=5,begin=false, end=false,pTick=blue,ptick=green)); add(shift(0,-1.5)*pic1); xaxis(pic2,"$x$",xmin=-3,xmax=3, ticks=Ticks(N=6,n=5,begin=false, end=false,pTick=blue,ptick=green)); add(shift(0,-3)*pic2); −3 −2 −1 0 1 2 3 x −3 −2 −1 0 1 2 3 x −3 −2 −1 0 1 2 3 x −1 0 Ïðèìåð 8.3. Óäàëåíèå íåêîòîðûõ îòìåòîê. import graph; size(7.5cm,0); xaxis(L="$x$",axis=YZero,xmin=-3,xmax=3, ticks=Ticks(ticklabel=OmitFormat(1), endlabel=false,Step=1,step=.5,end=false, modify=OmitTick(-2),pTick=bp+red, ptick=bp+.8green),arrow=Arrow(2mm)); −3 2 x ticks LeftTicks(Label format="", ticklabel ticklabel=null, bool beginlabel=true, bool endlabel=true, real[] Ticks, real[] ticks=new real[], real Size=0, real size=0, bool extend=false, pen pTick=nullpen, pen ptick=nullpen) В примере 8.4 как раз и изображаются только те черточки, координаты которых заданы специальными массивами. Ïðèìåð 8.4. Çàäàíèå îòìåòîê ìàññèâàìè. import graph; size(7.5cm,0); real[] tabT={-2,0,1}, tabt={-1.5,-0.5,0.5}; xaxis(L="$x$",axis=YZero(extend=true), xmin=-3,xmax=3,ticks=Ticks(Ticks=tabT, ticks=tabt,pTick=bp+red,ptick=green), arrow=Arrow(2mm),above=false); 8.1.2 Ïðîöåäóðà −2 0 1 yaxis Ось ординат изображается процедурой void yaxis(picture pic=currentpicture, Label L="", axis axis=XZero, real ymin=-infinity, real ymax=infinity, pen p=currentpen, x Глава 8. Модуль graph 100 ticks ticks=NoTicks, arrowbar arrow=None, bool above=false, bool autorotate=true); Смысл аргументов аналогичен смыслу аргументов процедуры xaxis. Отличие заключается в том, что имеется параметр autorotate, при значении которого true метка оси поворачивается. Расположение оси определяется с помощью одного из следующих процедур: XZero(bool extend=true) Рисует ось, начиная с x = 0 (или x = 1 для логарифмической оси), до самого края картинки, если не установлено extend=false. XEquals(real X, bool extend=true) Рисует ось, начиная с x = X, до края картинки, если не установлено extend=false. Left(bool extend=false) Ось будет расположена слева. Right(bool extend=false) Ось будет расположена справа. LeftRight(bool extend=false) Ось будет расположена и слева, и справа. На рис. примера 8.5 продемонстрировано использование двух осей ординат. Ïðèìåð 8.5. Äâå îñè îðäèíàò. import graph; size(7.5cm,0); xaxis(L=Label("$x$",align=4*right),xmin=-3, xmax=3,ticks=Ticks(endlabel=false, beginlabel=false,Step=1,step=.5, end=false,pTick=bp+red, ptick=bp+.8green),arrow=Arrow); yaxis(L="$y$", axis=LeftRight,ymin=-1, ymax=3,ticks=Ticks(beginlabel=false, begin=false,end=false,Step=1,step=.25, pTick=bp+red,ptick=bp+.8green), arrow=Arrow); 8.1.3 3 2 y 1 x 0 −2 −1 0 1 2 Ïðîöåäóðû xlimits, ylimits è limits Минимальное и максимальное значения x для графика можно зафиксировать с помощью процедуры xlimits(picture pic=currentpicture, real min=-infinity, real max=infinity, bool crop=NoCrop); и аналогичной процедуры ylimits для фиксации минимального и максимального значений y. Функция void limits(picture pic=currentpicture, pair min, pair max, bool crop=NoCrop); может быть использована, чтобы заключить координатные оси в прямоугольник, противоположными вершинами которого являются пары min и max. Созданные на картинке pic объекты будут обрезаны по заданным границам, если crop=Crop. Для обрезания графиков по заданным границам существует и отдельная функция crop(picture pic). В следующем примере 8.6 кое-что из рассмотренного применено для построения координатной сетки. Там же показано использование установки extend=true. Глава 8. Модуль graph 101 Ïðèìåð 8.6. Êîîðäèíàòíàÿ ñåòêà. import graph; size(7.5cm,0); 3 xlimits(-3, 3); ylimits(-1, 3); xaxis(L="$x$", axis=BottomTop, ticks=Ticks(Step=1,step=.5,extend=true, pTick=bp+red,ptick=1.2bp+dotted), arrow=Arrow); 2 y 0 yaxis(L="$y$", axis=LeftRight, ticks=Ticks(Step=1,step=.5,extend=true, pTick=bp+red,ptick=1.2bp+dotted), arrow=Arrow,above=true); 8.1.4 Ïðîöåäóðû 1 −1 −3 −2 −1 0 x 1 2 3 xequals è yequals Функции void xequals(picture pic=currentpicture, Label L="", real x, bool extend=false, real ymin=-infinity, real ymax=infinity, pen p=currentpen, ticks ticks=NoTicks, bool above=true, arrowbar arrow=None); void yequals(picture pic=currentpicture, Label L="", real y, bool extend=false, real xmin=-infinity, real xmax=infinity, pen p=currentpen, ticks ticks=NoTicks, bool above=true, arrowbar arrow=None); могут быть использованы для вызова процедур xaxis и yaxis с подходящими значениями XEquals(x,extend) и YEquals(y,extend). Их рекомендуется применять для рисования вертикальных линий и осей в требуемом (не стандартном) расположении. Пример 8.7 показывает построение осей координат для нестандартных диапазонов значений x и y, причем, минимальные значения для осей указаны в аргументах функций xequals и yequals. Ïðèìåð 8.7. Íåñòàíäàðòíûå àáñöèññû è îðäèíàòû. import graph; size(7.5cm,0); y 55 xlimits(17,23); ylimits(52,55); 54 xequals(L="$y$",x=17,ticks=Ticks(Step=1, step=.5,end=false), arrow=Arrow(HookHead)); yequals(L="$x$",y=52,ticks=Ticks(Step=1, step=.5,end=false), arrow=Arrow(HookHead)); 53 52 17 18 19 20 21 22 23 x Следующие процедуры полезны для расстановки черточек и их меток на осях вручную (если переменная Label задана как аргумент Label, то аргумент format будет использовать формат строки, опирающийся на расположение черточки): Глава 8. Модуль graph 102 void xtick(picture pic=currentpicture, Label L="", explicit pair z, pair dir=N, string format="", real size=Ticksize, pen p=currentpen); void xtick(picture pic=currentpicture, Label L="", real x, pair dir=N, string format="", real size=Ticksize, pen p = currentpen); void ytick(picture pic=currentpicture, Label L="", explicit pair z, pair dir=E, string format="", real size=Ticksize, pen p=currentpen); void ytick(picture pic=currentpicture, Label L="", real y, pair dir=E, string format="", real size=Ticksize, pen p=currentpen); void tick(picture pic=currentpicture, pair z, pair dir, real size=Ticksize, pen p=currentpen); void labelx(picture pic=currentpicture, Label L="", explicit pair z, align align=S, string format="", pen p=currentpen); void labelx(picture pic=currentpicture, Label L="", real x, align align=S, string format="", pen p=currentpen); void labelx(picture pic=currentpicture, Label L, string format="", explicit pen p=currentpen); void labely(picture pic=currentpicture, Label L="", explicit pair z, align align=W, string format="", pen p=currentpen); void labely(picture pic=currentpicture, Label L="", real y, align align=W, string format="", pen p=currentpen); void labely(picture pic=currentpicture, Label L, string format="", explicit pen p=currentpen); Пример 8.8 демонстрирует, как процедуры labelx и labely позволяют изобразить числа для отметок на оси так, чтобы их не пересекали линии сетки. Ïðèìåð 8.8. Íåñòàíäàðòíûå àáñöèññû è îðäèíàòû. import graph; size(7.5cm,0); xlimits(-1, 5); ylimits(-2, 2); xaxis(BottomTop, Ticks("%",extend=true, pTick=linewidth(.5pt),ptick=bp+dotted)); yaxis(LeftRight, Ticks("%",extend=true, pTick=linewidth(.5pt),ptick=bp+dotted)); xequals(Label("$y$",align=2.3N),0, p=linewidth(bp),Arrow(2mm)); yequals(Label("$x$",align=2.3E),0, p=linewidth(bp),Arrow(2mm)); labelx(Label("$2$",UnFill),1); labely(Label("$2$",UnFill),1); dot("$O$",(0,0),1.6SW); 8.1.5 Ïðîöåäóðà y 2 O 2 x axes Процедура void axes(picture pic=currentpicture, Label xlabel="", Label ylabel="", bool extend=true, pair min=(-infinity,-infinity), Глава 8. Модуль graph 103 pair max=(infinity,infinity), pen p=currentpen, arrowbar arrow=None, bool above=false); позволяет нарисовать сразу и ось абсцисс, и ось ординат, обе одинаковой формы со стрелками на концах. Оси изображаются поверх уже нарисованных объектов, только если задано above=true. В примере 8.9 эта процедура использована для изображения двух координатных систем. Ïðèìåð 8.9. Äâå ñèñòåìû êîîðäèíàò. import graph; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); xlimits(-1,6); ylimits(-1,3); xaxis(L="$x$",p=bp+blue,ticks=Ticks(NoZero, endlabel=false,beginlabel=false,Step=1, y Y step=.5,begin=false,end=false), 3 arrow=Arrow(TeXHead)); yaxis(L="$y$",p=bp+blue,ticks=Ticks(NoZero, 2 1 beginlabel=false,Step=1,step=.5, begin=false,end=false), 1 arrow=Arrow(TeXHead)); A labelx("$O$",0,2*SW,blue); picture pic; unitsize(pic,1cm); axes(pic,xlabel=Label("$X$",align=2.3E), O 1 2 3 ylabel=Label("$Y$",align=2.3N), min=(-4,-2),max=(3,2),p=dashed+mahogany, arrow=Arrow(HookHead)); labelx(pic,"$A$",(0,0),2*SW,mahogany); xtick(pic,"$1$",1,size=1mm,mahogany);xtick(pic,dir=S,1,size=1mm,red); ytick(pic,"$1$",1,size=1mm,mahogany);ytick(pic,dir=W,1,size=1mm,red); add(pic.fit(),(3,1)); 8.1.6 Ïðîöåäóðà X 1 4 5 x axis С помощью процедуры void axis(picture pic=currentpicture, Label L="", path g, pen p=currentpen, ticks ticks, ticklocate locate, arrowbar arrow=None, int[] divisor=new int[], bool above=false, bool opposite=false); любой путь можно представить в виде оси. Путь можно снабдить меткой, стрелкой и разметить черточками. Опциональный параметр divisor вводится для того, чтобы откорректировать ситуацию, когда черточки мешают друг другу. Значение флага opposite=true сигнализирует о наличии дополнительной оси без метки. Ось рисуется поверх уже нарисованных объектов только при above=true. Процедура размещения черточек ticklocate имеет следующий вид: ticklocate ticklocate(real a, real b, autoscaleT S=defaultS, real tickmin=-infinity, real tickmax=infinity, real time(real)=null, pair dir(real)=zero); Глава 8. Модуль graph 104 где a и b – значения черточек в точках point(g,0) и point(g,length(g)), соответственно; S – автомасштабирование; функция time(real v) возвращает время, соответствующее значению v; пара dir(real t) – абсолютное направление черточки как функции t (нуль означает рисование черточки перпендикулярно оси). Некоторое представление о новом инструменте дает размеченный черточками эллипс, изображенный в примере 8.10. Ïðèìåð 8.10. Ðàçìå÷åííûé ýëëèïñ. import graph; size(7.5cm,0); path g=ellipse((0,0),2,1); axis(Label("C",align=10W,black),g,blue, LeftTicks(endlabel=false,8,end=false), ticklocate(0,360,new real(real v) {path h=(0,0)--max(abs(max(g)), abs(min(g)))*dir(v); return intersect(g,h)[0];})); 135 C 90 45 180 0 225 270 315 8.2 Ïîñòðîåíèå ãðàôèêîâ 8.2.1 Ñïëàéíû При построении кривых, в частности, графиков функций, Asymptote широко применяет сплайны. Для стандартной кубической интерполяции Эрмита используются такие граничные условия как notaknot, natural, periodic, clamped(real slopea, real slopeb) или monotonic. Условие notaknot означает применение правила not-a-knot, смысл которого состоит в том, чтобы не изменять кубические многочлены, когда они проходят через предконцевые узлы x1 и xn−1 (узлы образуют последовательность x0 , x1 , . . . , xn ). Математически это выражается формулой 000 000 p000 p000 1 (x1 ) = p2 (x1 ), n−1 (xn−1 ) = pn (xn−1 ), где pi – кубический многочлен сплайна для отрезка [xi−1 , xi ], i = 1, n. Применение этого правила может улучшить гладкость сплайна на концах отрезка интерполяции. Условие natural приводит к построению натурального кубического сплайна, который характеризуется выполнением ограничений p001 (x0 ) = 0, p00n (xn ) = 0. Периодическая функция аппроксимируется периодическим сплайном, построение которого инициирует параметр periodic. В этом случае первый и последний узлы интерполяции отождествляются: S(x0 ) = S(xn ), S 0 (x0 ) = S 0 (xn ), S 00 (x0 ) = S 00 (xn ), где S(x) – сплайн на отрезке [x0 , xn ]. Условие clamped(real slopea, real slopeb) задает «наклоны» на концах отрезка интерполяции (slopea и slopeb): p01 (x0 ) = f 0 (x0 ), p0n (xn ) = f 0 (x0 ), Глава 8. Модуль graph 105 где f (x) – интерполируемая функция. Условие monotonic гарантирует построение монотонного кубического сплайна. Условие Hermite равносильно Hermite(notaknot) для непериодических данных и эквивалентно Hermite(periodic) для периодических данных. 8.2.2 Äåêàðòîâà ñèñòåìà êîîðäèíàò Ïðîñòûå ãðàôèêè Графики функций формирует процедура graph вида guide graph(picture pic=currentpicture, real f(real), real a, real b, int n=ngraph, real T(real)=identity, interpolate join=operator --); guide[] graph(picture pic=currentpicture, real f(real), real a, real b, int n=ngraph, real T(real)=identity, bool3 cond(real), interpolate join=operator --); Результатом ее применения является график функции f, который с учетом масштабирования строится, вообще говоря, на отрезке [T(a),T(b)]. Функция T задает масштаб, например, логарифмический; по-другому это называется переходом к логарифмической шкале. Число n определяет количество точек, равномерно распределенных на отрезке [a,b] и используемых для построения графика; увеличение их количества может улучшить его качество. Необязательный параметр cond подчиняет построение графика некоторым условиям. Если cond равен • true, то точка добавляется к существующему пути guide; • default, то точка добавляется к новому пути guide; • false, то точка пропускается и начинается новый путь guide. Точки соединяются посредством интерполяционных кривых: • operator -- означает линейную интерполяцию, которая может быть вызвана и словом Straight; • operator .. задает интерполяцию кубическими сплайнами Безье; допустимо задание и словом Spline; • Hermite влечет стандартную интерполяцию кубическими сплайнами с применением граничных условий notaknot, natural, periodic, clamped(real slopea, real slopeb) или monotonic. Стандартный график функции формируется и выглядит, как в примере 8.11. Более «оснащенные» оси координат (снабженные метками и соответствующими им значениями) можно увидеть на рис. примера 10.6. Çàêðàñêà îáëàñòåé ìåæäó ãðàôèêàìè Представляя графики функций в виде путей, нетрудно получить закрашиваемые области под, над или между графиками. Так, в примере 8.12 части графиков функций представлены путями graph(f1,-0.5,0.5) и graph(f2,0.5,-0.5), которые затем преобразованы в циклический путь graph(f1,-0.5,0.5)-- graph(f2,0.5,-0.5)--cycle, внутренняя область которого далее закрашивается в светло-зеленый цвет. Глава 8. Модуль graph 106 2 Ïðèìåð 8.11. Ãðàôèê ôóíêöèè y = e−x cos 5x. import graph; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); real f(real x){ return exp(-x^2)*cos(5*x);} pair F(real x){ return (x,f(x));} draw(graph(f,-1.8,1.8,operator ..), blue); label("$O$",(0,0),SW,mahogany); axes(xlabel=Label("$x$",align=2E, mahogany),ylabel=Label("$y$",align=2N, mahogany),min=(-2,-0.8),max=(2,1.3), p=orange,arrow=Arrow(2mm,Fill(mahogany))); y x O Ïðèìåð 8.12. Çàêðàøèâàíèå îáëàñòè ìåæäó ãðàôèêàìè ôóíêöèé. import graph; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); pair f1(real x){return (x,1.2+0.5x^2*cos(5*x));} pair f2(real x){return (x,0.4+0.5*x^2*sin(5*x));} fill(graph(f1,-0.5,0.5)-graph(f2,0.5,-0.5)--cycle,palegreen); draw(graph(f1,-1,0.8),blue); draw(graph(f2,-1,0.8),blue); axes(xlabel=Label("$x$",align=2E,mahogany), ylabel=Label("$y$",align=2N,mahogany), min=(-1.1,0),max=(1,1.5),p=orange, arrow=Arrow(2mm,Fill(mahogany)), above=true); label("$O$",(0,0),S,mahogany); y O x Ïîñòðîåíèå ãðàôèêîâ ïî òî÷êàì Имеется возможность строить графики функций по массивам значений аргумента и функции. Для этого имеются специальные операторы. guide graph(picture pic=currentpicture, pair[] z, interpolate join=operator --); guide[] graph(picture pic=currentpicture, pair[] z, bool3[] cond, interpolate join=operator --); Для картинки pic строится график функции с учетом масштабирования, используя данные массива z; при этом выполняются ограничения на индексы массива, задаваемые условием cond, и используется тип интерполяции join. Вместо массива z можно задавать отдельные массивы x и y: guide graph(picture pic=currentpicture, real[] x, real[] y, interpolate join=operator --); Глава 8. Модуль graph 107 guide[] graph(picture pic=currentpicture, real[] x, real[] y, bool3[] cond, interpolate join=operator --); В примере 8.13 график функции синего цвет строится на основе массива чисел x и массива y, данные для которого рассчитываются по формуле y = x2 . Для красного графика оба массива xx и yy определяются числовыми данными. В первом случае использовалась линейная интерполяция, во втором – сглаживание .. . Для оси ординат был выбран уменьшенный размер шрифта. Ïðèìåð 8.13. Ïîñòðîåíèå ãðàôèêîâ ïî òî÷êàì. import graph; size(7.5cm,5cm,IgnoreAspect); real[] x={0,1,2,3}; real[] y=x^2; real[] xx={0,1,2,3}; real[] yy={9,8,5,0}; draw(graph(x,y),blue); draw(graph(xx,yy,operator..),red); xaxis("$x$",BottomTop,LeftTicks); yaxis("$y$",LeftRight, RightTicks(Label(fontsize(8pt)),new real[]{0,4,9})); 9 y 4 0 0 1 x 2 3 Ñïèñîê îáîçíà÷åíèé К картинке pic может быть добавлен окруженный рамкой список обозначений legend, который формирует процедура frame legend(picture pic=currentpicture, int perline=1, real xmargin=legendmargin, real ymargin=xmargin, real linelength=legendlinelength, real hskip=legendhskip, real vskip=legendvskip, real maxwidth=0, real maxheight=0, bool hstretch=false, bool vstretch=false, pen p=currentpen); Опции xmargin и ymargin определяют поля для x и y, соответственно. Параметр perline задает число элементов списка в строке (по определению равно 1; для 0 выбор этого числа производится автоматически). Длину черточек определяет параметр linelength. Опции hskip и vskip назначают пропуск строк (для многострочных элементов списка). Параметры maxwidth и maxheight определяют верхние границы для ширины и высоты списка (неограниченность фиксируется значением 0). Параметры hstretch и vstretch разрешают списку простираться по горизонтали или по вертикали. Рамка списка рисуется пером p. Список добавляется к картинке операторами add или attach. Рис. примера 8.14 изображает графики нескольких синусоид и снабжен списком их обозначений. Занесение элемента в список в соответствии со своим определением выполняет оператор draw: при каждом исполнении оператора в список в цикле заносится строка str с необходимой информацией. Присоединяет список к рисунку оператор attach. В следующем примере 8.15 список обозначений помещен внизу рисунка и имеет вид таблицы из двух строк и двух столбцов. Глава 8. Модуль graph 108 Ïðèìåð 8.14. Ãðàôèê ôóíêöèè ñî ñïèñêîì îáîçíà÷åíèé. import graph; size(9cm,0); pen[] pens={green,blue,red, cyan,orange,Magenta,purple}; typedef real realfcn(real); realfcn F(real m){ return new real(real x) {return (1+m)*sin(x);};}; 2.5 for (int i=0; i<=6; ++i) {real m=0.5*i; string str="$" + string(m+1) + "\sin x$"; y 0 i==0 ? str="$\sin x$" : str; draw(graph(F(m),-2pi,2pi), −2.5 pens[i],str);} xlimits(-2pi,2pi); −6 −4 ylimits(-4,4); xaxis("$x$",BottomTop, Ticks(Size=3bp,size=2bp)); yaxis("$y$",LeftRight,Ticks(Size=3bp,size=2bp)); draw((-2pi,0)--(2pi,0),black+dotted+bp); draw((0,-4)--(0,4),black+dotted+bp); attach(legend(linelength=0.3cm,p=blue), point(E),20E,UnFill); sin x 1.5 sin x 2 sin x 2.5 sin x 3 sin x 3.5 sin x 4 sin x −2 0 x 2 4 6 Ïðèìåð 8.15. Ìíîãîñòðî÷íûé ñïèñîê îáîçíà÷åíèé. import graph; size(7.5cm,5cm,IgnoreAspect); real exp1(real x){return exp(-x^2);} real exp2(real x){return -exp(-x^2);} real expcos(real x){return exp(-x^2)*cos(5*x);} real expsin(real x){return exp(-x^2)*sin(5*x);} draw(graph(exp1,-2,2),green,"$e^{-x^2}$"); draw(graph(exp2,-2,2),Cyan,"$-e^{-x^2}$"); draw(graph(expsin,-2,2),red, "$e^{-x^2}\sin 5x$"); draw(graph(expcos,-2,2),blue, "$e^{-x^2}\cos 5x$"); xlimits(-2,2); ylimits(-1,1); xaxis("$x$",BottomTop,Ticks(Size=3bp, size=2bp)); yaxis("$y$",LeftRight,Ticks(Size=3bp, size=2bp)); attach(legend(2,linelength=0.3cm), (point(S).x,truepoint(S).y),10S,UnFill); 1 0.5 y 0 −0.5 −1 −2 −1 2 e−x 2 e−x sin 5x 0 x 1 −e−x 2 2 2 e−x cos 5x Ìàðêåðû Графики функций могут быть помечены маркерами. Маркер создается функцией Глава 8. Модуль graph 109 marker marker(path g, markroutine markroutine=marknodes, pen p=currentpen, filltype filltype=NoFill, bool above=true); Любой фрейм может быть преобразован в маркер с помощью процедуры marker marker(frame f, markroutine markroutine=marknodes, bool above=true); Процедура marknodes обеспечивает маркирование узлов Безье на кривой. Можно использовать и процедуру markuniform(pair z(real t), real a, real b, int n), чтобы разместить маркеры в точках z(t), отвечающих n значениям t, равномерно расположенным на отрезке [a,b]. Вот предопределенные маркеры: marker[] Mark={ marker(scale(circlescale)*unitcircle), marker(polygon(3)), marker(polygon(4)), marker(polygon(5)), marker(invert*polygon(3)), marker(cross(4)), marker(cross(6))}; marker[] MarkFill={ marker(scale(circlescale)*unitcircle,Fill), marker(polygon(3),Fill), marker(polygon(4),Fill), marker(polygon(5),Fill), marker(invert*polygon(3),Fill)}; Для обозначения границ погрешности могут быть использованы специальные процедуры: void errorbars(picture pic=currentpicture, pair[] z, pair[] dp, pair[] dm={}, bool[] cond={}, pen p=currentpen, real size=0); void errorbars(picture pic=currentpicture, real[] x, real[] y, real[] dpx, real[] dpy, real[] dmx={}, real[] dmy={}, bool[] cond={}, pen p=currentpen, real size=0); Положительные и отрицательные границы погрешности указываются элементами массива dp и необязательного массива dm. Если массив dm не определен, положительные и отрицательные границы погрешности предполагаются одинаковыми. Пример 8.16 демонстрирует как создание маркера, так и его применение. На графике показаны также границы погрешностей в узлах графика. В следующем примере 8.17 конструируется пользовательский фрейм маркера из правильного шестиугольника (процедура polygon(int n), см. выше) и циклической звездочки (процедура cross(int n, bool round=true, real r=0), где n – число сторон многоугольника, r – необязательный «внутренний» радиус). Копии этого фрейма добавляет к рисунку процедура markuniform(bool centered=false, int n, bool rotated=false), располагая их в пяти точках, равномерно распределенных вдоль графика. При этом по умолчанию осуществляется поворот на угол, соответствующий углу наклона касательной в данной точке графика Глава 8. Модуль graph 110 Ïðèìåð 8.16. Ìàðêèðîâêà ãðàôèêà ñ óêàçàíèåì ïîãðåøíîñòè. import graph; 100 size(7.5cm,5cm,IgnoreAspect); pair[] f={(5,5),(50,20),(90,90)}; pair[] df={(0,0),(5,7),(0,5)}; errorbars(f,df,red); draw(graph(f),"legend", marker(scale(0.8mm)*unitcircle,red, FillDraw(blue),above=false)); xaxis("$x$",BottomTop,LeftTicks); yaxis("$y$",LeftRight,RightTicks); attach(legend(linelength=0.5cm),point(W), 20ENE,UnFill); 80 y legend 60 40 20 0 0 20 40 x 60 80 100 Ïðèìåð 8.17. Ìàðêèðîâêà ãðàôèêà. import graph; size(7.5cm,5cm,IgnoreAspect); pair[] f={(5,5),(50,20),(90,90)}; frame mark; filldraw(mark,scale(0.8mm)*polygon(6), green,green); draw(mark,scale(0.8mm)*cross(6),blue); draw(graph(f),marker(mark,markuniform(5))); xaxis("$x$",BottomTop,LeftTicks); yaxis("$y$",LeftRight,RightTicks); yequals(55.0,red+Dotted); xequals(70.0,red+Dotted); 80 60 y 40 20 10 20 30 40 50 60 70 80 90 x (если требуется центрирование, маркеры будут отцентрированы внутри n равномерно распределенных вдоль кривой интервалов). Маркеры можно создавать и программно, фактически для каждого узла графика конструируя собственный маркер. Эту возможность демонстрирует пример 8.18. В следующем примере 8.19 осуществляется стандартная маркировка точками, а горизонтальная ось помечается строками, являющимися названиями месяцев. Ìàñøòàáèðîâàíèå Масштабирование по координатным осям может быть выполнено вручную, как это показано в примере 8.20. Автоматическое масштабирование осей может быть осуществлено с помощью одной из следующих процедур: void scale(picture pic=currentpicture, scaleT x, scaleT y); void scale(picture pic=currentpicture, bool xautoscale=true, bool yautoscale=xautoscale, bool zautoscale=yautoscale); Эти процедуры задают масштабирование для картинки pic. Процедуры модуля graph применяют масштабирование к необязательноу аргументу picture; если он не задан, масштабирвание применяется к currentpicture. Глава 8. Модуль graph 111 Ïðèìåð 8.18. Ïðîãðàììèðóåìàÿ ìàðêèðîâêà. import graph; size(7.5cm,5cm,IgnoreAspect); markroutine marks() { return new void(picture pic=currentpicture,frame f,path g){ path p=scale(1mm)*unitcircle; for(int i=0; i <= length(g); ++i) { pair z=point(g,i); frame f; if(i % 4 == 0) { fill(f,p); add(pic,f,z); } else { if(z.y > 50) { pic.add(new void(frame F, transform t) { path q=shift(t*z)*p; unfill(F,q); draw(F,q); }); } else { draw(f,p); add(pic,f,z); }}}}; } pair[] f={(5,5),(40,20),(55,51),(90,30)}; draw(graph(f),marker(marks())); scale(true); xaxis("$x$",BottomTop,LeftTicks); yaxis("$y$",LeftRight,RightTicks); 60 50 40 y 30 20 10 0 0 20 40 x 60 80 100 Ïðèìåð 8.19. Ñòàíäàðòíàÿ ìàðêèðîâêà. import graph; size(7.5cm,5cm,IgnoreAspect); real[] x=sequence(12); real[] y=sin(2pi*x/12); scale(false); string[] month={"Jan","Feb","Mar", "Apr","May","Jun","Jul","Aug","Sep", "Oct","Nov","Dec"}; draw(graph(x,y),red,MarkFill[0]); xaxis(BottomTop,LeftTicks(new string(real x){ return month[round(x % 12)];})); yaxis("$y$",LeftRight,RightTicks(4)); 1 0.5 y 0 −0.5 −1 Jan Apr Jul Oct В модуле graph предопределены две часто используемые процедуры Linear и Log. Все координаты картинки, включая те, которые задают пути и положение меток, трактуются как линейные. Чтобы преобразовать координаты графика в масштабированные координаты картинки, следует использовать pair Scale(picture pic=currentpicture, pair z); Применяя аналогичные процедуры, можно отдельно масштабировать как x, так и y: real ScaleX(picture pic=currentpicture, real x); Глава 8. Модуль graph 112 Ïðèìåð 8.20. Ïðîñòîå ìàñøòàáèðîâàíèå. 10 8 y/105 import graph; size(7.5cm,5cm,IgnoreAspect); real[] x={-1e-11,1e-11}; real[] y={0,1e6}; real xscale=round(log10(max(x))); real yscale=round(log10(max(y)))-1; draw(graph(x*10^(-xscale),y*10^(-yscale)), red); xaxis("$x/10^{"+(string) xscale+"}$",BottomTop,LeftTicks); yaxis("$y/10^{"+(string) yscale+"}$",LeftRight, RightTicks(trailingzero)); 6 4 2 0 −1 −0.5 0 x/10−11 0.5 1 real ScaleY(picture pic=currentpicture, real y); Для предопределенных процедур масштабирования могут быть заданы два необязательных булевских арумента automin=false и automax=automin. Оба они установлены в false, но могут быть переустановлены в true, чтобы состоялся автоматический выбор «хороших» значений, минимального и максимального. Процедура масштабирования Linear может также иметь в качестве аргумента мультипликативный масштабирующий множитель, а Linear(-1) означает обращение оси. В примере 8.21 показано, как получить график функции, осуществив его масштабирование вида log/log. Ïðèìåð 8.21. Ëîãàðèôìè÷åñêîå ìàñøòàáèðîâàíèå. 101 import graph; (3,5) size(7.5cm,5cm,IgnoreAspect); real f(real t) {return 1/t;} scale(Log,Log); draw(graph(f,0.1,10)); dot(Label("(3,5)",align=S),Scale((3,5))); xaxis("$x$",BottomTop,LeftTicks); yaxis("$y$",LeftRight,RightTicks); y 100 10−1 −1 10 100 x 101 Используя черточки-отметки на осях, нетрудно получить для системы координат логарифмическую сетку, как это показано на рис. примера 8.22. Для логарифмически масштабированных осей можно задать требуемую локализацию и формат черточек-отметок, см. пример 8.23. Не составляет труда использовать логарифмическое масштабирование с другим основанием логарифмов. В примере 8.24 ось ординат масштабируется двоичными логарифмами. При масштабировании иногда возникает необходимость сделать для наглядности разрыв в изображении графика функции. В примере 8.25 из оси абсцисс, имеющей линейное масштабирование, исключается отрезок [3; 8], а из оси ординат с логарифмическим масштабированием исключается отрезок [100; 1000]. Для последней оси границы разрыва автоматически Глава 8. Модуль graph 113 Ïðèìåð 8.22. Ëîãàðèôìè÷åñêàÿ êîîðäèíàòíàÿ ñåòêà. import graph; 101 size(7.5cm,5cm,IgnoreAspect); real f(real t) {return 1/t;} y 100 scale(Log,Log); draw(graph(f,0.1,10),red); pen thin=linewidth(0.5*linewidth()); xaxis("$x$",BottomTop,LeftTicks(begin=false, end=false,extend=true,ptick=thin)); 10−1 −1 10 yaxis("$y$",LeftRight,RightTicks(begin=false, end=false,extend=true,ptick=thin)); 100 x 101 Ïðèìåð 8.23. Îòìåòêè ïðè ëîãàðèôìè÷åñêîì ìàñøòàáèðîâàíèè. 100 νupp [Hz] import graph; size(7.5cm,5cm,IgnoreAspect); scale(Log,Log); draw(graph(identity,5,20)); xlimits(5,20); ylimits(1,100); xaxis("$M/M_\odot$",BottomTop, LeftTicks(DefaultFormat,new real[] {6,10,12,14,16,18})); yaxis("$\nu_{\rm upp}$ [Hz]",LeftRight,RightTicks (DefaultFormat)); 10 1 6 10 12 14 16 18 M/M⊙ Ïðèìåð 8.24. Ëîãàðèôìè÷åñêîå ìàñøòàáèðîâàíèå îñè Oy ïî îñíîâàíèþ 2. import graph; size(7.5cm,5cm,IgnoreAspect); real log2(real x) {static real log2=log(2); return log(x)/log2;} real pow2(real x) {return 2^x;} scaleT yscale=scaleT(log2,pow2, logarithmic=true); scale(Linear,yscale); real f(real x) {return 1+x^2;} draw(graph(f,-4,4),red); yaxis("$y$",ymin=1,ymax=f(5),RightTicks (Label(Fill(white))),EndArrow); xaxis("$x$",xmin=-5,xmax=5,LeftTicks, EndArrow); y 24 23 22 21 −5 20 0 5 x округляются до ближайшей целой степени основания логарифма. Имеется возможность дублировать координатную ось осью с другим масштабированием. Для этого служат процедуры picture secondaryX(picture primary=currentpicture, void f(picture)); picture secondaryY(picture primary=currentpicture, void f(picture)); Глава 8. Модуль graph 114 Ïðèìåð 8.25. Ëîãàðèôìè÷åñêîå ìàñøòàáèðîâàíèå ñ ðàçðûâîì ãðàôèêà. ≈ 104 ≈ y ≈ 101 100 ≈ import graph; size(7.5cm,5cm,IgnoreAspect); real a=3, b=8; real c=100, d=1000; scale(Broken(a,b),BrokenLog(c,d)); real[] x={1,2,4,6,10}; real[] y=x^4; draw(graph(x,y),red,MarkFill[0]); xaxis("$x$",BottomTop, LeftTicks(Break(a,b))); yaxis("$y$",LeftRight, RightTicks(Break(c,d))); label(rotate(90)*Break,(a,point(S).y)); label(rotate(90)*Break,(a,point(N).y)); label(Break,(point(W).x,ScaleY(c))); label(Break,(point(E).x,ScaleY(c))); 2 10 x В примере 8.26 вторая ось ординат secondaryY используется с линейным масштабом в противоположность исходной оси с логарифмическим масштабированием. 8.2.3 102 1.5 101 1.0 100 0.5 10−1 (1,0) 10−2 10−3 −2 10 0.0 −0.5 ωτ0 100 −1.0 Ïàðàìåòðè÷åñêîå çàäàíèå ôóíêöèè Чтобы сформировать график однопараметрической функции, применяют операторы guide graph(picture pic=currentpicture, real x(real), real y(real), real a, real b, int n=ngraph, real T(real)=identity, Arg G/π import graph; size(7.5cm,7cm,IgnoreAspect); texpreamble("\def\Arg{\mathop {\rm Arg}\nolimits}"); real ampl(real x) {return 2.5/(1+x^2);} real phas(real x) {return -atan(x)/pi;} scale(Log,Log); draw(graph(ampl,0.01,10)); ylimits(0.001,100); xaxis("$\omega\tau_0$",BottomTop,LeftTicks); yaxis("$|G(\omega\tau_0)|$",Left,RightTicks); picture q=secondaryY(new void(picture pic) { scale(pic,Log,Linear); draw(pic,graph(pic,phas,0.01,10),red); ylimits(pic,-1.0,1.5); yaxis(pic,"$\Arg G/\pi$",Right,red, LeftTicks("$% #.1f$", begin=false,end=false)); yequals(pic,1,Dotted);}); label(q,"(1,0)",Scale(q,(1,0)),red); add(q); |G(ωτ0)| Ïðèìåð 8.26. Äâå îñè îðäèíàò ñ ðàçëè÷íûì ìàñøòàáèðîâàíèåì. Глава 8. Модуль graph 115 interpolate join=operator --); guide[] graph(picture pic=currentpicture, real x(real), real y(real), real a, real b, int n=ngraph, real T(real)=identity, bool3 cond(real), interpolate join=operator --); Процедура возвращает путь в виде графика параметрически заданной функции (x(t),y(t)) для t из отрезка [T(a),T(b)], который подвергается дискретизации с помощью n точек, равномерно распределенных на отрезке [a,b]. Используется масштабирование, определенное для картинки pic. Необязательная функция cond типа bool3 позволяет вводить ограничения, а join задает вид интерполяции. Той же цели служат и следующие две процедуры, в которых вместо отдельных функций x(t) и y(t) используется функция-пара z(t): guide graph(picture pic=currentpicture, pair z(real), real a, real b, int n=ngraph, real T(real)=identity, interpolate join=operator --); guide[] graph(picture pic=currentpicture, pair z(real), real a, real b, int n=ngraph, real T(real)=identity, bool3 cond(real), interpolate join=operator --); Пример 8.27 демонстрирует практическую реализацию возможностей Asymptote при изображении параметрически заданных функций. Фактически разрисовывается один и тот же лепесток «цветка» (но разными красками), который затем с помощью оператора поворота приводится в требуемое положение на рис. Следует обратить внимание на довольно большое значение параметра n=3500 в процедуре draw, так как именно этим достигается ажурность рис. Ïðèìåð 8.27. Ïàðàìåòðè÷åñêè çàäàííàÿ ôóíêöèÿ ¾Öâåòèê-ñåìèöâåòèê¿. import graph; size(7.5cm,0); real r1=100, r2=25, a1=1, a2=500; real x(real t){ return r1*(1+cos(7*t))*cos(a1*t)+r2*cos(a2*t);} real y(real t){ return r1*(1+cos(7*t))*sin(a1*t)+r2*sin(a2*t);} fill(box((-225,-235),(240,235)),black); real tt=1*0.449, ang=51.429; void mydraw(int n,pen col){ draw(rotate(n*ang)*graph(x,y,-tt,tt,n=3500, join=operator..),col);} mydraw(0,orange+white); mydraw(1,magenta); mydraw(2,lightblue); mydraw(3,cyan); mydraw(4,red); mydraw(5,yellow); mydraw(6,green); 8.2.4 Ïîëÿðíàÿ ñèñòåìà êîîðäèíàò Графики функций в полярной системе координат формирует оператор guide polargraph(picture pic=currentpicture, real f(real), real a, real b, int n=ngraph, interpolate join=operator --); Глава 8. Модуль graph 116 Процедура возвращает график функции f, заданной на отрезке [a,b], в полярной системе координат. Указанный отрезок дискретизуется с помощью n точек, равномерно распределенных на отрезке. Используется масштабирование, определенное для картинки pic. В примере 8.28 показано применение этой процедуры к построению графика функции с заполнением и отрисовкой контура. Значение параметра n=7000 здесь в два раза больше, чем в примере 8.27, но и его маловато: контур листа изрезан недостаточно. Ïðèìåð 8.28. Ãðàôèê ôóíêöèè â ïîëÿðíîé ñèñòåìå êîîðäèíàò. import graph; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); real rho(real t){ return (1+sin(9*t))*(1+sin(t))* (1+0.33*sin(9*5*t))* (1+0.04*sin(9*33*t)); } guide g=polargraph(rho,0,2pi,n=7000, join=operator..); filldraw(g--cycle,deepgreen,orange); Еще одна процедура конструирует графики в полярной системе координат, извлекая информацию о функции из массивов (r,theta). guide polargraph(picture pic=currentpicture, real[] r, real[] theta, interpolate join=operator--); 8.2.5 Èçîáðàæåíèå âåêòîðíûõ ïîëåé Чтобы нарисовать векторное поле в виде стрелок, равномерно распределенных вдоль пути, используется процедура picture vectorfield(path vector(real), path g, int n, bool truesize=false, pen p=currentpen, arrowbar arrow=Arrow); Такое векторное поле показано на рис. примера 8.29. Следует обратить внимание на то, что vectorfield представляет собой картинку picture, поэтому добавляется к текущему рисунку с помощью процедуры add. Чтобы нарисовать векторное поле с nx×ny стрелками в прямоугольнике box(a,b), следует использовать процедуру picture vectorfield(path vector(pair), pair a, pair b, int nx=nmesh, int ny=nx, bool truesize=false, real maxlength=truesize ? 0 : maxlength(a,b,nx,ny), bool cond(pair z)=null, pen p=currentpen, arrowbar arrow=Arrow, margin margin=PenMargin); Глава 8. Модуль graph 117 Ïðèìåð 8.29. Âåêòîðíîå ïîëå âäîëü êðèâîé. import graph; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); real f(real x) {return sin(x);} path g=graph(f,0,2pi); draw(g,blue); typedef path vector(real); y real arrowlength=3.5mm; vector vector(pair a, pair b) { return new path(real x) { return O (0,0)--arrowlength*interp(a,b,x);};} add(vectorfield(vector(N,N),g,30,true,red, arrow=Arrow(1mm))); axes(xlabel=Label("$x$",align=2E,mahogany), ylabel=Label("$y$",align=2N,mahogany),min=(0,-1.2), max=(2pi+0.5,1.3),p=orange,arrow=Arrow(2mm,Fill(mahogany))); label("$O$",(0,0),W,mahogany); x Пример 8.30 демонстрирует реализацию такой возможности. Можно отметить рекордную лаконичность программы, ядро которой состоит из одной строчки кода. Ïðèìåð 8.30. Âåêòîðíîå ïîëå â ïðÿìîóãîëüíèêå. import graph; size(6cm,0); pair a=(0,0); pair b=(2pi,2pi); path vector(pair z) {return (0,0)--(sin(z.x),cos(z.y));} add(vectorfield(vector,a,b,blue)); Ãëàâà 9 Ìîäóëü palette Средства модуля служат для раскрашивания геометрических объектов с помощью различных палитр, в том числе предопределенных. Первоначальные сведения о палитрах были приведены в п. 6.2.1. Функция cmyk(pen[] Palette) используется для преобразования предопределенных и определенных пользователем палитр в цветовое пространство CMYK. График типа density plot, использующий палитру palette, можно сформировать с помощью функции f(x, y) и добавить к картинке pic: bounds image(picture pic=currentpicture, real f(real, real) range range=Full, pair initial, pair final, int nx=ngraph, int ny=nx, pen[] palette, bool antialias=false); Функция f дискретизуется в прямоугольнике, определяемом точками initial и final, с помощью nx×ny точек, равномерно распределенных в прямоугольнике. При этом учитывается текущее масштабирование pic. Цветовое пространство шкалируется в соответствии со шкалированием оси Oz. Результатом работы процедуры является следующая структура: struct bounds { real min; real max; // Âîçìîæíûå èíòåðâàëû îòìåòîê ticks int[] divisor; } Эта информация может быть использована для создания необязательной шкалы палитры в виде градации ее цветов. Цветовое пространство палитры соответствует диапазону значений, заданному аргументом range, который может быть Full, Automatic или определяться функцией Range(real min, real max). Значение Full означает диапазон от минимального до максимального значений функции над интервалами разбиения, в то время как Automatic заботится об «эстетике» границ. Цветной график density plot может быть также получен с использованием массива данных: bounds image(picture pic=currentpicture, real[][] f, range range=Full, pair initial, pair final, pen[] palette, bool transpose=(initial.x < final.x && initial.y < final.y), bool copy=true, bool antialias=false); Если точка initial лежит левее и ниже final, то по умолчанию индексы массива интерпретируются как в декартовой системе координат (первый индекс – x, второй – y) в отличие от матричного порядка (первый индекс – y, второй – x). 118 Глава 9. Модуль palette 119 Чтобы построить график по массиву нерегулярно распределенных точек и массиву значений функции f в них, следует обратиться к одной из следующих процедур: bounds image(picture pic=currentpicture, pair[] z, real[] f, range range=Full, pen[] palette); bounds image(picture pic=currentpicture, real[] x, real[] y, real[] f, range range=Full, pen[] palette); Необязательная шкала палитры создается процедурой void palette(picture pic=currentpicture, Label L="", bounds bounds, pair initial, pair final, axis axis=Right, pen[] palette, pen p=currentpen, paletteticks ticks=PaletteTicks, bool copy=true, bool antialias=false); Цветовое пространство palette берется из bounds и шкалируется в соответствии со шкалированием оси Oz. Ориентация шкалы определяется параметром axis, который может принимать значения Right, Left, Top или Bottom. Шкала изображается прямоугольником с углами initial и final. Аргумент paletteticks представляет собой специальный тип отметок следующего вида: paletteticks PaletteTicks(Label format="", ticklabel ticklabel=null, bool beginlabel=true, bool endlabel=true, int N=0, int n=0, real Step=0, real step=0, pen pTick=nullpen, pen ptick=nullpen); И график, и шкала могут быть вставлены во фрейм и добавлены к рисунку в требуемое место. При необходимости может быть выполнено выравнивание. Пример 9.1 показывает, как конструируется график вместе со шкалой. Ïðèìåð 9.1. Ãðàôèê âèäà density plot ñ âåðòèêàëüíîé øêàëîé öâåòíîñòè. import graph; import palette; size(7.5cm,0); int n=256; real ninv=2pi/n; real[][] v=new real[n][n]; for(int i=0; i < n; ++i) for(int j=0; j < n; ++j) v[i][j]=sin(i*ninv)*cos(j*ninv); pen[] Palette=BWRainbow(); picture bar; bounds range= image(v,(0,0),(1,1),Palette); palette(bar,"$A$",range,(0,0), (0.4cm,4.3cm),Right,Palette, PaletteTicks("$%+#.1f$")); add(bar.fit(),point(E),30E); +1.0 +0.5 0.0 A −0.5 −1.0 В следующем примере 9.2 демонстрируется логарифмическая шкала цветности. Можно также создать рисунок, используя двумерный массив pen или функцию f типа pen: void image(picture pic=currentpicture, pen[][] data, pair initial, pair final, bool transpose=(initial.x < final.x && initial.y < final.y), bool copy=true, bool antialias=false); Глава 9. Модуль palette 120 Ïðèìåð 9.2. Ãðàôèê âèäà density plot ñ ãîðèçîíòàëüíîé ëîãàðèôìè÷åñêîé øêàëîé öâåòíîñòè. import graph; import palette; size(9cm,9cm,IgnoreAspect); real f(real x, real y) { return 0.9*pow10(2*sin(x/5+2*y^0.25)) + 0.1*(1+cos(10*log(y)));} scale(Linear,Log,Log); pen[] Palette=BWRainbow(); bounds range=image(f,Automatic,(0,1), (100,100),nx=200,Palette); xaxis("$x$",BottomTop,LeftTicks, above=true); yaxis("$y$",LeftRight,RightTicks, above=true); palette("$f(x,y)$",range,(0,200), (100,250),Top,Palette, PaletteTicks(ptick=linewidth( 0.5*linewidth()))); 10 −3 10 −2 f (x, y) 10 100 101 102 40 80 100 −1 102 y 101 100 0 20 x 60 void image(picture pic=currentpicture, pen f(int, int), int width, int height, pair initial, pair final, bool transpose= (initial.x < final.x && initial.y < final.y), bool antialias=false); Эти возможности представлены в примерах 9.3 и 9.4. Ïðèìåð 9.3. Èñïîëüçîâàíèå äâóìåðíîãî ìàññèâà òèïà pen. import palette; size(7cm,7cm,IgnoreAspect); int n=256; real ninv=2pi/n; pen[][] v=new pen[n][n]; for(int i=0; i < n; ++i) for(int j=0; j < n; ++j) v[i][j]=rgb(0.5*(1+sin(i*ninv)), 0.5*(1+cos(j*ninv)),0); image(v,(0,0),(1,1)); Для удобства в модуле palette определены операторы, которые могут быть использованы для создания массива перьев при заданных функции f и палитре palette: Глава 9. Модуль palette Ïðèìåð 9.4. Èñïîëüçîâàíèå ôóíêöèè òèïà pen. import palette; size(7.5cm,7.5cm,IgnoreAspect); real fracpart(real x){ return (x-floor(x));} pair pws(pair z){ pair w=(z+exp(pi*I/5)/0.9)/ (1+z/0.9*exp(-pi*I/5)); return exp(w)*(w^3-0.5*I);} int N=512; pair a=(-1,-1); pair b=(0.5,0.5); real dx=(b-a).x/N; real dy=(b-a).y/N; pen f(int u, int v){ pair z=a+(u*dx,v*dy); pair w=pws(z); real phase=degrees(w,warn=false); real modulus=w == 0 ? 0: fracpart(log(abs(w))); return hsv(phase,1,sqrt(modulus));} image(f,N,N,(0,0),(300,300),antialias=true); pen[] palette(real[] f, pen[] palette); pen[][] palette(real[][] f, pen[] palette); 121 Ãëàâà 10 Ìîäóëü contour Пакет предназначен для изображения линий уровня функций. Как известно, линией уровня функции z = f (x, y) называется кривая, определяемая уравнением f (x, y) = C, где C– произвольная константа, уровень функции. В Asymptote линии уровня функции f, заданной в прямоугольнике box(a,b), причем уровни хранятся в массивe ñ, строит процедура guide[][] contour(real f(real,real), pair a, pair b, real[] c, int nx=ngraph, int ny=nx, interpolate join=operator --, int subsample=1); Параметры nx и ny определяют разрешение. Разрешение по умолчанию – ngraph × ngraph (ngraph по умолчанию равно 100). Разрешение может быть увеличено для достижения более высокой точности. Оператором интерполяции по умолчанию является оператор operator -(прямая). Сплайн-интерполяция (operator ..) может обеспечить более плавные линии, но может и привести к переполнению. Параметр subsample указывает на число внутренних точек, которые должны быть использованы для построения экземпляров линий уровня внутри каждого квадрата 1 х 1; значения 1, принятого по умолчанию, обычно бывает достаточно. Сами линии уровня рисуют процедуры void draw(picture pic=currentpicture, Label[] L=new Label[], guide[][] g, pen p=currentpen); void draw(picture pic=currentpicture, Label[] L=new Label[], guide[][] g, pen[] p); В первом случае все линии уровня изображаются одним цветом, как в примере 10.1, во втором – разными, как в примере 10.2. Чтобы построить линии уровня для массива данных, заданных на равномерной двумерной решетке в прямоугольнике box(a,b), применяют оператор guide[][] contour(real[][] f, pair a, pair b, real[] c, interpolate join=operator --, int subsample=1); Построение линий уровня для массива данных, заданных на регулярных неперекрывающихся ячейках, определяемых двумерным массивом z, выполняет оператор guide[][] contour(pair[][] z, real[][] f, real[] c, interpolate join=operator --, int subsample=1); 122 Глава 10. Модуль contour 123 Ïðèìåð 10.1. Èçîáðàæåíèå ëèíèé óðîâíÿ îäíèì öâåòîì. import contour; size(7.5cm,0); real f(real x, real y) {return 0.5*x^2+y^2;} int n=5; real[] c=new real[n]; c[0]=0.1; c[1]=0.2; c[2]= 0.3; c[3]=0.4; c[4]=0.5; draw(contour(f,(-1,-1),(1,1),c),blue); Ïðèìåð 10.2. Èçîáðàæåíèå ëèíèé óðîâíÿ ðàçíûìè öâåòàìè. import contour; size(7.5cm,0); real f(real x, real y) {return 0.5*x^2+y^2;} int n=5; real[] c=new real[n]; pen[] p=new pen[n]; c[0]=0.1; c[1]=0.2; c[2]= 0.3; c[3]=0.4; c[4]=0.5; p[0]=red; p[1]=blue; p[2]=green; p[3]=orange; p[4]=Cyan; draw(contour(f,(-1,-1),(1,1),c),p); Чтобы построить линии уровня для массива значений функции f, определенных нерегулярно расположенными точками z, используют процедуру guide[][] contour(pair[] z, real[] f, real[] c, interpolate join=operator --); В следующем примере 10.3 линии уровня для функции z = x2 − y 2 не только изображаются, но и маркируются. Разрешение выбрано равным 100×100. Используется штриховая линия для отрицательных значений и сплошная – для положительных (и нуля). В примере 10.4 показано, как можно сочетать линии уровня с графиком типа density plot. Пример 10.5 демонстрирует конструирование линий уровня из нерегулярным образом заданных данных. Модуль contour может быть использован и для построения графика разрывной функции. Пусть, например, требуется изобразить график функции y= −1 x2 − 4x + 3 , Глава 10. Модуль contour 124 Ïðèìåð 10.3. Ìàðêèðîâêà ëèíèé óðîâíÿ. import contour; size(7.5cm,0); real f(real x, real y) {return x^2-y^2;} int n=10; real[] c=new real[n]; for(int i=0; i < n; ++i) c[i]=(i-n/2)/n; pen[] p=sequence(new pen(int i){ return (c[i] >= 0 ? solid : dashed)+fontsize(8pt); },c.length); Label[] Labels=sequence(new Label(int i) {return Label(c[i] != 0 ? (string) c[i] : "", Relative(unitrand()),(0,0), UnFill(1bp)); },c.length); draw(Labels,contour(f,(-1,-1),(1,1),c),p); -0.5 -0.1 -0.3 -0.2 -0.4 0.2 0.3 0.1 0.4 0.4 0.1 0.3 0.2 -0.2 -0.3 -0.1 -0.4 -0.5 Ïðèìåð 10.4. Êîìáèíàöèÿ ãðàôèêîâ density plot è ëèíèé óðîâíÿ. import graph; import palette; import contour; size(10cm,10cm,IgnoreAspect); pair a=(0,0); pair b=(2pi,2pi); real f(real x, real y){ f (x, y) return cos(x)*sin(y);} −1 −0.8 −0.6 −0.4 −0.2 0 0.2 0.4 0.6 0.8 1 int N=200; int Divs=10; int divs=2; defaultpen(1bp); pen Tickpen=black; pen tickpen=gray+0.5*linewidth(currentpen); 6 pen[] Palette=BWRainbow(); 5 bounds range=image(f,Automatic,a,b,N,Palette); 4 real[] Cvals=uniform(range.min,range.max,Divs); y 3 draw(contour(f,a,b,Cvals,N,operator--), Tickpen); 2 real[] cvals; for(int i=0; i < Cvals.length-1; ++i) 1 cvals.append(uniform(Cvals[i],Cvals[i+1], 0 divs)[1:divs]); 0 1 2 3 4 5 6 draw(contour(f,a,b,cvals,N,operator--), x tickpen); xaxis("$x$",BottomTop,LeftTicks,above=true); yaxis("$y$",LeftRight,RightTicks,above=true); palette("$f(x,y)$",range,point(NW)+(0,0.5),point(NE)+(0,1),Top, Palette,PaletteTicks(N=Divs,n=divs,Tickpen,tickpen)); имеющей две точки разрыва второго рода: x = 1 и x = 3. Если попытаться сделать это средствами, рассмотренными в главе «Модуль graph», на дисплей будет выведена весьма затейливая кривая, ничего общего не имеющая с требуемым графиком. В примере 10.6 этому казусу Глава 10. Модуль contour 125 Ïðèìåð 10.5. Íåðåãóëÿðíûå äàííûå. int n=180; real f(real a, real b) {return a^2+b^2;} srand(1); real r() {return 1.1*(rand()/randMax*2-1);} pair[] points=new pair[n]; real[] values=new real[n]; for(int i=0; i < n; ++i) { points[i]=(r(),r()); values[i]=f(points[i].x,points[i].y); } draw(contour(points,values,new real[]{0.25,0.5,1},operator ..),blue); соответствуют две «закомментированные» строчки программы. Выход из этой ситуации дает изображение графика в виде линии уровня z = −1 функции z = y(x2 − 4x + 3). Пример 10.6 демонстрирует такую возможность. Ïðèìåð 10.6. Ãðàôèê ðàçðûâíîé ôóíêöèè. import graph; import contour; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); transform ec=scale(0.6); xaxis(L=Label(ec*"$x$",align=2E,mahogany), Ticks(ec*Label(black),NoZero, Size=2.5,size=1.5,mahogany),p=orange, Arrow(2mm,Fill(mahogany))); yaxis(L=Label(ec*"$y$",align=2N,mahogany), Ticks(ec*Label(black),NoZero, Size=2.5,size=1.5,mahogany),p=orange, Arrow(2mm,Fill(mahogany))); −2 draw((1,-2.9)--(1,2.9),dashed+heavygreen); draw((3,-2.9)--(3,2.9),dashed+heavygreen); labelx(ec*"$O$",0,NW,mahogany); real F(real x, real y) {return y*(x^2-4*x+3);} draw(contour(F,(-2.8,-2.9),(6.8,2.9), new real[] {-1}),0.5bp+blue); //real f(real x) {return -1/(x^2-4*x+3);} //draw(graph(f,-2.8,6.8,operator..),0.5bp+blue); y 2 1 O −1 x 1 2 3 4 5 6 −1 −2 Аналогичным образом может быть построен график неявно заданной функции. Представим, например, уравнение улитки Паскаля в виде (x2 + y 2 − 2x)2 − (x2 + y 2 ) = 0. Тогда, чтобы изобразить эту кривую, достаточно построить линию уровня z = 0 для функции z = (x2 + y 2 − 2x)2 − (x2 + y 2 ), как это показано на рис. примера 10.7. Глава 10. Модуль contour 126 Ïðèìåð 10.7. Ãðàôèê íåÿâíîé ôóíêöèè: óëèòêà Ïàñêàëÿ. import graph; import contour; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); xaxis(L=Label("$x$",align=2E,mahogany),p=orange, Arrow(2mm,Fill(mahogany))); yaxis(L=Label("$y$",align=2N,mahogany),p=orange, Arrow(2mm,Fill(mahogany))); real F(real x, real y) {return (x^2+y^2-2*x)^2-(x^2+y^2);} draw(contour(F,(-0.2,-2),(3.1,2), new real[] {0},operator ..),0.5bp+blue) y x Ãëàâà 11 Ìîäóëè markers, labelpath è patterns 11.1 Ìîäóëü markers Этот модуль содержит специализированные процедуры для маркирования путей и углов. Основной процедурой пакета является процедура markroutine markinterval(int n=1, frame f, bool rotated=false); которая помещает n копий фрейма f в центрах равномерно распределенных вдоль пути интервалов, при необходимости поворачивая их на угол, определяемый локальным положением касательной. Может быть использована процедура marker (см. раздел 8.2) для создания новых маркеров из следующих предопределенных фреймов: frame stickframe(int n=1, real size=0, pair space=0, real angle=0, pair offset=0, pen p=currentpen); frame circlebarframe(int n=1, real barsize=0, real radius=0, real angle=0, pair offset=0, pen p=currentpen, filltype filltype=NoFill, bool above=false); frame crossframe(int n=3, real size=0, pair space=0, real angle=0, pair offset=0, pen p=currentpen); frame tildeframe(int n=1, real size=0, pair space=0, real angle=0, pair offset=0, pen p=currentpen); Для удобства этот модуль содержит также маркеры StickIntervalMarker, CrossIntervalMarker, CircleBarIntervalMarker, TildeIntervalMarker из упомянутых фреймов. Рассмотрим их применение. Первый маркер рисует на пути черточки и определяется так: marker StickIntervalMarker(int i=2, int n=1, real size=0, real space=0, real angle=0, pair offset=0, bool rotated=true, pen p=currentpen, frame uniform=newframe, bool above=true); Из приводимых ниже примеров можно уяснить смысл параметров процедуры. Их описаний автор не нашел даже в исходниках, а взять на себя ответственность за их толкование не захотел. Пример 11.1 показывает применение рассмотренного маркера. Следующий маркер 127 Глава 11. Модули markers, labelpath и patterns 128 Ïðèìåð 11.1. Ïðèìåíåíèå ìàðêåðà StickIntervalMarker. import markers; size(7.5cm,0); path p=(0,0)--(1,0)--(2,0)--(3,0); transform T=shift(0,-0.5); draw(p,StickIntervalMarker(3,2,blue, dotframe(red))); p=T*p; draw(p,StickIntervalMarker(4,1,size=5mm, angle=-45,bp+.8green,dotframe(purple))); p=T*p; pen pn=linewidth(4bp); draw(p,pn,StickIntervalMarker(3,3,angle=25,pn,dotframe(red+pn))); marker CrossIntervalMarker(int i=2, int n=3, real size=0, real space=0, real angle=0, pair offset=0, bool rotated=true, pen p=currentpen, frame uniform=newframe, bool above=true); рисует крестики-звездочки, поворачивая их, если требуется на угол angle. Пример 11.2 демонстрирует некоторые его возможности. Ïðèìåð 11.2. Ïðèìåíåíèå ìàðêåðà CrossIntervalMarker. import markers; size(7.5cm,0); path p=(0,0)--(1,0)--(2,0)--(3,0); transform T=shift(0,-0.5); draw(p,CrossIntervalMarker(2,4,angle=0, bp+red,dotframe(purple))); p=T*p; draw(p,CrossIntervalMarker(3,3,bp+heavygreen, dotframe)); p=T*p; draw(p,CrossIntervalMarker(3,6,size=2mm, angle=45,bp+blue,dotframe(red))); Маркер marker CircleBarIntervalMarker(int i=2, int n=1, real barsize=0, real radius=0, real angle=0, pair offset=0, bool rotated=true, pen p=currentpen, filltype filltype=NoFill, bool circleabove=false, frame uniform=newframe, bool above=true); помечает путь кружочками: пустыми, заполненными, перечеркнутыми (с регулировкой угла перечеркивания), см. пример 11.3. Глава 11. Модули markers, labelpath и patterns 129 Ïðèìåð 11.3. Ïðèìåíåíèå ìàðêåðà CircleIntervalMarker. import markers; size(7.5cm,0); path p=(0,0)--(1,0)--(2,0)--(3,0); transform T=shift(0,-0.5); draw(p,CircleBarIntervalMarker(3,2, barsize=5mm,radius=1.5mm,angle=-45, darkgreen,filltype=NoFill, dotframe(purple))); p=T*p; draw(p,CircleBarIntervalMarker(n=3, barsize=8mm,radius=2mm, FillDraw(.8red),dotframe)); p=T*p; draw(p,CircleBarIntervalMarker(n=3,angle=30, barsize=8mm,radius=2mm,FillDraw(.8red),circleabove=true,dotframe)); Наконец, маркер marker TildeIntervalMarker(int i=2, int n=1, real size=0, real space=0, real angle=0, pair offset=0, bool rotated=true, pen p=currentpen, frame uniform=newframe, bool above=true); ставит на пути отметки в виде волнистых линий, которые можно использовать как сами по себе, так и образовывать из них новые фигуры. Результаты таких усилий демонстрирует пример 11.4. Ïðèìåð 11.4. Ïðèìåíåíèå ìàðêåðà TildeIntervalMarker. import markers; size(7.5cm,0); path p=(0,0)--(1,0)--(2,0)--(3,0); transform T=shift(0,-0.5); draw(p,TildeIntervalMarker(i=3,blue, dotframe(green))); p=T*p; draw(p,TildeIntervalMarker(i=3,n=2,red, angle=-20,dotframe(blue))); p=T*shift(relpoint(p,.5)+.65S)*scale(.5)* shift(-relpoint(p,.5))*rotate(45, relpoint(p,.15))*p; draw(p,TildeIntervalMarker(size=5mm,green, rotated=false,dotframe(red))); Если стандартные маркеры невыразительны, можно обратиться к более гибким возможностям, предоставляемым системой Asymptote. Некоторые из них показаны на рис. примера 11.5. Модуль также имеет в своем составе процедуру, которая маркирует угол AOB: void markangle(picture pic=currentpicture, Label L="", int n=1, real radius=0, real space=0, pair A, pair O, pair B, arrowbar arrow=None, pen p=currentpen, margin margin=NoMargin, marker marker=nomarker); Глава 11. Модули markers, labelpath и patterns 130 Ïðèìåð 11.5. Äðóãèå ìàðêèðîâêè. import markers; size(7.5cm,0); path p=(0,0)--(1,0)--(2,0)--(3,0); transform T=shift(0,-0.5); draw(p,marker(markinterval(3,dotframe, true))); p=T*p; draw(p,marker(stickframe,markuniform(4))); p=T*p; draw(p,marker(stickframe(red), markinterval(3,dotframe(blue),true))); p=T*p; frame cg; filldraw(cg,scale(5)*polygon(5),Yellow, bp+Magenta); marker markcg = marker(crossframe(n=4,size=1.5mm,bp+purple), markinterval(2,cg,true)); draw(p,markcg) В примере 11.6 эта процедура используется для обозначения внутреннего и внешнего углов, а в примере 11.7 – для обозначения различными способами углов треугольника. Ïðèìåð 11.6. Âíóòðåííèé è âíåøíèé óãëû. import markers; size(6cm,0); pair O=(0,0), A=(2,0), B=(1,1.5); draw(A--O--B,bp+blue); markangle(scale(1.5)*"$\alpha$",radius=40,A,O,B, ArcArrow,bp+red); markangle(scale(1.5)*"$\beta$",radius=30,B,O,A, BeginArcArrow(HookHead,2mm),bp+deepgreen); label("$O$",O,SW); label("$A$",A,SE);label("$B$",B,N); B α β O A 11.2 Ìîäóëü labelpath Процедура модуля с таким же названием позволяет придать метке форму заданного пути и расположить ее вдоль этого пути: void labelpath(picture pic=currentpicture, Label L, path g, string justify=Centered, pen p=currentpen); Параметр justify принимает значения LeftJustified, Centered или RightJustified. Компонента x преобразования shift, примененная к Label, интерпретируется как сдвиг вдоль кривой, а компонента y – как сдвиг в сторону от кривой. Другие преобразования метки игнорируются. Первый рис. примера 11.8 располагает метку прямо на кривой, второй рисует ее в форме кривой, но ниже нее. Глава 11. Модули markers, labelpath и patterns 131 Ïðèìåð 11.7. Óãëû òðåóãîëüíèêà. import markers; size(6cm,0); pair O=(0,0), A=(2,0), B=(1,1.5), C=A+dir(A--O,A--B); markangle(n=2,radius=20,A,O,B,p=bp+green, filltype=Fill(pink)); markangle(n=1,radius=20,O,B,A,p=bp+green, marker(markinterval(stickframe(n=2,4mm,0.5+red) ,true))); markangle(n=2,radius=20,B,A,O,p=bp+green, marker(markinterval(2,stickframe(n=1,2mm, 0.5bp+red),true))); draw(A--interp(A,C,1.68),brown+black); draw(A--O--B--cycle,bp+blue); label("$O$",O,SW); label("$A$",A,SE);label("$B$",B,N); B O A Ïðèìåð 11.8. Ìåòêà â ôîðìå ïóòè. is at est of curved la Th at e s t o f c u r ve d in Asymp to in Asympt els o b la te is is ls be te is Th import labelpath; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); path p=(-1,0)..(1,-0.7)..(1.5,-0.3)..(3,-.75); labelpath("This is a test of curved labels in Asymptote",p,blue); draw(p,red); path q=shift(0,-1)*p; labelpath(shift(0,-12)*"This is a test of curved labels in Asymptote",q,blue); draw(q,red); Часто приходится надписывать графики функций, размещая надпись вдоль графика. Результат такого оформления показан на рис. примера 11.9. 11.3 Ìîäóëü patterns Процедуры данного модуля позволяют создавать области, заполненные шаблонами (по-другому их называют ячеистыми, плиточными, или мозаичными структурами). Для этого используются образцы мозаик-картинок, имеющиеся в PostScript, которые Asymptote вызывает по их имени name. Добавление мозаик производится оператором void add(string name, picture pic, pair lb=0, pair rt=0); с необязательным указанием левого нижнего поля lb и правого верхнего rt. Чтобы заполнить рисунок мозаикой name, надо применить перо pattern("name"). Для получения мозаики из прямоугольников можно использовать процедуры picture tile(real Hx=5mm, real Hy=0, pen p=currentpen, filltype filltype=NoFill); Глава 11. Модули markers, labelpath и patterns 132 Ïðèìåð 11.9. Íàäïèñè âäîëü ãðàôèêà. import graph; import labelpath; size(7.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); pen penfill=rgb(0.85,1.0,0.85); real f1(real x) {return sqrt(4-x^2);} path grf1=graph(f1,-2,2); y real f2(real fi) {return 2-sin(fi);} path 2 2 grf2=polargraph(f2,pi,0,operator..); x +y =4 path grf=buildcycle(grf1,grf2); fill(grf,penfill); draw(grf,blue+1bp); D draw ((-2,0)--(2.3,0),orange,Arrow(size=3mm), 2 y )2 = 4(x2 + 2 L=Label("$x$",mahogany, 2 +y + y) (x position=EndPoint,align=E)); draw ((0,0)--(0,2.7),orange,Arrow(size=3mm), O L=Label("$y$",mahogany, position=EndPoint,align=N)); label("$O$",(0,0),1.5*S,mahogany); label("$D$",(0.6,1.45)); label("$2$",(2,0),1.5*S); labelpath(Label(shift(0,5)*"$x^2+y^2=4$",Relative(0.3)),grf1); labelpath(Label(shift(0,-13)*"$(x^2+y^2+y)^2=4(x^2+y^2)$",align=2*S),grf2); 2 x picture checker(real Hx=5mm, real Hy=0, pen p=currentpen); picture brick(real Hx=5mm, real Hy=0, pen p=currentpen); В примере 11.10 показано заполнение кругов такого рода мозаикой. Ïðèìåð 11.10. Çàïîëíåíèå ïðÿìîóãîëüíîé ìîçàèêîé. import patterns; size(7.5cm,0); add("tile",tile()); add("filledtilewithmargin",tile(6mm,4mm, red,Fill),(1mm,1mm),(1mm,1mm)); add("checker",checker()); add("brick",brick()); real s=2.3; filldraw(unitcircle,pattern("tile")); filldraw(shift(s,0)*unitcircle, pattern("filledtilewithmargin")); filldraw(shift(2s,0)*unitcircle,pattern("checker")); filldraw(shift(3s,0)*unitcircle,pattern("brick")); Штриховые заполнения реализуются процедурами picture hatch(real H=5mm, pair dir=NE, pen p=currentpen); picture crosshatch(real H=5mm, pen p=currentpen); применение которых демонстрирует пример 11.11. Чтобы мозаика отображалась на экране корректно, приходится иногда отключать сглаживание в PostScript-просмотрщике. Глава 11. Модули markers, labelpath и patterns Ïðèìåð 11.11. Çàïîëíåíèå øòðèõîâêîé. import patterns; size(7.5cm,0); add("hatch",hatch(1mm,blue)); add("hatchback",hatch(2mm,NW,red)); add("crosshatch", crosshatch(3mm,heavygreen)); real s=1.25; filldraw(unitsquare,pattern("hatch")); filldraw(shift(s,0)*unitsquare, pattern("hatchback")); filldraw(shift(2s,0)*unitsquare,pattern("crosshatch")); 133 Ãëàâà 12 Ïàêåò owchart Этот модуль предоставляет процедуры для рисования блок-схем. Первичной структурой является блок, соответствующий одному блоку блок-схемы. Следующие восемь функций возвращают позицию на соответствующей границе блока при заданном преобразовании изображения t: pair block.top(transform t=identity()); pair block.left(transform t=identity()); pair block.right(transform t=identity()); pair block.bottom(transform t=identity()); pair block.topleft(transform t=identity()); pair block.topright(transform t=identity()); pair block.bottomleft(transform t=identity()); pair block.bottomright(transform t=identity()); Чтобы получить произвольную позицию вдоль границы блока в пользовательских координатах, следует применить: pair block.position(real x, transform t=identity()); Центр блока в пользовательских координатах сохраняется в block.center, а размер блока в координатах PostScript задается как block.size. Фрейм, содержащий блок, можно получить с помощью frame block.draw(pen p=currentpen); Следующие процедуры генерации блоков в качестве аргумента типа object принимают метку, строку или фрейм. • Прямоугольный блок с необязательным заголовком (и контуром dx вокруг заголовка и тела): block rectangle(object header, object body, pair center=(0,0), pen headerpen=mediumgray, pen bodypen=invisible, pen drawpen=currentpen, real dx=3, real minheaderwidth=minblockwidth, real minheaderheight=minblockwidth, real minbodywidth=minblockheight, real minbodyheight=minblockheight); block rectangle(object body, pair center=(0,0), 134 Глава 12. Пакет flowchart 135 pen fillpen=invisible, pen drawpen=currentpen, real dx=3, real minwidth=minblockwidth, real minheight=minblockheight); • Блок-параллелограмм: block parallelogram(object body, pair center=(0,0), pen fillpen=invisible, pen drawpen=currentpen, real dx=3, real slope=2, real minwidth=minblockwidth, real minheight=minblockheight); • Блок-ромб: block diamond(object body, pair center=(0,0), pen fillpen=invisible, pen drawpen=currentpen, real ds=5, real dw=1, real height=20, real minwidth=minblockwidth, real minheight=minblockheight); • Блок в виде круга: block circle(object body, pair center=(0,0), pen fillpen=invisible, pen drawpen=currentpen, real dr=3, real mindiameter=mincirclediameter); • Прямоугольный блок со скругленными углами: block roundrectangle(object body, pair center=(0,0), pen fillpen=invisible, pen drawpen=currentpen, real ds=5, real dw=0, real minwidth=minblockwidth, real minheight=minblockheight); • Прямоугольный блок со скошенными углами: block bevel(object body, pair center=(0,0), pen fillpen=invisible, pen drawpen=currentpen, real dh=5, real dw=5, real minwidth=minblockwidth, real minheight=minblockheight); Для рисования путей, соединяющих точки point с помощью ломаных с прямыми углами между звеньями, служит процедура path path(pair point[] ... flowdir dir[]); Записи в dir определяют, следует ли рисовать последовательные сегменты между точками, указанными point, в горизонтальном, Horizontal, или вертикальном, Vertical, направлениях. На рис. 12.1 и 12.2 показаны примеры применения рассматриваемого модуля. Глава 12. Пакет flowchart 136 Ïðèìåð 12.1. Ñòðóêòóðíàÿ ñõåìà ñèñòåìû óïðàâëåíèÿ. import flowchart; size (0,4cm); block delay = roundrectangle("$e^{-sT_t}$",(0.33,0)); block system = roundrectangle("$\frac{s+3}{s^2+0.3s+1}$",(0.6,0)); block controller = roundrectangle("$0.06\left( 1 + \frac{1}{s}\right)$", (0.45,-0.25)); u s+3 block sum1 = e−sTt s2 +0.3s+1 circle("",(0.15,0), mindiameter=0.3cm); block junction1 = circle("",(0.75,0), fillpen=currentpen); 0.06 1 + 1s draw(delay); draw(system); draw(controller); draw(sum1); draw(junction1); add(new void(picture pic, transform t) { blockconnector operator -- = blockconnector(pic,t); block(0,0)--Label("$u$",align=N)--Arrow--sum1--Arrow--delay--Arrow-system--junction1--Label("$y$",align=N)--Arrow--block(1,0); junction1--Down--Left--Arrow--controller--Left--Up-Label("$-$",position=3,align=ESE)--Arrow--sum1;}); y Глава 12. Пакет flowchart 137 Ïðèìåð 12.2. Áëîê-ñõåìà ïðîãðàììû. size(0,300); import flowchart; block block1 = rectangle(Label("Example",magenta), pack(Label("Start:",heavygreen),"", Label("$A:=0$", blue), "$B:=1$"),(-0.5,3),palegreen, paleblue,red); block block2 = diamond(Label("Choice?",blue),(0,2), palegreen,red); block block3 = roundrectangle( "Do something",(-1,1)); block block4 = bevel("Don't do something",(1,1)); block block5=circle("End",(0,0)); draw(block1); draw(block2); draw(block3); draw(block4); draw(block5); add(new void(picture pic, transform t){ blockconnector operator -- = blockconnector(pic,t); /* draw(pic,block1.right(t)-block2.top(t)); */ block1--Right--Down--Arrow--block2; block2--Label("Yes",0.5,NW)-Left--Down--Arrow--block3; block2--Right--Label("No", 0.5,NE)--Down--Arrow--block4; block4--Down--Left--Arrow--block5; block3--Down--Right--Arrow--block5;}); Example Start: A := 0 B := 1 Yes Choice? Do something No Don’t do something End Ãëàâà 13 Ïàêåò asy-circ Пока еще идет речь о рисунках на плоскости, естественно изложить здесь и содержание пакета, указанного в заголовке главы, так как он предназначен для изображения электросхем, которые, как известно, изображаются преимущественно на плоскости. Пакет, автором которого является Christophe Casseau, несложно найти в Интернете, он поставляется в виде файла asy-circ 1.0.zip. В этом архиве имеется инструкция readme, которую следует выполнить, если есть необходимость в руководстве на английском языке. В противном случае файл можно просто распаковать и использовать. Основной частью пакета являются модули ElectricalComponents, Element, Circuit и файл MajDroite.sty. Их наличие в директории, в которой предполагается компилировать свой файл рисунка, обеспечит получение результата. Данный пакет позволяет не только рисовать электрические и электронные схемы, но и создавать на базе основных элементов сложные графические компоненты, которые затем можно использовать в качестве новых элементов. Таким образом конструкция схемы быстро и эффективно доводится до необходимых пределов. 13.1 Ýëåìåíòû Основным компонентом электрической схемы является ее элемент (Element), имеющий список свойств, которые задаются во время его создания в результате присваивания значений соответствующим параметрам. Начнем с анализа структуры сопротивления, пример 13.1. Ïðèìåð 13.1. Ðåçèñòîð. import ElectricalComponents; size(200); Element r=Resistor("R"); pair A=(0,0), B=(40,0); join(A,B,r); R Видно, что сопротивление Resistor (R) имеет тип Element. Вообще, создание элементов схемы типа Element всегда происходит единообразно: Element e = Èìÿ_ýëåìåíòà (ïàðàìåòðû_ýëåìåíòà); Строка "R" – это текстовая метка, связанная с элементом. Позже будет показано, что каждый элемент типа Element дополнительно к своим основным характеристикам снабжается функциями, которые позволяют пользователю получать доступ к его параметрам, изменять 138 Глава 13. Пакет asy-circ 139 их или дополнять. В программе указаны точки A и B, между которыми следует расположить резистор. Функция join () осуществляет рисование резистора. Пример 13.2 показывает другой способ объявления элементов. Они могут быть инициализированы с помощью функции join(). С одной стороны, эта функция делает код более компактным, с другой, – лишает пользователя некоторых преимуществ, предоставляемых декларацией Element. Детали поясняются ниже. Ïðèìåð 13.2. Ïîñëåäîâàòåëüíîå ñîåäèíåíèå ðåçèñòîðîâ. import ElectricalComponents; size(200); pair A=(0,0), B=(100,0); join(A,B,Resistor("$R_1$"), Resistor("$R_2$"),Resistor("$R_3$")); R1 R2 R3 Рассмотрим список параметров, доступных при инициализации элемента: pair center = (0,0), real size = 1, real angle = 0, pen fillpen = invisible, pen drawpen = currentpen ... Label [] L. center Позиция центра элемента в пользовательских координатах. size Размер элемента; параметр имеет смысл кратности значения; если указано size=2, то элемент будет иметь двойной размер. angle Угол поворота элемента по отношению к направлению проводника. fillpen Закрашивает элемент, если это возможно. Например, этот параметр не применим к индуктивности. drawpen Цвет обводки. L Текст метки; не имея возможности добавить отдельную метку, все же можно точно выбрать ее позицию в окрестности элемента. Рассмотрим пару примеров применения параметров. Чтобы поместить метку в определенное место окрестности элемента, можно использовать параметр align функции Label(), имеющейся в Asymptote. В примере 13.3 показано размещение меток в различных направлениях от помечаемых элементов; например, метка резистора расположена к югу (S) от него. Ïðèìåð 13.3. Èñïîëüçîâàíèå ïàðàìåòðîâ. import ElectricalComponents; size(200); pair A=(0,0), B=(100,0); join(A,B,Resistor(Label("$R_1$",align=S), size=2, fillpen=red+white, drawpen=red+1), Coil(Label("L",align=NW), size=2, drawpen=blue+white+2), Diode(Label("D",align=3SE), size=2, fillpen=green+white)); L R1 D Пример 13.4 демонстрирует повороты резисторов с помощью параметра angle. Приведем теперь базовый список элементов вместе с их изображениями, примеры 13.513.23. Глава 13. Пакет asy-circ Ïðèìåð 13.4. Ôàíòàçèè ìèðà ïðÿìîóãîëüíèêîâ. import ElectricalComponents; size(200); pair A=(0,0), B=(100,0); join(A,B,Resistor(size=2,angle=-20, fillpen=red+white), Resistor(size=2,angle=40,drawpen=green+1), Resistor(size=2,angle=-60,fillpen=blue+white)); Ïðèìåð 13.5. Âàðèàíòû èçîáðàæåíèÿ ðåçèñòîðà (Resistor è Zresistor). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Resistor(),Zresistor()); Ïðèìåð 13.6. Êîíäåíñàòîð (Capacitor). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Capacitor()); Ïðèìåð 13.7. Èíäóêòèâíîñòü (Coil). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Coil()); Ïðèìåð 13.8. Áàòàðåÿ (Battery). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Battery(size=1.8,sign=true), Battery(type=1,sign=true,size=1.8)); Ïðèìåð 13.9. Èäåàëüíûé ãåíåðàòîð (IdealGen). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,IdealGen()); Ïðèìåð 13.10. Ãåíåðàòîð òîêà (Icc). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Icc()); 140 Глава 13. Пакет asy-circ Ïðèìåð 13.11. Äèîä (Diode). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Diode()); Ïðèìåð 13.12. Ñòàáèëèòðîí (Zener). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Zener()); Ïðèìåð 13.13. Ìîòîð (Motor). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Motor()); Ïðèìåð 13.14. Ëàìïà (Lamp). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Lamp(fillpen=yellow),Lamp()); Ïðèìåð 13.15. Ïåðåêëþ÷àòåëü (Switch). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Switch()); Ïðèìåð 13.16. Ïîòåíöèîìåòð (Potentiometer). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Potentiometer()); 141 Глава 13. Пакет asy-circ Ïðèìåð 13.17. Òðàíçèñòîðû (PNP è NPN). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,PNP(),NPN()); Ïðèìåð 13.18. Îïåðàöèîííûé óñèëèòåëü (OA è AO). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,OA(size=.5),AO(size=.5)); Ïðèìåð 13.19. Ïðåðûâàòåëü (Tswitch). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Tswitch()); Ïðèìåð 13.20. Íàïðÿæåíèå (Tension). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Tension()); Ïðèìåð 13.21. Òîê (Intensity). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Intensity(size=3,fillpen=red)); Ïðèìåð 13.22. Çåìëÿ (Ground). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Ground(size=1.5), Ground(size=1.5,type=1)); 142 Глава 13. Пакет asy-circ 143 Ïðèìåð 13.23. Ýëåìåíò ñ ïåðåìåííûì çíà÷åíèåì ïàðàìåòðà (variable). import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); Element r=Resistor();r.variable(); Element z=Zresistor();z.variable(); join(A,B,r,z); 13.2 Ôóíêöèÿ join В общем случае прототип этой функции записывается как join(pair A, pair B, bool dot=true ... Element[] e) а ее параметры имеют следующий смысл. A Начальная точка. B Конечная точка. dot Надо ли отмечать красным цветом начальную и конечную точки. e Список элементов схемы. Если для элементов схемы нет специальных указаний, функция join() разместит их вдоль провода автоматически. Иными словами, элементы будут расположены так, чтобы отрезки проводов с обеих сторон элемента было примерно одинаковы. Но можно и точно указать положение элемента между точками A и B. Ранее было сказано, что элемент имеет функции, которые дополняют его основные параметры. Пришло время остановиться на этом подробнее. Упомянутые функции называются методами. Чтобы указать точное положение элемента схемы, используется метод setPosition(real a), где a – относительное положение между клеммами A и B на проводе. В частности, a = 0 для A и a = 1 для B. Если надо поместить элемент посреди отрезка AB, следует указать a = 0.5; впрочем, функция join() делает это по умолчанию. В примере 13.24 элементы придвинуты к концам отрезка AB. Ïðèìåð 13.24. Ìåòîä setPosition. import ElectricalComponents; size(200); Element r=Resistor(size=2,fillpen=green+white); r.setPosition(.2); Element b=Battery(sign=true,size=2); b.setPosition(.8); pair A=(0,0), B=(100,0); join(A,B,r,b); Таким образом, метод добавляется к имени элемента с помощью точки. Следует заметить, что join() считает отрезок AB направленным от A к B. Как показывает пример 13.25, рисование от A к B и от B к A – не одно и то же. Асимметричные элементы, такие, как диод, для изменения своей ориентации на противоположную имеют дополнительный аргумент type. Он может принимать два значения: 0 по умолчанию и 1, чтобы повернуть элемент на 180◦ , см. пример 13.26. Глава 13. Пакет asy-circ 144 Ïðèìåð 13.25. Îò A ê B è îò B ê A. import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(A,B,Diode(size=2,fillpen=yellow), Coil(drawpen=red+2)); dot(L=Label("A"),A,W); dot("B",B); pair A=(0,-10), B=(40,-10); join(B,A,Diode(size=2,fillpen=yellow), Coil(drawpen=red+2)); dot(L=Label("A"),A,W); dot("B",B); A B A B Ïðèìåð 13.26. Îò B ê A ñ äðóãèì íàïðàâëåíèåì äèîäà. import ElectricalComponents; size(200); pair A=(0,0), B=(40,0); join(B,A,Diode(type=1,size=2,fillpen=yellow), Coil(drawpen=red+2)); dot(L=Label("A"),A,W); dot("B",B); A B 13.3 Ìåòîäû ýëåìåíòîâ Если элемент инициализируется методом join(), он является анонимным элементом, и пользователь не может воспользоваться его методами. Это становится возможным только в том случае, если элемент объявлен как: Element e = ... Тогда становятся доступными следующие методы: setPosition(real a) addTension(int type=0, Label L, real decal=1.2, real multlength=1, pen p=black) addLabel(Label L, real rot=90) variable(real length=1, real rot=45, pen p=currentpen) connect(int n=0) 13.3.1 Ìåòîä setPosition Применение метода было показано в примере 13.24. 13.3.2 Ìåòîä addTension Этот метод добавляет к изображению элемента стрелку напряжения, направленную вдоль элемента. Метод имеет следующие параметры. type Принимает значения 0 или 1, определяя одно из двух возможных направлений стрелки. L Метка, текст которой обычно является обозначением напряжения. decal Расстояние между элементом и стрелкой. multlength Длина стрелки. Глава 13. Пакет asy-circ 145 p Перо для рисования стрелки. Следующие два примера, 13.27 и 13.28, показывают применение рассматриваемого метода. Ïðèìåð 13.27. Ìåòîä addTension. import ElectricalComponents; import Circuit; import math; real size=200; size(size); Element r=Battery(type=1,sign=true); r.addTension(Label("E",align=N,blue), multlength=8,pen=green); r.setPosition(.7); Element i=Intensity( Label("I",orange),fillpen=red); i.setPosition(.3); pair A=(0,0), B=(30,0); join(A,B,i,r); E I Ïðèìåð 13.28. Ìåòîä addTension. import ElectricalComponents; import Circuit; import math; real size=200; size(size); Element r=Resistor("R"); r.addTension(type=1, Label("$\mathrm{U_R}$",blue), decal=3,multlength=1.5,pen=green); r.setPosition(.7); Element i=Intensity( Label("I",orange),fillpen=red); i.setPosition(.2); pair A=(0,0), B=(40,0); join(A,B,i,r); 13.3.3 UR I R Ìåòîä addLabel Может показаться, что этот метод избыточен, так как элемент уже имеет параметр L, но, как уже было сказано выше, пакет asy-circ позволяет пользователю создавать сложные элементы динамически (объединяя несколько базовых элементов в одно целое). В этом случае и может понадобиться текстовая метка, относящаяся ко всему созданному элементу. На рис. 13.1 показаны четыре вновь созданных элемента, полученных из параллельных соединений базовых элементов, снабженные надписями, сгенерированными методом addLabel. 13.3.4 Ìåòîä variable Этот метод используется для указания того, что значение элемента настраивается пользователем. Использование метода для элементов различных типов показано на рис. 13.29. 13.3.5 Ìåòîä connect Метод connect() позволяет включать в схемы элементы с бо́льшим, чем два, числом клемм, в то время как метод join() зарезервирован для диполей. Глава 13. Пакет asy-circ 146 Bottom Center R3 R3 Top R2 R3 R2 Somewhere C1 R1 coil R1 R2 D1 R1 Рис. 13.1. Надписи над элементами, созданными пользователем. Ïðèìåð 13.29. Ìåòîä variable. import ElectricalComponents; size(200); Element r=Resistor(fillpen=pink); Element c=Coil(drawpen=orange+1); Element b=Battery();b.variable(rot=-45); r.variable(); c.variable(length=.8,p=orange+1); pair A=(0,0), B=(40,30), C=(40,0); pair D=(0,30); join(A,B,r); join(A,C,c); join(A,D,b); В примере 13.30 функция connect определяет координаты базы, коллектора и эмиттера транзистора, которые затем становятся координатами точек. Остается лишь использовать эти точки, чтобы нарисовать электросхему. Выражение center=(50,50) позволяет задать положение элемента на рисунке. Процедура n.draw рисует компонент – draw заменяет join. Ïðèìåð 13.30. Ìåòîä connect. import ElectricalComponents; size(200); Element n=PNP(center=(50,50)); n.draw(); pair B=n.connect(0), C=n.connect(1), Em=n.connect(2); pair A=(40,B.y),D=(C.x,60),F=(Em.x,40); draw(A--B^^Em--F^^C--D); dot(Label("A",align=N),A); dot(Label("D",align=E),D); dot(Label("F",align=E),F); dot(Label("B",align=NW),B); dot(Label("C",align=NW),C); dot(Label("E",align=SW),Em); D C A B E F Глава 13. Пакет asy-circ 147 13.4 Ýëåêòðè÷åñêèå öåïè 13.4.1 Êîìïîíîâî÷íàÿ ñåòêà Для удобства конструирования схем пакет asy-circ предлагает пользователю состоящую из точек компоновочную сетку, которую формирует функция addGrid(picture pic=currentpicture, int x=(int)max(currentpicture).x, int y=(int)max(currentpicture).y, int griddivx=10, int griddivy=10, pen pen=black); Здесь x – ширина сетки, y – ее высота, griddivx – число промежутков между точками по x, griddivy – число промежутков между точками по y, pen – цвет точек сетки. Из определения функции видно, что по умолчанию получим сетку 10×10 промежутков с точками черного цвета. В примере 13.31 сетка имеет стандартное разбиение 10 × 10, а в примере 13.32 показано заданное пользователем разбиение 5 × 5. Ïðèìåð 13.31. Ñòàíäàðòíîå ðàçáèåíèå íà ïðîìåæóòêè. import ElectricalComponents; size(200); pair A=(20,60), B=(120,120); dot("A",A,blue+6); dot("B",B,blue+6); addGrid(200,200,gray+3); B A (20,20) Ïðèìåð 13.32. Ðàçáèåíèå 5x5. import ElectricalComponents; size(200); pair A=(40,80), B=(120,120); dot("A",A,blue+6); dot("B",B,blue+6); addGrid(200,200,5,5,gray+3); B A (40,40) Глава 13. Пакет asy-circ 13.4.2 148 Ïðèìåðû ýëåêòðè÷åñêèõ ñõåì Примеры 13.33-13.37 демонстрируют возможности создания достаточно сложных схем с помощью данного пакета. import ElectricalComponents; size(200); pair A=(40,50), B=(150,10),C=(100,100); pair D=(40,150),F=(140,150); join(A,C,Intensity(size=1,type=1,"$I_1$", fillpen=red,head=TeXHead)); join(B,C,Intensity(size=3,"$I_2$", I3 I4 Ïðèìåð 13.33. Ïðàâèëî Êèðõãîôà. I1 I2 fillpen=blue+2,head=SimpleHead)); join(D,C,Intensity(size=3,type=1,"$I_3$", fillpen=green,head=HookHead)); join(F,C,Intensity(size=3,"$I_4$",fillpen=orange)); Ïðèìåð 13.34. Ñõåìà ñ ëàìïàìè. import ElectricalComponents; size(200); pair A=(0,40), B=(30,40), C=(60,40); pair D=(0,0), F=(30,0), G=(60,0); join(A,C,Lamp(fillpen=yellow), Switch("$K_1$")); join(B,F,Lamp(fillpen=yellow)); join(D,A,Battery(sign=true,size=1.8)); join(D,G,Resistor(),Resistor()); join(G,C,Lamp()); K1 13.5 Êîìïëåêñíûå ýëåìåíòû Используя комбинацию основных элементов asy-circ, соединенных параллельно или последовательно, можно создать новый элемент. Тип Element такого соединения может быть описан как супер Element, что дает ему возможность использовать методы Element. Для этой цели задействуем объект circuit(), который имеет параметром список элементов: Element e = Circuit(...Element[] e) Создать суперэлемент можно двумя способами: serie(real decal=20) parallel(real decal=10, real hc=0, int branch=1) Глава 13. Пакет asy-circ 149 Ïðèìåð 13.35. Ñõåìà ñ òðàíçèñòîðîì. RC IC RB IB NPN VCE E IE import ElectricalComponents; usepackage("MajDroite"); usepackage("mathdesign","charter"); size(200); Element n=NPN( Label("NPN",align=SW),center=(50,50)); n.draw(); pair B=n.connect(0), C=n.connect(2), E=n.connect(1); pair A=(20,B.y),D=(E.x,80),F=(C.x,20); pair G=(20,20),H=(70,20),I=(70,80); join(A,B,Resistor("$R_B$"), Intensity("$I_B$",fillpen=red)); join(D,E,Resistor(rotate(90)*"$R_C$"), Intensity("$I_C$",fillpen=red)); join(G,A,Battery(rotate(-90)*"E",type=1)); join(H,I, Battery(Label(rotate(-90)*"E",align=S), type=1)); join(C,F,Intensity("$I_E$",fillpen=red), Lamp(rotate(90)*"L",fillpen=yellow)); join(E+(2,0),C+(2,0), Tension(type=1, rotate(90)*"$V_{CE}$"), dot=false); draw(G--H^^D--I); E L Ïðèìåð 13.36. Ñîåäèíåíèå çâåçäîé. 13.5.1 R1 I1 I2 R2 V1 R3 I3 V3 V2 import ElectricalComponents; size(200); pair A=(0,60),B=(40,60); pair C=(0,40),D=(30,40); pair F=(0,20),G=(40,20); pair H=(0,0),I=(50,0),J=(50,40); join(A,B,Intensity("$I_1$",fillpen=red)); join(B,D,Zresistor("$R_1$")); join(C,D,Intensity("$I_2$",fillpen=red), Zresistor("$R_2$")); join(G,D,Zresistor("$R_3$")); join(F,G,Intensity("$I_3$",fillpen=red)); join(H,I,Ground()); join(H,A,Tension("$V_1$",drawpen=blue), dot=false); join(H+(7,0),C+(7,0),Tension(Label("$V_2$", align=NW),drawpen=blue),dot=false); join(H+(14,0),F+(14,0),Tension("$V_3$", drawpen=blue),dot=false); draw(H--I--J--D); Ïîñëåäîâàòåëüíûå è ïàðàëëåëüíûå öåïè Вначале рассмотрим последовательную цепь. Для нее вводится единственный параметр decal, который позволяет установить расстояние между элементами. В примере 13.38 показано, как создать суперэлемент из последовательного соединения элементов. Глава 13. Пакет asy-circ 150 Ïðèìåð 13.37. Çàçåìëåíèå. R 2 U2 I1 I2 R3 U3 U? R1 U1 import ElectricalComponents; size(200); pair A=(40,50), B=(20,150),C=(100,100); pair D=(150,100),E=(100,150), F=(140,150); join(A,C,Resistor(size=2,"$R_1$")); join(B,C,Resistor(size=2,"$R_2$")); join(D,C,Resistor(size=2,Label("$R_3$", align=S))); Element i1=Intensity(); i1.addLabel(Label(rotate(90)*"$I_1$", align=N)); i1.setPosition(.5); Element i2=Intensity(type=1); i2.addLabel(Label(rotate(-45)*"$I_2$", align=S)); i2.setPosition(.5); join(E,C,i1); join(F,C,i2); join((40,20),A,dot=false, Tension("$\mathrm{U_1}$")); Element g1=Ground(size=2,center=((40,20))); g1.draw(); join((20,20),B,dot=false, Tension("$\mathrm{U_2}$")); Element g2=Ground(size=2,center=((20,20))); g2.draw(); join((100,20),C,dot=false,Tension("U?", drawpen=blue)); Element g3=Ground(size=2,center=((100,20))); g3.draw(); join((150,20),D,dot=false, Tension("$\mathrm{U_3}$")); Element g4=Ground(size=2,center=((150,20))); g4.draw(); Ïðèìåð 13.38. Ñóïåðýëåìåíò èç ïîñëåäîâàòåëüíîé öåïè. import ElectricalComponents; size(200); Element e=Circuit(Resistor("$R_1$",size=2), Resistor("$R_2$",size=2)).serie(40); pair A=(0,0), B=(100,0); join(A,B,e); R1 R2 Метод parallel предполагает три параметра: • decal устанавливает расстояние между двумя компонентами по вертикали; • branch определяет ветвь, которая должна соединять суперэлемент с остальной частью схемы; • hc добавляет сдвиг точки подключения ветви; чтобы соединить середины двух ветвей, надо взять hc=decal/2. Как это работает, можно увидеть на примере 13.39. Глава 13. Пакет asy-circ 151 Ïðèìåð 13.39. Ñóïåðýëåìåíò èç ïàðàëëåëüíîãî ñîåäèíåíèÿ. import ElectricalComponents; size(200); R2 Element e=Circuit(Resistor("$R_1$",size=2), Resistor("$R_2$",size=2)) .parallel(decal=40,hc=20,branch=1); R1 pair A=(0,0), B=(100,0); join(A,B,e); 13.5.2 Ñîåäèíåíèÿ ñóïåðýëåìåíòîâ С помощью следующих далее примеров 13.40 и 13.41 можно познакомиться с применением методов элемента (Element) к суперэлементам. Это позволяет создавать изображения сравнительно сложных схем, используя минимальное количество кода. Ïðèìåð 13.40. Ïîñëåäîâàòåëüíî-ïàðàëëåëüíîå ñîåäèíåíèå. import ElectricalComponents; currentpen=fontsize(6); size(100); // Ñóïåðýëåìåíò 1 òèïà serie Element e1=Circuit(Resistor("$R_2$"), Resistor("$R_3$")).serie(25); // Ñóïåðýëåìåíò 2 òèïà parallel Element e2=Circuit(Resistor("$R_4$"),e1) .parallel(20,branch=2); // Ñóïåðýëåìåíò 3 òèïà serie Element e3=Circuit(Resistor("$R_1$"),e2) .serie(40); pair A=(0,0), B=(150,0); U R1 R2 R3 R4 // Íàïðÿæåíèå äëÿ âñåé öåïè e3.addTension(Label("U",fontsize(8)),decal=2); e3.size=2; join(A,B,e3); Следует, однако, обратить внимание на то, что использование суперэлементов может быть лимитировано точками соединения и функцией join. В примере 13.40 вначале создается суперэлемент e1 из резисторов R2 и R3. Далее из резистора R4 и суперэлемента e1 создается суперэлемент e2, а затем из него и резистора R1 конструируется суперэлемент e3, который фактически и представляет собой всю схему. При этом метка напряжения относится к e3, а, значит, и ко всей цепи. Еще некоторые нюансы создания схем из суперэлементов демонстрирует пример 13.41. Глава 13. Пакет asy-circ 152 2. 5Ω cir = ch R 5 Ω = 5Ω R 3 Su 10 = Ω 1 R 4 = 6Ω R 2 = 30 R 6 Ω = 20 R import ElectricalComponents; size(300); // Ñóïåðýëåìåíò 1 òèïà parallel Element e1= Circuit(Resistor("$R_2=30\Omega$"), Resistor("$R_1=10\Omega$")) .parallel(20,10,branch=1); // Ñóïåðýëåìåíò 2 òèïà serie Element e2= Circuit(e1,Resistor("$R_3=2.5\Omega$")) .serie(40); // Ñóïåðýëåìåíò 3 òèïà parallel Element e3=Circuit(Resistor("$R_6=20\Omega$"), Resistor("$R_5=5\Omega$")) .parallel(20,10,branch=1); // Ñóïåðýëåìåíò 4 òèïà serie Element e4=Circuit(Resistor("$R_4=6\Omega$"),e3) .serie(40); // Ñóïåðýëåìåíò 5 òèïà parallel Element e5=Circuit(e4,e2) .parallel(30,15,branch=1); pair A=(0,0), B=(70,70); // Ìåòêà-êîììåíòàðèé äëÿ âñåé ñõåìû e5.addLabel(Label("Such circuit",fontsize(16)+blue)); join(A,B,e5); cu it Ïðèìåð 13.41. Ñõåìà èç ñóïåðýëåìåíòîâ. ×àñòü III Ðèñîâàíèå â ïðîñòðàíñòâå 153 Ãëàâà 14 Ïðîåêöèè, ïåðñïåêòèâà, îñâåùåíèå 14.1 Íàñòðîéêè 14.1.1 Èñïîëüçîâàíèå ôîðìàòà PRC Формат PRC (Product Representation Compact) является форматом, который можно использовать для встраивания трехмерных данных в pdf-файл. Отличается высокой степенью сжатия трехмерных моделей. Просмотр таких pdf-файлов возможен в программе Adobe Reader, начиная с версии 9.0. Основная цель использования формата PRC – внедрение в pdf-документ интерактивных изображений, анимации. Для изучения трехмерных тел бывает удобно рассматривать их, поворачивая, меняя точку обзора и т. п. Формат PRC встраивается в pdf-файл, если задана установка settings.prc=true; причем, ей должна предшествовать установка settings.outformat="pdf", расположенная в тексте перед командами типа import graph3 или import three. Если возможность интерактивности не используется, надо задать противоположную установку settings.prc=false; Если для большинства изображений предусмотрена интерактивность, а для какого-то изображения она не предусмотрена, то в начале кода для этого изображения следует поместить settings.embed=false; 14.1.2 Ðàçðåøåíèå èçîáðàæåíèÿ Команда settings.render управляет степенью разрешения изображения. Например, если установить settings.render=1; разрешение будет равно одному пикселю на один «большой пункт», см. п. 6.1.2. В общем случае установка setting render=n дает разрешение в n пикселей на bp. Отрицательное значение n в форматах eps и pdf интерпретируется как |2n|, для других форматов – как |n|. По умолчанию считается, что разрешение равно -1. Вообще рендерингом называется процесс создания растрового изображения (набора точекпикселей) из векторного. Для того чтобы отобразить векторное изображение 3d на экране, 154 Глава 14. Проекции, перспектива, освещение 155 оно должно пройти процедуру рендеринга. Обычно это делает видеокарта и ОС компьютера, более качественные рендеры – специальные программы (растеризаторы). Картинка подвергается рендерингу по умолчанию дважды, но это можно отменить установкой antialias=1. Высокое разрешение достигается дискретизацией (тайлингом, разбиением образа на мелкую плитку) изображения. Если позволит видеокарта, рендеринг можно сделать более эффективным, увеличив максимальный размер плитки maxtile до размеров экрана (указывается в виде maxtile=(0,0)). Если видеокарта создает нежелательные черные линии, можно попробовать установить горизонтальные и вертикальные компоненты maxtile меньшими, чем размеры экрана. Размер плитки также лимитируется величиной maxviewport, которая ограничивает максимальную ширину и высоту обзора. Увеличивая параметр render, можно получить более качественное изображение, но трансляция будет идти дольше и размер файла тоже увеличится. Второй способ улучшения качества изображения заключается в том, чтобы перейти от формата pdf к формату png, который используется для растровых изображений, увеличив значение settings.render. Этот способ обычно и рекомендуется. Основным его недостатком является то, что двумерные части изображения будут растрированы. Таким образом, этот метод не подходит для получения изображений, включающих как дву-, так и трехмерные картинки. Допускается применять значение settings.render=0. 14.1.3 Ðàçìåðû Автоматическое вычисление размеров картинки выполняется с помощью двух перерисовок. Максимальный размер рисунка может быть указан командой void size3(picture pic=currentpicture, real x, real y=x, real z=y, bool keepAspect=pic.keepAspect); Впрочем, на окончательные размеры рисунка влияют конфигурация параллелепипеда, в который помещают трехмерное изображение, точка обзора, дополнительные поля. Именно, чтобы получить 3D-версию фрейма (выполненную фактически в качестве 3Dкартинки), применяется метод линейного программирования. Результат затем вместе с результатом второй перерисовки вписывается в размеры обзора в соответствии с параметрами обычного двумерного размера картинки. При этом может быть использована глобальная переменная-пара viewportmargin, чтобы добавить горизонтальные или вертикальные поля к размерам обзора. В качестве альтернативы может быть установлено минимальное значение viewportsize. 14.2 Êîñîóãîëüíàÿ ïðîåêöèÿ Для обычных, не интерактивных, рисунков выбор точки обзора трехмерного изображения сродни искусству. Выбрать подходящую точку обзора помогает предопределенная переменная currentprojection. Отметим, что единичные векторы базиса трехмерной системы координат обозначаются в Asymptote как X, Y, Z, а начало координат – как O. Для вычерчивания поверхностей и тел вращения часто используется косоугольная проекция, которая в Asymptote выражается процедурой oblique(real angle); Глава 14. Проекции, перспектива, освещение 156 В этой проекции оси абсцисс и ординат направлены так же, как на плоском чертеже, а ось Oz кажется выходящей из этой плоскости в направлении наблюдателя. Трехмерная точка (x, y, z) проектируется в двумерную точку (x − 0.5z, y − 0.5z). Если задан необязательный параметр angle, ось Oz чертится под указанным (в градусах) углом к оси Ox (см. рис. 14.1). Проекция obliqueZ является синонимом проекции oblique. currentprojection = oblique; currentprojection = oblique(30); Рис. 14.1. Стандартная косоугольная проекция. Чтобы на наблюдателя была направлена ось Ox, используют проекцию obliqueX(real angle); при которой точка (x, y, z) проектируется в точку (y − 0.5x, z − 0.5x). Имеет место и угол поворота оси (рис. 14.2). currentprojection = obliqueX; currentprojection = obliqueX(30); Рис. 14.2. Вперед выходит ось Ox. Наконец, если требуется, чтобы ось Oy казалась выходящей из плоскости рисунка, применяют проекцию obliqueY(real angle); В этом случае точка (x, y, z) проектируется в точку (x + 0.5y, z + 0.5y). Используется и угол поворота оси (рис. 14.3). 14.3 Îðòîãîíàëüíàÿ ïðîåêöèÿ Эта проекция имитирует взгляд на объект из некоторой довольно удаленной точки пространства. Соответствующая процедура имеет вид Глава 14. Проекции, перспектива, освещение currentprojection = obliqueY; 157 currentprojection = obliqueY(30); Рис. 14.3. Ось Oy удаляется от нас. orthographic(triple camera, triple up=Z, triple target=O, real zoom=1, pair viewportshift=0, bool showtarget=true, bool center=false); Тип переменной triple, как мы помним, означает тройку чисел. Наблюдатель находится в бесконечно удаленной точке пространства и смотрит на изображение в направлении от точки camera к точке target. Опциональный аргумент up указывает, что отмеченный им вектор будет направлен вертикально вверх. По умолчанию вверх направлен координатный вектор Z. Параллельные прямые изображаются параллельными прямыми. Если showtarget=true, то границы рисунка раздвигаются, чтобы точка target попала в поле зрения. Если параметр center=true, то target оказывается в центре поля зрения. Аргумент zoom – коэффициент изменения размеров рисунка. Если одна из координат точки camera равна нулю, получаем проекцию изображения на соответствующую координатную плоскость (рис. 14.4). camera = (5,2,3) camera = (5,2,0) Рис. 14.4. Ортогональная проекция. Применение проекции orthographic(real x, real y, real z, triple up=Z, triple target=O, real zoom=1, pair viewportshift=0, bool showtarget=true, bool center=false); Глава 14. Проекции, перспектива, освещение 158 эквивалентно orthographic((x,y,z),up,target,zoom,viewportshift,showtarget,center); В Asymptote определены стандартные точки обзора, используемые в техническом черчении: projection LeftView=orthographic(-X,showtarget=true); projection RightView=orthographic(X,showtarget=true); projection FrontView=orthographic(-Y,showtarget=true); projection BackView=orthographic(Y,showtarget=true); projection BottomView=orthographic(-Z,showtarget=true); projection TopView=orthographic(Z,showtarget=true); Процедура triple camera(real alpha, real beta); может быть использована для определения положения камеры для оси Ox, расположенной ниже горизонтали под углом alpha; оси Oy, расположенной вниз от горизонтали под углом beta; и оси Oz, направленной вверх. 14.4 Ïåðñïåêòèâà При использовании перспективы возникает эффект «реалистичности»: более близкие объекты кажутся бо́льшими. При этом обозначения осей координат и другие надписи на рисунке также выглядят бо́льшими, если они расположены ближе; в зависимости от контекста, это может быть как полезной особенностью, так и досадным артефактом. Процедура для создания подходящей перспективы выглядит следующим образом: perspective(triple camera, triple up=Z, triple target=O, real zoom=1, real angle=0, pair viewportshift=0, bool showtarget=true, bool autoadjust=true, bool center=autoadjust); Проектирование выполняется с учетом того, что обзор направлен от точки camera к точке target, причем, вектор up направлен вверх (рис. 14.5). Если render=0, проекция трехмерных кубических сплайнов Безье осуществляется с помощью их аппроксимации двумерными неоднородными рациональными B-сплайнами (NURBS), которые реализуют двумерную кривую Безье, содержащую дополнительные узлы и опорные точки. Если autoadjust=true, camera будет автоматически удерживаться за пределами ограничивающего объект параллелепипеда при любых интерактивных вращениях вокруг target. Если center=true, точка target будет располагаться в центре ограничивающего параллелепипеда. Применение проекции perspective(real x, real y, real z, triple up=Z, triple target=O, real zoom=1, real angle=0, pair viewportshift=0, bool showtarget=true, bool autoadjust=true, bool center=autoadjust); Глава 14. Проекции, перспектива, освещение up = Z 159 up = X + Z Рис. 14.5. Перспектива. равносильно perspective((x,y,z),up,target,zoom,angle,viewportshift,showtarget, autoadjust,center); По умолчанию currentprojection=perspective(5,4,2). 14.5 Îñâåùåíèå ïîâåðõíîñòè Освещение трехмерных объектов регулируется с помощью структуры light: struct light{real[][] diffuse; real[][] ambient; real[][] specular; pen background=nullpen; specularfactor; bool viewport; triple[] position; transform3 T=identity(4); bool on() {return position.length > 0;} }; Здесь diffuse – отраженный от поверхности рассеянный свет; ambient – фоновое освещение, заполняющее все пространство; specular – зеркальное освещение, исходящее из определенной точки пространства и отражающееся от поверхности в определенном направлении; background – цвет фона трехмерной канвы; specularfactor – степень освещенности объекта; viewport отмечает, движется ли источник света вместе с движением камеры; position – координаты направленных источников света. В Asymptote имеются предопределенные типы освещений: Viewport, White, Headlamp и nolight (рис. 14.6). Последний тип самый простой – никакого освещения. Остальные три определяются следующим образом: light Viewport=light(ambient=gray(0.1), specularfactor=3, viewport=true, (0.25,-0.25,1)); light White=light(new pen[] {rgb(0.38,0.38,0.45),rgb(0.6,0.6,0.67), Глава 14. Проекции, перспектива, освещение 160 Headlamp White Viewport nolight light((0,0,3)); light((0,0,3),(0,0,-3)); Рис. 14.6. Виды освещений. rgb(0.5,0.5,0.57)}, specularfactor=3, new triple[] {(-2,-1.5,-0.5),(2,1.1,-2.5),(-0.5,0,2)}); light Headlamp=light(gray(0.8), ambient=gray(0.1), specular=gray(0.7), specularfactor=3, viewport=true, dir(42,48)); По умолчанию предполагается, что currentlight=Headlamp. На двух последних картинках рис. 14.6 показано, как можно определить собственные источники освещения, расположенные в заданных точках. Имеется возможность детальнее установить свойства освещения, хотя это требует более глубокого проникновения в суть вопроса и множества проб и ошибок! В большинстве случаев для рисования в пространстве используются те же перья, что и для рисования на плоскости. Впрочем, для особых случаев имеется расширение понятия пера под названием material. Такое перо несет в себе три сорта света, прозрачность и отблеск. В общем случае material определяется следующим образом: material material(pen diffusepen=black, pen ambientpen=black, pen emissivepen=black, pen specularpen=mediumgray, real opacity=opacity(diffusepen), real shininess=defaultshininess); 14.6 Ñîçäàíèå èíòåðàêòèâíîãî ðèñóíêà Asymptote имеет в своем арсенале средства, позволяющие снабдить рисунки анимацией или интерактивностью. Не вдаваясь в глубины этой технологии, рассмотрим лишь одну возможность – оснащение интерактивностью рисунка, занимающего отдельный pdf-файл. Для этого соответствующий asy-файл должен иметь следующее обрамление: import settings; outformat="pdf"; Глава 14. Проекции, перспектива, освещение 161 leftbutton=new string[] {"rotate","zoom","shift","pan"}; middlebutton=new string[] {"menu"}; rightbutton=new string[] {"zoom/menu","rotateX","rotateY","rotateZ"}; wheelup=new string[] {"zoomin"}; wheeldown=new string[] {"zoomout"}; . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Строку с точками следует заменить на строки, формирующие трехмерное изображение. В результате, вызвав полученный pdf-файл в Acrobat Reader и пройдя барьеры обеспечения безопасности, можно позволить себе повращать изображение мышкой, рассматривая его со всех строн, а также уменьшить или увеличить, чтобы оценить рисунок как в целом, так и в деталях. Ãëàâà 15 Ìîäóëü three 15.1 Êîíòóðû è ïîâåðõíîñòè Этот модуль расширяет конструкции плоской геометрии на трехмерное пространство. Команды guide и path становятся командами guide3 и path3, а, кроме того, появляется новая процедура surface, описывающая поверхности. Вместо координат типа pair теперь повсеместно используются тройки triple. Точки и метки изображаются в пространстве с помощью тех же самых операторов, dot и label, которыми пользуются для рисования на плоскости. Но вот стрелки становятся объемными и для их рисования существует специальная процедура Arrow3, о чем речь еще впереди. Имеются два предопределенных пути unitcircle3 и unitsquare3, которые представляют собой единичную окружность и единичный квадрат: path3 unitcircle3 = X..Y..-X..-Y..cycle; path3 unitsquare3 = O--X--X+Y--Y--cycle; На рис. примера 15.1 можно увидеть построение путей в пространстве. Ïðèìåð 15.1. Ïóòè â ïðîñòðàíñòâå. import three; size(6.5cm,0); draw(unitcircle3,heavygreen); draw(((-1,-1,0)--(1,-1,0)--(1,1,0)-(-1,1,0)--cycle),brown); path3 gg=(1,0,0)..(0,1,1)..(-1,0,0).. (0,-1,1)..cycle; draw(gg,blue); dot(gg,red); Модуль three поддерживает преобразования двумерных путей в трехмерные и наоборот: path3 path3(path p, triple plane(pair)=XYplane); path path(path3 p, pair P(triple)=xypart); Из замкнутого пути path3 можно получить поверхность, натянутую на этот путь, с помощью процедуры surface surface(path3 external, triple[] internal=new triple[], triple[] normals=new triple[], pen[] colors=new pen[], bool3 planar=default); 162 Глава 15. Модуль three 163 Чтобы нарисовать поверхность, следует применить одну из следующих процедур рисования: void draw(picture pic=currentpicture, surface s, int nu=1, int nv=1, material surfacepen=currentpen, pen meshpen=nullpen, light light=currentlight, light meshlight=light, string name="", render render=defaultrender); void draw(picture pic=currentpicture, surface s, int nu=1, int nv=1, material[] surfacepen, pen meshpen, light light=currentlight, light meshlight=light, string name="", render render=defaultrender); void draw(picture pic=currentpicture, surface s, int nu=1, int nv=1, material[] surfacepen, pen[] meshpen=nullpens, light light=currentlight, light meshlight=light, string name="", render render=defaultrender); Параметры nu и nv определяют число подобластей, необходимых при рисовании необязательных линий сетки на поверхности для получения ячеек Безье. Параметр meshpen означает перо, изображающее линии сетки; его толщину можно изменить с помощью функций thin() (тонкое перо) , thick() (толстое перо) или, просто добавив к перу толщину линии bp, умноженную на подходящий коэффициент. Необязательный параметр name используется в качестве префикса для именования ячеек дерева PRC-модели. В простейшем случае поверхность создается и рисуется, как в примере 15.2. Ïðèìåð 15.2. Êîíñòðóèðîâàíèå ïîâåðõíîñòè èç êðèâîé. import three; size(6.5cm,0); path3 g=(1,0,0)..(0,1,1)..(-1,0,0).. (0,-1,1)..cycle; draw(surface(g),yellow); draw(((-1,-1,0)--(1,-1,0)--(1,1,0)-(-1,1,0)--cycle),blue); dot(g,red); В примере 15.3 сетка на поверхности состоит из 5 линий по каждому направлению, причем рисуется коричневым утолщенным пером, в то время как поверхность изображается желтым цветом. В следующем примере 15.4 количество линий сетки вдоль одного из направлений увеличено в три раза и выбраны другие цвета для изображения сетки и поверхности. Один из вариантов оператора draw для изображения поверхностей (см. выше) позволяет применить несколько цветов для рисования как поверхности, так и сетки на ней. Пример 15.5 демонстрирует использование такой возможности. В Asymptote имеются предопределенные поверхности: единичная сфера – unitsphere, единичная полусфера – unithemisphere, единичный круг – unitdisk, единичный квадрат – unitplane, единичный куб – unitcube, единичный цилиндр – unitcylinder, единичный конус-поверхность – unitcone, единичный конус-тело – unitsolidcone. Все эти поверхности показаны на рис. 15.6-15.13. Следующий пример 15.14 показывает, как создается плоская область, которая закрашивается четырьмя цветами. Вообще, плоские поверхности Безье конструируются с помощью Глава 15. Модуль three 164 Ïðèìåð 15.3. Ñåòêà 5 õ 5 íà ïîâåðõíîñòè. import three; currentprojection=orthographic(3,1,1); size(6.5cm,0); path3 g=(1,0,0)..(0,1,1)..(-1,0,0)..(0,-1,1).. cycle; draw(surface(g),nu=5,nv=5,yellow,brown+0.4bp); draw(((-1,-1,0)--(1,-1,0)--(1,1,0)--(-1,1,0)-cycle),blue); dot(g,red); Ïðèìåð 15.4. Ñåòêà 5 õ 15. import three; currentprojection=orthographic(3,1,1); size(6.5cm,0); path3 g=(1,0,0)..(0,1,1)..(-1,0,0)..(0,-1,1).. cycle; draw(surface(g),nu=5,nv=15,orange,black+0.4bp); draw(((-1,-1,0)--(1,-1,0)--(1,1,0)--(-1,1,0)-cycle),blue); dot(g,red); Ïðèìåð 15.5. Èñïîëüçîâàíèå ìàññèâîâ öâåòîâ. import three; currentprojection=orthographic(3,1,1); size(6.5cm,0); pen[] p1={paleblue,darkblue}, p2={palered,red,brown,darkbrown}; path3 g=(1,0,0)..(0,1,1)..(-1,0,0)..(0,-1,1).. cycle; draw(surface(g),nu=10, nv=10, p1,p2+thick()); draw((-1,-1,0)--(1,-1,0)--(1,1,0)--(-1,1,0)-cycle,blue); dot(g,red); процедуры Ореста Шардта bezulate, которая осуществляет декомпозицию областей, ограниченных (в соответствии с правилом заполнения zerowinding) простыми контурами (пересекающимися лишь на границах) на подобласти, ограниченные контурами длины 4 или меньше. Более эффективной является процедура, предназначенная для изображения пространственных мозаик, скомпонованных из множества треугольников, в которой можно задать вершины треугольников и цвета, отнесенные к этим вершинам. void draw(picture pic=currentpicture, triple[] v, int[][] vi, triple[] n={}, int[][] ni={}, material m=currentpen, pen[] p={}, int[][] pi={}, light light=currentlight); Здесь массив v является перечнем всех вершин, а массив vi состоит из подмассивов длины 3, Глава 15. Модуль three 165 Ïðèìåð 15.6. Åäèíè÷íàÿ ñôåðà. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitsphere,blue); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.7. Åäèíè÷íàÿ ïîëóñôåðà. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unithemisphere,red); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.8. Åäèíè÷íûé êðóã. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitdisk,green); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.9. Åäèíè÷íûé êâàäðàò. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitplane,cyan); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); содержащих элементы v, соответствующие вершинам каждого треугольника. Подобным же образом необязательные аргументы n и ni содержат данные о нормалях, а p и pi – о перьях, привязанных к вершинам. Этот подход демонстрирует пример 15.15. Глава 15. Модуль three 166 Ïðèìåð 15.10. Åäèíè÷íûé êóá. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitcube,yellow); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.11. Åäèíè÷íûé öèëèíäð. import three; size(4.5cm,0); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitcylinder,magenta); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.12. Åäèíè÷íûé êîíóñ (ïîâåðõíîñòü). import three; size(4.5cm,0); currentprojection=perspective(5,4,-1.5); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitcone,surfacepen=material(white,emissivepen=royalblue)); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); Ïðèìåð 15.13. Åäèíè÷íûé êîíóñ (òåëî). import three; size(4.5cm,0); currentprojection=perspective(5,4,-1.5); pen mahogany=cmyk(0,0.85,0.87,0.35); draw(unitsolidcone,surfacepen=material(white, emissivepen=royalblue); draw(O--1.5X,L=Label("$x$",EndPoint,WSW,mahogany),orange); draw(O--1.3Y,L=Label("$y$",EndPoint,ESE,mahogany),orange); draw(O--1.2Z,L=Label("$z$",EndPoint,N,mahogany),orange); 15.2 Ñòðåëêè Трехмерные версии стрелок и черточек на кривых и прямых можно нарисовать с помощью спецификаторов None, Blank, BeginBar3, EndBar3 (равносильно Bar3), Bars3, BeginArrow3, MidArrow3, EndArrow3 (равносильно Arrow3), Arrows3, BeginArcArrow3, EndArcArrow3 (равносильно ArcArrow3), MidArcArrow3 и ArcArrows3. Для черточек допускаются необязатель- Глава 15. Модуль three 167 Ïðèìåð 15.14. Çàïîëíåíèå ïëîñêîé îáëàñòè ðàçëè÷íûìè öâåòàìè. import three; size(6cm,0); path3 g=X..Y..-X..-Y..cycle; draw(surface(g,new pen[] {red,green,blue,black}),nolight); Ïðèìåð 15.15. Òðèàíãóëÿöèÿ îáëàñòåé. import three; size(7.5cm); triple[] v={O,X,X+Y,Y}; triple[] n={Z,X}; int[][] vi={{0,1,2},{2,3,0}}; int[][] ni={{0,0,0},{1,1,1}}; pen[] p={red+opacity(0.5),green+opacity(0.5), blue+opacity(0.5),black+opacity(0.5)}; int[][] pi={{0,1,2},{2,3,0}}; draw(v,vi,n,ni,red); draw(v+0.7Z,vi,p,pi); ные аргументы (real size=0, triple dir=O). Если size=0, используется длина черточки по умолчанию; если dir=O, черточка изображается перпендикулярно пути и в соответствии с исходным направлением обзора. Предопределенные острия стрелок называются DefaultHead3, HookHead3 и TeXHead3. Используются также двумерные острия, ориентированные соответственно исходной точке обзора (или вектором normal): DefaultHead2(triple normal=O), HookHead2(triple normal=O), TeXHead2(triple normal=O). Перечисленные возможности представлены в примерах 15.16 и 15.17 Ïðèìåð 15.16. Âèäû ñòðåëîê è ÷åðòî÷åê. import three; size(0,5cm); currentprojection=obliqueX; defaultpen(0.22mm); draw((0,0,5)--(0,1.5,5),Arrow3(DefaultHead3,red)); draw((0,0,4)--(0,1.5,4),Arrow3(HookHead3)); draw((0,0,3)--(0,1.5,3),Arrow3(TeXHead3)); draw((0,0,2)--(0,1.5,2),Arrows3(red)); draw((0,0,1)--(0,1.5,1),BeginArrow3(red)); draw((0,0,0)--(0,1.5,0),MidArrow3(red)); draw((0,2,5)--(0,3.5,5),Arrow3(red)); draw((0,2,4)--(0,3.5,4),ArcArrow3(red)); draw((0,2,3)--(0,3.5,3),Bar3); draw((0,2,2)--(0,3.5,2),Bars3); draw((0,2,1)--(0,3.5,1),BeginBar3); draw((0,2,0)--(0,3.5,0),EndBar3); В модуле three определены также трехмерные поля Глава 15. Модуль three 168 Ïðèìåð 15.17. Ñòðåëêè íà êðèâûõ. import three; size(12cm,0); defaultrender.merge=true;defaultpen(0.53mm); currentprojection=perspective(24,4,12); currentlight=light(gray(0.5),specularfactor=3,viewport=false, (0.5,-0.5,-0.25),(0.5,0.5,0.25),(0.5,0.5,1),(-0.5,-0.5,-1)); path3 g1=(-0.2,0,0)..(0.3,0.1,0)..(0.3,0.5,0); draw(g1,blue,Arrows3(TeXHead3),currentlight); path3 g2=(-0.4,-0.3,0) ..(0.6,0.2,0) ..(0.6,1,0); draw(g2,green, ArcArrows3( HookHead3), currentlight); path3 g3=(-0.6,-0.6,0) ..(0.9,0.3,0) ..(0.9,1.5,0); draw(g3,red, Arrows3( DefaultHead3), currentlight); path3 g4=(-0.8,-0.9,0)..(1.2,0.4,0)..(1.2,2,0); draw(g4,blue,Arrows3(TeXHead2),currentlight); path3 g5=(-1,-1.2,0)..(1.5,0.5,0)..(1.5,2.5,0); draw(g5,green,ArcArrows3(HookHead2,NoFill),currentlight); path3 g6=(-1.2,-1.5,0)..(1.8,0.6,0)..(1.8,3,0); draw(g6,red,Arrows3(DefaultHead2(normal=Z)),currentlight); NoMargin3, BeginMargin3, EndMargin3, Margin3, Margins3, BeginPenMargin2, EndPenMargin2, PenMargin2, PenMargins2, BeginPenMargin3, EndPenMargin3, PenMargin3, PenMargins3, BeginDotMargin3, EndDotMargin3, DotMargin3, DotMargins3, Margin3 TrueMargin3. 15.3 Îêðóæíîñòè, äóãè, ïðÿìîóãîëüíèêè è áîêñû Кроме упомянутой ранее единичной окружности unitcircle3, Asymptote умеет строить окружность произвольного радиуса r с центром в точке c в плоскости, расположенной перпендикулярно вектору normal: path3 circle(triple c, real r, triple normal=Z); По умолчанию плоскость окружности перпендикулярна вектору Z. Пример 15.18 показывает применение этой процедуры для рисования в трехмерном пространстве. Глава 15. Модуль three 169 Ïðèìåð 15.18. Îêðóæíîñòè â ïðîñòðàíñòâå. import three; size(7.5cm,0); currentprojection=perspective( camera=(6.36,-0.68,2.026), up=(-0.0038,-0.0037,0.011), target=(-0.0022,-0.0017,0.0089), zoom=1, angle=12.64, autoadjust=false); dot(O); path3 c1=unitcircle3; draw(c1, blue); draw(surface(c1), purple+opacity(0.5)); path3 c2=circle(O,0.75,Z-Y); draw(c2,orange); draw(surface(c2), yellow+opacity(0.5)); path3 c2=circle(O,0.75,Y); draw(c2,orange); draw(surface(c2),yellow+opacity(0.5)); path3 c2=circle(O,0.75,Z+Y); draw(c2,orange); draw(surface(c2),yellow+opacity(0.5)); Дуга в пространстве определяется в сферической системе координат x = ρ cos ϕ sin θ, y = ρ sin ϕ sin θ, z = ρ cos θ, ρ ≥ 0, −π ≤ ϕ < π, 0 ≤ θ ≤ π, с помощью процедуры path3 arc(triple c, real r, real theta1, real phi1, real theta2, real phi2, triple normal=O); Здесь c – центр окружности дуги, r – ее радиус, причем дуга проводится против часовой стрелки от точки c+r*dir(theta1,phi1) к точке c+r*dir(theta2,phi2) относительно векторного произведения cross(dir(theta1,phi1),dir(theta2,phi2)), если theta2>=theta1 и phi2>=phi1. Угол theta откладывается от оси Oz, а угол phi – от оси Ox. Параметр normal обязательно указывается, если радиус-векторы точки c и концов дуги коллинеарны. Рис. примера 15.19 демонстрирует и сферическую систему координат, и пример применения процедуры arc. Следует обратить внимание на вычисление координат точки A и ее проекции на плоскость xOy в сферической системе координат. Дополнительно можно указать значения параметра CW (по часовой стрелке) или CCW (против часовой стрелке), см. пример 15.20. Можно также построить дугу с центром в точке c от заданной точки v1 до другой заданной точки v2, используя следующую модификацию процедуры arc: path3 arc(triple c, triple v1, triple v2, triple normal=O, bool direction=CCW); Предполагается выполненным условие |v1-c|=|v2-c|. Рис. примера 15.21 показывает такое построение дуги «от точки до точки». Часто удобно вычислять координаты точек с помощью выражения r*dir(phi,theta), что продемонстрировано в примере 15.22. Прямоугольник в пространстве создает процедура path3 plane(triple u, triple v, triple O=O); Глава 15. Модуль three 170 Ïðèìåð 15.19. Ïîñòðîåíèå äóã â ïðîñòðàíñòâå. import three; size(7.5cm,0); currentprojection=obliqueX; draw(O--2*X,Arrow3);label("$x$",2*X,NW); draw(O--2*Y,Arrow3);label("$y$",2*Y,SE); draw(O--2*Z,Arrow3);label("$z$",2*Z,E); real theta1=0, theta2=40; real phi1=0, phi2=60; path3 arc1=arc(O,1.5,theta1,phi2,theta2,phi2); draw(arc1,red,Arrow3); draw(arc(O,1.5,theta2,phi2,90,phi2), red+dotted); path3 arc2=arc(O,1,90,phi1,90,phi2); draw(arc2,blue,Arrow3); triple pA=(1.5*Cos(phi2)*Sin(theta2), 1.5*Sin(phi2)*Sin(theta2), 1.5*Cos(theta2)); triple projA=(1.5*Cos(phi2)*Cos(90-theta2), 1.5*Sin(phi2)*Cos(90-theta2),0); dot("$A$",pA,NE); dot(projA); draw(O--pA--projA--cycle, dashed); draw(projA--(1.5*Cos(phi2),1.5*Sin(phi2),0), red+dotted); label("$\varphi$",(Cos(phi2/2),Sin(phi2/2),0),SE); label("$\theta$",(1.5*Cos(phi2)*Sin(theta2/2),1.5*Sin(phi2)*Sin(theta2/2), 1.5*Cos(theta2/2)),N); Ïðèìåð 15.20. Ïîñòðîåíèå äóã ïî è ïðîòèâ ÷àñîâîé ñòðåëêè. import three; size(7.5cm,0); currentprojection=obliqueX; draw(O--2*X,Arrow3);label("$x$",2*X,NW); draw(O--2*Y,Arrow3);label("$y$",2*Y,SE); draw(O--Z,Arrow3);label("$z$",Z,E); path3 arc1=arc(O,1,90,0,90,60,CCW); draw(arc1,blue,Arrow3); path3 arc2=arc(O,1,90,0,90,60,CW); draw(arc2,red,Arrow3); draw(O--(cos(pi/3),sin(pi/3),0),dotted); Этот прямоугольник лежит в плоскости с нормалью cross(u,v) (т. о., u, v являются направляющими векторами плоскости) и представляет собой путь O--O+u--O+u+v--O+v--cycle, проходящий через начало координат O. В примере 15.23 с помощью таких прямоугольников создается параллелепипед. Превращая прямоугольники в плоские поверхности, то есть закрашивая их, можно получить кусочки плоскостей, как в примере 15.24. Трехмерный бокс с противоположными вершинами в точках v1 и v2 можно получить с помощью функции Глава 15. Модуль three Ïðèìåð 15.21. Ïîñòðîåíèå äóãè îò îäíîé òî÷êè äî äðóãîé. import three; size(5cm,0); currentprojection=oblique; draw(O--2*X,Arrow3,blue); triple pA, v1, v2; pA=(1,0,0); v1=(1,sqrt(2)/4,-sqrt(2)/4); v2=(1,sqrt(2)/4,sqrt(2)/4); path3 arc1=arc(pA,v1,v2,CW); draw(arc1,red,Arrow3); Ïðèìåð 15.22. Âû÷èñëåíèå êîîðäèíàò òî÷åê ñ ïîìîùüþ ïðîöåäóðû dir. import three; size(5.5cm,0); currentprojection=perspective(4,1,2); real radius=1, theta=37, phi=60; real r=1.2; draw(Label("$x$",1),O--0.9*X,Arrow3(HookHead3)); draw(Label("$y$",1),O--r*Y,Arrow3(HookHead3)); draw(Label("$z$",1),O--r*Z,Arrow3(HookHead3)); label("$\rm O$",(0,0,0),W); triple pQ=radius*dir(theta,phi); draw(O--radius*dir(90,phi)^^O--pQ,dashed); dot("$R*\mathrm{dir}\left(\theta,\phi\right)$",pQ); draw("$\theta$",reverse(arc(O,0.5*pQ,0.5*Z)),N+0.3E, Arrow3(HookHead2)); draw("$\varphi$",arc(O,0.5*X,0.5*(pQ.x,pQ.y,0)), N+0.3E,Arrow3(HookHead2)); path3 p3=O--arc(O,radius,0,phi,90,phi)--cycle; draw(surface(p3),lightblue+opacity(0.2)); Ïðèìåð 15.23. Ñîñòàâëåííûé èç ïðÿìîóãîëüíèêîâ ïàðàëëåëåïèïåä. import three; size(5.5cm); currentprojection=oblique; triple u,v,w; u=(1,1,1); v=(1,-1,1); w=cross(u,v); path3 pl1=plane(u,v,O); path3 pl2=plane(u,w,O); path3 pl3=plane(u,w,v); path3 pl4=plane(u,v,w); draw(pl1,red); draw(pl2,blue); draw(pl3,cyan); draw(pl4,green); path3[] box(triple v1, triple v2); В частности, предопределенный единичный бокс задается как path3[] unitbox=box(O,(1,1,1)); Практическое построение таких параллелепипедов показано в примере 15.25. 171 Глава 15. Модуль three 172 Ïðèìåð 15.24. Çàêðàøåííûå ïðÿìîóãîëüíèêè. import three; size(5.5cm,0); currentprojection=orthographic( camera=(0.011,0.0023,0.0023), up=(0.0058,0.0008,0.015), target=(0,0,0),zoom=1); path3 pl1=plane(Y,Z,O); path3 pl2=plane(Y,Z,X); path3 pl3=plane(X,Z,(X+Y)/2); draw(surface(pl1),green); draw(surface(pl2),blue+opacity(0.6)); draw(surface(pl3),red+opacity(0.6)); Ïðèìåð 15.25. Áîêñû. import three; size(5.5cm,0); draw(O--X,red+linewidth(0.7pt),Arrow3); draw(O--Y,red+linewidth(0.7pt),Arrow3); draw(O--Z,red+linewidth(0.7pt),Arrow3); label("$\mathbf{i}$",0.5*X,SE); label("$\mathbf{j}$",0.5*Y,SW); label("$\mathbf{k}$",0.5*Z,SW); draw(unitbox,red); draw(box(O,2*(X+Y+Z)),blue); 15.4 Òðåõìåðíûå ïðåîáðàçîâàíèÿ Asymptote предлагает для использования в построении рисунков несколько аффинных преобразований трехмерного пространства, относящихся к типу transform3. Тождественное преобразование обозначается identity(4). Преобразование сдвига, или перемещения, выполняется с помощью оператора transform3 shift(triple v) который перемещает геометрический объект на вектор v. В примере 15.26 показано перемещение как точек, так и сложного геометрического объекта (треугольника). Для масштабирования объекта применяется несколько операторов. Масштабирование по соответствующим координатным осям осуществляется с помощью операторов transform3 xscale3(real x) transform3 yscale3(real y) transform3 zscale3(real z) Первый из них масштабирует в x раз вдоль оси Ox, второй – в y раз вдоль оси Oy, а третий – в z раз вдоль оси Oz. Глава 15. Модуль three 173 Ïðèìåð 15.26. Îïåðàòîð ñäâèãà (ïåðåìåùåíèÿ). import three; size(6cm); currentprojection=obliqueX; triple u=(-1.5,0.75,0), v=(0,-0.5,-1); transform3 t1=shift(u); transform3 t2=shift(v); triple pA=(0,0,0), pB=(0,0,1), pC=(1,3,0); path3 g=pA--pB--pC--cycle, g1=t1*g, g2=t2*g; draw(g,heavygreen); draw(g1,red); draw(g2,blue); draw(pA--t1 * pA,red+dotted,Arrow3); draw(pB--t1 * pB,red+dotted,Arrow3); draw(pC--t1 * pC,red+dotted,Arrow3); draw(pA--t2 * pA,blue+dotted,Arrow3); draw(pB--t2 * pB,blue+dotted,Arrow3); draw(pC--t2 * pC,blue+dotted,Arrow3); Применение всех трех преобразований демонстрирует пример 15.27, в котором выполняется растяжение единичного цилиндра unitcylinder (изображенного в верхней части рис.) поочередно вдоль каждой из координатных осей. Ïðèìåð 15.27. Ïîî÷åðåäíîå ìàñøòàáèðîâàíèå âäîëü êîîðäèíàòíûõ îñåé. import three; size(7.5cm,0); currentprojection=obliqueX; draw(O--1.9*X,Arrow3);label("$x$",1.9*X,NW); draw(O--1.9*Y,Arrow3);label("$y$",1.9*Y,S); draw(O--2*Z,Arrow3);label("$z$",2*Z,NW); real k=3; draw(unitcylinder,blue+opacity(0.75)); draw(shift(0,-5,-3.5)*xscale3(k) *unitcylinder,brown); draw(shift(0,0,-3.5)*yscale3(k) *unitcylinder,yellow); draw(shift(0,4.5,-3.5)*zscale3(k) *unitcylinder,red); Оператор transform3 scale3(real s) производит масштабирование на одну и ту же величину s сразу вдоль всех координатных осей, а оператор transform3 scale(real x, y, z) масштабирует один и тот же объект вдоль каждой координатной оси на разные величины x, y и z. В примере 15.28 единичный диск с помощью оператора scale3 масштабируется 5 раз, постепенно увеличиваясь в размерах и синхронно с этим изменяя свой цвет и расположение в пространстве. Пример 15.29 показывает, как, масштабируя сферу, можно превратить ее в почти плоский круг, получив в результате изображение, напоминающее планету Сатурн. Следующим преобразованием является вращение. Его реализуют два оператора. Первый Глава 15. Модуль three 174 Ïðèìåð 15.28. Îäíîâðåìåííîå ìàñøòàáèðîâàíèå âî âñåì êîîðäèíàòàì íà îäíó è òó æå âåëè÷èíó. import three; size(4cm,0); currentprojection=perspective(1,1,5); for(real s=1; s<=5; ++s){ transform3 ag=scale3(s); draw(ag*shift((0,0,s))*unitdisk,red+opacity(1/s)); } Ïðèìåð 15.29. Ñæàòèå ñôåðû îïåðàòîðîì scale. import three; size(7.5cm,0); currentprojection=orthographic(3,1,1); transform3 ag=scale(1.5,2.5,0.25); draw(unitsphere,blue+opacity(0.3)); draw(ag*unitsphere,heavymagenta); transform3 rotate(real angle, triple v) поворачивает геометрический объект на угол angle (в градусах) вокруг вектора v. Второй transform3 rotate(real angle, triple u, triple v) выполняет поворот на угол angle (в градусах) вокруг оси, проходящей через точки u и v. На рис. примера 15.30 синий треугольник поворачивается на угол −90◦ вокруг оси абсцисс и становится зеленым. В примере 15.31 синий куб поворачивается на 180◦ вокруг оси, проходящей через точки (0, 1, 0) и (1, 1, 0), и становится желтым. Отражение, или преобразование симметрии, объекта относительно плоскости производится оператором transform3 reflect(triple u, triple v, triple w) где u, v, w – точки, принадлежащие плоскости. Пример 15.32 демонстрирует отражение полусферы от плоскости. Следующим преобразованием является проектирование объекта в направлении dir на плоскость, проходящую через точку O (это не обязательно начало координат) и имеющую нормаль n: transform3 planeproject(triple n, triple O=O, triple dir=n); Если нормаль не известна, ее можно вычислить с помощью процедуры Глава 15. Модуль three Ïðèìåð 15.30. Ïîâîðîò òðåóãîëüíèêà âîêðóã îñè Ox. import three; size(5.5cm,0); currentprojection=orthographic(3,1,2); pen dotteddash=linetype("0 4 4 4"), p2=.8bp+blue+dotted; transform3 r=rotate(-90,X); triple pA=(1,0,1), pB=(4,0,1), pC=(1,0,4); path3 tri=pA--pB--pC--cycle; path3 trip=r*tri; draw(tri,blue); draw(trip,heavygreen); draw(O--X,Arrow3);label("$x$",1.3*X,NW); draw(O--Y,Arrow3);label("$y$",Y,SE); draw(O--Z,Arrow3);label("$z$",1.2*Z,E); draw((-1,0,0)--(4,0,0),red+dotteddash); draw(arc((pA.x,0,0),pA,r*pA,CCW),p2,Arrow3(2.5mm)); draw(arc((pB.x,0,0),pB,r*pB,CCW),p2,Arrow3(2.5mm)); draw(arc((pC.x,0,0),pC,r*pC,CCW),p2,Arrow3(2.5mm)); Ïðèìåð 15.31. Ïîâîðîò êóáà âîêðóã íåêîîðäèíàòíîé îñè. import three; size(5.5cm,0); currentprojection=obliqueX; currentlight=(3,2,6); pen dotteddash=linetype("0 4 4 4"); triple point1=(0,1,0), point2=(1,1,0); transform3 r=rotate(180,point1,point2); draw(unitcube,royalblue); draw(r*unitcube,yellow); draw((-0.5,1,0)--(1.5,1,0),dotteddash); Ïðèìåð 15.32. Ïðåîáðàçîâàíèå ñèììåòðèè. import three; size(7.5cm,0); currentprojection=orthographic(4,4,2); currentlight=(4,2,4); triple pA=(-3,0,0), pB=(0,3,0), pC=(0,0,3); transform3 sym=reflect(pA,pB,pC); path3 tr=pA--pB--pC--cycle; draw(surface(tr),opacity(0.2)); draw(unithemisphere,blue+opacity(0.5)); draw(sym*unithemisphere,red+opacity(0.5)); triple pS=(0,0,1), pE=(0,1,0); draw(pS--sym*pS,dashed); draw(pE--sym*pE,dashed); dot((pS+sym*pS)/2); dot((pE+sym*pE)/2); 175 Глава 15. Модуль three 176 triple normal(path3 p); получив в результате единичную нормаль для плоского пути p. Видимо, чтобы избавить от подобных расчетов, существует еще один вариант оператора проектирования, который дает проекцию в направлении dir на плоскость, которую определяет принадлежащий ей путь p: transform3 planeproject(path3 p, triple dir=normal(p)); В примере 15.33 красная кривая проектируется на все три координатные плоскости. Вначале единичный квадрат помещается на одну из координатных плоскостей с помощью трех из шести преобразований XY, YZ, ZX, YX, ZY, ZX, которые, вообще говоря, любой объект, в частности, метку, помещают на соответствующую координатную плоскость. Этот квадрат и есть тот путь, который определяет плоскость проектирования и на которую проектируется красная кривая. Естественно, как и всякое преобразование, преобразование planeproject «умножается» на эту кривую p. Ïðèìåð 15.33. Ïðîåêòèðîâàíèå íà êîîðäèíàòíûå ïëîñêîñòè. import three; size3(6cm,IgnoreAspect); currentprojection=orthographic(4,6,2); path3 p=(1,2,0)..(0,1,1)..(1,0,0)..(2,1,1)..cycle; draw(p,bp+red); draw(planeproject(XY*unitsquare3)*p,bp+brown); draw(planeproject(YZ*unitsquare3)*p,bp+heavygreen); draw(planeproject(ZX*unitsquare3)*p,bp+blue); draw(Label("$x$",2),O--2.1*X,Arrow3); draw(Label("$y$",2),O--2.2*Y,Arrow3); draw(Label("$z$",1),O--1.1*Z,Arrow3); Еще одно преобразование, которое можно назвать инвертированием, устанавливает связь между точками некоторой плоскости и точками трехмерного пространства, координаты которых вычисляются в соответствии с текущей косоугольной системой координат. Чаще всего в качестве упомянутой плоскости выступает плоскость экрана компьютера, нормаль к которой направлена к пользователю (наблюдателю). Это бывает необходимо для того, чтобы перевести экранные координаты точек в координаты точек, имитирующих изображение в текущей трехмерной проекции. На рис. примера 15.34 изображен равносторонний треугольник, вершины √которого √ в экранной, двумерной, системе координат задаются парами (0, 0), (3, 0), (1.5, 1.5 3), 1.5 3 ≈ 2.6. Проектируя вершины (синие штриховые линии) на оси трехмерной системы координат, получаем координаты вершин треугольника в этой системе координат. Такие вычисления и выполняет оператор invert. Следует обратить внимание на вывод координат двух вершин непосредственно на рис. Процедуры surface extrude(path p, triple axis=Z); surface extrude(Label L, triple axis=Z); создают поверхности в результате вытягивания пути p типа path (но не path3!) или метки L вдоль оси axis. Глава 15. Модуль three 177 Ïðèìåð 15.34. Ñâÿçü ìåæäó ýêðàííûìè è òðåõìåðíûìè êîîðäèíàòàìè. import three; size(7cm); currentprojection=orthographic(5,5,5); triple pO=(0,0,0), pA=invert((0,0),Z,pO), pB=invert((3,0),Z,pO), pC=invert((1.5,1.5*sqrt(3)),Z,pO); draw(pA--pB--pC--cycle,red); void affichercoordonnees(triple pM,align d){ label(format("(%.3f,",pM.x)+ format("%.3f,",pM.y)+ format("%.3f)",pM.z),pM,d);} affichercoordonnees(pB,E); affichercoordonnees(pC,NNE); draw(Label("$x$",4),-4.5X--2X,Arrow3); draw(Label("$y$",2),-2.5Y--3Y,Arrow3); draw(Label("$z$",4),O--5Z,Arrow3); void montrecoord(triple P, pen stylo){ draw((P.x,0,0)--P--(0,P.y,0),stylo);} montrecoord(pB,blue+dashed); montrecoord(pC,blue+dashed); На рисунке примера 15.35 одна окружность, сдвигаясь вдоль координатной оси Oz вычерчивает желтый цилиндр, а вторая, перемещаясь вдоль оси 0.5Y+1.7*Z, вырастает в красный цилиндр. Ïðèìåð 15.35. Âûòÿãèâàíèå îêðóæíîñòåé âäîëü çàäàííûõ îñåé. import three; currentprojection=obliqueX; size(5cm,0); path g=(1,0)..(0,1)..(-1,0)..(0,-1)..cycle; surface s=extrude(g); draw(s,yellow); path g2=scale(0.5)*g; surface s2=extrude(g2,0.5Y+1.7*Z); draw(s2,red); Второй пример, 15.36, на эту тему демонстрирует, что чертить в пространстве можно не только кривыми, но и буквами, в результате чего они становятся объемными, вырастая на требуемую величину в требуемом направлении. Глава 15. Модуль three Ïðèìåð 15.36. Ïðåâðàùåíèå ïëîñêèõ áóêâ â îáúåìíûå. import three; size(6.5cm,0); currentprojection=orthographic(-2,-2,3); currentlight=light((0,0,3),(0,-3,3)); Label L=Label("\textsc{Asymptote}",(0,0)); Label M=Label("\small is my tool", (0,0)); surface s=extrude(L,3*Z); surface t=shift((4,-6,-8))*rotate(45,X) *extrude(M,1.5Z); draw(s,blue); draw(t,red); 178 Ãëàâà 16 Ìîäóëü graph3 Модуль graph3 дает возможность изображать трехмерные системы координат и поверхности, являющиеся графиками функций. 16.1 Îñè êîîðäèíàò Все три координатные оси рисует процедура void axes3(picture pic=currentpicture, Label xlabel="", Label ylabel="", Label zlabel="", bool extend=false, triple min=(-infinity,-infinity,-infinity), triple max=(infinity,infinity,infinity), pen p=currentpen, arrowbar3 arrow=None); так что в простейшем случае они изображаются, как в примере 16.1, а в более «художественном» исполнении – как в примере 16.2. На последнем рис. следует обратить внимание на выравнивание названий осей. Для оси Ox использована конструкция dir(X-O), а для осей Oy и Oz – выражения dir(O--Y) и dir(O--Z), соответственно. Результат выполнения выравниваний одинаков: название оси располагается в направлении ее продолжения, вблизи ее конца. Вообще для направления, задаваемого вектором AB, выражение dir(A--B) обеспечивает изображение метки у точки B, а выражение dir(A-B) – у точки A. Ïðèìåð 16.1. Ïðîñòåéøåå ïðèìåíåíèå êîìàíäû axes3. import graph3; size(5.5cm,0); currentprojection=orthographic(1,1,1); axes3(min=(0,0,0),max=(1.5,1.5,1.5)); Если есть необходимость каждую ось изобразить или разметить специальным образом, то для ее рисования следует применить отдельный оператор. Например, чтобы нарисовать ось Ox, надо обратиться к процедуре 179 Глава 16. Модуль graph3 180 Ïðèìåð 16.2. Òðè îñè ñðàçó. import graph3; size(5.5cm,0); currentprojection=orthographic(1,1,1); pen mahogany=cmyk(0,0.85,0.87,0.35); axes3(xlabel=Label("$x$",dir(X-O),mahogany), ylabel=Label("$y$",dir(O--Y),mahogany), zlabel=Label("$z$",dir(O--Z),mahogany), min=(0,0,0),max=(1.5,1.5,1.5),orange, Arrow3(mahogany)); label("$O$",O,1.5*WNW,mahogany); void xaxis3(picture pic=currentpicture, Label L="", axis axis=YZZero, real xmin=-infinity, real xmax=infinity, pen p=currentpen, ticks3 ticks=NoTicks3, arrowbar3 arrow=None, bool above=false); Аналогичные процедуры yaxis3 и zaxis3 рисуют оси ординат и аппликат в трехмерном пространстве. Результат отдельного рисования координатных осей см. в примере 16.3. Ïðèìåð 16.3. Èíäèâèäóàëüíîå èçîáðàæåíèå îñåé. import graph3; size(5.5cm,0); currentprojection=orthographic(1,1,1); xaxis3("$x$",xmin=0,xmax=1.5,lightblue,Arrow3(blue)); yaxis3("$y$",ymin=0,ymax=1.5,lightred,Arrow3(red)); zaxis3("$z$",zmin=0,zmax=1.5,green, Arrow3(heavygreen)); label("$O$",O,1.5*WNW,brown); Аргумент axis в процедуре xaxis3 по умолчанию имеет значение YZZero, что означает расположение оси Ox на прямой y = 0, z = 0. Аналогичным образом для осей Oy и Oz используются значения XZZero и XYZero, соответственно. Чтобы указать другое расположение координатных осей, следует применить процедуры axis YZEquals(real y, real z, triple align=O, bool extend=false); axis XZEquals(real x, real z, triple align=O, bool extend=false); axis XYEquals(real x, real y, triple align=O, bool extend=false); Параметр ticks, как и в двумерном случае, принимает значения NoTicks3, InTicks, OutTicks и InOutTicks. Параметры xmin и xmax задавать не обязательно, Asymptote определяет их автоматически, исходя из размеров изображения. В примере 16.4 начало координат перенесено в точку (−2; −2; 0); метки на осях размещены различным образом; использованы отметки разных типов. Еще один вариант изображения осей предоставляют процедуры Глава 16. Модуль graph3 181 Ïðèìåð 16.4. Ìîäèôèêàöèè òðåõìåðíûõ îñåé. import graph3; size(5.5cm,0); currentprojection=orthographic(1,1,1); defaultpen(fontsize(10pt)); limits((-2,-2,0),(2,2,2)); xaxis3(Label("$x$", position=MidPoint, align=N),YZEquals(-2,0), InOutTicks()); yaxis3(Label("$y$", position=EndPoint, align=E),XZEquals(-2,0), OutTicks()); zaxis3("$z$",XYEquals(-2,-2), InTicks()); path3 g=(2,-2,0)--(-2,-2,0)--(-2,2,0)--(2,2,0)--cycle; draw(g,red); axis YZZero(triple align=O, bool extend=false); axis XZZero(triple align=O, bool extend=false); axis XYZero(triple align=O, bool extend=false); Необязательный параметр align применяется для изменения принятого по умолчанию выравнивания оси, черточек на ней и расположения названия оси. Впрочем, эти процедуры используются редко, так как являются частными случаями процедур YZEquals и т. д. Пример 16.5 демонстрирует задание логарифмического масштаба на оси аппликат. Ïðèìåð 16.5. Ëîãàðèôìè÷åñêèé ìàñøòàá. import graph3; size(0,200); size3(200,IgnoreAspect); currentprojection=perspective(5,2,2); scale(Linear,Linear,Log); xaxis3("$x$",0,1,OutTicks(2,2)); yaxis3("$y$",0,1,OutTicks(2,2)); zaxis3("$z$",1,30,OutTicks(beginlabel=false)); Если axis=Bounds, вместо осей координат Asymptote выводит ограничивающий рисунок параллелепипед (пример 16.6). Множитель типа XZ(), стоящий перед меткой-названием оси, определяет, в какой координатной плоскости лежит данная метка. Вообще-то bounds является процедурой вида axis Bounds(int type=Both, int type2=Both, triple align=O, bool extend=false); Каждый из параметров type и type2 принимает одно из трех значений: Min, Max или Both. Эти параметры определяют, какие из четырех возможных ребер трехмерного ограничивающего параллелепипеда будут нарисованы. На рис. примера 16.7 ребра красного цвета, параллельные оси Ox, занумерованы. Нетрудно заметить, что эти ребра определяются граничными Глава 16. Модуль graph3 182 Ïðèìåð 16.6. Îãðàíè÷èâàþùèé ïàðàëëåëåïèïåä. import graph3; size(5.5cm,0); currentprojection=orthographic(4,6,4); draw((1,0,0)..(0.5,0.5,0.8)..(0,1,1),red); xaxis3(XZ()*"$x$",Bounds,blue,InTicks); yaxis3(YZ()*"$y$",Bounds,blue, InTicks(beginlabel=false,Label,2,2)); zaxis3(XZ()*"$z$",Bounds,blue,InTicks); значениями координат x и z параллелепипеда. Поэтому ребру 1 отвечает набор параметров (Min,Min), то есть x → min, z → min. (Min,Both) означает, что x → min, а z берется и min, и max. Поэтому (Min,Both) выбирает ребра 1 и 2. Отметим также, что по умолчанию (Min,Both)=(Min). Ребра 2 и 4 выбираются сочетанием (Both,Max). Еще учтем, что (Both,Both)=(Both)=() оставляет все четыре ребра. В результате получаем таблицу 16.1. Таблица 16.1. x y Ребра Min Min 1 Min Max 2 Max Min 3 Max Max 4 Min Both 1, 2 Max Both 3, 4 Both Min 1, 3 Both Max 2, 4 Both Both 1, 2, 3, 4 Ïðèìåð 16.7. Ê îïðåäåëåíèþ ïàðàìåòðîâ bounds. import graph3; size(5.5cm); currentprojection=orthographic(4,6,3); limits((0,-2,0),(2,2,2)); xaxis3("$x$",Bounds(),red); yaxis3("$y$",Bounds(),blue); zaxis3("$z$",Bounds(),green); dot(O,L=Label("$O$"),NE); label("2",(1,-2,2),NW,red); label("1",(1,-2,0),NW,red); label("4",(1,2,2),NW,red); label("3",(1,2,0),NW,red); Рисунок 16.8 демонстрирует выбор ребер ограничивающего бокса. Как уже отмечалось, в трехмерном случае для рисования отметок используются опции NoTicks3, InTicks, OutTicks и InOutTicks. Они задают направление черточек для осей вида Bounds; другие типы осей наследуют направления, которые соответствуют направлениям Bounds(Min,Min). Как и в двумерном случае, можно разметить и «кривую» ось, заданную в виде некоторого пути (пример 16.9). Глава 16. Модуль graph3 183 Ïðèìåð 16.8. Âûáîð ðåáåð îãðàíè÷èâàþùåãî áîêñà. import graph3; size(5.5cm); limits((0,0,0),(10,10,6)); currentprojection=perspective(camera=(20,16,7)); xaxis3(Label("$x$",MidPoint,align=Y-Z), Bounds(Both,Min),OutTicks(endlabel=false),p=blue); yaxis3(Label("$y$",MidPoint,align=X-Z), Bounds(Both,Min),OutTicks(),p=red); zaxis3(Label("$z$",MidPoint,align=X-Y), Bounds(Min,Min),InTicks(),p=1bp+.5green); Ïðèìåð 16.9. Êîîðäèíàòíàÿ ðàçìåòêà êðèâîé. import graph3; size(5.5cm); path3 g= X..2Y..-X..-2Y..cycle;; currentprojection=perspective(10,10,10); axis(Label("C",position=0,align=15X),g, InTicks(endlabel=false,8,end=false), ticklocate(0,360,new real(real v) {path3 h=O--max(abs(max(g)),abs(min(g)))*dir(90,v); return intersect(g,h)[0];}, new triple(real t) {return cross(dir(g,t),Z);})); 16.2 Êîîðäèíàòíûå ñåòêè è ìîäóëü grid3 Если к работе подключить модуль grid3, рисунок можно снабдить трехмерной координатной сеткой. Построение такой сетки выполняет процедура grid3: void grid3(picture pic=currentpicture, grid3routinetype gridroutine=XYZgrid(real pos=Relative(0)), int N=0, int n=0, real Step=0, real step=0, bool begin=true, blbool end=true, pen pGrid=grey, pen pgrid=lightgrey, bool above=false); Большинство параметров нам известно из двумерного аналога, пояснения требует только использование процедуры gridroutine. Она имеет следующие ипостаси: • XYgrid создает на плоскости xOy координатную сетку, линии которой параллельны Oy; • YXgrid создает на плоскости xOy сетку, линии которой параллельны Ox; и т. д. • XYXgrid создает пару сеток: XYgrid и YXgrid; • YXYgrid делает то же самое; • ZXZgrid создает пару сеток: ZXgrid и XZgrid; и т. д.; Глава 16. Модуль graph3 184 • YX_YZgrid создает сетки YXgrid и YZgrid; • XY_XZgrid создает сетки XYgrid и XZgrid; • ZX_ZYgrid создает сетки ZXgrid и ZYgrid; • XYZgrid создает XYXgrid, ZYZgrid и XZXgrid. В первом примере 16.10 на эту тему показано, как строятся координатные сетки в плоскостях xOy и xOz в направлении осей Oy и Oz, соответственно. Ïðèìåð 16.10. Êîîðäèíàòíàÿ ñåòêà â xOy â íàïðàâëåíèè Oy è â xOz â íàïðàâëåíèè Oz. import grid3; size(7.5cm); limits((0,0,0),(5,5,3)); currentprojection= perspective(camera=(20,16,7)); xaxis3(Label("$x$",MidPoint,align=Y-Z), Bounds(Both,Min), OutTicks(endlabel=false),p=blue); yaxis3(Label("$y$",MidPoint,align=X-Z), Bounds(Both,Min),OutTicks(),p=red); zaxis3(Label("$z$",MidPoint,align=X-Y), Bounds(Both,Min), InTicks(),p=1bp+.5green); grid3(XYgrid); grid3(XZgrid); Того же эффекта можно было добиться, использовав вместо двух операторов grid3 один: grid3(XY_XZgrid). В следующем примере 16.11 на плоскости xOy реализуется направление линий сетки, перпендикулярное предыдущему. Ïðèìåð 16.11. Êîîðäèíàòíàÿ ñåòêà â ïëîñêîñòÿõ xOy è yOz. import grid3; size(7.5cm); limits((0,0,0),(5,5,3)); currentprojection= perspective(camera=(20,16,7)); xaxis3(Label("$x$",MidPoint,align=Y-Z), Bounds(Both,Min), OutTicks(endlabel=false),p=blue); yaxis3(Label("$y$",MidPoint,align=X-Z), Bounds(Both,Min),OutTicks(),p=red); zaxis3(Label("$z$",MidPoint,align=X-Y), Bounds(Both,Min), InTicks(),p=1bp+.5green); grid3(YXgrid); grid3(YZgrid); Построение двойной координатной сетки в плоскости xOy показано в примере 16.12 Рисунок примера 16.13 демонстрирует создание двойных сеток на трех координатных плоскостях. Естественно, что допускается использование логарифмического масштабирования (пример 16.14). Глава 16. Модуль graph3 Ïðèìåð 16.12. Äâîéíàÿ êîîðäèíàòíàÿ ñåòêà. import grid3; size(7.5cm); limits((0,0,0),(5,5,3)); currentprojection= perspective(camera=(20,16,7)); xaxis3(Label("$x$",MidPoint,align=Y-Z), Bounds(Both,Min), OutTicks(endlabel=false),p=blue); yaxis3(Label("$y$",MidPoint,align=X-Z), Bounds(Both,Min),OutTicks(),p=red); zaxis3(Label("$z$",MidPoint,align=X-Y), Bounds(Both,Min), InTicks(),p=1bp+.5green); grid3(XYXgrid); Ïðèìåð 16.13. Òðè äâîéíûå êîîðäèíàòíûå ñåòêè. import grid3; size(7.5cm); limits((0,0,0),(10,10,6)); currentprojection= perspective(camera=(20,16,7)); grid3(XYZgrid,Step=2); xaxis3(Label("$x$",MidPoint,align=Y-Z), Bounds(Both,Min), OutTicks(endlabel=false),p=blue); yaxis3(Label("$y$",MidPoint,align=X-Z), Bounds(Both,Min),OutTicks(),p=red); zaxis3(Label("$z$",MidPoint,align=X-Y), Bounds(Both,Min), InTicks(),p=1bp+.5green); Ïðèìåð 16.14. Ëîãàðèôìè÷åñêîå ìàñøòàáèðîâàíèå. import grid3; size(6cm); currentprojection=orthographic(1,1,1); scale(Log,Linear,Log); limits((1,0,1),(300,3,300)); grid3(XYZgrid); xaxis3("$x$",OutTicks()); yaxis3("$y$",OutTicks(NoZero)); zaxis3("$z$",InOutTicks()); 185 Глава 16. Модуль graph3 186 Параметр Relative(real x=0) определяет положение сетки по отношению к перпендикулярной к ней координатной оси. Значениям Relative(1), Relative(0.5) и Relative(0) отвечают, соответственно, pos=top, pos=middle и pos=bottom. Примеры 16.15 и 16.16 показывают, как влияет этот параметр на изменение положения сетки, связанной с плоскостью xOz. Следует также обратить внимание на совместное использование координатной сетки и традиционных координатных осей. Ïðèìåð 16.15. Ïðèíÿòîå ïî óìîë÷àíèþ çíà÷åíèÿ ïàðàìåòðà Relative. import graph3; import grid3; size(6cm); currentprojection=obliqueX; limits((0,-2,0),(4,6,4)); grid3(XYXgrid,Step=1,step=0); grid3(ZYZgrid,Step=1,step=0); grid3(XZXgrid,Step=1,step=0); axes3("$x$","$y$","$z$", red+linewidth(1pt),Arrow3(3mm)); Ïðèìåð 16.16. Relative(0.25). import graph3; import grid3; size(6cm); currentprojection=obliqueX; limits((0,-2,0),(4,6,4)); grid3(XYXgrid,Step=1,step=0); grid3(ZYZgrid,Step=1,step=0); grid3(XZXgrid(Relative(0.25)),Step=1,step=0); axes3("$x$","$y$","$z$", red+linewidth(1pt),Arrow3(3mm)); Следующий пример 16.16 дополняет предыдущий добавлением отметок на координатных осях. До сих пор координатная сетка присоединялась к рисунку с помощью процедуры grid3, но и в процедурах, изображающих координатные оси, можно указать на необходимость вычерчивания сетки. Это продемонстрировано в примере 16.18. 16.3 Ãðàôèêè ôóíêöèé êàê ïîâåðõíîñòè 16.3.1 Äåêàðòîâà ñèñòåìà êîîðäèíàò Поверхности могут задаваться аналитически как графики функций. В Asymptote такие поверхности создаются с помощью процедур surface surface(real f(pair z), pair a, pair b, int nx=nmesh, int ny=nx, bool cond(pair z)=null); surface surface(real f(pair z), pair a, pair b, int nx=nmesh, int ny=nx, splinetype xsplinetype, splinetype ysplinetype=xsplinetype, bool cond(pair z)=null); Глава 16. Модуль graph3 187 Ïðèìåð 16.17. Ïîëíîå êîîðäèíàòíîå ñíàðÿæåíèå. import graph3; import grid3; size(6cm); currentprojection=obliqueX; limits((0,-2,0),(4,6,4)); grid3(XYXgrid,Step=1,step=0); grid3(ZYZgrid,Step=1,step=0); grid3(XZXgrid,Step=1,step=0); xaxis3(Label("$x$",position=EndPoint,align=W), 0,4.5,red+linewidth(1pt),OutTicks(NoZero, beginlabel=false,endlabel=false,Step=2,step=1, end=false),Arrow3(3mm)); yaxis3(Label("$y$",position=EndPoint,align=N), -2,6.5,red+linewidth(1pt),OutTicks(NoZero, beginlabel=true,endlabel=false,Step=2,step=1, end=false),Arrow3(3mm)); zaxis3(Label("$z$",position=EndPoint,align=E), -2,4.5,red+linewidth(1pt),OutTicks(NoZero, beginlabel=false,endlabel=false,Step=2,step=1,end=false),Arrow3(3mm)); dot("$O$",O,SE); Ïðèìåð 16.18. Çàäàíèå ñåòîê â ïðîöåäóðàõ äëÿ îñåé. import grid3; size(6cm); currentprojection=orthographic(0.25,1,0.25); limits((-2,-2,0),(0,2,2)); real Step=1, step=0.5; xaxis3(Label("$x$",position=EndPoint,align=Z), YZEquals(-2,0),InOutTicks(Label(align=0.5*(Z-Y)), Step=Step,step=step,gridroutine=XYgrid,pGrid=red, pgrid=0.5red)); yaxis3(Label("$y$",position=EndPoint,align=Z), XZEquals(-2,0),InOutTicks(Label( align=-0.5*(X-Z)),Step=Step,step=step, gridroutine=YXgrid,pGrid=red,pgrid=0.5red)); zaxis3("$z$",XYEquals(-1,0),OutTicks(Label( align=-0.5*(X+Y)))); Поверхность определяется функцией f(z), областью задания которой является прямоугольник с противоположными вершинами a и b. Параметры nx и ny регулируют размеры ячеек Безье, формирующих поверхность. Чем больше значения этих параметров, тем качественнее изображение. По умолчанию nx=ny=nmesh=10. Для сглаживания сплайнами надо указать Spline. В примере 16.19 поверхность задается в виде функции z = −0,3(x2 + y 2 ), а количество ячеек Безье взято принятым по умолчанию. Из рис. видно, что получилась довольно грубая аппроксимация поверхности. Увеличив число ячеек Безье, получим лучшую аппроксимацию, пример 16.20. Еще более высокое качество обеспечивает применение сплайнов, реализованное в примере 16.21. Размеры pdf-файлов для изображений в этих трех примерах увеличивались следующим образом: 25, 50, 76 Кб. Условие cond, используемое в процедурах surface, служит для задания области определения функции. В предыдущих примерах выбиралась стандартная форма области – прямоугольник. Чтобы построить поверхность не над прямоугольником, а, например, над кругом, Глава 16. Модуль graph3 188 Ïðèìåð 16.19. Èçîáðàæåíèå ïîâåðõíîñòè z = −0.3(x2 + y2 ) ïðè nx=ny=10. import graph3; currentprojection=orthographic(2,3,4); currentlight=light(1,0,7); size(6.5cm,0); real f(pair z){ return -0.3(z.x^2+z.y^2); } surface s=surface(f,(-2,-2),(2,2)); draw(s,royalblue); Ïðèìåð 16.20. Èçîáðàæåíèå ïîâåðõíîñòè z = −0.3(x2 + y2 ) ïðè nx=ny=25. import graph3; currentprojection=orthographic(2,3,4); currentlight=light(1,0,7); size(6.5cm,0); real f(pair z){ return -0.3(z.x^2+z.y^2); } surface s=surface(f,(-2,-2),(2,2), nx=25,ny=25); draw(s,royalblue); Ïðèìåð 16.21. Ïðèìåíåíèå ñïëàéíîâ. import graph3; currentprojection=orthographic(2,3,4); currentlight=light(1,0,7); size(6.5cm,0); real f(pair z){ return -0.3(z.x^2+z.y^2); } surface s=surface(f,(-2,-2),(2,2),Spline); draw(s,royalblue); необходимо написать соответствующую булевскую функцию cond. Это и выполнено в примере 16.22. Правда, при малых значениях nx и ny край поверхности представляет собой весьма зубчатую линию. Поэтому пришлось взять nx=ny=400, чтобы зубчатость уменьшилась, но полностью устранить это явление не удалось, так как дальнейшее увеличение nx и ny привело к нехватке компьютерной памяти. При этом объем файла изображения составил 108 Кб. К более качественным результатам приводит использование подходящей системы координат Глава 16. Модуль graph3 189 (цилиндрической или сферической), о которых речь пойдет впереди (пример 16.25). Ïðèìåð 16.22. Èñïîëüçîâàíèå óñëîâèÿ cond. import graph3; currentprojection=orthographic(2,3,4); currentlight=light(1,0,7); size(6.5cm,0); real f(pair z){ return -(z.x^2+z.y^2);} surface s=surface(f,(-2,-2),(2,2),400,Spline, cond=new bool(pair z){ return z.x^2+z.y^2<=1 ? true : false;}); draw(s,heavygreen); В некоторых вариантах процедуры draw можно выполнить нанесение сетки на поверхность и задать освещение. Так, в примере 16.23 освещение отключено, а эффект трехмерности достигается нанесением на поверхность сетки. В примере же 16.24 используется освещение по Ïðèìåð 16.23. Ñåòêà è îòñóòñòâèå îñâåùåíèÿ. import graph3; size(7cm,0); currentprojection=perspective(2,2,2); real a=2; real f(pair z) {return exp(-abs(z)^2)+0.25;} surface s=surface(f,(-a,-a),(a,a),nx=25); path3 pl=plane(2*(a,0,0),2*(0,a,0),(-a,-a,0)); draw(surface(pl),gray); draw(s,lightgray, meshpen=black+thick(), nolight); xaxis3("",0,a+0.5,red,Arrow3); yaxis3("",0,a+0.5,red,Arrow3); zaxis3("",0,1.75,red,Arrow3); умолчанию. В результате на поверхности появляется блик от источника света. В обоих примерах аргументом функции является комплексная величина z. 16.3.2 Ïàðàìåòðè÷åñêîå çàäàíèå ïîâåðõíîñòè Следующие варианты процедур surface позволяют создавать графики параметрически заданных поверхностей: surface surface(triple f(pair z), pair a, pair b, int nu=nmesh, int nv=nu, bool cond(pair z)=null); surface surface(triple f(pair z), pair a, pair b, int nu=nmesh, int nv=nu, splinetype[] usplinetype, splinetype[] vsplinetype=Spline, bool cond(pair z)=null); surface surface(triple f(pair z), real[] u, real[] v, Глава 16. Модуль graph3 190 Ïðèìåð 16.24. Ñåòêà è îñâåùåíèå ïî óìîë÷àíèþ. import graph3; size(7cm,0); currentprojection=perspective(2,2,2); real a=2; real f(pair z) {return exp(-abs(z)^2)+0.25;} surface s=surface(f,(-a,-a),(a,a),nx=25); path3 pl=plane(2*(a,0,0),2*(0,a,0),(-a,-a,0)); draw(surface(pl),gray); draw(s,lightyellow, meshpen=brown+thick()); xaxis3("",0,a+0.5,red,Arrow3); yaxis3("",0,a+0.5,red,Arrow3); zaxis3("",0,1.75,red,Arrow3); splinetype[] usplinetype, splinetype[] vsplinetype=Spline, bool cond(pair z)=null); Теперь функция f зависит от пары z, компоненты которой считаются параметрами поверхности. Значением функции является тройка (вектор) декартовых координат точки в пространстве. Нарисуем таким образом однополостный гиперболоид, x2 y 2 z 2 + 2 − 2 = 1, a2 b c который можно определить в виде двупараметрической поверхности x = a ch u cos v, y = b ch u sin v, z = c cos v, −∞ < u < ∞, −π < v ≤ π. Результат приведен в примере 16.25. Ïðèìåð 16.25. Îäíîïîëîñòíûé ãèïåðáîëîèä êàê äâóïàðàìåòðè÷åñêàÿ ïîâåðõíîñòü. import graph3; currentprojection=perspective(2,2,2.8); size(6cm,0); real a=1.5; triple f(pair z){ return (cosh(z.x)*cos(z.y),cosh(z.x)*sin(z.y), sinh(z.x)); } surface s=surface(f,(-a,-pi),(a,pi),Spline); draw(s,lightred,meshpen=blue+thick()); Глава 16. Модуль graph3 16.3.3 191 Öèëèíäðè÷åñêàÿ ñèñòåìà êîîðäèíàò Если поверхность задана в одной из традиционных систем координат, то ее уравнение следует подходящим образом преобразовать. Изобразим, например, комбинацию из двух поверхностей: эллиптического параболоида z = 6 − x2 − y 2 и конуса x2 + y 2 − z 2 = 0, для чего перейдем к цилиндрической системе координат по формулам x = ρ cos ϕ, y = ρ sin ϕ, z = z. Параболоид в этой системе координат запишется как z = 6 − ρ2 , а конус – как z = ρ. Поскольку выбранные поверхности пересекаются при ρ = 2, то пределы изменения координат будут такими: 0 ≤ ϕ < 2π, 0 ≤ ρ ≤ 2. Выбирая для программирования в качестве параметров ϕ и ρ, получим искомое изображение, представленное на рис. примера 16.26. Ïðèìåð 16.26. Ïîâåðõíîñòü â öèëèíäðè÷åñêîé ñèñòåìå êîîðäèíàò. import graph3; currentprojection=orthographic(3,1,1); currentlight=Viewport; size(4cm,0); triple f1(pair z) {return (z.x*cos(z.y),z.x*sin(z.y), 6-z.x^2);} surface s1=surface(f1,(0,0),(2,2pi),Spline); draw(s1,yellow); triple f2(pair z) {return (z.x*cos(z.y),z.x*sin(z.y),z.x);} surface s2=surface(f2,(0,0),(2,2pi),Spline); draw(s2,green); 16.3.4 Ñôåðè÷åñêàÿ ñèñòåìà êîîðäèíàò Перейдем теперь к сферической системе координат x = ρ cos ϕ sin θ, y = ρ sin ϕ sin θ, z = ρ cos θ и построим в ней поверхность, заданную уравнением (x2 + y 2 + z 2 )2 = 3xyz. В сферической системе координат уравнение примет вид ρ= 3 sin 2ϕ sin 2θ sin θ. 4 Эту функцию ρ = ρ(ϕ, θ) запрограммируем отдельно и подставим в уравнения для координат x, y и z, так что в результате останется требуемых два параметра: ϕ и θ. Рис. примера 16.27 демонстрирует полученный результат. 16.3.5 Ïðîçðà÷íîñòü è çà÷åð÷èâàíèå Использование полупрозрачных поверхностей позволяет визуализировать детали их пересечений и невидимые части. Таким образом получено изображение пересечение цилиндра с полусферой в примере 16.28. Аналогичным образом выполняется построение поверхностей с самопересечением. Так, в примере 16.29 демонстрируется изображение полупрозрачной бутылки Клейна. Довольно эффективным средством рисования поверхностей является зачерчивание их отрезками прямых или дугами кривых. Изобразим, например, поверхность образованную параболическим цилиндром y = x2 , который ограничивают плоскости z = 0 и z = 3−y, см. пример Глава 16. Модуль graph3 192 Ïðèìåð 16.27. Ïîâåðõíîñòü â ñôåðè÷åñêîé ñèñòåìå êîîðäèíàò. import graph3; currentprojection=orthographic(3,3,3); currentlight=Viewport; size(6cm,0); real r(pair z){ return (3*sin(2*z.x)*sin(2*z.y)*sin(z.y)/4);} triple f(pair z) {return (r(z)*cos(z.x)*sin(z.y),r(z)*sin(z.x)*sin(z.y), r(z)*cos(z.y));} surface s=surface(f,(0,0),(2pi,pi),Spline); draw(s,purple); Ïðèìåð 16.28. Ïåðåñå÷åíèå ïîëóïðîçðà÷íîé ïîëóñôåðû ñ öèëèíäðîì. import graph3; currentprojection=orthographic(3,-3,3); currentlight=Viewport; size(6cm,0); draw(scale3(1.1)*unithemisphere,green+opacity(0.7)); triple f(pair z) {return (0.5*(1+cos(z.x)),0.5*sin(z.x),z.y);} surface s=surface(f,(0,0),(2pi,1.3),50,Spline); draw(s,y); draw(s,Yellow); 16.30. Чтобы нарисовать требуемую часть параболического цилиндра, заметим, что она образуется при движении вертикального отрезка с концами A(x, x2 , 0) и B(x, x2 , 3 − x2 ) вдоль направляющей y = x2 , z = 0. Таким образом, чтобы получить все точки такого отрезка, надо ввести параметр t ∈ [0; 1], и тогда отрезок представится тройкой (x, x2 , t(3 − x2 )). Движение отрезка обеспечивается изменением переменной x, а перемещение точки вдоль отрезка – изменением параметра t. В процедуре f параметр t обозначен z.x. Верхняя часть поверхности реализуется натягиванием плоской поверхности на контур так, как это уже рассматривалось ранее. Следует обратить внимание на то, как контуры поверхности получаются из процедуры f, определяющей поверхность, с помощью процедур f0 и f1. Более сложный пример зачерчивания поверхности, заданной графиком функции, приведен в Приложении B. Другой пример для поверхности, определяемой сплайнами, представлен в Приложении C. 16.3.6 Ïîñòðîåíèå ïîâåðõíîñòåé ïî òî÷êàì Если поверхности заданы точками в трехмерном пространстве, их изображения создаются специальными модификациями процедуры surface для этого случая: surface surface(real[][] f, real[] x, real[] y, splinetype xsplinetype=null, Глава 16. Модуль graph3 193 Ïðèìåð 16.29. Áóòûëêà Êëåéíà. settings.render=4; import graph3; size(5cm,0); currentprojection=orthographic((2,2,2)); currentlight=(3,2,1); path center_path=(0,0)---(0,3)..(1.5,3)..(0,0.3) ---(0,0); real bottomradius=0.6; real topradius=0.1; path radius_graph=(0,bottomradius) {up}::(0.3,1.2*bottomradius)---(0.6,1.2*bottomradius) ::(1.2,topradius)---(2,topradius) ::{up}(3,bottomradius); radius_graph=xscale(1/3)*radius_graph; radius_graph=(shift(-1,0)*radius_graph) &radius_graph&(shift(1,0)*radius_graph); real radius(real t){ return point(radius_graph, times(radius_graph,t)[0]).y;} triple F(pair w){ real t=w.x % 2.0; bool reverse=(t>=1.0); t %= 1.0; real relt=reltime(center_path,t); real theta=w.y; triple center=YZplane(point(center_path,relt)); pair tangent=dir(center_path,relt); if (reverse) tangent *= -1; triple normal=X; triple binormal=cross(YZplane(tangent),normal); triple v=normal*cos(theta)+binormal*sin(theta); return center+radius(t)*v;} surface kleinbottle=surface(F,(0.5,0),(1.5,2pi), nu=32,nv=16,Spline); draw(kleinbottle,blue+0.5*white+opacity(0.7)); splinetype ysplinetype=xsplinetype, bool[][] cond={}); surface surface(real[][] f, pair a, pair b, bool[][] cond={}); surface surface(real[][] f, pair a, pair b, splinetype xsplinetype, splinetype ysplinetype=xsplinetype, bool[][] cond={}); surface surface(triple[][] f, bool[][] cond={}); Первая процедура в качестве аргументов принимает массив real[][] f, элементы которого f[i][j] представляют собой значения функции f в точках (x[i],y[i]). Координаты последних также являются аргументами процедуры real[] x и real[] y. На рис. примера 16.31 показан эллиптический параболоид, построенный по точкам этой процедурой. Отметим, что поверхность проходит через заданные точки (x[i],y[i],f[i][j]) и представляет собой сглаженную мозаику криволинейных плиток-поверхностей. Вторая процедура в качестве входных параметров имеет диаметрально противоположные вершины pair a и pair b прямоугольника на плоскости xOy и заданные на прямоугольнике значения функции f. Предполагается, что эти значения вычислены в узлах равномерной сетки, покрывающей прямоугольник. Процедура сама строит по входным данным массивы real[] x и real[] y, используемые в предыдущей процедуре, которую затем и запускает. Вообще говоря, поверхность будет представлять собой несглаженную мозаику плиток, пример 16.32. Впрочем, если указать в аргументах процедуры Spline, получим гладкую поверхность, но это уже будет применение третьей процедуры из рассматриваемого списка процедур. Входным параметром последней процедуры является массив triple[][] f, элементы ко- Глава 16. Модуль graph3 194 Ïðèìåð 16.30. Çà÷åð÷èâàíèå ïîâåðõíîñòè îòðåçêîì. import graph3; size(7cm,0); currentprojection=orthographic((10,6,5)); real a=sqrt(3), b=1, dy=.5; triple f(pair p){ real x=p.x; real y=p.x^2; real z=p.y*(a^2-p.x^2); return (x,y,z);} triple f0(real t) {return f((t,0));} triple f1(real t) {return f((t,1));} surface s=surface(f,(-a,0),(a,1),100); path3 p0=graph(f0,-a,a,operator ..), p1=graph(f1,-a,a,operator ..); draw(s,yellow+opacity(.6)); draw(p0--cycle^^p1^^(0,a^2,0)); draw(surface(p1--cycle),yellow+opacity(.6)); draw((1,1,0)--(1,1,2),white+bp); limits((-.5,-.5,-.1),(a,a^2+.5,a^2+.5)); axes3("$x$","$y$","$z$",brown,Arrow3); label("$A$",(1,1,0),SSW); label("$B$",(1,1,2),NE); draw((-0.6,0.7,2.3)--(-1,0.7,2.7)); label("$z=3-y$",(-1,0.7,2.7),N); draw((0.5,0.25,1.7)--(0.3,-0.3,1.8)); label("$y=x^2$",(0.3,-0.3,1.8),NNW); Ïðèìåð 16.31. Ïîñòðîåíèå ïîâåðõíîñòè ïî òî÷êàì. import graph3; size(6cm,0); currentprojection=orthographic(1.5,-3,0.5); currentlight=light((0,0,3),(2,-2,1)); real[] x=new real[21]; real[] y=new real[21]; real[][] f=new real[21][21]; for (int i=0; i<21; ++i){ x[i]=-1+0.1*i;} for (int j=0; j<21; ++j){ y[j]=-1+0.1*j;} for (int i=0; i<21; ++i){ for (int j=0; j<21; ++j){ f[i][j]=2-x[i]^2-y[j]^2;}} surface s=surface(f,x,y); draw(s,blue); торого представляют собой точки поверхности, уже не сопровождаемые дополнительными массивами абсцисс и ординат или ограничивающим эти координаты прямоугольником. Поскольку точки образуют массив, который, естественно, двумерен, они должны размещаться на поверхности определенным образом. Требуется, чтобы они располагались на поверхности ряд за рядом, так же, как они располагаются в матрице – строка за строкой. Допускается, чтобы некоторые соседние точки были одинаковы: число точек в строке матрицы должно быть одним и тем же, но иногда требуется, чтобы точек в ряду было меньше, например, когда две соседние точки сливаются в одну. Глава 16. Модуль graph3 195 Ïðèìåð 16.32. Êóñî÷íî ãëàäêàÿ ïîâåðõíîñòü. import graph3; size(6cm,0); currentprojection=orthographic(1.5,-3,0.5); currentlight=light((0,0,3),(2,-2,1)); real[] x=new real[21]; real[] y=new real[21]; real[][] f=new real[21][21]; for (int i=0; i<21; ++i){ x[i]=-1+0.1*i;} for (int j=0; j<21; ++j){ y[j]=-1+0.1*j;} for (int i=0; i<21; ++i){ for (int j=0; j<21; ++j){ f[i][j]=2-x[i]^2-y[j]^2;}} surface s=surface(f,x,y); draw(s,blue); Получаемая поверхность будет состоять из плоских плиток, образующих пространственную мозаику, см. пример 16.33. Красными точками на рисунке показана нулевая строка точек массива f, голубыми точками – первая строка, желтыми – вторая и зелеными – третья. Возможно, именно такую поверхность и требуется построить. Другим применением подобной конструкции может быть создание подготовительного каркаса для получения гладкой поверхности (именно так проектируются корпуса автомобилей, судов и самолетов). Такая поверхность носит название NURBS (non-uniform rational B-spline, или неоднородный рациональный B-сплайн) и конструируется на основе того же массива f. Правда, в этом случае требуется задать еще два массива, real[] uknot и real[] vknot. Не вдаваясь в детали, отметим, что число элементов первого массива должно быть равно числу столбцов массива f плюс четыре, а число элементов второго – числу строк f плюс четыре. В простейшем случае первые элементы массивов должны быть нулями, а последние – единицами, причем при нечетном числе элементов в массиве в его середине должно стоять число 0.5. На рис. примера 16.34 показана построенная таким образом поверхность. Видно, что она фиксируется в своих углах, а через остальные точки массива f не проходит, но как бы притягивается к ним. Притяжение можно усилить или ослабить, если ввести массив весов точек weights. Пусть в рассматриваемом примере он определяется следующим образом: real[][] weights=array(f.length,array(f[0].length,1.0)); weights[0][2]=5; Эта запись означает, что массив weights имеет структуру массива f, его элементы равны единице, кроме элемента, соответствующего точке (3,1,-1), равного пяти. Поэтому притяжение этой точки будет в пять раз сильнее, если оператор draw теперь сделать таким: draw(f,uknot,vknot,weights,blue); Более тонкая регулировка формы поверхности требует определенных знаний в области мозаик Безье в пространстве и понимания используемых в Asymptote алгоритмов. 16.3.7 Ïðîåöèðîâàíèå, ïðîöåäóðà lift В некоторых случаях для специального вида изображений может быть полезна процедура Глава 16. Модуль graph3 196 Ïðèìåð 16.33. Ìîçàèêà èç ïëîñêèõ ïëèòîê. import graph3; size(6cm,0); currentprojection=orthographic(5,5,4); light((3,3,4)); triple[][] f= {{(3,-2,1),(3,-1,1),(3,1,-1),(3,2,-1),(3,3,-1)}, {(1,-2,1),(1,-1,1),(1,1,-1),(1,2,-1), (1,3,-1)}, {(0,-2,2),(0,-1,2),(-1,1,1),(-1,2,1), (-1,3,1)}, {(-3,-2,2),(-3,-1,2),(-3,1,1),(-3,2,1), (-3,3,1)}}; surface s=surface(f); draw(s,blue); for (int i=0; i<5; ++i){ dot(f[0][i],red); dot(f[1][i],cyan); dot(f[2][i],yellow); dot(f[3][i],green);} draw(f[0][1]--f[1][1]--f[2][1]--f[3][1],white); draw(f[0][2]--f[1][2]--f[2][2]--f[3][2],white); draw(f[0][3]--f[1][3]--f[2][3]--f[3][3],white); draw(f[0][0]--f[0][1]--f[0][2]--f[0][3]--f[0][4],red); draw(f[1][0]--f[1][1]--f[1][2]--f[1][3]--f[1][4],cyan); draw(f[2][0]--f[2][1]--f[2][2]--f[2][3]--f[2][4],yellow); draw(f[3][0]--f[3][1]--f[3][2]--f[3][3]--f[3][4],green); xaxis3(XZ()*"$x$",Bounds,InTicks); yaxis3(YZ()*"$y$",Bounds,InTicks(beginlabel=false,Label,2,2)); zaxis3(XZ()*"$z$",Bounds,InTicks); guide3[][] lift(real f(real x, real y), guide[][] g, interpolate3 join=operator --); которая проецирует (поднимает) двумерные кривые g, расположенные в плоскости xOy, на поверхность, которая задается функцией f. В примере 16.35 пурпурный отрезок и зеленый прямоугольник на плоскости xOy проецируются на поверхность в соответствующие фигуры. В следующем примере 16.36 показано, как использовать процедуру lift для триангуляции поверхности (эллиптического параболоида). В сочетании с уже рассмотренной выше процедурой contour процедура lift позволяет изобразить на поверхности линии ее сечения плоскостями, параллельными плоскости xOy, см. пример 16.37. 16.3.8 Èçîáðàæåíèå âåêòîðíûõ ïîëåé Векторное поле, нарисованное nu× nv стрелками на поверхности, расположенной над прямоугольником box(a,b), принадлежащим плоскости xOy, и определяемой параметрически заданной функцией f, может быть получено с помощью процедуры, picture vectorfield(path3 vector(pair v), triple f(pair z), pair a, pair b, int nu=nmesh, int nv=nu, bool truesize=false, real maxlength=truesize ? 0 : maxlength(f,a,b,nu,nv), Глава 16. Модуль graph3 197 Ïðèìåð 16.34. Ïîâåðõíîñòü NURBS. import graph3; size(6cm,0); currentprojection=orthographic(5,5,4); light((3,3,4)); real[] uknot={0,0,0,0,1,1,1,1}; real[] vknot={0,0,0,0,0.5,1,1,1,1}; triple[][] f= {{(3,-2,1),(3,-1,1),(3,1,-1),(3,2,-1),(3,3,-1)}, {(1,-2,1),(1,-1,1),(1,1,-1),(1,2,-1), (1,3,-1)}, {(0,-2,2),(0,-1,2),(-1,1,1),(-1,2,1), (-1,3,1)}, {(-3,-2,2),(-3,-1,2),(-3,1,1),(-3,2,1), (-3,3,1)}}; draw(f,uknot,vknot,blue); for (int i=0; i<5; ++i){ dot(f[0][i],red); dot(f[1][i],cyan); dot(f[2][i],yellow); dot(f[3][i],green); } draw(f[0][1]--f[1][1]--f[2][1]--f[3][1],white); draw(f[0][2]--f[1][2]--f[2][2]--f[3][2],white); draw(f[0][3]--f[1][3]--f[2][3]--f[3][3],white); draw(f[0][0]--f[0][1]--f[0][2]--f[0][3]--f[0][4],red); draw(f[1][0]--f[1][1]--f[1][2]--f[1][3]--f[1][4],cyan); draw(f[2][0]--f[2][1]--f[2][2]--f[2][3]--f[2][4],yellow); draw(f[3][0]--f[3][1]--f[3][2]--f[3][3]--f[3][4],green); xaxis3(XZ()*"$x$",Bounds,InTicks); yaxis3(YZ()*"$y$",Bounds,InTicks(beginlabel=false,Label,2,2)); zaxis3(XZ()*"$z$",Bounds,InTicks); Ïðèìåð 16.35. Ïðîåêòèðîâàíèå íà ïîâåðõíîñòü. import graph3; size(7cm,0); size3(7cm,IgnoreAspect); currentprojection=orthographic(-25,-25,600); limits((0,0,0),(10,10,300)); xaxis3(OutTicks(Step=2)); yaxis3(OutTicks(Step=2)); zaxis3(Bounds(Min,Max), InTicks(Step=100,Label(align=Y))); real f(pair z){ return 2z.x^2-z.x+z.y^2;} draw(surface(f,(0,0),(10,10),nx=10,Spline), lightgray,meshpen=black+0.2bp,nolight); guide[][] tabgui={{(6,1)--(9,1)--(9,4) --(6,4)--cycle},{(1,6)--(1,8)}}; draw(lift(f,tabgui),1bp+red); path[] p1=tabgui[0], p2=tabgui[1]; draw(path3(p1),1bp+.8green); draw(path3(p2),1bp+.8purple); bool cond(pair z)=null, pen p=currentpen, arrowbar3 arrow=Arrow3, margin3 margin=PenMargin3); Глава 16. Модуль graph3 198 Ïðèìåð 16.36. Òðèàíãóëÿöèÿ ïîâåðõíîñòè. import graph3; size(6cm,0); int np=100; pair[] points; real r() {return 1.2 * (rand()/randMax * 2-1);} for(int i=0; i < np; ++i) points.push((r(),r())); int[][] trn=triangulate(points); guide[][] mong; for(int i=0; i < trn.length; ++i){ guide[] gg={points[trn[i][0]]-points[trn[i][1]]--points[trn[i][2]]--cycle}; mong.push(gg);} real f(pair z){ return z.x^2+z.y^2;} draw( lift(f,mong),deepgreen+0.3bp); Ïðèìåð 16.37. Ëèíèè ñå÷åíèÿ ïîâåðõíîñòè. import graph3; import contour; size(6cm,0); currentprojection=perspective(2,2,2); limits((0,0,0),(1.5,1.5,2)); int n=3; real a=1; real f(pair z) {return ((z^n).x)/a^(n-1);} real[] lignesniveaux={-1.5,-1,-0.5,0,0.5,1,1.5}; draw(surface(f,(-a,-a),(a,a),nx=10,Spline),white, meshpen=.8bp+red,nolight); draw(lift(f,contour(f,(-a,-a),(a,a),lignesniveaux)), 1bp+blue); В примере 16.38 построено векторное поле градиента эллиптического параболоида как на самой поверхности, так и в виде проекции поля на плоскость xOy. 16.3.9 Ðàñêðàøèâàíèå ñ ïîìîùüþ ïàëèòð В трехмерном пространстве есть возможность применить процедуры модуля palette для раскрашивания поверхностей. Часто при этом используют функцию Gradient, создающую палитру перьев, и функцию map(triple direction), определяющую направление, в котором будет происходить смена цветов палитры при раскрашивании поверхности. На рис. примера 16.39 в качестве такого направления выбрана ось аппликат (указанием zpart). Результат не требует комментариев. Вместо непрерывного раскрашивания с помощью функции Gradient можно выполнить дискретное, задав для этой цели массив перьев pen[] pens=mean(palette(s.map(triple direction),pen[] palette)); Глава 16. Модуль graph3 199 Ïðèìåð 16.38. Èçîáðàæåíèå òðåõìåðíîãî âåêòîðíîãî ïîëÿ. import graph3; size(7cm,0); currentprojection=orthographic(1,-2,1); currentlight=(1,-1,0.5); real f(pair z){return abs(z)^2;} path3 gradient(pair z){ static real dx=sqrtEpsilon, dy=dx; return O--((f(z+dx)-f(z-dx))/2dx, (f(z+I*dy)-f(z-I*dy))/2dy,0);} pair a=(-1,-1); pair b=(1,1); draw(surface(f,a,b,Spline),gray+opacity(0.5)); triple F1(pair z) {return (z.x,z.y,f(z));} add(vectorfield(gradient,F1,a,b,red)); triple F2(pair z) {return (z.x,z.y,0);} add(vectorfield(gradient,F2,a,b,blue)); axes3("$x$","$y$","$z$",brown,Arrow3); Ïðèìåð 16.39. Ïðèìåíåíèå ôóíêöèé Gradient è map äëÿ ðàñêðàøèâàíèÿ ïîâåðõíîñòè. import graph3; import palette; size(4cm,0); currentprojection=orthographic((3,3,4.5)); triple f(pair w){ return (w.x*sin(w.y),w.x*cos(w.y),w.y/3);} limits((-.5,-.5,-.1),(5,5,5)); surface s=surface(f,(-1,0),(1,12),30,Spline); s.colors(palette(s.map(zpart), Gradient(red,green,blue))); draw(s); При этом можно использовать и предопределенные палитры. Так, в примерах 16.40 и 16.41 использована палитра Rainbow. В следующем примере 16.41 создается и раскрашивается не только сама поверхность, но и ее проекция на плоскость xOy. 16.3.10 Ðåëüåôíûå íàäïèñè íà ïîâåðõíîñòè Существует модификация процедуры surface, позволяющая делать надписи на поверхности вдоль видимых или невидимых линий сетки: surface surface(Label L, surface s, real uoffset, real voffset, real height=0, bool bottom=true, bool top=true); Размещение надписи на поверхности регулируется параметрами uoffset и voffset. Параметр height задает высоту букв, делая текст надписи рельефным. На рис. примера 16.43 над- Глава 16. Модуль graph3 200 Ïðèìåð 16.40. Ïðèìåíåíèå ïàëèòðû Rainbow â íàïðàâëåíèè îñè àïïëèêàò. import graph3; import palette; currentprojection=obliqueX; size(6cm,0); real f(pair z) {return -z.x^2-z.y^2;} surface s=surface(f,(-1,-1),(1,1),Spline); pen[] pens=mean(palette(s.map(zpart), Rainbow())); draw(s,pens); Ïðèìåð 16.41. Ïðèìåíåíèå ïàëèòðû Rainbow â íàïðàâëåíèè îñè àáñöèññ. import graph3; import palette; currentprojection=obliqueX; size(6cm,0); real f(pair z) {return -z.x^2-z.y^2;} surface s=surface(f,(-1,-1),(1,1),Spline); pen[] pens=mean(palette(s.map(xpart), Rainbow())); draw(s,pens); Ïðèìåð 16.42. Ðàñêðàøèâàíèå ïîâåðõíîñòè è åå ïðîåêöèè íà ïëîñêîñòü. import graph3; import palette; currentprojection=obliqueX; currentlight=light((1,1,3),(3,3,0)); size(6cm,0); real f(pair z) {return z.x^2+z.y^2;} surface s=surface(f,(-1,-1),(1,1),nx=50); pen[] pens=mean(palette(s.map(zpart), Gradient(red,yellow,brown))); draw(s,pens); path3 pl=plane(X,Y,O); draw(planeproject(pl)*s,pens,nolight); пись начинается с первой вертикальной линии сетки, которой соответствует uoffset=1, и со второй горизонтальной линии (voffset=2). Надпись рельефна, так как параметр height=0.1. Регулируя длину надписи с помощью преобразования xscale и т. п., поворачивая поверхность и изменяя количество линий сетки на ней, можно добиться требуемого расположения надписи и затем убрать с поверхности сетку, «закомментировав» ее (если в ней нет необходи- Глава 16. Модуль graph3 201 Ïðèìåð 16.43. Ðåëüåôíàÿ íàäïèñü íà ïîâåðõíîñòè. import graph3; size(7.5cm,0); currentprojection=orthographic(-0.3,-36,34); currentlight=(0,0,3); real f(pair z){return 0.4(z.x^2-z.y^2);} surface s=surface(f,(-1,-1),(1,1),nx=10); draw(s,blue, meshpen=black+thick()); draw(surface(xscale(3)*scale(0.25)* "$F(x,y,z)=0$",s,uoffset=1,voffset=2, height=0.1),yellow); мости). Рис. примера 16.44 был получен именно таким способом. Ïðèìåð 16.44. Ðåëüåôíàÿ íàäïèñü íà ñôåðå. import graph3; size(6.5cm,0); currentprojection=orthographic(2,5,2); triple f(pair z){ return (cos(z.x)*sin(z.y),sin(z.x)*sin(z.y) ,cos(z.y));} surface s=surface(f,(-pi,pi),(pi,0),12,Spline); draw(s,lightgreen); //,meshpen=yellow+thick()); draw(surface(xscale(1.7)*scale(0.15)* "$x^2+y^2+z^2=R^2$",s,uoffset=7, voffset=6,height=0.1),red); Выполняя надпись, можно использовать весь арсенал математических символов и формул, которые способен воспроизвести LATEX. В заключение в примере 16.45 приводится еще одно изображение бутылки Клейна с надписями, которые представляют собой формулы, описывающие эту поверхность: x = 3 cos u(1 + sin u) + (2 − cos u) cos u cos v, y = 8 sin u + (2 − cos u) sin u cos v, u ∈ [0, π] : z = (2 − cos u) sin v; x = 3 cos u(1 + sin u) − (2 − cos u) cos v, y = 8 sin u, u ∈ [π, 2π] : z = (2 − cos u) sin v. Глава 16. Модуль graph3 Ïðèìåð 16.45. Íàäïèñè íà áóòûëêå Êëåéíà. import graph3; size(8cm,0); currentprojection=perspective(camera=(25.1,-30.3,19.4),up=Z,target=(-0.6,0.7,-0.63), zoom=1,autoadjust=false); triple f(pair t){real u=t.x; real v=t.y; real r=2-cos(u); real x=3*cos(u)*(1+sin(u))+ r*cos(v)*(u<pi ? cos(u) : -1); real y=8*sin(u)+(u<pi ? r*sin(u)*cos(v) : 0); real z=r*sin(v); return (x,y,z);} surface s=surface(f,(0,0),(2pi,2pi), 8,8,Spline); draw(s,lightolive+white); string lo="$\displaystyle u\in[0,\pi]: \cases{x=3\cos u(1+\sin u)+(2-\cos u)\cos u\cos v,\cr y=8\sin u+(2-\cos u)\sin u\cos v,\cr z=(2-\cos u)\sin v.\cr}$"; string hi="$\displaystyle u\in[\pi,2\pi]:\\\cases{x=3\cos u(1+\sin u)-(2-\cos u)\cos v,\cr y=8\sin u,\cr z=(2-\cos u)\sin v.\cr}$"; real h=0.0125; draw(surface(xscale(-0.38)*yscale(-0.18)*lo,s,0,1.7,h)); draw(surface(xscale(0.26)*yscale(0.1)*rotate(90)*hi,s,4.9,1.4,h)); 202 Ãëàâà 17 Ìîäóëè solids è contour3 В модуле solids определена структура revolution, предназначенная для рисования поверхностей вращения. Прежде всего следует сказать, что такие поверхности вращения как сфера, конус и цилиндр уже представлены в этом пакете соответствующими процедурами. Например, с помощью revolution cylinder(triple c=O, real r, real h, triple axis=Z); { } triple C=c+r*perp(axis); axis=h*unit(axis); return revolution(c,C--C+axis,axis); создается «прозрачный» цилиндр с центром в точке c, радиуса r, высоты h и осью axis (по умолчанию axis=Z). Этот цилиндр можно увидеть на рис. примера 17.1. Интересно, что невидимую линию основания Asymptote по собственному почину рисует штрихами. Ïðèìåð 17.1. ¾Ïðîçðà÷íûé¿ öèëèíäð. import solids; currentprojection=orthographic(5,4,2); size(4cm,0); triple pO=(0,0,0); draw(cylinder(pO,1,2),blue+0.7bp); dot(Label("c",E),pO); dot((0,0,2)); draw(pO--(0,0,2),lightblue+dashed); Чтобы раскрасить «прозрачный» цилиндр, достаточно натянуть на него поверхность, как это сделано в примере 17.2. Аналогично процедура revolution cone(triple c=O, real r, real h, triple axis=Z, int n=nslice); { 203 Глава 17. Модули solids и contour3 204 Ïðèìåð 17.2. Îêðàøåííûé öèëèíäð. import solids; size(6cm); currentprojection=orthographic(3,4,2); currentlight=Viewport; triple pO=(0,0,0); revolution cyl=cylinder(pO,1,2,Y+Z); draw(surface(cyl),orange); axes3("$x$","$y$","$z$",min=(0,0,0),max=(1.3,2,2), Arrow3); axis=unit(axis); return revolution(c,approach(c+r*perp(axis)--c+h*axis,n),axis); } формирует «прозрачный» конус с центром в точке c, радиуса r, высоты h и осью axis (по умолчанию axis=Z). Его изображение приведено в примере 17.3, а окрашенный конус показан на рис. примера 17.4. Ïðèìåð 17.3. ¾Ïðîçðà÷íûé¿ êîíóñ. import solids; currentprojection=orthographic(5,4,2); size(4cm,0); triple pO=(0,0,0); draw(cylinder(pO,1,2),blue+0.7bp); dot(Label("c",W),pO); dot((0,0,2)); draw(pO--(0,0,2),lightblue+dashed); Сферу с центром в точке c радиуса r создает процедура revolution sphere(triple c=O, real r, int n=nslice); { return revolution(c,Arc(c,r,180,0,0,0,Y,n),Z); } Результат ее выполнения можно видеть на рис. примера 17.5. В определении последних двух процедур использовался параметр nslice. Его увеличение вообще говоря улучшает качество изображения. Можно сравнить изображение сферы на рис. примера 17.5, в котором было принято значение по умолчанию nslice=12, с ее вариантом на рис. примера 17.6, для которого было взято nslice=2. В последнем примере видны погрешности изображения в виде «ребер» сферы, как будто поверхность склеивали из отдельных частей. В общем случае поверхность вращения формируется процедурой Глава 17. Модули solids и contour3 205 Ïðèìåð 17.4. Îêðàøåííûé êîíóñ. import solids; size(5cm,0); currentprojection=orthographic(5,2,2); revolution CoRev=cone(O,1,2,axis=0.2*Y+2*Z,n=1); draw(surface(CoRev),blue+opacity(0.8)); axes3("$x$","$y$","$z$",min=(0,0,0), max=(1.5,1.35,2.15),Arrow3); Ïðèìåð 17.5. Îêðàøåííàÿ ñôåðà. import solids; size(5cm,0); currentprojection=orthographic(5,4,2); triple pO=(0,0,0); real a=2; revolution s=sphere(pO,a); draw(surface(s),springgreen); Ïðèìåð 17.6. Îêðàøåííàÿ ñôåðà ïðè n=2. import solids; size(5cm,0); currentprojection=orthographic(5,4,2); triple pO=(0,0,0); real a=2; revolution s=sphere(pO,a,n=2); draw(surface(s),springgreen); revolution(triple c, path3 g, triple axis, real angle1, real angle2); где g – кривая, которая, зачерчивая поверхность, вращается вокруг оси axis, проходящей через точку c, причем угол поворота изменяется (в плоскости, перпендикулярной оси axis) от угла angle1 до угла angle2. В примере 17.7 кривая совершает полный оборот, в примере 17.8 – неполный, в результате чего возникает разрез поверхности. Глава 17. Модули solids и contour3 206 Ïðèìåð 17.7. Ïîâåðõíîñòü âðàùåíèÿ â îáùåì ñëó÷àå. import solids; currentprojection = perspective(10,100,25); unitsize(2.5cm); triple pO=(0,0,0); path3 gene=(0,0.5,-2)..(0,0.5,-1)..(0,1,-0.5)..(0,1,0.5).. (0,0.5,1)..(0,0.5,2); revolution sur=revolution(pO,gene,Z,0,360); draw(surface(sur),red); shipout(bbox(0.01cm+invisible)); Ïðèìåð 17.8. Ïîâåðõíîñòü âðàùåíèÿ ñ ðàçðåçîì. import solids; currentprojection = perspective(10,100,25); unitsize(2.5cm); triple pO=(0,0,0); path3 gene=(0,0.5,-2)..(0,0.5,-1)..(0,1,-0.5)..(0,1,0.5).. (0,0.5,1)..(0,0.5,2); revolution sur=revolution(pO,gene,Z,120,360); draw(surface(sur),magenta); shipout(bbox(0.01cm+invisible)); Имеется также специальная процедура рисования draw, позволяющая получить каркас поверхности в виде линий типа transverse (полученных сечениями, перпендикулярными оси вращения) и линий типа longitudenal (полученных сечениями, параллельными этой оси). Заголовок процедуры: void draw(picture pic=currentpicture, revolution r, int m=0, int n=nslice, pen frontpen=currentpen, pen backpen=frontpen, pen longitudinalpen=frontpen, pen longitudinalbackpen=backpen, light light=currentlight, string name="", render render=defaultrender, projection P=currentprojection); Здесь m – количество линий поперечного сечения. Для таких сечений можно определить перья Глава 17. Модули solids и contour3 207 frontpen и backpen и longitudinalpen и longitudinalbackpen – для продольных сечений. На рис. примера 17.9 показан каркас сферы. Ïðèìåð 17.9. Êàðêàñ ñôåðû. import solids; currentprojection=perspective(10,100,25); size(6cm,0); real a=2.5; revolution r=sphere(O,a); draw(r,10, frontpen=blue+0.7bp, backpen=red+0.7bp, longitudinalpen=magenta+0.7bp, longitudinalbackpen=heavygreen+0.7bp); Пакет contour3 служит для изображения поверхностей, представляющих собой объекты нулевой размерности, описываемые функциями переменных x, y, z или матрицами вида real[][][]. Основная процедура модуля, с таким же названием, имеет следующий заголовок: vertex[][] contour3(real f(real, real, real), triple a, triple b, int nx=nmesh, int ny=nx, int nz=nx, projection P=currentprojection); С помощью данного модуля можно строить неявно заданные поверхности. Однако качество изображений оставляет желать лучшего. Доказательством служит пример 17.10, в котором была сделана попытка нарисовать тор. Значение nx=13 оказалось максимально возможным, дальнейшее его увеличение приводило к переполнению. Ïðèìåð 17.10. Èçîáðàæåíèå òîðà ñ ïîìîùüþ ïðîöåäóðû contour3. import contour3; currentprojection=orthographic(3,1,4); currentlight=Viewport; size(6cm,0); real f(real x,real y,real z){return (x^2+y^2+z^2+5)^2-36*(x^2+y^2);} surface s=surface(contour3(f,(-6,-6,-2),(6,6,2),13)); draw(s,blue); Ãëàâà 18 Ìîäóëè labelpath3 è tube Модуль labelpath3, как уже было сказано в разделе описания всех модулей, расширяет возможности модуля labelpath на трехмерное пространство, причем основная процедура называется почему-то не labelpath3, а по-прежнему – labelpath: surface labelpath(string s, path3 p, real angle=90, triple optional=O); Надпись в виде строки s выводится вдоль пути p. Угол angle регулирует поворот надписи вокруг пути. В примере 18.1 надпись в верхней части рис. выводится под углом −90◦ к пути. Вторая состоит из двух строк и выводится под углом 180◦ к плоскости, в которой лежит окружность. Ïðèìåð 18.1. Íàäïèñè âäîëü ïóòåé â ïðîñòðàíñòâå. import labelpath3; size(5cm,0); path3 g=(1,0,0)..(0,1,1)..(-1,0,0)..(0,-1,1).. cycle; path3 g2=shift(-Z)*reverse(unitcircle3); string txt1="\hbox{This is a test of \emph{curved} 3D labels in \textbf{Asymptote} (implemented with {\tt texpath}).}"; string txt2="This is a test of curved labels in Asymptote\\(implemented without the {\tt PSTricks pstextpath} macro)."; draw(surface(g),paleblue+opacity(0.5)); draw(labelpath(txt1, subpath(g,0,reltime(g,0.95)),angle=-90),orange); draw(g2,1bp+red); draw(labelpath(txt2,subpath(g2,0,3.9),angle=180, optional=rotate(-70,X)*Z)); На рис. примера 18.2 требовалось вывести надпись на плоскость z = 3 − y. Для этого был создан путь lab, который «почти принадлежал» этой плоскости, и вдоль него под углом 135◦ к нему была выведена надпись. Если бы путь принадлежал плоскости, он должен был бы иметь вид (0.6,1.8,1.2)--(-0.6,1.8,1.2), но тогда надпись была бы «утоплена» в плоскости в результате раскраски последней. Полупрозрачность поверхности дала бы возможность ее прочитать, но четкость была бы невысокой. Поэтому надпись приподнята над плоскостью z = 3 − y на 0.02. В примере 18.3 показан один из способов надписывания графика двумерной функции. Из процедуры f, производящей точки поверхности z = (x2 + y 2 )3/2 , была получена функция f1, 208 Глава 18. Модули labelpath3 и tube 209 Ïðèìåð 18.2. Íàäïèñü íà ïëîñêîñòè. import graph3; import labelpath3; import palette; size(5cm,0); currentprojection=orthographic((10,8,5)); currentlight=nolight; pen p3=rgb(.1,.1,.48); pen p4=rgb(.93,.55,.93); pen sface=bp+opacity(.6); real a=sqrt(3), b=1, dy=.5; triple f(pair p){ return (p.x,p.x^2,p.y*(a^2-p.x^2));} triple f0(real t) {return f((t,0));} triple f1(real t) {return f((t,1));} surface s=surface(f,(-a,0),(a,1),100); s.colors(palette(s.map(ypart),Gradient(sface+p3, sface+white,sface+p4))); path3 p0=graph(f0,-a,a,operator ..), p1=graph(f1,-a,a,operator ..); surface surtop=surface(p1--cycle); surtop.colors(palette(surtop.map(zpart), Gradient(sface+p4,sface+white,sface+p3))); draw(s); draw(surtop); draw(p0--cycle ^^ p1,blue); limits((-.5,-.5,-.1),(a,a^2+.5,a^2+.5)); path3 lab=(0.6,1.8,1.22)--(-0.6,1.8,1.22); draw(labelpath("$z=3-y$",lab,angle=135)); которая давала сечение части поверхности плоскостью, параллельной плоскости xOy. Получаемая таким образом кривая, лежащая немного выше изображения поверхности, не чертилась на рисунке, а служила тем путем, вдоль которого располагалась надпись. Модуль tube позволяет конструировать объемные кривые в виде трубок и других профилей. Трубки производит наиболее простой вариант процедуры tube tube(path3 g, real width, render render=defaultrender); Результатом ее выполнения является трубка диаметром width, для которой осью можно приближенно считать кривую g. Структурно трубка, скажем, t состоит из поверхности s и оси center типа path3, к которым можно обращатся как к t.s и t.center, соответственно. На рис. примера 18.4 можно видеть созданную таким образом трубку, причем трубка названа T, ее поверхность T.s раскрашена при помощи палитры BWRainbow, а ее ось T.center выглядывает в виде белой нити из верхнего конца трубки. Рендеринг трубки может выполняться довольно медленно, особенно если использовать растеризатор Adobe Reader. Делу может помочь установка thick=false, делая все линии ширины linewidth(0) (один пиксель независимо от разрешения). По умолчанию в трехмерном случае сетка на поверхности и линии контуров всегда рисуются тонкими, если только перо не настроено по-другому или установлено thin=false. Перья thin() и thick(), определенные в plai_npens.asy, также могут быть переопределены для выполнения специфических команд рисования. Следующая, более продвинутая, процедура задает трубчатую поверхность surface tube(path3 g, coloredpath section, transform T(real)=new transform(real t) return identity();, real corner=1, real relstep=0); Глава 18. Модули labelpath3 и tube 210 Ïðèìåð 18.3. Íàäïèñûâàíèå ãðàôèêà ôóíêöèè. import graph3; import labelpath3; import palette; size(6cm,0,keepAspect=true); currentprojection=orthographic((5,4,2)); currentlight=nolight; pen p2=rgb(.1,.1,.48); pen sface=bp+opacity(0.7); pen mahogany=cmyk(0,0.85,0.87,0.35); triple f(pair rf){ real xx=rf.x*cos(rf.y); real yy=rf.x*sin(rf.y); return (xx,yy,1.6*(xx^2+yy^2)^1.5);} triple f1(real fi){ return f((0.805,fi));} path3 lab=graph(f1,2.92,3.74,operator..); surface s=surface(f,(0,0),(0.79,2pi),8,Spline); s.colors(palette(s.map(ypart),Gradient(sface+p2, sface+white,sface+p2))); draw(s,meshpen=blue+0.3bp); draw(labelpath("$z=(x^2+y^2)^{3/2}$",reverse(lab), angle=180)); axes3(xlabel=Label("$x$",dir(X-O),mahogany),ylabel=Label("$y$",dir(Y-O),mahogany), zlabel=Label("$z$",dir(Z-O),mahogany),min=(0,0,0),max=(0.8,0.8,1.3),orange, Arrow3(mahogany)); Ïðèìåð 18.4. Òðóá÷àòàÿ ïîâåðõíîñòü è åå îñü. import graph3; import palette; size(3cm,0); currentprojection=orthographic(5,4,2); viewportmargin=(1cm,0); real r(real t) {return 3exp(-0.1*t);} real x(real t) {return r(t)*cos(t);} real y(real t) {return r(t)*sin(t);} real z(real t) {return t;} path3 p=graph(x,y,z,0,6*pi,50,operator ..); tube T=tube(p,2); surface s=T.s; s.colors(palette(s.map(zpart),BWRainbow())); draw(s); draw(T.center,white+thin()); Поверхность выстраивается вдоль пути g, имеет профиль (сечение) section, к которому в точке relpoint(g,t) может быть применено преобразование T. Параметр corner управляет числом элементарных трубок в угловых точках g. Отличное от нуля значение relstep определяет относительный шаг по параметру (в смысле relpoint(g,t)), который используется при конструировании элементарных трубок, расположенных вдоль g. Тип coloredpath является обощением пути path и имеет вид следующей структуры: Глава 18. Модули labelpath3 и tube 211 struct coloredpath { path p; pen[] pens(real t); int colortype=coloredSegments; } Здесь p представляет собой путь, расположенный в плоскости, и определяющий профиль трубчатой поверхности; циклический массив перьев pens служит для раскрашивания плиток поверхности в точке relpoint(g,t). Если colortype=coloredSegments, поверхность раскрашивается так, как если бы каждый сегмент сечения раскрашивался заданными перьями pens(t); если же colortype=coloredNodes, то раскраска идет так, как будто раскрашиваются узлы сечения. В примере 18.5 отдельно красным цветом изображена ось двух нарисованных ниже трубок. Для раскрашивания верхней трубки было выбрано раскрашивание на основе сегментов сечения, а для нижней – на основе узлов. Ïðèìåð 18.5. Ðàñêðàñêà îò ñåãìåíòîâ è îò óçëîâ. import graph3; import tube; import palette; size(7cm); currentprojection=orthographic(2,2,2); path3 g=(1,0,0)..(0,1,0.25)..(-1,0,.5).. (0,-1,0.75)..(1,0,1); draw(shift(0,0,0.5)*g,red); pen[] pens; pens[0]=red; pens[1]=blue; pens[2]=yellow; pair pA=(0,0), pB=(1,0), pC=rotate(60,pA)*pB; path sec=pA--pB--pC--cycle; path section=scale(0.15)*sec; coloredpath colorsec=coloredpath(section,pens, colortype=coloredSegments); draw(tube(g,colorsec)); colorsec=coloredpath(section,pens, colortype=coloredNodes); draw(shift((0,0,-0.6))*tube(g,colorsec)); Свой собственный путь coloredpath пользователь может сформировать с помощью одной из трех процедур: coloredpath coloredpath(path p, pen[] pens(real), int colortype=coloredSegments); coloredpath coloredpath(path p, pen[] pens=new pen[] currentpen, int colortype=coloredSegments); coloredpath coloredpath(path p, pen pen(real)); Во второй выбор пера не зависит от относительного значения параметра оси. Что касается третьей процедуры, то массив перьев содержит только одно перо, зависящее от относительного значения параметра. Глава 18. Модули labelpath3 и tube 212 То, что coloredpath наследуется от path, позволяет вместо coloredpath использовать path; в этом случае раскраска берется той, которая по умолчанию используется для поверхности. Следующий пример18.6 показывает, что профиль трубчатой поверхности может определять и символ, в качестве которого выбрано число π. Ïðèìåð 18.6. Ïîâåðõíîñòü ñ ïðîôèëåì ÷èñëà π . import tube; import graph3; import palette; size(7cm); currentlight=White; currentprojection=perspective(1,1,1,up=-Y); int e=1; real x(real t) {return cos(t)+2*cos(2t);} real y(real t) {return sin(t)-2*sin(2t);} real z(real t) {return 2*e*sin(3t);} path3 p=scale3(2)*graph(x,y,z,0,2pi,50, operator..)&cycle; pen[] pens=Gradient(6,red,blue,purple); pens.push(yellow); for (int i=pens.length-2; i>=0; --i) pens.push(pens[i]); path sec=scale(0.25)*texpath("$\pi$")[0]; coloredpath colorsec=coloredpath(sec,pens, colortype=coloredSegments); draw(tube(p,colorsec),render(merge=true)); Еще один пример 18.7 демонстрирует кольца Борромео. Ïðèìåð 18.7. Êîëüöà Áîððîìåî. import graph3; import tube; size(8cm); currentprojection=orthographic(2,2,2); real a=2, b=1.5; real x(real t) {return a*cos(t);} real y(real t) {return b*sin(t);} real z(real t) {return 0;} path3 an1=graph(x,y,z,0,2*pi); path3 an2=graph(z,x,y,0,2*pi); path3 an3=graph(y,z,x,0,2*pi); draw(tube(an1,scale(0.1)*unitcircle),red); draw(tube(an2,scale(0.1)*unitcircle),blue); draw(tube(an3,scale(0.1)*unitcircle),yellow); Применяя полученные сведения о построении поверхностей вращения, трубчатых поверхностей, методах их раскрашивания и освещения, можно создавать почти художественные произведения, см. Приложение D. Ïðèëîæåíèå A Ïëàí êâàðòèðû Ïëàí êâàðòèðû. settings.outformat="pdf"; settings.prc=false; size(8.5cm,0); defaultpen(fontsize(10pt)); // Âíåøíèå ñòåíû draw((0,0)--(6.5,0)--(6.5,1)--(6.8,1)--(6.8,1.3)--(6.5,1.3)--(6.5,9)--(0.3,9)-(0.3,9.5)--(0,9.5)--(0,9.1)--(-1.2,9.1)--(-1.2,8.8)--(0,8.8)--(0,3.5)--(-1.2,3.5) --(-1.2,3.2)--(0,3.2)--cycle); draw((0.3,0.3)--(6.2,0.3)--(6.2,8.7)--(0.3,8.7)--cycle); // Âíóòðåííèå ñòåíû draw((3.3,0.3)--(3.3,8.7)^^(3.45,0.3)--(3.45,8.7)); draw((0.3,4)--(3.3,4)^^(0.3,4.15)--(3.3,4.15)); draw((3.45,3)--(6.2,3)^^(3.45,3.1)--(6.2,3.1)); draw((4.6,3.1)--(4.6,7)--(6.2,7)^^(4.7,3.1)--(4.7,6.9)--(6.2,6.9)); draw((4.7,3.7)--(6.2,3.7)^^(4.7,3.8)--(6.2,3.8)); draw((4.7,5.2)--(6.2,5.2)^^(4.7,5.3)--(6.2,5.3)); draw((5.35,5.3)--(5.35,6.9)^^(5.4,5.3)--(5.4,6.9)); draw((4.7,6.25)--(5.35,6.25)^^(4.7,6.3)--(5.35,6.3)); // Áàëêîí draw((-1,8.8)--(-1,3.5)^^(-0.87,8.8)--(-0.87,3.5)); // Îêíà path[] window=(1.6,0)--(1.6,0.3)^^(2.9,0)--(2.9,0.3)^^(1.6,0.1)--(2.9,0.1)^^ (1.6,0.2)--(2.9,0.2); draw(window); draw(shift(2.2,0)*window); draw(shift(0.3,4.4)*rotate(90)*window); draw(yscale(0.7)*shift(0.3,9)*rotate(90)*window); draw((-0.2,7.85)--(0.5,7.85)); // Äâåðè path[] door=(3.3,8.3)--(3.45,8.3)^^(3.3,7.6)--(3.45,7.6)^^(3.15,7.95)--(3.6,7.95); draw(door); draw(shift(0,-4.4)*door); draw((6.2,8.5)--(6.5,8.5)); draw((6.2,7.7)--(6.5,7.7)); draw((6.05,8.1)--(6.65,8.1)); draw((5.6,6.9)--(5.6,7)); draw((6,6.9)--(6,7)); draw((5.8,6.8)--(5.8,7.1)); 213 Приложение A. План квартиры 214 3,16 2 12,7 3,22 3 7,4 2,74 4,81 1 15,2 3,95 h = 2,70 2,70 path[] doors=(4.6,6.8)--(4.7,6.8)^^(4.6,6.4)--(4.7,6.4)^^(4.5,6.6)--(4.8,6.6); draw(doors); draw(shift(0,-0.8)*doors); draw(yscale(1.2)*shift(0,-3.1)*doors); draw(shift(0,-3.2)*doors); draw((3.8,3)--(3.8,3.1)); draw((4.3,3)--(4.3,3.1)); draw((4.05,2.9)--(4.05,3.2)); // Âàííàÿ draw((5.5,3.9)--(6.1,3.9)--(6.1,5.1)--(5.5,5.1)--cycle); path[] umiv=(4.9,5.2)--(4.9,4.8)--(5.3,4.8)--(5.3,5.2)^^(5,5.2)--(5,4.95)^^ (4.95,4.95)--(5.05,4.95)^^(5.2,5.2)--(5.2,4.95)^^(5.15,4.95)--(5.25,4.95); draw(umiv); draw(shift(1,7.8)*rotate(-90)*umiv); // Êóõíÿ draw((5.7,1.8)--(6.1,1.8)--(6.1,2.2)--(5.7,2.2)--cycle); path unitcircle=(-0.05,0)..(0,0.05)..(0.05,0)..(0,-0.05)..cycle; draw(shift(5.8,1.9)*unitcircle); draw(shift(6,1.9)*unitcircle); draw(shift(5.8,2.1)*unitcircle); draw(shift(6,2.1)*unitcircle); // Òóàëåò real a=5.6,b=5.5; draw((a,b)--(a,b+0.4)..(a+0.2,b+0.6)..(a+0.4,b+0.4)--(a+0.4,b)--cycle); draw((a,b+0.15)--(a+0.4,b+0.15)); draw(shift(a+0.2,b+0.35)*scale(1.5)*unitcircle); // Ðàçìåðû draw((0.3,-0.35)--(3.3,-0.35),L=Label("3,22",Center,filltype=UnFill), Arrows(1.5mm),Bars); draw((3.5,-0.35)--(6.2,-0.35),L=Label("2,70",Center,filltype=UnFill), Arrows(1.5mm),Bars); draw((0.3,9.3)--(3.3,9.3),L=Label("3,16",Center,filltype=UnFill),Arrows(1.5mm),Bars); Приложение A. План квартиры 215 draw((1,0.3)--(1,4),L=Label(rotate(90)*"3,95",Center,filltype=UnFill),Arrows(1.5mm)); draw((1,4.2)--(1,8.7),L=Label(rotate(90)*"4,81",Center,filltype=UnFill), Arrows(1.5mm)); draw((5.4,0.3)--(5.4,3),L=Label(rotate(90)*"2,74",Center,filltype=UnFill), Arrows(1.5mm)); label("$h=2{,}70$",(2.15,8.3)); label("$\displaystyle{\frac{1}{15{,}2}}$",(2.15,6.45)); label("$\displaystyle{\frac{2}{12{,}7}}$",(2.15,2.15)); label("$\displaystyle{\frac{3}{7{,}4}}$",(4.45,1.65)); shipout(bbox(0.1cm+invisible)); Конечно, представленный вариант программирования плана квартиры вполне примитивен, так как играет здесь лишь иллюстративную роль. Разумнее было бы обратиться к возможностям создания картинок picture и добавления их к основному рисунку (см. раздел 7.7). Предметы мебели, ванну, газовую плиту, окна, двери и т. д. можно было бы изобразить в виде отдельных картинок, а затем с помощью перемещений и поворотов расположить в требуемом месте плана. Другим способом является написание специальных процедур рисования, параметры которых определяли бы центр изображаемого объекта на плане, его относительные размеры и ориентацию. Ïðèëîæåíèå B Çà÷åð÷èâàíèå I Построим и окрасим цилиндроид, ограниченный сверху поверхностью z = f (x, y) = 12 − 0.08x2 , а с боков – цилиндрической поверхностью (23 + 3x)y 2 = 12(36 − (x − 1)2 ); (B.1) при этом x ∈ [−5; 7], y ∈ [−5; 5]. Уравнение (B.1) позволяет записать контур, ограничивающий основание цилиндроида, в виде r 12(36 − (x − 1)2 ) y=± . (B.2) 23 + 3x Далее представим себе, что боковая поверхность цилиндроида образуется с помощью вертикального отрезка, перемещающегося вдоль этого контура параллельно самому себе. Для этого требуется найти параметризацию, позволяющую записать контур (B.2) как одно целое, т. е. как явную функцию, а не как многозначное отображение (B.2). Пусть параметр u принимает значения из отрезка [0; 2], а параметризованное значение x имеет вид −12u + 7, u ∈ [0; 1], x= 12u − 17, u ∈ [1; 2]. При изменении u от 0 до 2 переменная x изменяется сначала от 7 до −5, а затем наоборот – от −5 до 7. Теперь функцию (B.2) можно записать как s 12(36 − (x(u) − 1)2 ) y(u) = sign(1 − u) . 23 + 3x(u) Так как вертикальный отрезок [0; f (x, y)] можно задать с помощью еще одного параметра t ∈ [0; 1] в виде tf (x, y), то в целом боковая поверхность цилиндроида описывается тройкой координат (x(u), y(u), tf (x(u), y(u))), u ∈ [0; 2], t ∈ [0; 1]. Верхнюю часть цидиндроида получим уже известным способом, натянув на линию пересечения верхней его части и боковой поверхности вида (x(u), y(u), f (x(u), y(u))), 216 u ∈ [0; 2], Приложение B. Зачерчивание I 217 поверхность с помощью surface. Предварительно указанную линию пересечения необходимо, конечно, зациклить. Далее следует текст программы и результат ее работы в виде рисунка. Ïîñòðîåíèå è ðàñêðàñêà öèëèíäðîèäà. // Ðàçëè÷íûå óñòàíîâêè settings.outformat="pdf"; settings.prc=false; settings.render = 4; // Ïîäêëþ÷åíèå ìîäóëåé Asymptote import three; import graph3; import palette; // Óñòàíîâêà ðàçìåðà êàðòèíêè, òèïà ïðîåêöèè è îñâåùåíèÿ size(8cm,12cm,keepAspect=true); currentprojection=orthographic((2,-4.3,3.2)); currentlight=nolight; // Îïðåäåëåíèå ôóíêöèè y = sign(x) real sign(real x){ return (x<0) ? -1. : 1.; } // Çàäàíèå ôóíêöèè z = 12 - 0.08*x^2 real f(real x,real y){ return 12-0.08*x^2; } // Âû÷èñëåíèå àáñöèññû êîíòóðà îñíîâàíèÿ x = tau(t). // Êîãäà ïåðåìåííàÿ t ïðîõîäèò îòðåçîê [0,2], ïåðåìåííàÿ x ïðîõîäèò îòðåçîê [-5,7] // ñíà÷àëà â îäíîì íàïðàâëåíèè, à çàòåì - â ïðîòèâîïîëîæíîì. real tau(real t){ return (t<1) ? -12*t+7 : 12*t-17; } // Îðäèíàòà êîíòóðà îñíîâàíèÿ real cont(real t){ return sign(1-t)*sqrt(12*(36-(tau(t)-1)^2)/(23+3*tau(t))); } // Êîîðäèíàòû òî÷åê áîêîâîé ïîâåðõíîñòè öèëèíäðîèäà triple cheek(pair ut){ real u=ut.x; real t=ut.y; real xx=tau(u); real yy=cont(u); return (xx,yy,t*f(xx,yy)); } // Êîîðäèíàòû ëèíèé ïåðåñå÷åíèÿ áîêîâîé ïîâåðõíîñòè ñ îñíîâàíèåì è âåðõíåé ÷àñòüþ // öèëèíäðîèäà triple cheek0(real u) {return cheek((u,0));} triple cheek1(real u) {return cheek((u,1));} Приложение B. Зачерчивание I // Çàäàíèå íåîáõîäèìûõ äëÿ ðèñîâàíèÿ ïåðüåâ pen p3=rgb(.21,.88,1.); pen p4=rgb(.68,1.,1.); pen tcol=darkgray; pen p1=rgb(.1,.1,.48); pen p2=rgb(.93,.55,.93); pen mahogany=cmyk(0,0.85,0.87,0.35); pen conts=blue; pen decomp=black; pen sface=bp+opacity(.2); // Áîêîâàÿ ïîâåðõíîñòü surface cheekside=surface(cheek,(0,0),(2,1),50,Spline); // Ëèíèè ïåðåñå÷åíèÿ áîêîâîé ïîâåðõíîñòè ñ îñíîâàíèåì è âåðõíåé ÷àñòüþ öèëèíäðîèäà path3 botline=graph(cheek0,0,2,operator..); path3 topline=graph(cheek1,0,2,operator..); // Âåðõíÿÿ ÷àñòü ïîâåðõíîñòè surface topside=surface(topline--cycle); // Ôóíêöèè, îïèñûâàþùèå êðèâûå ðàçáèåíèÿ îñíîâàíèÿ öèëèíäðîèäà íà ÷àñòè real xh1(real x) {return x;} real yh1(real x) {return 0.05*x^2+2.0813;} real zh1(real x) {return 0;} real yh2(real x) {return 0.05*x^2;} real yh3(real x) {return 0.05*x^2-2.5;} real xv2(real y) {return 0.05*y^2+0.5;} real xv3(real y) {return 0.05*y^2+3.745;} // Êðèâûå ðàçáèåíèÿ îñíîâàíèÿ öèëèíäðîèäà íà ÷àñòè path3 hor1=graph(xh1,yh1,zh1,-4.32,4.18174,operator..); path3 hor2=graph(xh1,yh2,zh1,-4.92,6,operator..); path3 hor3=graph(xh1,yh3,zh1,-4.9,7,operator..); path3 vert1=graph(yh3,xh1,zh1,-4.39,4.39,operator..); path3 vert2=graph(xv2,xh1,zh1,-4,4,operator..); path3 vert3=graph(xv3,xh1,zh1,-2.97,2.9556,operator..); // Îïèñàíèå îêðàñêè ïîâåðõíîñòè cheekside.colors(palette(cheekside.map(xpart),Gradient(sface+p1, sface+white,sface+p2))); topside.colors(palette(cheekside.map(xpart),Gradient(sface+p2, sface+white,sface+p1))); // Ñîáñòâåííî ðèñîâàíèå êðèâûõ è ïîâåðõíîñòåé draw(cheekside); draw(topside); draw(hor1,decomp); draw(hor2,decomp); draw(hor3,decomp); draw(vert1,decomp); draw(vert2,decomp); draw(vert3,decomp); 218 Приложение B. Зачерчивание I draw(botline,conts); draw(topline,conts); // Ðèñîâàíèå îñåé, íàäïèñåé è ò.ï. draw ((-5.4,5.2,0)--(-5.5,-4.5,0),mahogany,Arrow3(size=3mm), L=Label("$x$",position=EndPoint,align=SW)); draw ((-5.4,5.2,0)--(6.7,5.2,0),mahogany,Arrow3(size=3mm), L=Label("$y$",position=EndPoint,align=E)); draw ((-5.4,5.2,0)--(-5.4,5.2,11.5),mahogany,Arrow3(size=3mm), L=Label("$z$",position=EndPoint,align=N)); draw((1.5,5,10)--(1.5,6.5,10.5)); label("$z=f(x,y)$",(0.3,6.5,10.3),NE,tcol); label("O",(-5.6,5.2,0.4),W,mahogany); label("$D$",(-2.2,-5.5,-0.3),tcol); label(rotate(-18)*"$\Delta S_i$",(0.4,0.6,0.5),W,red); // Îãðàíè÷èâàþùèé ðèñóíîê ïðÿìîóãîëüíèê shipout(bbox(0.01cm+invisible)); 219 Ïðèëîæåíèå C Çà÷åð÷èâàíèå II . В отличие от поверхности, заданной функцией двух переменных и построенной в Приложении B, рассмотрим поверхность, определяемую сплайнами. Пояснения будем делать, постепенно разворачивая листинг программы. Вначале выполним все предварительные установки: Ðèñóíîê ê ôîðìóëå Îñòðîãðàäñêîãî. settings.outformat="pdf"; settings.prc=false; settings.render = 4; import three; import graph3; import palette; size(0,7cm); currentprojection=orthographic((1,-3,1.3)); currentlight=nolight; pen mahogany=cmyk(0,0.85,0.87,0.35); pen p1=mediumblue;//rgb(.1,.1,.48); pen p2=mediumblue; pen sface=bp+opacity(0.7); Далее зададим с помощью сплайнов контур cont, расположенный в плоскости xOy (на рис. C.1 он ограничивает область D). Нарисуем этот контур с натянутой на него поверхностью, что́ и даст нам изображение области D. Затем поднимем контур на высоты a и b над xOy и зафиксируем его в этих положениях как contUp и contTop: path3 cont=(-2,0,0)..(-1,1,0)..(0,2,0)..(1.5,1.5,0)..(2,0,0)..(1.5,-0.3,0).. (0,-2,0)..(-1.3,-1.7,0)..cycle; real a=3,b=5; draw(cont,blue+thick()); draw(surface(cont),palegreen); 220 Приложение C. Зачерчивание II 221 path3 contUp=shift((0,0,a))*cont; path3 contTop=shift((0,0,b))*cont; draw(contUp,blue+thick()); draw(contTop,blue+thick()); Теперь опишем функции, которые определят нам поверхность, натянутую на контур cont (стоит напомнить, что этот контур лежит в плоскости xOy). Поверхность сформируем следующим образом. В зависимости от значения параметра t, 0 ≤ t ≤ 0.5, на контуре выберем две точки, n1(x1,y1,0) и n2=(x1,y1,0) с помощью функции relpoint: n1=relpoint(cont,t), n2=relpoint(cont,1-t). Отрезок, концами которого являются эти точки, задается равенствами x=x1+(x2-x1)*tau, y=x1+(y2-y1)*tau, где tau – параметр, принимающий значения в промежутке [0; 1]. Над отрезком построим параболу, проходящую через точки n1 и n2 и имеющую вершину в середине отрезка высоты zmax (t). Движение параболы в соответствии с изменением параметра t и вычертит поверхность над контуром cont. Эти построения выполняют функции taum, app и f. Функция taum определяет наивысшую точку параболы для данного значения t (наивысшей точкой поверхности является taumaxmax), функция app находит аппликату z для ординаты y упомянутого выше отрезка, а f выдает конечный итог – точку поверхности hatside. Еще одна функция, g, возвращает точки вертикального отрезка, который чертит боковую поверхность. real taumaxmax=1.5; real taum(real t){ return -16*taumaxmax*t*(t-0.5); } real app(real taum,real y,real y1,real y2){ real z; if (fabs(y1-y2)>0.0000001) z=-4*taum*(y^2-(y1+y2)*y+y1*y2)/(y1-y2)^2; else z=0; return z; } triple f(pair t_tau){ real t=t_tau.x; real tau=t_tau.y; real taumax=taum(t); triple n1=relpoint(cont,t); triple n2=relpoint(cont,1-t); real x1=n1.x; real y1=n1.y; real x2=n2.x; real y2=n2.y; real x=x1+(x2-x1)*tau; real y=y1+(y2-y1)*tau; real z=app(taumax,y,y1,y2); return (x,y,z); } triple g(pair t_tau){ real t=t_tau.x; real tau=t_tau.y; triple N=relpoint(cont,t); return (N.x,N.y,a+(b-a)*tau); } Приложение C. Зачерчивание II 222 Фактически же боковая поверхность изображается с помощью операторов surface cheekside=surface(g,(0,0),(1,1),50,Spline); cheekside.colors(palette(cheekside.map(xpart),Gradient(sface+heavycyan, sface+white,sface+heavycyan))); draw(cheekside); Верхняя часть поверхности получается сдвигом описанной выше поверхности hatside на b вверх: surface hatside=surface(f,(0,0),(0.5,1),50,Spline); hatside.colors(palette(hatside.map(xpart),Gradient(sface+p1, sface+white,sface+p2))); draw(shift((0,0,b))*hatside); Нижнюю ее часть upside получим отражением поверхности hatside от плоскости xOy (определяемой точками pA, pB и pC) и сдвигом вверх на a: triple pA=(-2,0,0),pB=(0,-2,0),pC=(0,2,0); surface upside=shift((0,0,a))*reflect(pA,pB,pC)*hatside; upside.colors(palette(upside.map(xpart),Gradient(sface+p1, sface+white,sface+p2))); draw(upside); Остается добавить векторы и надписи: pair t_tau_1=(0.43,0.5); triple pn1=f(t_tau_1)+(0,0,b); draw(pn1--pn1+(0.7,0.3,0.7),red+thick(),Arrow3(2mm),L=Label("$\mathbf{n}$", EndPoint)); pair t_tau_2=(0.45,0.7); triple pn2=shift((0,0,a))*reflect(pA,pB,pC)*f(t_tau_2); draw(pn2--pn2+(0.7,0.3,-0.8),red+thick(),Arrow3(2mm),L=Label("$\mathbf{n}$", EndPoint)); pair t_tau_3=(0.5,0.5); triple pn3=g(t_tau_3); draw(pn3--pn3+(0.8,0.3,0),red+thick(),Arrow3(2mm),L=Label("$\mathbf{n}$", EndPoint)); label("$D$",(0.2,-0.5,0.3)); label(rotate(10)*"$z=\beta(x,y)$",(0.8,-2,6)); label(rotate(10)*"$z=\alpha(x,y)$",(1,-2,2.95)); label("$V$",(0,0,(a+b)/2-0.3)); draw((-3,0.5,0)--(-2.5,-3,0),mahogany,Arrow3(size=2mm),L=Label("$x$", position=EndPoint,align=SSW)); Приложение C. Зачерчивание II draw((-3,0.5,0)--(-1.5,2,-0.1),mahogany,Arrow3(size=2mm),L=Label("$y$", position=EndPoint,align=E)); draw((-3,0.5,0)--(-3,0.5,5.7),mahogany,Arrow3(size=2mm),L=Label("$z$", position=EndPoint,align=N)); label("$O$",(-3,0.5,0),W,mahogany); shipout(bbox(0.01cm+invisible)); Рис. C.1. К формуле Остроградского. 223 Ïðèëîæåíèå D ×àøêà ñ íàïèòêîì Дабы завершить труды немалые по написанию труда сего, увенчаем оный столь незамысловатой, но приятной наградой в виде чашечки кофе. Вначале, как водится, пропишем необходимые установки в программе: ×àøêà ñ íàïèòêîì. settings.outformat="pdf"; settings.prc=false; import solids; import graph3; import palette; currentprojection=perspective(5,0,5.1); currentlight=light((10,10,3),(0,-2,10)); size(15cm,0); pen coffee=cmyk(0,0.5,1,0.45); Рисунок начнем с изображения блюдца, для чего зададим кривую saucer, описывающую поперечное сечение блюдца. Кривую с помощью revolution заставим вращаться вокруг оси Oz, после чего натянем на вращение поверхность ssauc. Зададим палитру для раскраски блюдца, используя map и Gradient, и выведем его на экран: path3 saucer=(0,0,0)--(0,3.7,0)..(0,4,-0.4)..(0,4.3,0)..(0,5.4,0.6)..(0,7,1.5)-(0,7,1.55)..(0,5.4,0.75)..(0,4.3,0.15)--(0,2.7,0.15)..(0,2.5,0.1)--(0,0,0.1); revolution sauc=revolution(O,saucer,Z,0,360); surface ssauc=surface(sauc); ssauc.colors(palette(ssauc.map(xpart),Gradient(mediumblue,lightblue,mediumblue))); draw(ssauc); Таким же способом создадим и чашку: path3 cupcurve=(0,0,0)--(0,3.4,0)--(0,3.4,0.3)..(0,3.9,1.2)..(0,5,6.5); revolution cup=revolution(O,cupcurve,Z,0,360); surface scup=surface(cup,n=30); scup.colors(palette(scup.map(ypart),Gradient(mediumgray,lightgray,mediumgray))); draw(scup); 224 Приложение D. Чашка с напитком 225 Преобразование unitdisk (единичного круга) позволит налить в чашку напиток, напоминающий кофе. А еще для «оживляжа» нарисуем на поверхности чашки три синих ободка: surface scoffee=shift(0,0,5.8)*scale3(4.9)*unitdisk; draw(scoffee,darkbrown); draw(shift(0,0,5.6)*scale3(4.99)*unitcircle3,royalblue+3bp); draw(shift(0,0,5.2)*scale3(4.99)*unitcircle3,royalblue+3bp); draw(shift(0,0,2.9)*scale3(4.57)*unitcircle3,royalblue+3bp); Остается пркрепить к чашке ручку. Тут-то и пригодится трубчатая поверхность! path3 handle=(0,4,1.8){dir(70,90)}..(0,6,5.5)..{dir(-80,90)}(0,5,4.9); tube T=tube(handle,0.7); surface s=T.s; s.colors(palette(s.map(ypart),Gradient(lightgray,palegray))); draw(s); shipout(bbox(0.1cm+invisible)); А вот и завершающий аккорд: Ëèòåðàòóðà [1] Asymptote: the Vector Graphics Language Официальное руководство. [2] E. Ashcroft, Z. Manna. The Translation of ’goto’ Programs to ’while’ Programs.– Stanford university, 1971. [3] E. A Ashcroft, Z. Manna. Translating Program Schemas to While Schemas.– 1971. [4] Волченко Ю. М. Введение во фракталы с примерами на языке Asymptote. – Электронный ресурс: http://math.volchenko.com. [5] Christophe Grospellier. Asymptote. Démarrage «rapide», 2014. [6] Филипп Ивальди. Евклидова геометрия на языке векторной графики Asymptote.– Волгоград, 2012. [7] Bruno M. Colombel. Asymptote — 3D, 2011. [8] Кроновер Р.М. Фракталы м хаос в динамических системах. Основы теории.– М.: Постмаркет, 2000. [9] Ю.Г. Крячков. Асимптота для начинающих. Создание рисунков на языке векторной графики Asymptote, 2014. [10] Ondřej Kutal. Tvorba matematické grafiky pomocí programu Asymptote.– Diplomová práce, Univrsitas Masarykiana brunensis, Brno, 2012. [11] Charles Staats III. An Asymptote tutorial, 2014. 226