So, gonna throw up an answer based on the throwaway idea I talked about in the comments...
The basic gist was "Define a type that conveys this concept of point duality, and use that in your relevant signatures so as to give the compiler the hints it needs"
One thing you should read whenever you hit the dreaded "Type cannot be inferred from usage" error: http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx
In that, Messr. Lippert spells out the harsh truth that only the parameters of the signature are checked during this inference stage, NOT the constraints. So we have to be a tiny bit more "specific" here.
First, let's define our "duality relationship" - I should note this is one way to set up these relationships, there are (in theory) an infinite variety of them.
public interface IDual<TPoint, TDualPoint>
where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint: IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{}
Now we go back and retrofit our existing signatures:
public interface IPoint<TPoint>
where TPoint:IPoint<TPoint>
{}
class TriPoint : IPoint<TriPoint>, IDual<TriPoint,HexPoint>
{}
class HexPoint : IPoint<HexPoint>, IDual<HexPoint,TriPoint>
{
// Normally you would rotate the point
public HexPoint Rotate240(){ return new HexPoint();}
}
And likewise on the "secondary types", the grids:
interface IGrid<TPoint, TDualPoint>
where TPoint: IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
TDualPoint GetDualPoint(TPoint point);
}
class HexGrid : IGrid<HexPoint, TriPoint>
{
public TriPoint GetDualPoint(HexPoint point)
{
return new TriPoint();
}
}
class TriGrid : IGrid<TriPoint, HexPoint>
{
public HexPoint GetDualPoint(TriPoint point)
{
return new HexPoint();
}
}
And finally on our utility methods:
static class Algorithms
{
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IEnumerable<IDual<TPoint, TDualPoint>> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
return
from TPoint point in shape
select transform(point);
}
public static IEnumerable<TPoint> TransformShape<TPoint, TDualPoint>(
IGrid<TPoint, TDualPoint> grid,
IEnumerable<IDual<TPoint, TDualPoint>> shape,
Func<TPoint, TPoint> transform)
where TPoint : IPoint<TPoint>, IDual<TPoint, TDualPoint>
where TDualPoint : IPoint<TDualPoint>, IDual<TDualPoint, TPoint>
{
return
from TPoint point in shape
//where transform(point) is in grid
select transform(point);
}
}
Note the signature on the method - we are saying "Hey, this list of things we're giving you, it absolutely has dual points", which is what's going to allow code like so:
HexGrid hexGrid = new HexGrid();
List<HexPoint> hexPointShape = new List<HexPoint>(); //Add some items
//Compiles
var rotatedShape1 = Algorithms
.TransformShape(
hexGrid,
hexPointShape,
point => point.Rotate240())
.ToList();
//Compiles
var rotatedShape2 = Algorithms
.TransformShape<HexPoint, TriPoint>(
hexPointShape,
point => point.Rotate240())
.ToList();
//Did not compile, but does now!
var rotatedShape3 = Algorithms
.TransformShape(
hexPointShape,
point => point.Rotate240())
.ToList();