http://mathling.com/art/draw  library module

http://mathling.com/art/draw


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

February 2022
Status: Stable, subject to expansion for new types

Imports

http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy"
http://mathling.com/geometric/edge
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy"
http://mathling.com/geometric/rectangle
import module namespace box="http://mathling.com/geometric/rectangle"
       at "../geo/rectangle.xqy"
http://mathling.com/geometric/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.xqy"
http://mathling.com/type/slot
import module namespace slot="http://mathling.com/type/slot"
       at "../types/slot.xqy"
http://mathling.com/svg/draw
import module namespace draw="http://mathling.com/svg/draw"
       at "../svg/draw.xqy"
http://mathling.com/geometric
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.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"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"

Functions

Function: drawing-map
declare function drawing-map() as map(xs:string,function(*)?)


drawing-map()
Construct the default drawing map appropriate to the configured
drawing method.

Returns
  • map(xs:string,function(*)?)
declare function this:drawing-map() as map(xs:string,function(*)?)
{
  if ($config:DRAWING-METHOD="art")
  then (
    util:merge-into((
      $geom:DRAWING-MAP,
      map {
        "ellipse": this:draw-ellipse#3,
        "ellipse-arc": this:draw-ellipse-arc#3,
        "def": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
        "ref": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
        "text": function-lookup(QName("http://mathling.com/type/text", "draw"), 3),
        (: Hooks to drawing special functions; callbacks for object drawing :)
        "draw:draw": this:draw#3,
        "draw:svg-style": draw:svg-style#1,
        "draw:dump-dynamic-parameters": draw:dump-dynamic-parameters#1,
        (: Default no-projection :)
        "draw:project": function ($regions as map(xs:string,item()*)*) as map(xs:string,item()*)* {$regions}
      }
    ))
  ) else draw:drawing-map()
}

Function: perspective-drawing-map
declare function perspective-drawing-map($canvas as map(xs:string,item()*), $p as xs:double, $q as xs:double, $r as xs:double) as map(xs:string,function(*)?)


perspective-drawing-map()
Drawing map for solid objects.

Params
  • canvas as map(xs:string,item()*): the canvas we are projecting on to; should have a "depth" key If it doesn't we'll use the max of height/width
  • p as xs:double: perspective parameter (x)
  • q as xs:double: perspective parameter (y)
  • r as xs:double: perspective parameter (z)
Returns
  • map(xs:string,function(*)?): drawing map
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,function(*)?)
{
  map {
    (: Special call back :)
    "draw:project": geom:project(?, $canvas, $p, $q, $r)
  }
}

Function: perspective-drawing-map
declare function perspective-drawing-map($canvas as map(xs:string,item()*)) as map(xs:string,function(*)?)


perspective-drawing-map()
Drawing map for solid objects using default perspective parameters.

Params
  • canvas as map(xs:string,item()*): the canvas we are projecting on to; should have a "depth" key If it doesn't we'll use the max of height/width
Returns
  • map(xs:string,function(*)?): drawing map
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*)
) as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    $canvas,
    0.5, 0.5, 0.5
  )
}

Function: perspective-drawing-map
declare function perspective-drawing-map() as map(xs:string,function(*)?)


perspective-drawing-map()
Drawing map for solid objects using default perspective parameters over
a medium resolution canvas.

Returns
  • map(xs:string,function(*)?): drawing map
declare function this:perspective-drawing-map() as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    core:canvas("medium")=>map:put("depth", core:width("medium")),
    0.5, 0.5, 0.5
  )
}

Function: draw
declare function draw($items as item()*, $properties as map(xs:string,item()*)?, $drawing as map(xs:string,function(*)?)) as item()*


draw()
Main drawing function. Known metadata elements will be drawn as metadata.
Other elements will be passed through. Map objects will be drawn according
to the corresponding entry in the drawing map. If there is no entry for them
attempts till be made to locate an appropriate drawing function: a "draw"
key, a lookup based on the "uri" key, or a lookup based on the URI in
the configured type map for the given "kind".

Params
  • items as item()*: the items to draw
  • properties as map(xs:string,item()*)?: additional style properties to use
  • drawing as map(xs:string,function(*)?): the drawing map
Returns
  • item()*: the drawn items, either SVG or Art XML
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  let $properties := ($properties, map{})[1]
  for $item in $items return typeswitch ($item)
  case map(*) return (
    let $kind := $item("kind")
    return (
      if (empty($kind)) then (
        if (empty($item=>map:keys())) then ()
        else (
          let $draw := $item("draw")
          return (
            if (exists($draw) and ($draw instance of function(*)))
            then $draw($item, $properties, $drawing)
            else (
              let $uri := $item("uri")
              let $draw :=
                if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
              return (
                if (exists($draw))
                then $draw($item, $properties, $drawing)
                else errors:error("ART-UNKNOWN", trace($item,"item"))
              )
            )
          )
        )
      ) else (
        let $draw := $drawing($kind)
        return (
          if (empty($draw)) then (
            let $draw := $item("draw")
            return (
              if (exists($draw) and ($draw instance of function(*)))
              then $draw($item, $properties, $drawing)
              else (
                let $uri := ($item("uri"),$config:TYPE-MAP($kind))[1]
                let $draw :=
                  if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
                return (
                  if (exists($draw))
                  then $draw($item, $properties, $drawing)
                  else errors:error("ART-UNKNOWN", trace($item,"item"))
                )
              )
            )
          ) else (
            $draw($item, $properties, $drawing)
          )
        )
      )
    )
  )
  case element(art:dynamic-parameter) return this:draw-metadata($item)
  case element(art:parameter) return this:draw-metadata($item)
  case element(art:metadata) return this:draw-metadata($item)
  case element(art:component) return this:draw-metadata($item)
  case element() return (: SVG passthrough :)
    element {node-name($item)} {
      $item/@*,
      this:draw($item/node(), $properties, $drawing)
    }
  case text() return $item
  case document-node() return this:draw($item/node(), $properties, $drawing)
  default return errors:error("ART-UNKNOWN", trace($item,"item"))
}

Function: draw
declare function draw($items as item()*, $properties as map(xs:string,item()*)?) as item()*


draw()
Main drawing function. Known metadata elements will be drawn as metadata.
Other elements will be passed through. Map objects will be drawn according
to the corresponding entry in the drawing map. If there is no entry for them
attempts till be made to locate an appropriate drawing function: a "draw"
key, a lookup based on the "uri" key, or a lookup based on the URI in
the configured type map for the given "kind". Uses the default drawing map.

Params
  • items as item()*: the items to draw
  • properties as map(xs:string,item()*)?: additional style properties to use
Returns
  • item()*: the drawn items, either SVG or Art XML
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?
) as item()*
{
  this:draw($items, $properties, 
    util:merge-into(
      this:drawing-map(),
      let $resolution := ($properties("resolution"),"medium")[1]
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      let $canvas := core:canvas($resolution)
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
}

Function: draw
declare function draw($items as item()*) as item()*


draw()
Main drawing function. Known metadata elements will be drawn as metadata.
Other elements will be passed through. Map objects will be drawn according
to the corresponding entry in the drawing map. If there is no entry for them
attempts till be made to locate an appropriate drawing function: a "draw"
key, a lookup based on the "uri" key, or a lookup based on the URI in
the configured type map for the given "kind". Uses the default drawing map
with no additional styling properties.

Params
  • items as item()*: the items to draw
Returns
  • item()*: the drawn items, either SVG or Art XML
declare function this:draw($items as item()*) as item()*
{
  this:draw($items, map{},
    util:merge-into(
      this:drawing-map(),
      this:perspective-drawing-map()
    )
  )
}

Function: perspective-draw
declare function perspective-draw($items as item()*, $properties as map(xs:string,item()*)?, $canvas as map(xs:string,item()*)) as item()*


perspective-draw()
Draw the items using the perspective drawing map for the given canvas size.
The perspective parameters are either the properties "perspective.p",
"perspective.q", "perspective.r", or the defaults (0.5).

Params
  • items as item()*: the items to draw
  • properties as map(xs:string,item()*)?: additional style properties to use
  • canvas as map(xs:string,item()*): the canvas
Returns
  • item()*: the drawn items, either SVG or Art XML
declare function this:perspective-draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  this:draw($items, $properties,
    util:merge-into(
      this:drawing-map(),
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
}

Function: draw-point-matrix
declare function draw-point-matrix($matrix as map(*), $colouring as function(item()*) as item(), $properties as map(xs:string,item()*)) as element()*


draw-point-matrix()
Efficient rendering of a point matrix, for when you have a matrix that
covers the whole canvas in a relatively small number of colours. This
converts each set of points in a single colour bucket into a path with
tiny lines at each point.

Params
  • matrix as map(*): a point matrix, in either core/matrix, core/array, or geometric/matrix format
  • colouring as function(item()*)asitem(): a function that maps a matrix value to a colour, the colour can be either a colour name or RGB string or a gradient stop number, an index into to the colours in $properties("gradient")
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*: drawn colour buckets, SVG or Art XML
declare function this:draw-point-matrix(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $properties as map(xs:string,item()*)
) as element()*
{
  this:draw-colour-buckets(
    draw:colour-buckets($matrix, $colouring),
    $properties
  )
}

Function: draw-colour-buckets
declare function draw-colour-buckets($buckets as map(*), $properties as map(xs:string,item()*)) as element()*


draw-colour-buckets()
Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
If you use numeric colour keys, properties must include "colours" with
the colour palette

Params
  • buckets as map(*): colour buckets, e.g. output from colour-buckets()
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*: drawn colour buckets, SVG or Art XML
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $properties as map(xs:string,item()*)
) as element()*
{
  if ($config:DRAWING-METHOD="art") then (
    this:draw-colour-buckets(
      $buckets,
      function($key as item()) as node()* {
        if ($key instance of xs:numeric) 
        then attribute stop {$key cast as xs:integer}
        else attribute colour {string($key)}
      },
      $properties
    )
  ) else (
    draw:draw-colour-buckets($buckets, $properties)
  )
}

Function: draw-colour-buckets
declare function draw-colour-buckets($buckets as map(*), $keyfn as function(item()) as node()*, $properties as map(xs:string,item()*)) as element()*


draw-colour-buckets()
Efficient rendering of a colour buckets; workhorse of draw-point-matrix()

Params
  • buckets as map(*): colour buckets, e.g. output from colour-buckets()
  • keyfn as function(item())asnode()*: mapping of key to attributes
  • properties as map(xs:string,item()*): style properties plus "gradient": the gradient to index colour values into (default=none) "width": how wide/long a line to make (default=1)
Returns
  • element()*: drawn colour buckets, SVG or Art XML
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $keyfn as function(item()) as node()*,
  $properties as map(xs:string,item()*)
) as element()*
{
  if ($config:DRAWING-METHOD="art") then (
    let $width := ($properties("width"),$properties("stroke-width"),1)[1]
    for $key in $buckets=>map:keys()
    let $points := $buckets($key)
    let $chunks := 1 + count($points) idiv 10000
    for $chunk in 1 to $chunks
    let $chunk-points := $points[position() > ($chunk - 1)*10000 and position() <= $chunk*10000]
    where exists($chunk-points)
    return (
      <art:colour-points>{
        $keyfn($key),
        util:as-attributes($properties),
        if ($config:DENSE-EDGES) then (
          let $path := 
            string-join(
              for $pt in $chunk-points return (
                edge:map-command('goto','absolute')||" "||
                point:px($pt)||" "||point:py($pt)||
                edge:map-command('line','absolute')||" "||
                (point:px($pt) + $width)||" "||(point:py($pt) + 0*$width)
              ),
              " "
            )
          return (
            attribute d {$path}
          )
        ) else (
          for $pt in $chunk-points return (
            <art:point x="{point:x($pt)}" y="{point:y($pt)}"/>
          )
        )
      }</art:colour-points>
    )
  ) else (
    draw:draw-colour-buckets($buckets, $keyfn, $properties)
  )
}

Function: drawing
declare function drawing($canvas as map(xs:string,item()*), $metadata as element()*, $defs as element()*, $styles as xs:string*, $content as item()*)


drawing()
Render a complete SVG. The components here should have already been
drawn (i.e. rendered to SVG)

Params
  • canvas as map(xs:string,item()*): defines size of canvas
  • metadata as element()*: metadata elements
  • defs as element()*: definitions
  • styles as xs:string*: CSS styles
  • content as item()*: body content
declare function this:drawing(
  $canvas as map(xs:string,item()*),
  $metadata as element()*,
  $defs as element()*,
  $styles as xs:string*,
  $content as item()* 
)
{
  if ($config:DRAWING-METHOD="art") then (
    <art:canvas width='{box:width($canvas)}' height='{box:height($canvas)}' edge='{box:edge($canvas)}'>
      <svg:style type="text/css">{$styles}</svg:style>
      <art:metadata>{$metadata}</art:metadata>
      {$defs, $content}
    </art:canvas>
  ) else (
    draw:drawing($canvas, $metadata, $defs, $styles, $content)
  )
}

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


to-CSS()
Render a property bundle as a CSS class style.

Params
  • name as xs:string: the style name
  • properties as map(xs:string,item()*): the properties
declare function this:to-CSS(
  $name as xs:string,
  $properties as map(xs:string,item()*)
)
{
  draw:to-CSS($name, $properties)
}

Function: dump-parameters
declare function dump-parameters($parameters as map(xs:string, item()*))


dump-parameters()
Dump a parameter bundle, for inclusion in metadata or debugging.

Params
  • parameters as map(xs:string,item()*): the parameters
declare function this:dump-parameters($parameters as map(xs:string, item()*))
{
  draw:dump-parameters($parameters)
}

Function: dump-parameters
declare function dump-parameters($parameters as map(xs:string, item()*), $parameter-qname as xs:QName, $i as xs:integer?)


dump-parameters()
Dump a parameter bundle, for inclusion in metadata or debugging.

Params
  • parameters as map(xs:string,item()*): the parameters
  • parameter-qname as xs:QName: name for wrapping element
  • i as xs:integer?: index to append to keys
declare function this:dump-parameters(
  $parameters as map(xs:string, item()*),
  $parameter-qname as xs:QName,
  $i as xs:integer?
)
{
  draw:dump-parameters($parameters, $parameter-qname, $i)
}

Function: dump-dynamic-parameters
declare function dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)


dump-dynamic-parameters()
Dump a dynamic parameter bundle, for inclusion in metadata or debugging.

Params
  • parameters as map(xs:string,item()*): the parameters
  • i as xs:integer?: index to append to keys
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)
{
  draw:dump-dynamic-parameters($parameters, $i)
}

Function: dump-dynamic-parameters
declare function dump-dynamic-parameters($parameters as map(xs:string, item()*))


dump-dynamic-parameters()
Dump a dynamic parameter bundle, for inclusion in metadata or debugging.

Params
  • parameters as map(xs:string,item()*): the parameters
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*))
{
  draw:dump-dynamic-parameters($parameters)
}

Function: dump-randomizer
declare function dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))


dump-randomizer()
Dump a randomizer descriptor, for inclusion in metadata or debugging.

Params
  • key as xs:string: name of randomizer for metadata
  • algorithm as map(xs:string,item()*): randomizer descriptor
declare function this:dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))
{
  draw:dump-randomizer($key, $algorithm)
}

Function: dump-randomizers
declare function dump-randomizers($randomizers as map(xs:string, item()*))


dump-randomizers()
Dump randomizer table, for inclusion in metadata or debugging.

Params
  • randomizers as map(xs:string,item()*): table of randomizer descriptors
declare function this:dump-randomizers($randomizers as map(xs:string, item()*))
{
  draw:dump-randomizers($randomizers)
}

Original Source Code

xquery version "3.1";
(:~
 : Art drawing framework. 
 : Copyright© Mary Holstege 2020-2023
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since February 2022
 : @custom:Status Stable, subject to expansion for new types
 :)
module namespace this="http://mathling.com/art/draw"; 

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 geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy";
import module namespace edge="http://mathling.com/geometric/edge"
       at "../geo/edge.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 slot="http://mathling.com/type/slot"
       at "../types/slot.xqy";
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace draw="http://mathling.com/svg/draw"
       at "../svg/draw.xqy";

declare namespace svg="http://www.w3.org/2000/svg";
declare namespace xhtml="http://www.w3.org/1999/xhtml";
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";

(:~
 : drawing-map()
 : Construct the default drawing map appropriate to the configured
 : drawing method.
 :)
declare function this:drawing-map() as map(xs:string,function(*)?)
{
  if ($config:DRAWING-METHOD="art")
  then (
    util:merge-into((
      $geom:DRAWING-MAP,
      map {
        "ellipse": this:draw-ellipse#3,
        "ellipse-arc": this:draw-ellipse-arc#3,
        "def": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
        "ref": function-lookup(QName("http://mathling.com/type/defref", "draw"), 3),
        "text": function-lookup(QName("http://mathling.com/type/text", "draw"), 3),
        (: Hooks to drawing special functions; callbacks for object drawing :)
        "draw:draw": this:draw#3,
        "draw:svg-style": draw:svg-style#1,
        "draw:dump-dynamic-parameters": draw:dump-dynamic-parameters#1,
        (: Default no-projection :)
        "draw:project": function ($regions as map(xs:string,item()*)*) as map(xs:string,item()*)* {$regions}
      }
    ))
  ) else draw:drawing-map()
};

declare %private function this:draw-ellipse-arc(
  $item as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  if ($config:DRAWING-METHOD="art") then (
    let $type := ($item=>map:get("type"),"")[1]
    let $center := $item=>edge:arc-center()
    let $ellipse := $item=>edge:arc-ellipse()
    let $rotation := ellipse:rotation($ellipse)
    return (
      if ($type ne "stroke" and ellipse:rotation($item=>edge:arc-ellipse()) != 0) then (
        (: XYZZY works for standalone edges; not for edges as part of a path :)
        (: But rotation angles inside the edge does weird things :)
        (: we don't have code for rotating ellipse-arcs right now :)
        this:draw(
          slot:slot(
            edge:draw($item=>map:put("rotation", 0), $properties, $drawing)
          )=>slot:center($center)=>slot:rotation($rotation),
          map {},
          $drawing (: edge got the style properties :)
        )
      ) else (
        edge:draw($item, $properties, $drawing)
      )
    )
  ) else (
    edge:draw($item, $properties, $drawing)
  )
};

declare %private function this:draw-ellipse(
  $item as map(xs:string,item()*),
  $properties as map(xs:string,item()*),
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  if ($config:DRAWING-METHOD="art") then (
    let $type := ($item=>map:get("type"),"")[1]
    return (
      if ($type="stroke") then (
        let $style-properties :=
          util:merge-into(($properties, util:exclude(ellipse:property-map($item),("r","type"))))
        let $center := $item=>ellipse:center()
        let $rx := $item=>ellipse:rx()
        let $ry := $item=>ellipse:ry()
        let $rotation := $item=>ellipse:rotation()
        return
        <art:stroke-path art:marker="polygon">{
          util:as-attributes($style-properties),
          let $edges :=
            if ($rx = $ry) then (
              edge:arc-by-angle($center, $rx, 0, 90, false(), false()),
              edge:arc-by-angle($center, $rx, 90, 180, false(), false()),
              edge:arc-by-angle($center, $rx, 180, 270, false(), false()),
              edge:arc-by-angle($center, $rx, 270, 360, false(), false())
            ) else if ($rotation = 0) then (
              let $pts := 
                for $t in (0.0, 0.25, 0.5, 1.0, 0.0) return (
                  $item=>ellipse:ellipse-point($t)
                )
              for $i in 1 to 4
              return (
                edge:ellipse-arc($center, $rx, $ry, $rotation, $pts[$i], $pts[$i + 1], false(), false())
              )
            ) else (
              (: approximation: we can't rotate ellipse arcs right now :)
              let $pts :=
                for $t in util:linspace(50, 0.0, 1.0) return (
                  $item=>ellipse:ellipse-point($t)
                )
              return edge:to-edges($pts)
            )
          return this:draw($edges, map {}, $drawing)
        }</art:stroke-path>
      ) else (
        ellipse:draw($item, $properties, $drawing)
      )
    )
  ) else (
    ellipse:draw($item, $properties, $drawing)
  )
};

(:~
 : perspective-drawing-map()
 : Drawing map for solid objects.
 :
 : @param $canvas: the canvas we are projecting on to; should have a "depth" key
 :   If it doesn't we'll use the max of height/width
 : @param $p: perspective parameter (x)
 : @param $q: perspective parameter (y)
 : @param $r: perspective parameter (z)
 : @return drawing map
 :)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*),
  $p as xs:double,
  $q as xs:double,
  $r as xs:double
) as map(xs:string,function(*)?)
{
  map {
    (: Special call back :)
    "draw:project": geom:project(?, $canvas, $p, $q, $r)
  }
};

(:~
 : perspective-drawing-map()
 : Drawing map for solid objects using default perspective parameters.
 :
 : @param $canvas: the canvas we are projecting on to; should have a "depth" key
 :   If it doesn't we'll use the max of height/width
 : @return drawing map
 :)
declare function this:perspective-drawing-map(
  $canvas as map(xs:string,item()*)
) as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    $canvas,
    0.5, 0.5, 0.5
  )
};

(:~
 : perspective-drawing-map()
 : Drawing map for solid objects using default perspective parameters over
 : a medium resolution canvas.
 :
 : @return drawing map
 :)
declare function this:perspective-drawing-map() as map(xs:string,function(*)?)
{
  this:perspective-drawing-map(
    core:canvas("medium")=>map:put("depth", core:width("medium")),
    0.5, 0.5, 0.5
  )
};

declare %private function this:draw-metadata(
  $item as element()
) as element()
{
  if ($config:DRAWING-METHOD="art")
  then $item
  else <svg:metadata>{$item}</svg:metadata>
};

(:~
 : draw()
 : Main drawing function. Known metadata elements will be drawn as metadata.
 : Other elements will be passed through. Map objects will be drawn according
 : to the corresponding entry in the drawing map. If there is no entry for them
 : attempts till be made to locate an appropriate drawing function: a "draw"
 : key, a lookup based on the "uri" key, or a lookup based on the URI in
 : the configured type map for the given "kind".
 :
 : @param $items: the items to draw
 : @param $properties: additional style properties to use
 : @param $drawing: the drawing map
 : @return the drawn items, either SVG or Art XML 
 :)
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $drawing as map(xs:string,function(*)?)
) as item()*
{
  let $properties := ($properties, map{})[1]
  for $item in $items return typeswitch ($item)
  case map(*) return (
    let $kind := $item("kind")
    return (
      if (empty($kind)) then (
        if (empty($item=>map:keys())) then ()
        else (
          let $draw := $item("draw")
          return (
            if (exists($draw) and ($draw instance of function(*)))
            then $draw($item, $properties, $drawing)
            else (
              let $uri := $item("uri")
              let $draw :=
                if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
              return (
                if (exists($draw))
                then $draw($item, $properties, $drawing)
                else errors:error("ART-UNKNOWN", trace($item,"item"))
              )
            )
          )
        )
      ) else (
        let $draw := $drawing($kind)
        return (
          if (empty($draw)) then (
            let $draw := $item("draw")
            return (
              if (exists($draw) and ($draw instance of function(*)))
              then $draw($item, $properties, $drawing)
              else (
                let $uri := ($item("uri"),$config:TYPE-MAP($kind))[1]
                let $draw :=
                  if (exists($uri)) then function-lookup(QName($uri, "draw"), 3) else ()
                return (
                  if (exists($draw))
                  then $draw($item, $properties, $drawing)
                  else errors:error("ART-UNKNOWN", trace($item,"item"))
                )
              )
            )
          ) else (
            $draw($item, $properties, $drawing)
          )
        )
      )
    )
  )
  case element(art:dynamic-parameter) return this:draw-metadata($item)
  case element(art:parameter) return this:draw-metadata($item)
  case element(art:metadata) return this:draw-metadata($item)
  case element(art:component) return this:draw-metadata($item)
  case element() return (: SVG passthrough :)
    element {node-name($item)} {
      $item/@*,
      this:draw($item/node(), $properties, $drawing)
    }
  case text() return $item
  case document-node() return this:draw($item/node(), $properties, $drawing)
  default return errors:error("ART-UNKNOWN", trace($item,"item"))
};

(:~
 : draw()
 : Main drawing function. Known metadata elements will be drawn as metadata.
 : Other elements will be passed through. Map objects will be drawn according
 : to the corresponding entry in the drawing map. If there is no entry for them
 : attempts till be made to locate an appropriate drawing function: a "draw"
 : key, a lookup based on the "uri" key, or a lookup based on the URI in
 : the configured type map for the given "kind". Uses the default drawing map.
 :
 : @param $items: the items to draw
 : @param $properties: additional style properties to use
 : @return the drawn items, either SVG or Art XML 
 :)
declare function this:draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?
) as item()*
{
  this:draw($items, $properties, 
    util:merge-into(
      this:drawing-map(),
      let $resolution := ($properties("resolution"),"medium")[1]
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      let $canvas := core:canvas($resolution)
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
};

(:~
 : draw()
 : Main drawing function. Known metadata elements will be drawn as metadata.
 : Other elements will be passed through. Map objects will be drawn according
 : to the corresponding entry in the drawing map. If there is no entry for them
 : attempts till be made to locate an appropriate drawing function: a "draw"
 : key, a lookup based on the "uri" key, or a lookup based on the URI in
 : the configured type map for the given "kind". Uses the default drawing map
 : with no additional styling properties.
 :
 : @param $items: the items to draw
 : @return the drawn items, either SVG or Art XML 
 :)
declare function this:draw($items as item()*) as item()*
{
  this:draw($items, map{},
    util:merge-into(
      this:drawing-map(),
      this:perspective-drawing-map()
    )
  )
};

(:~
 : perspective-draw()
 : Draw the items using the perspective drawing map for the given canvas size.
 : The perspective parameters are either the properties "perspective.p",
 : "perspective.q", "perspective.r", or the defaults (0.5).
 :
 : @param $items: the items to draw
 : @param $properties: additional style properties to use
 : @param $canvas: the canvas
 : @return the drawn items, either SVG or Art XML 
 :)
declare function this:perspective-draw(
  $items as item()*,
  $properties as map(xs:string,item()*)?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  this:draw($items, $properties,
    util:merge-into(
      this:drawing-map(),
      let $p := ($properties("perspective.p"), 0.5)[1]
      let $q := ($properties("perspective.q"), 0.5)[1]
      let $r := ($properties("perspective.r"), 0.5)[1]
      return this:perspective-drawing-map($canvas, $p, $q, $r)
    )
  )
};

(:~
 : draw-point-matrix()
 : Efficient rendering of a point matrix, for when you have a matrix that
 : covers the whole canvas in a relatively small number of colours. This
 : converts each set of points in a single colour bucket into a path with
 : tiny lines at each point.
 :
 : @param $matrix: a point matrix, in either core/matrix, core/array, or 
 :    geometric/matrix format
 : @param $colouring: a function that maps a matrix value to a colour,
 :    the colour can be either a colour name or RGB string or a gradient
 :    stop number, an index into to the colours in $properties("gradient")
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 : @return drawn colour buckets, SVG or Art XML
 :)
declare function this:draw-point-matrix(
  $matrix as map(*),
  $colouring as function(item()*) as item(),
  $properties as map(xs:string,item()*)
) as element()*
{
  this:draw-colour-buckets(
    draw:colour-buckets($matrix, $colouring),
    $properties
  )
};

(:~
 : draw-colour-buckets()
 : Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
 : If you use numeric colour keys, properties must include "colours" with
 : the colour palette
 :
 : @param $buckets: colour buckets, e.g. output from colour-buckets()
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 : @return drawn colour buckets, SVG or Art XML
 :)
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $properties as map(xs:string,item()*)
) as element()*
{
  if ($config:DRAWING-METHOD="art") then (
    this:draw-colour-buckets(
      $buckets,
      function($key as item()) as node()* {
        if ($key instance of xs:numeric) 
        then attribute stop {$key cast as xs:integer}
        else attribute colour {string($key)}
      },
      $properties
    )
  ) else (
    draw:draw-colour-buckets($buckets, $properties)
  )
};

(:~
 : draw-colour-buckets()
 : Efficient rendering of a colour buckets; workhorse of draw-point-matrix()
 :
 : @param $buckets: colour buckets, e.g. output from colour-buckets()
 : @param $keyfn: mapping of key to attributes
 : @param $properties: style properties plus 
 :    "gradient": the gradient to index colour values into (default=none)
 :    "width": how wide/long a line to make (default=1)
 : @return drawn colour buckets, SVG or Art XML
 :)
declare function this:draw-colour-buckets(
  $buckets as map(*),
  $keyfn as function(item()) as node()*,
  $properties as map(xs:string,item()*)
) as element()*
{
  if ($config:DRAWING-METHOD="art") then (
    let $width := ($properties("width"),$properties("stroke-width"),1)[1]
    for $key in $buckets=>map:keys()
    let $points := $buckets($key)
    let $chunks := 1 + count($points) idiv 10000
    for $chunk in 1 to $chunks
    let $chunk-points := $points[position() > ($chunk - 1)*10000 and position() <= $chunk*10000]
    where exists($chunk-points)
    return (
      <art:colour-points>{
        $keyfn($key),
        util:as-attributes($properties),
        if ($config:DENSE-EDGES) then (
          let $path := 
            string-join(
              for $pt in $chunk-points return (
                edge:map-command('goto','absolute')||" "||
                point:px($pt)||" "||point:py($pt)||
                edge:map-command('line','absolute')||" "||
                (point:px($pt) + $width)||" "||(point:py($pt) + 0*$width)
              ),
              " "
            )
          return (
            attribute d {$path}
          )
        ) else (
          for $pt in $chunk-points return (
            <art:point x="{point:x($pt)}" y="{point:y($pt)}"/>
          )
        )
      }</art:colour-points>
    )
  ) else (
    draw:draw-colour-buckets($buckets, $keyfn, $properties)
  )
};

(:~
 : drawing()
 : Render a complete SVG. The components here should have already been
 : drawn (i.e. rendered to SVG)
 :
 : @param $canvas: defines size of canvas
 : @param $metadata: metadata elements
 : @param $defs: definitions
 : @param $styles: CSS styles
 : @param $content: body content
 : @return SVG or Art XML document
 :)
declare function this:drawing(
  $canvas as map(xs:string,item()*),
  $metadata as element()*,
  $defs as element()*,
  $styles as xs:string*,
  $content as item()* 
)
{
  if ($config:DRAWING-METHOD="art") then (
    <art:canvas width='{box:width($canvas)}' height='{box:height($canvas)}' edge='{box:edge($canvas)}'>
      <svg:style type="text/css">{$styles}</svg:style>
      <art:metadata>{$metadata}</art:metadata>
      {$defs, $content}
    </art:canvas>
  ) else (
    draw:drawing($canvas, $metadata, $defs, $styles, $content)
  )
};

(:~
 : to-CSS()
 : Render a property bundle as a CSS class style.
 :
 : @param $name: the style name
 : @param $properties: the properties
 : @return CSS string
 :)
declare function this:to-CSS(
  $name as xs:string,
  $properties as map(xs:string,item()*)
)
{
  draw:to-CSS($name, $properties)
};

(:~
 : dump-parameters()
 : Dump a parameter bundle, for inclusion in metadata or debugging.
 :
 : @param $parameters: the parameters
 : @return metadata
 :)
declare function this:dump-parameters($parameters as map(xs:string, item()*))
{
  draw:dump-parameters($parameters)
};

(:~
 : dump-parameters()
 : Dump a parameter bundle, for inclusion in metadata or debugging.
 :
 : @param $parameters: the parameters
 : @param $parameter-qname: name for wrapping element
 : @param $i: index to append to keys
 : @return metadata
 :)
declare function this:dump-parameters(
  $parameters as map(xs:string, item()*),
  $parameter-qname as xs:QName,
  $i as xs:integer?
)
{
  draw:dump-parameters($parameters, $parameter-qname, $i)
};

(:~
 : dump-dynamic-parameters()
 : Dump a dynamic parameter bundle, for inclusion in metadata or debugging.
 :
 : @param $parameters: the parameters
 : @param $i: index to append to keys
 : @return metadata
 :)
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*), $i as xs:integer?)
{
  draw:dump-dynamic-parameters($parameters, $i)
};

(:~
 : dump-dynamic-parameters()
 : Dump a dynamic parameter bundle, for inclusion in metadata or debugging.
 :
 : @param $parameters: the parameters
 : @return metadata
 :)
declare function this:dump-dynamic-parameters($parameters as map(xs:string, item()*))
{
  draw:dump-dynamic-parameters($parameters)
};

(:~
 : dump-randomizer()
 : Dump a randomizer descriptor, for inclusion in metadata or debugging.
 :
 : @param $key: name of randomizer for metadata
 : @param $algorithm: randomizer descriptor
 : @return metadata
 :)
declare function this:dump-randomizer($key as xs:string, $algorithm as map(xs:string, item()*))
{
  draw:dump-randomizer($key, $algorithm)
};

(:~
 : dump-randomizers()
 : Dump randomizer table, for inclusion in metadata or debugging.
 :
 : @param $randomizers: table of randomizer descriptors
 : @return metadata
 :)
declare function this:dump-randomizers($randomizers as map(xs:string, item()*))
{
  draw:dump-randomizers($randomizers)
};