0 | module CyBy.Draw.Internal.Graph
   1 |
   2 | import Chem.Util
   3 | import CyBy.Draw.Internal.CoreDims
   4 | import CyBy.Draw.Internal.Atom
   5 | import CyBy.Draw.Internal.Role
   6 | import Data.Graph.Indexed.Subgraph
   7 | import Data.SortedMap
   8 | import Data.SortedSet
   9 | import Derive.Prelude
  10 | import Geom
  11 | import Text.Molfile
  12 | import Text.ParseError
  13 |
  14 | %language ElabReflection
  15 | %default total
  16 |
  17 | ||| In several places, we decide what to do on a UI event
  18 | ||| based on whether any nodes or edges are selected or being
  19 | ||| hovered over.
  20 | public export
  21 | data NOE : (a,b : Type) -> Type where
  22 |   None  : NOE a b      -- the empty case
  23 |   N     : a -> NOE a b -- the "nodes" case
  24 |   E     : b -> NOE a b -- the "edges" case
  25 |
  26 | ||| Creates an `NOE` from two `Maybe`s.
  27 | export
  28 | noe : Maybe a -> Lazy (Maybe b) -> NOE a b
  29 | noe mn me = maybe (maybe None E me) N mn
  30 |
  31 | ||| Like `noe`, but edges take precedence.
  32 | export
  33 | eon : Lazy (Maybe a) -> Maybe b -> NOE a b
  34 | eon mn me = maybe (maybe None N mn) E me
  35 |
  36 | ||| Creates an `NOE` from two `Lists`s.
  37 | |||
  38 | ||| This returns `None` in case both lists are empty.
  39 | export
  40 | eons : List a -> Lazy (List b) -> NOE (List a) (List b)
  41 | eons _  es@(_::_) = E es
  42 | eons ns []        = if null ns then None else N ns
  43 |
  44 | ||| Bond type used in cyby-draw.
  45 | |||
  46 | ||| This is an mol-file bond paired with a role used for drawing.
  47 | public export
  48 | record CDBond where
  49 |   constructor CB
  50 |   role    : Role
  51 |   molBond : MolBond
  52 |
  53 | %runElab derive "CDBond" [Show,Eq]
  54 |
  55 | export %inline
  56 | Cast CDBond Role where cast = role
  57 |
  58 | export %inline
  59 | ModRole CDBond where modRole f = {role $= f}
  60 |
  61 | ||| Graph type used for drawing molecules.
  62 | public export
  63 | 0 CDGraph : Type
  64 | CDGraph = Graph CDBond CDAtom
  65 |
  66 | ||| Order-indexed graph type used for drawing molecules.
  67 | public export
  68 | 0 CDIGraph : Nat -> Type
  69 | CDIGraph k = IGraph k CDBond CDAtom
  70 |
  71 | export %inline
  72 | toMolGraph : Graph CDBond CDAtom -> MolGraphAT
  73 | toMolGraph = bimap CDBond.molBond CDAtom.atom
  74 |
  75 | export %inline
  76 | toMolfile : Graph CDBond CDAtom -> MolfileAT
  77 | toMolfile g = MkMolfile "" "created by cyby-draw 1.0" "" (toMolGraph g) []
  78 |
  79 | ||| Initialize a mol-file graph (with perceived atom types) to be used
  80 | ||| in one of the drawing canvases. This includes normalizing the
  81 | ||| molecule to a bond-length of 1.25 Angstrom.
  82 | export
  83 | initGraph : MolGraphAT -> CDGraph
  84 | initGraph (G o g) = G o $ bimap (CB None) (CA None) (normalizeMol g)
  85 |
  86 | ||| Reads and initializes a `CDGraph` from a mol-file string.
  87 | export
  88 | readMolfileE : String -> Either String CDGraph
  89 | readMolfileE mol =
  90 |   case readMol {es = [ParseError MolErr]} mol of
  91 |     Left (Here e)               => Left "\{e}"
  92 |     Right (MkMolfile _ _ _ g _) => Right $ initGraph (perceiveMolAtomTypes g)
  93 |
  94 | ||| Like `readMolfileE` but returns the empty graph in case of a read error.
  95 | export
  96 | readMolfile : String -> CDGraph
  97 | readMolfile = either (const $ G 0 empty) id . readMolfileE
  98 |
  99 | ||| Re-calculates the atom types of a mol graph.
 100 | export
 101 | adjAtomTypes : {k : _} -> CDIGraph k -> CDIGraph k 
 102 | adjAtomTypes =
 103 |   mapWithAdj (\(A (CA r a) ns) => CA r $ calcMolAtomType (map molBond ns) a)
 104 |
 105 | --------------------------------------------------------------------------------
 106 | -- Bond CyCling
 107 | --------------------------------------------------------------------------------
 108 |
 109 | adjStereo : BondStereo -> MolBond -> MolBond
 110 | adjStereo Up   (MkBond fs _ Up)   = MkBond (not fs) Single Up
 111 | adjStereo Down (MkBond fs _ Down) = MkBond (not fs) Single Down
 112 | adjStereo bs   b                  = MkBond True Single bs
 113 |
 114 | nextType : BondOrder -> BondOrder
 115 | nextType Single = Dbl
 116 | nextType Dbl    = Triple
 117 | nextType Triple = Single
 118 |
 119 | export
 120 | newBond : BondOrder -> BondStereo -> MolBond -> MolBond
 121 | newBond Single NoBondStereo b =
 122 |   if b.stereo == NoBondStereo then cast $ nextType b.type else cast Single
 123 | newBond Single s            b = adjStereo s b
 124 | newBond Dbl    _            _ = cast Dbl
 125 | newBond Triple _            _ = cast Triple
 126 |
 127 | --------------------------------------------------------------------------------
 128 | -- Highlighting
 129 | --------------------------------------------------------------------------------
 130 |
 131 | hlAdj : IArray o Bool -> Fin o -> Adj o CDBond CDAtom -> Adj o CDBond CDAtom
 132 | hlAdj arr x (A l ns) =
 133 |   case arr `at` x of
 134 |     False => A l ns
 135 |     True  => A (set Highlight l) (mapKV (setIf Highlight . at arr) ns)
 136 |
 137 | ||| Highlight the nodes corresponding to the given natural numbers.
 138 | export
 139 | highlight : List Nat -> CDGraph -> CDGraph
 140 | highlight ns (G o $ IG gr) =
 141 |   let arr := fromPairs o False (map (,True) ns)
 142 |    in G o $ IG (mapWithIndex (hlAdj arr) gr)
 143 |
 144 | --------------------------------------------------------------------------------
 145 | -- Visibility and Abbreviations
 146 | --------------------------------------------------------------------------------
 147 |
 148 | ||| Returns the number of the abbreviation group of an atom (if any).
 149 | export %inline
 150 | groupNr : CDIGraph k -> Fin k -> Maybe Nat
 151 | groupNr g = map nr . label . atom . lab g
 152 |
 153 | ||| True, if the given node is part of an abbreviation.
 154 | export
 155 | inAbbreviation : CDIGraph k -> Fin k -> Bool
 156 | inAbbreviation g = isJust . groupNr g
 157 |
 158 | ||| Returns the largest group number in the given graph.
 159 | export %inline
 160 | maxGroupNr : {k : _} -> CDIGraph k -> Nat
 161 | maxGroupNr = foldr (\a,n => maybe n (max n . nr) a.atom.label) 0
 162 |
 163 | ||| Returns the group numbers found in a molecule
 164 | export
 165 | groupNrs : {k : _} -> CDIGraph k -> SortedSet Nat
 166 | groupNrs =
 167 |   foldr (\a,ss => maybe ss ((`insert` ss) . nr) a.atom.label) SortedSet.empty
 168 |
 169 | groupMap : SortedSet Nat -> SortedMap Nat Nat -> List Nat -> SortedMap Nat Nat
 170 | groupMap used m []      = m
 171 | groupMap used m (x::xs) =
 172 |  let n := next x in groupMap (insert n used) (insert x n m) xs
 173 |   where
 174 |     next : Nat -> Nat
 175 |     next k = if contains k used then next (assert_smaller k $ S k) else k
 176 |
 177 | ||| Adjusts abbreviation numbers of a template before merging it
 178 | ||| with an existing molecule.
 179 | |||
 180 | ||| This avoids having several abbreviations with the same number
 181 | ||| in the new (merged) molecule, which would then all be expanded at
 182 | ||| the same time with a single double click.
 183 | export
 184 | adjTemplate : {k,m : _} -> CDIGraph k -> CDIGraph m -> CDIGraph m
 185 | adjTemplate c t  =
 186 |   let mp := groupMap (groupNrs c) empty (Prelude.toList $ groupNrs t)
 187 |    in map (adj mp) t
 188 |
 189 |   where
 190 |     nr  : SortedMap Nat Nat -> AtomGroup -> AtomGroup
 191 |     nr mp (G x lbl) = G (fromMaybe x $ lookup x mp) lbl
 192 |
 193 |     adj : SortedMap Nat Nat -> CDAtom -> CDAtom
 194 |     adj mp (CA r a) = CA r $ {label $= map (nr mp)} a
 195 |
 196 | ||| True, if any neighbour of the given node is part of the given
 197 | ||| abbreviation (given as its ID).
 198 | export
 199 | anyNotInGroup : CDIGraph k -> Fin k -> Nat -> Bool
 200 | anyNotInGroup g x n = any (not . inGroup n) (neighbourLabels g x)
 201 |
 202 | ||| Custom label to be displayed for a node (if any).
 203 | export
 204 | customLabel : CDIGraph k -> Fin k -> Maybe String
 205 | customLabel  g x = do
 206 |   G n lbl <- label . atom $ lab g x
 207 |   guard $ lbl /= "" && anyNotInGroup g x n
 208 |   pure lbl
 209 |
 210 | ||| An atom is visible, if a) it is not part of an abbreviation, or b),
 211 | ||| at least one of its neighbours is not part of an abbreviation.
 212 | export
 213 | visible : CDIGraph k -> Fin k -> Bool
 214 | visible g x =
 215 |   case Atom.label . atom $ lab g x of
 216 |     Nothing      => True
 217 |     Just (G n _) => anyNotInGroup g x n
 218 |
 219 | ||| We show an atom's label if a) it is a non-carbon, b) it is an isolate
 220 | ||| carbon (no explicit neighbours), or c) `s.showC` is set to `True`
 221 | export
 222 | labelVisible : (showC : Bool) -> CDIGraph k -> Fin k -> Bool
 223 | labelVisible showC g x =
 224 |   let A (CA _ a) ns  := adj g x
 225 |       MkI e m := a.elem
 226 |    in    showC
 227 |       || null ns
 228 |       || a.type.name == "C.allene"
 229 |       || e /= C 
 230 |       || isJust m
 231 |       || a.charge /= 0
 232 |       || isJust (customLabel g x)
 233 |
 234 | ||| Returns a list of those nodes of a molecule that will be visible
 235 | ||| in the drawing, that is, nodes that are not hidden because
 236 | ||| they are part of an abbreviation.
 237 | export
 238 | visibleNodes : {k : _} -> CDIGraph k -> List (Fin k)
 239 | visibleNodes g = filter (visible g) (nodes g)
 240 |
 241 | ||| Returns the visible nodes that within the given bounds.
 242 | export
 243 | atomsIn : {k : _} -> CDIGraph k -> Bounds2D Mol ->  List (Fin k)
 244 | atomsIn g bs =
 245 |   filter (\n => visible g n && inBounds (point $ lab g n) bs) (nodes g)
 246 |
 247 | nonAbbreviatedNodes : {k : _} -> CDIGraph k -> List (Fin k)
 248 | nonAbbreviatedNodes g = filter (not . inAnyGroup . lab g) (nodes g)
 249 |
 250 | ||| Returns the list of visible neighbours of an atome, that is,
 251 | ||| neighbours that are not hidded because they are part of an
 252 | ||| abbreviation.
 253 | export
 254 | visibleNeighbours : CDIGraph k -> Fin k -> List (Fin k)
 255 | visibleNeighbours g x = filter (visible g) (neighbours g x)
 256 |
 257 | ||| Returns a list of those nodes of a molecule that are hidden
 258 | ||| because they are part of an abbreviation.
 259 | export
 260 | hiddenNodes : {k : _} -> CDIGraph k -> List (Fin k)
 261 | hiddenNodes g = filter (not . visible g) (nodes g)
 262 |
 263 | ||| Returns a list of those edges of a molecule that will be visible
 264 | ||| in the drawing, that is, edges that are not hidden because
 265 | ||| they are part of an abbreviation.
 266 | export
 267 | visibleEdges : {k : _} -> CDIGraph k -> List (Edge k CDBond)
 268 | visibleEdges g = filter (\(E x y _) => visible g x && visible g y) (edges g)
 269 |
 270 | ||| Given a list `ns` of nodes in a molecule, returns a list containing
 271 | ||| also the other nodes belonging to the same abbreviations (if any)
 272 | ||| as the nodes in `ns`.
 273 | ||| transitively via abbreviations.
 274 | export
 275 | groupNodes : {k : _} -> CDIGraph k -> (ns : List (Fin k)) -> List (Fin k)
 276 | groupNodes g ns =
 277 |   let gs@(_::_) := ns >>= toList . groupNr g | Nil => Nil
 278 |    in filter (any (`elem` gs) . groupNr g) (hiddenNodes g)
 279 |
 280 | ||| Given a list `ns` of nodes in a molecule, returns a list containing
 281 | ||| also the other nodes belonging to the same abbreviations (if any)
 282 | ||| as the nodes in `ns`.
 283 | ||| transitively via abbreviations.
 284 | export
 285 | plusGroupNodes : {k : _} -> CDIGraph k -> (ns : List (Fin k)) -> List (Fin k)
 286 | plusGroupNodes g ns = ns ++ groupNodes g ns
 287 |
 288 | --------------------------------------------------------------------------------
 289 | -- Geometry
 290 | --------------------------------------------------------------------------------
 291 |
 292 | ||| Returns the position of the given node in a mol graph.
 293 | export %inline
 294 | pointAt : CDIGraph k -> Fin k -> Point Id
 295 | pointAt g = pointId . lab g
 296 |
 297 | ||| Computes the angles of all visible bonds connecting the given node.
 298 | export
 299 | bondAngles : CDIGraph k -> Fin k -> List Angle
 300 | bondAngles g x =
 301 |   let p  := pointId (lab g x)
 302 |       ns := lab g <$> visibleNeighbours g x
 303 |    in mapMaybe (\k => angle $ pointId k - p) ns
 304 |
 305 | parameters {k : Nat}
 306 |            {auto cd : CoreDims}
 307 |
 308 |   ||| Returns the node closest to the given point,
 309 |   ||| but only if it is closer than the defined atom radius.
 310 |   export
 311 |   closestNodeList : List (Fin k) -> Point Id -> CDIGraph k -> Maybe (Fin k)
 312 |   closestNodeList ks p g = do
 313 |     x <- minBy (distance p . pointId . lab g) ks
 314 |     let q := pointAt g x
 315 |     guard $ near p q cd.radiusAtom
 316 |     pure x
 317 |
 318 |   ||| Finds the visible node closest to the given point, but only
 319 |   ||| if it is closer than the defined atom radius and it fulfills
 320 |   ||| the given predicate.
 321 |   export
 322 |   closestNodeWhere : (Fin k -> Bool) -> Point Id -> CDIGraph k -> Maybe (Fin k)
 323 |   closestNodeWhere pred p g = closestNodeList (filter pred $ nodes g) p g
 324 |
 325 |   ||| Finds the visible node closest to the given point, but only
 326 |   ||| if it is closer than the defined atom radius.
 327 |   export %inline
 328 |   closestNode : Point Id -> CDIGraph k -> Maybe (Fin k)
 329 |   closestNode p g = closestNodeWhere (visible g) p g
 330 |
 331 |   ||| Finds the visible edge closest to the given point, but only
 332 |   ||| if it is closer than the defined atom radius.
 333 |   export
 334 |   closestEdge : Point Id -> CDIGraph k -> Maybe (Edge k CDBond)
 335 |   closestEdge p g = do 
 336 |     ed <- minBy distEdge $ visibleEdges g
 337 |     guard $ distEdge ed <= cd.radiusAtom
 338 |     pure ed
 339 |
 340 |     where
 341 |       distEdge : Edge k n -> Double
 342 |       distEdge (E x y _) = distanceToLineSegment p (pointAt g x) (pointAt g y)
 343 |
 344 |   ||| Returns the item (node or edge) closest to the current mouse position.
 345 |   export
 346 |   closestItem : Point Id -> CDIGraph k -> NOE (Fin k) (Edge k CDBond)
 347 |   closestItem p g = noe (closestNode p g) (closestEdge p g)
 348 |
 349 |   ||| Generously approximates the bounds of an atom in the drawing.
 350 |   export
 351 |   approxBounds : CDIGraph k -> Fin k -> Bounds2D Id
 352 |   approxBounds g x =
 353 |     case visible g x of
 354 |       False => neutral
 355 |       True  => case group $ lab g x of
 356 |         Nothing      =>
 357 |           let r := 2 * cd.radiusAtom
 358 |               P x y := pointAt g x
 359 |            in BS (range (x-r) (x+r)) (range (y-r) (y+r))
 360 |         Just (G _ l) =>
 361 |           let w := cast {to = Double} (length l * cd.fontSize)
 362 |               h := cast {to = Double} cd.fontSize
 363 |               P x y := pointAt g x
 364 |            in BS (range (x-w) (x+w)) (range (y-h) (y+h))
 365 |
 366 | --------------------------------------------------------------------------------
 367 | --          Hovering
 368 | --------------------------------------------------------------------------------
 369 |
 370 | ||| Removes all roles from atoms and bonds in the given graph.
 371 | export %inline
 372 | clear : CDGraph -> CDGraph
 373 | clear = bimap clear clear
 374 |
 375 | ||| Unset all roles with the exception of `Hover` and `Selected`
 376 | export %inline
 377 | cleanup : CDGraph -> CDGraph
 378 | cleanup = bimap (keep Persistent) (keep Persistent)
 379 |
 380 | %inline
 381 | unHover : {k : _} -> CDIGraph k -> CDIGraph k
 382 | unHover = bimap (unset Hover) (unset Hover)
 383 |
 384 | hoverE : Fin k -> Fin k -> Fin k -> Adj k CDBond CDAtom -> Adj k CDBond CDAtom
 385 | hoverE x y z (A a ns) =
 386 |   if x == z || y == z
 387 |      then A a $ mapKV (\w => setIf Hover (w == x || w == y)) ns
 388 |      else A a ns
 389 |
 390 | ||| Adjusts the `Hovering` flag of all atoms and edges in the molecule.
 391 | ||| The visible atom closest to the given point is set to
 392 | ||| `Hover` if it is not further away than `radiusAtom`.
 393 | |||
 394 | ||| Otherwise, the visible edge closest to the given point is set to
 395 | ||| `Hovering` if it is not further away than `radiusAtom`.
 396 | |||
 397 | ||| If the atom, over which the mouse hovers is part of an abbreviation,
 398 | ||| all other atoms in the abbreviations will be set to `Hovering` as well.
 399 | |||
 400 | ||| The `hatom` predicate is used to figure out if we can currently hover over
 401 | ||| a given atom.
 402 | |||
 403 | ||| The `hbond` predicate is used to figure out if we can currently hover over
 404 | ||| a given bond (its bool argument should be `True`, if the bond is connected
 405 | ||| to at least one atom in an abbreviation group)
 406 | export
 407 | hover :
 408 |      {k : _}
 409 |   -> {auto cd : CoreDims}
 410 |   -> (hbond : CDBond -> Bool-> Bool)
 411 |   -> (hatom : CDAtom -> Bool)
 412 |   -> Point Id
 413 |   -> CDIGraph k
 414 |   -> CDIGraph k
 415 | hover hbond hatom p g0 =
 416 |   let g := unHover g0
 417 |    in case closestItem p g of
 418 |         N n         =>
 419 |           if hatom (lab g n)
 420 |              then mapWithCtxt (\x,(A a _) => setIf Hover (x == n) a) g
 421 |              else g
 422 |         E (E x y b) =>
 423 |           if hbond b (inAnyGroup (lab g x) || inAnyGroup (lab g y))
 424 |              then mapCtxt (hoverE x y) g
 425 |              else g
 426 |         None        => g
 427 |
 428 | ||| Adds the given role to the currently hovered atoms
 429 | export %inline
 430 | ifHover : Role -> CDGraph -> CDGraph
 431 | ifHover r = map (\x => setIf r (is Hover x) x)
 432 |
 433 | ||| Replaces a given old Role with a given new Role.
 434 | ||| For all other nodes, it removes the new Role.
 435 | |||
 436 | ||| For example:
 437 | ||| - If 'New' is currently set, it removes 'New' and sets 'Hover'.
 438 | ||| - If 'Hover' is set, it removes 'Hover'.
 439 | export
 440 | replaceWith : Cast a Role => ModRole a => Role -> Role -> a -> a
 441 | replaceWith old new a =
 442 |   if      is old a then unset old (set new a)
 443 |   else if is new a then unset new a
 444 |   else a
 445 |
 446 | ||| 'New' is replaced with 'Hover', and any existing 'Hover' roles are removed.
 447 | export
 448 | hoverIfNew : CDGraph -> CDGraph
 449 | hoverIfNew = map (New `replaceWith` Hover)
 450 |
 451 | ||| Returns the currently hovered edges or atoms atoms
 452 | export %inline
 453 | hoveredItem : {k : _} -> CDIGraph k -> NOE (Fin k, CDAtom) (Edge k CDBond)
 454 | hoveredItem g =
 455 |   eon (find (is Hover . snd) (labNodes g)) (find (is Hover . label) (edges g))
 456 |
 457 | ||| Selects the currently hovered atoms and bonds.
 458 | |||
 459 | ||| The `SelectMode` flags indicate, if currently selected items should
 460 | ||| be kept or not, or if no item should be selected at all. The first
 461 | ||| value is used for edge selection and the second for node selection.
 462 | export %inline
 463 | selectHovered : SelectMode -> SelectMode -> CDGraph -> CDGraph
 464 | selectHovered em nm = bimap (selectIfHovered em) (selectIfHovered nm)
 465 |
 466 | --------------------------------------------------------------------------------
 467 | --          Selecting Nodes
 468 | --------------------------------------------------------------------------------
 469 |
 470 | public export
 471 | record SelectZones where
 472 |   constructor SZ
 473 |   dragUL : Point Id -- upper left corner for dragging
 474 |   dragLR : Point Id -- lower right corner for dragging
 475 |   rotUL  : Point Id -- upper left corner for rotating
 476 |   rotLR  : Point Id -- lower right corner for rotating
 477 |
 478 | ||| Selects all nodes that are a) currently being hovered over, or visible
 479 | ||| and in the given rectangle.
 480 | export
 481 | select : (start,end : Point Id) -> CDGraph -> CDGraph
 482 | select s e (G o g) = G o $ mapWithCtxt sel g
 483 |   where
 484 |     sel : Fin o -> Adj o CDBond CDAtom -> CDAtom
 485 |     sel n (A a _) =
 486 |       let p := pointId a
 487 |        in setIf Selected (is Hover a || (visible g n && inRectangle p s e)) a
 488 |
 489 | ||| Returns `True` if the given node is currently selected.
 490 | |||
 491 | ||| In case the `includeEdges` flag is set to `True`, this will also
 492 | ||| return `True` if one of the edges connecting the node is currently
 493 | ||| selected.
 494 | export
 495 | isSelected : CDIGraph k -> (includeEdges : Bool) -> Fin k -> Bool
 496 | isSelected g include n =
 497 |   let A a bs := adj g n
 498 |    in is Selected a || (include && any (is Selected) bs)
 499 |
 500 | ||| The list of currently selected nodes.
 501 | export
 502 | selectedNodes : {k : _} -> CDIGraph k -> (includeEdges : Bool) -> List (Fin k)
 503 | selectedNodes g include = filter (isSelected g include) (nodes g)
 504 |
 505 | ||| The list of currently selected edges.
 506 | export
 507 | selectedEdges : {k : _} -> CDIGraph k -> List (Fin k, Fin k)
 508 | selectedEdges =
 509 |   mapMaybe (\(E x y b) => if is Selected b then Just (x,y) else Nothing) . edges
 510 |
 511 | export
 512 | selectedItems : {k : _} -> CDIGraph k -> NOE (List $ Fin k) (List (Fin k, Fin k))
 513 | selectedItems g = eons (selectedNodes g False) (selectedEdges g)
 514 |
 515 | nodeBounds : CDIGraph k -> Fin k -> Bounds2D Mol
 516 | nodeBounds g = bounds . lab g
 517 |
 518 | edgeBounds : CDIGraph k -> (Fin k,Fin k) -> Bounds2D Mol
 519 | edgeBounds g (x,y) = bounds (lab g x) <+> bounds (lab g y)
 520 |
 521 | ||| Computes the top left and bottom right corner of the bounding box
 522 | ||| containing the currently selected atoms (if any)
 523 | export
 524 | selectionCorners : CDGraph -> Maybe (Point Mol, Point Mol)
 525 | selectionCorners (G o g) =
 526 |   case selectedItems g of
 527 |     N ns@(_ :: _ :: _) => corners $ foldMap (nodeBounds g) ns
 528 |     E ps               => corners $ foldMap (edgeBounds g) ps
 529 |     _                  => Nothing
 530 |
 531 | ||| Checks, if there is enough space to grab the box in the canvas.
 532 | export
 533 | selectZones : (s : CoreDims) => (p1,p2 : Point Id) -> SelectZones
 534 | selectZones p1 p2 =
 535 |   let b  := s.selectBufferSize
 536 |       bs := bounds p1 <+> bounds p2
 537 |
 538 |       -- translation vector for the two corners of the inner buffer
 539 |       -- this is zero if the bounding box formed by `p1` and `p2` is
 540 |       -- already large enough, otherwise both dimensions are expanded as
 541 |       -- needed
 542 |       vd := scale 0.5 $ V (max 0 (b - width bs)) (max 0 (b - height bs))
 543 |       vr := vid b b
 544 |       d1 := translate (negate vd) p1
 545 |       d2 := translate vd p2
 546 |    in SZ d1 d2 (translate (negate vr) d1) (translate vr d2)
 547 |
 548 | --------------------------------------------------------------------------------
 549 | --          Editing Molecules
 550 | --------------------------------------------------------------------------------
 551 |
 552 | ||| Creates an uncharged atom at the given position and with the given role.
 553 | export
 554 | isotopeAt : Isotope -> Point Id -> Role -> CDAtom
 555 | isotopeAt i p r =
 556 |   let pos := toCoords (convert p) [0,0,0]
 557 |    in CA r (MkAtom i 0 pos NoRadical 0 unknown () Nothing)
 558 |
 559 | ||| Creates an uncharged atom at the given position and with the given role.
 560 | export %inline
 561 | elemAt : Elem -> Point Id -> Role -> CDAtom
 562 | elemAt = isotopeAt . cast
 563 |
 564 | ||| Inserts an uncharged atom at the given position and with the given
 565 | ||| role.
 566 | export %inline
 567 | insIsotopeAt :
 568 |      {k : _}
 569 |   -> CDIGraph k
 570 |   -> Isotope
 571 |   -> Point Id
 572 |   -> Role
 573 |   -> CDIGraph (S k)
 574 | insIsotopeAt g i p r = insNode g (isotopeAt i p r)
 575 |
 576 | ||| Inserts an uncharged atom at the given position and with the given
 577 | ||| role.
 578 | export %inline
 579 | insElemAt : {k : _} -> CDIGraph k -> Elem -> Point Id -> Role -> CDIGraph (S k)
 580 | insElemAt g = insIsotopeAt g . cast
 581 |
 582 | ||| Computes the preferred angle for a new bond based on the bond type
 583 | ||| and angles to already existing bonds.
 584 | export
 585 | preferredAngle : (isLinear : Bool) -> List Angle -> Angle
 586 | preferredAngle _     []  = (negate 1.0 / 6.0) * pi
 587 | preferredAngle True  [x] = x + pi
 588 | preferredAngle False [x] =
 589 |   if (x >= zero && x <= halfPi) || (x >= pi && x <= threeHalfPi)
 590 |      then x + twoThirdPi
 591 |      else x - twoThirdPi
 592 | preferredAngle _     xs  = largestBisector xs
 593 |
 594 | -- Checks if the geometry at an atom is linear.
 595 | isLinear : List BondOrder -> Bool
 596 | isLinear [Triple,_] = True
 597 | isLinear [_,Triple] = True
 598 | isLinear [Dbl,Dbl]  = True
 599 | isLinear _          = False
 600 |
 601 | ||| Preferred position for a new atom bound to an existing one based on the
 602 | ||| largest bisector of angles between existing bonds
 603 | export
 604 | bestPos : CDIGraph k -> MolBond -> Fin k -> Point Id -> Point Id
 605 | bestPos g b n p =
 606 |   let orders   := b.type :: map (type . molBond) (edgeLabels g n)
 607 |       newAngle := preferredAngle (isLinear orders) (bondAngles g n)
 608 |    in translate (polar BondLengthInPixels newAngle) p
 609 |
 610 | ||| Equally spaced sequence of `s.angleSteps` angles from 0 until 2pi.
 611 | export
 612 | stepAngles : (s : CoreDims) => List Angle
 613 | stepAngles =
 614 |   let step = angle (TwoPi / cast s.angleSteps)
 615 |    in map (\x => cast x * step) [0.. pred s.angleSteps]
 616 |
 617 | ||| Suggested position for a new atom based on the current mouse position.
 618 | ||| The boolean flag indicates if "Shift" is currently down, in which case
 619 | ||| we just return the current point.
 620 | export
 621 | suggestedPos : CoreDims => Bool -> (atom, current : Point Id) -> Point Id
 622 | suggestedPos True pa pc = pc
 623 | suggestedPos False pa pc =
 624 |   let Just mouseAngle := angle (pc - pa) | Nothing => pc
 625 |       Just bondAngle  := closestAngle mouseAngle stepAngles | Nothing => pc
 626 |    in translate (polar BondLengthInPixels bondAngle) pa
 627 |
 628 | ||| Draws a new bond from the given node to the suggested position.
 629 | ||| If another atom is already close to the current mouse position
 630 | ||| or the suggested position, connect the two atoms instead.
 631 | export
 632 | bondTo :
 633 |      {k : _}
 634 |   -> {auto s : CoreDims}
 635 |   -> CDBond
 636 |   -> Fin k
 637 |   -> (current, suggested : Point Id)
 638 |   -> CDIGraph k
 639 |   -> Either (CDIGraph k) (CDIGraph $ S k)
 640 | bondTo b n pc ps g =
 641 |   maybe
 642 |     (Right $ insEdge (edge n b) (insElemAt g C ps New))
 643 |     (\e => Left $ insEdge e g)
 644 |     (closeEdge pc <|> closeEdge ps)
 645 |   where
 646 |     closeEdge : Point Id -> Maybe (Edge k CDBond)
 647 |     closeEdge p = closestNode p g >>= \k => mkEdge n k b
 648 |
 649 | ||| From two graphs, returns pairs of visible nodes closest
 650 | ||| to each other (but no farther apart than `s.radiusAtom`).
 651 | export
 652 | nodesToMerge :
 653 |      {k,m : _}
 654 |   -> {auto s : CoreDims}
 655 |   -> CDIGraph k
 656 |   -> CDIGraph m
 657 |   -> List (Fin k, Fin m)
 658 | nodesToMerge g t =
 659 |  let area := overlap {margin = s.radiusAtom }(bounds g) (Geom.Bounds.bounds t)
 660 |      ng   := g `atomsIn` area
 661 |      nt   := t `atomsIn` area
 662 |   in mapMaybe (\x => (x,) <$> closestNodeList nt (pointAt g x) t) ng
 663 |
 664 | -- Offset between origin atom and template atoms as a vector in `Mol` space.
 665 | offset : CDIGraph k -> CDIGraph m -> List (Fin k, Fin m) -> Vector (transform Mol)
 666 | offset _  _  []             = vzero
 667 | offset g1 g2 ((n1,n2) :: _) = point (lab g1 n1) - point (lab g2 n2)
 668 |
 669 | -- create new bonds between the merging template atoms and the corresponding
 670 | -- neighbours of the original molecule
 671 | newEdges :
 672 |      {k,m : _}
 673 |   -> CDIGraph m
 674 |   -> List (Fin k, Fin m)
 675 |   -> List (Fin k, Fin m, CDBond)
 676 | newEdges t ps = do
 677 |   (a1,a2) <- ps
 678 |   (\(x,l) => (a1,x,l)) <$> neighboursAsPairs t a2
 679 |
 680 | inBond : {m : _} -> (n1,n2 : Fin m) -> Edge m CDBond -> Bool
 681 | inBond n1 n2 bnd =
 682 |   (n1 == bnd.node1 && n2 == bnd.node2) || (n2 == bnd.node1 && n1 == bnd.node2)
 683 |
 684 | -- if there is a template bond which would be deleted due to the node merging,
 685 | -- create the bond between the corresponding origin molecule nodes
 686 | createStableBond :
 687 |      {k,m : _}
 688 |   -> List (Fin k, Fin m)
 689 |   -> List (Edge k CDBond)
 690 |   -> Edge m CDBond
 691 |   -> Maybe $ Edge k CDBond
 692 | createStableBond nsToMerge bondsG bnd =
 693 |   case filter (\(_,nt) => nt == bnd.node1 || nt == bnd.node2) nsToMerge of
 694 |     [(nm1,_),(nm2,_)] =>
 695 |       if any (inBond nm1 nm2) bondsG
 696 |          then Nothing
 697 |          else mkEdge nm1 nm2 $ CB None ({type := Single} bnd.label.molBond)
 698 |     _                  => Nothing
 699 |
 700 | incNode : {m : _} -> (k : Nat) -> Fin m -> Maybe (Fin $ k + m)
 701 | incNode k x = tryNatToFin (k + finToNat x)
 702 |
 703 | ||| After moving or rotating the selected nodes in a graph,
 704 | ||| check for pairs of close nodes and merge them.
 705 | export
 706 | mergeCloseNodes : CoreDims => {k:_} -> CDIGraph k -> CDGraph
 707 | mergeCloseNodes g =
 708 |   let ns        := filter (not . inAbbreviation g) (selectedNodes g True)
 709 |       lMergeN   := mapMaybe closestPair ns
 710 |       offset    := negate $ offset g g lMergeN
 711 |       lnewBonds := mapMaybe (\(x,y,l) => mkEdge x y l) (newEdges g lMergeN)
 712 |       mol'      := insEdges lnewBonds $ mapIf doAdjust (translate offset) g
 713 |    in delNodes (map snd lMergeN) mol'
 714 |
 715 |   where
 716 |     canSelfMerge : Fin k -> Bool
 717 |     canSelfMerge x =
 718 |       let a := lab g x in not (isSelected g True x || inAnyGroup a)
 719 |
 720 |     doAdjust : CDAtom -> Bool
 721 |     doAdjust a = is Selected a
 722 |
 723 |     closestPair : Fin k -> Maybe (Fin k, Fin k)
 724 |     closestPair x = (x,) <$> closestNodeWhere canSelfMerge (pointAt g x) g
 725 |
 726 | ||| Merges two graphs and merges atoms, which are overlapping.
 727 | ||| If bonds should be added between the two graphs, the last argument
 728 | ||| allows a list of the according nodes and its bond type.
 729 | export
 730 | mergeGraphs' :
 731 |      {auto _ : CoreDims}
 732 |   -> {k,m:_}
 733 |   -> CDIGraph k
 734 |   -> CDIGraph m
 735 |   -> List (Fin k, Fin m, CDBond)
 736 |   -> CDGraph
 737 | mergeGraphs' g t bs =
 738 |   let t'        := adjTemplate g t
 739 |       lMergeN   := nodesToMerge g t'
 740 |       offset    := offset g t' lMergeN
 741 |       lnewBonds := newEdges t' lMergeN ++ bs
 742 |       mergeEsG  := lMergeN >>= edgesTo g . fst
 743 |       mergeEsT  := lMergeN >>= edgesTo t' . snd
 744 |       -- replaces bonds between merging (and therefore to be deleted) template
 745 |       -- atoms and origin atoms
 746 |       -- this is only done if there isn't already a bond from the origin
 747 |       -- molecule present at the same position
 748 |       mol'      :=
 749 |         insEdges (mapMaybe (createStableBond lMergeN mergeEsG) mergeEsT) g
 750 |       mol''     := mergeGraphsWithEdges mol' (translate offset t') lnewBonds
 751 |    in delNodes (mapMaybe (incNode k . snd) lMergeN) mol''
 752 |
 753 | -- This connects a template to a graph by connecting the template's
 754 | -- zero node via a single bond with the given node of the current molecule.
 755 | -- The template is rotated and translated in such a way that we get
 756 | -- preferrable bond angles both at the current graph and the template.
 757 | mergeGraphsOnAtom : CoreDims => {k,m : _} -> Fin k -> CDIGraph k -> CDIGraph m -> CDGraph
 758 | mergeGraphsOnAtom {m = 0}   _ g _ = G _ g
 759 | mergeGraphsOnAtom {m = S _} n g t =
 760 |   let as     := bondAngles g n
 761 |       an     := preferredAngle False as
 762 |       a0     := preferredAngle False (bondAngles t 0)
 763 |       tr     := rotate (an - a0 + pi) t
 764 |       offset := point (lab g n) - point (lab tr 0)
 765 |       v      := polar BondLengthInAngstrom an + offset
 766 |       bond   := CB New $ cast Single
 767 |    in mergeGraphs' g (translate v tr) [(n,0,bond)]
 768 |
 769 | -- This connects a template to a graph by replacing the template's
 770 | -- smallest edge given edge of the current molecule.
 771 | -- The template is rotated and translated in such a way that the two
 772 | -- edges are properly aligned.
 773 | --
 774 | -- There are two ways to align the bonds of template and
 775 | -- molecule, so we try both and keep the one with its
 776 | -- center closer to the mouse position. This allows us to
 777 | -- flip between the two placements by moving the mouse
 778 | -- from one side of a bond to the other.
 779 | mergeGraphsOnBond :
 780 |      {k,m : _}
 781 |   -> {auto cd : CoreDims}
 782 |   -> Point Mol
 783 |   -> Edge k CDBond
 784 |   -> CDIGraph k
 785 |   -> CDIGraph m
 786 |   -> CDGraph
 787 | mergeGraphsOnBond p (E n1 n2 _) g t =
 788 |   case edges t of
 789 |     []      => G k g
 790 |     E n3 n4 _ :: _ =>
 791 |       let Just ag := angle (pointAt g n1 - pointAt g n2) | Nothing => G k g
 792 |           Just at := angle (pointAt t n3 - pointAt t n4) | Nothing => G k g
 793 |           tr1     := rotate (ag - at) t
 794 |           tt1     := translate (point (lab g n1) - point (lab tr1 n3)) tr1
 795 |           tr2     := rotate (ag - at + pi) t
 796 |           tt2     := translate (point (lab g n1) - point (lab tr2 n4)) tr2
 797 |        in if distance p (center tt1) <= distance p (center tt2)
 798 |              then mergeGraphs' g tt1 []
 799 |              else mergeGraphs' g tt2 []
 800 |
 801 | ||| Add the template to the existing graph depending on clicking on an atom,
 802 | ||| a bond or elsewhere on the canvas.
 803 | export
 804 | mergeGraphs : CoreDims => Point Id -> (g, t : CDGraph) -> CDGraph
 805 | mergeGraphs c (G o1 g) (G o2 t) =
 806 |   case hoveredItem g of
 807 |     N k  => mergeGraphsOnAtom (fst k) g t
 808 |     E e  => mergeGraphsOnBond (convert c) e g t
 809 |     None => mergeGraphs' g t []
 810 |
 811 | -- Rotating of the template around the clicked atom. The template will
 812 | -- be connected to this atom by a new bond.
 813 | -- First the template is placed in such a way that preferrable bond angles are
 814 | -- created. Then, if the mouse cursor is moving away (min 1/4 of a std-bond
 815 | -- length) from the clicked atom, the template and its connecting bond rotat
 816 | -- around the clicked atom of the original molecule.
 817 | rotTemplOnAtom :
 818 |      {auto _ : CoreDims}
 819 |   -> {s,k,m : _}
 820 |   -> (cursor : Point s)
 821 |   -> (clickedAtom : Fin k)
 822 |   -> (mol : CDIGraph k)
 823 |   -> (templ : CDIGraph m)
 824 |   -> CDIGraph m
 825 | rotTemplOnAtom {m = 0}   _ _ _ t = t
 826 | rotTemplOnAtom {m = S _} p n g t =
 827 |   let pMol    := point $ lab g n
 828 |       a0      := preferredAngle False (bondAngles t 0)
 829 |       an      := preferredAngle False (bondAngles g n)
 830 |       angle   := an - a0 + pi
 831 |       tr      := rotate angle t
 832 |       offset  := pMol - point (lab tr 0)
 833 |       v       := polar BondLengthInAngstrom an + offset
 834 |       tr'     := translate v tr
 835 |       Just aT := Vector.angle (pMol - center tr') | Nothing => tr'
 836 |       Just aM := Vector.angle (pMol - convert p)  | Nothing => tr'
 837 |       aSteps  := fromMaybe (aM - aT) (closestAngle (aM - aT) stepAngles)
 838 |    in if distance (convert p) (point (lab g n)) <
 839 |          value (BondLengthInAngstrom / 4)
 840 |          then tr'
 841 |          else rotateAt pMol aSteps tr'
 842 |
 843 | -- If one atom of the template and another atom of the origin molecule overlap,
 844 | -- rotate the template around the overlapping point in accordance of the cursor
 845 | -- movement.
 846 | -- Here, rotating the template is only allowed in defined step angles.
 847 | -- To avoid unwanted rotation, the cursor has to be moved a minimum of
 848 | -- 1/4 of a standard bond length from its initial position (left mouse
 849 | -- button click).
 850 | rotTemplAroundOverlap :
 851 |      {auto _ : CoreDims}
 852 |   -> {s,k,m : _}
 853 |   -> (initCursor,cursor : Point s)
 854 |   -> (aMol : Fin k)
 855 |   -> (aTempl : Fin m)
 856 |   -> (mol : CDIGraph k)
 857 |   -> (templ : CDIGraph m)
 858 |   -> CDGraph
 859 | rotTemplAroundOverlap ip p nm nt g t =
 860 |   let pMol    := point $ lab g nm
 861 |       pTemA   := point $ lab t nt
 862 |       offset  := pMol - pTemA
 863 |       t'      := translate offset t
 864 |       Just a0 := angle (pMol - center t') | Nothing => G _ t'
 865 |       Just aM := angle (pMol - convert p) | Nothing => G _ t'
 866 |       angle   := aM - a0
 867 |       aSteps  := fromMaybe angle (closestAngle angle stepAngles)
 868 |    in if distance ip p > value (BondLengthInAngstrom / 4)
 869 |          then G _ $ rotateAt pMol aSteps t'
 870 |          else G _ t'
 871 |
 872 | ||| Attaches an atom to a mol graph.
 873 | |||
 874 | ||| Depending on the current mouse position and whether "Shift" is
 875 | ||| pressed or not, the drawing tool suggest a different bond length
 876 | ||| and angle for the new bond.
 877 | export
 878 | addBondE :
 879 |      {k : _}
 880 |   -> {t : _}
 881 |   -> {auto cd   : CoreDims}
 882 |   -> (shiftDown : Bool)
 883 |   -> (mousePos  : Maybe $ Point t)
 884 |   -> (newBond   : MolBond)
 885 |   -> CDIGraph k
 886 |   -> Either (CDIGraph k) (CDIGraph $ S k)
 887 | addBondE @{cd} shiftDown p mb g =
 888 |   let b          := CB New mb -- new `CDBond`
 889 |       Just (n,a) := find (is Origin . snd) (labNodes g) | Nothing => Left g
 890 |       pa         := pointId a -- position of atom we attach new bond to
 891 |       pc         := maybe pa pointId p -- current mouse position
 892 |       
 893 |    in if near pa pc cd.radiusAtom
 894 |          -- if mouse is close to origin atom
 895 |          -- use largest bisector as new bond angle and draw a bond
 896 |          -- of preferred length
 897 |          then bondTo b n pc (bestPos g mb n pa) g
 898 |   
 899 |          -- else use an angle close to the one of the vector connecting
 900 |          -- the origin atom and the mouse pointer, unless "Shift" is down,
 901 |          -- in which case we use the mouse position without modification
 902 |          else bondTo b n pc (suggestedPos shiftDown pa pc) g
 903 |
 904 | ||| Attaches an atom to a mol graph.
 905 | |||
 906 | ||| Depending on the current mouse position and whether "Shift" is
 907 | ||| pressed or not, the drawing tool suggest a different bond length
 908 | ||| and angle for the new bond.
 909 | export
 910 | addBond :
 911 |      {k : _}
 912 |   -> {t : _}
 913 |   -> {auto cd   : CoreDims}
 914 |   -> (shiftDown : Bool)
 915 |   -> (mousePos  : Maybe $ Point t)
 916 |   -> (newBond   : MolBond)
 917 |   -> CDIGraph k
 918 |   -> CDGraph
 919 | addBond sd p mb g = either (G k) (G $ S k) (addBondE sd p mb g)
 920 |
 921 | ||| Adds an uncharged atom of the given isotope at the given position.
 922 | |||
 923 | ||| This either replaces the currently hovered atom, or it inserts a new
 924 | ||| isolate atom.
 925 | export
 926 | addAtom :
 927 |      {auto cd : CoreDims}
 928 |   -> {t : _}
 929 |   -> {k : _}
 930 |   -> CDIGraph k
 931 |   -> Isotope
 932 |   -> Point t
 933 |   -> CDGraph
 934 | addAtom g i p =
 935 |   case hoveredItem g of
 936 |     N (x,_) => 
 937 |       let g2 := setNode x (isotopeAt i (pointAt g x) New) g
 938 |        in delNodes (groupNodes g [x]) g2
 939 |     _       => case closestNode (convert p) g of
 940 |       Nothing => G _ $ insIsotopeAt g i (convert p) New
 941 |
 942 |       -- in the `Just` case, we have a node close to the mouse pointer
 943 |       -- that is not set to "hovered". This means, replacing that node is
 944 |       -- a non-op. We don't want to insert another atom on top of it, so
 945 |       -- we return the graph unmodified.
 946 |       Just _  => G _ g
 947 |
 948 | export
 949 | setAbbreviationAt :
 950 |      {auto cd : CoreDims}
 951 |   -> {k,m        : Nat}
 952 |   -> (lbl        : String)
 953 |   -> (node,neigh : Fin k)
 954 |   -> (abbr       : CDIGraph (S m))
 955 |   -> (mol        : CDIGraph k)
 956 |   -> CDGraph
 957 | setAbbreviationAt lbl n1 n2 a g =
 958 |   let nr      := S (maxGroupNr g)
 959 |       pn      := point (lab g n1) -- position of label
 960 |       pc      := point (lab g n2) -- node to which label is connected
 961 |       Just an := angle (pn - pc) | Nothing => G k g
 962 |       a0      := preferredAngle False (bondAngles a 0)
 963 |       ar      := rotate (an-a0+pi) (setGroup (G nr lbl) <$> a)
 964 |       at      := translate (pn - point (lab ar 0)) ar
 965 |       bond    := CB New $ maybe (cast Single) molBond (elab g n1 n2)
 966 |       gm      := mergeGraphsWithEdges g at [(n2,0,bond)]
 967 |    in delNodes (plusGroupNodes gm [weakenN _ n1]) gm
 968 |
 969 | ||| Replaces the atom or abbreviation at the given position with
 970 | ||| an abbreviation.
 971 | export
 972 | setAbbreviation :
 973 |      {auto cd : CoreDims}
 974 |   -> (shiftDown : Bool)
 975 |   -> (lbl       : String)
 976 |   -> (mouse     : Point Id)
 977 |   -> (abbr      : CDGraph)
 978 |   -> (mol       : CDGraph)
 979 |   -> CDGraph
 980 | setAbbreviation _  lbl mouse (G 0 _) g = g
 981 | setAbbreviation sd lbl mouse (G (S m) a) (G k g) =
 982 |   case find (is Origin . snd) (labNodes g) of
 983 |     Nothing     => G k g
 984 |     Just (n1,_) => case visibleNeighbours g n1 of
 985 |       [n2] => setAbbreviationAt lbl n1 n2 a g
 986 |       _    => case addBondE sd (Just mouse) (cast Single) g of
 987 |         Left _   => G k g
 988 |         Right g2 => setAbbreviationAt lbl last (weaken n1) a g2
 989 |
 990 | ||| Expands the currently hovered-over abbreviation (if any)
 991 | export
 992 | expand : CDGraph -> CDGraph
 993 | expand (G o g) =
 994 |   let N (n1,CA _ a) := hoveredItem g | _ => G o g
 995 |       Just (G n _)  := label a       | _ => G o g
 996 |    in G o $ map (clearGroup n) g
 997 |
 998 | %inline new : CDBond -> CDBond
 999 | new = {role := New}
1000 |
1001 | %inline translateTemplateAtom : Vector (transform Mol) -> CDAtom -> CDAtom
1002 | translateTemplateAtom v (CA _ a) = CA None $ translate v a
1003 |
1004 | export
1005 | ||| Translating the template to the mouse position and setting the roles to new.
1006 | translateTemplate :
1007 |      {auto _ : CoreDims}
1008 |   -> {s : _}
1009 |   -> Point s
1010 |   -> (t : CDGraph)
1011 |   -> CDGraph
1012 | translateTemplate p t =
1013 |   let v := convert p - center t in bimap new (translateTemplateAtom v) t
1014 |
1015 | ||| Merges a template graph with the current mol graph based on the
1016 | ||| current mouse position.
1017 | export
1018 | addTemplate : CoreDims => {s : _} -> Point s -> (t, mol : CDGraph) -> CDGraph
1019 | addTemplate p t mol =
1020 |   let v := convert p - center t
1021 |    in mergeGraphs (convert p) mol (bimap new (translateTemplateAtom v) t)
1022 |
1023 | ||| Merges a template graph with the current mol graph based on the
1024 | ||| current mouse position. If an atom is clicked on, the template
1025 | ||| can be rotated around it by the mouse cursor.
1026 | ||| If exactly two atoms (molecule and template) overlap, the template
1027 | ||| can be rotated around the overlapping atoms by holding down the
1028 | ||| the mouse button and moving the cursor.
1029 | export
1030 | addTemplateRot :
1031 |      {auto _ : CoreDims}
1032 |   -> {s,k,n : _}
1033 |   -> (cursor : Point s)
1034 |   -> Either (Fin n) (Fin n, Fin k, Point s)
1035 |   -> (templ : CDIGraph k)
1036 |   -> (mol : CDIGraph n)
1037 |   -> CDGraph
1038 | addTemplateRot {n = S j,k = S i} p (Left f) t m =
1039 |   let rotTemp := rotTemplOnAtom p f m t
1040 |       bnd     := CB New $ cast Single
1041 |    in mergeGraphs' m rotTemp [(f,0,bnd)]
1042 | addTemplateRot {n = S j,k = S i} p (Right (fm,ft,ip)) t m =
1043 |   let (G _ rotTemp) := rotTemplAroundOverlap ip p fm ft m t
1044 |    in mergeGraphs' m rotTemp []
1045 | addTemplateRot _ _ _ m = G _ m
1046 |
1047 | ||| Remove the abbreviation labels from orphaned abbreviation atoms.
1048 | ||| The list of nodes have had an edge remove and now belong to potentially
1049 | ||| orphaned abbreviation groups.
1050 | export
1051 | clearOrphanGroups : {k : _} -> List (Fin k) -> CDIGraph k -> CDIGraph k 
1052 | clearOrphanGroups ns g =
1053 |   let gs@(_::_) := ns >>= toList . groupNr g | Nil => g
1054 |    in map (\x => foldl (flip clearGroup) x gs) g
1055 |
1056 | ||| Deletes all selected nodes or edge from the graph
1057 | ||| (including nodes selected transitively via abbreviations).
1058 | |||
1059 | ||| When we delete an edge, we are at risk of creating an orphaned
1060 | ||| abbreviation: An invisible set of abbreviated nodes no longer
1061 | ||| connected to the visible part of the molecule. In such a case,
1062 | ||| we could either delete the whole abbreviation, or make the
1063 | ||| orphaned nodes visible. Here, we opt for the latter. If users
1064 | ||| want to delete the whole abbreviation, they can do so by
1065 | ||| deleting the atom in question.
1066 | |||
1067 | ||| If no nodes or edges are currently selected, this deletes the
1068 | ||| node or edge that is currently being hovered over.
1069 | export
1070 | deleteSelected : CDGraph -> CDGraph
1071 | deleteSelected (G o g) =
1072 |   case selectedItems g of
1073 |     None => case hoveredItem g of
1074 |       None => G o g
1075 |       N x  => delNS [fst x]
1076 |       E x  => delES [(node1 x, node2 x)]
1077 |     N ns => delNS ns
1078 |     E es => delES es
1079 |
1080 |   where
1081 |     delNS : List (Fin o) -> CDGraph
1082 |     delNS ns = delNodes (plusGroupNodes g ns) g
1083 |
1084 |     delES : List (Fin o,Fin o) -> CDGraph
1085 |     delES es =
1086 |       let ns := es >>= \(x,y) => [x,y]
1087 |        in G o $ clearOrphanGroups ns (delEdges es g)
1088 |
1089 | mapNodeIf : {k : _} -> (Fin k -> Bool) -> (n -> n) -> IGraph k e n -> IGraph k e n
1090 | mapNodeIf p f = mapWithCtxt (\n,a => if p n then f a.label else a.label) 
1091 |
1092 | groupSelected : CDIGraph k -> List Nat -> Fin k -> Bool
1093 | groupSelected g ns n =
1094 |   let (CA _ a) := lab g n
1095 |    in isSelected g True n || any ((`elem` ns) . nr) a.label
1096 |
1097 | ||| Translates the selected atoms in a molecule by a vector given
1098 | ||| as a start and end point.
1099 | export
1100 | moveSelected :
1101 |      {auto _ : CoreDims}
1102 |   -> (start, end : Point Mol)
1103 |   -> CDGraph
1104 |   -> CDGraph
1105 | moveSelected start end (G o g) =
1106 |   let gs := selectedNodes g True >>= toList . groupNr g
1107 |    in mergeCloseNodes $ mapNodeIf (groupSelected g gs) (translate $ end - start) g
1108 |
1109 | export
1110 | rotateTempl :
1111 |      {auto cd : CoreDims}
1112 |   -> Bool
1113 |   -> (start, end : Point Mol)
1114 |   -> CDGraph
1115 |   -> CDGraph
1116 | rotateTempl cont start end g =
1117 |   let Just d := angle (end   - start) | Nothing => g
1118 |       phi    := fromMaybe d (closestAngle d $ if cont then [] else stepAngles)
1119 |    in rotateAt start phi g
1120 |
1121 | ||| Rotates the selected atoms around the center of the selection
1122 | ||| by an angle defined by the two points.
1123 | |||
1124 | ||| If the `Bool` argument is set to `False`, we use step-wise rotation
1125 | ||| as defined in the `CoreDims` argument, otherwise, we use 
1126 | ||| continuous rotation.
1127 | export
1128 | rotateSelected :
1129 |      {auto cd : CoreDims}
1130 |   -> Bool
1131 |   -> (start, end : Point Mol)
1132 |   -> CDGraph
1133 |   -> CDGraph
1134 | rotateSelected cont start end (G o g) =
1135 |   let ns := selectedNodes g True
1136 |       c  := center $ foldMap (bounds . lab g) ns
1137 |       Just ae := angle (end   - c) | Nothing => G o g
1138 |       Just as := angle (start - c) | Nothing => G o g
1139 |       d       := ae - as
1140 |       phi     := fromMaybe d (closestAngle d $ if cont then [] else stepAngles)
1141 |       gs := ns >>= toList . groupNr g
1142 |    in mergeCloseNodes $ mapNodeIf (groupSelected g gs) (rotateAt c phi) g
1143 |
1144 | ||| Extracts the selected subgraph (or the whole graph, if no atoms are
1145 | ||| selected) from a drawing graph.
1146 | export
1147 | selectedSubgraph : (includeEmptySelection : Bool) -> CDGraph -> CDGraph
1148 | selectedSubgraph b (G o g) =
1149 |   case plusGroupNodes g (selectedNodes g False) of
1150 |     [] => if b then G o g else G 0 empty
1151 |     ns => snd <$> subgraphL g ns
1152 |