Координаты указателя и попадание в фигуру
Мы уже умеем рисовать документ «Схемы», но нажатие на изображение пока не связано с данными. В этом уроке получим точку в координатах документа и найдём фигуру под ней. Это основа выделения и будущего перетаскивания. Основная трудность возникает при разных размерах SVG и страницы: экранная точка сама по себе ещё не говорит, куда пользователь указал внутри схемы.
Понадобятся model.mjs и view.mjs из архива серии. Полный пример пока не запускался. Ниже мы разбираем вычисления и ожидаемые случаи, которые читатель сможет воспроизвести после запуска. Подготовьте представление о координатах из первого урока и порядке рисования из второго.
Почему clientX недостаточно
Событие указателя содержит clientX и clientY: положение относительно области просмотра браузера. У SVG есть отступы, собственный размер, viewBox и преобразование при сохранении пропорций. Вычитание левого края контейнера учитывает только часть этих условий. Например, при документе 1000 × 700 и контейнере 600 × 600 могут появиться свободные полосы вокруг изображения.
Пока размер страницы и контейнера случайно совпадает, ошибка незаметна. Затем пользователь уменьшает окно или приближает схему, и фигура начинает выбираться рядом с курсором. Поэтому преобразование должно опираться на текущую матрицу SVG. Метод getScreenCTM() возвращает матрицу преобразования локальной системы координат элемента к экранной системе.
Для обратного перехода берём обратную матрицу:
export function documentPoint(svg, event) {
const matrix = svg.getScreenCTM();
if (!matrix) throw new Error('Полотно ещё не имеет координат');
return new DOMPoint(event.clientX, event.clientY)
.matrixTransform(matrix.inverse());
}
Это полная функция из view.mjs. Мы вызываем метод у корневого SVG, потому что документ рисуется непосредственно в его координатах. Если позднее появится отдельная преобразованная группа сцены, преобразование нужно будет получать для неё. Иначе координаты модели и координаты точки снова окажутся в разных пространствах.
Проверка отсутствующей матрицы нужна при невидимом или ещё не подготовленном полотне. Ошибка должна попасть в сообщение интерфейса, а не продолжить вычисление с фиктивным нулём. В обычном показанном редакторе матрица ожидается доступной, но предположение о состоянии DOM не следует превращать в молчаливое значение по умолчанию.
Попадание в прямоугольник
У прямоугольника известны положение верхнего левого угла и размеры. Точка находится внутри, когда её горизонтальная координата лежит от x до x + width, а вертикальная — от y до y + height. В учебном примере границы включены: нажатие точно на край относится к фигуре.
point.x >= shape.x &&
point.x <= shape.x + shape.width &&
point.y >= shape.y &&
point.y <= shape.y + shape.height
Это выражение из ветви прямоугольника в hitShape(), а не отдельный обработчик события. Для начальной фигуры «Редактор» точка 150, 150 подходит, а точка 350, 150 уже находится за правым краем 340. Здесь не нужно читать DOM-атрибуты или вычислять размеры подписи. Источником геометрии остаётся документ.
Мы выбираем тело фигуры, а не вынесенную за него часть текста. Для узкой фигуры длинная подпись может визуально выходить за границы, но область выбора по-прежнему определяется прямоугольником. Это предсказуемое правило первой версии. Если позднее текст станет самостоятельным объектом, у него появится собственная геометрия и договор попадания.
Эллипс и нормализованная точка
Ограничивающий прямоугольник эллипса включает углы, которые не входят в сам эллипс. Чтобы эти пустые области не считались фигурой, используем уравнение эллипса. Сначала переводим точку относительно центра и делим координаты на соответствующие радиусы. Внутри фигуры сумма квадратов полученных значений не превышает единицу.
const dx = (point.x - shape.x - shape.width / 2)
/ (shape.width / 2);
const dy = (point.y - shape.y - shape.height / 2)
/ (shape.height / 2);
return dx * dx + dy * dy <= 1;
В центре оба значения равны нулю. В крайней правой точке dx равен единице, dy — нулю: попадание допускается. В углу ограничивающего прямоугольника оба значения по модулю равны единице, сумма квадратов равна двум: точка отклоняется. Размеры больше нуля уже гарантированы validateDocument(), поэтому деление на нулевой радиус исключается на границе модели.
Эти вычисления относятся к нашим двум простым типам. Произвольный SVG-path потребовал бы другого алгоритма и нового формата документа. Добавлять тег пути в рендерер, оставляя прямоугольную проверку попадания, было бы недостаточно: изображение и поведение разошлись бы.
Какая фигура считается верхней
Возможны наложения. Рендерер рисует массив по порядку, поэтому проверку попадания выполняем в обратном порядке и возвращаем первую подходящую фигуру. Так выбирается объект, который пользователь видит сверху. В hitShape() используется копия массива перед reverse(), чтобы поиск не поменял порядок документа.
Если вызвать doc.shapes.reverse() напрямую, само выделение начнёт переставлять слои. Такая ошибка особенно неприятна: вначале щелчок покажется работающим, а следующая перерисовка изменит картинку. Копия [...doc.shapes] отделяет порядок обхода от сохранённого порядка. Геометрию самих фигур при этом не копируем, потому что поиск её не меняет.
При отсутствии попадания функция возвращает undefined. Обработчик превращает этот результат в selected = null и снимает выделение. Пустое место — полноценный ожидаемый исход, а не ошибка документа. Во время выбора пока не создаём запись истории: просмотр разных фигур не меняет иллюстрацию.
Область выбора в нашей версии не расширяется вокруг тонкого края. Это означает, что человек должен попасть внутрь тела фигуры. Для маленьких объектов на сенсорном экране можно было бы добавить допустимое расстояние, но измерять его нужно в понятных единицах. Постоянные десять единиц документа при сильном приближении занимают другое число экранных пикселей. Поэтому такое расширение лучше выразить в экранном размере и перевести через преобразование.
Также не используем event.target как окончательный ответ о выбранной фигуре. Нажатие может прийти по подписи, фону или временной группе, а рендерер пересоздаёт дочерние узлы. Проверка модели даёт единый результат для разных деталей SVG и позволяет позднее перенести представление на Canvas без изменения геометрического договора.
Сценарии для самостоятельного разбора
Возьмите одну и ту же точку документа и рассмотрите её при полном виде и при приближении. Матрица преобразования будет другой, но возвращённые координаты должны описывать тот же объект. Затем расположите эллипс поверх прямоугольника. В зоне их пересечения ожидается выбор последнего элемента массива; после перестановки слоёв результат должен поменяться.
Отдельно сравните центр, край и угол эллипса. Это помогает обнаружить замену настоящей геометрии её ограничивающим прямоугольником. Пока эти проверки описаны, а не выполнены. Теперь редактор знает, какая фигура выбрана и где находится указатель в документе. В следующем уроке используем разницу двух таких точек для перемещения, сохранив исходное положение на время всего жеста.