http://mathling.com/geometric  library module

http://mathling.com/geometric


Module with functions providing some handy geometric operations.
General operations that apply across region type are here, with dispatch
to the more specific operations in those modules where possible. (This
migration is a work-in-progress: some shuffling around is to be expected
going forwards.)

Copyright© Mary Holstege 2020-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)

April 2021
Status: Incomplete, subject to refactoring

Imports

http://mathling.com/type/reach
import module namespace reach="http://mathling.com/type/reach"
       at "../types/reach.xqy"
http://mathling.com/geometric/complex-polygon
import module namespace cpoly="http://mathling.com/geometric/complex-polygon"
       at "../geo/complex-polygon.xqy"
http://mathling.com/type/distribution
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy"
http://mathling.com/type/wrapper
import module namespace wrapper="http://mathling.com/type/wrapper"
       at "../types/wrapper.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/type/slot
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.xqy"
http://mathling.com/geometric/affine
import module namespace affine="http://mathling.com/geometric/affine"
       at "../geo/affine.xqy"
http://mathling.com/type/space
import module namespace space="http://mathling.com/type/space"
       at "../types/space.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/geometric/path
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy"
http://mathling.com/geometric/coordinates
import module namespace coord="http://mathling.com/geometric/coordinates"
       at "../geo/coordinates.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/geometric/transform
import module namespace transform="http://mathling.com/geometric/transform"
       at "../geo/transform.xqy"
http://mathling.com/geometric/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy"
http://mathling.com/type/text
import module namespace text="http://mathling.com/type/text"
       at "../types/text.xqy"
http://mathling.com/type/mask
import module namespace mask="http://mathling.com/type/mask"
       at "../types/mask.xqy"
http://mathling.com/geometric/solid
import module namespace solid="http://mathling.com/geometric/solid"
       at "../geo/solid.xqy"
http://mathling.com/core/callable
import module namespace f="http://mathling.com/core/callable"
       at "../core/callable.xqy"
http://mathling.com/core/roots
import module namespace roots="http://mathling.com/core/roots"
       at "../core/roots.xqy"
http://mathling.com/geometric/graph
import module namespace graph="http://mathling.com/geometric/graph"
       at "../geo/graph.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/sdf
import module namespace sdf="http://mathling.com/sdf"
       at "../sdf/sdf.xqy"
http://mathling.com/core/vector
import module namespace v="http://mathling.com/core/vector"
       at "../core/vector.xqy"
http://mathling.com/core/config
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy"
http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy"

Variables

Variable: $precision as xs:integer

Variable: $DRAWING-MAP as map(xs:string,function(*)?)

Functions

Function: kind
declare function kind($region as map(xs:string,item()*)) as xs:string

Params
  • region as map(xs:string,item()*)
Returns
  • xs:string
declare function this:kind($region as map(xs:string,item()*)) as xs:string
{
  ($region("kind"),"")[1]
}

Function: property-map
declare function property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "point" return point:property-map($region)
  case "box" return box:property-map($region)
  case "block" return box:property-map($region)
  case "space" return box:property-map($region)
  case "edge" return edge:property-map($region)
  case "arc" return edge:property-map($region)
  case "ellipse-arc" return edge:property-map($region)
  case "quad" return edge:property-map($region)
  case "cubic" return edge:property-map($region)
  case "path" return path:property-map($region)
  case "polygon" return path:property-map($region)
  case "complex-polygon" return cpoly:property-map($region)
  case "graph" return graph:property-map($region)
  case "ellipse" return ellipse:property-map($region)
  case "slot" return slot:property-map($region)
  case "wrapper" return wrapper:property-map($region)
  case "mask" return mask:property-map($region)
  case "reach" return reach:property-map($region)
  case "sphere" return solid:property-map($region)
  case "ellipsoid" return solid:property-map($region)
  case "face" return solid:property-map($region)
  case "tetrahedron" return solid:property-map($region)
  case "cube" return solid:property-map($region)
  case "octahedron" return solid:property-map($region)
  case "icosahedron" return solid:property-map($region)
  case "dodecahedron" return solid:property-map($region)
  case "polyhedron" return solid:property-map($region)
  default return map {}
}

Function: properties
declare function properties($region as map(xs:string,item()*)) as xs:string*

Params
  • region as map(xs:string,item()*)
Returns
  • xs:string*
declare function this:properties($region as map(xs:string,item()*)) as xs:string*
{
  switch(this:kind($region))
  case "point" return point:properties($region)
  case "box" return box:properties($region)
  case "block" return box:properties($region)
  case "space" return box:properties($region)
  case "edge" return edge:properties($region)
  case "arc" return edge:properties($region)
  case "ellipse-arc" return edge:properties($region)
  case "quad" return edge:properties($region)
  case "cubic" return edge:properties($region)
  case "path" return path:properties($region)
  case "polygon" return path:properties($region)
  case "complex-polygon" return cpoly:properties($region)
  case "graph" return graph:properties($region)
  case "ellipse" return ellipse:properties($region)
  case "slot" return slot:properties($region)
  case "wrapper" return wrapper:properties($region)
  case "mask" return mask:properties($region)
  case "reach" return reach:properties($region)
  case "sphere" return solid:properties($region)
  case "ellipsoid" return solid:properties($region)
  case "face" return solid:properties($region)
  case "tetrahedron" return solid:properties($region)
  case "cube" return solid:properties($region)
  case "octahedron" return solid:properties($region)
  case "icosahedron" return solid:properties($region)
  case "dodecahedron" return solid:properties($region)
  case "polyhedron" return solid:properties($region)
  default return ()
}

Function: with-properties
declare function with-properties($regions as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:with-properties($regions as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "point" return point:with-properties($region, $properties)
  case "box" return box:with-properties($region, $properties)
  case "block" return box:with-properties($region, $properties)
  case "space" return box:with-properties($region, $properties)
  case "edge" return edge:with-properties($region, $properties)
  case "arc" return edge:with-properties($region, $properties)
  case "ellipse-arc" return edge:with-properties($region, $properties)
  case "quad" return edge:with-properties($region, $properties)
  case "cubic" return edge:with-properties($region, $properties)
  case "path" return path:with-properties($region, $properties)
  case "polygon" return path:with-properties($region, $properties)
  case "complex-polygon" return cpoly:with-properties($region, $properties)
  case "graph" return graph:with-properties($region, $properties)
  case "ellipse" return ellipse:with-properties($region, $properties)
  case "slot" return slot:with-properties($region, $properties)
  case "wrapper" return wrapper:with-properties($region, $properties)
  case "mask" return mask:with-properties($region, $properties)
  case "reach" return reach:with-properties($region, $properties)
  case "sphere" return solid:with-properties($region, $properties)
  case "ellipsoid" return solid:with-properties($region, $properties)
  case "face" return solid:with-properties($region, $properties)
  case "tetrahedron" return solid:with-properties($region, $properties)
  case "cube" return solid:with-properties($region, $properties)
  case "octahedron" return solid:with-properties($region, $properties)
  case "icosahedron" return solid:with-properties($region, $properties)
  case "dodecahedron" return solid:with-properties($region, $properties)
  case "polyhedron" return solid:with-properties($region, $properties)
  default return errors:error("GEOM-BADREGION", ($region, "with-properties"))
}

Function: with-edge-properties
declare function with-edge-properties($regions as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • properties as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:with-edge-properties(
  $regions as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $new-edges :=
        for $edge in this:edges($region) return (
          $edge=>this:with-properties($properties)
        )
      return
        switch(this:kind($region))
        case "path" return path:path($new-edges, $region=>this:property-map())
        case "polygon" return path:polygon($new-edges, $region=>this:property-map())
        case "graph" return graph:graph($region=>graph:vertices(), $new-edges, $region=>this:property-map())
        case "edge" return $new-edges
        case "arc" return $new-edges
        case "ellipse-arc" return $new-edges
        case "quad" return $new-edges
        case "cubic" return $new-edges
        default return errors:error("GEOM-BADREGION", ($region, "with-edge-properties"))
    },
    true()
  )
}

Function: points
declare function points($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*


points()
Get 2D points from the region

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:points($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch(this:kind($region))
      case "point" return $region=>point:as-dimension(2)
      case "box" return box:points($region)
      case "block" return box:points($region)
      case "space" return box:points($region)
      case "path" return path:points($region)
      case "polygon" return path:points($region)
      case "graph" return graph:points($region)
      case "edge" return edge:points($region)
      case "arc" return edge:points($region)
      case "ellipse-arc" return edge:points($region)
      case "quad" return edge:points($region)
      case "cubic" return edge:points($region)
      case "face" return solid:points($region)
      case "tetrahedron" return solid:points($region)
      case "cube" return solid:points($region)
      case "octahedron" return solid:points($region)
      case "icosahedron" return solid:points($region)
      case "dodecahedron" return solid:points($region)
      case "polyhedron" return solid:points($region)
      default return errors:error("GEOM-BADREGION", ($region, "points"))
    },
    false()
  )
}

Function: vertices
declare function vertices($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*


vertices()
Get all the region's points.

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:vertices($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch(this:kind($region))
      case "point" return $region
      case "box" return box:vertices($region)
      case "block" return box:vertices($region)
      case "space" return box:vertices($region)
      case "path" return path:vertices($region)
      case "polygon" return path:vertices($region)
      case "graph" return graph:vertices($region)
      case "edge" return edge:vertices($region)
      case "arc" return edge:vertices($region)
      case "ellipse-arc" return edge:vertices($region)
      case "quad" return edge:vertices($region)
      case "cubic" return edge:vertices($region)
      case "face" return solid:vertices($region)
      case "tetrahedron" return solid:vertices($region)
      case "cube" return solid:vertices($region)
      case "octahedron" return solid:vertices($region)
      case "icosahedron" return solid:vertices($region)
      case "dodecahedron" return solid:vertices($region)
      case "polyhedron" return solid:vertices($region)
      default return errors:error("GEOM-BADREGION", ($region, "vertices"))
    },
    false()
  )
}

Function: vertex
declare function vertex($region as map(xs:string,item()*), $i as xs:integer) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
  • i as xs:integer
Returns
  • map(xs:string,item()*)
declare function this:vertex($region as map(xs:string,item()*), $i as xs:integer) as map(xs:string,item()*)
{
  this:vertices($region)[$i]
}

Function: edges
declare function edges($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*


edges()
Get the edges some regions

Params
  • regions as map(xs:string,item()*)*: The regions
Returns
  • map(xs:string,item()*)*
declare function this:edges( $regions as map(xs:string,item()*)* ) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch (this:kind($region))
      case "box" return box:edges($region)
      case "block" return box:edges($region)
      case "space" return box:edges($region)
      case "path" return path:edges($region)
      case "polygon" return path:edges($region)
      case "graph" return graph:edges($region)
      case "arc" return $region
      case "ellipse-arc" return $region
      case "edge" return $region
      case "cubic" return $region
      case "quad" return $region
      case "face" return solid:edges($region)
      case "tetrahedron" return solid:edges($region)
      case "cube" return solid:edges($region)
      case "octahedron" return solid:edges($region)
      case "icosahedron" return solid:edges($region)
      case "dodecahedron" return solid:edges($region)
      case "polyhedron" return solid:edges($region)
      default return errors:error("GEOM-BADREGION", ($region, "edges"))
    },
    false()
  )
}

Function: to-edges
declare function to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)*


to-edges()
Convert a sequence of points into a sequence of edges.

Params
  • points as map(xs:string,item()*)*: the sequence of points
Returns
  • map(xs:string,item()*)*
declare function this:to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  edge:to-edges($points)
}

Function: to-edges
declare function to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*


to-edges()
Convert a sequence of points into a sequence of edges.

Params
  • points as item()*: the sequence of points
  • properties as map(xs:string,item()*): edge properties
Returns
  • map(xs:string,item()*)*
declare function this:to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  edge:to-edges($points, $properties)
}

Function: as-polygon
declare function as-polygon($region as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:as-polygon(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "box" return path:polygon(this:edges($region))
  case "space" return path:polygon(this:edges($region))
  case "edge" return path:polygon($region) 
  case "arc" return path:polygon($region) 
  case "ellipse-arc" return path:polygon($region) 
  case "quad" return path:polygon($region)  
  case "cubic" return path:polygon($region) 
  case "path" return path:as-polygon($region)
  case "polygon" return $region
  case "face" return solid:polygon($region)
  default return errors:error("GEOM-BADREGION", ($region, "as-polygon"))
}

Function: as-box
declare function as-box($region as map(xs:string,item()*)) as map(xs:string,item()*)

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:as-box(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "point" return
    util:merge-into($region=>point:property-map(), box:box($region, $region))
  case "box" return box:as-box($region)
  case "block" return box:as-box($region)
  case "space" return box:as-box($region)
  case "edge" return
    util:merge-into(
      $region=>edge:property-map(),
      box:box($region=>edge:start(), $region=>edge:end())
    )
  case "polygon" return
    (: Only for the special case that it is an axis-aligned box
     : This test is a little incomplete: you could have degenerate
     : cases or butterfly edges, in which case this ends up being
     : the bounding box. Those checks aren't worth the bother.
     :)
    let $edges := this:edges($region)
    let $xs := distinct-values($edges!this:vertices(.)!point:px(.))
    let $ys := distinct-values($edges!this:vertices(.)!point:py(.))
    let $zs := distinct-values($edges!this:vertices(.)!point:pz(.))
    return (
      if ( (count($edges) eq 4) and
           (count($xs) eq 2) and
           (count($ys) eq 2) and
           (count($zs) eq 1)
         )
      then (
        util:merge-into(
          $region=>path:property-map(),
          box:box(min($xs), min($ys), max($xs), max($ys))
        )
      ) else (
        errors:error("GEOM-BADREGION", ($region, "as-box"))
      )
    )
  default return errors:error("GEOM-BADREGION", ($region, "as-box"))
}

Function: apply-matrix2
declare function apply-matrix2($regions as map(xs:string,item()*)*, $affine2 as xs:double*) as map(xs:string,item()*)*


apply-matrix2()
Apply a 2D affine matrix to the region

Params
  • regions as map(xs:string,item()*)*
  • affine2 as xs:double*: the 2D matrix (6 values)
Returns
  • map(xs:string,item()*)*
declare function this:apply-matrix2(
  $regions as map(xs:string,item()*)*,
  $affine2 as xs:double*
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:apply-matrix2($pt, $affine2)
    }
  )
}

Function: apply-matrix3
declare function apply-matrix3($regions as map(xs:string,item()*)*, $affine3 as xs:double*) as map(xs:string,item()*)*


apply-matrix3()
Apply a 3D affine matrix to the region

Params
  • regions as map(xs:string,item()*)*
  • affine3 as xs:double*: the 3D matrix (12 values)
Returns
  • map(xs:string,item()*)*
declare function this:apply-matrix3(
  $regions as map(xs:string,item()*)*,
  $affine3 as xs:double*
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:apply-matrix3($pt, $affine3)
    }
  )
}

Function: translate
declare function translate($regions as map(xs:string,item()*)*, $x as xs:double, $y as xs:double) as map(xs:string,item()*)*


translate()
Translate the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to translate
  • x as xs:double: how much to translate in the x direction
  • y as xs:double: how much to translate in the y direction
Returns
  • map(xs:string,item()*)*
declare function this:translate(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:translate($pt, $x, $y)
    }
  )
}

Function: translate
declare function translate($regions as map(xs:string,item()*)*, $x as xs:double, $y as xs:double, $z as xs:double) as map(xs:string,item()*)*


translate()
Translate the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to translate
  • x as xs:double: how much to translate in the x direction
  • y as xs:double: how much to translate in the y direction
  • z as xs:double: how much to translate in the z direction
Returns
  • map(xs:string,item()*)*
declare function this:translate(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double,
  $z as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:translate($pt, $x, $y, $z)
    }
  )
}

Function: scale
declare function scale($regions as map(xs:string,item()*)*, $scaling as xs:double) as map(xs:string, item()*)*


scale()
Scale the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to scale
  • scaling as xs:double: how much to scale the point by (all dimensions)
Returns
  • map(xs:string,item()*)*
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $scaling as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $scaling),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $scaling),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $scaling),
          $region=>ellipse:rx() * $scaling,
          $region=>ellipse:ry() * $scaling,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $scaling)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $scaling), affine:scale($points[2], $scaling),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:sphere(
            affine:scale(solid:center($region), $scaling),
            solid:radius($region) * $scaling
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $scaling),
            solid:rx($region) * $scaling,
            solid:ry($region) * $scaling,
            solid:rz($region) * $scaling
          )
        )
      )
      (: Uniform scaling of regular solids are OK, special handling :)
      case "tetrahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:tetrahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "cube" return (
        util:merge-into(
          solid:property-map($region),
          solid:cube(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "octahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:octahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "icosahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:icosahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "dodecahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:dodecahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $scaling)
          }
        )
      )
    },
    true()
  )
}

Function: scale
declare function scale($regions as map(xs:string,item()*)*, $sx as xs:double, $sy as xs:double) as map(xs:string, item()*)*


scale()
Scale the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to scale
  • sx as xs:double: how much to scale the point by in the x dimension
  • sy as xs:double: how much to scale the point by in the y dimension
Returns
  • map(xs:string,item()*)*
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $sx, $sy),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $sx, $sy),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $sx, $sy),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $sx, $sy)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $sx, $sy), affine:scale($points[2], $sx, $sy),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region)
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region)
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $sx, $sy)
          }
        )
      )
    },
    true()
  )
}

Function: scale3D
declare function scale3D($regions as map(xs:string,item()*)*, $sx as xs:double, $sy as xs:double, $sz as xs:double) as map(xs:string, item()*)*


scale3D()
Scale the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to scale
  • sx as xs:double: how much to scale the point by in the x dimension
  • sy as xs:double: how much to scale the point by in the y dimension
  • sz as xs:double: how much to scale the point by in the z dimension
Returns
  • map(xs:string,item()*)*
declare function this:scale3D(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $sz as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale3D($region=>ellipse:center(), $sx, $sy, $sz),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale3D($region=>edge:arc-circle(), $sx, $sy, $sz)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale3D($points[1], $sx, $sy, $sz), affine:scale3D($points[2], $sx, $sy, $sz),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region) * $sz
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region) * $sz
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale3D($pt, $sx, $sy, $sz)
          }
        )
      )
    },
    true()
  )
}

Function: scale
declare function scale($regions as map(xs:string,item()*)*, $sx as xs:double, $sy as xs:double, $center as map(xs:string,item()*)) as map(xs:string, item()*)*


scale()
Scale the regions by the given amount, but keep centered on a particular
point. Normally scaling will shift points closer to the origin. This
allows you do shift then closer to a different center of scaling instead,
so you can, for example, scale a circle about its own center, keeping it
in place.

Params
  • regions as map(xs:string,item()*)*: regions to scale
  • sx as xs:double: how much to scale the point by in the x dimension
  • sy as xs:double: how much to scale the point by in the y dimension
  • center as map(xs:string,item()*): center of scaling
Returns
  • map(xs:string,item()*)*
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $sx, $sy, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $sx, $sy, $center),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $sx, $sy, $center),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $sx, $sy, $center)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $sx, $sy, $center), affine:scale($points[2], $sx, $sy, $center),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy, $center),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region)
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy, $center),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region)
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $sx, $sy, $center)
          }
        )
      )
    },
    true()
  )
}

Function: scale-in-place
declare function scale-in-place($regions as map(xs:string,item()*)*, $x as xs:double, $y as xs:double) as map(xs:string,item()*)*


scale-in-place()
Scale using the center of the region as point of scaling.

Params
  • regions as map(xs:string,item()*)*
  • x as xs:double
  • y as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:scale-in-place(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double
) as map(xs:string,item()*)*
{
  for $region in $regions return
  let $location := this:region-center($region)
  return 
    this:translate(
      this:scale($region, $x, $y),
      point:px($location) - $x*point:px($location),
      point:py($location) - $y*point:py($location)
    )
}

Function: scale3D
declare function scale3D($regions as map(xs:string,item()*)*, $sx as xs:double, $sy as xs:double, $sz as xs:double, $center as map(xs:string,item()*)) as map(xs:string, item()*)*


scale3D()
Scale the regions by the given amount, but keep centered on a particular
point. Normally scaling will shift points closer to the origin. This
allows you do shift then closer to a different center of scaling instead,
so you can, for example, scale a circle about its own center, keeping it
in place.

Params
  • regions as map(xs:string,item()*)*: regions to scale
  • sx as xs:double: how much to scale the point by in the x dimension
  • sy as xs:double: how much to scale the point by in the y dimension
  • sz as xs:double: how much to scale the point by in the z dimension
  • center as map(xs:string,item()*): center of scaling
Returns
  • map(xs:string,item()*)*
declare function this:scale3D(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $sz as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz, $center),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale3D($region=>ellipse:center(), $sx, $sy, $sz, $center),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale3D($region=>edge:arc-circle(), $sx, $sy, $sz, $center)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale3D($points[1], $sx, $sy, $sz, $center), affine:scale3D($points[2], $sx, $sy, $sz, $center),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz, $center),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region) * $sz
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz, $center),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region) * $sz
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale3D($pt, $sx, $sy, $sz, $center)
          }
        )
      )
    },
    true()
  )
}

Function: scale-to-unit-circle
declare function scale-to-unit-circle($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:scale-to-unit-circle(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $region in $regions return
  let $bounds := this:bounding-box($region)
  let $depth := max(this:vertices($region)!point:pz(.))
  return this:scale-to-unit-circle($region, $bounds=>map:put("depth",$depth))
}

Function: scale-to-unit-circle
declare function scale-to-unit-circle($regions as map(xs:string,item()*)*, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:scale-to-unit-circle(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $width := box:width($canvas)
  let $height := box:height($canvas)
  let $depth := ($canvas("depth"),max(($width,$height)))[1]
  return this:scale3D($regions, 1 div $width, 1 div $height, 1 div $depth)
}

Function: scale-to-canvas
declare function scale-to-canvas($regions as map(xs:string,item()*)*, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • canvas as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:scale-to-canvas(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $width := box:width($canvas)
  let $height := box:height($canvas)
  return this:scale($regions, $width, $height)
}

Function: rotate
declare function rotate($regions as map(xs:string,item()*)*, $degrees as xs:double) as map(xs:string,item()*)*


rotate()
Rotate the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to rotate
  • degrees as xs:double: how many degrees to rotate the region
Returns
  • map(xs:string,item()*)*
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $degrees as xs:double
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $degrees),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $degrees),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $degrees)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $degrees,          
              $angles[2] - $degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $degrees),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $degrees),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $degrees)
          }
        )
      )
    },
    true()
  )
}

Function: rotate
declare function rotate($regions as map(xs:string,item()*)*, $roll-degrees as xs:double, $pitch-degrees as xs:double, $yaw-degrees as xs:double) as map(xs:string,item()*)*


rotate()
Rotate the regions by the given amount.

Params
  • regions as map(xs:string,item()*)*: regions to rotate
  • roll-degrees as xs:double: how many degrees to rotate the region forwards and back (x)
  • pitch-degrees as xs:double: how many degrees to rotate the region up and down (y)
  • yaw-degrees as xs:double: how many degrees to rotate the region left and right (z)
Returns
  • map(xs:string,item()*)*
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $roll-degrees as xs:double,
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      (: but arcs are 2D, so this is just nonsense, really :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $roll-degrees, $pitch-degrees, $yaw-degrees)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $roll-degrees,          
              $angles[2] - $pitch-degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        (: More nonsense :)
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $roll-degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $roll-degrees, $pitch-degrees, $yaw-degrees)
          }
        )
      )
    },
    true()
  )
}

Function: rotate
declare function rotate($regions as map(xs:string,item()*)*, $degrees as xs:double, $center as map(xs:string,item()*)) as map(xs:string,item()*)*


rotate()
Rotate the regions by the given amount around a center of rotation.
Normally rotation is relative to the origin.

Params
  • regions as map(xs:string,item()*)*: regions to rotate
  • degrees as xs:double: how many degrees to rotate the region
  • center as map(xs:string,item()*): center of rotation
Returns
  • map(xs:string,item()*)*
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $degrees as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $degrees, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $degrees, $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $degrees, $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $degrees,          
              $angles[2] - $degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $degrees, $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $degrees, $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $degrees, $center)
          }
        )
      )
    },
    true()
  )
}

Function: rotate
declare function rotate($regions as map(xs:string,item()*)*, $roll-degrees as xs:double, $pitch-degrees as xs:double, $yaw-degrees as xs:double, $center as map(xs:string,item()*)) as map(xs:string,item()*)*


rotate()
Rotate the regions by the given amount around a center of rotation.
Normally rotation is relative to the origin.

Params
  • regions as map(xs:string,item()*)*: regions to rotate
  • roll-degrees as xs:double: how many degrees to rotate the region forwards and back (x)
  • pitch-degrees as xs:double: how many degrees to rotate the region up and down (y)
  • yaw-degrees as xs:double: how many degrees to rotate the region left and right (z)
  • center as map(xs:string,item()*): center of rotation
Returns
  • map(xs:string,item()*)*
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $roll-degrees as xs:double,
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      (: but arcs are 2D, so this is just nonsense, really :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $roll-degrees,          
              $angles[2] - $pitch-degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        (: More nonsense :)
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $roll-degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $roll-degrees, $pitch-degrees, $yaw-degrees, $center)
          }
        )
      )
    },
    true()
  )
}

Function: shear
declare function shear($regions as map(xs:string,item()*)*, $xy as xs:double, $yx as xs:double) as map(xs:string,item()*)*


shear()
Shear the regions by the given amount.
You may wish to think of this as skewing angles instead:
math:tan(util:radians($skew-degrees)) for $xy or $yx
But note that tan approaches infinity near 90°/270°

Params
  • regions as map(xs:string,item()*)*: regions to shear.
  • xy as xs:double: how much to shear x relative to y
  • yx as xs:double: how much to shear y relative to x
Returns
  • map(xs:string,item()*)*
declare function this:shear(
  $regions as map(xs:string,item()*)*,
  $xy as xs:double,
  $yx as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:shear($pt, $xy, $yx)
    }
  )
}

Function: shear
declare function shear($regions as map(xs:string,item()*)*, $xy as xs:double, $yx as xs:double, $xz as xs:double, $zx as xs:double, $yz as xs:double, $zy as xs:double) as map(xs:string,item()*)*


shear()
Shear the regions by the given amount.
You may wish to think of this as skewing angles instead:
math:tan(math:radians($skew-degrees)) for $xy or $yx
But note that tan approaches infinity near 90°/270°

Params
  • regions as map(xs:string,item()*)*: regions to shear.
  • xy as xs:double: how much to shear x relative to y
  • yx as xs:double: how much to shear y relative to x
  • xz as xs:double: how much to shear x relative to z
  • zx as xs:double: how much to shear z relative to x
  • yz as xs:double: how much to shear y relative to z
  • zy as xs:double: how much to shear z relative to y
Returns
  • map(xs:string,item()*)*
declare function this:shear(
  $regions as map(xs:string,item()*)*,
  $xy as xs:double,
  $yx as xs:double,
  $xz as xs:double,
  $zx as xs:double,
  $yz as xs:double,
  $zy as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:shear($pt, $xy, $yx, $xz, $zx, $yz, $zy)
    }
  )
}

Function: reflect
declare function reflect($regions as map(xs:string,item()*)*, $center as map(xs:string,item()*)) as map(xs:string,item()*)*


reflect()
Reflect the regions across the center.

Params
  • regions as map(xs:string,item()*)*: regions to reflect.
  • center as map(xs:string,item()*): center of reflection.
Returns
  • map(xs:string,item()*)*
declare function this:reflect(
  $regions as map(xs:string,item()*)*,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:reflect($region=>path:edges(), $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:reflect($region=>path:edges(), $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:reflect($region=>edge:arc-circle(), $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] + 180,
              $angles[2] + 180,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:reflect($region=>ellipse:center(), $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:reflect($region=>ellipse:center(), $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + 180,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:reflect($pt, $center)
          }
        )
      )
    },
    true()
  )
}

Function: reflect
declare function reflect($regions as map(xs:string,item()*)*, $start as map(xs:string,item()*), $end as map(xs:string,item()*)) as map(xs:string,item()*)*


reflect()
Reflect the regions across the line.

Params
  • regions as map(xs:string,item()*)*: regions to reflect.
  • start as map(xs:string,item()*): start of reflection line
  • end as map(xs:string,item()*): end of reflection line
Returns
  • map(xs:string,item()*)*
declare function this:reflect(
  $regions as map(xs:string,item()*)*,
  $start as map(xs:string,item()*),
  $end as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:reflect($region=>path:edges(), $start, $end),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:reflect($region=>path:edges(), $start, $end),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:reflect($region=>edge:arc-circle(), $start, $end)
        let $angles := $arc=>edge:arc-angles()
        return ( 
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] + 180,
              $angles[2] + 180,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:reflect($pt, $start, $end)
          }
        )
      )
    },
    true()
  )
}

Function: perspective
declare function perspective($regions as map(xs:string,item()*)*, $camera as map(xs:string,item()*), $roll-degrees as xs:double, (: camera angles :) $pitch-degrees as xs:double, $yaw-degrees as xs:double, $display as map(xs:string,item()*)) as map(xs:string,item()*)*


perspective()
Perform a perspective transformation on the regions, rotated relative to
camera angles. Generally speaking camera location is at negative or zero
Z and the display surface is at some positive Z.
Not, strictly speaking, an affine transformation, because of the remapping
of the points back to 2D.

Params
  • regions as map(xs:string,item()*)*: regions to compute perspective on.
  • camera as map(xs:string,item()*): location of the camera
  • roll-degrees as xs:double: camera angle (x)
  • pitch-degrees as xs:double: camera angle (y)
  • yaw-degrees as xs:double: camera angle (z)
  • display as map(xs:string,item()*): display surface relative to camera
Returns
  • map(xs:string,item()*)*
declare function this:perspective(
  $regions as map(xs:string,item()*)*,
  $camera as map(xs:string,item()*),
  $roll-degrees as xs:double, (: camera angles :)
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double,
  $display as map(xs:string,item()*) (: display surface relative to camera :)
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:perspective($pt, $camera, $roll-degrees, $pitch-degrees, $yaw-degrees, $display)
    }
  )
}

Function: perspective
declare function perspective($regions as map(xs:string,item()*)*, $camera as map(xs:string,item()*), $display as map(xs:string,item()*)) as map(xs:string,item()*)*


perspective()
Perform a perspective transformation on the regions. Generally speaking
camera location is at negative or zero Z and the display surface is at
some positive Z.
Not, strictly speaking, an affine transformation, because of the remapping
of the points back to 2D.

Params
  • regions as map(xs:string,item()*)*: regions to compute perspective on.
  • camera as map(xs:string,item()*): location of the camera
  • display as map(xs:string,item()*): display surface relative to camera
Returns
  • map(xs:string,item()*)*
declare function this:perspective(
  $regions as map(xs:string,item()*)*,
  $camera as map(xs:string,item()*),
  $display as map(xs:string,item()*) (: display surface relative to camera :)
) as map(xs:string,item()*)*
{
  this:perspective($regions, $camera, 0, 0, 0, $display)
}

Function: projection
declare function projection($regions as map(xs:string,item()*)*, $α as xs:double, $β as xs:double) as map(xs:string,item()*)*


projection()
Perform a simple projection of the regions. Like isometric but with different
angles. A little easier to control that full 3-point perspective.
Using ~(±45, 90) is generally good. Note: you get 3d "objects" out, with the z
axis flattened to 0. You'll need to draw using the default map to avoid an
additional automatic perspective shift.

Params
  • regions as map(xs:string,item()*)*: regions to compute isometric projection on
  • α as xs:double: pitch degrees
  • β as xs:double: roll degrees
Returns
  • map(xs:string,item()*)*
declare function this:projection(
  $regions as map(xs:string,item()*)*,
  $α as xs:double,
  $β as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:projection($pt, $α, $β)
    }
  )
}

Function: isometric
declare function isometric($regions as map(xs:string,item()*)*, $positive as xs:boolean) as map(xs:string,item()*)*


isometric()
Perform a isometric projection of the regions.
Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
to draw using the default map to avoid an additional automatic perspective shift.

Params
  • regions as map(xs:string,item()*)*: regions to compute isometric projection on
  • positive as xs:boolean: tilt up or down?
Returns
  • map(xs:string,item()*)*
declare function this:isometric(
  $regions as map(xs:string,item()*)*,
  $positive as xs:boolean
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:isometric($pt, $positive)
    }
  )
}

Function: stereographic
declare function stereographic($solids as map(xs:string,item()*)*) as map(xs:string,item()*)*


stereographic()
Stereographic projection from point on unit sphere to Cartesian plane.
The regions are translated and scaled to the unit sphere based on the
bounding sphere and then remapped and rescaled back after mapping.
This generally gives nicer results than using the raw coordinates.
Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
to draw using the default map to avoid an additional automatic perspective shift.

Params
  • solids as map(xs:string,item()*)* 3D objects to map
Returns
  • map(xs:string,item()*)*
declare function this:stereographic(
  $solids as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $bounding-sphere := this:bounding-sphere($solids)
  let $scale := solid:radius($bounding-sphere)
  let $offset := solid:center($bounding-sphere)
  let $neg-offset := point:minus($offset)
  return (
    $solids=>
    this:translate(
      point:px($neg-offset), point:py($neg-offset), point:pz($neg-offset)
    )=>
    this:scale(1 div $scale)=>
    this:mutate(coord:stereographic#1)=>
    this:scale($scale)=>
    this:translate(point:px($offset), point:py($offset))
  )
}

Function: stereographic
declare function stereographic($solids as map(xs:string,item()*)*, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


stereographic()
Stereographic projection from point on unit sphere to Cartesian plane.
The regions are translated to the origin for mapping, but not pre-scaled.
The results are centered and scaled to the canvas.
Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
to draw using the default map to avoid an additional automatic perspective shift.

Params
  • solids as map(xs:string,item()*)* 3D objects to map
  • canvas as map(xs:string,item()*) canvas space to scale and center to
Returns
  • map(xs:string,item()*)*
declare function this:stereographic(
  $solids as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $bounding-sphere := this:bounding-sphere($solids)
  let $offset := solid:center($bounding-sphere)
  let $neg-offset := point:minus($offset)
  let $raw := (
    $solids=>
    this:translate(
      point:px($neg-offset), point:py($neg-offset), point:pz($neg-offset)
    )=>
    this:mutate(coord:stereographic#1)
  )
  let $bb := this:bounding-box(this:vertices($raw))
  let $scaling :=
    min((
      box:width($canvas) div box:width($bb),
      box:height($canvas) div box:height($bb)
    ))
  return (
    $raw=>
    this:scale($scaling)=>
    this:translate(point:px($offset), point:py($offset))
  )
}

Function: stereographic3D
declare function stereographic3D($regions as map(xs:string,item()*)*, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


stereographic3D()
Inverse stereographic projection from Cartesian plane to unit sphere.
The regions are pre-translated to the origin and then the results
are scaled and centered to the canvas.

Params
  • regions as map(xs:string,item()*)* 2D regions to map
  • canvas as map(xs:string,item()*) canvas space to scale and center to
Returns
  • map(xs:string,item()*)*
declare function this:stereographic3D(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $ccenter := box:center($canvas)
  let $raw :=
    $regions=>
      this:translate(-point:px($ccenter), -point:py($ccenter))=>
      this:mutate(coord:stereographic3D#1)
  let $bb := this:bounding-box(this:vertices($raw))
  let $center := box:center($bb)
  let $scaling :=
    min((
      box:width($canvas) div box:width($bb),
      box:height($canvas) div box:height($bb)
    ))
  return (
    $raw=>
      this:translate(-point:px($center), -point:py($center))=>
      this:scale($scaling)=>
      this:translate(point:px($ccenter), point:py($ccenter))
  )
}

Function: apply-transforms
declare function apply-transforms($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*


apply-transforms()
Apply the transformations to the object. This is if we are going to be doing
operations on the slot, like intersections etc. In general none of the
operations apply transforms in advance. Look for a transform property or if
this is a lot, the various slot transformation properties.

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:apply-transforms($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  let $ok := function ($attr as xs:string) as xs:boolean {
    matches($attr, "^[ ]*matrix[ ]*[(][ 1234567890,.-]+[)][ ]*$")=>trace("ok "||$attr)
  }
  for $region in $regions return 
  switch (this:kind($region))
  case "slot" return (
    slot:slot(
      let $location := slot:location($region)
      let $rotation := slot:rotation($region)
      let $center := slot:center($region)
      let $scale := slot:scale($region)
      let $skew := slot:skew($region)
      let $transform := transform:transform()
      let $transform :=
        if (exists($location))
        then $transform=>transform:translate(point:px($location), point:py($location))
        else $transform
      let $transform :=
        if (exists($scale)) then (
          if (exists($center))
          then (
            $transform=>
              transform:translate(-point:px($center), -point:py($center))=>
              transform:scale(point:px($scale), point:py($scale))=>
              transform:translate(point:px($center), point:py($center))
          ) else $transform=>transform:scale(point:px($scale), point:py($scale))
        ) else $transform
      let $transform :=
        if (exists($rotation)) then (
          if (exists($center))
          then $transform=>transform:rotate($center, $rotation)
          else $transform=>transform:rotate($rotation)
        )
        else $transform
      let $transform :=
        if (exists($skew))
        then $transform=>transform:skew(point:px($skew), point:py($skew))
        else $transform
      return (
        if (exists($region("transform")) and $ok($region("transform"))) then (
          this:apply-transforms($region=>slot:body())=>
            this:apply-matrix2(transform:parse-matrix($region("transform")))=>
            map:remove("transform")
        ) else (
          this:apply-transforms($region=>slot:body())
        )
      ),
      slot:property-map($region)
    )
  )
  case "wrapper" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>wrapper:body(
        this:apply-transforms($region=>wrapper:body())
      )=>this:apply-matrix2(transform:parse-matrix($region("transform")))=>
         map:remove("transform")
    ) else (
      $region=>wrapper:body(this:apply-transforms($region=>wrapper:body()))
    )
  )
  case "mask" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>mask:body(
        this:apply-transforms($region=>mask:body())=>
          this:apply-matrix2(transform:parse-matrix($region("transform")))=>
          map:remove("transform")
      )
    ) else (
      $region=>mask:body(this:apply-transforms($region=>mask:body()))
    )
  )
  case "reach" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>reach:body(
        this:apply-transforms($region=>reach:body())=>
          this:apply-matrix2(transform:parse-matrix($region("transform")))=>
          map:remove("transform")
      )
    ) else (
      $region=>reach:body(this:apply-transforms($region=>reach:body()))
    )
  )
  default return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>this:apply-matrix2(transform:parse-matrix($region("transform")))=>
         map:remove("transform")
    ) else (
      $region
    )
  )
}

Function: distance
declare function distance($p1 as map(xs:string,item()*), $p2 as map(xs:string,item()*)) as xs:double

Params
  • p1 as map(xs:string,item()*)
  • p2 as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:distance($p1 as map(xs:string,item()*), $p2 as map(xs:string,item()*)) as xs:double
{
  point:distance($p1, $p2)
}

Function: polar-distance
declare function polar-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:polar-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:polar-distance($p, $q)
}

Function: taxi-distance
declare function taxi-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:taxi-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:taxi-distance($p, $q)
}

Function: avg-distance
declare function avg-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:avg-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:avg-distance($p, $q)
}

Function: rail-distance
declare function rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:rail-distance($p, $q)
}

Function: rail-distance
declare function rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $origin as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
  • origin as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $origin as map(xs:string,item()*)) as xs:double
{
  point:rail-distance($p, $q, $origin)
}

Function: chebyshev-distance
declare function chebyshev-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:chebyshev-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:chebyshev-distance($p, $q)
}

Function: minkowski-distance
declare function minkowski-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $order as xs:double) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
  • order as xs:double
Returns
  • xs:double
declare %art:deprecated function this:minkowski-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $order as xs:double) as xs:double
{
  point:minkowski-distance($p, $q, $order)
}

Function: cosine-similarity
declare function cosine-similarity($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:cosine-similarity($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:cosine-similarity($p, $q)
}

Function: cosine-distance
declare function cosine-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double

Params
  • p as map(xs:string,item()*)
  • q as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:cosine-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:cosine-distance($p, $q)
}

Function: shortest-distance
declare function shortest-distance($pt as map(xs:string,item()*), $region as map(xs:string,item()*)) as xs:double


shortest-distance()
Distance from a point to a region. Warning: for curves and most solids this
is using SDF conversion, which is not terribly efficient if you are going
to be comparing a lot of points to the region. You'd be better off doing
the conversion once and running the SDF function yourself.
Only some of the solids are supported right now.
Returns 0 for interior points.

Params
  • pt as map(xs:string,item()*)
  • region as map(xs:string,item()*)
Returns
  • xs:double
declare function this:shortest-distance(
  $pt as map(xs:string,item()*),
  $region as map(xs:string,item()*)
) as xs:double
{
  switch (this:kind($region))
  case "point" return this:distance($pt, $region)
  case "box" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "block" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "space" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "edge" return (
    edge:point-distance($region, $pt)
  )
  case "arc" return (
    let $f := sdf:toSDF($region, map {"width":0})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "quad" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "cubic" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "path" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (path:polygon-contains-point($region,$pt)) then 0
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "complex-polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (cpoly:polygon-contains-point($region,$pt)) then 0
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "ellipse" return (
    if (ellipse:is-circle($region)) then (
      max((0, point:distance(ellipse:center($region), $pt) - ellipse:radius($region)))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "sphere" return (
    max((0, point:distance(solid:center($region), $pt) - solid:radius($region)))
  )
  case "ellipsoid" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "tetrahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "cube" return (
    if (solid:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "octahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  default return errors:error("GEOM-BADREGION", ($region, "shortest-distance"))
}

Function: signed-distance
declare function signed-distance($pt as map(xs:string,item()*), $region as map(xs:string,item()*)) as xs:double


signed-distance()
Distance from a point to a region. Warning: for curves and solids this is
using SDF conversion, which is not terribly efficient if you are going to be
comparing a lot of points to the region. You'd be better off doing the
conversion once and running the SDF function yourself.
Only some of the solids are supported right now.
Will return negative values for interior points; 0 for those on the boundary.

Params
  • pt as map(xs:string,item()*)
  • region as map(xs:string,item()*)
Returns
  • xs:double
declare function this:signed-distance(
  $pt as map(xs:string,item()*),
  $region as map(xs:string,item()*)
) as xs:double
{
  switch (this:kind($region))
  case "point" return this:distance($pt, $region)
  case "box" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "block" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "space" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "edge" return (
    if (point:same(edge:start($region), edge:end($region))) then (
      this:distance($pt, edge:start($region))
    ) else (
      let $v := edge:start($region)
      let $w := edge:end($region)
      let $l := edge:linear-length($region)
      let $lsquared := $l*$l
      let $w_sub_v := point:sub($w, $v)
      let $t := util:clamp(point:dot(point:sub($pt, $v), $w_sub_v) div $lsquared, 0, 1)
      return (
        this:distance($pt,
          point:point(
            point:px($v) + $t * point:px($w_sub_v),
            point:py($v) + $t * point:py($w_sub_v)
          )
        )
      )
    )
  )
  case "arc" return (
    let $f := sdf:toSDF($region, map {"width":0})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "quad" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "cubic" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "path" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (path:polygon-contains-point($region,$pt))
      then -min(this:edges($region)!this:shortest-distance($pt, .))
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "complex-polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (cpoly:polygon-contains-point($region,$pt))
      then -min(this:edges($region)!this:shortest-distance($pt, .))
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "ellipse" return
    if (ellipse:is-circle($region)) then (
      this:distance(ellipse:center($region), $pt) - ellipse:radius($region)
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  case "sphere" return (
    point:distance(solid:center($region), $pt) - solid:radius($region)
  )
  case "ellipsoid" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "tetrahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "cube" return (
    if (solid:contains-point($region,$pt)) 
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "octahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  default return errors:error("GEOM-BADREGION", ($region, "signed-distance"))
}

Function: snap
declare function snap($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:snap($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:snap($region)
      case "box" return box:snap($region)
      case "block" return box:snap($region)
      case "space" return box:snap($region)
      case "edge" return edge:snap($region)
      case "arc" return edge:snap($region)
      case "ellipse-arc" return edge:snap($region)
      case "quad" return edge:snap($region)
      case "cubic" return edge:snap($region)
      case "path" return path:snap($region)
      case "polygon" return path:snap($region)
      case "graph" return graph:snap($region)
      case "ellipse" return ellipse:snap($region)
      case "sphere" return solid:snap($region)
      case "ellipsoid" return solid:snap($region)
      case "face" return solid:snap($region)
      case "tetrahedron" return solid:snap($region)
      case "cube" return solid:snap($region)
      case "octahedron" return solid:snap($region)
      case "icosahedron" return solid:snap($region)
      case "dodecahedron" return solid:snap($region)
      case "polyhedron" return solid:snap($region)
      default return $region
    },
    true()
  )
}

Function: decimal
declare function decimal($regions as map(xs:string,item()*)*, $digits as xs:integer) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • digits as xs:integer
Returns
  • map(xs:string,item()*)*
declare function this:decimal(
  $regions as map(xs:string,item()*)*,
  $digits as xs:integer
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:decimal($region, $digits)
      case "box" return box:decimal($region, $digits)
      case "block" return box:decimal($region, $digits)
      case "space" return box:decimal($region, $digits)
      case "edge" return edge:decimal($region, $digits)
      case "arc" return edge:decimal($region, $digits)
      case "ellipse-arc" return edge:decimal($region, $digits)
      case "quad" return edge:decimal($region, $digits)
      case "cubic" return edge:decimal($region, $digits)
      case "path" return path:decimal($region, $digits)
      case "polygon" return path:decimal($region, $digits)
      case "graph" return graph:decimal($region, $digits)
      case "ellipse" return ellipse:decimal($region, $digits)
      case "sphere" return solid:decimal($region, $digits)
      case "ellipsoid" return solid:decimal($region, $digits)
      case "face" return solid:decimal($region, $digits)
      case "tetrahedron" return solid:decimal($region, $digits)
      case "cube" return solid:decimal($region, $digits)
      case "octahedron" return solid:decimal($region, $digits)
      case "icosahedron" return solid:decimal($region, $digits)
      case "dodecahedron" return solid:decimal($region, $digits)
      case "polyhedron" return solid:decimal($region, $digits)
      default return $region
    },
    true()
  )
}

Function: quote
declare function quote($regions as item()*) as xs:string

Params
  • regions as item()*
Returns
  • xs:string
declare function this:quote(
  $regions as item()*
) as xs:string
{
  string-join(
    for $region in $regions return typeswitch($region)
    case map(*) return switch(this:kind($region))
      case "point" return point:quote($region)
      case "box" return box:quote($region)
      case "block" return box:quote($region)
      case "space" return box:quote($region)
      case "edge" return edge:quote($region)
      case "arc" return edge:quote($region)
      case "ellipse-arc" return edge:quote($region)
      case "quad" return edge:quote($region)
      case "cubic" return edge:quote($region)
      case "path" return path:quote($region)
      case "polygon" return path:quote($region)
      case "complex-polygon" return cpoly:quote($region)
      case "graph" return graph:quote($region)
      case "ellipse" return ellipse:quote($region)
      case "sphere" return solid:quote($region)
      case "ellipsoid" return solid:quote($region)
      case "face" return solid:quote($region)
      case "tetrahedron" return solid:quote($region)
      case "cube" return solid:quote($region)
      case "octahedron" return solid:quote($region)
      case "icosahedron" return solid:quote($region)
      case "dodecahedron" return solid:quote($region)
      case "polyhedron" return solid:quote($region)
      default return errors:quote($region)
    default return errors:quote($region)
    ,
    " + "
  )
}

Function: same
declare function same($this as map(xs:string,item()*), $other as map(xs:string,item()*)) as xs:boolean

Params
  • this as map(xs:string,item()*)
  • other as map(xs:string,item()*)
Returns
  • xs:boolean
declare function this:same(
  $this as map(xs:string,item()*),
  $other as map(xs:string,item()*)
) as xs:boolean
{
  (this:kind($this)=this:kind($other) and (
    switch (this:kind($this))
    case "point" return point:same($this, $other)
    case "box" return box:same($this, $other)
    case "block" return box:same($this, $other)
    case "space" return box:same($this, $other)
    case "edge" return edge:same($this, $other)
    case "arc" return edge:same($this, $other)
    case "ellipse-arc" return edge:same($this, $other)
    case "quad" return edge:same($this, $other)
    case "cubic" return edge:same($this, $other)
    case "path" return path:same($this, $other)
    case "polygon" return path:same($this, $other)
    case "complex-polygon" return cpoly:same($this, $other)
    case "graph" return graph:same($this, $other)
    case "ellipse" return ellipse:same($this, $other)
    case "sphere" return solid:same($this, $other)
    case "ellipsoid" return solid:same($this, $other)
    case "face" return solid:same($this, $other)
    case "tetrahedron" return solid:same($this, $other)
    case "cube" return solid:same($this, $other)
    case "octahedron" return solid:same($this, $other)
    case "icosahedron" return solid:same($this, $other)
    case "dodecahedron" return solid:same($this, $other)
    case "polyhedron" return solid:same($this, $other)
    default return deep-equal($this,$other)
  )) or (
    (: edge case: degenerate complex polygons :)
    this:kind($this)=("complex-polygon","polygon") and
    this:kind($other)=("complex-polygon","polygon") and
    cpoly:same($this, $other)
  )
}

Function: mutate
declare function mutate($regions as map(xs:string,item()*)*, $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*)) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • mutation as function(map(xs:string,item()*))asmap(xs:string,item()*)
Returns
  • map(xs:string,item()*)*
declare function this:mutate(
  $regions as map(xs:string,item()*)*,
  $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:mutate($region, $mutation)
      case "box" return box:mutate($region, $mutation)
      case "block" return box:mutate($region, $mutation)
      case "space" return box:mutate($region, $mutation)
      case "edge" return edge:mutate($region, $mutation)
      case "arc" return edge:mutate($region, $mutation)
      case "ellipse-arc" return edge:mutate($region, $mutation)
      case "quad" return edge:mutate($region, $mutation)
      case "cubic" return edge:mutate($region, $mutation)
      case "path" return path:mutate($region, $mutation)
      case "polygon" return path:mutate($region, $mutation)
      case "graph" return graph:mutate($region, $mutation)
      case "ellipse" return ellipse:mutate($region, $mutation)
      case "sphere" return solid:mutate($region, $mutation)
      case "ellipsoid" return solid:mutate($region, $mutation)
      case "face" return solid:mutate($region, $mutation)
      case "tetrahedron" return solid:mutate($region, $mutation)
      case "cube" return solid:mutate($region, $mutation)
      case "octahedron" return solid:mutate($region, $mutation)
      case "icosahedron" return solid:mutate($region, $mutation)
      case "dodecahedron" return solid:mutate($region, $mutation)
      case "polyhedron" return solid:mutate($region, $mutation)
      default return $region
    },
    true()
  )
}

Function: project
declare function project($regions as map(xs:string,item()*)*, $p as xs:double, $q as xs:double, $r as xs:double) as map(xs:string,item()*)*


project()
Simple 3-point projection of regions. Does not account for stroke widths.
You will almost certainly need to map from canvas space to the unit circle
and back.

Params
  • regions as map(xs:string,item()*)*
  • p as xs:double
  • q as xs:double
  • r as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:project(
  $regions as map(xs:string,item()*)*,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $x := point:px($pt)
      let $y := point:py($pt)
      let $z := point:pz($pt)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return point:point($x div $d, $y div $d)
    }
  )
}

Function: project
declare function project($regions as map(xs:string,item()*)*, $canvas as map(xs:string,item()*), $p as xs:double, $q as xs:double, $r as xs:double) as map(xs:string,item()*)*


project()
Simple 3-point projection of regions. Does not account for stroke widths.
Does the mapping from canvas space to the unit circle and back.

Params
  • regions as map(xs:string,item()*)*
  • canvas as map(xs:string,item()*) should have a "depth" property
  • p as xs:double
  • q as xs:double
  • r as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:project(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $scaled := this:scale-to-unit-circle($pt, $canvas)
      let $x := point:px($scaled)
      let $y := point:py($scaled)
      let $z := point:pz($scaled)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return this:scale-to-canvas(point:point($x div $d, $y div $d), $canvas)
    }
  )
}

Function: project-strokes
declare function project-strokes($regions as map(xs:string,item()*)*, $default-stroke-width as xs:integer, $p as xs:double, $q as xs:double, $r as xs:double, $tolerance as xs:double) as map(xs:string,item()*)*


project-strokes()
Project regions. Converts simple strokes into closed paths with the
stroke width projected as well. To do this certain circles and ellipses
need to be interpolated to polygons: tolerance says how finely that
interpolation is done.
You will almost certainly need to map from canvas space to the unit circle
and back.
Also note that this will turn polygons into a set of polygons (one
per edge), so if you wanted it filled you're out of luck. And if you
wanted it filled, why are you projecting stroke width in the first place?
Just project it and fill it.

Params
  • regions as map(xs:string,item()*)*
  • default-stroke-width as xs:integer
  • p as xs:double
  • q as xs:double
  • r as xs:double
  • tolerance as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:project-strokes(
  $regions as map(xs:string,item()*)*,
  $default-stroke-width as xs:integer,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double,
  $tolerance as xs:double
) as map(xs:string,item()*)*
{
  let $hypoteneuse :=
    util:round(math:sqrt(2*$default-stroke-width*$default-stroke-width))
  let $projection := 
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $x := point:px($pt)
      let $y := point:py($pt)
      let $z := point:pz($pt)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return point:point($x div $d, $y div $d)
    }
  for $region in $regions
  let $kind := this:kind($region)
  return (
    if ($kind="point") then (
      point:mutate($region, $projection)
    ) else if ($kind=("edge","quad","cubic")) then (
      let $leading := edge:mutate($region, $projection)
      let $trailing := edge:mutate(this:translate(edge:reverse($region), $hypoteneuse, $hypoteneuse), $projection)
      return 
        path:polygon(($leading, edge:edge(edge:end($leading),edge:start($trailing)),$trailing))
    ) else if ($kind="arc") then (
      (: n interpolations : length / tolerance :)
      let $n := edge:arc-length($region) idiv $tolerance
      let $edges := edge:interpolate-arc($n, $region, false())
      for $edge in $edges return (
        this:project-strokes(
          $edge, $default-stroke-width, $p, $q, $r, $tolerance
        )
      )
    ) else if ($kind="ellipse") then (
      (: n interpolations : perimeter / tolerance :)
      let $n := ellipse:perimeter($region) idiv $tolerance
      let $edges := ellipse:interpolate($n, $region)
      for $edge in $edges return (
        this:project-strokes(
          $edge, $default-stroke-width, $p, $q, $r, $tolerance
        )
      )
    ) else (
      let $edges := this:edges($region) return (
        for $edge in $edges return (
          this:project-strokes(
            $edge, $default-stroke-width, $p, $q, $r, $tolerance
          )
        ),
        for $edge at $i in tail($edges)
        where not(point:same(edge:end($edges[$i]), edge:start($edge)))
        return (
          this:project-strokes(
            edge:edge(edge:end($edges[$i]), edge:start($edge)),
            $default-stroke-width, $p, $q, $r, $tolerance
          )
        ),
        if ($kind="polygon") then (
          if (not(point:same(edge:end($edges[last()]), edge:start($edges[1])))) then (
            this:project-strokes(
              edge:edge(edge:end($edges[last()]), edge:start($edges[1])),
              $default-stroke-width, $p, $q, $r, $tolerance
            )
          ) else ()
        ) else ()
      )
    )
  )
}

Function: project-strokes
declare function project-strokes($regions as map(xs:string,item()*)*, $default-stroke-width as xs:integer, $p as xs:double, $q as xs:double, $r as xs:double) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • default-stroke-width as xs:integer
  • p as xs:double
  • q as xs:double
  • r as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:project-strokes(
  $regions as map(xs:string,item()*)*,
  $default-stroke-width as xs:integer,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:project-strokes($regions, $default-stroke-width, $p, $q, $r, 1)
}

Function: area
declare function area($regions as map(xs:string,item()*)*) as xs:double

Params
  • regions as map(xs:string,item()*)*
Returns
  • xs:double
declare function this:area($regions as map(xs:string,item()*)*) as xs:double
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "box" return box:area($region)
    case "block" return solid:surface-area($region)
    case "space" return box:area($region)
    case "polygon" return path:area($region)
    case "complex-polygon" return cpoly:area($region)
    case "ellipse" return ellipse:area($region)
    case "slot" return this:area($region=>slot:body())
    case "wrapper" return this:area($region=>wrapper:body())
    case "mask" return this:area($region=>mask:body())
    case "reach" return this:area($region=>reach:body())
    case "sphere" return solid:surface-area($region)
    case "ellipsoid" return solid:surface-area($region)
    case "face" return solid:surface-area($region)
    case "tetrahedron" return solid:surface-area($region)
    case "cube" return solid:surface-area($region)
    case "octahedron" return solid:surface-area($region)
    case "icosahedron" return solid:surface-area($region)
    case "dodecahedron" return solid:surface-area($region)
    case "polyhedron" return solid:surface-area($region)
    default return 0
  )
}

Function: length
declare function length($regions as map(xs:string,item()*)*) as xs:double

Params
  • regions as map(xs:string,item()*)*
Returns
  • xs:double
declare function this:length($regions as map(xs:string,item()*)*) as xs:double
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "box" return sum(this:edges($region)!edge:length(.))
    case "space" return sum(this:edges($region)!edge:length(.))
    case "polygon" return path:length($region)
    case "complex-polygon" return cpoly:length($region)
    case "path" return path:length($region)
    case "ellipse" return ellipse:perimeter($region)
    case "arc" return edge:length($region)
    case "quad" return edge:length($region)
    case "cubic" return edge:length($region)
    case "edge" return edge:length($region)
    case "slot" return this:length($region=>slot:body())
    case "wrapper" return this:length($region=>wrapper:body())
    case "mask" return this:length($region=>mask:body())
    case "reach" return this:length($region=>reach:body())
    default return 0
  )
}

Function: n-faces
declare function n-faces($regions as map(xs:string,item()*)*) as xs:integer

Params
  • regions as map(xs:string,item()*)*
Returns
  • xs:integer
declare function this:n-faces($regions as map(xs:string,item()*)*) as xs:integer
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "slot" return this:n-faces($region=>slot:body())
    case "wrapper" return this:n-faces($region=>wrapper:body())
    case "mask" return this:n-faces($region=>mask:body())
    case "reach" return this:n-faces($region=>reach:body())
    default return solid:n-faces($region)
  ) cast as xs:integer
}

Function: path-angle
declare function path-angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double


path-angle()
Compute the angle (azimuth) from one point to the next, in degrees
Return 0 if points are the same

Params
  • last as map(xs:string,item()*)?: previous point; use (0,0) if no previous
  • curr as map(xs:string,item()*): current point
Returns
  • xs:double
declare function this:path-angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
{
  point:angle($last, $curr)
}

Function: path-inclination
declare function path-inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double


path-inclination()
Compute the inclination angle from one point in the next, in degrees
Return 0 if the points are the same

Params
  • last as map(xs:string,item()*)?: previous point; use (0,0,0) if no previous
  • curr as map(xs:string,item()*): current point
Returns
  • xs:double
declare function this:path-inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
{
  point:inclination($last, $curr)
}

Function: path-length
declare function path-length($path as map(xs:string,item()*)) as xs:double


path-length()
Compute total length of travel along a path. If path includes cubic
edges the result is approximated.

Params
  • path as map(xs:string,item()*): the path
Returns
  • xs:double
declare function this:path-length($path as map(xs:string,item()*)) as xs:double
{
  path:length($path)
}

Function: destination
declare function destination($point as map(xs:string,item()*), $degrees as xs:double, $length as xs:double) as map(xs:string,item()*)


destination()
Point at a particular distance and direction.

Params
  • point as map(xs:string,item()*): starting point
  • degrees as xs:double: bearing from point
  • length as xs:double: how far along bearing to travel
Returns
  • map(xs:string,item()*)
declare function this:destination(
  $point as map(xs:string,item()*),
  $degrees as xs:double,
  $length as xs:double
) as map(xs:string,item()*)
{
  point:destination($point, $degrees, $length)
}

Function: destination
declare function destination($point as map(xs:string,item()*), $azimuth_degrees as xs:double, $inclination_degrees as xs:double, $length as xs:double) as map(xs:string,item()*)


destination()
Point (3D) at a particular distance, azimuth, and inclination.

Params
  • point as map(xs:string,item()*): starting point
  • azimuth_degrees as xs:double: angle of azimuth from point
  • inclination_degrees as xs:double: angle of inclination from point
  • length as xs:double: how far along bearing to travel
Returns
  • map(xs:string,item()*)
declare function this:destination(
  $point as map(xs:string,item()*),
  $azimuth_degrees as xs:double,
  $inclination_degrees as xs:double,
  $length as xs:double
) as map(xs:string,item()*)
{
  point:destination($point, $azimuth_degrees, $inclination_degrees, $length)
}

Function: region-center
declare function region-center($region as map(xs:string,item()*)) as map(xs:string,item()*)


region-center()
Approximate center of region. A rough approximation for polygons with
curved edges. 2D

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:region-center(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  (: Sometimes we set the center we know/care about :)
  if ($region=>map:contains("center")) then $region("center")
  else
  switch (this:kind($region))
  case "point" return $region
  case "box" return $region=>box:center()
  case "block" return $region=>box:center()
  case "space" return $region=>box:center()
  case "path" return (
    let $n := count(this:edges($region))
    return switch ($n)
    case 0 return $point:ORIGIN (: GIGO :)
    case 1 return this:edges($region)[1]=>edge:edge-point(0.5)
    default return $region=>path:path-point(0.5)
  )
  case "polygon" return $region=>path:polygon-center()
  case "complex-polygon" return $region=>cpoly:polygon-center()
  case "arc" return $region=>edge:edge-point(0.5)
  case "edge" return $region=>edge:edge-point(0.5)
  case "quad" return $region=>edge:edge-point(0.5)
  case "cubic" return $region=>edge:edge-point(0.5)
  case "ellipse" return $region=>ellipse:center()
  case "sphere" return $region=>solid:center()
  case "ellipsoid" return $region=>solid:center()
  case "face" return $region=>solid:center()
  case "tetrahedron" return $region=>solid:center()
  case "cube" return $region=>solid:center()
  case "octahedron" return $region=>solid:center()
  case "icosahedron" return $region=>solid:center()
  case "dodecahedron" return $region=>solid:center()
  case "polyhedron" return $region=>solid:center()
  case "slot" return
    point:centroid(
      for $sub in $region=>slot:body() return this:region-center($sub)
    )
  case "wrapper" return
    point:centroid(
      for $sub in $region=>wrapper:body() return this:region-center($sub)
    )
  case "mask" return
    point:centroid(
      for $sub in $region=>mask:body() return this:region-center($sub)
    )
  case "reach" return
    point:centroid(
      for $sub in $region=>reach:body() return this:region-center($sub)
    )
  default return errors:error("GEOM-BADREGION", ($region, "region-center"))
}

Function: interpolate-using
declare function interpolate-using($regions as map(xs:string,item()*)*, $divisions as function(item()) as xs:double*) as map(xs:string,item()*)*

Params
  • regions as map(xs:string,item()*)*
  • divisions as function(item())asxs:double*
Returns
  • map(xs:string,item()*)*
declare function this:interpolate-using(
  $regions as map(xs:string,item()*)*,
  $divisions as function(item()) as xs:double*
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch ($region=>map:get("kind"))
      case "edge" return edge:interpolate-edge-using($region, $divisions)
      case "arc" return edge:interpolate-arc-using($region, $divisions)
      case "quad" return edge:interpolate-quadratic-using($region, $divisions)
      case "cubic" return edge:interpolate-cubic-using($region, $divisions)
      case "path" return this:interpolate-using($region=>this:edges(), $divisions)
      case "polygon" return this:interpolate-using($region=>this:edges(), $divisions)
      case "graph" return this:interpolate-using($region=>this:edges(), $divisions)
      case "box" return this:interpolate-using($region=>this:edges(), $divisions)
      case "space" return this:interpolate-using($region=>this:edges(), $divisions)
      case "ellipse" return ellipse:interpolate-using($region, $divisions)
      case "face" return this:interpolate-using($region=>solid:polygon(), $divisions)
      default return $region
    },
    false()
  )
}

Function: interpolate
declare function interpolate($n as xs:integer, $regions as map(xs:string,item()*)*, $exclusive as xs:boolean) as map(xs:string,item()*)*


interpolate()
Interpolate the edges of the region; return the interpolated points.
Note: when interpolating paths or polygons, exclusivity is still edge-wise
so with exclusive interpolation you'll lose the last point and with
inclusive interpolation you'll end up with duplicate points because end
of one edge is the start of the next edge. Exclusivity doesn't apply to
ellipse interpolation. (Zen koan: what is the end of a closed loop?)

Params
  • n as xs:integer: number of points of interpolation for each edge
  • regions as map(xs:string,item()*)*: the set of regions
  • exclusive as xs:boolean: include end point? default=false
Returns
  • map(xs:string,item()*)*
declare function this:interpolate(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*,
  $exclusive as xs:boolean
) as map(xs:string,item()*)*
{
  this:interpolate-using($regions,
    if ($exclusive) then (
      function ($region as map(xs:string,item()*)) as xs:double* {
        util:linspace($n, 0, 1, not(this:kind($region)="ellipse"))
      }
    ) else (
      function ($region as map(xs:string,item()*)) as xs:double* {
        util:linspace($n, 0, 1)
      }
    )
  )
}

Function: interpolate
declare function interpolate($n as xs:integer, $regions as map(xs:string,item()*)*) as map(xs:string,item()*)*

Params
  • n as xs:integer
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)*
declare function this:interpolate(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  this:interpolate($n, $regions, false())
}

Function: interpolate-jittered
declare function interpolate-jittered($n as xs:integer, $regions as map(xs:string,item()*)*, $jitter-percent as xs:integer) as map(xs:string,item()*)*

Params
  • n as xs:integer
  • regions as map(xs:string,item()*)*
  • jitter-percent as xs:integer
Returns
  • map(xs:string,item()*)*
declare %art:non-deterministic function this:interpolate-jittered(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*,
  $jitter-percent as xs:integer
) as map(xs:string,item()*)*
{
  this:interpolate-using($regions,
    function ($region as map(xs:string,item()*)) as xs:double* {
      let $even-divisions := util:linspace($n, 0, 1)
      let $jitter :=
        if ($jitter-percent > 0)
        then dist:uniform(1.0 - $jitter-percent div 100, 1.0)
        else dist:constant(1.0)
      let $js := rand:randomize($n, $jitter)
      for $t at $i in $even-divisions return $t * $js[$i] 
    }
  )
}

Function: region-point
declare function region-point($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)*

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)*
declare function this:region-point(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)*
{
  switch(this:kind($region))
  case "point" return $region
  case "edge" return edge:edge-point($region, $t)
  case "arc" return edge:edge-point($region, $t)
  case "ellipse-arc" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "ellipse-arc"))
  case "quad" return edge:edge-point($region, $t)
  case "cubic" return edge:edge-point($region, $t)
  case "path" return path:path-point($region, $t)
  case "polygon" return path:path-point($region, $t)
  case "complex-polygon" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "complex-polygon"))
  case "graph" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "graph"))
  case "ellipse" return ellipse:ellipse-point($region, $t)
  case "slot" return this:region-point($region=>slot:body(), $t)
  case "wrapper" return this:region-point($region=>wrapper:body(), $t)
  case "mask" return this:region-point($region=>mask:body(), $t)
  case "reach" return this:region-point($region=>reach:body(), $t)
  default return errors:error("GEOM-BADREGION", ($region, "region-point"))
}

Function: tangent
declare function tangent($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)


tangent()
Return the tangent vector to the given point on the edge (2D)

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)
declare function this:tangent(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "edge" return edge:tangent($region, $t)
  case "quad" return edge:tangent($region, $t)
  case "cubic" return edge:tangent($region, $t)
  case "arc" return edge:tangent($region, $t)
  case "ellipse-arc" return edge:tangent($region, $t)
  case "path" return path:tangent($region, $t)
  case "polygon" return path:tangent($region, $t)
  case "ellipse" return edge:tangent($region, $t) 
  case "box" return this:tangent(this:as-polygon($region), $t)
  case "space" return this:tangent(this:as-polygon($region), $t)
  case "face" return this:tangent(this:as-polygon($region), $t)
  default return errors:error("GEOM-NOTIMPLEMENTED", ("tangent", this:kind($region)))
}

Function: normal
declare function normal($region as map(xs:string,item()*), $t as xs:double) as map(xs:string,item()*)


normal()
Return the normal vector to the given point on the edge/path (2D)

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • map(xs:string,item()*)
declare function this:normal(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  this:tangent($region, $t)=>point:perpendicular()
}

Function: tangent-angle
declare function tangent-angle($region as map(xs:string,item()*), $t as xs:double) as xs:double

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • xs:double
declare function this:tangent-angle(
  $region as map(xs:string,item()*),
  $t as xs:double
) as xs:double
{
  if (this:kind($region)="ellipse") then ellipse:tangent-angle($region, $t)
  else point:angle($point:ORIGIN2D, this:tangent($region, $t))
}

Function: normal-angle
declare function normal-angle($region as map(xs:string,item()*), $t as xs:double) as xs:double

Params
  • region as map(xs:string,item()*)
  • t as xs:double
Returns
  • xs:double
declare function this:normal-angle(
  $region as map(xs:string,item()*),
  $t as xs:double
) as xs:double
{
  this:tangent-angle($region, $t) + 90
}

Function: random-points
declare function random-points($N as xs:integer, $last as map(xs:string,item()*)?, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*


random-points()
Generate N random points using the given algorithms.

Params
  • N as xs:integer: How many points to return
  • last as map(xs:string,item()*)?: Previous value (needed for markov, available to key callback)
  • x as map(xs:string,item()*) The algorithm to use for x coordinates, a map with various parameter keys
  • y as map(xs:string,item()*) The algorithm to use for y coordinates, a map with various parameter keys
Returns
  • map(xs:string,item()*)*
declare %art:non-deterministic function this:random-points($N as xs:integer, $last as map(xs:string,item()*)?, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*
{
  (: XYZZY we'd really like to be able to apply algorithms to $last as a point :)
  let $xs := rand:randomize($N, if (empty($last)) then () else point:px($last), $x)
  let $ys := rand:randomize($N, if (empty($last)) then () else point:py($last), $y)
  for $i in 1 to $N
  return point:point($xs[$i], $ys[$i])
}

Function: random-points
declare function random-points($N as xs:integer, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*


random-points()
Generate N random points using the given algorithms.

Params
  • N as xs:integer: How many points to return
  • x as map(xs:string,item()*) The algorithm to use for x coordinates, a map with various parameter keys
  • y as map(xs:string,item()*) The algorithm to use for y coordinates, a map with various parameter keys
Returns
  • map(xs:string,item()*)*
declare %art:non-deterministic function this:random-points($N as xs:integer, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*
{
   this:random-points($N, (), $x, $y)
}

Function: random-point
declare function random-point($x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)


random-point()
Generate a random point using the given algorithms.

Params
  • x as map(xs:string,item()*) The algorithm to use for x coordinates, a map with various parameter keys
  • y as map(xs:string,item()*) The algorithm to use for y coordinates, a map with various parameter keys
Returns
  • map(xs:string,item()*)
declare %art:non-deterministic function this:random-point($x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)
{
   this:random-points(1, (), $x, $y)
}

Function: random-points-in
declare function random-points-in($N as xs:integer, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*


random-points-in()
Generate N random points within the canvas (uniform distribution)

Params
  • N as xs:integer: How many points to return
  • canvas as map(xs:string,item()*): canvas (space) with height/width/edge parameters
Returns
  • map(xs:string,item()*)*
declare %art:non-deterministic function this:random-points-in($N as xs:integer, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $edge := box:edge($canvas)
  let $min-x := box:min-x($canvas)
  let $max-x := box:max-x($canvas)
  let $min-y := box:min-y($canvas)
  let $max-y := box:max-y($canvas)
  let $x := dist:uniform($min-x + $edge, $max-x - $edge)=>dist:cast("integer")
  let $y := dist:uniform($min-y + $edge, $max-y - $edge)=>dist:cast("integer")
  let $xs := rand:randomize($N, (), $x)
  let $ys := rand:randomize($N, (), $y)
  for $i in 1 to $N
  return point:point($xs[$i], $ys[$i])
}

Function: random-point-in
declare function random-point-in($canvas as map(xs:string,item()*)) as map(xs:string,item()*)


random-point-in()
Generate a random point within the canvas (uniform distribution)

Params
  • canvas as map(xs:string,item()*): canvas (space) with height/width/edge parameters
Returns
  • map(xs:string,item()*)
declare %art:non-deterministic function this:random-point-in($canvas as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:random-points-in(1, $canvas)
}

Function: minimal-bounding-circle
declare function minimal-bounding-circle($points as map(xs:string,item()*)*) as map(xs:string,item()*)


minimal-bounding-circle()
Minimal bounding circle for the given set of points.
If the points are ordered in some way, shuffle them for better convergence.

Params
  • points as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)
declare function this:minimal-bounding-circle(
  $points as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  this:weltz($points, ())
}

Function: bounding-circle
declare function bounding-circle($region as map(xs:string,item()*)) as map(xs:string,item()*)


bounding-circle()
Bounding circle for a region
Note: not necessarily minimal for non-linear-edges

Params
  • region as map(xs:string,item()*)
Returns
  • map(xs:string,item()*)
declare function this:bounding-circle(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "point" return this:trivial-bounding-circle($region)
  case "edge" return this:trivial-bounding-circle(this:points($region))
  case "quad" return
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "cubic" return
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "arc" return 
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "path" return
    let $pts := 
      if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
      then (
        this:points($region)
      ) else (
        let $boxes := this:edges($region)!this:bounding-box(.)
        return $boxes!this:points(.)
      )
    return
      this:minimal-bounding-circle($pts)
  case "box" return this:minimal-bounding-circle(this:points($region))
  case "space" return this:minimal-bounding-circle(this:points($region))
  case "ellipse" return
    if ($region=>ellipse:is-circle())
    then $region
    else ellipse:circle(ellipse:center($region), max((ellipse:rx($region), ellipse:ry($region))))
  case "polygon" return 
    let $pts := 
      if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
      then (
        this:points($region)
      ) else (
        let $boxes := this:edges($region)!this:bounding-box(.)
        return $boxes!this:points(.)
      )
    return
      this:minimal-bounding-circle($pts)
  case "complex-polygon" return this:bounding-circle(cpoly:outer($region))
  default return errors:error("GEOM-BADREGION", ($region, "bounding-circle"))
}

Function: bounding-box
declare function bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)


bounding-box()
Minimum box surrounding the region. Some approximation for non-linear
edges.

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)
declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
{
  let $boxes := (
    for $region in $regions return switch (this:kind($region))
    case "point" return box:box($region, $region)
    case "edge" return edge:bounding-box($region)
    case "quad" return edge:bounding-box($region)
    case "cubic" return edge:bounding-box($region)
    case "arc" return edge:bounding-box($region)
    case "path" return path:bounding-box($region)
    case "polygon" return path:bounding-box($region)
    case "complex-polygon" return cpoly:bounding-box($region)
    case "box" return $region
    case "space" return box:box(box:min-point($region), box:max-point($region))
    case "ellipse" return ellipse:bounding-box($region)
    case "wrapper" return this:bounding-box($region=>wrapper:body())
    case "mask" return this:bounding-box($region=>mask:body())
    case "reach" return this:bounding-box($region=>reach:body())
    case "slot" return this:bounding-box($region=>slot:body()) (: FIXME: ignores transforms :)
    default return errors:error("GEOM-BADREGION", ($region, "bounding-box"))
  )
  return (
    if (empty($boxes)) then box:box(0,0,0,0)
    else if (empty(tail($boxes))) then $boxes
    else (
      let $pts := $boxes!this:points(.)
      return
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
    )
  )
}

Function: bounding-sphere
declare function bounding-sphere($regions as map(xs:string,item()*)*) as map(xs:string,item()*)


bounding-sphere()
A sphere that contains the regions.
2D regions are treated as a z=0

Params
  • regions as map(xs:string,item()*)*
Returns
  • map(xs:string,item()*)
declare function this:bounding-sphere(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  let $spheres := (
    for $region in $regions
    let $kind := this:kind($region)
    return (
      if ($kind="point") then solid:sphere($region,0)
      else if ($kind=("edge","quad","cubic","arc","path","polygon","complex-polygon","box","space","ellipse")) then (
        let $circle := this:bounding-box($region)
        return solid:sphere(ellipse:center($circle), ellipse:radius($circle))
      ) else if ($kind="wrapper") then (
        this:bounding-sphere($region=>wrapper:body())
      ) else if ($kind="mask") then (
        this:bounding-sphere($region=>mask:body())
      ) else if ($kind="reach") then (
        this:bounding-sphere($region=>reach:body())
      ) else if ($kind="slot") then (
        this:bounding-sphere($region=>slot:body()) (: FIXME: ignores transforms :)
      ) else if ($kind=("sphere","ellipsoid","tetrahedron","cube","octahedron","icosahedron","dodecahedron","block","polyhedron","face")) then (
        solid:circumsphere($region)
      ) else (
        errors:error("GEOM-BADREGION", ($region, "bounding-sphere"))
      )
    )
  )
  let $relevant-spheres := (
    for $i in 1 to count($spheres)
    let $center := solid:center($spheres[$i])
    let $radius := solid:radius($spheres[$i])
    return (
      (: if some sphere we've already seen contains this one, skip it :)
      if (
        some $j in 1 to $i - 1 satisfies (
          point:distance(solid:center($spheres[$j]), $center) <=
          solid:radius($spheres[$j]) - $radius
        )
      ) then () else (
        $spheres[$i]
      )
    )
  )
  return (
    if (empty($relevant-spheres)) then solid:sphere(point:point(0,0),0)
    else if (empty(tail($relevant-spheres))) then $relevant-spheres
    else (
      let $centers := $relevant-spheres!solid:center(.)
      let $center := 
        point:point(avg($centers!point:px(.)), avg($centers!point:py(.)), avg($centers!point:pz(.)))
      let $radius := max(
        for $i in 1 to count($relevant-spheres)
        return point:distance($center, $centers[$i]) + solid:radius($relevant-spheres[$i])
      )
      return (
        solid:sphere($center, $radius)
      )
    )
  )
}

Function: delegate
declare function delegate($regions as map(xs:string,item()*)*, $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*, $keep as xs:boolean) as map(xs:string,item()*)*


delegate()
Common delegation pattern for a lot of generic geo functions. The function
being delegated can return a sequence of any kind of region, but if you
apply this to complex polygons it should return a polygon if you have $keep.

Status: Bleeding edge
Params
  • regions as map(xs:string,item()*)*: sequence of regions
  • function as function(map(xs:string,item()*))asmap(xs:string,item()*)*: function that converts the region into other ones
  • keep as xs:boolean: preserve wrappers etc. in output
Returns
  • map(xs:string,item()*)*
declare function this:delegate(
  $regions as map(xs:string,item()*)*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "complex-polygon" return
    if ($keep) then (
      cpoly:complex-polygon(
        $function(cpoly:outer($region)),
        this:delegate(cpoly:inners($region), $function, $keep),
        $region=>this:property-map()
      )
    ) else (
      $function(cpoly:outer($region)),
      this:delegate(cpoly:inners($region), $function, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body( this:delegate($region=>wrapper:body(), $function, $keep) )
    ) else (
      this:delegate($region=>wrapper:body(), $function, $keep)
    )
  case "slot" return
    if ($keep) then (
      $region=>slot:body( this:delegate($region=>slot:body(), $function, $keep) )
    ) else (
      this:delegate($region=>slot:body(), $function, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body( this:delegate($region=>mask:body(), $function, $keep) )
    ) else (
      this:delegate($region=>mask:body(), $function, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body( this:delegate($region=>reach:body(), $function, $keep) )
    ) else (
      this:delegate($region=>reach:body(), $function, $keep)
    )
  default return $function($region)
}

Function: process-polygons
declare function process-polygons($regions as map(xs:string,item()*)*, $process as function(map(xs:string,item()*)) as item()*, $keep as xs:boolean) as item()*


process-polygons()
Walk through the regions, peering into slots and wrappers and the like,
applying the given function to each path, polygon, or complex-polygon.
Example: Replace all polygons with extruded polyhedra
geom:process-polygons($regions, solid:extrude(?, 10, 100), true())

Status: Bleeding edge
Params
  • regions as map(xs:string,item()*)*: the regions to process
  • process as function(map(xs:string,item()*))asitem()*: function that takes a region
  • keep as xs:boolean: if true(), keep wrappers and non-path/polygon regions
Returns
  • item()*: accumulation of applied function
declare function this:process-polygons(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-polygons(slot:body($region), $process, $keep))
    ) else (
      this:process-polygons(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-polygons(wrapper:body($region), $process, $keep))
    ) else (
      this:process-polygons(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-polygons(mask:body($region), $process, $keep))
    ) else (
      this:process-polygons(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-polygons(reach:body($region), $process, $keep))
    ) else (
      this:process-polygons(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
}

Function: process-areas
declare function process-areas($regions as map(xs:string,item()*)*, $process as function(map(xs:string,item()*)) as item()*, $keep as xs:boolean) as item()*


process-areas()
Walk through the regions, peering into slots and wrappers and the like,
applying the given function to each 2D region (points, edges = 1D)

Status: Bleeding edge
Params
  • regions as map(xs:string,item()*)*: the regions to process
  • process as function(map(xs:string,item()*))asitem()*: function that takes a region
  • keep as xs:boolean: if true(), keep wrappers and other unprocessed regions
Returns
  • item()*: accumulation of applied function
declare function this:process-areas(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "box" return $process($region)
  case "space" return $process($region)
  case "ellipse" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-areas(slot:body($region), $process, $keep))
    ) else (
      this:process-areas(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-areas(wrapper:body($region), $process, $keep))
    ) else (
      this:process-areas(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-areas(mask:body($region), $process, $keep))
    ) else (
      this:process-areas(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-areas(reach:body($region), $process, $keep))
    ) else (
      this:process-areas(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
}

Function: process-solids
declare function process-solids($regions as map(xs:string,item()*)*, $process as function(map(xs:string,item()*)) as item()*, $keep as xs:boolean) as item()*


process-solids()
Walk through the regions, peering into slots and wrappers and the like,
applying the given function to each solid (including faces).

Status: Bleeding edge
Params
  • regions as map(xs:string,item()*)*: the regions to process
  • process as function(map(xs:string,item()*))asitem()*: function that takes a region
  • keep as xs:boolean: if true(), keep wrappers and other unprocessed regions
Returns
  • item()*: accumulation of applied function
declare function this:process-solids(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "face" return $process($region)
  case "sphere" return $process($region)
  case "cube" return $process($region)
  case "tetrahedron" return $process($region)
  case "octahedron" return $process($region)
  case "dodecahedron" return $process($region)
  case "icosahedron" return $process($region)
  case "polyhedron" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-solids(slot:body($region), $process, $keep))
    ) else (
      this:process-solids(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-solids(wrapper:body($region), $process, $keep))
    ) else (
      this:process-solids(wrapper:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-solids(reach:body($region), $process, $keep))
    ) else (
      this:process-solids(reach:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-solids(mask:body($region), $process, $keep))
    ) else (
      this:process-solids(mask:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
}

Function: process-paths
declare function process-paths($regions as map(xs:string,item()*)*, $process as function(map(xs:string,item()*)) as item()*, $keep as xs:boolean) as item()*


process-paths()
Walk through the regions, peering into slots and wrappers and the like,
applying the given function to each path.
Example: Replace all polygons with extruded polyhedra
geom:process-polygons($regions, solid:extrude(?, 10, 100), true())

Status: Bleeding edge
Params
  • regions as map(xs:string,item()*)*: the regions to process
  • process as function(map(xs:string,item()*))asitem()*: function that takes a region
  • keep as xs:boolean: if true(), keep wrappers and non-path/polygon regions
Returns
  • item()*: accumulation of applied function
declare function this:process-paths(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-paths(slot:body($region), $process, $keep))
    ) else (
      this:process-paths(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-paths(wrapper:body($region), $process, $keep))
    ) else (
      this:process-paths(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-paths(mask:body($region), $process, $keep))
    ) else (
      this:process-paths(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-paths(reach:body($region), $process, $keep))
    ) else (
      this:process-paths(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
}

Function: point
declare function point($x as xs:double, $y as xs:double) as map(xs:string,item()*)

Params
  • x as xs:double
  • y as xs:double
Returns
  • map(xs:string,item()*)
declare %art:deprecated function this:point($x as xs:double, $y as xs:double) as map(xs:string,item()*)
{
  point:point($x, $y)
}

Function: point
declare function point($x as xs:double, $y as xs:double, $z as xs:double) as map(xs:string,item()*)

Params
  • x as xs:double
  • y as xs:double
  • z as xs:double
Returns
  • map(xs:string,item()*)
declare %art:deprecated function this:point($x as xs:double, $y as xs:double, $z as xs:double) as map(xs:string,item()*)
{
  point:point($x, $y, $z)
}

Function: x
declare function x($point as map(xs:string,item()*)) as xs:integer

Params
  • point as map(xs:string,item()*)
Returns
  • xs:integer
declare %art:deprecated function this:x($point as map(xs:string,item()*)) as xs:integer
{
  point:x($point)
}

Function: y
declare function y($point as map(xs:string,item()*)) as xs:integer

Params
  • point as map(xs:string,item()*)
Returns
  • xs:integer
declare %art:deprecated function this:y($point as map(xs:string,item()*)) as xs:integer
{
  point:y($point)
}

Function: px
declare function px($point as map(xs:string,item()*)) as xs:double

Params
  • point as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:px($point as map(xs:string,item()*)) as xs:double
{
  point:px($point)
}

Function: py
declare function py($point as map(xs:string,item()*)) as xs:double

Params
  • point as map(xs:string,item()*)
Returns
  • xs:double
declare %art:deprecated function this:py($point as map(xs:string,item()*)) as xs:double
{
  point:py($point)
}

Function: midpoint
declare function midpoint($a as map(xs:string,item()*), $b as map(xs:string,item()*)) as map(xs:string,item()*)


midpoint()
Return midpoint between two points

Params
  • a as map(xs:string,item()*): one point
  • b as map(xs:string,item()*): the other
Returns
  • map(xs:string,item()*)
declare %art:deprecated function this:midpoint(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  edge:midpoint($a, $b)
}

Original Source Code

xquery version "3.1";
(:~
 : Module with functions providing some handy geometric operations.
 : General operations that apply across region type are here, with dispatch
 : to the more specific operations in those modules where possible. (This
 : migration is a work-in-progress: some shuffling around is to be expected
 : going forwards.)
 :
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since April 2021
 : @custom:Status Incomplete, subject to refactoring
 :)
module namespace this="http://mathling.com/geometric"; 

declare namespace art="http://mathling.com/art";
declare namespace map="http://www.w3.org/2005/xpath-functions/map";
declare namespace array="http://www.w3.org/2005/xpath-functions/array";
declare namespace math="http://www.w3.org/2005/xpath-functions/math";

import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace f="http://mathling.com/core/callable"
       at "../core/callable.xqy";
import module namespace v="http://mathling.com/core/vector"
       at "../core/vector.xqy";
import module namespace roots="http://mathling.com/core/roots"
       at "../core/roots.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace dist="http://mathling.com/type/distribution"
       at "../types/distributions.xqy";
import module namespace space="http://mathling.com/type/space"
       at "../types/space.xqy";
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.xqy";
import module namespace wrapper="http://mathling.com/type/wrapper"
       at "../types/wrapper.xqy";
import module namespace mask="http://mathling.com/type/mask"
       at "../types/mask.xqy";
import module namespace reach="http://mathling.com/type/reach"
       at "../types/reach.xqy";
import module namespace text="http://mathling.com/type/text"
       at "../types/text.xqy";
import module namespace affine="http://mathling.com/geometric/affine"
       at "../geo/affine.xqy";
import module namespace coord="http://mathling.com/geometric/coordinates"
       at "../geo/coordinates.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy";
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace path="http://mathling.com/geometric/path"
       at "../geo/path.xqy";
import module namespace graph="http://mathling.com/geometric/graph"
       at "../geo/graph.xqy";
import module namespace solid="http://mathling.com/geometric/solid"
       at "../geo/solid.xqy";
import module namespace cpoly="http://mathling.com/geometric/complex-polygon"
       at "../geo/complex-polygon.xqy";
import module namespace transform="http://mathling.com/geometric/transform"
       at "../geo/transform.xqy";
import module namespace sdf="http://mathling.com/sdf"
       at "../sdf/sdf.xqy";

declare variable $this:precision as xs:integer := math:log10(1 div $config:ε) cast as xs:integer;

(:====================================================================== 
 : Generic accessors
 :======================================================================:)

declare function this:kind($region as map(xs:string,item()*)) as xs:string
{
  ($region("kind"),"")[1]
};

declare function this:property-map($region as map(xs:string,item()*)) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "point" return point:property-map($region)
  case "box" return box:property-map($region)
  case "block" return box:property-map($region)
  case "space" return box:property-map($region)
  case "edge" return edge:property-map($region)
  case "arc" return edge:property-map($region)
  case "ellipse-arc" return edge:property-map($region)
  case "quad" return edge:property-map($region)
  case "cubic" return edge:property-map($region)
  case "path" return path:property-map($region)
  case "polygon" return path:property-map($region)
  case "complex-polygon" return cpoly:property-map($region)
  case "graph" return graph:property-map($region)
  case "ellipse" return ellipse:property-map($region)
  case "slot" return slot:property-map($region)
  case "wrapper" return wrapper:property-map($region)
  case "mask" return mask:property-map($region)
  case "reach" return reach:property-map($region)
  case "sphere" return solid:property-map($region)
  case "ellipsoid" return solid:property-map($region)
  case "face" return solid:property-map($region)
  case "tetrahedron" return solid:property-map($region)
  case "cube" return solid:property-map($region)
  case "octahedron" return solid:property-map($region)
  case "icosahedron" return solid:property-map($region)
  case "dodecahedron" return solid:property-map($region)
  case "polyhedron" return solid:property-map($region)
  default return map {}
};

declare function this:properties($region as map(xs:string,item()*)) as xs:string*
{
  switch(this:kind($region))
  case "point" return point:properties($region)
  case "box" return box:properties($region)
  case "block" return box:properties($region)
  case "space" return box:properties($region)
  case "edge" return edge:properties($region)
  case "arc" return edge:properties($region)
  case "ellipse-arc" return edge:properties($region)
  case "quad" return edge:properties($region)
  case "cubic" return edge:properties($region)
  case "path" return path:properties($region)
  case "polygon" return path:properties($region)
  case "complex-polygon" return cpoly:properties($region)
  case "graph" return graph:properties($region)
  case "ellipse" return ellipse:properties($region)
  case "slot" return slot:properties($region)
  case "wrapper" return wrapper:properties($region)
  case "mask" return mask:properties($region)
  case "reach" return reach:properties($region)
  case "sphere" return solid:properties($region)
  case "ellipsoid" return solid:properties($region)
  case "face" return solid:properties($region)
  case "tetrahedron" return solid:properties($region)
  case "cube" return solid:properties($region)
  case "octahedron" return solid:properties($region)
  case "icosahedron" return solid:properties($region)
  case "dodecahedron" return solid:properties($region)
  case "polyhedron" return solid:properties($region)
  default return ()
};

declare function this:with-properties($regions as map(xs:string,item()*)*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "point" return point:with-properties($region, $properties)
  case "box" return box:with-properties($region, $properties)
  case "block" return box:with-properties($region, $properties)
  case "space" return box:with-properties($region, $properties)
  case "edge" return edge:with-properties($region, $properties)
  case "arc" return edge:with-properties($region, $properties)
  case "ellipse-arc" return edge:with-properties($region, $properties)
  case "quad" return edge:with-properties($region, $properties)
  case "cubic" return edge:with-properties($region, $properties)
  case "path" return path:with-properties($region, $properties)
  case "polygon" return path:with-properties($region, $properties)
  case "complex-polygon" return cpoly:with-properties($region, $properties)
  case "graph" return graph:with-properties($region, $properties)
  case "ellipse" return ellipse:with-properties($region, $properties)
  case "slot" return slot:with-properties($region, $properties)
  case "wrapper" return wrapper:with-properties($region, $properties)
  case "mask" return mask:with-properties($region, $properties)
  case "reach" return reach:with-properties($region, $properties)
  case "sphere" return solid:with-properties($region, $properties)
  case "ellipsoid" return solid:with-properties($region, $properties)
  case "face" return solid:with-properties($region, $properties)
  case "tetrahedron" return solid:with-properties($region, $properties)
  case "cube" return solid:with-properties($region, $properties)
  case "octahedron" return solid:with-properties($region, $properties)
  case "icosahedron" return solid:with-properties($region, $properties)
  case "dodecahedron" return solid:with-properties($region, $properties)
  case "polyhedron" return solid:with-properties($region, $properties)
  default return errors:error("GEOM-BADREGION", ($region, "with-properties"))
};

declare function this:with-edge-properties(
  $regions as map(xs:string,item()*)*,
  $properties as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $new-edges :=
        for $edge in this:edges($region) return (
          $edge=>this:with-properties($properties)
        )
      return
        switch(this:kind($region))
        case "path" return path:path($new-edges, $region=>this:property-map())
        case "polygon" return path:polygon($new-edges, $region=>this:property-map())
        case "graph" return graph:graph($region=>graph:vertices(), $new-edges, $region=>this:property-map())
        case "edge" return $new-edges
        case "arc" return $new-edges
        case "ellipse-arc" return $new-edges
        case "quad" return $new-edges
        case "cubic" return $new-edges
        default return errors:error("GEOM-BADREGION", ($region, "with-edge-properties"))
    },
    true()
  )
};

(:~
 : points()
 : Get 2D points from the region
 :)
declare function this:points($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch(this:kind($region))
      case "point" return $region=>point:as-dimension(2)
      case "box" return box:points($region)
      case "block" return box:points($region)
      case "space" return box:points($region)
      case "path" return path:points($region)
      case "polygon" return path:points($region)
      case "graph" return graph:points($region)
      case "edge" return edge:points($region)
      case "arc" return edge:points($region)
      case "ellipse-arc" return edge:points($region)
      case "quad" return edge:points($region)
      case "cubic" return edge:points($region)
      case "face" return solid:points($region)
      case "tetrahedron" return solid:points($region)
      case "cube" return solid:points($region)
      case "octahedron" return solid:points($region)
      case "icosahedron" return solid:points($region)
      case "dodecahedron" return solid:points($region)
      case "polyhedron" return solid:points($region)
      default return errors:error("GEOM-BADREGION", ($region, "points"))
    },
    false()
  )
};

(:~
 : vertices()
 : Get all the region's points.
 :)
declare function this:vertices($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch(this:kind($region))
      case "point" return $region
      case "box" return box:vertices($region)
      case "block" return box:vertices($region)
      case "space" return box:vertices($region)
      case "path" return path:vertices($region)
      case "polygon" return path:vertices($region)
      case "graph" return graph:vertices($region)
      case "edge" return edge:vertices($region)
      case "arc" return edge:vertices($region)
      case "ellipse-arc" return edge:vertices($region)
      case "quad" return edge:vertices($region)
      case "cubic" return edge:vertices($region)
      case "face" return solid:vertices($region)
      case "tetrahedron" return solid:vertices($region)
      case "cube" return solid:vertices($region)
      case "octahedron" return solid:vertices($region)
      case "icosahedron" return solid:vertices($region)
      case "dodecahedron" return solid:vertices($region)
      case "polyhedron" return solid:vertices($region)
      default return errors:error("GEOM-BADREGION", ($region, "vertices"))
    },
    false()
  )
};

declare function this:vertex($region as map(xs:string,item()*), $i as xs:integer) as map(xs:string,item()*)
{
  this:vertices($region)[$i]
};

(:~ 
 : edges()
 :   Get the edges some regions
 : @param $regions: The regions
 :)
declare function this:edges( $regions as map(xs:string,item()*)* ) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch (this:kind($region))
      case "box" return box:edges($region)
      case "block" return box:edges($region)
      case "space" return box:edges($region)
      case "path" return path:edges($region)
      case "polygon" return path:edges($region)
      case "graph" return graph:edges($region)
      case "arc" return $region
      case "ellipse-arc" return $region
      case "edge" return $region
      case "cubic" return $region
      case "quad" return $region
      case "face" return solid:edges($region)
      case "tetrahedron" return solid:edges($region)
      case "cube" return solid:edges($region)
      case "octahedron" return solid:edges($region)
      case "icosahedron" return solid:edges($region)
      case "dodecahedron" return solid:edges($region)
      case "polyhedron" return solid:edges($region)
      default return errors:error("GEOM-BADREGION", ($region, "edges"))
    },
    false()
  )
};

(:~
 : to-edges()
 : Convert a sequence of points into a sequence of edges.
 :
 : @param $points: the sequence of points
 :)
declare function this:to-edges($points as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  edge:to-edges($points)
};

(:~
 : to-edges()
 : Convert a sequence of points into a sequence of edges.
 :
 : @param $points: the sequence of points
 : @param $properties: edge properties
 :)
declare function this:to-edges($points as item()*, $properties as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  edge:to-edges($points, $properties)
};
 
(:====================================================================== 
 : Representation conversions
 :======================================================================:)

declare function this:as-polygon(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "box" return path:polygon(this:edges($region))
  case "space" return path:polygon(this:edges($region))
  case "edge" return path:polygon($region) 
  case "arc" return path:polygon($region) 
  case "ellipse-arc" return path:polygon($region) 
  case "quad" return path:polygon($region)  
  case "cubic" return path:polygon($region) 
  case "path" return path:as-polygon($region)
  case "polygon" return $region
  case "face" return solid:polygon($region)
  default return errors:error("GEOM-BADREGION", ($region, "as-polygon"))
};

declare function this:as-box(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "point" return
    util:merge-into($region=>point:property-map(), box:box($region, $region))
  case "box" return box:as-box($region)
  case "block" return box:as-box($region)
  case "space" return box:as-box($region)
  case "edge" return
    util:merge-into(
      $region=>edge:property-map(),
      box:box($region=>edge:start(), $region=>edge:end())
    )
  case "polygon" return
    (: Only for the special case that it is an axis-aligned box
     : This test is a little incomplete: you could have degenerate
     : cases or butterfly edges, in which case this ends up being
     : the bounding box. Those checks aren't worth the bother.
     :)
    let $edges := this:edges($region)
    let $xs := distinct-values($edges!this:vertices(.)!point:px(.))
    let $ys := distinct-values($edges!this:vertices(.)!point:py(.))
    let $zs := distinct-values($edges!this:vertices(.)!point:pz(.))
    return (
      if ( (count($edges) eq 4) and
           (count($xs) eq 2) and
           (count($ys) eq 2) and
           (count($zs) eq 1)
         )
      then (
        util:merge-into(
          $region=>path:property-map(),
          box:box(min($xs), min($ys), max($xs), max($ys))
        )
      ) else (
        errors:error("GEOM-BADREGION", ($region, "as-box"))
      )
    )
  default return errors:error("GEOM-BADREGION", ($region, "as-box"))
};

(:====================================================================== 
 : Affine transformations
 : The operations are allowed for all the region types, but they really
 : don't do the "right" thing for some of them, particularly boxes/spaces,
 : ellipses, arcs, and ellipsoids. For example, you can rotate a box,
 : but you don't get a rotated box out of the end of it, because boxes
 : here are axis-aligned boxes of space between minimum and maximum x-y
 : bounds. I allow the operation to proceed in these naive ways because
 : it allows for interesting variations in my art, and allows affine
 : transformations that preserve region types. If you want geometrically
 : correct transformations of the overall shapes, make a polygon from
 : the source region and transform that. For boxes you can make a polygon
 : from the edges, for ellipses you can use interpolate-ellipse() to 
 : get a set of points to construct a polygon from; similarly for arcs.
 : You can scale and translate rectangles as is just fine. It is the
 : rotations and shears that will fail. 
 : 
 : 3D solids will general do weird things when you apply 2D transforms to them
 : and ellipsoids are badly supported because I need to add rotation angles
 : to the representation.
 :
 : The basic rule of this API is that you get out the same kind of thing
 : you put in; so the cases where this can't be done you get "wrong"
 : results. Exception: generally you get generic polyhedra if you do anything
 : to a regular solid, because (naturally!) it is no longer regular so 
 : you can't stick with the simpler representation.
 :======================================================================:)

(:----------------------------------------------------------------------
 : Public-facing API
 : You can use the lower-level one in affine.xqy, e.g. if you want to 
 : chain a bunch of transforms and only want to calculate the matrix
 : once. Otherwise these are more usable.
 :----------------------------------------------------------------------:)


(:~
 : apply-matrix2()
 : Apply a 2D affine matrix to the region
 : 
 : @param $region: the region to apply the affine transformation to
 : @param $affine2: the 2D matrix (6 values)
 :)
declare function this:apply-matrix2(
  $regions as map(xs:string,item()*)*,
  $affine2 as xs:double*
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:apply-matrix2($pt, $affine2)
    }
  )
};


(:~
 : apply-matrix3()
 : Apply a 3D affine matrix to the region
 : 
 : @param $region: the region to apply the affine transformation to
 : @param $affine3: the 3D matrix (12 values)
 :)
declare function this:apply-matrix3(
  $regions as map(xs:string,item()*)*,
  $affine3 as xs:double*
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:apply-matrix3($pt, $affine3)
    }
  )
};

(:~
 : translate()
 : Translate the regions by the given amount.
 : 
 : @param $regions: regions to translate
 : @param $x: how much to translate in the x direction
 : @param $y: how much to translate in the y direction
 :)
declare function this:translate(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:translate($pt, $x, $y)
    }
  )
};

(:~
 : translate()
 : Translate the regions by the given amount.
 : 
 : @param $regions: regions to translate
 : @param $x: how much to translate in the x direction
 : @param $y: how much to translate in the y direction
 : @param $z: how much to translate in the z direction
 :)
declare function this:translate(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double,
  $z as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:translate($pt, $x, $y, $z)
    }
  )
};

(:~
 : scale()
 : Scale the regions by the given amount.
 : 
 : @param $regions: regions to scale
 : @param $scaling: how much to scale the point by (all dimensions)
 :)
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $scaling as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $scaling),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $scaling),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $scaling),
          $region=>ellipse:rx() * $scaling,
          $region=>ellipse:ry() * $scaling,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $scaling)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $scaling), affine:scale($points[2], $scaling),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:sphere(
            affine:scale(solid:center($region), $scaling),
            solid:radius($region) * $scaling
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $scaling),
            solid:rx($region) * $scaling,
            solid:ry($region) * $scaling,
            solid:rz($region) * $scaling
          )
        )
      )
      (: Uniform scaling of regular solids are OK, special handling :)
      case "tetrahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:tetrahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "cube" return (
        util:merge-into(
          solid:property-map($region),
          solid:cube(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "octahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:octahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "icosahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:icosahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      case "dodecahedron" return (
        util:merge-into(
          solid:property-map($region),
          solid:dodecahedron(
            affine:scale(solid:center($region), $scaling),
            solid:scale($region) * $scaling
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $scaling)
          }
        )
      )
    },
    true()
  )
};

(:~
 : scale()
 : Scale the regions by the given amount.
 : 
 : @param $regions: regions to scale
 : @param $sx: how much to scale the point by in the x dimension
 : @param $sy: how much to scale the point by in the y dimension
 :)
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $sx, $sy),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $sx, $sy),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $sx, $sy),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $sx, $sy)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $sx, $sy), affine:scale($points[2], $sx, $sy),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region)
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region)
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $sx, $sy)
          }
        )
      )
    },
    true()
  )
};

(:~
 : scale3D()
 : Scale the regions by the given amount.
 : 
 : @param $regions: regions to scale
 : @param $sx: how much to scale the point by in the x dimension
 : @param $sy: how much to scale the point by in the y dimension
 : @param $sz: how much to scale the point by in the z dimension
 :)
declare function this:scale3D(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $sz as xs:double
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale3D($region=>ellipse:center(), $sx, $sy, $sz),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale3D($region=>edge:arc-circle(), $sx, $sy, $sz)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale3D($points[1], $sx, $sy, $sz), affine:scale3D($points[2], $sx, $sy, $sz),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region) * $sz
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region) * $sz
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale3D($pt, $sx, $sy, $sz)
          }
        )
      )
    },
    true()
  )
};

(:~
 : scale()
 : Scale the regions by the given amount, but keep centered on a particular
 : point. Normally scaling will shift points closer to the origin. This
 : allows you do shift then closer to a different center of scaling instead,
 : so you can, for example, scale a circle about its own center, keeping it
 : in place.
 : 
 : @param $regions: regions to scale
 : @param $sx: how much to scale the point by in the x dimension
 : @param $sy: how much to scale the point by in the y dimension
 : @param $center: center of scaling
 :)
declare function this:scale(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale($region=>path:edges(), $sx, $sy, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale($region=>path:edges(), $sx, $sy, $center),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale($region=>ellipse:center(), $sx, $sy, $center),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale($region=>edge:arc-circle(), $sx, $sy, $center)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale($points[1], $sx, $sy, $center), affine:scale($points[2], $sx, $sy, $center),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy, $center),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region)
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale(solid:center($region), $sx, $sy, $center),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region)
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale($pt, $sx, $sy, $center)
          }
        )
      )
    },
    true()
  )
};

(:~
 : scale-in-place()
 : Scale using the center of the region as point of scaling.
 :)
declare function this:scale-in-place(
  $regions as map(xs:string,item()*)*,
  $x as xs:double,
  $y as xs:double
) as map(xs:string,item()*)*
{
  for $region in $regions return
  let $location := this:region-center($region)
  return 
    this:translate(
      this:scale($region, $x, $y),
      point:px($location) - $x*point:px($location),
      point:py($location) - $y*point:py($location)
    )
};

(:~
 : scale3D()
 : Scale the regions by the given amount, but keep centered on a particular
 : point. Normally scaling will shift points closer to the origin. This
 : allows you do shift then closer to a different center of scaling instead,
 : so you can, for example, scale a circle about its own center, keeping it
 : in place.
 : 
 : @param $regions: regions to scale
 : @param $sx: how much to scale the point by in the x dimension
 : @param $sy: how much to scale the point by in the y dimension
 : @param $sz: how much to scale the point by in the z dimension
 : @param $center: center of scaling
 :)
declare function this:scale3D(
  $regions as map(xs:string,item()*)*,
  $sx as xs:double,
  $sy as xs:double,
  $sz as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string, item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:scale3D($region=>path:edges(), $sx, $sy, $sz, $center),
          $region=>this:property-map()
        )
      )
      (: Ellipses need special handling: scale radii too :)
      case "ellipse" return (
        ellipse:ellipse(
          affine:scale3D($region=>ellipse:center(), $sx, $sy, $sz, $center),
          $region=>ellipse:rx() * $sx,
          $region=>ellipse:ry() * $sy,
          $region=>ellipse:rotation(),
          $region=>this:property-map()
        )
      )
      (: Arcs need special handling :)
      case "arc" return (
        let $points := $region=>edge:arc-ends()
        let $angles := $region=>edge:arc-angles()
        let $circle := this:scale3D($region=>edge:arc-circle(), $sx, $sy, $sz, $center)
        return (
          if (exists($points)) then (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                affine:scale3D($points[1], $sx, $sy, $sz, $center), affine:scale3D($points[2], $sx, $sy, $sz, $center),
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          ) else (
            util:merge-into(
              $region=>this:property-map(),
              edge:arc-by-angle(
                $circle=>ellipse:center(),
                $circle=>ellipse:radius(),
                $angles[1],
                $angles[2],
                $region=>edge:arc-flipped(),
                $region=>edge:arc-large()
              )
            )
          )
        )
      )
      (: Spheres need special handling: need to scale radius as well :)
      case "sphere" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz, $center),
            solid:radius($region) * $sx,
            solid:radius($region) * $sy,
            solid:radius($region) * $sz
          )
        )
      )
      case "ellipsoid" return (
        util:merge-into(
          $region,
          solid:ellipsoid(
            affine:scale3D(solid:center($region), $sx, $sy, $sz, $center),
            solid:rx($region) * $sx,
            solid:ry($region) * $sy,
            solid:rz($region) * $sz
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:scale3D($pt, $sx, $sy, $sz, $center)
          }
        )
      )
    },
    true()
  )
};

declare function this:scale-to-unit-circle(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  for $region in $regions return
  let $bounds := this:bounding-box($region)
  let $depth := max(this:vertices($region)!point:pz(.))
  return this:scale-to-unit-circle($region, $bounds=>map:put("depth",$depth))
};

declare function this:scale-to-unit-circle(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $width := box:width($canvas)
  let $height := box:height($canvas)
  let $depth := ($canvas("depth"),max(($width,$height)))[1]
  return this:scale3D($regions, 1 div $width, 1 div $height, 1 div $depth)
};

declare function this:scale-to-canvas(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $width := box:width($canvas)
  let $height := box:height($canvas)
  return this:scale($regions, $width, $height)
};

(:~
 : rotate()
 : Rotate the regions by the given amount.
 : 
 : @param $regions: regions to rotate
 : @param $degrees: how many degrees to rotate the region
 :)
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $degrees as xs:double
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $degrees),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $degrees),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $degrees)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $degrees,          
              $angles[2] - $degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $degrees),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $degrees),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $degrees)
          }
        )
      )
    },
    true()
  )
};

(:~
 : rotate()
 : Rotate the regions by the given amount.
 : 
 : @param $regions: regions to rotate
 : @param $roll-degrees: how many degrees to rotate the region forwards and back (x)
 : @param $pitch-degrees: how many degrees to rotate the region up and down (y)
 : @param $yaw-degrees: how many degrees to rotate the region left and right (z)
 :)
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $roll-degrees as xs:double,
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      (: but arcs are 2D, so this is just nonsense, really :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $roll-degrees, $pitch-degrees, $yaw-degrees)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $roll-degrees,          
              $angles[2] - $pitch-degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        (: More nonsense :)
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $roll-degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $roll-degrees, $pitch-degrees, $yaw-degrees)
          }
        )
      )
    },
    true()
  )
};

(:~
 : rotate()
 : Rotate the regions by the given amount around a center of rotation.
 : Normally rotation is relative to the origin.
 : 
 : @param $regions: regions to rotate
 : @param $degrees: how many degrees to rotate the region 
 : @param $center: center of rotation
 :)
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $degrees as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $degrees, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $degrees, $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $degrees, $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $degrees,          
              $angles[2] - $degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $degrees, $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $degrees, $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $degrees, $center)
          }
        )
      )
    },
    true()
  )
};

(:~
 : rotate()
 : Rotate the regions by the given amount around a center of rotation.
 : Normally rotation is relative to the origin.
 : 
 : @param $regions: regions to rotate
 : @param $roll-degrees: how many degrees to rotate the region forwards and back (x)
 : @param $pitch-degrees: how many degrees to rotate the region up and down (y)
 : @param $yaw-degrees: how many degrees to rotate the region left and right (z)
 : @param $center: center of rotation
 :)
declare function this:rotate(
  $regions as map(xs:string,item()*)*,
  $roll-degrees as xs:double,
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  (: Note: ellipsoid is wrong: we need to add rotation angles to support this :)
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:rotate($region=>path:edges(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      (: but arcs are 2D, so this is just nonsense, really :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:rotate($region=>edge:arc-circle(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] - $roll-degrees,          
              $angles[2] - $pitch-degrees,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        (: More nonsense :)
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:rotate($region=>ellipse:center(), $roll-degrees, $pitch-degrees, $yaw-degrees, $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + $roll-degrees,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:rotate($pt, $roll-degrees, $pitch-degrees, $yaw-degrees, $center)
          }
        )
      )
    },
    true()
  )
};

(:~
 : shear()
 : Shear the regions by the given amount.
 : You may wish to think of this as skewing angles instead: 
 : math:tan(util:radians($skew-degrees)) for $xy or $yx 
 : But note that tan approaches infinity near 90°/270°
 : 
 : @param $regions: regions to shear.
 : @param $xy: how much to shear x relative to y
 : @param $yx: how much to shear y relative to x
 :)
declare function this:shear(
  $regions as map(xs:string,item()*)*,
  $xy as xs:double,
  $yx as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:shear($pt, $xy, $yx)
    }
  )
};

(:~
 : shear()
 : Shear the regions by the given amount. 
 : You may wish to think of this as skewing angles instead: 
 : math:tan(math:radians($skew-degrees)) for $xy or $yx 
 : But note that tan approaches infinity near 90°/270°
 : 
 : @param $regions: regions to shear.
 : @param $xy: how much to shear x relative to y
 : @param $yx: how much to shear y relative to x
 : @param $xz: how much to shear x relative to z
 : @param $zx: how much to shear z relative to x
 : @param $yz: how much to shear y relative to z
 : @param $zy: how much to shear z relative to y
 :)
declare function this:shear(
  $regions as map(xs:string,item()*)*,
  $xy as xs:double,
  $yx as xs:double,
  $xz as xs:double,
  $zx as xs:double,
  $yz as xs:double,
  $zy as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:shear($pt, $xy, $yx, $xz, $zx, $yz, $zy)
    }
  )
};

(:~
 : reflect()
 : Reflect the regions across the center.
 : 
 : @param $regions: regions to reflect.
 : @param $center: center of reflection.
 :)
declare function this:reflect(
  $regions as map(xs:string,item()*)*,
  $center as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:reflect($region=>path:edges(), $center),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:reflect($region=>path:edges(), $center),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:reflect($region=>edge:arc-circle(), $center)
        let $angles := $arc=>edge:arc-angles()
        return (
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] + 180,
              $angles[2] + 180,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      case "ellipse" return (
        if (ellipse:is-circle($region)) then (
          ellipse:circle(
            affine:reflect($region=>ellipse:center(), $center),
            $region=>ellipse:radius(),
            $region=>this:property-map()
          )
        ) else (
          ellipse:ellipse(
            affine:reflect($region=>ellipse:center(), $center),
            $region=>ellipse:rx(),
            $region=>ellipse:ry(),
            $region=>ellipse:rotation() + 180,
            $region=>this:property-map()
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:reflect($pt, $center)
          }
        )
      )
    },
    true()
  )
};

(:~
 : reflect()
 : Reflect the regions across the line.
 : 
 : @param $regions: regions to reflect.
 : @param $start: start of reflection line
 : @param $end: end of reflection line
 :)
declare function this:reflect(
  $regions as map(xs:string,item()*)*,
  $start as map(xs:string,item()*),
  $end as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "polygon" return (
        path:polygon(
          this:reflect($region=>path:edges(), $start, $end),
          $region=>this:property-map()
        )
      )
      case "path" return (
        path:path(
          this:reflect($region=>path:edges(), $start, $end),
          $region=>this:property-map()
        )
      )
      (: arcs need special handling: convert to angle arc first :)
      case "arc" return (
        let $arc := edge:as-angle-arc($region)
        let $circle := this:reflect($region=>edge:arc-circle(), $start, $end)
        let $angles := $arc=>edge:arc-angles()
        return ( 
          util:merge-into(
            $region=>this:property-map(),
            edge:arc-by-angle(
              $circle=>ellipse:center(),
              $circle=>ellipse:radius(),
              $angles[1] + 180,
              $angles[2] + 180,
              $arc=>edge:arc-flipped(),
              $arc=>edge:arc-large()
            )
          )
        )
      )
      default return (
        this:mutate($region,
          function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
            affine:reflect($pt, $start, $end)
          }
        )
      )
    },
    true()
  )
};

(:~
 : perspective()
 : Perform a perspective transformation on the regions, rotated relative to 
 : camera angles. Generally speaking camera location is at negative or zero
 : Z and the display surface is at some positive Z.
 : Not, strictly speaking, an affine transformation, because of the remapping
 : of the points back to 2D.
 : 
 : @param $regions: regions to compute perspective on.
 : @param $camera: location of the camera
 : @param $roll-degrees: camera angle (x)
 : @param $pitch-degrees: camera angle (y)
 : @param $yaw-degrees: camera angle (z)
 : @param $display: display surface relative to camera
 :)
declare function this:perspective(
  $regions as map(xs:string,item()*)*,
  $camera as map(xs:string,item()*),
  $roll-degrees as xs:double, (: camera angles :)
  $pitch-degrees as xs:double,
  $yaw-degrees as xs:double,
  $display as map(xs:string,item()*) (: display surface relative to camera :)
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:perspective($pt, $camera, $roll-degrees, $pitch-degrees, $yaw-degrees, $display)
    }
  )
};

(:~
 : perspective()
 : Perform a perspective transformation on the regions. Generally speaking
 : camera location is at negative or zero Z and the display surface is at
 : some positive Z.
 : Not, strictly speaking, an affine transformation, because of the remapping
 : of the points back to 2D.
 : 
 : @param $regions: regions to compute perspective on.
 : @param $camera: location of the camera
 : @param $display: display surface relative to camera
 :)
declare function this:perspective(
  $regions as map(xs:string,item()*)*,
  $camera as map(xs:string,item()*),
  $display as map(xs:string,item()*) (: display surface relative to camera :)
) as map(xs:string,item()*)*
{
  this:perspective($regions, $camera, 0, 0, 0, $display)
};

(:~
 : projection()
 : Perform a simple projection of the regions. Like isometric but with different
 : angles. A little easier to control that full 3-point perspective.
 : Using ~(±45, 90) is generally good. Note: you get 3d "objects" out, with the z
 : axis flattened to 0. You'll need to draw using the default map to avoid an
 : additional automatic perspective shift.
 : 
 : @param $regions: regions to compute isometric projection on
 : @param $α: pitch degrees
 : @param $β: roll degrees
 :)
declare function this:projection(
  $regions as map(xs:string,item()*)*,
  $α as xs:double,
  $β as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:projection($pt, $α, $β)
    }
  )
};

(:~
 : isometric()
 : Perform a isometric projection of the regions.
 : Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
 : to draw using the default map to avoid an additional automatic perspective shift.
 : 
 : @param $regions: regions to compute isometric projection on
 : @param $positive: tilt up or down?
 :)
declare function this:isometric(
  $regions as map(xs:string,item()*)*,
  $positive as xs:boolean
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      affine:isometric($pt, $positive)
    }
  )
};

(:~
 : stereographic()
 : Stereographic projection from point on unit sphere to Cartesian plane.
 : The regions are translated and scaled to the unit sphere based on the
 : bounding sphere and then remapped and rescaled back after mapping.
 : This generally gives nicer results than using the raw coordinates.
 : Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
 : to draw using the default map to avoid an additional automatic perspective shift.
 : 
 : @param $solids 3D objects to map
 :)
declare function this:stereographic(
  $solids as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  let $bounding-sphere := this:bounding-sphere($solids)
  let $scale := solid:radius($bounding-sphere)
  let $offset := solid:center($bounding-sphere)
  let $neg-offset := point:minus($offset)
  return (
    $solids=>
    this:translate(
      point:px($neg-offset), point:py($neg-offset), point:pz($neg-offset)
    )=>
    this:scale(1 div $scale)=>
    this:mutate(coord:stereographic#1)=>
    this:scale($scale)=>
    this:translate(point:px($offset), point:py($offset))
  )
};

(:~
 : stereographic()
 : Stereographic projection from point on unit sphere to Cartesian plane.
 : The regions are translated to the origin for mapping, but not pre-scaled.
 : The results are centered and scaled to the canvas. 
 : Note: you get 3d "objects" out, with the z axis flattened to 0. You'll need
 : to draw using the default map to avoid an additional automatic perspective shift.
 : 
 : @param $solids 3D objects to map
 : @param $canvas canvas space to scale and center to
 :)
declare function this:stereographic(
  $solids as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $bounding-sphere := this:bounding-sphere($solids)
  let $offset := solid:center($bounding-sphere)
  let $neg-offset := point:minus($offset)
  let $raw := (
    $solids=>
    this:translate(
      point:px($neg-offset), point:py($neg-offset), point:pz($neg-offset)
    )=>
    this:mutate(coord:stereographic#1)
  )
  let $bb := this:bounding-box(this:vertices($raw))
  let $scaling :=
    min((
      box:width($canvas) div box:width($bb),
      box:height($canvas) div box:height($bb)
    ))
  return (
    $raw=>
    this:scale($scaling)=>
    this:translate(point:px($offset), point:py($offset))
  )
};

(:~
 : stereographic3D()
 : Inverse stereographic projection from Cartesian plane to unit sphere.
 : The regions are pre-translated to the origin and then the results
 : are scaled and centered to the canvas.
 : 
 : @param $regions 2D regions to map
 : @param $canvas canvas space to scale and center to
 :)
declare function this:stereographic3D(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  let $ccenter := box:center($canvas)
  let $raw :=
    $regions=>
      this:translate(-point:px($ccenter), -point:py($ccenter))=>
      this:mutate(coord:stereographic3D#1)
  let $bb := this:bounding-box(this:vertices($raw))
  let $center := box:center($bb)
  let $scaling :=
    min((
      box:width($canvas) div box:width($bb),
      box:height($canvas) div box:height($bb)
    ))
  return (
    $raw=>
      this:translate(-point:px($center), -point:py($center))=>
      this:scale($scaling)=>
      this:translate(point:px($ccenter), point:py($ccenter))
  )
};

(:~
 : apply-transforms()
 : Apply the transformations to the object. This is if we are going to be doing
 : operations on the slot, like intersections etc. In general none of the
 : operations apply transforms in advance. Look for a transform property or if
 : this is a lot, the various slot transformation properties.
 :)
declare function this:apply-transforms($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  let $ok := function ($attr as xs:string) as xs:boolean {
    matches($attr, "^[ ]*matrix[ ]*[(][ 1234567890,.-]+[)][ ]*$")=>trace("ok "||$attr)
  }
  for $region in $regions return 
  switch (this:kind($region))
  case "slot" return (
    slot:slot(
      let $location := slot:location($region)
      let $rotation := slot:rotation($region)
      let $center := slot:center($region)
      let $scale := slot:scale($region)
      let $skew := slot:skew($region)
      let $transform := transform:transform()
      let $transform :=
        if (exists($location))
        then $transform=>transform:translate(point:px($location), point:py($location))
        else $transform
      let $transform :=
        if (exists($scale)) then (
          if (exists($center))
          then (
            $transform=>
              transform:translate(-point:px($center), -point:py($center))=>
              transform:scale(point:px($scale), point:py($scale))=>
              transform:translate(point:px($center), point:py($center))
          ) else $transform=>transform:scale(point:px($scale), point:py($scale))
        ) else $transform
      let $transform :=
        if (exists($rotation)) then (
          if (exists($center))
          then $transform=>transform:rotate($center, $rotation)
          else $transform=>transform:rotate($rotation)
        )
        else $transform
      let $transform :=
        if (exists($skew))
        then $transform=>transform:skew(point:px($skew), point:py($skew))
        else $transform
      return (
        if (exists($region("transform")) and $ok($region("transform"))) then (
          this:apply-transforms($region=>slot:body())=>
            this:apply-matrix2(transform:parse-matrix($region("transform")))=>
            map:remove("transform")
        ) else (
          this:apply-transforms($region=>slot:body())
        )
      ),
      slot:property-map($region)
    )
  )
  case "wrapper" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>wrapper:body(
        this:apply-transforms($region=>wrapper:body())
      )=>this:apply-matrix2(transform:parse-matrix($region("transform")))=>
         map:remove("transform")
    ) else (
      $region=>wrapper:body(this:apply-transforms($region=>wrapper:body()))
    )
  )
  case "mask" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>mask:body(
        this:apply-transforms($region=>mask:body())=>
          this:apply-matrix2(transform:parse-matrix($region("transform")))=>
          map:remove("transform")
      )
    ) else (
      $region=>mask:body(this:apply-transforms($region=>mask:body()))
    )
  )
  case "reach" return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>reach:body(
        this:apply-transforms($region=>reach:body())=>
          this:apply-matrix2(transform:parse-matrix($region("transform")))=>
          map:remove("transform")
      )
    ) else (
      $region=>reach:body(this:apply-transforms($region=>reach:body()))
    )
  )
  default return (
    if (exists($region("transform")) and $ok($region("transform"))) then (
      $region=>this:apply-matrix2(transform:parse-matrix($region("transform")))=>
         map:remove("transform")
    ) else (
      $region
    )
  )
};

(:====================================================================== 
 : Distance metrics
 :======================================================================:)

declare %art:deprecated function this:distance($p1 as map(xs:string,item()*), $p2 as map(xs:string,item()*)) as xs:double
{
  point:distance($p1, $p2)
};

declare %art:deprecated function this:polar-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:polar-distance($p, $q)
};

declare %art:deprecated function this:taxi-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:taxi-distance($p, $q)
};

declare %art:deprecated function this:avg-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:avg-distance($p, $q)
};

declare %art:deprecated function this:rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:rail-distance($p, $q)
};

declare %art:deprecated function this:rail-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $origin as map(xs:string,item()*)) as xs:double
{
  point:rail-distance($p, $q, $origin)
};

declare %art:deprecated function this:chebyshev-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:chebyshev-distance($p, $q)
};

(: $order=p; p=1=>taxi; p=2=>Euclidean; p=>∞=>Chebyshev; p<1 not a metric 
   unless remove final pow to 1 div $order
 :)
declare %art:deprecated function this:minkowski-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*), $order as xs:double) as xs:double
{
  point:minkowski-distance($p, $q, $order)
};

(: 
  similarity = (A·B)/(∥A∥∥B∥) = ΣAiBi/(√Ai²√Bi²)
  as a metric = acos(similarity)/π => [0,1]
 :)
declare %art:deprecated function this:cosine-similarity($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:cosine-similarity($p, $q)
};

declare %art:deprecated function this:cosine-distance($p as map(xs:string,item()*), $q as map(xs:string,item()*)) as xs:double
{
  point:cosine-distance($p, $q)
};

(:~ 
 : shortest-distance()
 : Distance from a point to a region. Warning: for curves and most solids this
 : is using SDF conversion, which is not terribly efficient if you are going
 : to be comparing a lot of points to the region. You'd be better off doing
 : the conversion once and running the SDF function yourself. 
 : Only some of the solids are supported right now.
 : Returns 0 for interior points.
 :)
declare function this:shortest-distance(
  $pt as map(xs:string,item()*),
  $region as map(xs:string,item()*)
) as xs:double
{
  switch (this:kind($region))
  case "point" return this:distance($pt, $region)
  case "box" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "block" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "space" return (
    if (box:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "edge" return (
    edge:point-distance($region, $pt)
  )
  case "arc" return (
    let $f := sdf:toSDF($region, map {"width":0})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "quad" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "cubic" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "path" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (path:polygon-contains-point($region,$pt)) then 0
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "complex-polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (cpoly:polygon-contains-point($region,$pt)) then 0
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "ellipse" return (
    if (ellipse:is-circle($region)) then (
      max((0, point:distance(ellipse:center($region), $pt) - ellipse:radius($region)))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return max((0, $f(point:pcoordinates($pt))))
    )
  )
  case "sphere" return (
    max((0, point:distance(solid:center($region), $pt) - solid:radius($region)))
  )
  case "ellipsoid" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "tetrahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  case "cube" return (
    if (solid:contains-point($region,$pt)) then 0
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "octahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return max((0, $f(point:pcoordinates($pt))))
  )
  default return errors:error("GEOM-BADREGION", ($region, "shortest-distance"))
};

(:~ 
 : signed-distance()
 : Distance from a point to a region. Warning: for curves and solids this is
 : using SDF conversion, which is not terribly efficient if you are going to be
 : comparing a lot of points to the region. You'd be better off doing the
 : conversion once and running the SDF function yourself.
 : Only some of the solids are supported right now.
 : Will return negative values for interior points; 0 for those on the boundary.
 :)
declare function this:signed-distance(
  $pt as map(xs:string,item()*),
  $region as map(xs:string,item()*)
) as xs:double
{
  switch (this:kind($region))
  case "point" return this:distance($pt, $region)
  case "box" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "block" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "space" return (
    if (box:contains-point($region,$pt))
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "edge" return (
    if (point:same(edge:start($region), edge:end($region))) then (
      this:distance($pt, edge:start($region))
    ) else (
      let $v := edge:start($region)
      let $w := edge:end($region)
      let $l := edge:linear-length($region)
      let $lsquared := $l*$l
      let $w_sub_v := point:sub($w, $v)
      let $t := util:clamp(point:dot(point:sub($pt, $v), $w_sub_v) div $lsquared, 0, 1)
      return (
        this:distance($pt,
          point:point(
            point:px($v) + $t * point:px($w_sub_v),
            point:py($v) + $t * point:py($w_sub_v)
          )
        )
      )
    )
  )
  case "arc" return (
    let $f := sdf:toSDF($region, map {"width":0})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "quad" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "cubic" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "path" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (path:polygon-contains-point($region,$pt))
      then -min(this:edges($region)!this:shortest-distance($pt, .))
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "complex-polygon" return (
    if (every $edge in this:edges($region) satisfies this:kind($edge)="edge") then (
      if (cpoly:polygon-contains-point($region,$pt))
      then -min(this:edges($region)!this:shortest-distance($pt, .))
      else min(this:edges($region)!this:shortest-distance($pt, .))
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  )
  case "ellipse" return
    if (ellipse:is-circle($region)) then (
      this:distance(ellipse:center($region), $pt) - ellipse:radius($region)
    ) else (
      let $f := sdf:toSDF($region, map {})=>f:function()
      return $f(point:pcoordinates($pt))
    )
  case "sphere" return (
    point:distance(solid:center($region), $pt) - solid:radius($region)
  )
  case "ellipsoid" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "tetrahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  case "cube" return (
    if (solid:contains-point($region,$pt)) 
    then -min(this:edges($region)!this:shortest-distance($pt, .))
    else min(this:edges($region)!this:shortest-distance($pt, .))
  )
  case "octahedron" return (
    let $f := sdf:toSDF($region, map {})=>f:function()
    return $f(point:pcoordinates($pt))
  )
  default return errors:error("GEOM-BADREGION", ($region, "signed-distance"))
};

(:======================================================================
 : Generic operations
 :======================================================================:)

declare function this:snap($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:snap($region)
      case "box" return box:snap($region)
      case "block" return box:snap($region)
      case "space" return box:snap($region)
      case "edge" return edge:snap($region)
      case "arc" return edge:snap($region)
      case "ellipse-arc" return edge:snap($region)
      case "quad" return edge:snap($region)
      case "cubic" return edge:snap($region)
      case "path" return path:snap($region)
      case "polygon" return path:snap($region)
      case "graph" return graph:snap($region)
      case "ellipse" return ellipse:snap($region)
      case "sphere" return solid:snap($region)
      case "ellipsoid" return solid:snap($region)
      case "face" return solid:snap($region)
      case "tetrahedron" return solid:snap($region)
      case "cube" return solid:snap($region)
      case "octahedron" return solid:snap($region)
      case "icosahedron" return solid:snap($region)
      case "dodecahedron" return solid:snap($region)
      case "polyhedron" return solid:snap($region)
      default return $region
    },
    true()
  )
};

declare function this:decimal(
  $regions as map(xs:string,item()*)*,
  $digits as xs:integer
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:decimal($region, $digits)
      case "box" return box:decimal($region, $digits)
      case "block" return box:decimal($region, $digits)
      case "space" return box:decimal($region, $digits)
      case "edge" return edge:decimal($region, $digits)
      case "arc" return edge:decimal($region, $digits)
      case "ellipse-arc" return edge:decimal($region, $digits)
      case "quad" return edge:decimal($region, $digits)
      case "cubic" return edge:decimal($region, $digits)
      case "path" return path:decimal($region, $digits)
      case "polygon" return path:decimal($region, $digits)
      case "graph" return graph:decimal($region, $digits)
      case "ellipse" return ellipse:decimal($region, $digits)
      case "sphere" return solid:decimal($region, $digits)
      case "ellipsoid" return solid:decimal($region, $digits)
      case "face" return solid:decimal($region, $digits)
      case "tetrahedron" return solid:decimal($region, $digits)
      case "cube" return solid:decimal($region, $digits)
      case "octahedron" return solid:decimal($region, $digits)
      case "icosahedron" return solid:decimal($region, $digits)
      case "dodecahedron" return solid:decimal($region, $digits)
      case "polyhedron" return solid:decimal($region, $digits)
      default return $region
    },
    true()
  )
};

declare function this:quote(
  $regions as item()*
) as xs:string
{
  string-join(
    for $region in $regions return typeswitch($region)
    case map(*) return switch(this:kind($region))
      case "point" return point:quote($region)
      case "box" return box:quote($region)
      case "block" return box:quote($region)
      case "space" return box:quote($region)
      case "edge" return edge:quote($region)
      case "arc" return edge:quote($region)
      case "ellipse-arc" return edge:quote($region)
      case "quad" return edge:quote($region)
      case "cubic" return edge:quote($region)
      case "path" return path:quote($region)
      case "polygon" return path:quote($region)
      case "complex-polygon" return cpoly:quote($region)
      case "graph" return graph:quote($region)
      case "ellipse" return ellipse:quote($region)
      case "sphere" return solid:quote($region)
      case "ellipsoid" return solid:quote($region)
      case "face" return solid:quote($region)
      case "tetrahedron" return solid:quote($region)
      case "cube" return solid:quote($region)
      case "octahedron" return solid:quote($region)
      case "icosahedron" return solid:quote($region)
      case "dodecahedron" return solid:quote($region)
      case "polyhedron" return solid:quote($region)
      default return errors:quote($region)
    default return errors:quote($region)
    ,
    " + "
  )
};

declare function this:same(
  $this as map(xs:string,item()*),
  $other as map(xs:string,item()*)
) as xs:boolean
{
  (this:kind($this)=this:kind($other) and (
    switch (this:kind($this))
    case "point" return point:same($this, $other)
    case "box" return box:same($this, $other)
    case "block" return box:same($this, $other)
    case "space" return box:same($this, $other)
    case "edge" return edge:same($this, $other)
    case "arc" return edge:same($this, $other)
    case "ellipse-arc" return edge:same($this, $other)
    case "quad" return edge:same($this, $other)
    case "cubic" return edge:same($this, $other)
    case "path" return path:same($this, $other)
    case "polygon" return path:same($this, $other)
    case "complex-polygon" return cpoly:same($this, $other)
    case "graph" return graph:same($this, $other)
    case "ellipse" return ellipse:same($this, $other)
    case "sphere" return solid:same($this, $other)
    case "ellipsoid" return solid:same($this, $other)
    case "face" return solid:same($this, $other)
    case "tetrahedron" return solid:same($this, $other)
    case "cube" return solid:same($this, $other)
    case "octahedron" return solid:same($this, $other)
    case "icosahedron" return solid:same($this, $other)
    case "dodecahedron" return solid:same($this, $other)
    case "polyhedron" return solid:same($this, $other)
    default return deep-equal($this,$other)
  )) or (
    (: edge case: degenerate complex polygons :)
    this:kind($this)=("complex-polygon","polygon") and
    this:kind($other)=("complex-polygon","polygon") and
    cpoly:same($this, $other)
  )
};

declare function this:mutate(
  $regions as map(xs:string,item()*)*,
  $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*)
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function ($region as map(xs:string,item()*)) as map(xs:string,item()*) {
      switch(this:kind($region))
      case "point" return point:mutate($region, $mutation)
      case "box" return box:mutate($region, $mutation)
      case "block" return box:mutate($region, $mutation)
      case "space" return box:mutate($region, $mutation)
      case "edge" return edge:mutate($region, $mutation)
      case "arc" return edge:mutate($region, $mutation)
      case "ellipse-arc" return edge:mutate($region, $mutation)
      case "quad" return edge:mutate($region, $mutation)
      case "cubic" return edge:mutate($region, $mutation)
      case "path" return path:mutate($region, $mutation)
      case "polygon" return path:mutate($region, $mutation)
      case "graph" return graph:mutate($region, $mutation)
      case "ellipse" return ellipse:mutate($region, $mutation)
      case "sphere" return solid:mutate($region, $mutation)
      case "ellipsoid" return solid:mutate($region, $mutation)
      case "face" return solid:mutate($region, $mutation)
      case "tetrahedron" return solid:mutate($region, $mutation)
      case "cube" return solid:mutate($region, $mutation)
      case "octahedron" return solid:mutate($region, $mutation)
      case "icosahedron" return solid:mutate($region, $mutation)
      case "dodecahedron" return solid:mutate($region, $mutation)
      case "polyhedron" return solid:mutate($region, $mutation)
      default return $region
    },
    true()
  )
};

(:~
 : project()
 : Simple 3-point projection of regions. Does not account for stroke widths.
 : You will almost certainly need to map from canvas space to the unit circle
 : and back.
 :)
declare function this:project(
  $regions as map(xs:string,item()*)*,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $x := point:px($pt)
      let $y := point:py($pt)
      let $z := point:pz($pt)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return point:point($x div $d, $y div $d)
    }
  )
};

(:~
 : project()
 : Simple 3-point projection of regions. Does not account for stroke widths.
 : Does the mapping from canvas space to the unit circle and back.
 : @param $canvas should have a "depth" property
 :)
declare function this:project(
  $regions as map(xs:string,item()*)*,
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:mutate($regions,
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $scaled := this:scale-to-unit-circle($pt, $canvas)
      let $x := point:px($scaled)
      let $y := point:py($scaled)
      let $z := point:pz($scaled)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return this:scale-to-canvas(point:point($x div $d, $y div $d), $canvas)
    }
  )
};

(:~ 
 : project-strokes()
 : Project regions. Converts simple strokes into closed paths with the
 : stroke width projected as well. To do this certain circles and ellipses
 : need to be interpolated to polygons: tolerance says how finely that
 : interpolation is done. 
 : You will almost certainly need to map from canvas space to the unit circle
 : and back.
 : Also note that this will turn polygons into a set of polygons (one 
 : per edge), so if you wanted it filled you're out of luck. And if you
 : wanted it filled, why are you projecting stroke width in the first place?
 : Just project it and fill it. 
 :)
declare function this:project-strokes(
  $regions as map(xs:string,item()*)*,
  $default-stroke-width as xs:integer,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double,
  $tolerance as xs:double
) as map(xs:string,item()*)*
{
  let $hypoteneuse :=
    util:round(math:sqrt(2*$default-stroke-width*$default-stroke-width))
  let $projection := 
    function ($pt as map(xs:string,item()*)) as map(xs:string,item()*) {
      let $x := point:px($pt)
      let $y := point:py($pt)
      let $z := point:pz($pt)
      let $d := $p * $x + $q * $y + $r * $z + 1
      return point:point($x div $d, $y div $d)
    }
  for $region in $regions
  let $kind := this:kind($region)
  return (
    if ($kind="point") then (
      point:mutate($region, $projection)
    ) else if ($kind=("edge","quad","cubic")) then (
      let $leading := edge:mutate($region, $projection)
      let $trailing := edge:mutate(this:translate(edge:reverse($region), $hypoteneuse, $hypoteneuse), $projection)
      return 
        path:polygon(($leading, edge:edge(edge:end($leading),edge:start($trailing)),$trailing))
    ) else if ($kind="arc") then (
      (: n interpolations : length / tolerance :)
      let $n := edge:arc-length($region) idiv $tolerance
      let $edges := edge:interpolate-arc($n, $region, false())
      for $edge in $edges return (
        this:project-strokes(
          $edge, $default-stroke-width, $p, $q, $r, $tolerance
        )
      )
    ) else if ($kind="ellipse") then (
      (: n interpolations : perimeter / tolerance :)
      let $n := ellipse:perimeter($region) idiv $tolerance
      let $edges := ellipse:interpolate($n, $region)
      for $edge in $edges return (
        this:project-strokes(
          $edge, $default-stroke-width, $p, $q, $r, $tolerance
        )
      )
    ) else (
      let $edges := this:edges($region) return (
        for $edge in $edges return (
          this:project-strokes(
            $edge, $default-stroke-width, $p, $q, $r, $tolerance
          )
        ),
        for $edge at $i in tail($edges)
        where not(point:same(edge:end($edges[$i]), edge:start($edge)))
        return (
          this:project-strokes(
            edge:edge(edge:end($edges[$i]), edge:start($edge)),
            $default-stroke-width, $p, $q, $r, $tolerance
          )
        ),
        if ($kind="polygon") then (
          if (not(point:same(edge:end($edges[last()]), edge:start($edges[1])))) then (
            this:project-strokes(
              edge:edge(edge:end($edges[last()]), edge:start($edges[1])),
              $default-stroke-width, $p, $q, $r, $tolerance
            )
          ) else ()
        ) else ()
      )
    )
  )
};

declare function this:project-strokes(
  $regions as map(xs:string,item()*)*,
  $default-stroke-width as xs:integer,
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,item()*)*
{
  this:project-strokes($regions, $default-stroke-width, $p, $q, $r, 1)
};

declare function this:area($regions as map(xs:string,item()*)*) as xs:double
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "box" return box:area($region)
    case "block" return solid:surface-area($region)
    case "space" return box:area($region)
    case "polygon" return path:area($region)
    case "complex-polygon" return cpoly:area($region)
    case "ellipse" return ellipse:area($region)
    case "slot" return this:area($region=>slot:body())
    case "wrapper" return this:area($region=>wrapper:body())
    case "mask" return this:area($region=>mask:body())
    case "reach" return this:area($region=>reach:body())
    case "sphere" return solid:surface-area($region)
    case "ellipsoid" return solid:surface-area($region)
    case "face" return solid:surface-area($region)
    case "tetrahedron" return solid:surface-area($region)
    case "cube" return solid:surface-area($region)
    case "octahedron" return solid:surface-area($region)
    case "icosahedron" return solid:surface-area($region)
    case "dodecahedron" return solid:surface-area($region)
    case "polyhedron" return solid:surface-area($region)
    default return 0
  )
};

declare function this:length($regions as map(xs:string,item()*)*) as xs:double
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "box" return sum(this:edges($region)!edge:length(.))
    case "space" return sum(this:edges($region)!edge:length(.))
    case "polygon" return path:length($region)
    case "complex-polygon" return cpoly:length($region)
    case "path" return path:length($region)
    case "ellipse" return ellipse:perimeter($region)
    case "arc" return edge:length($region)
    case "quad" return edge:length($region)
    case "cubic" return edge:length($region)
    case "edge" return edge:length($region)
    case "slot" return this:length($region=>slot:body())
    case "wrapper" return this:length($region=>wrapper:body())
    case "mask" return this:length($region=>mask:body())
    case "reach" return this:length($region=>reach:body())
    default return 0
  )
};

declare function this:n-faces($regions as map(xs:string,item()*)*) as xs:integer
{
  sum (
    for $region in $regions return switch(this:kind($region))
    case "slot" return this:n-faces($region=>slot:body())
    case "wrapper" return this:n-faces($region=>wrapper:body())
    case "mask" return this:n-faces($region=>mask:body())
    case "reach" return this:n-faces($region=>reach:body())
    default return solid:n-faces($region)
  ) cast as xs:integer
};

(:======================================================================
 : Angles, destinations, etc.
 :======================================================================:)
 
(:~
 : path-angle()
 : Compute the angle (azimuth) from one point to the next, in degrees
 : Return 0 if points are the same
 :
 : @param $last: previous point; use (0,0) if no previous
 : @param $curr: current point
 :)
declare function this:path-angle($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
{
  point:angle($last, $curr)
};

(:~
 : path-inclination()
 : Compute the inclination angle from one point in the next, in degrees
 : Return 0 if the points are the same
 :
 :
 : @param $last: previous point; use (0,0,0) if no previous
 : @param $curr: current point
 :)
declare function this:path-inclination($last as map(xs:string,item()*)?, $curr as map(xs:string,item()*)) as xs:double
{
  point:inclination($last, $curr)
};

(:~
 : path-length()
 : Compute total length of travel along a path. If path includes cubic
 : edges the result is approximated.
 :
 : @param $path: the path
 :)
declare function this:path-length($path as map(xs:string,item()*)) as xs:double
{
  path:length($path)
};

(:~
 : destination()
 : Point at a particular distance and direction.
 : 
 : @param $point: starting point
 : @param $degrees: bearing from point
 : @param $length: how far along bearing to travel
 :) 
declare function this:destination(
  $point as map(xs:string,item()*),
  $degrees as xs:double,
  $length as xs:double
) as map(xs:string,item()*)
{
  point:destination($point, $degrees, $length)
};

(:~
 : destination()
 : Point (3D) at a particular distance, azimuth, and inclination.
 : 
 : @param $point: starting point
 : @param $azimuth_degrees: angle of azimuth from point
 : @param $inclination_degrees: angle of inclination from point
 : @param $length: how far along bearing to travel
 :) 
declare function this:destination(
  $point as map(xs:string,item()*),
  $azimuth_degrees as xs:double,
  $inclination_degrees as xs:double,
  $length as xs:double
) as map(xs:string,item()*)
{
  point:destination($point, $azimuth_degrees, $inclination_degrees, $length)
};

(:====================================================================== 
 : Centers, midpoints
 :====================================================================== :)

(:~
 : region-center()
 : Approximate center of region. A rough approximation for polygons with
 : curved edges. 2D
 :)
declare function this:region-center(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  (: Sometimes we set the center we know/care about :)
  if ($region=>map:contains("center")) then $region("center")
  else
  switch (this:kind($region))
  case "point" return $region
  case "box" return $region=>box:center()
  case "block" return $region=>box:center()
  case "space" return $region=>box:center()
  case "path" return (
    let $n := count(this:edges($region))
    return switch ($n)
    case 0 return $point:ORIGIN (: GIGO :)
    case 1 return this:edges($region)[1]=>edge:edge-point(0.5)
    default return $region=>path:path-point(0.5)
  )
  case "polygon" return $region=>path:polygon-center()
  case "complex-polygon" return $region=>cpoly:polygon-center()
  case "arc" return $region=>edge:edge-point(0.5)
  case "edge" return $region=>edge:edge-point(0.5)
  case "quad" return $region=>edge:edge-point(0.5)
  case "cubic" return $region=>edge:edge-point(0.5)
  case "ellipse" return $region=>ellipse:center()
  case "sphere" return $region=>solid:center()
  case "ellipsoid" return $region=>solid:center()
  case "face" return $region=>solid:center()
  case "tetrahedron" return $region=>solid:center()
  case "cube" return $region=>solid:center()
  case "octahedron" return $region=>solid:center()
  case "icosahedron" return $region=>solid:center()
  case "dodecahedron" return $region=>solid:center()
  case "polyhedron" return $region=>solid:center()
  case "slot" return
    point:centroid(
      for $sub in $region=>slot:body() return this:region-center($sub)
    )
  case "wrapper" return
    point:centroid(
      for $sub in $region=>wrapper:body() return this:region-center($sub)
    )
  case "mask" return
    point:centroid(
      for $sub in $region=>mask:body() return this:region-center($sub)
    )
  case "reach" return
    point:centroid(
      for $sub in $region=>reach:body() return this:region-center($sub)
    )
  default return errors:error("GEOM-BADREGION", ($region, "region-center"))
};

(:======================================================================:
 : Interpolations
 : In general interpolate() is edge-by-edge
 : If you want to get a set of interpolated points over a path as a
 : whole use path-point() over a set of $t values, with an augmented
 : path (path:with-edge-ts()) for efficiency
 : Return points
 :======================================================================:)

declare function this:interpolate-using(
  $regions as map(xs:string,item()*)*,
  $divisions as function(item()) as xs:double*
) as map(xs:string,item()*)*
{
  this:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      switch ($region=>map:get("kind"))
      case "edge" return edge:interpolate-edge-using($region, $divisions)
      case "arc" return edge:interpolate-arc-using($region, $divisions)
      case "quad" return edge:interpolate-quadratic-using($region, $divisions)
      case "cubic" return edge:interpolate-cubic-using($region, $divisions)
      case "path" return this:interpolate-using($region=>this:edges(), $divisions)
      case "polygon" return this:interpolate-using($region=>this:edges(), $divisions)
      case "graph" return this:interpolate-using($region=>this:edges(), $divisions)
      case "box" return this:interpolate-using($region=>this:edges(), $divisions)
      case "space" return this:interpolate-using($region=>this:edges(), $divisions)
      case "ellipse" return ellipse:interpolate-using($region, $divisions)
      case "face" return this:interpolate-using($region=>solid:polygon(), $divisions)
      default return $region
    },
    false()
  )
};

(:~
 : interpolate()
 : Interpolate the edges of the region; return the interpolated points.
 : Note: when interpolating paths or polygons, exclusivity is still edge-wise
 : so with exclusive interpolation you'll lose the last point and with 
 : inclusive interpolation you'll end up with duplicate points because end
 : of one edge is the start of the next edge. Exclusivity doesn't apply to
 : ellipse interpolation. (Zen koan: what is the end of a closed loop?)
 :
 : @param $n: number of points of interpolation for each edge
 : @param $regions: the set of regions
 : @param $exclusive: include end point? default=false
 :)
declare function this:interpolate(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*,
  $exclusive as xs:boolean
) as map(xs:string,item()*)*
{
  this:interpolate-using($regions,
    if ($exclusive) then (
      function ($region as map(xs:string,item()*)) as xs:double* {
        util:linspace($n, 0, 1, not(this:kind($region)="ellipse"))
      }
    ) else (
      function ($region as map(xs:string,item()*)) as xs:double* {
        util:linspace($n, 0, 1)
      }
    )
  )
};

declare function this:interpolate(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  this:interpolate($n, $regions, false())
};

declare %art:non-deterministic function this:interpolate-jittered(
  $n as xs:integer,
  $regions as map(xs:string,item()*)*,
  $jitter-percent as xs:integer
) as map(xs:string,item()*)*
{
  this:interpolate-using($regions,
    function ($region as map(xs:string,item()*)) as xs:double* {
      let $even-divisions := util:linspace($n, 0, 1)
      let $jitter :=
        if ($jitter-percent > 0)
        then dist:uniform(1.0 - $jitter-percent div 100, 1.0)
        else dist:constant(1.0)
      let $js := rand:randomize($n, $jitter)
      for $t at $i in $even-divisions return $t * $js[$i] 
    }
  )
};


declare function this:region-point(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)*
{
  switch(this:kind($region))
  case "point" return $region
  case "edge" return edge:edge-point($region, $t)
  case "arc" return edge:edge-point($region, $t)
  case "ellipse-arc" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "ellipse-arc"))
  case "quad" return edge:edge-point($region, $t)
  case "cubic" return edge:edge-point($region, $t)
  case "path" return path:path-point($region, $t)
  case "polygon" return path:path-point($region, $t)
  case "complex-polygon" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "complex-polygon"))
  case "graph" return errors:error("GEOM-NOTIMPLEMENTED", ("region-point", "graph"))
  case "ellipse" return ellipse:ellipse-point($region, $t)
  case "slot" return this:region-point($region=>slot:body(), $t)
  case "wrapper" return this:region-point($region=>wrapper:body(), $t)
  case "mask" return this:region-point($region=>mask:body(), $t)
  case "reach" return this:region-point($region=>reach:body(), $t)
  default return errors:error("GEOM-BADREGION", ($region, "region-point"))
};

(:~
 : tangent()
 : Return the tangent vector to the given point on the edge (2D)
 :)
declare function this:tangent(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  switch(this:kind($region))
  case "edge" return edge:tangent($region, $t)
  case "quad" return edge:tangent($region, $t)
  case "cubic" return edge:tangent($region, $t)
  case "arc" return edge:tangent($region, $t)
  case "ellipse-arc" return edge:tangent($region, $t)
  case "path" return path:tangent($region, $t)
  case "polygon" return path:tangent($region, $t)
  case "ellipse" return edge:tangent($region, $t) 
  case "box" return this:tangent(this:as-polygon($region), $t)
  case "space" return this:tangent(this:as-polygon($region), $t)
  case "face" return this:tangent(this:as-polygon($region), $t)
  default return errors:error("GEOM-NOTIMPLEMENTED", ("tangent", this:kind($region)))
};

(:~
 : normal()
 : Return the normal vector to the given point on the edge/path (2D)
 :)
declare function this:normal(
  $region as map(xs:string,item()*),
  $t as xs:double
) as map(xs:string,item()*)
{
  this:tangent($region, $t)=>point:perpendicular()
};

declare function this:tangent-angle(
  $region as map(xs:string,item()*),
  $t as xs:double
) as xs:double
{
  if (this:kind($region)="ellipse") then ellipse:tangent-angle($region, $t)
  else point:angle($point:ORIGIN2D, this:tangent($region, $t))
};

declare function this:normal-angle(
  $region as map(xs:string,item()*),
  $t as xs:double
) as xs:double
{
  this:tangent-angle($region, $t) + 90
};

(:~ 
 : random-points()
 : Generate N random points using the given algorithms.
 :
 : @param $N: How many points to return
 : @param $last: Previous value (needed for markov, available to key callback)
 : @param $x The algorithm to use for x coordinates, a map with various parameter keys
 : @param $y The algorithm to use for y coordinates, a map with various parameter keys
 :)
declare %art:non-deterministic function this:random-points($N as xs:integer, $last as map(xs:string,item()*)?, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*
{
  (: XYZZY we'd really like to be able to apply algorithms to $last as a point :)
  let $xs := rand:randomize($N, if (empty($last)) then () else point:px($last), $x)
  let $ys := rand:randomize($N, if (empty($last)) then () else point:py($last), $y)
  for $i in 1 to $N
  return point:point($xs[$i], $ys[$i])
};

(:~ 
 : random-points()
 : Generate N random points using the given algorithms.
 :
 : @param $N: How many points to return
 : @param $x The algorithm to use for x coordinates, a map with various parameter keys
 : @param $y The algorithm to use for y coordinates, a map with various parameter keys
 :)
declare %art:non-deterministic function this:random-points($N as xs:integer, $x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)*
{
   this:random-points($N, (), $x, $y)
};

(:~ 
 : random-point()
 : Generate a random point using the given algorithms.
 :
 : @param $x The algorithm to use for x coordinates, a map with various parameter keys
 : @param $y The algorithm to use for y coordinates, a map with various parameter keys
 :)
declare %art:non-deterministic function this:random-point($x as map(xs:string, item()*), $y as map(xs:string, item()*)) as map(xs:string,item()*)
{
   this:random-points(1, (), $x, $y)
};

(:~ 
 : random-points-in()
 : Generate N random points within the canvas (uniform distribution)
 :
 : @param $N: How many points to return
 : @param $canvas: canvas (space) with height/width/edge parameters
 :)
declare %art:non-deterministic function this:random-points-in($N as xs:integer, $canvas as map(xs:string,item()*)) as map(xs:string,item()*)*
{
  let $edge := box:edge($canvas)
  let $min-x := box:min-x($canvas)
  let $max-x := box:max-x($canvas)
  let $min-y := box:min-y($canvas)
  let $max-y := box:max-y($canvas)
  let $x := dist:uniform($min-x + $edge, $max-x - $edge)=>dist:cast("integer")
  let $y := dist:uniform($min-y + $edge, $max-y - $edge)=>dist:cast("integer")
  let $xs := rand:randomize($N, (), $x)
  let $ys := rand:randomize($N, (), $y)
  for $i in 1 to $N
  return point:point($xs[$i], $ys[$i])
};

(:~ 
 : random-point-in()
 : Generate a random point within the canvas (uniform distribution)
 :
 : @param $canvas: canvas (space) with height/width/edge parameters
 :)
declare %art:non-deterministic function this:random-point-in($canvas as map(xs:string,item()*)) as map(xs:string,item()*)
{
  this:random-points-in(1, $canvas)
};


(:======================================================================
 : More complex algorithms
 :======================================================================:)


(:~
 : trivial-bounding-circle()
 : Construct minimal bounding circle for trivial case; another helper function
 :)
declare %private function this:trivial-bounding-circle(
  $points as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  util:assert(count($points) <= 3, "Function only for small numbers of points"),
  switch (count($points))
  case 0 return ellipse:circle($point:ORIGIN, 0)
  case 1 return ellipse:circle($points[1], 0)
  case 2 return ellipse:circle(this:midpoint($points[1], $points[2]), this:distance($points[1],$points[2]) div 2)
  case 3 return (
    (: See if we can do the 2pt circle :)
    let $circle := ellipse:circle-from($points[1], $points[2])
    return if (ellipse:contains-point($circle, $points[3])) then $circle
    else let $circle := ellipse:circle-from($points[2], $points[3])
    return if (ellipse:contains-point($circle, $points[1])) then $circle
    else let $circle := ellipse:circle-from($points[3], $points[1])
    return if (ellipse:contains-point($circle, $points[2])) then $circle
    else ellipse:circle-from($points[1], $points[2], $points[3])
  )
  default return util:assert(false(), "Cannot happen")
};

(:~ 
 : weltz()
 : Implement the Weltz algorithm for computing the minimal bounding circle
 : of a set of points.
 :)
declare %private function this:weltz(
  $points as map(xs:string,item()*)*,
  $others as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  if (empty($points) or count($others)=3) then (
    this:trivial-bounding-circle($others)
  ) else (
    let $sub-circle := this:weltz(tail($points), $others)
    return (
      if (ellipse:contains-point($sub-circle, head($points)))
      then $sub-circle
      else this:weltz(tail($points), ($others, head($points)))
    )
  )
};

(:~
 : minimal-bounding-circle()
 : Minimal bounding circle for the given set of points. 
 : If the points are ordered in some way, shuffle them for better convergence.
 :)
declare function this:minimal-bounding-circle(
  $points as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  this:weltz($points, ())
};

(:~
 : bounding-circle()
 : Bounding circle for a region
 : Note: not necessarily minimal for non-linear-edges
 :)
declare function this:bounding-circle(
  $region as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  switch (this:kind($region))
  case "point" return this:trivial-bounding-circle($region)
  case "edge" return this:trivial-bounding-circle(this:points($region))
  case "quad" return
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "cubic" return
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "arc" return 
    this:minimal-bounding-circle(this:points(this:bounding-box($region)))
  case "path" return
    let $pts := 
      if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
      then (
        this:points($region)
      ) else (
        let $boxes := this:edges($region)!this:bounding-box(.)
        return $boxes!this:points(.)
      )
    return
      this:minimal-bounding-circle($pts)
  case "box" return this:minimal-bounding-circle(this:points($region))
  case "space" return this:minimal-bounding-circle(this:points($region))
  case "ellipse" return
    if ($region=>ellipse:is-circle())
    then $region
    else ellipse:circle(ellipse:center($region), max((ellipse:rx($region), ellipse:ry($region))))
  case "polygon" return 
    let $pts := 
      if (every $edge in this:edges($region) satisfies this:kind($edge)="edge")
      then (
        this:points($region)
      ) else (
        let $boxes := this:edges($region)!this:bounding-box(.)
        return $boxes!this:points(.)
      )
    return
      this:minimal-bounding-circle($pts)
  case "complex-polygon" return this:bounding-circle(cpoly:outer($region))
  default return errors:error("GEOM-BADREGION", ($region, "bounding-circle"))
};

(:~
 : bounding-box()
 : Minimum box surrounding the region. Some approximation for non-linear
 : edges.
 :)
declare function this:bounding-box($regions as map(xs:string,item()*)*) as map(xs:string,item()*)
{
  let $boxes := (
    for $region in $regions return switch (this:kind($region))
    case "point" return box:box($region, $region)
    case "edge" return edge:bounding-box($region)
    case "quad" return edge:bounding-box($region)
    case "cubic" return edge:bounding-box($region)
    case "arc" return edge:bounding-box($region)
    case "path" return path:bounding-box($region)
    case "polygon" return path:bounding-box($region)
    case "complex-polygon" return cpoly:bounding-box($region)
    case "box" return $region
    case "space" return box:box(box:min-point($region), box:max-point($region))
    case "ellipse" return ellipse:bounding-box($region)
    case "wrapper" return this:bounding-box($region=>wrapper:body())
    case "mask" return this:bounding-box($region=>mask:body())
    case "reach" return this:bounding-box($region=>reach:body())
    case "slot" return this:bounding-box($region=>slot:body()) (: FIXME: ignores transforms :)
    default return errors:error("GEOM-BADREGION", ($region, "bounding-box"))
  )
  return (
    if (empty($boxes)) then box:box(0,0,0,0)
    else if (empty(tail($boxes))) then $boxes
    else (
      let $pts := $boxes!this:points(.)
      return
        box:box(
          min($pts!point:px(.)), min($pts!point:py(.)),
          max($pts!point:px(.)), max($pts!point:py(.))
        )
    )
  )
};

(:~
 : bounding-sphere()
 : A sphere that contains the regions. 
 : 2D regions are treated as a z=0
 :)
declare function this:bounding-sphere(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)
{
  let $spheres := (
    for $region in $regions
    let $kind := this:kind($region)
    return (
      if ($kind="point") then solid:sphere($region,0)
      else if ($kind=("edge","quad","cubic","arc","path","polygon","complex-polygon","box","space","ellipse")) then (
        let $circle := this:bounding-box($region)
        return solid:sphere(ellipse:center($circle), ellipse:radius($circle))
      ) else if ($kind="wrapper") then (
        this:bounding-sphere($region=>wrapper:body())
      ) else if ($kind="mask") then (
        this:bounding-sphere($region=>mask:body())
      ) else if ($kind="reach") then (
        this:bounding-sphere($region=>reach:body())
      ) else if ($kind="slot") then (
        this:bounding-sphere($region=>slot:body()) (: FIXME: ignores transforms :)
      ) else if ($kind=("sphere","ellipsoid","tetrahedron","cube","octahedron","icosahedron","dodecahedron","block","polyhedron","face")) then (
        solid:circumsphere($region)
      ) else (
        errors:error("GEOM-BADREGION", ($region, "bounding-sphere"))
      )
    )
  )
  let $relevant-spheres := (
    for $i in 1 to count($spheres)
    let $center := solid:center($spheres[$i])
    let $radius := solid:radius($spheres[$i])
    return (
      (: if some sphere we've already seen contains this one, skip it :)
      if (
        some $j in 1 to $i - 1 satisfies (
          point:distance(solid:center($spheres[$j]), $center) <=
          solid:radius($spheres[$j]) - $radius
        )
      ) then () else (
        $spheres[$i]
      )
    )
  )
  return (
    if (empty($relevant-spheres)) then solid:sphere(point:point(0,0),0)
    else if (empty(tail($relevant-spheres))) then $relevant-spheres
    else (
      let $centers := $relevant-spheres!solid:center(.)
      let $center := 
        point:point(avg($centers!point:px(.)), avg($centers!point:py(.)), avg($centers!point:pz(.)))
      let $radius := max(
        for $i in 1 to count($relevant-spheres)
        return point:distance($center, $centers[$i]) + solid:radius($relevant-spheres[$i])
      )
      return (
        solid:sphere($center, $radius)
      )
    )
  )
};


(:~
 : delegate()
 : Common delegation pattern for a lot of generic geo functions. The function
 : being delegated can return a sequence of any kind of region, but if you
 : apply this to complex polygons it should return a polygon if you have $keep.
 :
 : @param $regions: sequence of regions
 : @param $function: function that converts the region into other ones
 : @param $keep: preserve wrappers etc. in output
 : @custom:Status Bleeding edge
 :)
declare function this:delegate(
  $regions as map(xs:string,item()*)*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean
) as map(xs:string,item()*)*
{
  for $region in $regions return switch(this:kind($region))
  case "complex-polygon" return
    if ($keep) then (
      cpoly:complex-polygon(
        $function(cpoly:outer($region)),
        this:delegate(cpoly:inners($region), $function, $keep),
        $region=>this:property-map()
      )
    ) else (
      $function(cpoly:outer($region)),
      this:delegate(cpoly:inners($region), $function, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body( this:delegate($region=>wrapper:body(), $function, $keep) )
    ) else (
      this:delegate($region=>wrapper:body(), $function, $keep)
    )
  case "slot" return
    if ($keep) then (
      $region=>slot:body( this:delegate($region=>slot:body(), $function, $keep) )
    ) else (
      this:delegate($region=>slot:body(), $function, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body( this:delegate($region=>mask:body(), $function, $keep) )
    ) else (
      this:delegate($region=>mask:body(), $function, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body( this:delegate($region=>reach:body(), $function, $keep) )
    ) else (
      this:delegate($region=>reach:body(), $function, $keep)
    )
  default return $function($region)
};

(:~
 : process-polygons()
 : Walk through the regions, peering into slots and wrappers and the like,
 : applying the given function to each path, polygon, or complex-polygon.
 : Example: Replace all polygons with extruded polyhedra
 : geom:process-polygons($regions, solid:extrude(?, 10, 100), true())
 :
 : @param $regions: the regions to process
 : @param $process: function that takes a region
 : @param $keep: if true(), keep wrappers and non-path/polygon regions
 : @return accumulation of applied function
 : @custom:Status Bleeding edge
 :)
declare function this:process-polygons(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-polygons(slot:body($region), $process, $keep))
    ) else (
      this:process-polygons(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-polygons(wrapper:body($region), $process, $keep))
    ) else (
      this:process-polygons(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-polygons(mask:body($region), $process, $keep))
    ) else (
      this:process-polygons(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-polygons(reach:body($region), $process, $keep))
    ) else (
      this:process-polygons(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
};

(:~
 : process-areas()
 : Walk through the regions, peering into slots and wrappers and the like,
 : applying the given function to each 2D region (points, edges = 1D)
 :
 : @param $regions: the regions to process
 : @param $process: function that takes a region
 : @param $keep: if true(), keep wrappers and other unprocessed regions
 : @return accumulation of applied function
 : @custom:Status Bleeding edge
 :)
declare function this:process-areas(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "box" return $process($region)
  case "space" return $process($region)
  case "ellipse" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-areas(slot:body($region), $process, $keep))
    ) else (
      this:process-areas(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-areas(wrapper:body($region), $process, $keep))
    ) else (
      this:process-areas(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-areas(mask:body($region), $process, $keep))
    ) else (
      this:process-areas(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-areas(reach:body($region), $process, $keep))
    ) else (
      this:process-areas(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
};

(:~
 : process-solids()
 : Walk through the regions, peering into slots and wrappers and the like,
 : applying the given function to each solid (including faces).
 :
 : @param $regions: the regions to process
 : @param $process: function that takes a region
 : @param $keep: if true(), keep wrappers and other unprocessed regions
 : @return accumulation of applied function
 : @custom:Status Bleeding edge
 :)
declare function this:process-solids(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "face" return $process($region)
  case "sphere" return $process($region)
  case "cube" return $process($region)
  case "tetrahedron" return $process($region)
  case "octahedron" return $process($region)
  case "dodecahedron" return $process($region)
  case "icosahedron" return $process($region)
  case "polyhedron" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-solids(slot:body($region), $process, $keep))
    ) else (
      this:process-solids(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-solids(wrapper:body($region), $process, $keep))
    ) else (
      this:process-solids(wrapper:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-solids(reach:body($region), $process, $keep))
    ) else (
      this:process-solids(reach:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-solids(mask:body($region), $process, $keep))
    ) else (
      this:process-solids(mask:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
};

(:~
 : process-paths()
 : Walk through the regions, peering into slots and wrappers and the like,
 : applying the given function to each path.
 : Example: Replace all polygons with extruded polyhedra
 : geom:process-polygons($regions, solid:extrude(?, 10, 100), true())
 :
 : @param $regions: the regions to process
 : @param $process: function that takes a region
 : @param $keep: if true(), keep wrappers and non-path/polygon regions
 : @return accumulation of applied function
 : @custom:Status Bleeding edge
 :)
declare function this:process-paths(
  $regions as map(xs:string,item()*)*,
  $process as function(map(xs:string,item()*)) as item()*,
  $keep as xs:boolean
) as item()*
{
  for $region in $regions return switch(this:kind($region))
  case "path" return $process($region)
  case "polygon" return $process($region)
  case "complex-polygon" return $process($region)
  case "slot" return
    if ($keep) then (
      $region=>slot:body(this:process-paths(slot:body($region), $process, $keep))
    ) else (
      this:process-paths(slot:body($region), $process, $keep)
    )
  case "wrapper" return
    if ($keep) then (
      $region=>wrapper:body(this:process-paths(wrapper:body($region), $process, $keep))
    ) else (
      this:process-paths(wrapper:body($region), $process, $keep)
    )
  case "mask" return
    if ($keep) then (
      $region=>mask:body(this:process-paths(mask:body($region), $process, $keep))
    ) else (
      this:process-paths(mask:body($region), $process, $keep)
    )
  case "reach" return
    if ($keep) then (
      $region=>reach:body(this:process-paths(reach:body($region), $process, $keep))
    ) else (
      this:process-paths(reach:body($region), $process, $keep)
    )
  default return if ($keep) then $region else ()
};

(:======================================================================
 : Wrapper for XSLT module use
 :======================================================================:)

declare %art:deprecated function this:point($x as xs:double, $y as xs:double) as map(xs:string,item()*)
{
  point:point($x, $y)
};

declare %art:deprecated function this:point($x as xs:double, $y as xs:double, $z as xs:double) as map(xs:string,item()*)
{
  point:point($x, $y, $z)
};

declare %art:deprecated function this:x($point as map(xs:string,item()*)) as xs:integer
{
  point:x($point)
};

declare %art:deprecated function this:y($point as map(xs:string,item()*)) as xs:integer
{
  point:y($point)
};

declare %art:deprecated function this:px($point as map(xs:string,item()*)) as xs:double
{
  point:px($point)
};

declare %art:deprecated function this:py($point as map(xs:string,item()*)) as xs:double
{
  point:py($point)
};

(:~ 
 : midpoint()
 : Return midpoint between two points
 :
 : @param $a: one point
 : @param $b: the other
 :)
declare %art:deprecated function this:midpoint(
  $a as map(xs:string,item()*),
  $b as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  edge:midpoint($a, $b)
};

declare variable $this:DRAWING-MAP as map(xs:string,function(*)?) := map {
  "point": point:draw#3,
  "box": box:draw#3,
  "space": box:draw#3,
  "ellipse": ellipse:draw#3, 
  "edge": edge:draw#3,
  "quad": edge:draw#3,
  "cubic":edge:draw#3,
  "arc": edge:draw#3,
  "ellipse-arc": edge:draw#3,
  "path": path:draw#3,
  "polygon": path:draw#3,
  "complex-polygon": cpoly:draw#3,
  "graph": graph:draw#3,
  "slot": slot:draw#3,
  "wrapper": wrapper:draw#3,
  "mask": mask:draw#3,
  "reach": reach:draw#3,
  "block": solid:draw#3,
  "sphere": solid:draw#3,
  "ellipsoid": solid:draw#3,
  "face": solid:draw#3,
  "tetrahedron": solid:draw#3,
  "cube": solid:draw#3,
  "octahedron": solid:draw#3,
  "icosahedron": solid:draw#3,
  "dodecahedron" :solid:draw#3,
  "polyhedron": solid:draw#3
};