Implementação de Peças de Quebra-Cabeça Vetoriais usando Avalonia

Ao desenvolver um jogo de quebra-cabeça, desenhar peças vteoriais de forma programática se torna um desafio. A solução consiste em criar uma geometria personalizada. A seguir, será demonstrado como utilizá-la e, por fim, o código-fonte completo.

Início Rápido

É possível atribuir uma instância de GeometriaPeca à proprieddae Data de um Path para exibir uma peça. Exemplo:

// Tamanho principal (sem considerar os encaixes)
var dimPrincipal = 100;

// Configuração dos encaixes em cada borda. O enum 'TipoRecorte' possui três valores:
// Concavo (encaixe para dentro), Convexo (encaixe para fora), Plano (borda reta).
var config = new ConfiguracaoPeca(
    esquerda: TipoRecorte.Convexo,
    cima: TipoRecorte.Convexo,
    direita: TipoRecorte.Concavo,
    baixo: TipoRecorte.Plano);

// Cria a geometria da peça
var geometria = new GeometriaPeca(dimPrincipal, config);

caminhoPeca.Data = geometria;

Propriedades da Peça

A classe expõe propriedades úteis como o tamanho real (área útil), o tamanho total (incluindo encaixes) e o centro da peça. O código abaixo monta um cenário para teste visual:

<Grid ColumnDefinitions="auto,1*">
    <Button Name="btnTestar" Click="AoClicarBotao" Content="Testar" />
    <Canvas Grid.Column="1" ClipToBounds="True">
        <Rectangle Name="retanguloTotal" Fill="Yellow" />
        <Rectangle Name="retanguloPrincipal" Fill="Aqua" />
        <Path Name="caminhoPeca" Stroke="Black" StrokeThickness="2" />
        <Ellipse Name="pontoCentro" Width="20" Height="20" Fill="Red"
                 RenderTransformOrigin="0.5,0.5">
            <Ellipse.RenderTransform>
                <TranslateTransform X="-10" Y="-10" />
            </Ellipse.RenderTransform>
        </Ellipse>
    </Canvas>
</Grid>

private List<ConfiguracaoPeca> _listaConfiguracoes;
private int _indiceAtual = 0;

protected override void OnLoaded(RoutedEventArgs e)
{
    base.OnLoaded(e);
    _listaConfiguracoes = new List<ConfiguracaoPeca>();

    var valoresEnum = Enum.GetValues(typeof(TipoRecorte));
    foreach (TipoRecorte esq in valoresEnum)
    {
        foreach (TipoRecorte sup in valoresEnum)
        {
            foreach (TipoRecorte dir in valoresEnum)
            {
                foreach (TipoRecorte inf in valoresEnum)
                {
                    _listaConfiguracoes.Add(new ConfiguracaoPeca(esq, sup, dir, inf));
                }
            }
        }
    }
}

private void AoClicarBotao(object? sender, RoutedEventArgs e)
{
    var dimPrincipal = 100;
    var configuracaoAtual = _listaConfiguracoes[_indiceAtual];
    _indiceAtual = (_indiceAtual + 1) % _listaConfiguracoes.Count;

    var peca = new GeometriaPeca(dimPrincipal, configuracaoAtual);
    caminhoPeca.Data = peca;

    // Atualiza os elementos visuais com as propriedades da peça
    Canvas.SetLeft(pontoCentro, peca.CentroPrincipal.X);
    Canvas.SetTop(pontoCentro, peca.CentroPrincipal.Y);

    retanguloTotal.Width = peca.TamanhoTotal.Width;
    retanguloTotal.Height = peca.TamanhoTotal.Height;

    retanguloPrincipal.Width = peca.RetanguloPrincipal.Width;
    retanguloPrincipal.Height = peca.RetanguloPrincipal.Height;
    Canvas.SetLeft(retanguloPrincipal, peca.RetanguloPrincipal.X);
    Canvas.SetTop(retanguloPrincipal, peca.RetanguloPrincipal.Y);
}

Código-Fonte Principal

A implementação completa inclui a definição dos tipos, a lógica de construção da geometria e os cálculos necessários para gerar as curvas dos encaixes.

using Avalonia;
using Avalonia.Media;
using System;
using System.Collections.Generic;

namespace MinhaSolucao
{
    /// <summary>
    /// Define o tipo de recorte na borda de uma peça.
    /// </summary>
    public enum TipoRecorte
    {
        Concavo, // Recorte para dentro
        Convexo, // Recorte para fora
        Plano    // Borda reta
    }

    /// <summary>
    /// Armazena a configuração de recortes para as quatro bordas de uma peça.
    /// </summary>
    public class ConfiguracaoPeca
    {
        public TipoRecorte Esquerda { get; }
        public TipoRecorte Cima { get; }
        public TipoRecorte Direita { get; }
        public TipoRecorte Baixo { get; }

        public ConfiguracaoPeca(TipoRecorte esquerda, TipoRecorte cima, TipoRecorte direita, TipoRecorte baixo)
        {
            Esquerda = esquerda;
            Cima = cima;
            Direita = direita;
            Baixo = baixo;
        }

        public static ConfiguracaoPeca DeString(string texto)
        {
            var partes = texto.Split(',', StringSplitOptions.RemoveEmptyEntries);
            return new ConfiguracaoPeca(
                Enum.Parse<TipoRecorte>(partes[0], true),
                Enum.Parse<TipoRecorte>(partes[1], true),
                Enum.Parse<TipoRecorte>(partes[2], true),
                Enum.Parse<TipoRecorte>(partes[3], true));
        }
    }

    /// <summary>
    /// Gera uma PathGeometry que representa uma peça de quebra-cabeça com encaixes.
    /// </summary>
    public class GeometriaPeca : PathGeometry
    {
        // Propriedades Públicas
        public Point CentroPrincipal { get; }
        public double DimensaoPrincipal { get; }
        public Size TamanhoTotal { get; }
        public Rect RetanguloPrincipal { get; }
        public ConfiguracaoPeca Configuracao { get; }
        public double TamanhoEncaixe { get; }

        // Construtor principal
        public GeometriaPeca(double dimPrincipal, ConfiguracaoPeca configuracao)
        {
            Configuracao = configuracao;
            DimensaoPrincipal = dimPrincipal;
            TamanhoEncaixe = ObterTamanhoEncaixe(dimPrincipal);

            ConstruirGeometria(dimPrincipal, configuracao);
            CalcularPropriedadesDerivadas(dimPrincipal, configuracao);
        }

        // Constrói a geometria da peça
        private void ConstruirGeometria(double dim, ConfiguracaoPeca config)
        {
            var dados = new DadosFormaPecaCompleta(dim, config);
            var figura = new PathFigure { IsClosed = true, StartPoint = dados.PontoInicial };

            for (int i = 0; i < dados.PontosDoContorno.Count - 1; i++)
            {
                var segmento = CriarSegmento(dados, i);
                if (segmento != null)
                {
                    figura.Segments.Add(segmento);
                }
            }

            Figures ??= new PathFigures();
            Figures.Add(figura);
        }

        // Cria um segmento (linha ou arco) baseado nos dados da forma
        private PathSegment? CriarSegmento(IDadosForma dados, int indice)
        {
            if (indice < 0 || indice >= dados.PontosDoContorno.Count)
                return null;

            var pontoAtual = dados.PontosDoContorno[indice];
            var pontoProximo = dados.PontosDoContorno[indice + 1];

            return pontoAtual.Tipo switch
            {
                TipoPonto.Linha => new LineSegment { Point = pontoProximo.Coordenada },
                TipoPonto.Arco => new ArcSegment
                {
                    Point = pontoProximo.Coordenada,
                    Size = pontoAtual.TamanhoArco,
                    RotationAngle = pontoAtual.AnguloArco,
                    IsLargeArc = pontoAtual.ArcoMaior,
                    SweepDirection = pontoAtual.DirecaoVarredura
                },
                _ => null
            };
        }

        // Calcula propriedades como centro e retângulo principal
        private void CalcularPropriedadesDerivadas(double dim, ConfiguracaoPeca config)
        {
            double offsetX = config.Esquerda == TipoRecorte.Convexo ? TamanhoEncaixe : 0;
            double offsetY = config.Cima == TipoRecorte.Convexo ? TamanhoEncaixe : 0;

            CentroPrincipal = new Point(dim / 2 + offsetX, dim / 2 + offsetY);
            RetanguloPrincipal = new Rect(offsetX, offsetY, dim, dim);

            double larguraTotal = dim;
            double alturaTotal = dim;
            if (config.Esquerda == TipoRecorte.Convexo) larguraTotal += TamanhoEncaixe;
            if (config.Direita == TipoRecorte.Convexo) larguraTotal += TamanhoEncaixe;
            if (config.Cima == TipoRecorte.Convexo) alturaTotal += TamanhoEncaixe;
            if (config.Baixo == TipoRecorte.Convexo) alturaTotal += TamanhoEncaixe;

            TamanhoTotal = new Size(larguraTotal, alturaTotal);
        }

        // Método estático auxiliar
        public static double ObterTamanhoEncaixe(double dimPrincipal) => dimPrincipal / 4.0;
    }

    // --- Classes auxiliares internas ---

    internal enum TipoPonto { Linha, Arco }

    internal class PontoContorno
    {
        public TipoPonto Tipo { get; }
        public Point Coordenada { get; }
        public double AnguloArco { get; }
        public Size TamanhoArco { get; }
        public SweepDirection DirecaoVarredura { get; }
        public bool ArcoMaior { get; }

        // Métodos fábrica simplificados
        public static PontoContorno Linha(Point ponto) => new() { Tipo = TipoPonto.Linha, Coordenada = ponto };
        public static PontoContorno Arco(Point ponto, Size tamanho, double angulo, SweepDirection direcao, bool maior) =>
            new() { Tipo = TipoPonto.Arco, Coordenada = ponto, TamanhoArco = tamanho, AnguloArco = angulo, DirecaoVarredura = direcao, ArcoMaior = maior };
    }

    internal interface IDadosForma
    {
        Point PontoInicial { get; }
        List<PontoContorno> PontosDoContorno { get; }
    }

    internal class DadosFormaPecaCompleta : IDadosForma
    {
        public Point PontoInicial { get; }
        public List<PontoContorno> PontosDoContorno { get; }

        // Campos para cálculos geométricos internos
        private readonly double _raioGrande;
        private readonly double _angulo;
        private readonly double _distanciaPrincipal;
        private readonly double _meio;
        private readonly Size _tamanhoPequeno;

        public DadosFormaPecaCompleta(double dimPrincipal, ConfiguracaoPeca config)
        {
            var encaixe = GeometriaPeca.ObterTamanhoEncaixe(dimPrincipal);
            _raioGrande = encaixe * 0.45;
            _distanciaPrincipal = encaixe * 0.55;
            _angulo = 45 * Math.PI / 180;
            _tamanhoPequeno = new Size(CalcularRaioPequeno(_distanciaPrincipal, _raioGrande, _angulo));
            _meio = CalcularMeio(_distanciaPrincipal, _raioGrande, _angulo);

            PontoInicial = new Point(
                config.Esquerda == TipoRecorte.Convexo ? encaixe : 0,
                config.Cima == TipoRecorte.Convexo ? encaixe : 0);

            PontosDoContorno = GerarContornoCompleto(dimPrincipal, config, encaixe);
        }

        private List<PontoContorno> GerarContornoCompleto(double dim, ConfiguracaoPeca config, double encaixe)
        {
            var pontos = new List<PontoContorno>();
            var metade = dim / 2;

            // Define os quatro cantos do quadrado principal
            var a = PontoInicial;
            var b = a + new Point(dim, 0);
            var c = b + new Point(0, dim);
            var d = c + new Point(-dim, 0);

            // Processa cada borda (A-B, B-C, C-D, D-A)
            pontos.AddRange(GerarBorda(a, b, config.Cima, true));
            pontos.AddRange(GerarBorda(b, c, config.Direita, false));
            pontos.AddRange(GerarBorda(c, d, config.Baixo, true));
            pontos.AddRange(GerarBorda(d, a, config.Esquerda, false));

            return pontos;
        }

        private List<PontoContorno> GerarBorda(Point inicio, Point fim, TipoRecorte tipo, bool eHorizontal)
        {
            var pontos = new List<PontoContorno> { PontoContorno.Linha(inicio) };

            if (tipo == TipoRecorte.Plano) return pontos;

            var vetorDir = (fim - inicio).Normalize();
            var normal = eHorizontal ? new Vector(0, 1) : new Vector(1, 0);
            var pontoMedio = (inicio + fim) / 2;
            var metadeComprimento = (fim - inicio).Length / 2;

            // Calcula pontos de controle para a curva do encaixe
            var dir = tipo == TipoRecorte.Convexo ? 1 : -1;
            var p1 = inicio + vetorDir * (metadeComprimento - _meio);
            var p2 = pontoMedio + normal * dir * _distanciaPrincipal;
            var p3 = pontoMedio + normal * dir * (-_distanciaPrincipal);
            var p4 = fim - vetorDir * (metadeComprimento - _meio);

            // Adiciona os arcos que formam o encaixe
            pontos.Add(PontoContorno.Arco(p1, _tamanhoPequeno, 0, dir > 0 ? SweepDirection.CounterClockwise : SweepDirection.Clockwise, false));
            pontos.Add(PontoContorno.Arco(p2, new Size(_raioGrande), 0, dir > 0 ? SweepDirection.Clockwise : SweepDirection.CounterClockwise, true));
            pontos.Add(PontoContorno.Arco(p3, _tamanhoPequeno, 0, dir > 0 ? SweepDirection.CounterClockwise : SweepDirection.Clockwise, false));
            pontos.Add(PontoContorno.Linha(p4));

            return pontos;
        }

        // Métodos auxiliares de cálculo
        private static double CalcularRaioPequeno(double distancia, double raioGrande, double angulo) =>
            (distancia - raioGrande * Math.Cos(angulo)) / (1 + Math.Cos(angulo));

        private static double CalcularMeio(double distancia, double raioGrande, double angulo)
        {
            var raioPequeno = CalcularRaioPequeno(distancia, raioGrande, angulo);
            return (raioPequeno + raioGrande) * Math.Sin(angulo);
        }
    }
}

Componente de Conveniência

Para simplificar o uso, pode-se encapsular a geometria em um controle Path personalizado:

/// <summary>
/// Um controle Path que renderiza automaticamente uma peça de quebra-cabeça.
/// </summary>
public class PecaQuebraCabeca : Path
{
    public Point CentroPrincipal { get; }
    public double DimensaoPrincipal { get; }
    public Size TamanhoTotal { get; }
    public Rect RetanguloPrincipal { get; }
    public ConfiguracaoPeca Configuracao { get; }
    public double TamanhoEncaixe { get; }

    public PecaQuebraCabeca(double dimPrincipal, ConfiguracaoPeca configuracao)
    {
        var geometria = new GeometriaPeca(dimPrincipal, configuracao);

        // Atribui a geometria ao Path
        Data = geometria;

        // Expõe as propriedades calculadas
        CentroPrincipal = geometria.CentroPrincipal;
        DimensaoPrincipal = geometria.DimensaoPrincipal;
        TamanhoTotal = geometria.TamanhoTotal;
        RetanguloPrincipal = geometria.RetanguloPrincipal;
        Configuracao = geometria.Configuracao;
        TamanhoEncaixe = geometria.TamanhoEncaixe;
    }
}

Tags: Avalonia C# Vetor Quebra-Cabeça XAML

Publicado em 7-15 09:44