Lição 26 - Ferramenta de seleção
Na Lição 14 - Modo de seleção só implementamos a seleção simples por clique em formas individuais. Nesta lição, vamos continuar melhorando a ferramenta de seleção, acrescentando seleção múltipla, seleção por letreiro e laço.
Seleção múltipla
A partir da seleção por clique individual, manter Shift pressionado permite adicionar ou remover formas da seleção atual.

No modo de seleção, decidimos se a seleção atual é preservada pelo estado de input.shiftKey (se Shift está pressionado): se não estiver, passamos à seleção única; se estiver, adicionamos a forma alvo à seleção existente:
if (selection.mode === SelectionMode.SELECT) {
if (layersSelected.length > 1 && layersSelected.includes(selected.id)) {
// deselect if already selected in a group
api.deselectNodes([selected]);
} else {
api.selectNodes([selected], input.shiftKey); // whether to add to existed selection
}
}Na Lição 21 - Transformer implementamos a seleção de uma única forma. Em seguida, precisamos exibir um Group para várias formas selecionadas. Ao contrário do Transformer de seleção única, o Group da seleção múltipla não precisa considerar rotation e scale.
export class RenderTransformer extends System {
getOBB(camera: Entity): OBB {
const { selecteds } = camera.read(Transformable);
// Single selected, keep the original OBB include rotation & scale.
if (selecteds.length === 1 && selecteds[0].has(ComputedBounds)) {
const { obb } = selecteds[0].read(ComputedBounds);
return obb;
}
if (selecteds.length > 1) {
}
}
}O efeito é o seguinte. A lógica para transformar todas as formas selecionadas durante o redimensionamento já foi apresentada na Lição 21 - Transformar formas, então não vamos repeti-la aqui:
Seleção por letreiro
A imagem a seguir vem de Select layers and objects in Figma

Essa ferramenta de seleção por letreiro se chama "marquee". Veja: Make selections with the Rectangular Marquee tool. Chamamos a área retangular formada de pincel (Brush).
Quando a seleção por letreiro termina (mouse up), primeiro precisamos ocultar o retângulo do pincel (vamos ver a implementação na próxima seção) e depois usar o método rápido de picking apresentado na Lição 8 - Usar indexação espacial para aceleração. Como a largura e a altura do retângulo podem ser negativas, conforme a direção do arraste, é preciso fazer alguns cálculos para garantir que a BBox seja válida:
if (input.pointerUpTrigger) {
if (selection.mode === SelectionMode.BRUSH) {
// Hide Brush...
if (selection.brush) {
const { x, y, width, height } = selection.brush.read(Rect);
// Make a valid BBox
const minX = Math.min(x, x + width);
const minY = Math.min(y, y + height);
const maxX = Math.max(x, x + width);
const maxY = Math.max(y, y + height);
const selecteds = api
.elementsFromBBox(minX, minY, maxX, maxY) // Use space index
.filter((e) => !e.has(UI))
.map((e) => api.getNodeByEntity(e));
api.selectNodes(selecteds); // Finish selection
}
}
}Durante a seleção por letreiro, também queremos mostrar o estado da seleção em tempo real, com destaque e Transformer. Com base na lógica de picking e seleção acima, adicionamos o destaque:

api.selectNodes(selecteds);
if (needHighlight) {
api.highlightNodes(selecteds);
}É claro que, para componentes no espaço do viewport, também dá para implementá-los com SVG, como vamos ver na ferramenta Laço a seguir.
Desmarcar com Esc
Pressionar Esc com algo selecionado cancela a seleção. Além disso, durante a seleção por letreiro, precisamos ocultar o pincel:
if (input.key === 'Escape') {
api.selectNodes([]);
if (selection.mode === SelectionMode.BRUSH) {
this.hideBrush(selection);
}
}Bloquear e desbloquear camadas
Camadas bloqueadas não podem ser selecionadas. Para detalhes, veja: Lock and unlock layers
Seleção laço
Em comparação com a Ferramenta Letreiro Retangular, a Ferramenta Laço permite seleções mais precisas, criando polígonos irregulares.
Na edição de imagem baseada em IA, a ferramenta laço também pode criar máscaras com mais precisão para fazer inpainting. A imagem abaixo mostra o efeito no Figma, em que as áreas selecionadas podem ser apagadas ou separadas em camadas:
Desenhar o laço
Na Lição 25 - Ferramenta lápis, vimos como desenhar caminhos de polilinha livremente. Continuamos desenhando os caminhos do laço dentro do contêiner SVG, em geral representados por linhas tracejadas com animação (formigas marchando).
<path d="...">
<animate
attribute-name="stroke-dashoffset"
stroke-dasharray="7 7"
stroke-dashoffset="10"
from="0"
to="-14"
dur="0.3s"
/>
</path>Primeiro, converta as coordenadas dos pontos do sistema de coordenadas do viewport para o da tela. Depois, simplifique o caminho de acordo com o zoom atual da câmera: em zoom alto, a seleção precisa de granularidade mais fina, e o contrário em zoom baixo. Menos vértices também melhoram o desempenho da renderização e a eficiência da detecção de interseção que vem a seguir:
import simplify from 'simplify-js';
const lassoPath = super
.getCurrentTrail()
?.originalPoints?.map(([x, y]) => this.api.viewport2Canvas({ x, y }));
const simplifyDistance = 5 / this.api.getAppState().cameraZoom;
const points = simplify(lassoPath, simplifyDistance).map((p) => [p.x, p.y]);
// Hit-test the current path as it changes for preview, and again on pointerup
// before clearing the trail to commit the final selection.
const selected = selectByLassoPath(this.api, points);Arraste ao redor das formas para ver os acertos na hora: os contornos aparecem ou somem conforme o laço muda. O laço incompleto se fecha sozinho de volta ao ponto inicial para o teste de acerto. Solte para confirmar a seleção final em um único passo de desfazer. Pressione Escape, cancele o gesto de toque, inicie um gesto de pinça ou troque de ferramenta para limpar a prévia sem selecionar. Toques e movimentos mínimos não selecionam nada. Escolha a ferramenta laço de novo para outra seleção. O modo de desenho de máscara cria o caminho só ao soltar e não mostra a prévia da seleção de formas.
O teste de acerto usa coordenadas de mundo, incluindo as transformações pai de cada forma, a rotação e os espelhamentos. Polilinhas abertas são testadas segmento a segmento, sem uma aresta de fechamento implícita. Formas bloqueadas e ocultas ficam de fora.
Detecção de interseção de polígonos
Com o Path criado, precisamos obter a geometria da cena que o intersecta. Como antes, começamos pela Lição 8 - Usar indexação espacial para aceleração.
function selectByLassoPath(api: API, lassoPath: [number, number][]) {
const lassoBounds = lassoPath.reduce(
(acc, item) => {
return [
Math.min(acc[0], item[0]),
Math.min(acc[1], item[1]),
Math.max(acc[2], item[0]),
Math.max(acc[3], item[1]),
];
},
[Infinity, Infinity, -Infinity, -Infinity],
) as [number, number, number, number];
// Hit-test with rbush
const elements = api.elementsFromBBox(
lassoBounds[0],
lassoBounds[1],
lassoBounds[2],
lassoBounds[3],
);
// elementsFromBBox excludes locked elements; also filter UI and hidden nodes.
}Depois da detecção rápida pela caixa delimitadora, dois casos precisam ser tratados: o laço está inteiramente dentro do gráfico; o laço intersecta o gráfico.
function isPolygonsIntersect(points1: number[][], points2: number[][]) {
let isIn = false;
// Determine whether a point lies inside a polygon. If any point is inside another polygon, return true.
points2.forEach((point) => {
if (isPointInPolygon(points1, point[0], point[1])) {
isIn = true;
return false;
}
});
if (isIn) {
return true;
}
}