packages feed

moonlight-triangulation-1.4.0.2: bench/spade-compare/hs/Moonlight/Triangulation/Bench/SpadeCompare/Voronoi.hs

{-# LANGUAGE DataKinds #-}

-- | The Voronoi dual gate and the dual sweep lane.
module Moonlight.Triangulation.Bench.SpadeCompare.Voronoi where

import Data.List (sort)
import System.FilePath ((</>))
import Moonlight.Triangulation
import Moonlight.Triangulation.Dcel
import Moonlight.Triangulation.Voronoi
import Moonlight.Triangulation.Bench.SpadeCompare.Support

-- | The dual, pinned by its combinatorics and by nothing else.
--
-- Every verb the dual offers resolves a primal element, so its answers are
-- exact over a mesh the bulk-load gate already pins bit for bit. Voronoi vertex
-- POSITIONS are the exception and are deliberately absent from every artifact
-- here: a position is a circumcentre, and the two implementations do not form
-- one the same way. spade takes the reciprocal of the determinant and
-- multiplies by it; this side first rescales both difference vectors by their
-- largest component and only then divides. Each rounds where the other does
-- not, so the positions cannot be bit-identical, and a gate over them would be
-- grading the arithmetic rather than the dual. What IS determined about a
-- position is whether it exists at all, and the vertex artifact pins that.
--
-- Direction vectors are pinned in full: each component is one subtraction of
-- two stored coordinates on both sides, so there is nothing left to round.
writeVoronoiGate :: FilePath -> Int -> IO ()
writeVoronoiGate directory count = do
  triangulation <- delaunayOf count
  let prefix = directory </> ("voronoi-" <> show count)
  writeFile
    (prefix <> "-dual.txt")
    (unlines (sort (map (dualEdgeRecord triangulation) (directedVoronoiEdges triangulation))))
  writeFile (prefix <> "-cells.txt") (unlines (voronoiCellLines triangulation))
  writeFile (prefix <> "-vertices.txt") (unlines (voronoiVertexLines triangulation))

-- | One directed dual edge: the primal edge it names, the site of the dual face
-- to its left, the dual vertex at each end, the destinations its dual @next@
-- and @prev@ reach, and its direction vector. @next@ and @prev@ rotate about
-- the primal origin, so naming them by destination alone loses nothing.
dualEdgeRecord
  :: DelaunayTriangulation Point
  -> DirectedVoronoiEdgeId
  -> String
dualEdgeRecord triangulation edge =
  concat
    [ coordHex (site (origin triangulation primal))
    , coordHex (site (destination triangulation primal))
    , coordHex (site (voronoiFaceSite (voronoiIncidentFace triangulation edge)))
    , dualVertexName triangulation primal (voronoiFrom triangulation edge)
    , dualVertexName triangulation primal (voronoiTo triangulation edge)
    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiNext triangulation edge))))
    , coordHex (site (destination triangulation (asDelaunayDirectedEdge (voronoiPrevious triangulation edge))))
    , coordHex (voronoiDirectionVector triangulation edge)
    ]
 where
  primal = asDelaunayDirectedEdge edge
  site = vertexPoint triangulation

-- | An inner dual vertex named by the one corner of its dual face that is not
-- an endpoint of the edge the question was asked about — the record already
-- carries the other two, so the apex completes the face. An outer dual vertex
-- is the edge itself and needs no name.
--
-- The @X@ tag cannot be reached: an inner dual vertex names an inner face by
-- construction, and 'innerFaceVertices' refuses only the outer one. spade
-- cannot emit it at all, its dual vertex carrying a face handle already typed
-- inner, so an @X@ appearing here is a disagreement rather than a crash.
dualVertexName
  :: DelaunayTriangulation Point
  -> DirectedEdgeId
  -> VoronoiVertexId
  -> String
dualVertexName _ _ (OuterVoronoiVertex _) = "O"
dualVertexName triangulation primal (InnerVoronoiVertex face) =
  case innerFaceVertices triangulation face of
    Nothing -> "X"
    Just (a, b, c)
      | isApex a -> named a
      | isApex b -> named b
      | otherwise -> named c
 where
  from = origin triangulation primal
  to = destination triangulation primal
  isApex vertex = vertex /= from && vertex /= to
  named vertex = "I" <> coordHex (vertexPoint triangulation vertex)

-- | The dual faces, one line each, plus the three enumerator lengths. The
-- undirected dual carries no artifact of its own: it is the primal undirected
-- edge set relabelled, which every existing edge gate already pins, so its
-- length is the only claim left to make about it.
voronoiCellLines :: DelaunayTriangulation Point -> [String]
voronoiCellLines triangulation =
  [ "faces " <> show (length (voronoiFaces triangulation))
  , "directed " <> show (length (directedVoronoiEdges triangulation))
  , "undirected " <> show (length (undirectedVoronoiEdges triangulation))
  ]
    <> sort
      [ coordHex (vertexPoint triangulation (voronoiFaceSite face)) <> concat (rotateToSmallest ring)
      | face <- voronoiFaces triangulation
      , let ring =
              [ coordHex
                  ( vertexPoint
                      triangulation
                      (destination triangulation (asDelaunayDirectedEdge edge))
                  )
              | edge <- voronoiFaceAdjacentEdges triangulation face
              ]
      ]

-- | The inner dual vertices, keyed by their dual face's three corners. Each
-- line carries whether the vertex has a position — the only exactly determined
-- thing about a circumcentre — and the outgoing triple the dual reports.
voronoiVertexLines :: DelaunayTriangulation Point -> [String]
voronoiVertexLines triangulation =
  sort
    [ concat (sort (map coordHex [site a, site b, site c]))
        <> existence dual
        <> concat (rotateToSmallest (outgoing dual))
    | face <- innerFaces triangulation
    , Just (a, b, c) <- [innerFaceVertices triangulation face]
    , let dual = InnerVoronoiVertex face
    ]
 where
  site = vertexPoint triangulation

  existence dual = case voronoiVertexPosition triangulation dual of
    Nothing -> "N"
    Just _ -> "J"

  outgoing dual = case voronoiVertexOutgoingEdges triangulation dual of
    Nothing -> []
    Just edges ->
      [ coordHex (site (origin triangulation primal)) <> coordHex (site (destination triangulation primal))
      | edge <- edges
      , let primal = asDelaunayDirectedEdge edge
      ]

-- | The whole dual, walked the way spade's own documentation walks it: every
-- dual face, its adjacent dual edges, and the dual vertex each of those runs
-- to. The position of an inner dual vertex is summed rather than merely
-- reached, because a 'Maybe' scrutinised only to its outer constructor can wrap
-- a thunk and the lane would then be timing the allocation of a circumcentre
-- instead of the circumcentre. Unbounded cells are counted, so a dual that
-- stopped classifying hull edges as outer moves the answer rather than the cost.
voronoiSweep :: DelaunayTriangulation Point -> (Double, Int)
voronoiSweep triangulation = foldl' overCell (0, 0) (voronoiFaces triangulation)
 where
  overCell accumulator face =
    foldl' overEdge accumulator (voronoiFaceAdjacentEdges triangulation face)

  overEdge (!total, !unbounded) edge =
    case voronoiVertexPosition triangulation (voronoiTo triangulation edge) of
      Nothing -> (total, unbounded + 1)
      Just (Point x y) -> (total + x + y, unbounded)