4
\$\begingroup\$

This checks the intersection between a Ray and a Plane and between a Ray and a Quad (in 3D):

import Linear

data Ray a   = Ray {rayPos :: V3 a, rayDir :: V3 a}
data Plane a = Plane {planePos :: V3 a, planeNorm :: V3 a}
type Quad a  = V4 (V3 a)

hitPlane :: (Num a, Fractional a) => Ray a -> Plane a -> V3 a
hitPlane (Ray rPos rDir) (Plane pPos pNorm) = rPos + dot (pPos - rPos) pNorm / dot rDir pNorm *^ rDir

hitQuad :: (Ord a, Epsilon a, Num a, Floating a) => Ray a -> Quad a -> Maybe (V3 a)
hitQuad ray (quad@(V4 a b c d)) = if hitInsideQuad then Just hitPoint else Nothing 
    where hitInsideQuad = insideQuad hitPoint quad
          planeNormal   = cross (b - a) (d - a)
          hitPoint      = hitPlane ray (Plane a planeNormal)

insideQuad :: (Num a, Ord a) => V3 a -> Quad a -> Bool
insideQuad pos (V4 a b c d) = all inside borders where
    borders      = [(a,b),(b,c),(c,d),(d,a)]
    inside (a,b) = dot (b - a) (pos - a) > 0

Please, analyze if:

  1. (Most importantly) I'm using instances correctly (I'm suspect something is wrong with the amount of instances on the types).

  2. My design of types is correct and linguistic.

  3. My code is comprehensible.

  4. If I did something stupid in general.

\$\endgroup\$
3
\$\begingroup\$

I'll preface this by saying that I'm not a Haskell programmer, so take my comments advisedly.

  1. I would appreciate a short comment specifying the behaviour of each type and function. For example:

    -- The point of intersection between a ray and a plane.
    hitPlane :: (Fractional a) => Ray a -> Plane a -> V3 a
    

    This allows a reader to quickly understand the purpose of a function without having to read the code to find out. Also it provides a specification that can be checked against the implementation.

  2. In geometry the term ray usually refers to the half-line with a starting point and direction. So a ray does not necessarily hit a plane, and the type of hitPlane ought to be:

    hitPlane :: (RealFrac a) => Ray a -> Plane a -> Maybe (V3 a)
    

    If you intend your Ray type to be a full line, then it ought to be called Line instead, to avoid confusion. But even a full line might not have a point of intersection with a plane, because it might be parallel to the plane. In this case there will be a division by zero error: it would be better to avoid this and return Nothing.

  3. I don't understand all the details of the class constraints. hitPlane requires both Num a and Fractional a but if I understand the standard Haskell type documentation, the latter implies the former, so the Num a is redundant. Similarly for hitQuad, where Num a is redundant given that you have Floating a.

  4. hitQuad requires Epsilon a and Floating a, but I don't see how either of these classes is necessary. I would have expected just Ord a, Fractional a, and this combination is the same as RealFrac a.

  5. Some of the helper functions could be usefully made into top-level functions. For example, the implementation of hitQuad finds the plane containing a triangle of points. But this operation is generally useful:

    -- The plane containing three points, with a normal chosen so that
    -- the points are clockwise when looking in the normal direction.
    planeContaining :: (Num a) => (V3 a, V3 a, V3 a) -> Plane a
    planeContaining (a, b, c) = Plane a (cross (b - a) (c - a))
    
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.