Skip to content

Instantly share code, notes, and snippets.

@lancejpollard
Created January 19, 2023 14:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lancejpollard/7d09e03ef1897eb0f5427fa1b3030e36 to your computer and use it in GitHub Desktop.
Save lancejpollard/7d09e03ef1897eb0f5427fa1b3030e36 to your computer and use it in GitHub Desktop.
MagicTile Key Snippets for understanding Hyperbolic Tessellation Implementation
private void TrackClosest( Cell cell )
{
	// Are we tracking?
	if( !m_trackClosest )
		return;

	Complex transformedCenter = m_mouseMotion.Isometry.Apply( cell.Boundary.Center.ToComplex() );
	
	double distToOrigin = transformedCenter.Magnitude;
	if( distToOrigin < m_closestDist )
	{
		m_closestDist = distToOrigin;
		m_mouseMotion.Closest = cell;
	}
}

private void DrawCellDirectly( Cell cell )
{
	// What will be the closest to origin after panning transform?
	TrackClosest( cell );

	int lod = LOD( cell );
	if( -1 == lod )
		return;

	if( m_settings.ShowOnlyFundamental && !cell.IsMaster )
		return;

	// ZZZ:performance - avoid cloning these by passing in mobius to drawing functions?
	foreach( Sticker sticker in cell.Stickers )
	{
		if( sticker.Twisting )
			continue;

		Polygon p = sticker.Poly.Clone();
		p.Transform( m_mouseMotion.Isometry );
		Color color = GetStickerColor( sticker );
		GLUtils.DrawConcavePolygon( p, color, GrabModelTransform() );
	}
}

private void RenderDirectly( bool renderingTexture = false )
{
	SetupStandardGLSettings( m_settings.ColorTileEdges );
	if( !renderingTexture )
		GL.Disable( EnableCap.Texture2D );

	// We use the stencil buffer,
	// and need the depth buffer enabled as well.
	GL.ClearStencil( 0 );
	GL.Clear( ClearBufferMask.StencilBufferBit );
	GL.StencilMask( 0x01 );
	GL.Enable( EnableCap.StencilTest );
	GL.Disable( EnableCap.DepthTest );

	// Track what is close to the center.
	ResetClosest();

	foreach( Cell master in m_puzzle.MasterCells )
	{
		DrawCellDirectly( master );

		foreach( Cell slave in m_puzzle.SlaveCells( master ) )
			DrawCellDirectly( slave );
	}

	DrawMovingStickersDirectly();

	// Draw a background disk if needed.
	if( m_settings.SphericalModel == SphericalModel.Fisheye )
		FillBackgroundExceptDisk();

	GL.Disable( EnableCap.StencilTest );
}

private void ResetClosest()
{
	m_closestDist = double.MaxValue;
	m_mouseMotion.Closest = null;
	m_mouseMotion.Template = m_puzzle.MasterCells.First();
}

private void PerformClick( ClickData clickData )
{
	// Handle macros.
	if( AltDown )
	{
		if( this.ShowAsSkew )
		{
			string message = "Sorry, macros not supported on skew polyhedra puzzles at this time.";
			System.Windows.Forms.MessageBox.Show( message, "Unsupported", MessageBoxButtons.OK, MessageBoxIcon.Information );
		}

		// Get info about where we've clicked.
		Vector3D? spaceCoordsNoMouseMotion;
		Cell closest = FindClosestCell( clickData.X, clickData.Y, out spaceCoordsNoMouseMotion );
		if( closest == null || !spaceCoordsNoMouseMotion.HasValue )	// ZZZ - make more robust
			return;

		// Starting a macro?
		if( CtrlDown && clickData.Button == MouseButtons.Left )
		{
			Macro m = this.TwistHandler.m_workingMacro;
			m.SetupMobius( closest, spaceCoordsNoMouseMotion.Value, m_puzzle, m_mouseMotion.Isometry.Reflected );
			m.StartRecording();
			m_status();
		}
		else
		{
			// We're executing a macro.
			bool left = clickData.Button == MouseButtons.Left;
			bool right = clickData.Button == MouseButtons.Right;
			if( left || right )
			{
				Macro selected = m_selectedMacro();
				if( selected == null )
					return;

				Macro transformedMacro = selected.Transform( closest, spaceCoordsNoMouseMotion.Value, 
					m_puzzle, m_mouseMotion.Isometry.Reflected );
				this.TwistHandler.ApplyMacro( transformedMacro, right );
			}
		}

		return;
	}	

	//
	// From here on down, we're doing normal twisting.
	//
	bool skewReverseTwist = false;
	if( (ShowOnSurface || ShowAsSkew) && !RenderingDisks )
	{
		bool forPicking = true;
		if( m_puzzle.AllTwistData.Count > 0 )	// Trying to do picking on tilings will cause issues.
			RenderSurface( forPicking, clickData.X, clickData.Y, ref skewReverseTwist );
	}
	else
	{
		FindClosestTwistingCircles( clickData.X, clickData.Y );
	}

	if( m_puzzle.Config.IsToggling)
	{
		PerformTogglingClick(clickData);
	}

	if( m_closestTwistingCircles == null )
	{
		return;
	}

	SingleTwist twist = new SingleTwist();
	twist.IdentifiedTwistData = m_closestTwistingCircles.IdentifiedTwistData;
	twist.LeftClick = clickData.Button == MouseButtons.Left;
	if( m_puzzle.Config.Earthquake )
	{
		twist.SliceMask = m_choppedPantsSeg / 2;

		TwistData td = m_closestTwistingCircles;
		Vector3D lookup = td.Pants.TinyOffset( m_choppedPantsSeg );
		Vector3D reflected = td.Pants.Hexagon.Segments[m_choppedPantsSeg].ReflectPoint( lookup );
		TwistData tdEarthQuake = m_puzzle.ClosestTwistingCircles( reflected );
		
		twist.IdentifiedTwistDataEarthquake = tdEarthQuake.IdentifiedTwistData;
		twist.SliceMaskEarthquake = tdEarthQuake.Pants.Closest( reflected ) / 2;
	}
	else
		twist.SliceMask = this.SliceMaskEnsureSlice;
	
	// Correction when clicking on mirrored tiles for non-orientable puzzles.
	// We want the user to always see the tiles they left-click turn CCW.
	if( m_mouseMotion.Isometry.Reflected ^ m_closestTwistingCircles.Reverse )
		twist.LeftClick = !twist.LeftClick;
	
	// This correction is for skew puzzles.
	if( skewReverseTwist )
		twist.LeftClick = !twist.LeftClick;

	this.TwistHandler.StartRotate( twist );
}

private Cell FindClosestCell( int X, int Y, out Vector3D? spaceCoordsNoMouseMotion )
{
	spaceCoordsNoMouseMotion = SpaceCoordsNoMouseMotion( X, Y );

	if( m_puzzle == null || !spaceCoordsNoMouseMotion.HasValue )
		return null;

	return m_puzzle.ClosestCell( spaceCoordsNoMouseMotion.Value );
}

internal Cell ClosestCell( Vector3D location )
{
	NearTreeObject nearTreeObject;
	bool found = m_cellNearTree.FindNearestNeighbor( out nearTreeObject, location, double.MaxValue );
	if( !found )
		return null;

	Cell result = (Cell)nearTreeObject.ID;
	return result;
}

private void AddMaster( Tile tile, Tiling tiling, PuzzleIdentifications identifications, Dictionary<Vector3D, Cell> completed )
{
	Cell master = SetupCell( tiling.Tiles.First(), tile.Boundary, completed );
	master.IndexOfMaster = m_masters.Count;

	// Paranoia.
	if( 0 != this.Config.ExpectedNumColors &&
		master.IndexOfMaster >= this.Config.ExpectedNumColors )
	{
		//Debug.Assert( false );
		// It will already have an invalid index.
		master.IndexOfMaster = -1;
		return;
	}

	m_masters.Add( master );

	// This is to help with recentering on puzzles constructed via group relations.
	// We need to recurse deeper for some of them, but just for the slaves of the central tile.
	Tile template = tiling.Tiles.First();
	TilingPositions positions = null;
	if( master.IndexOfMaster == 0 && UsingRelations )
	{
		tiling = null;
		positions = new TilingPositions();
		positions.Build( new TilingConfig( Config.P, Config.Q, maxTiles: Config.NumTiles * 5 ) );
	}

	// Now add all the slaves for this master.
	List<Cell> parents = new List<Cell>();
	parents.Add( master );
	AddSlavesRecursive( master, parents, tiling, positions, template, identifications, completed );
}

private Cell ApplyOneIsometry( Cell master, Cell parent, Isometry identIsometry, Tiling tiling, TilingPositions positions, Tile template,
			Dictionary<Vector3D, Cell> completed )
{
	// Conjugate to get the identification relative to this parent.
	// NOTE: When we don't conjugate, some cells near the boundary are missed being identified.
	//		 I got around that by configuring the number of colors in the puzzle, and never adding more than that expected amount.
	//		 That was maybe a good thing to do anyway.
	// But conjugating was causing me lots of headaches because, e.g. it was causing extraneous mirroring/rotations
	// in puzzles like the Klein bottle, which don't have symmetrical identifications.  So I took it out for now.
	// NOTE: Later I tried conjugating for spherical puzzles, but that just produced bad puzzles (copies would have different colors adjacent).
	//		 So I think this is right.
	//Isometry conjugated = parent.Isometry.Inverse() * identIsometry * parent.Isometry;
	Isometry conjugated = identIsometry;

	// We can use the conjugates when using relations, because those are regular maps.
	//if( UsingRelations )
	//	conjugated = parent.Isometry.Inverse() * identIsometry * parent.Isometry;

	Vector3D newCenter = parent.VertexCircle.CenterNE;
	newCenter = conjugated.ApplyInfiniteSafe( newCenter );

	// ZZZ - Hack for spherical.  Some centers were projecting to very large values rather than DNE.
	if( Infinity.IsInfinite( newCenter ) )
		newCenter = Infinity.InfinityVector2D;

	// In the tiling?
	Tile tile;
	if( tiling != null && !tiling.TilePositions.TryGetValue( newCenter, out tile ) )
	{
		return null;
	}
	if( positions != null && !positions.Positions.Contains( newCenter ) )
	{
		return null;
	}

	// Already done this one?
	if( completed.ContainsKey( newCenter ) )
		return null;

	// New! Add it.
	Polygon boundary = parent.Boundary.Clone();
	boundary.Transform( conjugated );
	Cell slave = SetupCell( template, boundary, completed );
	AddSlave( master, slave );
	return slave;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment