В разработке
Начнём определение класса Turtle
с команд, загружающих
необходимые модули:
Perluse Math::Trig; use POSIX; use File::Temp; use RGBColor;
Класс Math::Trig
понадобится для кое-каких
тригонометрических вычислений; в нём определены, в частности, процедуры
cos
и sin
, а также
deg2rad
, пересчитывающая углы из градусов в радианы. Из
класса POSIX
мы позаимствуем процедуры
floor
и ceil
, которые вычисляют
для заданного числа его
пол
(наибольшее целое число, не превосходящее )
и потолок
(наименьшее целое число, большее или равное ).
Класс File::Temp
позволяет создавать и использовать
временные файлы, чьи имена создаются автоматически в соответствии с заданным
шаблоном и гарантированно отличаются от имён уже существующих файлов. Временные
файлы удаляются автоматически при завершении программы.
Все перечисленные классы входят с стандартный набор классов, поставляемых
вместе с perl. А вот класс RGBColor
мы запрограммируем сами в отдельном модуле.
Конструктор new
создаёт новый объект — черепаху. Вновь
созданный объект будет содержать стек запомненных состояний, включающий пока
единственное состояние: черепаха в начале координат, лицом повёрнута на восток,
единичная толщина линий, чёрный цвет рисования, единичный масштабный
коэффициент.
Габаритный прямоугольник в начале работы точечный (все координаты левого нижнего и правого верхнего углов нулевые).
Свойство tmpfile получает в качестве значения вновь
созданный объект File::Temp
. При вызове конструктора
этого класса передаётся список пар ключ-значение, посредством которых можно
придать временному файлу необходимые свойства. Из многочисленных возможных
свойств нас заинтересует только одно: template — шаблон
имени файла. В шаблоне 'Turtle-XXXXXXXX'
несколько идущих
подряд букв X
заменяются на такое же количество случайных
символов, разрешённых в именах файлов — буквы или цифры, причём гарантируется,
что файла с таким именем не существует. К примеру, может получиться такое имя:
Turtle-JFNpsR4m
. Временный файл сразу после создания
открыт для записи и представляет собой объект ввода-вывода, так что методы
класса Turtle
могут сразу записывать в него:
Perl$self->{tmpfile}->print(…);
Итак, получаем следующий код конструктора:
Perlsub new() { my $class=shift; my $self ={ savestack=>[ { x=>0, y=>0, direction=>0, linewitdh=>1, color=>RGBColor->new([0, 0, 0]), scale=>1, } ], bbox=>{llx=>0, lly=>0, urx=>0, ury=>0}, tmpfile=>File::Temp->new(template=>'Turtle-XXXXXXXX'), }; bless $self, $class; return $self; }
Способы сохранения и восстановления состояний черепахи подробно обсуждались
в разделе «Состояние черепахи».
Однако необходимо дополнить определения методов save
и restore
добавочными командами.
Каждый раз, когда изменяется запоминаемое свойство черепахи, выдаётся
соответствующая PostScript-команда. Скажем, при изменении свойства
linewitdh PostScript-машина получает команду
setlinewidth с нужным параметром. Если изменению свойства
предшествовал вызов метода save
, а после изменения
последует вызов restore
, черепаха восстановит своё
состояние, но PostScript-машина уже не узнает, что толщина линий восстановлена
до предварительно запомненного значения. Об этом следует позаботиться особо.
Одна из возможных стратегий заключается в том, что каждый вызов
restore
будет сопровождаться PostScript-командами,
устанавливающих текущие толщину линий и цвет рисования. Если в число
запоминаемых свойств включить ещё какие-нибудь, об их восстановлении точно
так же потребуется извещать PostScript-машину.
PostScript-машина, точнее, та её часть, которая отвечает за рисование, имеет
много общего с рисующей черепахой. Она тоже имеет набор запоминаемых свойств,
в том числе цвет рисования, толщину линий, форму углов и концов путей, текущий
путь и текущую точку на пути, а также многое другое. Там тоже есть стек
для запомненных графических состояний (не путайте со стеком операндов). Вся
совокупность графических запоминаемых свойств сохраняется в виде фрейма в этом
стеке по команде gsave, а восстанавливается по команде
grestore. Грех не воспользоваться такой возможностью. Итак,
в дополнение к чисто черепашьим манипуляциям со стеком состояний метод
save
должен выдать PostScript-команду
gsave:
Perlsub save() { my $self=shift; push @{$self->{savestack}}, {}; $self->writePostScript('gsave'); }
Точно так же метод restore
будет завершаться
PostScript-командой grestore:
Perlsub restore() { my $self=shift; die "$0: Стек состояний пуст\n" if @{$self->{savestack}}==1; pop @{$self->{savestack}}; $self->writePostScript('grestore'); }
Здесь делается проверка на случай, если предпринята попытка снять с черепашьего
стека состояний самый глубокий фрейм. Если такое произойдёт, черепаха будет
в растерянности: не известны ни толщина линий, ни цвет. PostScript-машина тоже
не будет в восторге, если вызову оператора grestore не
предшествует вызов gsave. Поэтому при нарушении баланса
между вызовами save
и restore
программа будет немедленно остановлена с формулировкой «Стек состояний пуст».
Идеи, лежащие в основе методов getProperty
и setProperty
, уже достаточно подробно обсуждались,
поэтому приведём их код без пояснений:
Perlsub setProperty($$) { my $self=shift; my $property=shift; $self->{savestack}[-1]{$property}=shift; } sub getProperty($) { my $self=shift; my $property=shift; my $savestack=$self->{savestack}; for(my $i=$#$savestack; $i>=0; $i--) { return $savestack->[$i]{$property} if exists $savestack->[$i]{$property}; } }
Помимо универсальных геттера и сеттера getProperty
и setProperty
определим специализированные,
предназначенные для получения или установки конкретных сохраняемых свойств
черепахи. Как читатель, наверное, уже догадался, они будут обращаться
к универсальным геттеру и сеттеру, передавая им конкретное имя свойства.
Большинство из них устроены однотипно, и отличаются только именем свойства:
Perlsub setXY($$) { my $self=shift; $self->setProperty('x', shift); $self->setProperty('y', shift); } sub getXY() { my $self=shift; return ($self->getProperty('x'), $self->getProperty('y')); } sub setScale($) { shift->setProperty('scale', shift); } sub getScale() { return shift->getProperty('scale'); } sub setDirection($) { shift->setProperty('direction', shift); } sub getDirection() { return shift->getProperty('direction'); } sub setLineWidth($) { my $self=shift; my $linewitdh=shift; $self->setProperty('linewitdh', $linewitdh); $self->writePostScript($linewitdh, 'setlinewidth'); } sub getLineWidth() { return shift->getProperty('linewitdh'); } sub setColor($) { my $self=shift; my $color=RGBColor->new(shift); $self->setProperty('color', $color); $self->writePostScript(@$color, 'setrgbcolor'); } sub getColor() { return shift->getProperty('color'); }
Слегка отличаются методы setLineWidth
и setColor
, которые, помимо изменения сохраняемого
свойства, выдают PostScript-команды, соответственно setlinewidth
и setrgbcolor, вызывая метод
writePostScript
.
Пришёл черёд методов, отвечающих за геометрические свойства черепахи: угол поворота и положение на плоскости.
Метод rotate
программируется совершенно прямолинейно:
к текущему значению свойства direction прибавляется угол
поворота, переданный как параметр, и полученное значение устанавливается как
новое значение того же свойства:
Perlsub rotate($) { my $self=shift; $self->setDirection($self->getDirection+shift); }
Метод jump
, перемещающий черепаху на заданный шаг
без черчения линий, устроен сложнее. Переданный как параметр шаг прежде всего
домножается на текущее значение масштабного коэффициента, а затем используется
для вычисления новых координат черепахи по формулам:
Здесь , — текущие координаты черепахи, , — её новые координаты,
— текущий масштабный множитель,
— текущий угол поворота
черепахи, — шаг. Поскольку
тригонометрические функции работают с углами, заданными в радианах, а черепашьи
углы задаются в градусах, перед тригонометрическими вычислениями углы следует
пересчитать в радианы. Применим для этого процедуру
deg2rad
из класса Math::Trig
.
Perlsub jump($) { my $self=shift; my $step=shift; $step*=$self->getScale; my ($x, $y)=$self->getXY; my $angleRad=deg2rad($self->getDirection); $self->setXY($x+$step*cos($angleRad), $y+$step*sin($angleRad)); }
Метод forward
, в отличие
от jump
, чертит линию. В его код мы вставим вызов
jump
, но, кроме того, также и дополнительные команды.
В самом конце тела метода поместим вызов
writePostScript
, который выводит во временный файл
команды PostScript, посвящённые рисованию отрезка. Кроме того,
до и после вызова jump
вызовем также и метод
modifyBBox
, который изменяет габаритный прямоугольник
картинки таким образом, чтобы в этот прямоугольник поместился отрезок. Толщина
отрезка при этом, естественно, учитывается:
Perlsub forward($) { my $self=shift; my $step=shift; my ($x, $y)=$self->getXY; $self->modifyBBox; $self->jump($step); $self->modifyBBox; $self->writePostScript ( 'newpath', $x, $y, 'moveto', $self->getXY, 'lineto', 'stroke' ); }
Почему метод modifyBBox
должен вызываться дважды —
до и после вызова jump
? Для того, чтобы габаритный
прямоугольник вместил отрезок с концами в точках
и
,
необходимо и достаточно, чтобы прямоугольник вместил два кружка диаметра с центрами в этих точках, где — текущая толщина линии.
Таким образом, новые координаты левого нижнего и правого верхнего углов прямоугольника связаны с соответствующими старыми координатами и по формулам Здесь и — координаты черепахи.
Всеми этими вычислениями и занимается метод
modifyBBox
:
Perlsub modifyBBox() { my $self=shift; my $lineWidthHalf=$self->getLineWitdh/2; my ($x, $y)=$self->getXY; $self->{bbox}{llx}=min($self->{bbox}{llx}, $x-$lineWidthHalf); $self->{bbox}{lly}=min($self->{bbox}{lly}, $y-$lineWidthHalf); $self->{bbox}{urx}=max($self->{bbox}{urx}, $x+$lineWidthHalf); $self->{bbox}{ury}=max($self->{bbox}{ury}, $y+$lineWidthHalf); }
Конечно же, нужно определить методы min
и max
. Эти методы получают два числа и возвращают
наименьшее или наибольшее из них, и им не требуется доступ к объекту для этих
вычислений. Поэтому и вызываются они не как методы, а как процедуры (перед их
именами не указывается объект и стрелка). Хотя тело методов и выглядит
несколько загадочно, мы опускаем объяснения, и целиком полагаемся на интеллект
читателя:
Perlsub min($$) { return $_[$_[0]<$_[1]? 0: 1]; } sub max($$) { return $_[$_[0]>$_[1]? 0: 1]; }
Этот метод очень прост и не нуждается в комментариях:
Perlsub writePostScript(@) { shift->{tmpfile}->print("@_\n"); }
Наиболее громоздкий метод посвящён сохранению картинки, нарисованной черепахой, в графическом файле. Он принимает три необязательных параметра — имя графического файла, название графического устройства и разрешение. Если какой-то из них не задан, используются значения по умолчанию:
Perlsub writePicture(;$$$) { my $self=shift; my $outputFileName=shift//'TurtleOut.eps'; my $device=shift//'epswrite'; my $resolution=shift//72; …
Теперь можно запускать интерпретатор PostScript. Запустить из программы на Perl
другую программу можно несколькими способами. Наиболее простой из них,
использующий встроенную процедуру system
, нам не подойдёт,
так как он не позволит направить данные из программы на Perl на стандартный
ввод интерпретатора. Если при запуске интерпретатора заставить его читать
программу на PostScript со стандартного ввода, он будет ждать ввода
с клавиатуры. Наш замысел состоял в том, чтобы данные на стандартный ввод
направлялись из нашей программы. Поэтому мы воспользуемся другим способом
запуска программ — открытием процесса для ввода. Работающую программу,
ожидающую данных со стандартного ввода, можно рассматривать как файл, открытый
для записи — и то и другое является потребителем символов. Подобно тому, как
файл открывается для записи, можно открыть и процесс (в этом случае задаётся не
имя файла, а командная строка запуска процесса). Процесс при этом запускается,
а его стандартный ввод становится дескриптором файла, открытого для записи.
Всё, что записывается в этот дескриптор, открытый таким способом процесс
получит как будто со стандартного ввода.
Эта возможность не является характерной именно для языка Perl, скорее это свойство многозадачных операционных систем. Она позволяет организовать однонаправленный обмен данными между двумя процессами, связанными семейными узами — родительским и детским (если один процесс запущен другим, он является по отношению к другому детским, а тот, в свою очередь, приходится ему родителем). К слову скажем, что процесс можно открыть и для чтения, и в этом случае родитель может читать из открытого дескриптора всё, что детский процесс записывает на стандартный вывод. Таким способом, подчёркиваем, можно организовать лишь одностороннюю передачу данных — от родителя к потомку или наоборот. Для двусторонней связи понадобятся другие средства — сетевые возможности операционной системы.
Следующая группа команд посвящается вычислениям габаритных прямоугольников и размеров картинки. Напомним, габаритный прямоугольник нам понадобился для того, чтобы дать знать о размерах картинки интерпретатору PostScript. Как это делается? В самом начале программы на PostScript нужно поместить специальные комментарии:
PostScript%!PS-Adobe-3.0 EPSF-3.0 %%BoundingBox: 230 351 381 440 %%HiResBoundingBox: 230.5 351.774402 380.5 439.225598 %%EndComments
Самый первый из них, тот, что начинается с %!
извещает
интерпретатор о том, что программа соответствует третьей версии языка
PostScript и используется специальный формат EPSF третьей
версии (Encapsulated PostScript —
инкапсулированный PostScript). Этот формат применяется для одностраничных
PostScript-документов, которые могут быть вставлены (инкапсулированы) в другие
PostScript-документы. Программы в формате EPSF должны
удовлетворять неким дополнительным требованиям, обсуждать которые было бы не
совсем своевременно. Мы используем этот формат лишь с одной целью: заставить
интерпретатор прочитать следующие комментарии, особенно эти:
PostScript%%BoundingBox: 230 351 381 440 %%HiResBoundingBox: 230.5 351.774402 380.5 439.225598
Второй из них описывает габаритный прямоугольник. Первый тоже, но он
предназначен для программ, которые оперируют только с габаритными
прямоугольниками, имеющими целочисленные координаты вершин. Естественно, эти
координаты получаются в результате некоторого округления (как в верхнюю, так
и в нижнюю сторону). Приставка HiRes
означает High Resolution — высокая точность.
Прочитав их, интерпретатор перед началом работы подготовит устройство вывода картинки именно нужного размера. Конечно, и без этих комментариев в программе на языке PostScript имеется достаточно информации для вычисления размера картинки, но, к сожалению, её невозможно получить, пока программа не будет выполнена до конца. Программам, которые имеют дело с документами PostScript, такую информацию нужно иметь сразу, и комментарий как раз её предоставляет. Обратите внимание, что эти комментарии начинаются с двойного знака процента, в знак того, что это специальные комментарии. Это так называемые структурные комментарии. Они содержат информацию о документе, которую не так просто получить из самого документа. Помимо сведений о габаритных прямоугольниках, в структурных комментариях можно указать сведения о программном обеспечении, с помощью которого создан документ, дату его создания, название, количество страниц (для многостраничных документов), можно пометить начало каждой страницы и сделать многое другое:
PostScript%%Creator: Turtle.pm %%Title: Pentagram %%CreationDate: Sun Jul 11 16:24:22 2010
Итак, в массив @hiResBoundingBox
отправим четыре координаты
двух углов габаритного прямоугольника:
Perlmy %bbox=%{$self->{bbox}}; my @hiResBoundingBox=@bbox{sort keys %bbox};
Грязный трюк во второй строчке позволил поместить значения из ассоциативного
массива %bbox
в нужном порядке. В силу счастливого стечения
обстоятельств правильный порядок значений соответствует алфавитному порядку
ключей llx, lly,
urx, ury. Если бы не так, пришлось бы
писать что-то вроде
Perlmy @hiResBoundingBox=@bbox{qw/llx lly urx ury/};
Оставшиеся вычисления самоочевидны:
Perlmy $width=ceil(($bbox{urx}-$bbox{llx})*$resolution/72); my $height=ceil(($bbox{ury}-$bbox{lly})*$resolution/72); my @boundingBox =( floor($hiResBoundingBox[0]), floor($hiResBoundingBox[1]), ceil($hiResBoundingBox[2]), ceil($hiResBoundingBox[3]) );
Теперь пора запускать интерпретатор. Это делается командой
open
, которая раньше служила нам для открытия файлов,
а нынче послужит для открытия процесса. Второй её параметр будет уже не
'>'
, как для файлов, открываемых для записи,
а '|-'
. Третий параметр, который теперь будет уже не
именем файла, а командной строкой запуска процесса, требует детального
обсуждения.
Начинается командная строка с имени команды. Если у вас Linux
, скорее всего, это будет
gs. Если Windows
, то
gswin32. Затем идут ключи. Ключ -q
подавляет вывод приветствия интерпретатора. Ключ -dNOPAUSE
не
даёт интерпретатору останавливаться после каждой обработанной страницы и ждать
нажатия клавиши Enter для продолжения. Ключ
-dEPSCrop
принуждает интерпретатор прочитать структурные
комментарии и задать правильный размер картинки. Ключ
-sDEVICE=
позволяет
выбрать устройство. С помощью ключа
устройство
-r
указывается
разрешение. Ключ разрешение
-o
задаёт имя графического файла на выходе. После ключей нужно задать имя файла
с программой на языке PostScript, но если указывается дефис файл вывода
-
,
чтение программы осуществляется со стандартного ввода (что мы и хотели):
Perlopen my $gsPipe, '|-', sprintf("gs -q -dNOPAUSE -dEPSCrop -sDEVICE=\"\%s\" -r\%s -o\"\%s\" -", $device, $resolution, $outputFileName);
На тот случай, если имя устройства или, особенно, имя выходного файла содержат пробелы, в командной строке они заключены в кавычки.
В только что открытый процесс выводим заголовок и команду, которая делает концы отрезков закруглёнными:
Perl$gsPipe->print(<<__POSTSCRIPT__); \%!PS-Adobe-3.0 EPSF-3.0; \%\%BoundingBox: @boundingBox \%\%HiResBoundingBox: @hiResBoundingBox \%\%EndComments 1 setlinecap __POSTSCRIPT__
Затем копируем туда же полное содержимое временного файла. Заметим, что
до вызова метода writePicture
во временный файл велась
запись, а теперь потребовалось читать из него. К этому моменту в файле могло
оказаться не всё, что в него было записано. Читатель спросит: как же это
возможно. Вспомним, что для ускорения ввода/вывода может использоваться
буферизация. Данные, отправляемые в файл, попадают в него не немедленно,
а через промежуточное звено — буфер. Это область памяти накапливает данные,
которые отправляются в файл либо при наполнении буфера, либо при закрытии
файла, либо по явному требованию. Насколько заполнен буфер вывода к этому
моменту, нам неизвестно. Закрывать временный файл неуместно — мы можем захотеть
писать в него и дальше, так что остаётся последняя возможность — явное
требование сбросить буфер, то есть отправить его содержимое в файл. Это
делается для объектов файлового вывода вызовом для них метода
flush
:
Perl$self->{tmpfile}->flush;
Открываем временный файл для чтения (он продолжает быть открытым для записи), быстро копируем его содержимое в процесс, и тут же закрываем.
Perlopen my $tmpReader, '<', $self->{tmpfile}->filename; $gsPipe->print(<$tmpReader>); $tmpReader->close;
Для открытия временного файла потребовалось его имя, оно получено вызовом
метода filename
для объекта
File::Temp
.
Отправляем в процесс уже известную нам команду showpage, затем структурный комментарий, помечающий конец файла, и, наконец, закрываем процесс.
Perl$gsPipe->print("showpage\n"); $gsPipe->print("\%\%EOF\n"); $gsPipe->close or die "$0: Продолжать невозможно: GhostScript завершился с ошибкой\n";
Закрытие процессов имеет больше шансов завершиться неудачей, нежели закрытие
файлов. Закрытие процесса приводит к сбрасыванию буфера и завершению процесса.
Неудача при закрытии означает, что процесс завершился с ошибкой (в этом случае
вызов метода close
возвратит ненулевое значение). Мы
педантично проследим за подобными ошибками, а, поскольку совершенно непонятно,
что делать в такой ситуации, мы просто завершим программу с выдачей
соответствующего сообщения. Наша правильная (надеемся) программа отправляет
в интерпретатор правильный PostScript. Из-за чего может спасовать
интерпретатор? Может не хватить памяти, если черепаха рисует что-то чудовищное
по размеру. Или недостаточно места на диске для записи графического файла. Или
указано имя не поддерживаемого устройства. Мало ли что может случиться…
Завершается определение метода печатью отчёта о проделанной работе:
Perl… my $outputFileSize=-s $outputFileName; print(<<__REPORT__); * Записан файл: «$outputFileName» * Размер файла: $outputFileSize * Устройство: $device * Размер: ${width}×$height * Разрешение: $resolution dpi ** __REPORT__ }
Вот теперь класс Turtle
готов.