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;
}
}