Обход дерева элементов

В основе работы с библиотекой Snap.svg лежит понимание того, что SVG-документ представляет собой древовидную структуру. Каждый элемент (прямоугольник, круг, группа и т.д.) может быть родителем или потомком других элементов. Это дерево аналогично DOM-структуре HTML, но с особенностями, характерными для векторной графики.

Типичная иерархия выглядит следующим образом:

  • Корневой элемент <svg>
  • Вложенные контейнеры <g>
  • Графические элементы (<rect>, <circle>, <path>, <text> и др.)

Группы (g) играют ключевую роль, так как позволяют объединять элементы и управлять ими как единым целым.


Получение доступа к элементам

Snap.svg предоставляет удобные методы для выбора элементов:

var s = Snap("#svg");
var rect = s.select("rect");
var allCircles = s.selectAll("circle");
  • select() возвращает первый найденный элемент
  • selectAll() — коллекцию элементов

Эти методы используют CSS-селекторы, что делает их гибкими и мощными.


Методы обхода дерева

Доступ к родительскому элементу

Каждый элемент Snap имеет ссылку на своего родителя:

var parent = element.parent();

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


Получение потомков

Для получения дочерних элементов используется метод:

var children = element.children();

Результат — массив элементов, входящих в текущий узел.


Поиск внутри поддерева

Методы select() и selectAll() работают не только от корня, но и относительно любого элемента:

var group = s.select("g");
var innerRects = group.selectAll("rect");

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


Итерация по элементам

Коллекции, возвращаемые selectAll(), поддерживают метод forEach():

s.selectAll("circle").forEach(function(el) {
    el.attr({ fill: "red" });
});

Это основной способ последовательного обхода элементов.


Рекурсивный обход дерева

Для полного обхода всех узлов используется рекурсия:

function traverse(element) {
    console.log(element.type);

    element.children().forEach(function(child) {
        traverse(child);
    });
}

traverse(s);

Особенности:

  • element.type содержит тип SVG-элемента (rect, circle, g и т.д.)
  • рекурсия позволяет обойти дерево любой глубины

Фильтрация элементов при обходе

В процессе обхода часто требуется работать только с определёнными типами элементов:

function traverse(element) {
    if (element.type === "rect") {
        element.attr({ fill: "blue" });
    }

    element.children().forEach(traverse);
}

Фильтрация может основываться на:

  • типе (element.type)
  • атрибутах (element.attr("class"))
  • пользовательских данных (element.data())

Использование пользовательских данных

Snap.svg позволяет привязывать данные к элементам:

element.data("id", 123);

При обходе:

if (element.data("id") === 123) {
    // обработка
}

Это удобно для построения логики, связанной с состоянием элементов.


Работа с группами (g)

Группы — ключевой инструмент при обходе:

var group = s.group(rect1, rect2);

При обходе:

  • группы рассматриваются как обычные элементы
  • содержат дочерние элементы
  • позволяют применять трансформации и стили ко всем потомкам

Пример:

function colorizeGroup(group) {
    group.children().forEach(function(el) {
        el.attr({ stroke: "black" });
    });
}

Обход с учётом трансформаций

Элементы могут иметь трансформации (transform), которые наследуются:

element.transform("translate(50,50)");

При обходе важно учитывать:

  • трансформации родителя влияют на потомков
  • визуальное положение элемента может отличаться от его локальных координат

Для анализа используется:

var matrix = element.transform().localMatrix;

Обход и изменение структуры

Snap.svg позволяет изменять дерево в процессе обхода:

Удаление элементов

element.remove();

Перемещение

element.appendTo(newParent);

Вставка

parent.add(newElement);

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


Безопасный обход при изменении дерева

Рекомендуемый подход:

var children = element.children().slice();

children.forEach(function(child) {
    if (child.type === "circle") {
        child.remove();
    } else {
        traverse(child);
    }
});

Копия массива (slice()) предотвращает ошибки.


Поиск предков с условиями

Иногда требуется найти ближайшего родителя с определёнными характеристиками:

function findParent(el, condition) {
    var current = el.parent();

    while (current) {
        if (condition(current)) {
            return current;
        }
        current = current.parent();
    }

    return null;
}

Пример использования:

var group = findParent(element, function(el) {
    return el.type === "g";
});

Практические сценарии обхода

Массовое изменение стилей

s.selectAll("*").forEach(function(el) {
    el.attr({ opacity: 0.5 });
});

Поиск элементов без атрибутов

function findWithoutFill(root) {
    var result = [];

    function traverse(el) {
        if (!el.attr("fill")) {
            result.push(el);
        }

        el.children().forEach(traverse);
    }

    traverse(root);
    return result;
}

Подсчёт элементов

function countElements(root) {
    var count = 0;

    function traverse(el) {
        count++;
        el.children().forEach(traverse);
    }

    traverse(root);
    return count;
}

Оптимизация обхода

При работе с большими SVG-документами важно учитывать:

  • минимизацию количества рекурсивных вызовов
  • использование селекторов вместо полного обхода, когда это возможно
  • кэширование результатов

Пример:

var rects = s.selectAll("rect");

Вместо:

traverse(s);

Комбинированные стратегии

На практике часто используется сочетание методов:

  1. Грубый отбор через selectAll()
  2. Точная фильтрация вручную
  3. Локальный обход поддеревьев

Пример:

s.selectAll("g").forEach(function(group) {
    group.selectAll("rect").forEach(function(rect) {
        rect.attr({ fill: "green" });
    });
});

Особенности работы с текстовыми узлами

Текст (text) также участвует в дереве:

var text = s.text(10, 10, "Hello");

При обходе:

  • текст рассматривается как обычный элемент
  • может иметь вложенные <tspan>

Работа с вложенными SVG

SVG может содержать вложенные <svg> элементы:

  • они создают отдельные области координат
  • обход продолжается рекурсивно
var nested = s.select("svg svg");

Итеративный обход без рекурсии

Альтернатива рекурсии — стек:

function traverseIterative(root) {
    var stack = [root];

    while (stack.length > 0) {
        var el = stack.pop();

        console.log(el.type);

        stack.push.apply(stack, el.children());
    }
}

Преимущества:

  • избегание переполнения стека вызовов
  • лучший контроль над процессом

Управление порядком обхода

  • DFS (глубина) — стандартный рекурсивный подход
  • BFS (ширина):
function traverseBFS(root) {
    var queue = [root];

    while (queue.length > 0) {
        var el = queue.shift();

        console.log(el.type);

        queue.push.apply(queue, el.children());
    }
}

Связь с DOM

Snap.svg оборачивает реальные DOM-элементы SVG:

element.node

Это позволяет:

  • использовать нативные методы DOM
  • комбинировать Snap.svg и стандартный JavaScript

Пример:

console.log(element.node.parentNode);

Распространённые ошибки при обходе

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

Архитектурные подходы

Для сложных проектов применяются:

  • разделение логики обхода и обработки
  • использование паттернов (Visitor)
  • хранение ссылок на элементы вне дерева для быстрого доступа

Пример паттерна Visitor:

function visit(element, handlers) {
    if (handlers[element.type]) {
        handlers[element.type](element);
    }

    element.children().forEach(function(child) {
        visit(child, handlers);
    });
}

Использование:

visit(s, {
    rect: function(el) {
        el.attr({ fill: "yellow" });
    },
    circle: function(el) {
        el.attr({ fill: "blue" });
    }
});