Основы информатики и программирования

Изображение полиэдра

Разбить на страницы
Показывать лекцию целиком

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

В книгах [9] и [8] содержится большое количество дополнительной информации, связанной с рассматриваемой задачей и ее различными обобщениями. Вопросы работы с пакетом AWT и системой документирования в языке Java более подробно могут быть изучены с помощью книг [11] и [10] .

Постановка задачи

В настоящее время во многих областях науки и техники используются различные методы моделирования геометрических объектов в трехмерном пространстве. Один из способов моделирования состоит в том, чтобы аппроксимировать реальный объект набором выпуклых плоских многоугольников. Такой набор мы будем называть полиэдром , многоугольники — гранями, а их стороны — ребрами полиэдра.

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

(рис 13.1) Пример полиэдра

При изображении полиэдра реально рисуются не сами многоугольники, а линии, их ограничивающие, то есть их ребра. Если, однако, изобразить все ребра полиэдра, то получится весьма запутанная, непривычная для человека картина. Для получения удобного для человека рисунка нужно при построении изображения учитывать, что некоторые ребра могут оказаться полностью или частично невидимыми, так как их могут загородить грани полиэдра. Таким образом, при построении изображения надо удалить (не рисовать) части ребер, которые не видны. По этой причине рассматриваемая задача в литературе часто называется задачей удаления невидимых линий.

Задача 13.1. Напишите программу, которая строит изображение заданного полиэдра с удалением невидимых линий. Программа должна быть реализована в виде автономного приложения на языке Java и использовать возможности пакета AWT.

В результате решения этой задачи полиэдр должен изображаться примерно так, как это показано на рис. 13.1.

Изображение полиэдра зависит от того, откуда на него посмотреть. Это направления должно быть задано в точной постановке задачи. Направление, противоположное направлению взгляда, будем называть вектором проектирования и считать, что он направлен вверх. Этого всегда можно добиться, осуществив поворот полиэдра вместе с заданным вектором проектирования. Для того чтобы упростить терминологию, договоримся называть в дальнейшем параллельные вектору проектирования прямые и плоскости вертикальными, а ортогональные ему — горизонтальными.

Рассматриваемая задача, таким образом, сводится к следующей: построить проекцию полиэдра с удалением невидимых линий на бесконечно высокую горизонтальную плоскость $$z=H$$. Требуемое плоское изображение при этом задано не однозначно, так как вращения вокруг вертикальной оси $$Oz$$ приводят к поворотам итоговой картинки. Будем поэтому считать, что полная постановка задачи кроме самого полиэдра и вектора проектирования содержит также задание угла поворота в плоскости проекции.

(рис 13.2) Изображение полиэдра

Такую уточненную постановку задачи иллюстрирует рис. 13.2.

Способ задания вектора проектирования и угла поворота в плоскости проекции вполне ясен, а вот как следует задавать полиэдр нужно обсудить. Простейший способ — перечисление координат всех вершин каждой из его граней. В силу ряда недостатков такого представления, однако, этот способ на практике не применяется.

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

Способ представления полиэдра, который будет использован нами при решении рассматриваемой задачи, несколько отличен от только что описанного. Он предусматривает указание числа и координат вершин полиэдра, а также количества граней и ребер с указанием для каждой из граней номеров всех ее вершин. По этой причине многие ребра оказываются указанными дважды или даже большее число раз (так как принадлежат нескольким граням), что является, безусловно, недостатком этого способа представления.

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

Информация, представляющая полиэдр, является достаточно объемной, и поэтому было бы весьма неудобно вводить ее, используя клавиатуру. Гораздо разумнее создать описание полиэдра в специальном файле, а программе передать его имя. Вот как будет выглядеть этот файл, представляющий поверхность единичного куба с центром в начале координат и гранями, параллельными координатным плоскостям:

8       6       24
    -0.5    -0.5    0.5     
    -0.5    0.5     0.5     
    0.5     0.5     0.5     
    0.5     -0.5    0.5     
    -0.5    -0.5    -0.5    
    -0.5    0.5     -0.5    
    0.5     0.5     -0.5    
    0.5     -0.5    -0.5    
    4       1    2    3    4    
    4       5    6    2    1    
    4       3    2    6    7    
    4       3    7    8    4    
    4       1    4    8    5    
    4       8    7    6    5

Первая строка говорит о том, что у куба 8 вершин, 6 граней и 24 (!) ребра. Число 24, вдвое превышающее реальное количество ребер у куба, является результатом уже описанного выше эффекта, — каждое из ребер куба принадлежит ровно двум граням.

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

Технические детали работы с файлами и чтения информации из них могут быть изучены заинтересованными читателями самостоятельно с использованием итогового текста проекта, приведенного в конце параграфа, и литературы, содержащей описание библиотек (пакетов) языка Java.

Проектирование основных классов

В данной секции будет построена иерархия классов, необходимых для реализации рассматриваемого проекта. Здесь же мы рассмотрим (в самых общих чертах) использование библиотеки AWT программами, не являющимися аплетами.

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

public class R3Vector {
    private double x, y, z;

    public R3Vector(double x, double y, double z) {
        this.x = x; this.y = y; this.z = z;
    }
    public R3Vector() throws Exception {
        x = Xterm.inputDouble("X-коорд. вектора проектирования -> ");
        y = Xterm.inputDouble("Y-коорд. вектора проектирования -> ");
        z = Xterm.inputDouble("Z-коорд. вектора проектирования -> ");
    }
}

Для представления вершин полиэдра можно использовать выведенный из класса R3Vector класс Vertex. Конструктор данного класса обязан явным образом (с помощью ключевого слова super ) вызвать конструктор его базового класса R3Vector, так как иначе компилятор автоматически вставит вызов конструктора класса R3Vector без параметров.

public class Vertex extends R3Vector {
    public Vertex(double x, double y, double z) {
        super(x, y, z);
    }
}

Ребра полиэдра будем представлять с помощью класса Edge, в каждом экземпляре которого содержатся две private -компоненты типа Vertex, соответствующие началу и концу ребра. В подобных ситуациях для обеспечения возможности получения координат начала и конца ребра используют так называемые методы-селекторы getBegin и getEnd.

public class Edge {
    private Vertex begin;
    private Vertex end;

    public Edge(Vertex begin, Vertex end) {
        this.begin = begin; this.end = end;
    }
    public final Vertex getBegin() {
        return begin;
    }
    public final Vertex getEnd() {
        return end;
    }
}

Грани полиэдра будем описывать с помощью класса Facet. Так как грань задается последовательностью ее вершин, то в каждом экземпляре класса должен содержаться массив объектов типа Vertex, для получения количества и координат которых необходимо предусмотреть соответствующие методы.

public class Facet {
    private Vertex[] vertexes;
    public Facet(Vertex[] vertexes) {
        this.vertexes = vertexes;
    public final int getVertexesQuantity() {
        return vertexes.length;
    }
    public final Vertex getVertex(int i) {
        return vertexes[i];
    }

Полиэдр в целом, наконец, будем задавать с помощью класса Polyedr. Каждый экземпляр этого класса будет хранить в массивах vertexes, edges и facets все вершины, ребра и грани полиэдра. При этом оказывается, что часть информации хранится продублированной, так как каждая из граней полиэдра уже содержит все ее вершины. Устранение этого дублирования является еще одной из задач, предлагаемых в конце параграфа.

При создании нового экземпляра класса Polyedr с помощью конструктора вся информация о полиэдре, хранящаяся в файле file в описанном выше формате, должна быть прочитана из него и размещена должным образом в компонентах создаваемого экземпляра. Для реализации этих действий необходимо использование пакетов java.util и java.io, что обуславливает необходимость наличия директив import в начале файла. Все технические детали осуществления чтения информации из файла оставляются читателю, а полный исходный текст проектируемого класса приведен для справки в последней секции параграфа.

import java.util.*;
import java.io.*;
public class Polyedr {
    private Vertex[] vertexes;
    private Edge[]   edges;
    private Facet[]  facets;

    public Polyedr(String file) throws Exception {
        ...
    }
    public final int getVertexesQuantity() {
        return vertexes.length;
    }
    public final Vertex getVertex(int i) {
        return vertexes[i];
    }
    public final int getEdgesQuantity() {
        return edges.length;
    }
    public final Edge getEdge(int i) {
        return edges[i];
    }
    public final int getFacetsQuantity() {
        return facets.length;
    }
    public final Facet getFacet(int i) {
        return facets[i];
    }
}

Решаемая задача изображения проекции полиэдра может быть разделена на две следующим образом: сначала вычислим плоское изображение проекции, состоящее из совокупности прямолинейных отрезков ребер (точнее, их видимых частей), а затем осуществим вывод этого изображения в специально созданном новом окне (более точное название для него — фрейм).

Задачу вывода плоского изображения в отдельном фрейме поручим осуществлять классу AwtDrawer. Графическая библиотека (пакет java.awt ) содержит среди множества других класс Frame, позволяющий создать фрейм и, используя простейшие графические примитивы, построить в нем требуемое изображение. Класс AwtDrawer будет являться расширением класса Frame, а основным методом рисования проекции отрезков ребер будет метод draw, изображающий прямолинейный отрезок. Координаты точек при этом предполагаются нормированными — и абсцисса и ордината должны быть заключены между нулем и единицей.

import java.awt.*;
public class AwtDrawer extends Frame {
    private static final int XLEN = 500;
    private static final int YLEN = 500;
    private static final int DELTA= 100;
    private Image offScrImage;
    private Graphics offScrGC;
    private Graphics g;
    public AwtDrawer() {
        super("Построение изображения полиэдра");
        setSize(XLEN+2*DELTA, YLEN+2*DELTA);
        setBackground(Color.white);
        g = getGraphics();
        show();

   offScrImage = createImage(XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC = offScrImage.getGraphics();
        offScrGC.setColor(Color.white);
        offScrGC.fillRect(0,0,XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC.setColor(Color.black);
     }
    public final void draw(double xb, double yb, double xe, double ye) {
        int x0 = DELTA + (int)(XLEN * xb);
        int y0 = DELTA + (int)(YLEN * yb);
        int x1 = DELTA + (int)(XLEN * xe);
        int y1 = DELTA + (int)(YLEN * ye);
        offScrGC.drawLine(x0, y0, x1, y1);
    }
    public void update(Graphics g) {
        paint(g);
    }
    public void paint(Graphics g) {
        g.drawImage(offScrImage, 0, 0, this); 
    }
}

Поясним назначение некоторых констант, переменных и методов, которые встречаются в реализации класса AwtDrawer.

Первые три строки тела конструктора этого класса последовательно устанавливают заголовок создаваемого фрейма, его размер и цвет фона. Вокруг области рисования размером XLENxYLEN предусмотрены поля шириной DELTA.

Класс Graphics пакета AWT уже встречался нам при рассмотрении аплетов. Именно в нем определены все основные графические примитивы. По некоторым причинам, обсуждение которых выходит за рамки нашего курса, изображение сначала строится в так называемом внеэкранном буфере offScrImage, а во фрейм выводится с помощью метода drawImage при вызове методов paint и update. Метод draw, аргументами которого являются числа в диапазоне от 0 до 1, вычисляет соответствующие им координаты фрейма и с помощью метода drawLine рисует заданный отрезок.

Вернемся теперь к задаче определения множества отрезков на плоскости, которые должны быть изображены.

В качестве первого шага в этом направлении полезно решить относительно простую задачу построения проекции всех ребер полиэдра целиком. Создадим для этих целей класс SimpleDrawer, который вполне логично сделать выведенным из класса AwtDrawer. В качестве компонент в этот класс включим сам полиэдр, ортонормированный базис, состоящий из нормированного вектора проектирования и двух единичных ортогональных друг другу векторов в плоскости проекции, минимальные значения $$x$$ и $$y$$ координат проекции вершин полиэдра в плоскости проекции и длину минимальной стороны квадрата, в который целиком поместится вся проекция полиэдра.

Ряд методов класса SimpleDrawer предназначен для нахождения координат проекции произвольного трехмерного вектора, как реальных, так и нормализованных (в той системе координат на плоскости проекции, в которой изображаемой части фрейма соответствует квадрат $$0 \leqslant x \leqslant 1 \land 0 \leqslant y \leqslant 1$$ ). Вычисление реальных координат проекции, как это хорошо известно из аналитической геометрии, сводится к вычислению скалярных произведений с помощью статического метода scalMul класса R3Vector, а их нормализация — к линейному преобразованию (сдвигу и гомотетии).

public class SimpleDrawer extends AwtDrawer {
    protected Polyedr p;
    protected R3Vector pr;
    private R3Vector x;
    private R3Vector y;
    private double xmin;
    private double ymin;
    private double size;
    private double xProection(R3Vector v) {
        return R3Vector.scalMul(v, x);
    }
    private double yProection(R3Vector v) {
        return R3Vector.scalMul(v, y);
    }
    protected double xnProection(R3Vector v) {
        return (xProection(v) - xmin)/size;
    }
    protected double ynProection(R3Vector v) {
        return (yProection(v) - ymin)/size;
    }

   public SimpleDrawer(Polyedr p, R3Vector pr, double angle) {
        this.p  = p;
        this.pr = pr.normalize();
        double a = pr.getX();
        double b = pr.getY();
        double c = pr.getZ();
        if (a != 0. || b != 0.) {
            x = new R3Vector(-b, a, 0.);
        } else {
            x = new R3Vector(0., c, -b);
        }
        y = R3Vector.vectMul(x, pr);
        x.normalize();
        y.normalize();
        R3Vector nx = R3Vector.plus(R3Vector.mul(Math.cos(angle), x),
                                    R3Vector.mul(-Math.sin(angle), y));
        R3Vector ny = R3Vector.plus(R3Vector.mul(Math.sin(angle), x),
                                    R3Vector.mul(Math.cos(angle), y));
        x = nx;
        y = ny;
        xmin = ymin = Double.MAX_VALUE;	
        double xmax, ymax;
        xmax = ymax = Double.MIN_VALUE;	
        for (int i=0; i<p.getVertexesQuantity(); i++) {
            double xi = xProection(p.getVertex(i));
            double yi = yProection(p.getVertex(i));
            if (xi < xmin) xmin = xi;
            if (yi < ymin) ymin = yi;
            if (xi > xmax) xmax = xi;
            if (yi > ymax) ymax = yi;
        }
        size = ymax - ymin;
        if (xmax - xmin > size) size = xmax - xmin;
    }
    public final void draw() {
        for (int i=0; i<p.getEdgesQuantity(); i++)
            drawEdge(p.getEdge(i));
        Xterm.print("\n");
    }	
    public void drawEdge(Edge s) {
        Vertex begin = s.getBegin();
        Vertex end   = s.getEnd();
        draw(xnProection(begin), ynProection(begin),
             xnProection(end), ynProection(end));
        Xterm.print(".");
    } 
}

Всю основную подготовительную работу выполняет конструктор класса. Первой из его задач является нахождение ортонормированного базиса, одним из векторов которого является вектор проектирования единичной длины. Нормализация вектора осуществляется при этом с помощью метода normalize класса R3Vector. Второй из векторов искомой тройки строится с помощью уже найденного вектора нормали (нормированному вектору проектирования) по следующему правилу. Если $$(a,b,c)$$ — координаты вектора нормали, то векторы с координатами $$(-b,a,0)$$ и $$(0,c,-b)$$ оба ему ортогональны, и при этом один из них заведомо не нулевой. Он-то и выбирается в качестве второго из векторов базиса (в программной реализации ему соответствует компонента x класса SimpleDrawer ).

Третий вектор базиса (компонента y ) находится с помощью вычисления векторного произведения (метод vectMul ) двух уже найденных. Заключительным шагом является осуществление поворота на заданный угол angle в плоскости проекции. Как известно из аналитической геометрии, поворот в плоскости $$Oxy$$ на угол $$\alpha$$ эквивалентен умножению вектора координат $$(x,y)$$ на матрицу$$\left( \begin{array}{cc} \cos\alpha -\sin\alpha\\ \sin\alpha \cos\alpha\\ \end{array} \right).$$ Для осуществления этих операций используются методы mul и plus класса R3Vector, позволяющие умножить вектор на число и вычислить сумму двух векторов соответственно.

Вторая задача конструктора класса SimpleDrawer — определение квадрата наименьшего размера, в котором целиком поместится проекция всех ребер полиэдра. Способ индуктивного вычисления минимального и максимального значений последовательности координат вершин нам хорошо известен. Определение длины стороны искомого квадрата после этого сводится к выбору максимальной из двух длин сторон найденного ограничивающего прямоугольника проекции.

Метод draw, который и должен построить проекцию полиэдра, осуществляет это с помощью метода drawEdge, последовательно вызываемого для всех ребер. Последний из методов вычисляет нормализованные координаты проекции обрабатываемого ребра и вызывает метод draw базового класса AwtDrawer для рисования требуемого отрезка. Так как для сложных полиэдров их изображения могут строиться достаточно долго, а визуализация результата происходит только после полного завершения решения задачи, то рисование каждого ребра во внеэкранном буфере заканчивается выводом точки на экран терминала. Появляющаяся при этом последовательность точек позволяет следить за процессом построения изображения.

Наиболее содержательная часть исходной задачи — учет затенения частей ребер гранями полиэдра — будет решаться с помощью класса ShadowDrawer, обсуждению которого посвящена следующая секция параграфа. В ней выяснится, что для работы с тенями (точнее с просветами — дополнениями теней) целесообразно создать еще два класса — Segment и L1ListSegments.

Чуть позже будет рассмотрен и один из возможных методов оптимизации, позволяющий ускорить построение изображения сложных полиэдров, содержащих десятки тысяч ребер и тысячи граней, с помощью так называемого двумерного хеширования граней. Для этих целей будет создан специальный класс SmartDrawer.

(рис 13.3) Иерархия классов проекта

Метод main, выполняющий ввод имени файла с описанием изображаемого полиэдра, конкретного способа изображения, вектора проектирования и угла поворота в плоскости проекции, реализуется в отдельном классе PolyedrTest, которым завершается построение иерархии классов, используемых для решения исходной задачи. Все классы иерархии показаны на рисунке рис. 13.3.

Работа с тенями от граней

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

(рис 13.4) Тень от грани на ребре

Если предположить, что бесконечно высоко расположен источник света, то на каждом ребре образуются освещенные и затененные участки. Первые мы будем называть просветами, а вторые — тенями. Рисунок 13.4 иллюстрирует эти понятия.

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

Важным наблюдением, существенно упрощающим искомый алгоритм, является тот факт, что задача нахождения теней и просветов на конкретном ребре — одномерная. Сопоставив началу ребра координату 0, а концу — 1, мы можем установить взаимно однозначное соответствие между трехмерными координатами точек ребра и введенными одномерными координатами. После этого каждый из просветов будет представлять собой отрезок, а реализовывать его будет класс Segment, в каждом экземпляре которого должны храниться одномерные координаты начала и конца обрабатываемого отрезка. Методы данного класса обязаны обеспечить все необходимые действия с отрезками, которые понадобятся при определении множества просветов.

public class Segment {
    private double begin, end;

    public Segment(double begin, double end) {
        this.begin = begin; this.end = end;
    }
    public final boolean degenerate() {
        return begin >= end;
    }
    public final Segment leftSub(Segment s) {
        return new Segment(begin, Math.min(end, s.begin));
    }
    public final Segment rightSub(Segment s) {
        return new Segment(Math.max(begin, s.end), end);
    }
    public final Segment intersection(Segment s) {
        begin = Math.max(begin, s.begin);
        end = Math.min(end, s.end);
        return this;
    }
    public final double getBegin() {
        return begin;
    }
    public final double getEnd() {
        return end;
    }
}

Указанные действия включают в себя проверку вырожденности отрезка (метод degenerate ), а также вычисление пересечения двух отрезков и определения обеих компонент разности двух отрезков. Отрезок $$[a,b]$$ является вырожденным, если он представляет из себя точку или пустое множество, то есть если $$a \geqslant b$$. Пересечение двух отрезков всегда является отрезком (возможно, вырожденным), который вычисляется в методе intersection, а разность двух отрезков всегда состоит из двух отрезков, каждый из которых может оказаться вырожденным. Левая и правая компоненты разности вычисляются с помощью методов leftSub и rightSub соответственно.

(рис 13.5) Разность просвета и тени

Совокупность всех просветов ребра целесообразно хранить в односвязном списке сегментов, для чего будем использовать изученную нами ранее ссылочную реализацию (класс L1ListSegments ). В самом начале обработки очередного ребра множество просветов состоит из одного элемента, совпадающего со всем ребром, — отрезка $$[0,1]$$. Последовательный учет теней от граней можно считать функцией на пространстве последовательности граней. Легко заметить, что эта функция индуктивна — зная список просветов, который учитывает тени от нескольких граней, и тень от еще одной, новой грани, легко вычислить новый список просветов, учитывающий и тень от новой грани. Для этого достаточно для всех просветов вычислить их разности с отрезком тени от грани (см. рис. 13.5).

В зависимости от расположения отрезков, соответствующих просвету и тени, их разность может оказаться пустой (если просвет целиком попал в тень), состоящей из единственной точки, двух точек, одного отрезка, точки и отрезка, и двух отрезков. Как это уже обсуждалось ранее, мы не будем различать эти ситуации. Гораздо удобнее считать, что разность всегда состоит из двух отрезков, каждый из которых может быть вырожденным.

Приведем ту часть реализации класса ShadowDrawer, которая уже обсуждена нами.

public class ShadowDrawer extends SimpleDrawer {
    protected R3Vector begin;
    protected R3Vector end;
    private static final int MAXSIZE = 128;
    private L1ListSegments list;
    private static final double t0 = 0.;
    private static final double t1 = 1.;
    private R3Vector R3(double t) {
        return R3Vector.plus( R3Vector.mul((1.-t), begin),
                              R3Vector.mul(t, end) );
    }
    protected final void addShadow(Facet f) {
        try {
            Segment s = shadow(f);
            if (!s.degenerate()) {
                list.toFront();
                while (!list.end()) {
                    Segment next = list.erase();
                    Segment left = next.leftSub(s);
                    if (!left.degenerate()) {
                        list.insert(left);
                        list.forward();
                    }
                    Segment right = next.rightSub(s);
                    if (!right.degenerate()) {
                        list.insert(right);
                        list.forward();
                    }
                }
            }
        } catch(Exception e) {
            Xterm.println("Слишком много видимых отрезков ребра.");
            System.exit(0);
        }
    }
    protected void addShadow() {
        for(int j=0; j<p.getFacetsQuantity(); j++)
            addShadow(p.getFacet(j));
    }

    public ShadowDrawer(Polyedr p, R3Vector pr, double angle) {
        super(p, pr, angle);
        list = new L1ListSegments(MAXSIZE);
    }
    public final void drawEdge(Edge s) {
        begin = s.getBegin();
        end = s.getEnd();
        list.clear();
        try {
            list.insert(new Segment(t0, t1));
        } catch(Exception e) {;}
        addShadow();
        try {
            for (list.toFront(); ! list.end(); list.forward()) {
                Segment u = list.after();
                R3Vector begin = R3(u.getBegin());
                R3Vector end   = R3(u.getEnd());
                draw(xnProection(begin), ynProection(begin),
                     xnProection(end), ynProection(end));
            }
        } catch(Exception e) {;}
        Xterm.print(".");
    } 
}

Метод drawEdge фиксирует обрабатываемое ребро, записывая его начало и конец в компоненты begin и end, и инициализирует список просветов list, помещая в него единственный элемент — ребро целиком. Затем с помощью вызова метода addShadow производится учет теней от всех граней полиэдра и осуществляется изображение всего списка оставшихся просветов. Для изображения просвета необходимо вычисление трехмерных координат точек ребра по известным их одномерным координатам. Это действие выполняется с помощью метода R3 по известной из аналитической геометрии формуле.

Учет тени от конкретной грани производится путем определения одномерной тени на обрабатываемом ребре с помощью вызова метода shadow и индуктивного перевычисления списка просветов по алгоритму, описанному выше. Отметим дополнительно, что вырожденные отрезки в список просветов никогда не включаются.

Заключительным этапом решения задачи является реализация метода shadow. Этот метод должен определить отрезок на обрабатываемом ребре, который представляет собой тень от фиксированной грани полиэдра.

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

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

(рис 13.6) Нахождение тени от грани

Естественно попытаться свести нахождение пересечения ребра с призмой к каким-нибудь более элементарным операциям. Мы сделаем это, представив призму в виде пересечения открытых полупространств. Например, треугольную призму можно представить в виде пересечения четырех полупространств (см. рис. 13.6).

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

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

private static final double EPSILON = 1.e-12;
    private Segment shadow(Facet f) {
        if (f.vertical(pr))
            return new Segment(t1, t0);
        int n = f.getVertexesQuantity();
        Vertex a = f.getVertex(n-1);
        Vertex b = f.getVertex(0);
        Segment result = hCross(f, a); 
        if (result.degenerate()) return result;
        result.intersection(vCross(a, b, f.getCenter()));
        if (result.degenerate()) return result;
        for (int i=1; i<n; i++) {
            a = b;
            b = f.getVertex(i);
            result.intersection(vCross(a, b, f.getCenter()));
            if (result.degenerate()) return result;
        }
        return result;
    }
    private Segment hCross(Facet f, R3Vector a) {
        R3Vector n = f.getNormal();
        if (R3Vector.scalMul(n, pr) < 0.0) n.mul(-1);
        return crossWith(a, n);
    }
    private Segment vCross(R3Vector a, R3Vector b, R3Vector c) {
        R3Vector n = R3Vector.vectMul(R3Vector.minus(b,a), pr);
        if (R3Vector.scalMul(n, R3Vector.minus(a,c)) < 0.0) n.mul(-1);
        return crossWith(a, n);
    }
    private Segment crossWith(R3Vector a, R3Vector n) {
        double f0 = R3Vector.scalMul(n, R3Vector.minus(begin, a));
        double f1 = R3Vector.scalMul(n, R3Vector.minus(end, a));
        if(Math.abs(f0) < EPSILON) f0 = 0.;
        if(Math.abs(f1) < EPSILON) f1 = 0.;
        if(f0 >= 0.  f1 >= 0.) return new Segment(t1, t0);
        if(f0 < 0.   f1 < 0. ) return new Segment(t0, t1);
        double t =  - f0 / (f1 - f0);
        if (f0 < 0.) return new Segment(t0, t);
        return new Segment(t, t1);
    }

Договоримся задавать полупространство, пересечения ребра с которым необходимо уметь вычислять, точкой $$A$$ на ограничивающей его плоскости и вектором внешней нормали $$\vec n$$ к этой плоскости. Не разбираясь пока в деталях реализации метода crossWith с указанными аргументами (в тексте программы это точка a и вектор нормали n ), обсудим задачу нахождения одномерной тени, решаемую методами shadow, hCross и vCross.

Для реализации метода hCross нахождения пересечения с горизонтальным полупространством нужно найти нормаль к грани (выбрав правильное направление) и вызвать метод crossWith. Ясно, что из соображений эффективности операцию вычисления нормали к грани лучше выполнить только один раз, что и делается в конструкторе класса Facet. Направление нормали будет правильным при условии положительности скалярного произведения ее с вектором проектирования. Если это условие нарушено, то вектор $$\vec n$$ следует заменить на противоположный, умножив его на минус единицу.

Вычисление пересечения с вертикальным полупространством с помощью метода vCross удастся реализовать, зная соответствующее ребро грани (точки $$A$$ и $$B$$ ) и центр грани $$C$$. Точку $$A$$ при этом можно использовать в качестве аргумента для вызова метода crossWith непосредственно, а нормаль к вертикальной плоскости, проходящей через ребро $$AB$$, находится следующим образом. Сначала вычисляется векторное произведение вектора $$\overrightarrow{AB}$$ и вектора проектирования. Результат этой операции заведомо является нормалью, но может быть направлен не в ту сторону — не наружу, а внутрь.

Так как все грани полиэдра по самому его определению являются выпуклыми многоугольниками, центр каждой из граней всегда находится внутри ее. По этой причине вектор $$\overrightarrow{AC}$$ всегда направлен внутрь вертикального полупространства, пересечение с которым мы ищем. Если точки $$A$$ и $$B$$ являются вершинами грани, следующими друг за другом в порядке против часовой стрелки, то положительность скалярного произведения векторов $$\overrightarrow{AC}$$ и вычисленного по указанному выше способу вектора нормали гарантирует нужное направление последнего. В противном случае необходимо его умножение на минус единицу. Вычисление центра грани также целесообразно производить единственный раз — в конструкторе класса Facet.

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

Для невертикальной грани сначала вычисляется отрезок result, являющийся результатом пересечения ребра с горизонтальным полупространством. Затем последовательно для всех ребер грани индуктивно находится пересечение текущего значения величины result с вертикальными полупространствами, соответствующими ребрам. При этом вычисления немедленно прекращаются, если будет получен вырожденный отрезок.

Теперь нам осталось разобраться с последним из методов класса ShadowDrawer — методом crossWith нахождения пересечения ребра $$PQ$$ с полупространством, заданным точкой $$A$$ и вектором внешней нормали $$\vec n$$. В одномерных координатах точкам $$P$$ и $$Q$$ соответствует ноль и единица, а результатом работы метода должны быть одномерные координаты отрезка пересечения.

Рассмотрим функцию $$f(x) = <\vec n, \overrightarrow{AX}>$$, где $$X$$ — точка на ребре $$PQ$$, $$x$$ — ее одномерная координата, а угловые скобки означают скалярное произведение двух векторов. Эта функция линейна и является знакопостоянной на отрезке $$[0,1]$$ в том случае, если ребро $$PQ$$ целиком лежит вне или внутри полупространства. В случае пересечения ребра с плоскостью, ограничивающей полупространство, одномерная координата точки пересечения находится из уравнения $$f(x) = 0$$.

Из вышесказанного следует, что задача нахождения пересечения ребра с полупространством может быть решена таким образом. Вычислим сначала значения функции $$f(x)$$ в концах отрезка — $$f(0)$$ и $$f(1)$$ (в программной реализации им соответствуют величины f0 и f1 ). Если обе эти величины неотрицательны, то пересечение пусто, если, наоборот, они обе отрицательны, то весь отрезок лежит в полупространстве.

Для определения одномерной координаты точки пересечения решим уравнение $$f(x) = 0$$. В силу линейности функции $$f(x)$$ она имеет вид $$f(x) = \alpha + \beta x$$. Так как $$f(0) = \alpha$$, а $$f(1) = \alpha + \beta$$, то коэффициенты $$\alpha$$ и $$\beta$$ легко определяются. После этого легко вычислить и корень указанного уравнения: $$\alpha = f(0)$$, $$\beta = f(1) - f(0)$$, $$x = -f(0)/(f(1) - f(0))$$.

Теперь остается выбрать только нужную часть отрезка, что и реализовано в двух последних строках тела метода crossWith. В заключение обсудим назначение третьей и четвертой строк, в которых используется малая по абсолютной величине константа EPSILON. Их роль — попытка исправить те ошибки в работе метода, которые обусловлены приближенными вычислениями с числами типа double. В результате ошибок округления некоторые видимые части ребер не изображаются и наоборот.

Добавление указанных операторов несколько улучшает ситуацию, не исправляя ее полностью. Одна из задач, предназначенных для самостоятельного решения, предлагает справиться с этой проблемой.

Некоторые технологические вопросы и оптимизация

Рассматриваемый нами проект является достаточно объемным, а его исходные тексты размещены во многих файлах, что делает особенно полезным использование ряда технологических приемов при работе с ним.

Применение утилиты make значительно облегчает решение задач на модификацию. Используемый в проекте Makefile позволяет корректно откомпилировать текст программы и стартовать процесс построения изображения любого из более, чем 15 полиэдров, файлы с данными которых содержатся в директории data. Для построения изображения куба, например, достаточно выполнения команды make cube.

Большая часть возможностей утилиты make, используемых в данном проекте, уже была рассмотрена ранее. Обсудим лишь те действия, которые выполняются при построении цели doc. Вот соответствующий фрагмент из управляющего файла.

doc:
        javadoc -d doc -version -author -private *.java

Задание на построение цели doc сводится, как видно из этой записи, к вызову утилиты javadoc, являющейся частью исполняющей среды языка Java, с определенным набором параметров. Эта утилита предназначена для автоматической генерации документации, связанной с программным проектом.

При самом первом знакомстве с языком Java было отмечено, что в нем предусмотрены три вида комментариев, из которых до сих пор мы имели дело только с двумя. Многострочный комментарий, начинающийся с символов /** и завершающийся символами */ предназначен для размещения в тексте программы комментариев, которые могут быть впоследствии обработаны утилитой javadoc с целью построения полноценной документации о программе.

Правильное использование этой утилиты позволяет получить описание классов и пакетов в гипертекстовом формате (на языке HTML), что чрезвычайно удобно. Программист обязан, конечно, при написании программы размещать в ее тексте необходимые директивы, распознаваемые утилитой javadoc. Все они, впрочем, имеют очень простые синтаксис и семантику.

(рис 13.7) Фрагмент результата работы утилиты

Приведенный в последней секции параграфа полный текст проекта иллюстрирует использование таких комментариев. Иерархия классов проекта "Изображение полиэдра", приведенная выше на рисунке 13.3, является результатом работы утилиты javadoc. На рис. 13.7 можно увидеть еще один из фрагментов документации о проекте — часть алфавитного списка всех компонент, методов и классов.

Как легко убедиться, построенная программа работает достаточно медленно при построении изображения сложного полиэдра, хотя мы и пытались уже в процессе ее создания заботиться об эффективности. Сейчас будет предложен метод, носящий имя фильтрование граней \label{filtr}, который позволяет заметно ускорить работу программы.

Основное время в процессе нахождения плоского изображения полиэдра при обработке очередного ребра тратится на вычисление тени от всех граней. При этом значительная часть работы выполняется заведомо впустую — далекие от ребра грани не могут его затенить. Эта пустая трата времени тем значительнее, чем больше число ребер и граней, так как в этом случае больший процент граней можно было не рассматривать. Попробуем найти простой способ, который позволил бы разделить все грани на "далекие" и "близкие".

Хорошей идеей является попытка использования ограничивающих прямоугольников для проекций ребер и граней. Во-первых, ограничивающие прямоугольники легко быстро вычислить, а во-вторых, легко проверить, пересекаются ли два таких прямоугольника. Отсутствие пересечения прямоугольников при этом гарантирует отсутствие тени от грани на ребро.

Реализация описанной идеи позволяет заменить для большинства граней ресурсоемкую операцию нахождения тени от нее на определение пересечения ее ограничивающего прямоугольника (который должен вычисляться еще в конструкторе) с ограничивающим прямоугольником ребра (который тоже может быть вычислен заранее). Только в случае их пересечения необходимо будет вызывать метод вычисления тени.

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

Другим возможным способом оптимизации построения изображения является использование двумерного хеширования граней. Основной целью этого метода является полный отказ от рассмотрения "далеких" граней при построении изображения конкретного ребра.

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

Класс SmartDrawer, полный текст которого приведен ниже, реализует эту идею.

Задачи для самостоятельного решения

Задача 13.2. Оптимизируйте текст эталонного проекта "Изображение полиэдра" так, чтобы:

a) устранить многократное изображение видимых частей ребер, связанное с особенностями задания полиэдра, описанными в начале лекции;

b) реализовать идею фильтрования граней, базируясь на реализации класса ShadowDrawer ;

c) минимизировать действия, выполняемые в классе ShadowDrawer, связанные с необходимостью обращения (умножения на минус единицу) нормалей к граням;

d) максимально исправить влияние ошибок округления при работе с числами, заданными в форме с плавающей точкой, которые приводят к тому, что отдельные видимые части ребер не изображаются и наоборот.

Задача 13.3. Модифицируйте текст эталонного проекта "Изображение полиэдра" так, чтобы:

a) невидимые части ребер изображались пунктиром;

b) грани, имеющие максимальную площадь среди полностью видимых граней, заштриховывалась;

c) изображалась только та часть полиэдра, которая попадает в полупространство, заданное с помощью точки на ограничивающей его плоскости и вектора внешней нормали (эта задача эквивалентна построению изображения предварительно усеченного полиэдра);

d) изображалось сечение полиэдра предварительно заданной плоскостью;

e) строились плоские изображения четырехмерных полиэдров.

Задача 13.4. Модифицируйте текст эталонного проекта "Изображение полиэдра" так, чтобы вычислялась и печаталась следующая величина:

a) количество видимых вершин;

b) количество полностью видимых ребер;

c) количество частично видимых ребер;

d) количество полностью видимых граней;

e) количество частично видимых граней;

f) количество ребер, удаленных от начала координат не более, чем на единичное растояние;

g) количество граней, удаленных от начала координат не более, чем на единичное растояние;

h) количество граней полиэдра, пересекающихся с заданным отрезком;

i) количество ребер полиэдра, пересекающихся с заданным отрезком;

j) периметр проекции полиэдра;

k) площадь проекции полиэдра.

Полный текст проекта

Сначала приведем содержание управляющего файла для утилиты make.

MAKEFILE

# -*- mode: makefile -*-

.PHONY : all doc clean veryclean cube ccc sticks tagl king dragon            \
         x29 apple glass4 torus pear vw bones beeth teapot cow babem

JAVAC      = javac

JAVAFILES  := Xterm.java R3Vector.java Vertex.java Edge.java Facet.java      \
              Polyedr.java Segment.java L1ListSegments.java                  \
              AwtDrawer.java SimpleDrawer.java ShadowDrawer.java             \
              SmartDrawer.java PolyedrTest.java

CLASSFILES := Xterm.class R3Vector.class Vertex.class Edge.class Facet.class \
              Polyedr.class Segment.class L1ListSegments.class               \
              AwtDrawer.class SimpleDrawer.class ShadowDrawer.class          \
              SmartDrawer.class PolyedrTest.class

%.class: %.java
	$(JAVAC) $<

cube:	$(CLASSFILES)
	java PolyedrTest data/cube.geom

ccc:	$(CLASSFILES)
	java PolyedrTest data/ccc.geom

sticks:	$(CLASSFILES)
	java PolyedrTest data/sticks.geom

tagl:	$(CLASSFILES)
	java PolyedrTest data/tagl.geom

king:	$(CLASSFILES)
	java PolyedrTest data/king.geom

dragon:	$(CLASSFILES)
	java PolyedrTest data/dragon.geom

x29:	$(CLASSFILES)
	java PolyedrTest data/x29.geom

apple:	$(CLASSFILES)
	java PolyedrTest data/apple_logo.geom

glass4:	$(CLASSFILES)
	java PolyedrTest data/glass4.geom

torus:	$(CLASSFILES)
	java PolyedrTest data/torus.geom

pear:	$(CLASSFILES)
	java PolyedrTest data/pear.geom

vw:	$(CLASSFILES)
	java PolyedrTest data/vw.geom

bones:	$(CLASSFILES)
	java PolyedrTest data/foot_bones.geom

beeth:	$(CLASSFILES)
	java PolyedrTest data/beethoven.geom

teapot:	$(CLASSFILES)
	java PolyedrTest data/teapot.geom

cow:	$(CLASSFILES)
	java PolyedrTest data/cow.geom

babem:	$(CLASSFILES)
	java PolyedrTest data/babem.geom

doc:
	javadoc -d doc -version -author -private *.java

expand:	
	for i in Makefile *.java; do expand $$i >$$i.expand; done

clean:
	rm -f *.class *.expand

veryclean:
	rm -f *.class *.expand doc/*.html

А вот как выглядят все остальные файлы проекта.

R3Vector.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс R3Vector, реализующий вектор (Vector) в пространстве (R3).
 */
public class R3Vector {
    /**
     * Координаты вектора.
     */
     private double x, y, z;

    /**
     * Конструктор вектора, заданного его координатами.
     * @param x X-координата вектора.
     * @param y Y-координата вектора.
     * @param z Z-координата вектора.
     */
    public R3Vector(double x, double y, double z) {
	this.x = x; this.y = y; this.z = z;
    }
    /**
     * Конструктор вектора, координаты которого вводятся с клавиатуры.
     * @exception Exception Исключительная ситуация, возникающая
     * при ошибках ввода координат с клавиатуры.
     */
    public R3Vector() throws Exception {
	x = Xterm.inputDouble("X-коорд. вектора проектирования -> ");
	y = Xterm.inputDouble("Y-коорд. вектора проектирования -> ");
	z = Xterm.inputDouble("Z-коорд. вектора проектирования -> ");
    }
    /**
     * Получить X-координату вектора.
     * @return X-координата вектора.
     */
    public final double getX() {
	return x;
    }
    /**
     * Получить Y-координату вектора.
     * @return Y-координата вектора.
     */
    public final double getY() {
	return y;
    }
    /**
     * Получить Z-координату вектора.
     * @return Z-координата вектора.
     */
    public final double getZ() {
	return z;
    }
     /**
     * Нормировать ненулевой вектор.
     */
    public final R3Vector normalize() {
	double norm = Math.sqrt(x*x+y*y+z*z);
	x /= norm; y /= norm; z /= norm;
	return this;
    }
    /**
     * Найти сумму двух векторов.
     * @param a Первый вектор-слагаемое.
     * @param b Второй вектор-слагаемое.
     * @return Сумма векторов.
     */
    public static R3Vector plus(R3Vector a, R3Vector b) {
	return new R3Vector(a.x+b.x, a.y+b.y, a.z+b.z);
    }
    /**
     * Добавить заданный вектор.
     * @param b Добавляемый вектор.
     * @return Вектор-результат.
     */
    public final R3Vector plus(R3Vector b) {
	x += b.x; y += b.y; z += b.z;
	return this;
    }
    /**
     * Найти разность двух векторов.
     * @param a Вектор-уменьшаемое.
     * @param b Вектор-вычитаемое.
     * @return Разность векторов.
     */
    public static R3Vector minus(R3Vector a, R3Vector b) {
	return new R3Vector(a.x-b.x, a.y-b.y, a.z-b.z);
    }
    /**
     * Вычесть заданный вектор.
     * @param b Вычитаемый вектор.
     * @return Вектор-результат.
     */
    public final R3Vector minus(R3Vector b) {
	x -= b.x; y -= b.y; z -= b.z;
	return this;
    }
    /**
     * Найти произведение вектора на число.
     * @param k Число, на которое умножается вектор.
     * @param a Исходный вектор.
     * @return Вектор-результат.
     */
    public static R3Vector mul(double k, R3Vector a) {
	return new R3Vector(k*a.x, k*a.y, k*a.z);
    }
    /**
     * Умножить вектор на заданное число.
     * @param k Число, на которое умножается вектор.
     * @return Вектор-результат.
     */
    public final R3Vector mul(double k) {
	x *= k; y *= k; z *= k;
	return this;
    }
    /**
     * Найти скалярное произведение векторов.
     * @param a Первый вектор.
     * @param b Второй вектор.
     * @return Скалярное произведение векторов.
     */
    public static double scalMul(R3Vector a, R3Vector b) {
	return a.x*b.x + a.y*b.y + a.z*b.z;
    }
    /**
     * Найти векторное произведение векторов.
     * @param a Первый вектор.
     * @param b Второй вектор.
     * @return Векторное произведение векторов.
     */
    public static R3Vector vectMul(R3Vector a, R3Vector b) {
	return new R3Vector(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x);
    }
}

Vertex.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Vertex, реализующий вершину полиэдра.
 */
public class Vertex extends R3Vector {
    /**
     * Конструктор.
     * @param x X-координата вершины.
     * @param y Y-координата вершины.
     * @param z Z-координата вершины.
     */
    public Vertex(double x, double y, double z) {
	super(x, y, z);
    }
}

Edge.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Edge, реализующий ребро полиэдра.
 */
public class Edge {
    /**
     * Начало ребра.
     */
    private Vertex begin;
    /**
     * Конец ребра.
     */
    private Vertex end;

    /**
     * Конструктор.
     * @param begin Начало ребра.
     * @param end Конец ребра.
     */
    public Edge(Vertex begin, Vertex end) {
	this.begin = begin; this.end = end;
    }
    /**
     * Получить начало ребра.
     * @return Начало ребра.
     */
    public final Vertex getBegin() {
	return begin;
    }
    /**
     * Получить конец ребра.
     * @return Конец ребра.
     */
    public final Vertex getEnd() {
	return end;
    }
}

Facet.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Facet, реализующий грань полиэдра.
 */
public class Facet {
    /**
     * Массив вершин полиэдра, принадлежащих грани.
     */
    private Vertex[] vertexes;
    /**
     * Центр грани.
     */
    private R3Vector center;
    /**
     * Вектор нормали к грани.
     */
    private R3Vector normal;
    
    /**
     * Конструктор.
     * @param vertexes Вершины полиэдра, образующие грань.
     */
    public Facet(Vertex[] vertexes) {
	this.vertexes = vertexes;
	normal = R3Vector.vectMul(R3Vector.minus(vertexes[1], vertexes[0]),
				   R3Vector.minus(vertexes[2], vertexes[0]));
	center = new R3Vector(0., 0., 0.);
	for (int i=0; i<vertexes.length; i++) {
	    center.plus(vertexes[i]);
	}
	center.mul(1./(double)vertexes.length);
    }
    /**
     * Получить количество вершин.
     * @return Количество вершин грани.	
     */
    public final int getVertexesQuantity() {
	return vertexes.length;
    }
    /**
     * Получить вершину.
     * @param i Номер вершины.
     * @return Вершина грани.
     */
    public final Vertex getVertex(int i) {
	return vertexes[i];
    }
    /**
     * Получить центр грани.
     * @return Центр грани.
     */
    public final R3Vector getCenter() {
	return center;
    }
    /**
     * Получить нормаль к грани.
     * @return Нормаль грани.
     */
    public final R3Vector getNormal() {
	return normal;
    }
    /**
     * Является ли грань "вертикальной"?
     * @param pr Вектор проектирования.
     * @return Параллельна ли грань вектору проектирования?
     */
    public final boolean vertical(R3Vector pr) {
	return R3Vector.scalMul(normal, pr) == 0.;
    }
}

Polyedr.java

import java.util.*;
import java.io.*;
/**
 * @author Е.А. Роганов
 * @version 1.1
 * Класс Polyedr, реализующий полиэдр.
 */
public class Polyedr {
    /**
     * Массив вершин полиэдра.
     */
    private Vertex[] vertexes;
    /**
     * Массив ребер полиэдра.
     */
    private Edge[]   edges;
    /**
     * Массив граней полиэдра.
     */
    private Facet[]  facets;

    /**
     * Конструктор класса.
     * @param file Файл, содержащий описание полиэдра.
     * @exception Exception Исключительная ситуация, возникающая при
     * ошибках чтения или преобразования данных.
     */
    public Polyedr(String file) throws Exception {
	RandomAccessFile f = new RandomAccessFile(file, "r");
	StringTokenizer st = new StringTokenizer(f.readLine());

	vertexes  = new Vertex[Integer.parseInt(st.nextToken())];
	facets = new Facet[Integer.parseInt(st.nextToken())];
	edges  = new Edge[Integer.parseInt(st.nextToken())];	
	
	for (int i=0; i<vertexes.length; i++) {
	    st = new StringTokenizer(f.readLine());
	    double x = Double.valueOf(st.nextToken()).doubleValue();
	    double y = Double.valueOf(st.nextToken()).doubleValue();
	    double z = Double.valueOf(st.nextToken()).doubleValue();
	    vertexes[i] = new Vertex(x,y,z);
	}
	int k = 0;
	for (int i=0; i<facets.length; i++) {
	    st = new StringTokenizer(f.readLine());
	    int size = Integer.parseInt(st.nextToken());
	    Vertex[] facet = new Vertex[size];
	    facet[0] = vertexes[Integer.parseInt(st.nextToken()) - 1];
	    for (int j=1; j<size; j+=1) {
		facet[j] = vertexes[Integer.parseInt(st.nextToken()) - 1];
		edges[k++] = new Edge(facet[j], facet[j-1]);
	    }
	    edges[k++] = new Edge(facet[size-1], facet[0]);
	    facets[i] = new Facet(facet);
	}
    }
    /**
     * Получить количество вершин.
     * @return Количество вершин полиэдра.	
     */
    public final int getVertexesQuantity() {
	return vertexes.length;
    }
    /**
     * Получить вершину.
     * @param i Номер вершины.
     * @return Вершина полиэдра.
     */
    public final Vertex getVertex(int i) {
	return vertexes[i];
    }
    /**
     * Получить количество ребер.
     * @return Количество ребер полиэдра.	
     */
    public final int getEdgesQuantity() {
	return edges.length;
    }
    /**
     * Получить ребро.
     * @param i Номер ребра.
     * @return Ребро полиэдра.
     */
    public final Edge getEdge(int i) {
	return edges[i];
    }
    /**
     * Получить количество граней.
     * @return Количество граней полиэдра.	
     */
    public final int getFacetsQuantity() {
	return facets.length;
    }
    /**
     * Получить грань.
     * @param i Номер грани.
     * @return Грань полиэдра.
     */
    public final Facet getFacet(int i) {
	return facets[i];
    }
}

Segment.java

/**
 * @author Е.А. Роганов
 * @version 1.q	
 * Класс Segment, реализующий одномерный отрезок.
 */
public class Segment {
    /**
     * Координаты начала и конца отрезка.
     */
    private double begin, end;

    /**
     * Конструктор отрезка.
     * @param begin Начало отрезка.
     * @param end Начало отрезка.
     */
    public Segment(double begin, double end) {
	this.begin = begin; this.end = end;
    }
    /**
     * Вырожден ли отрезок?
     * @return Вырожден ли отрезок?
     */
    public final boolean degenerate() {
	return begin >= end;
    }
    /**
     * Найти левый отрезок разности с отрезком s.
     * @param s Вычитаемый отрезок.
     * @return Левый отрезок разности.
     */
    public final Segment leftSub(Segment s) {
	return new Segment(begin, Math.min(end, s.begin));
    }
    /**
     * Найти правый отрезок разности с отрезком s.
     * @param s Вычитаемый отрезок.
     * @return Правый отрезок разности.
     */
    public final Segment rightSub(Segment s) {
	return new Segment(Math.max(begin, s.end), end);
    }
    /**
     * Найти пересечение с отрезком s.
     * @param s Отрезок, с которым находится пересечение.
     * @return Отрезок-пересечение.
     */
    public final Segment intersection(Segment s) {
	begin = Math.max(begin, s.begin);
	end = Math.min(end, s.end);
	return this;
    }
    /**
     * Получить начало отрезка.
     * @return Начало отрезка.
     */
    public final double getBegin() {
	return begin;
    }
    /**
     * Получить конец отрезка.
     * @return Конец отрезка.
     */
    public final double getEnd() {
	return end;
    }
}

L1ListSegments.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс L1ListSegments, реализующий односвязный список отрезков.
 */
public class L1ListSegments {
    /**
     * Массив отрезков.
     */
    private Segment[] array;
    /**
     * Массив ссылок.
     */
    private int[] next;
    /**
     * Нил списка.
     */
    private int nilList;
    /**
     * Нил свободного места.
     */
    private int nilFree;
    /**
     * Элемент до указателя.
     */
    private int before;
    /**
     * Элемент за указателем.
     */
    private int after;
    /**
     * Связать два элемента.
     * @param first Первый элемент.
     * @param second Второй элемент.
     */
    private void link(int first, int second) {
        next[first] = second;
    }
    /**
     * Захватить место.
     * @return Индекс занимаемого элемента.
     */
    private int mallocIndex() {
        int index = next[nilFree];
        link(nilFree, next[index]);
        return index;
    }
    /**
     * Освободить место.
     * @param index Индекс освобождаемого элемента.
     */
    private void freeIndex(int index) {
        link(index, next[nilFree]);
        link(nilFree, index);
    }

    /** 
     * Конструктор класса.
     * @param size Максимальный размер списка.
     */
    public L1ListSegments(int size) {
        array = new Segment[size];
        next  = new int[size + 2];
        nilList = size;
        nilFree = size + 1;

        link(nilList, nilList);

        link(nilFree, 0);
        for (int i=0; i<size-1; i++)
            link(i, i+1);
        link(size-1, nilFree);

        before = after = nilList;
    }
    /** 
     * Пуст ли список?
     * @return Пуст ли список?
     */
    public final boolean empty() {
        return next[nilList] == nilList;
    }
    /** 
     * Сделать список пустым.
     */
    public final void clear() {
        try {
            toFront();
            while(true)
               erase();
        } catch(Exception e) {
            ;
        }
    }
    /** 
     * Передвинуть указатель в начало списка.
     */
    public final void toFront() {
        before = nilList;
        after  = next[nilList];
    }
    /** 
     * Указатель в конце списка?
     * @return Указатель в конце списка?
     */
    public final boolean end() {
        return after == nilList;
    }
    /** 
     * Передвинуть указатель вперед.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке передвинуть вперед указатель, находящийся в конце списка.
     */
    public final void forward() throws Exception {
        if (after == nilList) throw new Exception();
        before = after;
        after  = next[after];
    }
    /** 
     * Получить элемент за указателем.
     * @return Элемент за указателем.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке получить элемент за указателем, находящимся в конце списка.
     */
    public final Segment after() throws Exception {
        return array[after];
    }
    /** 
     * Добавить элемент за указателем.
     * @param val Включаемый отрезок (сегмент).
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке добавить элемент в заполненный до конца список.
     */
    public final void insert(Segment val) throws Exception {
        int index = mallocIndex();
        link(before, index);
        link(index, after);
        after = index;
        array[index] = val;
    }
    /** 
     * Удалить элемент за указателем.
     * @return Удаляемый элемент.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке удалить элемент из пустого списка.
     */
    public final Segment erase() throws Exception {
        Segment val   = array[after];
        int index = after;
        after = next[index];
        link(before, after);
        freeIndex(index);
        return val;
    }
}

AwtDrawer.java

import java.awt.*;
/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс AwtDrawer, обеспечивающий визуализацию
 * плоского изображения.
 */
public class AwtDrawer extends Frame {
    /**
     * Ширина области рисования.
     */
    private static final int XLEN = 500;
    /**
     * Высота области рисования.
     */
    private static final int YLEN = 500;
    /**
     * Ширина "полей" вокруг области рисования.
     */
    private static final int DELTA= 100;
    /**
     * Внеэкранный буфер.
     */
    private Image offScrImage;
    /**
     * Графический контекст внеэкранного буфера.
     */
    private Graphics offScrGC;
    /**
     * Графический контекст фрейма на экране.
     */
    private Graphics g;
    /**
     * Конструктор класса.
     */
    public AwtDrawer() {
	super("Построение изображения полиэдра");
	setSize(XLEN+2*DELTA, YLEN+2*DELTA);
	setBackground(Color.white);
	g = getGraphics();
	show();

	offScrImage = createImage(XLEN+2*DELTA, YLEN+2*DELTA);
	offScrGC = offScrImage.getGraphics();
        offScrGC.setColor(Color.white);
        offScrGC.fillRect(0,0,XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC.setColor(Color.black);
     }
    /**
     * Изобразить отрезок на плоскости, заданный его концами.
     * @param xb X-координата начала отрезка ( 0 <= xb <= 1 ).
     * @param yb Y-координата начала отрезка ( 0 <= yb <= 1 ).
     * @param xe X-координата конца  отрезка ( 0 <= xe <= 1 ).
     * @param ye Y-координата конца  отрезка ( 0 <= ye <= 1 ).
     */
    public final void draw(double xb, double yb, double xe, double ye) {
	int x0 = DELTA + (int)(XLEN * xb);
	int y0 = DELTA + (int)(YLEN * yb);
	int x1 = DELTA + (int)(XLEN * xe);
	int y1 = DELTA + (int)(YLEN * ye);
	offScrGC.drawLine(x0, y0, x1, y1);
    }
    /**
     * Переизобразить содержимое фрейма.
     */
    public void update(Graphics g) {
	paint(g);
    }
    public void paint(Graphics g) {
	g.drawImage(offScrImage, 0, 0, this); 
    }
}

SimpleDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс SimpleDrawer, обеспечивающий изображение проекции полиэдра.
 */
public class SimpleDrawer extends AwtDrawer {
    /**
     * Полиэдр.
     */
    protected Polyedr p;
    /**
     * Единичный вектор проектирования.
     */
    protected R3Vector pr;
    /**
     * Единичный X-вектор плоскости проектирования.
     */
    private R3Vector x;
    /**
     * Единичный Y-вектор плоскости проектирования.
     */
    private R3Vector y;
    /**
     * Минимальная X-координата проекции полиэдра.
     */
    private double xmin;
    /**
     * Минимальная Y-координата проекции полиэдра.
     */
    private double ymin;
    /**
     * Размер проекции полиэдра.
     */
    private double size;
    /**
     * Вычислить X-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return X-координата проекции этого вектора.
     */
    private double xProection(R3Vector v) {
	return R3Vector.scalMul(v, x);
    }
    /**
     * Вычислить Y-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Y-координата проекции этого вектора.
     */
    private double yProection(R3Vector v) {
	return R3Vector.scalMul(v, y);
    }
    /**
     * Вычислить нормализованную X-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Нормализованная X-координата проекции этого вектора.
     */
    protected double xnProection(R3Vector v) {
	return (xProection(v) - xmin)/size;
    }
    /**
     * Вычислить нормализованную Y-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Нормализованная Y-координата проекции этого вектора.
     */
    protected double ynProection(R3Vector v) {
	return (yProection(v) - ymin)/size;
    }

    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public SimpleDrawer(Polyedr p, R3Vector pr, double angle) {
	this.p  = p;
	this.pr = pr.normalize();
	double a = pr.getX();
	double b = pr.getY();
	double c = pr.getZ();
	if (a != 0. || b != 0.) {
	    x = new R3Vector(-b, a, 0.);
	} else {
	    x = new R3Vector(0., c, -b);
	}
	y = R3Vector.vectMul(x, pr);
	x.normalize();
	y.normalize();
	R3Vector nx = R3Vector.plus( R3Vector.mul(Math.cos(angle), x),
				     R3Vector.mul(-Math.sin(angle), y) );
	R3Vector ny = R3Vector.plus( R3Vector.mul(Math.sin(angle), x),
				     R3Vector.mul(Math.cos(angle), y) );
	x = nx;
	y = ny;
	xmin = ymin = Double.MAX_VALUE;	
	double xmax, ymax;
	xmax = ymax = Double.MIN_VALUE;	
	for (int i=0; i<p.getVertexesQuantity(); i++) {
	    double xi = xProection(p.getVertex(i));
	    double yi = yProection(p.getVertex(i));
	    if (xi < xmin) xmin = xi;
	    if (yi < ymin) ymin = yi;
	    if (xi > xmax) xmax = xi;
	    if (yi > ymax) ymax = yi;
	}
	size = ymax - ymin;
	if (xmax - xmin < size) size = xmax - xmin;
    }
    /**
     * Изобразить проекцию полиэдра.
     */
    public final void draw() {
	for (int i=0; i<p.getEdgesQuantity(); i++)
	    drawEdge(p.getEdge(i));
	Xterm.print("\n");
    }	
    /**
     * Изобразить ребро полиэдра.
     * @param s Обрабатываемое ребро полиэдра.
     */
    public void drawEdge(Edge s) {
	Vertex begin = s.getBegin();
	Vertex end   = s.getEnd();
	draw(xnProection(begin), ynProection(begin),
	     xnProection(end), ynProection(end));
	Xterm.print(".");
    } 
}

ShadowDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс ShadowDrawer, обеспечивающий изображение проекции 
 * полиэдра с удалением невидимых линий.
 */
public class ShadowDrawer extends SimpleDrawer {
    /**
     * Начало изображаемого ребра.
     */
    protected R3Vector begin;
    /**
     * Конец изображаемого ребра.
     */
    protected R3Vector end;
    /**
     * Максимальный размер списка.
     */
    private static final int MAXSIZE = 128;
    /**
     * Односвязный список видимых отрезков ребра.
     */
    private L1ListSegments list;
    /**
     * Достаточно малая константа, предназначенная для
     * решения проблемы неточного представления действительных
     * чисел на ЭВМ.
     */
    private static final double EPSILON = 1.e-12;
    /**
     * Одномерная координата начала обрабатываемого ребра.
     */
    private static final double t0 = 0.;
    /**
     * Одномерная координата конца обрабатываемого ребра.
     */
    private static final double t1 = 1.;
    /**
     * Вычислить пространственные координаты точки ребра,
     * заданной одномерной координатой.
     * @param t Одномерная координата точки на ребре.
     * @return Трехмерная координата этой точки ребра.
     */
    private R3Vector R3(double t) {
	return R3Vector.plus( R3Vector.mul((1.-t), begin),
			      R3Vector.mul(t, end) );
    }
    /**
     * Вычислить одномерный отрезок тени от грани.
     * @param f Грань полиэдра.
     * @return Отрезок тени от этой грани на ребре.
     */
    private Segment shadow(Facet f) {
	if (f.vertical(pr))
	    return new Segment(t1, t0);
	int n = f.getVertexesQuantity();
	Vertex a = f.getVertex(n-1);
	Vertex b = f.getVertex(0);
	Segment result = hCross(f, a); 
	if (result.degenerate()) return result;
	result.intersection(vCross(a, b, f.getCenter()));
        if (result.degenerate()) return result;
	for (int i=1; i<n; i++) {
	    a = b;
	    b = f.getVertex(i);
	    result.intersection(vCross(a, b, f.getCenter()));
	    if (result.degenerate()) return result;
	}
	return result;
    }
    /**
     * Вычислить пересечение с "горизонтальным" полупространством.
     * @param f Грань полиэдра.
     * @param a Точка (вектор) на грани.
     * @return Отрезок пересечения ребра с "горизонтальным" полупространством.
     */
    private Segment hCross(Facet f, R3Vector a) {
	R3Vector n = f.getNormal();
	if (R3Vector.scalMul(n, pr) < 0.0) n.mul(-1);
	return crossWith(a, n);
    }
    /**
     * Вычислить пересечение с "вертикальным" полупространством.
     * @param a Точка (вектор) на грани.
     * @param b Точка (вектор) на грани.
     * @param c Точка (вектор) на грани.
     * @return Отрезок пересечения ребра с "вертикальным" полупространством.
     */
    private Segment vCross(R3Vector a, R3Vector b, R3Vector c) {
	R3Vector n = R3Vector.vectMul(R3Vector.minus(b,a), pr);
	if (R3Vector.scalMul(n, R3Vector.minus(a,c)) < 0.0) n.mul(-1);
	return crossWith(a, n);
    }
    /**
     * Вычислить пересечение отрезка с заданным полупространством.
     * @param a Точка (вектор) на грани.
     * @param n Вектор внешней нормали к полупространству.
     * @return Отрезок пересечения ребра с полупространством.
     */
    private Segment crossWith(R3Vector a, R3Vector n) {
	double f0 = R3Vector.scalMul(n, R3Vector.minus(begin, a));
	double f1 = R3Vector.scalMul(n, R3Vector.minus(end, a));
	if(Math.abs(f0) < EPSILON) f0 = 0.;
	if(Math.abs(f1) < EPSILON) f1 = 0.;
	if(f0 >= 0.  f1 >= 0.) return new Segment(t1, t0);
	if(f0 < 0.   f1 < 0. ) return new Segment(t0, t1);
	double t =  - f0 / (f1 - f0);
	if (f0 < 0.) return new Segment(t0, t);
	return new Segment(t, t1);
    }
    /**
     * Учесть тень от одной грани.
     * @param f Грань, тень от которой должна быть учтена.
     */
    protected final void addShadow(Facet f) {
	try {
	    Segment s = shadow(f);
	    if (!s.degenerate()) {
		list.toFront();
		while (!list.end()) {
		    Segment next = list.erase();
		    Segment left = next.leftSub(s);
		    if (!left.degenerate()) {
			list.insert(left);
			list.forward();
		    }
		    Segment right = next.rightSub(s);
		    if (!right.degenerate()) {
			list.insert(right);
			list.forward();
		    }
		}
	    }
	} catch(Exception e) {
	    Xterm.println("Слишком много видимых отрезков ребра.");
	    System.exit(0);
        }
    }
    /**
     * Учесть тень от всех граней.
     */
    protected void addShadow() {
	for(int j=0; j<p.getFacetsQuantity(); j++)
	    addShadow(p.getFacet(j));
    }
    
    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public ShadowDrawer(Polyedr p, R3Vector pr, double angle) {
	super(p, pr, angle);
	list = new L1ListSegments(MAXSIZE);
    }
    /**
     * Изобразить видимую часть ребра полиэдра.
     * @param s Обрабатываемое ребро полиэдра.
     */
    public final void drawEdge(Edge s) {
	begin = s.getBegin();
	end = s.getEnd();
	list.clear();
	try {
	    list.insert(new Segment(t0, t1));
	} catch(Exception e) {;}
	addShadow();
	try {
	    for (list.toFront(); ! list.end(); list.forward()) {
		Segment u = list.after();
		R3Vector begin = R3(u.getBegin());
		R3Vector end   = R3(u.getEnd());
		draw(xnProection(begin), ynProection(begin),
		     xnProection(end), ynProection(end));
	    }
	} catch(Exception e) {;}
	Xterm.print(".");
    } 
}

SmartDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс SmartDrawer, обеспечивающий изображение проекции полиэдра
 * с использованием двумерного хеширования граней.
 */
public class SmartDrawer extends ShadowDrawer {
    /**
     * Количество гнезд сетки в строке и столбце.
     */
    private final static int SIZE = 50;
    /**
     * Максимальное число граней в гнезде.
     */
    private final static int MAXFACETS = 200;
    /**
     * Массив гнезд, в которые попадает обрабатываемое ребро.
     */
    private boolean[][] sockets;
    /**
     * Массив количеств граней в гнездах.
     */
    private int[][]    nmbFacets;
    /**
     * Массив множеств номеров граней.
     */
    private int[][][]  hashFacets;
    /**
     * Найти последний индекс гнезда, соответствующий t.
     * @param t Координата.
     */
    private int lastVal(double t) {
	return Math.min((int)(t*SIZE),SIZE-1);
    }
    /**
     * Найти первый индекс гнезда, соответствующий t.
     * @param t Координата.
     */
    private int firstVal(double t) {
	return (int)(t*SIZE);
    }

    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public SmartDrawer(Polyedr p, R3Vector pr, double angle) {
	super(p, pr, angle);
	nmbFacets  = new int[SIZE][SIZE];
	hashFacets = new int[SIZE][SIZE][MAXFACETS];
	int i,j,k;
	int imax = p.getFacetsQuantity();
 	for (i=0; i<imax; i++) {
	    Facet f=p.getFacet(i);
	    k = f.getVertexesQuantity();
	    double x0 = 1., y0 = 1., x1 = 0., y1 = 0.;
	    for (j=0; j<k; j++) {
		R3Vector v = f.getVertex(j);
		double x = xnProection(v);
		double y = ynProection(v);
	        if (x < x0) x0 = x;
	        if (y < y0) y0 = y;
	        if (x > x1) x1 = x;
	        if (y > y1) y1 = y;
	    }
	    int jm = lastVal(x1); 
	    int km = lastVal(y1);
	    for (j=firstVal(x0); j<=jm; j++)
		for (k=firstVal(y0); k<=km; k++)
		    hashFacets[ j ][ k ][ nmbFacets[j][k]++ ] = i;
	}
    }

    /**
     * Учесть тень от всех граней.
     */
    protected void addShadow() {
	int i,j,k;
	double x0 = Math.min(xnProection(begin), xnProection(end));
	double x1 = Math.max(xnProection(begin), xnProection(end));
	double y0 = Math.min(ynProection(begin), ynProection(end));
	double y1 = Math.max(ynProection(begin), ynProection(end));
	int jm = lastVal(x1);
	int km = lastVal(y1);

	for (j=firstVal(x0); j<=jm; j++)
	    for (k=firstVal(y0); k<=km; k++)
		for (i=0; i<nmbFacets[j][k]; i++)
		    addShadow(p.getFacet( hashFacets[j][k][i] ));
    }
}

PolyedrTest.java

import java.util.*;
import java.io.*;
/**
 * @author Е.А. Роганов
 * @version 1.1
 * Класс PolyedrTest, ...
 */
public class PolyedrTest {
    /**
     * Функция main.
     * @param args Массив аргументов командной строки. 
     * @exception Exception Исключительная ситуация, возникающая
     * при ошибках чтения или преобразования данных из файла.
     */
    public static void main(String[] args) throws Exception {
	Polyedr p = new Polyedr(args[0]);
	int type = Xterm.inputInt("Возможные типы изображения полиэдра:\n"+
			      "    без удаления невидимых линий - 0\n"+
			      "    с удалением невидимых ребер  - 1\n"+
			      "    с использованием хеширования - 2\n"+
			      "Выберите тип изображения (от 0 до 2) -> " );
	while (true) {
	    R3Vector pr = new R3Vector();
	    double angle = Xterm.inputDouble("Угол поворота в плоскости " +
					     "проекции (в градусах): ");
	    switch (type) {
	      case 0:
		SimpleDrawer d = new SimpleDrawer(p, pr, Math.PI*angle/180.0);
		d.draw();
		break;
	      case 1:
		ShadowDrawer sd = new ShadowDrawer(p, pr, Math.PI*angle/180.0);
		sd.draw();
		break;
	      case 2:
		SmartDrawer smd = new SmartDrawer(p, pr, Math.PI*angle/180.0);
		smd.draw();
		break;
	      default:
		Xterm.println("Неверный тип изображения");
	    }
	    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
	}
    }
}
Страницы:

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

В книгах [9] и [8] содержится большое количество дополнительной информации, связанной с рассматриваемой задачей и ее различными обобщениями. Вопросы работы с пакетом AWT и системой документирования в языке Java более подробно могут быть изучены с помощью книг [11] и [10] .

Постановка задачи

В настоящее время во многих областях науки и техники используются различные методы моделирования геометрических объектов в трехмерном пространстве. Один из способов моделирования состоит в том, чтобы аппроксимировать реальный объект набором выпуклых плоских многоугольников. Такой набор мы будем называть полиэдром , многоугольники — гранями, а их стороны — ребрами полиэдра.

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

(рис 13.1) Пример полиэдра

При изображении полиэдра реально рисуются не сами многоугольники, а линии, их ограничивающие, то есть их ребра. Если, однако, изобразить все ребра полиэдра, то получится весьма запутанная, непривычная для человека картина. Для получения удобного для человека рисунка нужно при построении изображения учитывать, что некоторые ребра могут оказаться полностью или частично невидимыми, так как их могут загородить грани полиэдра. Таким образом, при построении изображения надо удалить (не рисовать) части ребер, которые не видны. По этой причине рассматриваемая задача в литературе часто называется задачей удаления невидимых линий.

Задача 13.1. Напишите программу, которая строит изображение заданного полиэдра с удалением невидимых линий. Программа должна быть реализована в виде автономного приложения на языке Java и использовать возможности пакета AWT.

В результате решения этой задачи полиэдр должен изображаться примерно так, как это показано на рис. 13.1.

Изображение полиэдра зависит от того, откуда на него посмотреть. Это направления должно быть задано в точной постановке задачи. Направление, противоположное направлению взгляда, будем называть вектором проектирования и считать, что он направлен вверх. Этого всегда можно добиться, осуществив поворот полиэдра вместе с заданным вектором проектирования. Для того чтобы упростить терминологию, договоримся называть в дальнейшем параллельные вектору проектирования прямые и плоскости вертикальными, а ортогональные ему — горизонтальными.

Рассматриваемая задача, таким образом, сводится к следующей: построить проекцию полиэдра с удалением невидимых линий на бесконечно высокую горизонтальную плоскость $$z=H$$. Требуемое плоское изображение при этом задано не однозначно, так как вращения вокруг вертикальной оси $$Oz$$ приводят к поворотам итоговой картинки. Будем поэтому считать, что полная постановка задачи кроме самого полиэдра и вектора проектирования содержит также задание угла поворота в плоскости проекции.

(рис 13.2) Изображение полиэдра

Такую уточненную постановку задачи иллюстрирует рис. 13.2.

Способ задания вектора проектирования и угла поворота в плоскости проекции вполне ясен, а вот как следует задавать полиэдр нужно обсудить. Простейший способ — перечисление координат всех вершин каждой из его граней. В силу ряда недостатков такого представления, однако, этот способ на практике не применяется.

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

Способ представления полиэдра, который будет использован нами при решении рассматриваемой задачи, несколько отличен от только что описанного. Он предусматривает указание числа и координат вершин полиэдра, а также количества граней и ребер с указанием для каждой из граней номеров всех ее вершин. По этой причине многие ребра оказываются указанными дважды или даже большее число раз (так как принадлежат нескольким граням), что является, безусловно, недостатком этого способа представления.

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

Информация, представляющая полиэдр, является достаточно объемной, и поэтому было бы весьма неудобно вводить ее, используя клавиатуру. Гораздо разумнее создать описание полиэдра в специальном файле, а программе передать его имя. Вот как будет выглядеть этот файл, представляющий поверхность единичного куба с центром в начале координат и гранями, параллельными координатным плоскостям:

8       6       24
    -0.5    -0.5    0.5     
    -0.5    0.5     0.5     
    0.5     0.5     0.5     
    0.5     -0.5    0.5     
    -0.5    -0.5    -0.5    
    -0.5    0.5     -0.5    
    0.5     0.5     -0.5    
    0.5     -0.5    -0.5    
    4       1    2    3    4    
    4       5    6    2    1    
    4       3    2    6    7    
    4       3    7    8    4    
    4       1    4    8    5    
    4       8    7    6    5

Первая строка говорит о том, что у куба 8 вершин, 6 граней и 24 (!) ребра. Число 24, вдвое превышающее реальное количество ребер у куба, является результатом уже описанного выше эффекта, — каждое из ребер куба принадлежит ровно двум граням.

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

Технические детали работы с файлами и чтения информации из них могут быть изучены заинтересованными читателями самостоятельно с использованием итогового текста проекта, приведенного в конце параграфа, и литературы, содержащей описание библиотек (пакетов) языка Java.

Проектирование основных классов

В данной секции будет построена иерархия классов, необходимых для реализации рассматриваемого проекта. Здесь же мы рассмотрим (в самых общих чертах) использование библиотеки AWT программами, не являющимися аплетами.

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

public class R3Vector {
    private double x, y, z;

    public R3Vector(double x, double y, double z) {
        this.x = x; this.y = y; this.z = z;
    }
    public R3Vector() throws Exception {
        x = Xterm.inputDouble("X-коорд. вектора проектирования -> ");
        y = Xterm.inputDouble("Y-коорд. вектора проектирования -> ");
        z = Xterm.inputDouble("Z-коорд. вектора проектирования -> ");
    }
}

Для представления вершин полиэдра можно использовать выведенный из класса R3Vector класс Vertex. Конструктор данного класса обязан явным образом (с помощью ключевого слова super ) вызвать конструктор его базового класса R3Vector, так как иначе компилятор автоматически вставит вызов конструктора класса R3Vector без параметров.

public class Vertex extends R3Vector {
    public Vertex(double x, double y, double z) {
        super(x, y, z);
    }
}

Ребра полиэдра будем представлять с помощью класса Edge, в каждом экземпляре которого содержатся две private -компоненты типа Vertex, соответствующие началу и концу ребра. В подобных ситуациях для обеспечения возможности получения координат начала и конца ребра используют так называемые методы-селекторы getBegin и getEnd.

public class Edge {
    private Vertex begin;
    private Vertex end;

    public Edge(Vertex begin, Vertex end) {
        this.begin = begin; this.end = end;
    }
    public final Vertex getBegin() {
        return begin;
    }
    public final Vertex getEnd() {
        return end;
    }
}

Грани полиэдра будем описывать с помощью класса Facet. Так как грань задается последовательностью ее вершин, то в каждом экземпляре класса должен содержаться массив объектов типа Vertex, для получения количества и координат которых необходимо предусмотреть соответствующие методы.

public class Facet {
    private Vertex[] vertexes;
    public Facet(Vertex[] vertexes) {
        this.vertexes = vertexes;
    public final int getVertexesQuantity() {
        return vertexes.length;
    }
    public final Vertex getVertex(int i) {
        return vertexes[i];
    }

Полиэдр в целом, наконец, будем задавать с помощью класса Polyedr. Каждый экземпляр этого класса будет хранить в массивах vertexes, edges и facets все вершины, ребра и грани полиэдра. При этом оказывается, что часть информации хранится продублированной, так как каждая из граней полиэдра уже содержит все ее вершины. Устранение этого дублирования является еще одной из задач, предлагаемых в конце параграфа.

При создании нового экземпляра класса Polyedr с помощью конструктора вся информация о полиэдре, хранящаяся в файле file в описанном выше формате, должна быть прочитана из него и размещена должным образом в компонентах создаваемого экземпляра. Для реализации этих действий необходимо использование пакетов java.util и java.io, что обуславливает необходимость наличия директив import в начале файла. Все технические детали осуществления чтения информации из файла оставляются читателю, а полный исходный текст проектируемого класса приведен для справки в последней секции параграфа.

import java.util.*;
import java.io.*;
public class Polyedr {
    private Vertex[] vertexes;
    private Edge[]   edges;
    private Facet[]  facets;

    public Polyedr(String file) throws Exception {
        ...
    }
    public final int getVertexesQuantity() {
        return vertexes.length;
    }
    public final Vertex getVertex(int i) {
        return vertexes[i];
    }
    public final int getEdgesQuantity() {
        return edges.length;
    }
    public final Edge getEdge(int i) {
        return edges[i];
    }
    public final int getFacetsQuantity() {
        return facets.length;
    }
    public final Facet getFacet(int i) {
        return facets[i];
    }
}

Решаемая задача изображения проекции полиэдра может быть разделена на две следующим образом: сначала вычислим плоское изображение проекции, состоящее из совокупности прямолинейных отрезков ребер (точнее, их видимых частей), а затем осуществим вывод этого изображения в специально созданном новом окне (более точное название для него — фрейм).

Задачу вывода плоского изображения в отдельном фрейме поручим осуществлять классу AwtDrawer. Графическая библиотека (пакет java.awt ) содержит среди множества других класс Frame, позволяющий создать фрейм и, используя простейшие графические примитивы, построить в нем требуемое изображение. Класс AwtDrawer будет являться расширением класса Frame, а основным методом рисования проекции отрезков ребер будет метод draw, изображающий прямолинейный отрезок. Координаты точек при этом предполагаются нормированными — и абсцисса и ордината должны быть заключены между нулем и единицей.

import java.awt.*;
public class AwtDrawer extends Frame {
    private static final int XLEN = 500;
    private static final int YLEN = 500;
    private static final int DELTA= 100;
    private Image offScrImage;
    private Graphics offScrGC;
    private Graphics g;
    public AwtDrawer() {
        super("Построение изображения полиэдра");
        setSize(XLEN+2*DELTA, YLEN+2*DELTA);
        setBackground(Color.white);
        g = getGraphics();
        show();

   offScrImage = createImage(XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC = offScrImage.getGraphics();
        offScrGC.setColor(Color.white);
        offScrGC.fillRect(0,0,XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC.setColor(Color.black);
     }
    public final void draw(double xb, double yb, double xe, double ye) {
        int x0 = DELTA + (int)(XLEN * xb);
        int y0 = DELTA + (int)(YLEN * yb);
        int x1 = DELTA + (int)(XLEN * xe);
        int y1 = DELTA + (int)(YLEN * ye);
        offScrGC.drawLine(x0, y0, x1, y1);
    }
    public void update(Graphics g) {
        paint(g);
    }
    public void paint(Graphics g) {
        g.drawImage(offScrImage, 0, 0, this); 
    }
}

Поясним назначение некоторых констант, переменных и методов, которые встречаются в реализации класса AwtDrawer.

Первые три строки тела конструктора этого класса последовательно устанавливают заголовок создаваемого фрейма, его размер и цвет фона. Вокруг области рисования размером XLENxYLEN предусмотрены поля шириной DELTA.

Класс Graphics пакета AWT уже встречался нам при рассмотрении аплетов. Именно в нем определены все основные графические примитивы. По некоторым причинам, обсуждение которых выходит за рамки нашего курса, изображение сначала строится в так называемом внеэкранном буфере offScrImage, а во фрейм выводится с помощью метода drawImage при вызове методов paint и update. Метод draw, аргументами которого являются числа в диапазоне от 0 до 1, вычисляет соответствующие им координаты фрейма и с помощью метода drawLine рисует заданный отрезок.

Вернемся теперь к задаче определения множества отрезков на плоскости, которые должны быть изображены.

В качестве первого шага в этом направлении полезно решить относительно простую задачу построения проекции всех ребер полиэдра целиком. Создадим для этих целей класс SimpleDrawer, который вполне логично сделать выведенным из класса AwtDrawer. В качестве компонент в этот класс включим сам полиэдр, ортонормированный базис, состоящий из нормированного вектора проектирования и двух единичных ортогональных друг другу векторов в плоскости проекции, минимальные значения $$x$$ и $$y$$ координат проекции вершин полиэдра в плоскости проекции и длину минимальной стороны квадрата, в который целиком поместится вся проекция полиэдра.

Ряд методов класса SimpleDrawer предназначен для нахождения координат проекции произвольного трехмерного вектора, как реальных, так и нормализованных (в той системе координат на плоскости проекции, в которой изображаемой части фрейма соответствует квадрат $$0 \leqslant x \leqslant 1 \land 0 \leqslant y \leqslant 1$$ ). Вычисление реальных координат проекции, как это хорошо известно из аналитической геометрии, сводится к вычислению скалярных произведений с помощью статического метода scalMul класса R3Vector, а их нормализация — к линейному преобразованию (сдвигу и гомотетии).

public class SimpleDrawer extends AwtDrawer {
    protected Polyedr p;
    protected R3Vector pr;
    private R3Vector x;
    private R3Vector y;
    private double xmin;
    private double ymin;
    private double size;
    private double xProection(R3Vector v) {
        return R3Vector.scalMul(v, x);
    }
    private double yProection(R3Vector v) {
        return R3Vector.scalMul(v, y);
    }
    protected double xnProection(R3Vector v) {
        return (xProection(v) - xmin)/size;
    }
    protected double ynProection(R3Vector v) {
        return (yProection(v) - ymin)/size;
    }

   public SimpleDrawer(Polyedr p, R3Vector pr, double angle) {
        this.p  = p;
        this.pr = pr.normalize();
        double a = pr.getX();
        double b = pr.getY();
        double c = pr.getZ();
        if (a != 0. || b != 0.) {
            x = new R3Vector(-b, a, 0.);
        } else {
            x = new R3Vector(0., c, -b);
        }
        y = R3Vector.vectMul(x, pr);
        x.normalize();
        y.normalize();
        R3Vector nx = R3Vector.plus(R3Vector.mul(Math.cos(angle), x),
                                    R3Vector.mul(-Math.sin(angle), y));
        R3Vector ny = R3Vector.plus(R3Vector.mul(Math.sin(angle), x),
                                    R3Vector.mul(Math.cos(angle), y));
        x = nx;
        y = ny;
        xmin = ymin = Double.MAX_VALUE;	
        double xmax, ymax;
        xmax = ymax = Double.MIN_VALUE;	
        for (int i=0; i<p.getVertexesQuantity(); i++) {
            double xi = xProection(p.getVertex(i));
            double yi = yProection(p.getVertex(i));
            if (xi < xmin) xmin = xi;
            if (yi < ymin) ymin = yi;
            if (xi > xmax) xmax = xi;
            if (yi > ymax) ymax = yi;
        }
        size = ymax - ymin;
        if (xmax - xmin > size) size = xmax - xmin;
    }
    public final void draw() {
        for (int i=0; i<p.getEdgesQuantity(); i++)
            drawEdge(p.getEdge(i));
        Xterm.print("\n");
    }	
    public void drawEdge(Edge s) {
        Vertex begin = s.getBegin();
        Vertex end   = s.getEnd();
        draw(xnProection(begin), ynProection(begin),
             xnProection(end), ynProection(end));
        Xterm.print(".");
    } 
}

Всю основную подготовительную работу выполняет конструктор класса. Первой из его задач является нахождение ортонормированного базиса, одним из векторов которого является вектор проектирования единичной длины. Нормализация вектора осуществляется при этом с помощью метода normalize класса R3Vector. Второй из векторов искомой тройки строится с помощью уже найденного вектора нормали (нормированному вектору проектирования) по следующему правилу. Если $$(a,b,c)$$ — координаты вектора нормали, то векторы с координатами $$(-b,a,0)$$ и $$(0,c,-b)$$ оба ему ортогональны, и при этом один из них заведомо не нулевой. Он-то и выбирается в качестве второго из векторов базиса (в программной реализации ему соответствует компонента x класса SimpleDrawer ).

Третий вектор базиса (компонента y ) находится с помощью вычисления векторного произведения (метод vectMul ) двух уже найденных. Заключительным шагом является осуществление поворота на заданный угол angle в плоскости проекции. Как известно из аналитической геометрии, поворот в плоскости $$Oxy$$ на угол $$\alpha$$ эквивалентен умножению вектора координат $$(x,y)$$ на матрицу$$\left( \begin{array}{cc} \cos\alpha -\sin\alpha\\ \sin\alpha \cos\alpha\\ \end{array} \right).$$ Для осуществления этих операций используются методы mul и plus класса R3Vector, позволяющие умножить вектор на число и вычислить сумму двух векторов соответственно.

Вторая задача конструктора класса SimpleDrawer — определение квадрата наименьшего размера, в котором целиком поместится проекция всех ребер полиэдра. Способ индуктивного вычисления минимального и максимального значений последовательности координат вершин нам хорошо известен. Определение длины стороны искомого квадрата после этого сводится к выбору максимальной из двух длин сторон найденного ограничивающего прямоугольника проекции.

Метод draw, который и должен построить проекцию полиэдра, осуществляет это с помощью метода drawEdge, последовательно вызываемого для всех ребер. Последний из методов вычисляет нормализованные координаты проекции обрабатываемого ребра и вызывает метод draw базового класса AwtDrawer для рисования требуемого отрезка. Так как для сложных полиэдров их изображения могут строиться достаточно долго, а визуализация результата происходит только после полного завершения решения задачи, то рисование каждого ребра во внеэкранном буфере заканчивается выводом точки на экран терминала. Появляющаяся при этом последовательность точек позволяет следить за процессом построения изображения.

Наиболее содержательная часть исходной задачи — учет затенения частей ребер гранями полиэдра — будет решаться с помощью класса ShadowDrawer, обсуждению которого посвящена следующая секция параграфа. В ней выяснится, что для работы с тенями (точнее с просветами — дополнениями теней) целесообразно создать еще два класса — Segment и L1ListSegments.

Чуть позже будет рассмотрен и один из возможных методов оптимизации, позволяющий ускорить построение изображения сложных полиэдров, содержащих десятки тысяч ребер и тысячи граней, с помощью так называемого двумерного хеширования граней. Для этих целей будет создан специальный класс SmartDrawer.

(рис 13.3) Иерархия классов проекта

Метод main, выполняющий ввод имени файла с описанием изображаемого полиэдра, конкретного способа изображения, вектора проектирования и угла поворота в плоскости проекции, реализуется в отдельном классе PolyedrTest, которым завершается построение иерархии классов, используемых для решения исходной задачи. Все классы иерархии показаны на рисунке рис. 13.3.

Работа с тенями от граней

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

(рис 13.4) Тень от грани на ребре

Если предположить, что бесконечно высоко расположен источник света, то на каждом ребре образуются освещенные и затененные участки. Первые мы будем называть просветами, а вторые — тенями. Рисунок 13.4 иллюстрирует эти понятия.

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

Важным наблюдением, существенно упрощающим искомый алгоритм, является тот факт, что задача нахождения теней и просветов на конкретном ребре — одномерная. Сопоставив началу ребра координату 0, а концу — 1, мы можем установить взаимно однозначное соответствие между трехмерными координатами точек ребра и введенными одномерными координатами. После этого каждый из просветов будет представлять собой отрезок, а реализовывать его будет класс Segment, в каждом экземпляре которого должны храниться одномерные координаты начала и конца обрабатываемого отрезка. Методы данного класса обязаны обеспечить все необходимые действия с отрезками, которые понадобятся при определении множества просветов.

public class Segment {
    private double begin, end;

    public Segment(double begin, double end) {
        this.begin = begin; this.end = end;
    }
    public final boolean degenerate() {
        return begin >= end;
    }
    public final Segment leftSub(Segment s) {
        return new Segment(begin, Math.min(end, s.begin));
    }
    public final Segment rightSub(Segment s) {
        return new Segment(Math.max(begin, s.end), end);
    }
    public final Segment intersection(Segment s) {
        begin = Math.max(begin, s.begin);
        end = Math.min(end, s.end);
        return this;
    }
    public final double getBegin() {
        return begin;
    }
    public final double getEnd() {
        return end;
    }
}

Указанные действия включают в себя проверку вырожденности отрезка (метод degenerate ), а также вычисление пересечения двух отрезков и определения обеих компонент разности двух отрезков. Отрезок $$[a,b]$$ является вырожденным, если он представляет из себя точку или пустое множество, то есть если $$a \geqslant b$$. Пересечение двух отрезков всегда является отрезком (возможно, вырожденным), который вычисляется в методе intersection, а разность двух отрезков всегда состоит из двух отрезков, каждый из которых может оказаться вырожденным. Левая и правая компоненты разности вычисляются с помощью методов leftSub и rightSub соответственно.

(рис 13.5) Разность просвета и тени

Совокупность всех просветов ребра целесообразно хранить в односвязном списке сегментов, для чего будем использовать изученную нами ранее ссылочную реализацию (класс L1ListSegments ). В самом начале обработки очередного ребра множество просветов состоит из одного элемента, совпадающего со всем ребром, — отрезка $$[0,1]$$. Последовательный учет теней от граней можно считать функцией на пространстве последовательности граней. Легко заметить, что эта функция индуктивна — зная список просветов, который учитывает тени от нескольких граней, и тень от еще одной, новой грани, легко вычислить новый список просветов, учитывающий и тень от новой грани. Для этого достаточно для всех просветов вычислить их разности с отрезком тени от грани (см. рис. 13.5).

В зависимости от расположения отрезков, соответствующих просвету и тени, их разность может оказаться пустой (если просвет целиком попал в тень), состоящей из единственной точки, двух точек, одного отрезка, точки и отрезка, и двух отрезков. Как это уже обсуждалось ранее, мы не будем различать эти ситуации. Гораздо удобнее считать, что разность всегда состоит из двух отрезков, каждый из которых может быть вырожденным.

Приведем ту часть реализации класса ShadowDrawer, которая уже обсуждена нами.

public class ShadowDrawer extends SimpleDrawer {
    protected R3Vector begin;
    protected R3Vector end;
    private static final int MAXSIZE = 128;
    private L1ListSegments list;
    private static final double t0 = 0.;
    private static final double t1 = 1.;
    private R3Vector R3(double t) {
        return R3Vector.plus( R3Vector.mul((1.-t), begin),
                              R3Vector.mul(t, end) );
    }
    protected final void addShadow(Facet f) {
        try {
            Segment s = shadow(f);
            if (!s.degenerate()) {
                list.toFront();
                while (!list.end()) {
                    Segment next = list.erase();
                    Segment left = next.leftSub(s);
                    if (!left.degenerate()) {
                        list.insert(left);
                        list.forward();
                    }
                    Segment right = next.rightSub(s);
                    if (!right.degenerate()) {
                        list.insert(right);
                        list.forward();
                    }
                }
            }
        } catch(Exception e) {
            Xterm.println("Слишком много видимых отрезков ребра.");
            System.exit(0);
        }
    }
    protected void addShadow() {
        for(int j=0; j<p.getFacetsQuantity(); j++)
            addShadow(p.getFacet(j));
    }

    public ShadowDrawer(Polyedr p, R3Vector pr, double angle) {
        super(p, pr, angle);
        list = new L1ListSegments(MAXSIZE);
    }
    public final void drawEdge(Edge s) {
        begin = s.getBegin();
        end = s.getEnd();
        list.clear();
        try {
            list.insert(new Segment(t0, t1));
        } catch(Exception e) {;}
        addShadow();
        try {
            for (list.toFront(); ! list.end(); list.forward()) {
                Segment u = list.after();
                R3Vector begin = R3(u.getBegin());
                R3Vector end   = R3(u.getEnd());
                draw(xnProection(begin), ynProection(begin),
                     xnProection(end), ynProection(end));
            }
        } catch(Exception e) {;}
        Xterm.print(".");
    } 
}

Метод drawEdge фиксирует обрабатываемое ребро, записывая его начало и конец в компоненты begin и end, и инициализирует список просветов list, помещая в него единственный элемент — ребро целиком. Затем с помощью вызова метода addShadow производится учет теней от всех граней полиэдра и осуществляется изображение всего списка оставшихся просветов. Для изображения просвета необходимо вычисление трехмерных координат точек ребра по известным их одномерным координатам. Это действие выполняется с помощью метода R3 по известной из аналитической геометрии формуле.

Учет тени от конкретной грани производится путем определения одномерной тени на обрабатываемом ребре с помощью вызова метода shadow и индуктивного перевычисления списка просветов по алгоритму, описанному выше. Отметим дополнительно, что вырожденные отрезки в список просветов никогда не включаются.

Заключительным этапом решения задачи является реализация метода shadow. Этот метод должен определить отрезок на обрабатываемом ребре, который представляет собой тень от фиксированной грани полиэдра.

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

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

(рис 13.6) Нахождение тени от грани

Естественно попытаться свести нахождение пересечения ребра с призмой к каким-нибудь более элементарным операциям. Мы сделаем это, представив призму в виде пересечения открытых полупространств. Например, треугольную призму можно представить в виде пересечения четырех полупространств (см. рис. 13.6).

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

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

private static final double EPSILON = 1.e-12;
    private Segment shadow(Facet f) {
        if (f.vertical(pr))
            return new Segment(t1, t0);
        int n = f.getVertexesQuantity();
        Vertex a = f.getVertex(n-1);
        Vertex b = f.getVertex(0);
        Segment result = hCross(f, a); 
        if (result.degenerate()) return result;
        result.intersection(vCross(a, b, f.getCenter()));
        if (result.degenerate()) return result;
        for (int i=1; i<n; i++) {
            a = b;
            b = f.getVertex(i);
            result.intersection(vCross(a, b, f.getCenter()));
            if (result.degenerate()) return result;
        }
        return result;
    }
    private Segment hCross(Facet f, R3Vector a) {
        R3Vector n = f.getNormal();
        if (R3Vector.scalMul(n, pr) < 0.0) n.mul(-1);
        return crossWith(a, n);
    }
    private Segment vCross(R3Vector a, R3Vector b, R3Vector c) {
        R3Vector n = R3Vector.vectMul(R3Vector.minus(b,a), pr);
        if (R3Vector.scalMul(n, R3Vector.minus(a,c)) < 0.0) n.mul(-1);
        return crossWith(a, n);
    }
    private Segment crossWith(R3Vector a, R3Vector n) {
        double f0 = R3Vector.scalMul(n, R3Vector.minus(begin, a));
        double f1 = R3Vector.scalMul(n, R3Vector.minus(end, a));
        if(Math.abs(f0) < EPSILON) f0 = 0.;
        if(Math.abs(f1) < EPSILON) f1 = 0.;
        if(f0 >= 0.  f1 >= 0.) return new Segment(t1, t0);
        if(f0 < 0.   f1 < 0. ) return new Segment(t0, t1);
        double t =  - f0 / (f1 - f0);
        if (f0 < 0.) return new Segment(t0, t);
        return new Segment(t, t1);
    }

Договоримся задавать полупространство, пересечения ребра с которым необходимо уметь вычислять, точкой $$A$$ на ограничивающей его плоскости и вектором внешней нормали $$\vec n$$ к этой плоскости. Не разбираясь пока в деталях реализации метода crossWith с указанными аргументами (в тексте программы это точка a и вектор нормали n ), обсудим задачу нахождения одномерной тени, решаемую методами shadow, hCross и vCross.

Для реализации метода hCross нахождения пересечения с горизонтальным полупространством нужно найти нормаль к грани (выбрав правильное направление) и вызвать метод crossWith. Ясно, что из соображений эффективности операцию вычисления нормали к грани лучше выполнить только один раз, что и делается в конструкторе класса Facet. Направление нормали будет правильным при условии положительности скалярного произведения ее с вектором проектирования. Если это условие нарушено, то вектор $$\vec n$$ следует заменить на противоположный, умножив его на минус единицу.

Вычисление пересечения с вертикальным полупространством с помощью метода vCross удастся реализовать, зная соответствующее ребро грани (точки $$A$$ и $$B$$ ) и центр грани $$C$$. Точку $$A$$ при этом можно использовать в качестве аргумента для вызова метода crossWith непосредственно, а нормаль к вертикальной плоскости, проходящей через ребро $$AB$$, находится следующим образом. Сначала вычисляется векторное произведение вектора $$\overrightarrow{AB}$$ и вектора проектирования. Результат этой операции заведомо является нормалью, но может быть направлен не в ту сторону — не наружу, а внутрь.

Так как все грани полиэдра по самому его определению являются выпуклыми многоугольниками, центр каждой из граней всегда находится внутри ее. По этой причине вектор $$\overrightarrow{AC}$$ всегда направлен внутрь вертикального полупространства, пересечение с которым мы ищем. Если точки $$A$$ и $$B$$ являются вершинами грани, следующими друг за другом в порядке против часовой стрелки, то положительность скалярного произведения векторов $$\overrightarrow{AC}$$ и вычисленного по указанному выше способу вектора нормали гарантирует нужное направление последнего. В противном случае необходимо его умножение на минус единицу. Вычисление центра грани также целесообразно производить единственный раз — в конструкторе класса Facet.

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

Для невертикальной грани сначала вычисляется отрезок result, являющийся результатом пересечения ребра с горизонтальным полупространством. Затем последовательно для всех ребер грани индуктивно находится пересечение текущего значения величины result с вертикальными полупространствами, соответствующими ребрам. При этом вычисления немедленно прекращаются, если будет получен вырожденный отрезок.

Теперь нам осталось разобраться с последним из методов класса ShadowDrawer — методом crossWith нахождения пересечения ребра $$PQ$$ с полупространством, заданным точкой $$A$$ и вектором внешней нормали $$\vec n$$. В одномерных координатах точкам $$P$$ и $$Q$$ соответствует ноль и единица, а результатом работы метода должны быть одномерные координаты отрезка пересечения.

Рассмотрим функцию $$f(x) = <\vec n, \overrightarrow{AX}>$$, где $$X$$ — точка на ребре $$PQ$$, $$x$$ — ее одномерная координата, а угловые скобки означают скалярное произведение двух векторов. Эта функция линейна и является знакопостоянной на отрезке $$[0,1]$$ в том случае, если ребро $$PQ$$ целиком лежит вне или внутри полупространства. В случае пересечения ребра с плоскостью, ограничивающей полупространство, одномерная координата точки пересечения находится из уравнения $$f(x) = 0$$.

Из вышесказанного следует, что задача нахождения пересечения ребра с полупространством может быть решена таким образом. Вычислим сначала значения функции $$f(x)$$ в концах отрезка — $$f(0)$$ и $$f(1)$$ (в программной реализации им соответствуют величины f0 и f1 ). Если обе эти величины неотрицательны, то пересечение пусто, если, наоборот, они обе отрицательны, то весь отрезок лежит в полупространстве.

Для определения одномерной координаты точки пересечения решим уравнение $$f(x) = 0$$. В силу линейности функции $$f(x)$$ она имеет вид $$f(x) = \alpha + \beta x$$. Так как $$f(0) = \alpha$$, а $$f(1) = \alpha + \beta$$, то коэффициенты $$\alpha$$ и $$\beta$$ легко определяются. После этого легко вычислить и корень указанного уравнения: $$\alpha = f(0)$$, $$\beta = f(1) - f(0)$$, $$x = -f(0)/(f(1) - f(0))$$.

Теперь остается выбрать только нужную часть отрезка, что и реализовано в двух последних строках тела метода crossWith. В заключение обсудим назначение третьей и четвертой строк, в которых используется малая по абсолютной величине константа EPSILON. Их роль — попытка исправить те ошибки в работе метода, которые обусловлены приближенными вычислениями с числами типа double. В результате ошибок округления некоторые видимые части ребер не изображаются и наоборот.

Добавление указанных операторов несколько улучшает ситуацию, не исправляя ее полностью. Одна из задач, предназначенных для самостоятельного решения, предлагает справиться с этой проблемой.

Некоторые технологические вопросы и оптимизация

Рассматриваемый нами проект является достаточно объемным, а его исходные тексты размещены во многих файлах, что делает особенно полезным использование ряда технологических приемов при работе с ним.

Применение утилиты make значительно облегчает решение задач на модификацию. Используемый в проекте Makefile позволяет корректно откомпилировать текст программы и стартовать процесс построения изображения любого из более, чем 15 полиэдров, файлы с данными которых содержатся в директории data. Для построения изображения куба, например, достаточно выполнения команды make cube.

Большая часть возможностей утилиты make, используемых в данном проекте, уже была рассмотрена ранее. Обсудим лишь те действия, которые выполняются при построении цели doc. Вот соответствующий фрагмент из управляющего файла.

doc:
        javadoc -d doc -version -author -private *.java

Задание на построение цели doc сводится, как видно из этой записи, к вызову утилиты javadoc, являющейся частью исполняющей среды языка Java, с определенным набором параметров. Эта утилита предназначена для автоматической генерации документации, связанной с программным проектом.

При самом первом знакомстве с языком Java было отмечено, что в нем предусмотрены три вида комментариев, из которых до сих пор мы имели дело только с двумя. Многострочный комментарий, начинающийся с символов /** и завершающийся символами */ предназначен для размещения в тексте программы комментариев, которые могут быть впоследствии обработаны утилитой javadoc с целью построения полноценной документации о программе.

Правильное использование этой утилиты позволяет получить описание классов и пакетов в гипертекстовом формате (на языке HTML), что чрезвычайно удобно. Программист обязан, конечно, при написании программы размещать в ее тексте необходимые директивы, распознаваемые утилитой javadoc. Все они, впрочем, имеют очень простые синтаксис и семантику.

(рис 13.7) Фрагмент результата работы утилиты

Приведенный в последней секции параграфа полный текст проекта иллюстрирует использование таких комментариев. Иерархия классов проекта "Изображение полиэдра", приведенная выше на рисунке 13.3, является результатом работы утилиты javadoc. На рис. 13.7 можно увидеть еще один из фрагментов документации о проекте — часть алфавитного списка всех компонент, методов и классов.

Как легко убедиться, построенная программа работает достаточно медленно при построении изображения сложного полиэдра, хотя мы и пытались уже в процессе ее создания заботиться об эффективности. Сейчас будет предложен метод, носящий имя фильтрование граней \label{filtr}, который позволяет заметно ускорить работу программы.

Основное время в процессе нахождения плоского изображения полиэдра при обработке очередного ребра тратится на вычисление тени от всех граней. При этом значительная часть работы выполняется заведомо впустую — далекие от ребра грани не могут его затенить. Эта пустая трата времени тем значительнее, чем больше число ребер и граней, так как в этом случае больший процент граней можно было не рассматривать. Попробуем найти простой способ, который позволил бы разделить все грани на "далекие" и "близкие".

Хорошей идеей является попытка использования ограничивающих прямоугольников для проекций ребер и граней. Во-первых, ограничивающие прямоугольники легко быстро вычислить, а во-вторых, легко проверить, пересекаются ли два таких прямоугольника. Отсутствие пересечения прямоугольников при этом гарантирует отсутствие тени от грани на ребро.

Реализация описанной идеи позволяет заменить для большинства граней ресурсоемкую операцию нахождения тени от нее на определение пересечения ее ограничивающего прямоугольника (который должен вычисляться еще в конструкторе) с ограничивающим прямоугольником ребра (который тоже может быть вычислен заранее). Только в случае их пересечения необходимо будет вызывать метод вычисления тени.

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

Другим возможным способом оптимизации построения изображения является использование двумерного хеширования граней. Основной целью этого метода является полный отказ от рассмотрения "далеких" граней при построении изображения конкретного ребра.

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

Класс SmartDrawer, полный текст которого приведен ниже, реализует эту идею.

Задачи для самостоятельного решения

Задача 13.2. Оптимизируйте текст эталонного проекта "Изображение полиэдра" так, чтобы:

a) устранить многократное изображение видимых частей ребер, связанное с особенностями задания полиэдра, описанными в начале лекции;

b) реализовать идею фильтрования граней, базируясь на реализации класса ShadowDrawer ;

c) минимизировать действия, выполняемые в классе ShadowDrawer, связанные с необходимостью обращения (умножения на минус единицу) нормалей к граням;

d) максимально исправить влияние ошибок округления при работе с числами, заданными в форме с плавающей точкой, которые приводят к тому, что отдельные видимые части ребер не изображаются и наоборот.

Задача 13.3. Модифицируйте текст эталонного проекта "Изображение полиэдра" так, чтобы:

a) невидимые части ребер изображались пунктиром;

b) грани, имеющие максимальную площадь среди полностью видимых граней, заштриховывалась;

c) изображалась только та часть полиэдра, которая попадает в полупространство, заданное с помощью точки на ограничивающей его плоскости и вектора внешней нормали (эта задача эквивалентна построению изображения предварительно усеченного полиэдра);

d) изображалось сечение полиэдра предварительно заданной плоскостью;

e) строились плоские изображения четырехмерных полиэдров.

Задача 13.4. Модифицируйте текст эталонного проекта "Изображение полиэдра" так, чтобы вычислялась и печаталась следующая величина:

a) количество видимых вершин;

b) количество полностью видимых ребер;

c) количество частично видимых ребер;

d) количество полностью видимых граней;

e) количество частично видимых граней;

f) количество ребер, удаленных от начала координат не более, чем на единичное растояние;

g) количество граней, удаленных от начала координат не более, чем на единичное растояние;

h) количество граней полиэдра, пересекающихся с заданным отрезком;

i) количество ребер полиэдра, пересекающихся с заданным отрезком;

j) периметр проекции полиэдра;

k) площадь проекции полиэдра.

Полный текст проекта

Сначала приведем содержание управляющего файла для утилиты make.

MAKEFILE

# -*- mode: makefile -*-

.PHONY : all doc clean veryclean cube ccc sticks tagl king dragon            \
         x29 apple glass4 torus pear vw bones beeth teapot cow babem

JAVAC      = javac

JAVAFILES  := Xterm.java R3Vector.java Vertex.java Edge.java Facet.java      \
              Polyedr.java Segment.java L1ListSegments.java                  \
              AwtDrawer.java SimpleDrawer.java ShadowDrawer.java             \
              SmartDrawer.java PolyedrTest.java

CLASSFILES := Xterm.class R3Vector.class Vertex.class Edge.class Facet.class \
              Polyedr.class Segment.class L1ListSegments.class               \
              AwtDrawer.class SimpleDrawer.class ShadowDrawer.class          \
              SmartDrawer.class PolyedrTest.class

%.class: %.java
	$(JAVAC) $<

cube:	$(CLASSFILES)
	java PolyedrTest data/cube.geom

ccc:	$(CLASSFILES)
	java PolyedrTest data/ccc.geom

sticks:	$(CLASSFILES)
	java PolyedrTest data/sticks.geom

tagl:	$(CLASSFILES)
	java PolyedrTest data/tagl.geom

king:	$(CLASSFILES)
	java PolyedrTest data/king.geom

dragon:	$(CLASSFILES)
	java PolyedrTest data/dragon.geom

x29:	$(CLASSFILES)
	java PolyedrTest data/x29.geom

apple:	$(CLASSFILES)
	java PolyedrTest data/apple_logo.geom

glass4:	$(CLASSFILES)
	java PolyedrTest data/glass4.geom

torus:	$(CLASSFILES)
	java PolyedrTest data/torus.geom

pear:	$(CLASSFILES)
	java PolyedrTest data/pear.geom

vw:	$(CLASSFILES)
	java PolyedrTest data/vw.geom

bones:	$(CLASSFILES)
	java PolyedrTest data/foot_bones.geom

beeth:	$(CLASSFILES)
	java PolyedrTest data/beethoven.geom

teapot:	$(CLASSFILES)
	java PolyedrTest data/teapot.geom

cow:	$(CLASSFILES)
	java PolyedrTest data/cow.geom

babem:	$(CLASSFILES)
	java PolyedrTest data/babem.geom

doc:
	javadoc -d doc -version -author -private *.java

expand:	
	for i in Makefile *.java; do expand $$i >$$i.expand; done

clean:
	rm -f *.class *.expand

veryclean:
	rm -f *.class *.expand doc/*.html

А вот как выглядят все остальные файлы проекта.

R3Vector.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс R3Vector, реализующий вектор (Vector) в пространстве (R3).
 */
public class R3Vector {
    /**
     * Координаты вектора.
     */
     private double x, y, z;

    /**
     * Конструктор вектора, заданного его координатами.
     * @param x X-координата вектора.
     * @param y Y-координата вектора.
     * @param z Z-координата вектора.
     */
    public R3Vector(double x, double y, double z) {
	this.x = x; this.y = y; this.z = z;
    }
    /**
     * Конструктор вектора, координаты которого вводятся с клавиатуры.
     * @exception Exception Исключительная ситуация, возникающая
     * при ошибках ввода координат с клавиатуры.
     */
    public R3Vector() throws Exception {
	x = Xterm.inputDouble("X-коорд. вектора проектирования -> ");
	y = Xterm.inputDouble("Y-коорд. вектора проектирования -> ");
	z = Xterm.inputDouble("Z-коорд. вектора проектирования -> ");
    }
    /**
     * Получить X-координату вектора.
     * @return X-координата вектора.
     */
    public final double getX() {
	return x;
    }
    /**
     * Получить Y-координату вектора.
     * @return Y-координата вектора.
     */
    public final double getY() {
	return y;
    }
    /**
     * Получить Z-координату вектора.
     * @return Z-координата вектора.
     */
    public final double getZ() {
	return z;
    }
     /**
     * Нормировать ненулевой вектор.
     */
    public final R3Vector normalize() {
	double norm = Math.sqrt(x*x+y*y+z*z);
	x /= norm; y /= norm; z /= norm;
	return this;
    }
    /**
     * Найти сумму двух векторов.
     * @param a Первый вектор-слагаемое.
     * @param b Второй вектор-слагаемое.
     * @return Сумма векторов.
     */
    public static R3Vector plus(R3Vector a, R3Vector b) {
	return new R3Vector(a.x+b.x, a.y+b.y, a.z+b.z);
    }
    /**
     * Добавить заданный вектор.
     * @param b Добавляемый вектор.
     * @return Вектор-результат.
     */
    public final R3Vector plus(R3Vector b) {
	x += b.x; y += b.y; z += b.z;
	return this;
    }
    /**
     * Найти разность двух векторов.
     * @param a Вектор-уменьшаемое.
     * @param b Вектор-вычитаемое.
     * @return Разность векторов.
     */
    public static R3Vector minus(R3Vector a, R3Vector b) {
	return new R3Vector(a.x-b.x, a.y-b.y, a.z-b.z);
    }
    /**
     * Вычесть заданный вектор.
     * @param b Вычитаемый вектор.
     * @return Вектор-результат.
     */
    public final R3Vector minus(R3Vector b) {
	x -= b.x; y -= b.y; z -= b.z;
	return this;
    }
    /**
     * Найти произведение вектора на число.
     * @param k Число, на которое умножается вектор.
     * @param a Исходный вектор.
     * @return Вектор-результат.
     */
    public static R3Vector mul(double k, R3Vector a) {
	return new R3Vector(k*a.x, k*a.y, k*a.z);
    }
    /**
     * Умножить вектор на заданное число.
     * @param k Число, на которое умножается вектор.
     * @return Вектор-результат.
     */
    public final R3Vector mul(double k) {
	x *= k; y *= k; z *= k;
	return this;
    }
    /**
     * Найти скалярное произведение векторов.
     * @param a Первый вектор.
     * @param b Второй вектор.
     * @return Скалярное произведение векторов.
     */
    public static double scalMul(R3Vector a, R3Vector b) {
	return a.x*b.x + a.y*b.y + a.z*b.z;
    }
    /**
     * Найти векторное произведение векторов.
     * @param a Первый вектор.
     * @param b Второй вектор.
     * @return Векторное произведение векторов.
     */
    public static R3Vector vectMul(R3Vector a, R3Vector b) {
	return new R3Vector(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x);
    }
}

Vertex.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Vertex, реализующий вершину полиэдра.
 */
public class Vertex extends R3Vector {
    /**
     * Конструктор.
     * @param x X-координата вершины.
     * @param y Y-координата вершины.
     * @param z Z-координата вершины.
     */
    public Vertex(double x, double y, double z) {
	super(x, y, z);
    }
}

Edge.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Edge, реализующий ребро полиэдра.
 */
public class Edge {
    /**
     * Начало ребра.
     */
    private Vertex begin;
    /**
     * Конец ребра.
     */
    private Vertex end;

    /**
     * Конструктор.
     * @param begin Начало ребра.
     * @param end Конец ребра.
     */
    public Edge(Vertex begin, Vertex end) {
	this.begin = begin; this.end = end;
    }
    /**
     * Получить начало ребра.
     * @return Начало ребра.
     */
    public final Vertex getBegin() {
	return begin;
    }
    /**
     * Получить конец ребра.
     * @return Конец ребра.
     */
    public final Vertex getEnd() {
	return end;
    }
}

Facet.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс Facet, реализующий грань полиэдра.
 */
public class Facet {
    /**
     * Массив вершин полиэдра, принадлежащих грани.
     */
    private Vertex[] vertexes;
    /**
     * Центр грани.
     */
    private R3Vector center;
    /**
     * Вектор нормали к грани.
     */
    private R3Vector normal;
    
    /**
     * Конструктор.
     * @param vertexes Вершины полиэдра, образующие грань.
     */
    public Facet(Vertex[] vertexes) {
	this.vertexes = vertexes;
	normal = R3Vector.vectMul(R3Vector.minus(vertexes[1], vertexes[0]),
				   R3Vector.minus(vertexes[2], vertexes[0]));
	center = new R3Vector(0., 0., 0.);
	for (int i=0; i<vertexes.length; i++) {
	    center.plus(vertexes[i]);
	}
	center.mul(1./(double)vertexes.length);
    }
    /**
     * Получить количество вершин.
     * @return Количество вершин грани.	
     */
    public final int getVertexesQuantity() {
	return vertexes.length;
    }
    /**
     * Получить вершину.
     * @param i Номер вершины.
     * @return Вершина грани.
     */
    public final Vertex getVertex(int i) {
	return vertexes[i];
    }
    /**
     * Получить центр грани.
     * @return Центр грани.
     */
    public final R3Vector getCenter() {
	return center;
    }
    /**
     * Получить нормаль к грани.
     * @return Нормаль грани.
     */
    public final R3Vector getNormal() {
	return normal;
    }
    /**
     * Является ли грань "вертикальной"?
     * @param pr Вектор проектирования.
     * @return Параллельна ли грань вектору проектирования?
     */
    public final boolean vertical(R3Vector pr) {
	return R3Vector.scalMul(normal, pr) == 0.;
    }
}

Polyedr.java

import java.util.*;
import java.io.*;
/**
 * @author Е.А. Роганов
 * @version 1.1
 * Класс Polyedr, реализующий полиэдр.
 */
public class Polyedr {
    /**
     * Массив вершин полиэдра.
     */
    private Vertex[] vertexes;
    /**
     * Массив ребер полиэдра.
     */
    private Edge[]   edges;
    /**
     * Массив граней полиэдра.
     */
    private Facet[]  facets;

    /**
     * Конструктор класса.
     * @param file Файл, содержащий описание полиэдра.
     * @exception Exception Исключительная ситуация, возникающая при
     * ошибках чтения или преобразования данных.
     */
    public Polyedr(String file) throws Exception {
	RandomAccessFile f = new RandomAccessFile(file, "r");
	StringTokenizer st = new StringTokenizer(f.readLine());

	vertexes  = new Vertex[Integer.parseInt(st.nextToken())];
	facets = new Facet[Integer.parseInt(st.nextToken())];
	edges  = new Edge[Integer.parseInt(st.nextToken())];	
	
	for (int i=0; i<vertexes.length; i++) {
	    st = new StringTokenizer(f.readLine());
	    double x = Double.valueOf(st.nextToken()).doubleValue();
	    double y = Double.valueOf(st.nextToken()).doubleValue();
	    double z = Double.valueOf(st.nextToken()).doubleValue();
	    vertexes[i] = new Vertex(x,y,z);
	}
	int k = 0;
	for (int i=0; i<facets.length; i++) {
	    st = new StringTokenizer(f.readLine());
	    int size = Integer.parseInt(st.nextToken());
	    Vertex[] facet = new Vertex[size];
	    facet[0] = vertexes[Integer.parseInt(st.nextToken()) - 1];
	    for (int j=1; j<size; j+=1) {
		facet[j] = vertexes[Integer.parseInt(st.nextToken()) - 1];
		edges[k++] = new Edge(facet[j], facet[j-1]);
	    }
	    edges[k++] = new Edge(facet[size-1], facet[0]);
	    facets[i] = new Facet(facet);
	}
    }
    /**
     * Получить количество вершин.
     * @return Количество вершин полиэдра.	
     */
    public final int getVertexesQuantity() {
	return vertexes.length;
    }
    /**
     * Получить вершину.
     * @param i Номер вершины.
     * @return Вершина полиэдра.
     */
    public final Vertex getVertex(int i) {
	return vertexes[i];
    }
    /**
     * Получить количество ребер.
     * @return Количество ребер полиэдра.	
     */
    public final int getEdgesQuantity() {
	return edges.length;
    }
    /**
     * Получить ребро.
     * @param i Номер ребра.
     * @return Ребро полиэдра.
     */
    public final Edge getEdge(int i) {
	return edges[i];
    }
    /**
     * Получить количество граней.
     * @return Количество граней полиэдра.	
     */
    public final int getFacetsQuantity() {
	return facets.length;
    }
    /**
     * Получить грань.
     * @param i Номер грани.
     * @return Грань полиэдра.
     */
    public final Facet getFacet(int i) {
	return facets[i];
    }
}

Segment.java

/**
 * @author Е.А. Роганов
 * @version 1.q	
 * Класс Segment, реализующий одномерный отрезок.
 */
public class Segment {
    /**
     * Координаты начала и конца отрезка.
     */
    private double begin, end;

    /**
     * Конструктор отрезка.
     * @param begin Начало отрезка.
     * @param end Начало отрезка.
     */
    public Segment(double begin, double end) {
	this.begin = begin; this.end = end;
    }
    /**
     * Вырожден ли отрезок?
     * @return Вырожден ли отрезок?
     */
    public final boolean degenerate() {
	return begin >= end;
    }
    /**
     * Найти левый отрезок разности с отрезком s.
     * @param s Вычитаемый отрезок.
     * @return Левый отрезок разности.
     */
    public final Segment leftSub(Segment s) {
	return new Segment(begin, Math.min(end, s.begin));
    }
    /**
     * Найти правый отрезок разности с отрезком s.
     * @param s Вычитаемый отрезок.
     * @return Правый отрезок разности.
     */
    public final Segment rightSub(Segment s) {
	return new Segment(Math.max(begin, s.end), end);
    }
    /**
     * Найти пересечение с отрезком s.
     * @param s Отрезок, с которым находится пересечение.
     * @return Отрезок-пересечение.
     */
    public final Segment intersection(Segment s) {
	begin = Math.max(begin, s.begin);
	end = Math.min(end, s.end);
	return this;
    }
    /**
     * Получить начало отрезка.
     * @return Начало отрезка.
     */
    public final double getBegin() {
	return begin;
    }
    /**
     * Получить конец отрезка.
     * @return Конец отрезка.
     */
    public final double getEnd() {
	return end;
    }
}

L1ListSegments.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс L1ListSegments, реализующий односвязный список отрезков.
 */
public class L1ListSegments {
    /**
     * Массив отрезков.
     */
    private Segment[] array;
    /**
     * Массив ссылок.
     */
    private int[] next;
    /**
     * Нил списка.
     */
    private int nilList;
    /**
     * Нил свободного места.
     */
    private int nilFree;
    /**
     * Элемент до указателя.
     */
    private int before;
    /**
     * Элемент за указателем.
     */
    private int after;
    /**
     * Связать два элемента.
     * @param first Первый элемент.
     * @param second Второй элемент.
     */
    private void link(int first, int second) {
        next[first] = second;
    }
    /**
     * Захватить место.
     * @return Индекс занимаемого элемента.
     */
    private int mallocIndex() {
        int index = next[nilFree];
        link(nilFree, next[index]);
        return index;
    }
    /**
     * Освободить место.
     * @param index Индекс освобождаемого элемента.
     */
    private void freeIndex(int index) {
        link(index, next[nilFree]);
        link(nilFree, index);
    }

    /** 
     * Конструктор класса.
     * @param size Максимальный размер списка.
     */
    public L1ListSegments(int size) {
        array = new Segment[size];
        next  = new int[size + 2];
        nilList = size;
        nilFree = size + 1;

        link(nilList, nilList);

        link(nilFree, 0);
        for (int i=0; i<size-1; i++)
            link(i, i+1);
        link(size-1, nilFree);

        before = after = nilList;
    }
    /** 
     * Пуст ли список?
     * @return Пуст ли список?
     */
    public final boolean empty() {
        return next[nilList] == nilList;
    }
    /** 
     * Сделать список пустым.
     */
    public final void clear() {
        try {
            toFront();
            while(true)
               erase();
        } catch(Exception e) {
            ;
        }
    }
    /** 
     * Передвинуть указатель в начало списка.
     */
    public final void toFront() {
        before = nilList;
        after  = next[nilList];
    }
    /** 
     * Указатель в конце списка?
     * @return Указатель в конце списка?
     */
    public final boolean end() {
        return after == nilList;
    }
    /** 
     * Передвинуть указатель вперед.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке передвинуть вперед указатель, находящийся в конце списка.
     */
    public final void forward() throws Exception {
        if (after == nilList) throw new Exception();
        before = after;
        after  = next[after];
    }
    /** 
     * Получить элемент за указателем.
     * @return Элемент за указателем.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке получить элемент за указателем, находящимся в конце списка.
     */
    public final Segment after() throws Exception {
        return array[after];
    }
    /** 
     * Добавить элемент за указателем.
     * @param val Включаемый отрезок (сегмент).
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке добавить элемент в заполненный до конца список.
     */
    public final void insert(Segment val) throws Exception {
        int index = mallocIndex();
        link(before, index);
        link(index, after);
        after = index;
        array[index] = val;
    }
    /** 
     * Удалить элемент за указателем.
     * @return Удаляемый элемент.
     * @exception Exception Исключительная ситуация, возникающая при
     * попытке удалить элемент из пустого списка.
     */
    public final Segment erase() throws Exception {
        Segment val   = array[after];
        int index = after;
        after = next[index];
        link(before, after);
        freeIndex(index);
        return val;
    }
}

AwtDrawer.java

import java.awt.*;
/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс AwtDrawer, обеспечивающий визуализацию
 * плоского изображения.
 */
public class AwtDrawer extends Frame {
    /**
     * Ширина области рисования.
     */
    private static final int XLEN = 500;
    /**
     * Высота области рисования.
     */
    private static final int YLEN = 500;
    /**
     * Ширина "полей" вокруг области рисования.
     */
    private static final int DELTA= 100;
    /**
     * Внеэкранный буфер.
     */
    private Image offScrImage;
    /**
     * Графический контекст внеэкранного буфера.
     */
    private Graphics offScrGC;
    /**
     * Графический контекст фрейма на экране.
     */
    private Graphics g;
    /**
     * Конструктор класса.
     */
    public AwtDrawer() {
	super("Построение изображения полиэдра");
	setSize(XLEN+2*DELTA, YLEN+2*DELTA);
	setBackground(Color.white);
	g = getGraphics();
	show();

	offScrImage = createImage(XLEN+2*DELTA, YLEN+2*DELTA);
	offScrGC = offScrImage.getGraphics();
        offScrGC.setColor(Color.white);
        offScrGC.fillRect(0,0,XLEN+2*DELTA, YLEN+2*DELTA);
        offScrGC.setColor(Color.black);
     }
    /**
     * Изобразить отрезок на плоскости, заданный его концами.
     * @param xb X-координата начала отрезка ( 0 <= xb <= 1 ).
     * @param yb Y-координата начала отрезка ( 0 <= yb <= 1 ).
     * @param xe X-координата конца  отрезка ( 0 <= xe <= 1 ).
     * @param ye Y-координата конца  отрезка ( 0 <= ye <= 1 ).
     */
    public final void draw(double xb, double yb, double xe, double ye) {
	int x0 = DELTA + (int)(XLEN * xb);
	int y0 = DELTA + (int)(YLEN * yb);
	int x1 = DELTA + (int)(XLEN * xe);
	int y1 = DELTA + (int)(YLEN * ye);
	offScrGC.drawLine(x0, y0, x1, y1);
    }
    /**
     * Переизобразить содержимое фрейма.
     */
    public void update(Graphics g) {
	paint(g);
    }
    public void paint(Graphics g) {
	g.drawImage(offScrImage, 0, 0, this); 
    }
}

SimpleDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс SimpleDrawer, обеспечивающий изображение проекции полиэдра.
 */
public class SimpleDrawer extends AwtDrawer {
    /**
     * Полиэдр.
     */
    protected Polyedr p;
    /**
     * Единичный вектор проектирования.
     */
    protected R3Vector pr;
    /**
     * Единичный X-вектор плоскости проектирования.
     */
    private R3Vector x;
    /**
     * Единичный Y-вектор плоскости проектирования.
     */
    private R3Vector y;
    /**
     * Минимальная X-координата проекции полиэдра.
     */
    private double xmin;
    /**
     * Минимальная Y-координата проекции полиэдра.
     */
    private double ymin;
    /**
     * Размер проекции полиэдра.
     */
    private double size;
    /**
     * Вычислить X-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return X-координата проекции этого вектора.
     */
    private double xProection(R3Vector v) {
	return R3Vector.scalMul(v, x);
    }
    /**
     * Вычислить Y-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Y-координата проекции этого вектора.
     */
    private double yProection(R3Vector v) {
	return R3Vector.scalMul(v, y);
    }
    /**
     * Вычислить нормализованную X-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Нормализованная X-координата проекции этого вектора.
     */
    protected double xnProection(R3Vector v) {
	return (xProection(v) - xmin)/size;
    }
    /**
     * Вычислить нормализованную Y-координату проекции точки.
     * @param v Трехмерный вектор.
     * @return Нормализованная Y-координата проекции этого вектора.
     */
    protected double ynProection(R3Vector v) {
	return (yProection(v) - ymin)/size;
    }

    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public SimpleDrawer(Polyedr p, R3Vector pr, double angle) {
	this.p  = p;
	this.pr = pr.normalize();
	double a = pr.getX();
	double b = pr.getY();
	double c = pr.getZ();
	if (a != 0. || b != 0.) {
	    x = new R3Vector(-b, a, 0.);
	} else {
	    x = new R3Vector(0., c, -b);
	}
	y = R3Vector.vectMul(x, pr);
	x.normalize();
	y.normalize();
	R3Vector nx = R3Vector.plus( R3Vector.mul(Math.cos(angle), x),
				     R3Vector.mul(-Math.sin(angle), y) );
	R3Vector ny = R3Vector.plus( R3Vector.mul(Math.sin(angle), x),
				     R3Vector.mul(Math.cos(angle), y) );
	x = nx;
	y = ny;
	xmin = ymin = Double.MAX_VALUE;	
	double xmax, ymax;
	xmax = ymax = Double.MIN_VALUE;	
	for (int i=0; i<p.getVertexesQuantity(); i++) {
	    double xi = xProection(p.getVertex(i));
	    double yi = yProection(p.getVertex(i));
	    if (xi < xmin) xmin = xi;
	    if (yi < ymin) ymin = yi;
	    if (xi > xmax) xmax = xi;
	    if (yi > ymax) ymax = yi;
	}
	size = ymax - ymin;
	if (xmax - xmin < size) size = xmax - xmin;
    }
    /**
     * Изобразить проекцию полиэдра.
     */
    public final void draw() {
	for (int i=0; i<p.getEdgesQuantity(); i++)
	    drawEdge(p.getEdge(i));
	Xterm.print("\n");
    }	
    /**
     * Изобразить ребро полиэдра.
     * @param s Обрабатываемое ребро полиэдра.
     */
    public void drawEdge(Edge s) {
	Vertex begin = s.getBegin();
	Vertex end   = s.getEnd();
	draw(xnProection(begin), ynProection(begin),
	     xnProection(end), ynProection(end));
	Xterm.print(".");
    } 
}

ShadowDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс ShadowDrawer, обеспечивающий изображение проекции 
 * полиэдра с удалением невидимых линий.
 */
public class ShadowDrawer extends SimpleDrawer {
    /**
     * Начало изображаемого ребра.
     */
    protected R3Vector begin;
    /**
     * Конец изображаемого ребра.
     */
    protected R3Vector end;
    /**
     * Максимальный размер списка.
     */
    private static final int MAXSIZE = 128;
    /**
     * Односвязный список видимых отрезков ребра.
     */
    private L1ListSegments list;
    /**
     * Достаточно малая константа, предназначенная для
     * решения проблемы неточного представления действительных
     * чисел на ЭВМ.
     */
    private static final double EPSILON = 1.e-12;
    /**
     * Одномерная координата начала обрабатываемого ребра.
     */
    private static final double t0 = 0.;
    /**
     * Одномерная координата конца обрабатываемого ребра.
     */
    private static final double t1 = 1.;
    /**
     * Вычислить пространственные координаты точки ребра,
     * заданной одномерной координатой.
     * @param t Одномерная координата точки на ребре.
     * @return Трехмерная координата этой точки ребра.
     */
    private R3Vector R3(double t) {
	return R3Vector.plus( R3Vector.mul((1.-t), begin),
			      R3Vector.mul(t, end) );
    }
    /**
     * Вычислить одномерный отрезок тени от грани.
     * @param f Грань полиэдра.
     * @return Отрезок тени от этой грани на ребре.
     */
    private Segment shadow(Facet f) {
	if (f.vertical(pr))
	    return new Segment(t1, t0);
	int n = f.getVertexesQuantity();
	Vertex a = f.getVertex(n-1);
	Vertex b = f.getVertex(0);
	Segment result = hCross(f, a); 
	if (result.degenerate()) return result;
	result.intersection(vCross(a, b, f.getCenter()));
        if (result.degenerate()) return result;
	for (int i=1; i<n; i++) {
	    a = b;
	    b = f.getVertex(i);
	    result.intersection(vCross(a, b, f.getCenter()));
	    if (result.degenerate()) return result;
	}
	return result;
    }
    /**
     * Вычислить пересечение с "горизонтальным" полупространством.
     * @param f Грань полиэдра.
     * @param a Точка (вектор) на грани.
     * @return Отрезок пересечения ребра с "горизонтальным" полупространством.
     */
    private Segment hCross(Facet f, R3Vector a) {
	R3Vector n = f.getNormal();
	if (R3Vector.scalMul(n, pr) < 0.0) n.mul(-1);
	return crossWith(a, n);
    }
    /**
     * Вычислить пересечение с "вертикальным" полупространством.
     * @param a Точка (вектор) на грани.
     * @param b Точка (вектор) на грани.
     * @param c Точка (вектор) на грани.
     * @return Отрезок пересечения ребра с "вертикальным" полупространством.
     */
    private Segment vCross(R3Vector a, R3Vector b, R3Vector c) {
	R3Vector n = R3Vector.vectMul(R3Vector.minus(b,a), pr);
	if (R3Vector.scalMul(n, R3Vector.minus(a,c)) < 0.0) n.mul(-1);
	return crossWith(a, n);
    }
    /**
     * Вычислить пересечение отрезка с заданным полупространством.
     * @param a Точка (вектор) на грани.
     * @param n Вектор внешней нормали к полупространству.
     * @return Отрезок пересечения ребра с полупространством.
     */
    private Segment crossWith(R3Vector a, R3Vector n) {
	double f0 = R3Vector.scalMul(n, R3Vector.minus(begin, a));
	double f1 = R3Vector.scalMul(n, R3Vector.minus(end, a));
	if(Math.abs(f0) < EPSILON) f0 = 0.;
	if(Math.abs(f1) < EPSILON) f1 = 0.;
	if(f0 >= 0.  f1 >= 0.) return new Segment(t1, t0);
	if(f0 < 0.   f1 < 0. ) return new Segment(t0, t1);
	double t =  - f0 / (f1 - f0);
	if (f0 < 0.) return new Segment(t0, t);
	return new Segment(t, t1);
    }
    /**
     * Учесть тень от одной грани.
     * @param f Грань, тень от которой должна быть учтена.
     */
    protected final void addShadow(Facet f) {
	try {
	    Segment s = shadow(f);
	    if (!s.degenerate()) {
		list.toFront();
		while (!list.end()) {
		    Segment next = list.erase();
		    Segment left = next.leftSub(s);
		    if (!left.degenerate()) {
			list.insert(left);
			list.forward();
		    }
		    Segment right = next.rightSub(s);
		    if (!right.degenerate()) {
			list.insert(right);
			list.forward();
		    }
		}
	    }
	} catch(Exception e) {
	    Xterm.println("Слишком много видимых отрезков ребра.");
	    System.exit(0);
        }
    }
    /**
     * Учесть тень от всех граней.
     */
    protected void addShadow() {
	for(int j=0; j<p.getFacetsQuantity(); j++)
	    addShadow(p.getFacet(j));
    }
    
    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public ShadowDrawer(Polyedr p, R3Vector pr, double angle) {
	super(p, pr, angle);
	list = new L1ListSegments(MAXSIZE);
    }
    /**
     * Изобразить видимую часть ребра полиэдра.
     * @param s Обрабатываемое ребро полиэдра.
     */
    public final void drawEdge(Edge s) {
	begin = s.getBegin();
	end = s.getEnd();
	list.clear();
	try {
	    list.insert(new Segment(t0, t1));
	} catch(Exception e) {;}
	addShadow();
	try {
	    for (list.toFront(); ! list.end(); list.forward()) {
		Segment u = list.after();
		R3Vector begin = R3(u.getBegin());
		R3Vector end   = R3(u.getEnd());
		draw(xnProection(begin), ynProection(begin),
		     xnProection(end), ynProection(end));
	    }
	} catch(Exception e) {;}
	Xterm.print(".");
    } 
}

SmartDrawer.java

/**
 * @author Е.А. Роганов
 * @version 1.1	
 * Класс SmartDrawer, обеспечивающий изображение проекции полиэдра
 * с использованием двумерного хеширования граней.
 */
public class SmartDrawer extends ShadowDrawer {
    /**
     * Количество гнезд сетки в строке и столбце.
     */
    private final static int SIZE = 50;
    /**
     * Максимальное число граней в гнезде.
     */
    private final static int MAXFACETS = 200;
    /**
     * Массив гнезд, в которые попадает обрабатываемое ребро.
     */
    private boolean[][] sockets;
    /**
     * Массив количеств граней в гнездах.
     */
    private int[][]    nmbFacets;
    /**
     * Массив множеств номеров граней.
     */
    private int[][][]  hashFacets;
    /**
     * Найти последний индекс гнезда, соответствующий t.
     * @param t Координата.
     */
    private int lastVal(double t) {
	return Math.min((int)(t*SIZE),SIZE-1);
    }
    /**
     * Найти первый индекс гнезда, соответствующий t.
     * @param t Координата.
     */
    private int firstVal(double t) {
	return (int)(t*SIZE);
    }

    /**
     * Конструктор класса.
     * @param p Полиэдр.
     * @param pr Вектор проектирования.
     * @param angle Угол поворота в плоскости проекции.
     */
    public SmartDrawer(Polyedr p, R3Vector pr, double angle) {
	super(p, pr, angle);
	nmbFacets  = new int[SIZE][SIZE];
	hashFacets = new int[SIZE][SIZE][MAXFACETS];
	int i,j,k;
	int imax = p.getFacetsQuantity();
 	for (i=0; i<imax; i++) {
	    Facet f=p.getFacet(i);
	    k = f.getVertexesQuantity();
	    double x0 = 1., y0 = 1., x1 = 0., y1 = 0.;
	    for (j=0; j<k; j++) {
		R3Vector v = f.getVertex(j);
		double x = xnProection(v);
		double y = ynProection(v);
	        if (x < x0) x0 = x;
	        if (y < y0) y0 = y;
	        if (x > x1) x1 = x;
	        if (y > y1) y1 = y;
	    }
	    int jm = lastVal(x1); 
	    int km = lastVal(y1);
	    for (j=firstVal(x0); j<=jm; j++)
		for (k=firstVal(y0); k<=km; k++)
		    hashFacets[ j ][ k ][ nmbFacets[j][k]++ ] = i;
	}
    }

    /**
     * Учесть тень от всех граней.
     */
    protected void addShadow() {
	int i,j,k;
	double x0 = Math.min(xnProection(begin), xnProection(end));
	double x1 = Math.max(xnProection(begin), xnProection(end));
	double y0 = Math.min(ynProection(begin), ynProection(end));
	double y1 = Math.max(ynProection(begin), ynProection(end));
	int jm = lastVal(x1);
	int km = lastVal(y1);

	for (j=firstVal(x0); j<=jm; j++)
	    for (k=firstVal(y0); k<=km; k++)
		for (i=0; i<nmbFacets[j][k]; i++)
		    addShadow(p.getFacet( hashFacets[j][k][i] ));
    }
}

PolyedrTest.java

import java.util.*;
import java.io.*;
/**
 * @author Е.А. Роганов
 * @version 1.1
 * Класс PolyedrTest, ...
 */
public class PolyedrTest {
    /**
     * Функция main.
     * @param args Массив аргументов командной строки. 
     * @exception Exception Исключительная ситуация, возникающая
     * при ошибках чтения или преобразования данных из файла.
     */
    public static void main(String[] args) throws Exception {
	Polyedr p = new Polyedr(args[0]);
	int type = Xterm.inputInt("Возможные типы изображения полиэдра:\n"+
			      "    без удаления невидимых линий - 0\n"+
			      "    с удалением невидимых ребер  - 1\n"+
			      "    с использованием хеширования - 2\n"+
			      "Выберите тип изображения (от 0 до 2) -> " );
	while (true) {
	    R3Vector pr = new R3Vector();
	    double angle = Xterm.inputDouble("Угол поворота в плоскости " +
					     "проекции (в градусах): ");
	    switch (type) {
	      case 0:
		SimpleDrawer d = new SimpleDrawer(p, pr, Math.PI*angle/180.0);
		d.draw();
		break;
	      case 1:
		ShadowDrawer sd = new ShadowDrawer(p, pr, Math.PI*angle/180.0);
		sd.draw();
		break;
	      case 2:
		SmartDrawer smd = new SmartDrawer(p, pr, Math.PI*angle/180.0);
		smd.draw();
		break;
	      default:
		Xterm.println("Неверный тип изображения");
	    }
	    Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
	}
    }
}
Вернуться к учебному плану