Разработка

В разработке

Начнём определение класса Turtle с команд, загружающих необходимые модули:

use Math::Trig;
use POSIX;
use File::Temp;
use RGBColor;

Класс Math::Trig понадобится для кое-каких тригонометрических вычислений; в нём определены, в частности, процедуры cos и sin, а также deg2rad, пересчитывающая углы из градусов в радианы. Из класса POSIX мы позаимствуем процедуры floor и ceil, которые вычисляют для заданного числа x его пол x (наибольшее целое число, не превосходящее x) и потолок x (наименьшее целое число, большее или равное x).

Класс File::Temp позволяет создавать и использовать временные файлы, чьи имена создаются автоматически в соответствии с заданным шаблоном и гарантированно отличаются от имён уже существующих файлов. Временные файлы удаляются автоматически при завершении программы.

Все перечисленные классы входят с стандартный набор классов, поставляемых вместе с perl. А вот класс RGBColor мы запрограммируем сами в отдельном модуле.

Конструктор new создаёт новый объект — черепаху. Вновь созданный объект будет содержать стек запомненных состояний, включающий пока единственное состояние: черепаха в начале координат, лицом повёрнута на восток, единичная толщина линий, чёрный цвет рисования, единичный масштабный коэффициент.

Габаритный прямоугольник в начале работы точечный (все координаты левого нижнего и правого верхнего углов нулевые).

Свойство tmpfile получает в качестве значения вновь созданный объект File::Temp. При вызове конструктора этого класса передаётся список пар ключ-значение, посредством которых можно придать временному файлу необходимые свойства. Из многочисленных возможных свойств нас заинтересует только одно: template — шаблон имени файла. В шаблоне 'Turtle-XXXXXXXX' несколько идущих подряд букв X заменяются на такое же количество случайных символов, разрешённых в именах файлов — буквы или цифры, причём гарантируется, что файла с таким именем не существует. К примеру, может получиться такое имя: Turtle-JFNpsR4m. Временный файл сразу после создания открыт для записи и представляет собой объект ввода-вывода, так что методы класса Turtle могут сразу записывать в него:

$self->{tmpfile}->print();

Итак, получаем следующий код конструктора:

sub 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:

Perl
sub save() { my $self=shift; push @{$self->{savestack}}, {}; $self->writePostScript('gsave'); }

Точно так же метод restore будет завершаться PostScript-командой grestore:

Perl
sub restore() { my $self=shift; die "$0: Стек состояний пуст\n" if @{$self->{savestack}}==1; pop @{$self->{savestack}}; $self->writePostScript('grestore'); }

Здесь делается проверка на случай, если предпринята попытка снять с черепашьего стека состояний самый глубокий фрейм. Если такое произойдёт, черепаха будет в растерянности: не известны ни толщина линий, ни цвет. PostScript-машина тоже не будет в восторге, если вызову оператора grestore не предшествует вызов gsave. Поэтому при нарушении баланса между вызовами save и restore программа будет немедленно остановлена с формулировкой «Стек состояний пуст».

Помимо универсальных геттера и сеттера getProperty и setProperty определим специализированные, предназначенные для получения или установки конкретных сохраняемых свойств черепахи. Как читатель, наверное, уже догадался, они будут обращаться к универсальным геттеру и сеттеру, передавая им конкретное имя свойства. Большинство из них устроены однотипно, и отличаются только именем свойства:

sub 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 прибавляется угол поворота, переданный как параметр, и полученное значение устанавливается как новое значение того же свойства:

sub rotate($)
{
	my $self=shift;
	$self->setDirection($self->getDirection+shift);
}

Метод jump, перемещающий черепаху на заданный шаг без черчения линий, устроен сложнее. Переданный как параметр шаг прежде всего домножается на текущее значение масштабного коэффициента, а затем используется для вычисления новых координат черепахи по формулам: x = x + k s cos α , y = y + k s sin α . Здесь x, y — текущие координаты черепахи, x, y — её новые координаты, k — текущий масштабный множитель, α — текущий угол поворота черепахи, s — шаг. Поскольку тригонометрические функции работают с углами, заданными в радианах, а черепашьи углы задаются в градусах, перед тригонометрическими вычислениями углы следует пересчитать в радианы. Применим для этого процедуру deg2rad из класса Math::Trig.

sub 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, который изменяет габаритный прямоугольник картинки таким образом, чтобы в этот прямоугольник поместился отрезок. Толщина отрезка при этом, естественно, учитывается:

sub 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? Для того, чтобы габаритный прямоугольник вместил отрезок с концами в точках x y и x y , необходимо и достаточно, чтобы прямоугольник вместил два кружка диаметра w с центрами в этих точках, где w — текущая толщина линии.

Таким образом, новые координаты левого нижнего x LL y LL и правого верхнего x UR y UR углов прямоугольника связаны с соответствующими старыми координатами x LL y LL и x UR y UR по формулам x LL = min x LL x w 2 , y LL = min y LL y w 2 , x UR = max x UR x + w 2 , y UR = max y UR y + w 2 . Здесь x и y — координаты черепахи.

Всеми этими вычислениями и занимается метод modifyBBox:

sub 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. Эти методы получают два числа и возвращают наименьшее или наибольшее из них, и им не требуется доступ к объекту для этих вычислений. Поэтому и вызываются они не как методы, а как процедуры (перед их именами не указывается объект и стрелка). Хотя тело методов и выглядит несколько загадочно, мы опускаем объяснения, и целиком полагаемся на интеллект читателя:

sub min($$)	{ return $_[$_[0]<$_[1]? 0: 1];	}

sub max($$)	{ return $_[$_[0]>$_[1]? 0: 1]; }

Наиболее громоздкий метод посвящён сохранению картинки, нарисованной черепахой, в графическом файле. Он принимает три необязательных параметра — имя графического файла, название графического устройства и разрешение. Если какой-то из них не задан, используются значения по умолчанию:

sub 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 нужно поместить специальные комментарии:

%!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 должны удовлетворять неким дополнительным требованиям, обсуждать которые было бы не совсем своевременно. Мы используем этот формат лишь с одной целью: заставить интерпретатор прочитать следующие комментарии, особенно эти:

%%BoundingBox: 230 351 381 440
%%HiResBoundingBox: 230.5 351.774402 380.5 439.225598

Второй из них описывает габаритный прямоугольник. Первый тоже, но он предназначен для программ, которые оперируют только с габаритными прямоугольниками, имеющими целочисленные координаты вершин. Естественно, эти координаты получаются в результате некоторого округления (как в верхнюю, так и в нижнюю сторону). Приставка HiRes означает High Resolution — высокая точность.

Прочитав их, интерпретатор перед началом работы подготовит устройство вывода картинки именно нужного размера. Конечно, и без этих комментариев в программе на языке PostScript имеется достаточно информации для вычисления размера картинки, но, к сожалению, её невозможно получить, пока программа не будет выполнена до конца. Программам, которые имеют дело с документами PostScript, такую информацию нужно иметь сразу, и комментарий как раз её предоставляет. Обратите внимание, что эти комментарии начинаются с двойного знака процента, в знак того, что это специальные комментарии. Это так называемые структурные комментарии. Они содержат информацию о документе, которую не так просто получить из самого документа. Помимо сведений о габаритных прямоугольниках, в структурных комментариях можно указать сведения о программном обеспечении, с помощью которого создан документ, дату его создания, название, количество страниц (для многостраничных документов), можно пометить начало каждой страницы и сделать многое другое:

%%Creator: Turtle.pm
%%Title: Pentagram
%%CreationDate: Sun Jul 11 16:24:22 2010

Итак, в массив @hiResBoundingBox отправим четыре координаты двух углов габаритного прямоугольника:

	my %bbox=%{$self->{bbox}};
	my @hiResBoundingBox=@bbox{sort keys %bbox};

Информатика-54© А. Н. Швец