Skip to main content

13. Улучшения

В этой главе мы напишем unit-тесты, добавим рисование системы координат и реализуем масштабирование СК. Дальше дополним рисование курсора мыши, когда он находится над панелью рисования, будет добавлен вывод статистики. Также мы пропишем панель помощи

d

Пример готового проекта лежит здесь. Для выполнения текущего задания Fork от него делать запрещается.

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

Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте Fork готового проекта из предыдущей главы.

Unit тесты

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

Это довольно неудобный способ. Лучше будет написать автоматические тесты, которые выполнят всю рутину за нас. Такие тесты называются unit-тестами.

Все unit тесты создаются при помощи методов с аннотацией @Test. Неважно, в каком файле они определены.

Обычно тесты определяют в отдельных классах, а сами классы располагают внутри папки test/java. Если у вас этой папки вдруг нет, создайте её заново.

Чтобы создать файл Unit-теста, нужно кликнуть по папке test/java правой кнопкой мыши и выбрать New->Java Class.

w

Добавим в неё класс UnitTest.

w

В самом начале мы уже создали несколько Unit тестов.

Напомню, класс тестов лежит в src/test/java

example

Теперь нам необходимо прописать сами тесты. Напишем два теста, которые считают сумму от 0 до 9 и от 0 до 20, а потом сверяют с заданным заранее ответом. Это делается при помощи ключевого слова assert.

Проще всего воспринимать assert как функцию, принимающую логическую переменную типа boolean просто она указывается без скобок. Соответственно если sum в первом тесте не будет равна 45, то программа сгенерирует исключение AssertionError.

import org.junit.Test;

/**
* Класс тестирования
*/
public class UnitTest {

/**
* Первый тест
*/
@Test
public void firstTest() {
// сумма изначально равна 0
int sum = 0;
// суммируем числа от 0 до 9
for (int i = 0; i < 10; i++) {
sum += i;
}
// проверяем, равна ли сумма 45
assert sum == 45;
}

/**
* Второй тест
*/
@Test
public void secondTest() {
// сумма изначально равна 0
int sum = 0;
// суммируем числа от 0 до 19
for (int i = 0; i < 20; i++) {
sum += i;
}
// проверяем, равна ли сумма 200
assert sum == 200;
}
}

Классы тестов, как и любые другие можно документировать с помощью javaDoc. Если вы считаете, что это лишнее, то при сборке javaDoc вам нужно убрать галочку с пункта Include test sources.

w

Тесты в первый раз запускаются точно так же, как и обычные приложения. Только вместо метода main() здесь у класса может быть несколько точек входа. Строго говоря, каждый метод с аннотацией @Test является точкой входа. Поэтому для первого запуска теста нужно не просто открыть класс, но ещё и перевести курсор внутрь соответствующего метода. Например, можно просто кликнуть по его области видимости (то, что между фигурными скобками).

Теперь выбираем Run->Run...

w

В новом окне выбираем пункт UnitTest.firstTest

w

Тест запустится и внизу Idea выведет информацию о его результате.

w

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

Тогда в окне запуска появится новый пункт UnitTest

w

Второй тест выдал нам ошибку. Перейдём по ссылке (она выделена синим).

w

Ссылка привела нас к строке assert sum==200. Idea нам подсказывает, что переменная sum может не быть равна 190.

w

Так и есть. Сумма чисел от 0 до 19 равна 190. Поменяем условие на assert sum==190.

import org.junit.Test;

/**
* Класс тестирования
*/
public class UnitTest {

/**
* Первый тест
*/
@Test
public void firstTest() {
// сумма изначально равна 0
int sum = 0;
// суммируем числа от 0 до 9
for (int i = 0; i < 10; i++) {
sum += i;
}
// проверяем, равна ли сумма 45
assert sum == 45;
}

/**
* Второй тест
*/
@Test
public void secondTest() {
// сумма изначально равна 0
int sum = 0;
// суммируем числа от 0 до 19
for (int i = 0; i < 20; i++) {
sum += i;
}
// проверяем, равна ли сумма 190
assert sum == 190;
}
}

Теперь оба теста выполнятся успешно.

w

Перепишем теперь unit тесты так, чтобы они проверяли решение нашей задачи.

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

import app.Point;
import app.Task;
import misc.CoordinateSystem2d;
import misc.Vector2d;
import org.junit.Test;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
* Класс тестирования
*/
public class UnitTest {

/**
* Тест
*
* @param points список точек
* @param crossedCoords мн-во пересечений
* @param singleCoords мн-во разности
*/
private static void test(ArrayList<Point> points, Set<Vector2d> crossedCoords, Set<Vector2d> singleCoords) {
Task task = new Task(new CoordinateSystem2d(10, 10, 20, 20), points);
task.solve();
// проверяем, что координат пересечения в два раза меньше, чем точек
assert crossedCoords.size() == task.getCrossed().size() / 2;
// проверяем, что координат разности столько же, сколько точек
assert singleCoords.size() == task.getSingle().size();

// проверяем, что все координаты всех точек пересечения содержатся в множестве координат
for (Point p : task.getCrossed()) {
assert crossedCoords.contains(p.getPos());
}

// проверяем, что все координаты всех точек разности содержатся в множестве координат
for (Point p : task.getSingle()) {
assert singleCoords.contains(p.getPos());
}
}


/**
* Первый тест
*/
@Test
public void test1() {
ArrayList<Point> points = new ArrayList<>();

points.add(new Point(new Vector2d(1, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(-1, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(-1, 1), Point.PointSet.SECOND_SET));
points.add(new Point(new Vector2d(2, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(1, 2), Point.PointSet.SECOND_SET));
points.add(new Point(new Vector2d(1, 2), Point.PointSet.FIRST_SET));

Set<Vector2d> crossedCoords = new HashSet<>();
crossedCoords.add(new Vector2d(1, 2));
crossedCoords.add(new Vector2d(-1, 1));

Set<Vector2d> singleCoords = new HashSet<>();
singleCoords.add(new Vector2d(1, 1));
singleCoords.add(new Vector2d(2, 1));

test(points, crossedCoords, singleCoords);
}

/**
* Второй тест
*/
@Test
public void test2() {
ArrayList<Point> points = new ArrayList<>();

points.add(new Point(new Vector2d(1, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(2, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(2, 2), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(1, 2), Point.PointSet.FIRST_SET));

Set<Vector2d> crossedCoords = new HashSet<>();

Set<Vector2d> singleCoords = new HashSet<>();
singleCoords.add(new Vector2d(1, 1));
singleCoords.add(new Vector2d(2, 1));
singleCoords.add(new Vector2d(2, 2));
singleCoords.add(new Vector2d(1, 2));

test(points, crossedCoords, singleCoords);
}

/**
* Третий тест
*/
@Test
public void test3() {
ArrayList<Point> points = new ArrayList<>();

points.add(new Point(new Vector2d(1, 1), Point.PointSet.FIRST_SET));
points.add(new Point(new Vector2d(2, 1), Point.PointSet.SECOND_SET));
points.add(new Point(new Vector2d(2, 2), Point.PointSet.SECOND_SET));
points.add(new Point(new Vector2d(1, 2), Point.PointSet.FIRST_SET));

Set<Vector2d> crossedCoords = new HashSet<>();

Set<Vector2d> singleCoords = new HashSet<>();
singleCoords.add(new Vector2d(1, 1));
singleCoords.add(new Vector2d(2, 1));
singleCoords.add(new Vector2d(2, 2));
singleCoords.add(new Vector2d(1, 2));

test(points, crossedCoords, singleCoords);
}
}

Чтобы запустить тесты, кликните по области видимости класса, но снаружи метода. Т.е. любое свободное место не внутри метода.

После этого нажмите Run->Run...

example

В меню запуска выберите UnitTest

example

Все тесты должны выполниться корректно

example

Создайте новый коммит unit tests и отправьте его на сервер.

Система координат

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

Для этого введём в классе Task константу

    /**
* Порядок разделителя сетки, т.е. раз в сколько отсечек
* будет нарисована увеличенная
*/
private static final int DELIMITER_ORDER = 10;

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

В класс CoordinateSystem2i добавим два метода

    /**
* Получить вектор подобия двух систем координат
* (значения единичного размера, указанного в переданнной в аргументах СК в текущей СК)
*
* @param coordinateSystem система координат, подобие с которой нужно получить
* @return вектор подобий вдоль соответствующиъ осей координат
*/
public Vector2i getSimilarity(CoordinateSystem2d coordinateSystem) {
return new Vector2i(
(int) ((size.x - 1) / (coordinateSystem.getSize().x)),
(int) ((size.y - 1) / (coordinateSystem.getSize().y))
);
}

/**
* Получить вектор подобия двух систем координат
* (значения единичного размера, указанного в переданнной в аргументах СК в текущей СК)
*
* @param coordinateSystem система координат, подобие с которой нужно получить
* @return вектор подобий вдоль соответствующиъ осей координат
*/
public Vector2i getSimilarity(CoordinateSystem2i coordinateSystem) {
return new Vector2i(
(size.x - 1) / (coordinateSystem.getSize().x - 1),
(size.y - 1) / (coordinateSystem.getSize().y - 1)
);
}

и ещё два в CoordinateSystem2d

    /**
* Получить вектор подобия двух систем координат
* (значения единичного размера, указанного в переданнной в аргументах СК в текущей СК)
*
* @param coordinateSystem система координат, подобие с которой нужно получить
* @return вектор подобий вдоль соответствующиъ осей координат
*/
public Vector2d getSimilarity(CoordinateSystem2d coordinateSystem) {
return new Vector2d(
size.x / coordinateSystem.getSize().x,
size.y / coordinateSystem.getSize().y
);
}

/**
* Получить вектор подобия двух систем координат
* (значения единичного размера, указанного в переданнной в аргументах СК в текущей СК)
*
* @param coordinateSystem система координат, подобие с которой нужно получить
* @return вектор подобий вдоль соответствующиъ осей координат
*/
public Vector2d getSimilarity(CoordinateSystem2i coordinateSystem) {
return new Vector2d(
size.x / (coordinateSystem.getSize().x - 1),
size.y / (coordinateSystem.getSize().y - 1)
);
}

Теперь добавим цвет сетки в Colors

   /**
* Цвет сетки
*/
public static final int TASK_GRID_COLOR = Misc.getColor(64, 255, 255, 255);

и пропишем сам метод рисования сетки координат в классе Task

    /**
* Рисование сетки
*
* @param canvas область рисования
* @param windowCS СК окна
*/
public void renderGrid(Canvas canvas, CoordinateSystem2i windowCS) {
// сохраняем область рисования
canvas.save();
// получаем ширину штриха(т.е. по факту толщину линии)
float strokeWidth = 0.03f / (float) ownCS.getSimilarity(windowCS).y + 0.5f;
// создаём перо соответствующей толщины
try (var paint = new Paint().setMode(PaintMode.STROKE).setStrokeWidth(strokeWidth).setColor(TASK_GRID_COLOR)) {
// перебираем все целочисленные отсчёты нашей СК по оси X
for (int i = (int) (ownCS.getMin().x); i <= (int) (ownCS.getMax().x); i++) {
// находим положение этих штрихов на экране
Vector2i windowPos = windowCS.getCoords(i, 0, ownCS);
// каждый 10 штрих увеличенного размера
float strokeHeight = i % DELIMITER_ORDER == 0 ? 5 : 2;
// рисуем вертикальный штрих
canvas.drawLine(windowPos.x, windowPos.y, windowPos.x, windowPos.y + strokeHeight, paint);
canvas.drawLine(windowPos.x, windowPos.y, windowPos.x, windowPos.y - strokeHeight, paint);
}
// перебираем все целочисленные отсчёты нашей СК по оси Y
for (int i = (int) (ownCS.getMin().y); i <= (int) (ownCS.getMax().y); i++) {
// находим положение этих штрихов на экране
Vector2i windowPos = windowCS.getCoords(0, i, ownCS);
// каждый 10 штрих увеличенного размера
float strokeHeight = i % 10 == 0 ? 5 : 2;
// рисуем горизонтальный штрих
canvas.drawLine(windowPos.x, windowPos.y, windowPos.x + strokeHeight, windowPos.y, paint);
canvas.drawLine(windowPos.x, windowPos.y, windowPos.x - strokeHeight, windowPos.y, paint);
}
}
// восстанавливаем область рисования
canvas.restore();
}

Также вынесем рисование задачи в отдельный метод

    /**
* Рисование задачи
*
* @param canvas область рисования
* @param windowCS СК окна
*/
private void renderTask(Canvas canvas, CoordinateSystem2i windowCS) {
canvas.save();
// создаём перо
try (var paint = new Paint()) {
for (Point p : points) {
if (!solved) {
paint.setColor(p.getColor());
} else {
if (crossed.contains(p))
paint.setColor(CROSSED_COLOR);
else
paint.setColor(SUBTRACTED_COLOR);
}
// y-координату разворачиваем, потому что у СК окна ось y направлена вниз,
// а в классическом представлении - вверх
Vector2i windowPos = windowCS.getCoords(p.pos.x, p.pos.y, ownCS);
// рисуем точку
canvas.drawRect(Rect.makeXYWH(windowPos.x - POINT_SIZE, windowPos.y - POINT_SIZE, POINT_SIZE * 2, POINT_SIZE * 2), paint);
}
}
canvas.restore();
}

Теперь перепишем метод paint()

    /**
* Рисование
*
* @param canvas область рисования
* @param windowCS СК окна
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS) {
// Сохраняем последнюю СК
lastWindowCS = windowCS;
// рисуем координатную сетку
renderGrid(canvas, lastWindowCS);
// рисуем задачу
renderTask(canvas, windowCS);
}

Теперь в задаче появились оси координат

d

Создайте новый коммит cs rendering и отправьте его на сервер.

Масштабирование

Правда, отображается всего 1010 отчётов. Давайте теперь напишем масштабирование.

Для начала нам нужно написать метод вещественной СК CoordinateSystem2d, масштабирующий её относительно заданного центра в заданное количество раз

    /**
* Масштабировать СК пропорционально
*
* @param s коэффициент
* @param center центр масштабирования
*/
public void scale(double s, Vector2d center) {
// если центр масштабирования находится вне СК
if (!checkCoords(center)) {
PanelLog.warning("центр масштабирования находится вне области");
return;
}

// рассчитываем новые размеры СК
Vector2d newSize = Vector2d.mul(size, s);

// получаем коэффициенты масштабирования
Vector2d k = new Vector2d(
(max.x - center.x) / (center.x - min.x),
(max.y - center.y) / (center.y - min.y)
);

// рассчитываем новые границы
double newXMin = center.x - newSize.x / (k.x + 1);
double newYMin = center.y - newSize.y / (k.y + 1);

double newXMax = center.x + newSize.x * k.x / (k.x + 1);
double newYMax = center.y + newSize.y * k.y / (k.y + 1);

set(newXMin, newYMin, newXMax - newXMin, newYMax - newYMin);
}

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

Тогда, зная центр, новые размеры и отношение, несложно вывести формулы, используемые в методе scale()

Масштабировать мы будем с помощью колёсика мыши. События вращения колёсика мыши передают его смещение относительно прошлого положения. Причём один обычно отсчёт соответствует 100.

Поэтому нам нужно ввести понижающий множитель в классе Task

    /**
* коэффициент колёсика мыши
*/
private static final float WHEEL_SENSITIVE = 0.001f;

Теперь пропишем сам метод масштабирования

    /**
* Масштабирование области просмотра задачи
*
* @param delta прокрутка колеса
* @param center центр масштабирования
*/
public void scale(float delta, Vector2i center) {
if (lastWindowCS == null) return;
// получаем координаты центра масштабирования в СК задачи
Vector2d realCenter = ownCS.getCoords(center, lastWindowCS);
// выполняем масштабирование
ownCS.scale(1 + delta * WHEEL_SENSITIVE, realCenter);
}

Теперь изменим обработчик панели рисования

    /**
* Обработчик событий
* при перегрузке обязателен вызов реализации предка
*
* @param e событие
*/
@Override
public void accept(Event e) {
// вызов обработчика предка
super.accept(e);
// если событие - это клик мышью
if (e instanceof EventMouseButton ee) {
// если последнее положение мыши сохранено и курсор был внутри
if (lastMove != null && lastInside && ee.isPressed()) {
// если событие - нажатие мыши
if (ee.isPressed())
// обрабатываем клик по задаче
task.click(lastWindowCS.getRelativePos(lastMove), ee.getButton());
}
} else if (e instanceof EventMouseScroll ee) {
if (lastMove != null && lastInside)
task.scale(ee.getDeltaY(), lastWindowCS.getRelativePos(lastMove));
window.requestFrame();
}
}

Теперь масштабирование будет работать относительно курсора мыши

d

Создайте новый коммит scale works и отправьте его на сервер.

Указатель мыши

Добавим метод в класс Task, рассчитывающий положение курсора мыши в задаче

   /**
* Получить положение курсора мыши в СК задачи
*
* @param x координата X курсора
* @param y координата Y курсора
* @param windowCS СК окна
* @return вещественный вектор положения в СК задачи
*/
@JsonIgnore
public Vector2d getRealPos(int x, int y, CoordinateSystem2i windowCS) {
return ownCS.getCoords(x, y, windowCS);
}

Не забудьте его аннотировать @JsonIgnore.

Теперь напишем рисование мыши. Добавим новый метод в класс Task

    /**
* Рисование курсора мыши
*
* @param canvas область рисования
* @param windowCS СК окна
* @param font шрифт
* @param pos положение курсора мыши
*/
public void paintMouse(Canvas canvas, CoordinateSystem2i windowCS, Font font, Vector2i pos) {
// создаём перо
try (var paint = new Paint().setColor(TASK_GRID_COLOR)) {
// сохраняем область рисования
canvas.save();
// рисуем перекрестие
canvas.drawRect(Rect.makeXYWH(0, pos.y - 1, windowCS.getSize().x, 2), paint);
canvas.drawRect(Rect.makeXYWH(pos.x - 1, 0, 2, windowCS.getSize().y), paint);
// смещаемся немного для красивого вывода текста
canvas.translate(pos.x + 3, pos.y - 5);
// положение курсора в пространстве задачи
Vector2d realPos = getRealPos(pos.x, pos.y, lastWindowCS);
// выводим координаты
canvas.drawString(realPos.toString(), 0, 0, font, paint);
// восстанавливаем область рисования
canvas.restore();
}
}

Теперь перепишем paintImpl() в панели рисования

    /**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
task.paint(canvas, windowCS);
if (lastInside && lastMove != null)
task.paintMouse(canvas, windowCS, FONT12, lastWindowCS.getRelativePos(lastMove));
}

Запустим программу. Всё работает.

d

Создайте новый коммит mouse cursor и отправьте его на сервер.

FPS

Теперь добавим отображение FPS. Мы будем рисовать не только число, но и статистику FPS за последние несколько тактов.

Для расчёта FPS напишем класс суммирующей FIFO очереди. Т.е. помимо обычной логики "первый вошёл - первый вышел", мы добавим ей функцию расчёта среднего значения.

В пакет misc добавим класс SumQueue

package misc;

/**
* Класс суммирующей очереди
*/
public class SumQueue {
/**
* Предел суммирования
*/
int SUM_LIMIT = 1000;
/**
* Начальная длина очереди
*/
int QUEUE_INIT_LENGTH = 180;
/**
* Сколько записей мы уже получили
*/
int dataLength;
/**
* положение курсора
*/
int cursor;
/**
* Значения
*/
public float[] values = new float[QUEUE_INIT_LENGTH];

/**
* Добавить элемент
*
* @param a значение элемента
*/
public void add(float a) {
// задаём новое значение
values[cursor] = a;
// сдвигаем курсор по кругу
cursor = (cursor + 1) % values.length;
// если длина данных меньше длины массива элементов
if (dataLength < values.length)
// увеличиваем её на 1
dataLength++;
}

/**
* Получить среднее значение очереди
*
* @return среднее значение очереди
*/
public double getMean() {
double res = 0;
int cnt = 0;
// перебираем все элементы массива
for (int i = 0; i < Math.min(dataLength, values.length); i++) {
// но со сдвигом
var realPos = (cursor - i + values.length) % values.length;
// если dt на
if (values[realPos] > 0) {
res += values[realPos];
cnt++;
}
// если результат больше предела
if (res > SUM_LIMIT)
// прерываем цикл
break;
}
// выводим среднее
return res / cnt;
}

/**
* Получить длину очереди
*
* @return длина очереди
*/
public int getLength() {
return values.length;
}

/**
* Задать длину очереди
*
* @param len новая длина
*/
public void setLength(int len) {
// курсор в начало
cursor = 0;
// пересоздаём массив
values = new float[len];
// данных нет
dataLength = 0;
}

/**
* Получить элемент очереди ппо индексу
*
* @param i индекс
* @return элемент очереди
*/
public float get(int i) {
// получаем положение с учётом зацикливания
var idx = (cursor + i) % values.length;
return values[idx];
}
}

Добавим теперь цвета статистики FPS в Colors

    /**
* Цвет подложки
*/
public static final int STATS_BACKGROUND_COLOR = Misc.getColor(64, 51, 200, 51);
/**
* Цвет подложки
*/
public static final int STATS_COLOR = Misc.getColor(255, 51, 200, 51);
/**
* Цвет текста
*/
public static final int STATS_TEXT_COLOR = Misc.getColor(255, 255, 255, 255);

Нам осталось добавить класс Stats в тот же пакет misc

package misc;

import io.github.humbleui.skija.*;
import misc.CoordinateSystem2i;
import misc.SumQueue;

import static app.Colors.*;

/**
* Cтатистика
*/
public class Stats {

/**
* Время старта
*/
public long prevTime = System.nanoTime();
/**
* Очередь временных меток
*/
private final SumQueue deltaTimes = new SumQueue();


/**
* Рисование
*
* @param canvas область рисования
* @param windowCS СК окна
* @param font шрифт
* @param padding отступ
*/
public void paint(Canvas canvas, CoordinateSystem2i windowCS, Font font, int padding) {
// создаём кисть
try (var paint = new Paint()) {
// сохраняем область рисования
canvas.save();
// смещаем
canvas.translate(padding, windowCS.getSize().y - padding - 32);
// задаём цвет подложки
paint.setColor(STATS_BACKGROUND_COLOR);
// X, Y, ширина, высота,
// радиусы скругления для каждого угла
canvas.drawRRect(RRect.makeXYWH(
0, 0, windowCS.getSize().x - padding * 2, 32,
4, 4, 0, 0), paint);
paint.setColor(STATS_COLOR);

// рисуем сам график
for (int i = 0; i < deltaTimes.getLength(); i++) {
float currentDelta = deltaTimes.get(i);
canvas.drawRect(Rect.makeXYWH(i, Math.min(windowCS.getSize().y, 32 - currentDelta), 1, currentDelta), paint);
}

// рассчитываем длину очереди
int len = windowCS.getSize().x - padding * 2;
// если она получилась положительной и новая длина отличается от старой
if (len > 0 && deltaTimes.getLength() != len) {
// задаём новую длину
deltaTimes.setLength(len);
}

// получаем текущее время в мс.
long now = System.nanoTime();
// переводим его в секунды
deltaTimes.add((now - prevTime) / 1000000.0f);
// сохраняем новое время
prevTime = now;

// восстанавливаем область рисования
canvas.restore();

// сохраняем область рисования
canvas.save();
// задаём цвет
paint.setColor(STATS_TEXT_COLOR);
// перемещаемся в правый верхний край
canvas.translate(windowCS.getSize().x - padding - 50, padding + 15);
// формируем строку с текущим fps
String fps = String.format("%.01f", (1 / deltaTimes.getMean() * 1000));
// выводим её
canvas.drawString("FPS: " + fps, 0, 0, font, paint);
// восстанавливаем область рисования
canvas.restore();
}
}

}

Теперь добавим объект статистики на панель рисования PanelRendering:

    /**
* Статистика fps
*/
private final Stats fpsStats;

и инициализируем её в конструкторе:

    ...
fpsStats = new Stats();
...

Теперь слегка перепишем метод paint()

    /**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// рисуем задачу
task.paint(canvas, windowCS);
// рисуем статистику фпс
fpsStats.paint(canvas, windowCS, FONT12, padding);
// рисуем перекрестие, если мышь внутри области рисования этой панели
if (lastInside && lastMove != null)
task.paintMouse(canvas, windowCS, FONT12, lastWindowCS.getRelativePos(lastMove));
}

Теперь программа начнёт отображать ещё и статистику по кадрам в секунду

d

Создайте новый коммит render statistic и отправьте его на сервер.

Панель помощи

Теперь заполним панель помощи.

Добавим цвета панели помощи в Colors:

    /**
* Цвет текста
*/
public static final int HELP_TEXT = Misc.getColor(255, 255, 255, 255);
/**
* Цвет фона
*/
public static final int HELP_TEXT_BACKGROUND = Misc.getColor(50, 0, 0, 0);

В своей сути панель Help - это просто список линий. Поэтому подробно её код разбирать не будем.

package panels;

import java.util.*;

import io.github.humbleui.jwm.*;
import io.github.humbleui.skija.*;
import io.github.humbleui.skija.RRect;
import misc.CoordinateSystem2i;

import static app.Colors.HELP_TEXT;
import static app.Colors.HELP_TEXT_BACKGROUND;
import static app.Fonts.FONT12;

/**
* Панель поддержки
*/
public class PanelHelp extends GridPanel {
/**
* Отступ в списке
*/
float HELP_PADDING = 8;

/**
* Управляющие сочетания клавиш
*/
record Shortcut(String command, boolean ctrl, String text) {
}

/**
* список управляющих сочетаний клавиш
*/
public List<Shortcut> shortcuts = new ArrayList<>();

/**
* Панель поддержки
*
* @param window окно
* @param drawBG флаг, нужно ли рисовать подложку
* @param color цвет подложки
* @param padding отступы
* @param gridWidth кол-во ячеек сетки по ширине
* @param gridHeight кол-во ячеек сетки по высоте
* @param gridX координата в сетке x
* @param gridY координата в сетке y
* @param colspan кол-во колонок, занимаемых панелью
* @param rowspan кол-во строк, занимаемых панелью
*/
public PanelHelp(
Window window, boolean drawBG, int color, int padding, int gridWidth, int gridHeight,
int gridX, int gridY, int colspan, int rowspan
) {
super(window, drawBG, color, padding, gridWidth, gridHeight, gridX, gridY, colspan, rowspan);
shortcuts.add(new Shortcut("O", true, "Открыть"));
shortcuts.add(new Shortcut("S", true, "Сохранить"));
shortcuts.add(new Shortcut("H", true, "Свернуть"));
shortcuts.add(new Shortcut("1", true, "Во весь экран/Обычный размер"));
shortcuts.add(new Shortcut("2", true, "Полупрозрачное окно/обычное"));
shortcuts.add(new Shortcut("Esc", false, "Закрыть окно"));
shortcuts.add(new Shortcut("ЛКМ", false, "Добавить в первое множество"));
shortcuts.add(new Shortcut("ПКМ", false, "Добавить во второе множество"));
}

/**
* Метод под рисование в конкретной реализации
*
* @param canvas область рисования
* @param windowCS СК окна
*/
@Override
public void paintImpl(Canvas canvas, CoordinateSystem2i windowCS) {
// получаем модификатор в зависимости от операционной системы
// 8984 - код символа cmd у Mac
String modifier = Platform.CURRENT == Platform.MACOS ? ((char) 8984 + " ") : "Ctrl ";


// Получаем кисти
try (Paint bg = new Paint().setColor(HELP_TEXT_BACKGROUND);
Paint fg = new Paint().setColor(HELP_TEXT)) {
// метрика фона
FontMetrics metrics = FONT12.getMetrics();
// высота букв
float capHeight = metrics.getCapHeight();
// ширина команды прибавления
float bgWidth = 0;
// получаем строку с модификатором
try (TextLine line = TextLine.make(modifier + "W", FONT12)) {
bgWidth = line.getWidth() + 4 * HELP_PADDING;
}
// получаем высоту
float bgHeight = capHeight + HELP_PADDING * 2;

// положение первой строки
float x = HELP_PADDING;
float y = HELP_PADDING;

// перебираем комбинации
for (Shortcut shortcut : shortcuts) {
// получаем полный текст команды
String shortcutCommand = shortcut.ctrl ? modifier + shortcut.command : shortcut.command;
// формируем строку команды
try (TextLine line = TextLine.make(shortcutCommand, FONT12)) {
canvas.drawRRect(RRect.makeXYWH(x, y, bgWidth, bgHeight, 4), bg);
canvas.drawTextLine(line, x + (bgWidth - line.getWidth()) / 2, y + HELP_PADDING + capHeight, fg);
}
// формируем строку с описанием
try (TextLine line = TextLine.make(shortcut.text, FONT12);) {
canvas.drawTextLine(line, x + bgWidth + HELP_PADDING, y + HELP_PADDING + capHeight, fg);
}
// смещаемся вниз на
y += HELP_PADDING + capHeight * 2 + 2;
}

}
}
}

Запустим программу и убедимся, что панель помощи отображается корректно

d

Создайте новый коммит help panel и отправьте его на сервер.

Задание

Пример готового проекта лежит здесь. Для выполнения первого задания Fork от него делать запрещается.

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

Если у вас не выполнено предыдущее задание или выполнено не полностью, то сделайте Fork готового проекта из предыдущей главы.

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

  1. unit test - напишите три unit-теста для проверки правильности решения задачи
  2. cs rendering - пропишите рисование системы координат
  3. scale works - пропишите масштабирование задачи с помощью колёсика мыши
  4. mouse cursor - пропишите отображение курсора мыши с помощью перекрестия
  5. render statistic - добавьте рисование статистики на панель рисования
  6. help panel - пропишите панель помощи

d

Ссылку на github репозиторий необходимо отправить в поле ввода задания на сайте mdl.

Ссылка на контест