http://mathling.com/geometric/render3d  library module

http://mathling.com/geometric/render3d


A drawing paradigm for 3d objects that does distance-based opacity
with projection.

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

May 2024
Status: Bleeding edge

Function Index

Imports

http://mathling.com/geometric/solid
import module namespace solid="http://mathling.com/geometric/solid"
       at "../geo/solid.xqy"
http://mathling.com/type/defref
import module namespace def="http://mathling.com/type/defref"
       at "../types/defref.xqy"
http://mathling.com/math
import module namespace mmath="http://mathling.com/math"
       at "../math/math.xqy"
http://mathling.com/type/reach
import module namespace reach="http://mathling.com/type/reach"
       at "../types/reach.xqy"
http://mathling.com/svg/gradients
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.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/geometric/projection
import module namespace project="http://mathling.com/geometric/projection"
       at "../geo/projection.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
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy"
http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point"
       at "../geo/point.xqy"
http://mathling.com/geometric/graph
import module namespace graph="http://mathling.com/geometric/graph"
       at "../geo/graph.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/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/ellipse
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.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/type/mask
import module namespace mask="http://mathling.com/type/mask"
       at "../types/mask.xqy"

Functions

Function: render3d
declare function render3d($regions as item()*, $z-to-alpha as function(xs:double) as xs:double, $α as xs:double, $β as xs:double) as item()*


render3d()
3D rendering adjustment to the shapes. Edges and faces will be given
the opacity (alpha) determined by the depth-to-opacity function and
projected with the given angles. 2D regions will be left alone. If you
want to include them in the mix then $regions=>translate(0,0,0) will
do the trick.

Params
  • regions as item()*: items to render
  • z-to-alpha as function(xs:double)asxs:double: function mapping depth (z) to opacity
  • α as xs:double: pitch degrees
  • β as xs:double: roll degrees
Returns
  • item()*: regions ordered from back to front and projected with depth-sensitive opacity
declare function this:render3d(
  $regions as item()*,
  $z-to-alpha as function(xs:double) as xs:double,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  for $region in $regions return (
    if ($region instance of map(xs:string,item()*)) then (
      let $kind := util:kind($region)
      return (
        if ($kind=("sphere","ellipsoid")) then (
          let $ellipse := ellipse:ellipse(solid:center($region), solid:rx($region), solid:ry($region))
          let $min-opacity := $z-to-alpha(point:pz(solid:center($region)) - solid:rz($region))
          let $max-opacity := $z-to-alpha(point:pz(solid:center($region)) + solid:rz($region))
          let $fill := ($region("fill"), $region("colour"))[1]
          let $fill-gradient := (
            if (empty($fill)) then ()
            else if ($fill="none") then ()
            else (
              let $def := gradient:gradient($fill)
              return (
                if (exists($def)) then (
                  $def=>
                    gradient:gradient-fade($max-opacity, $min-opacity)=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")=>
                    gradient:gradient-name(rand:id($fill))
                ) else (
                  gradient:gradient-definition(
                    rand:id($fill), "outflow", ($max-opacity, $min-opacity), $fill
                  )=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")
                )
              )
            )
          )
          let $fill-ref := if (empty($fill-gradient)) then $fill else gradient:id($fill-gradient)
          let $stroke := ($region("stroke"), $region("colour"))[1]
          let $stroke-gradient := (
            if (empty($stroke)) then ()
            else if ($stroke="none") then ()
            else (
              let $def := gradient:gradient($stroke)
              return (
                if (exists($def)) then (
                  $def=>
                    gradient:gradient-fade($max-opacity, $min-opacity)=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")=>
                    gradient:gradient-name(rand:id($stroke))
                ) else (
                  gradient:gradient-definition(
                    rand:id($stroke), "normal", $min-opacity, $stroke
                  )=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")
                )
              )
            )
          )
          let $stroke-ref := if (empty($stroke-gradient)) then $stroke else gradient:id($stroke-gradient)
          return (
            def:def($fill-gradient),
            def:def($stroke-gradient),
            slot:slot(
              $ellipse=>
              ellipse:interpolate(
                ellipse:perimeter($ellipse) idiv 10 (:fineness:)
              )=>
              path:polygon()=>
              project:projection($α, $β)=>
              path:decimal(1)=>
              path:with-properties(
                util:merge-into(
                  solid:property-map($region)=>util:exclude(("opacity","colour","fill","stroke")),
                  map {
                    "colour": $fill-ref,
                    "stroke": $stroke-ref
                  }
                )
              )
            )
          )
        ) else if ($kind=solid:solids()) then (
          slot:slot(
            $region=>
              solid:ordered-translucent-faces($z-to-alpha)=>
              project:projection($α, $β)=>
              solid:as-polygon()=>
              path:decimal(1)=>
              path:with-properties(
                solid:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            solid:property-map($region)=>util:include("opacity")
          )
        ) else if (
          point:max-dimension((point:point(0,0),this:process-regions($region, this:zpoints#1, false(), "slot"))) < 3
        ) then (
          $region
        ) else if ($kind=("path","polygon")) then (
          slot:slot(
            $region=>
              path:ordered-translucent-edges($z-to-alpha)=>
              project:projection($α, $β)=>
              path:consolidate-by-colour-and-opacity()=>
              path:decimal(1)=>
              path:with-properties(
                path:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            path:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind=("edge","quad","cubic","arc","ellipse-arc")) then (
          slot:slot(
            $region=>
              path:ordered-translucent-edges($z-to-alpha)=>
              project:projection($α, $β)=>
              edge:decimal(1)=>
              edge:with-properties(
                edge:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            edge:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind=("complex-polygon")) then (
          slot:slot(
            for $poly in (cpoly:inners($region), cpoly:outer($region))
            return (
              slot:slot(
                $poly=>
                  path:ordered-translucent-edges($z-to-alpha)=>
                  project:projection($α, $β)=>
                  path:consolidate-by-colour-and-opacity()=>
                  path:decimal(1)=>
                  path:with-properties(
                    path:property-map($poly)=>util:exclude("opacity")
                  )
              )=>slot:with-properties(
                path:property-map($poly)=>util:include("opacity")
              )
            )
          )=>slot:with-properties(cpoly:property-map($region))
        ) else if ($kind="graph") then (
          slot:slot(
            graph:edges($region)=>
            path:ordered-translucent-edges($z-to-alpha)=>
            project:projection($α, $β)=>
            edge:decimal(1)=>
            path:with-properties(
              graph:property-map($region)=>util:exclude("opacity")
            )
          )=>slot:with-properties(
            graph:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind="point") then (
          $region=>
            project:projection($α, $β)=>
            point:decimal(1)=>
            point:with-properties(
              if (exists($region("opacity")))
              then map {"opacity": $region("opacity")*$z-to-alpha(point:pz($region))}
              else map {"opacity": $z-to-alpha(point:pz($region))}
            )
        ) else if ($kind="ellipse") then (
            $region=>
              project:projection($α, $β)=>
              ellipse:decimal(1)=>
              ellipse:with-properties(
                if (exists($region("opacity")))
                then map {"opacity": $region("opacity")*$z-to-alpha(point:pz(ellipse:center($region)))}
                else map {"opacity": $z-to-alpha(point:pz(ellipse:center($region)))}
              )
        ) else if ($kind=("space","box")) then (
          path:polygon(box:edges($region))=>
            path:with-properties($region=>box:property-map())=>
            this:render3d($z-to-alpha, $α, $β)
        ) else if ($kind="wrapper") then (
          $region=>wrapper:body(
            $region=>wrapper:body()=>this:render3d($z-to-alpha, $α, $β)
          )
        ) else if ($kind="slot") then (
          (: XYZZY FIX ME: handling slot transforms, locations :)
          if ($region=>slot:body() instance of map(xs:string,item()*)*) then (
            $region=>slot:body(
              $region=>slot:body()=>this:render3d($z-to-alpha, $α, $β)
            )
          ) else (
            $region
          )
        ) else if ($kind="def") then (
          if ($region=>def:body() instance of map(xs:string,item()*)*) then (
            $region=>def:body(
              $region=>def:body()=>this:render3d($z-to-alpha, $α, $β)
            )
          ) else (
            $region
          )
        ) else if ($kind="ref") then (
          if (point:dimension(def:location($region)) < 3) then (
            $region
          ) else (
            $region=>
              def:location(
                def:location($region)=>
                  project:projection($α, $β)=>
                  geom:decimal(1)
              )=>map:put("opacity", $z-to-alpha(point:pz(def:location($region))))
          )
        ) else if ($kind=("mask","reach")) then (
          (: Bad idea to mess with opacities of reaches and masks :)
          $region=>project:projection($α, $β)=>geom:decimal(1)
        ) else (
          $region=>
            project:projection($α, $β)=>
            geom:decimal(1)=>
            geom:with-properties(
              if (exists($region("opacity")))
              then map {"opacity": $region("opacity")*$z-to-alpha(0)}
              else map {"opacity": $z-to-alpha(0)}
            )
        )
      )
    ) else (
      $region (: pass-through literal elements etc. :)
    )
  )
}

Function: render3d
declare function render3d($regions as item()*, $min-opacity as xs:double, $max-opacity as xs:double, $α as xs:double, $β as xs:double) as item()*


render3d()
3D rendering adjustment to the shapes. Edges and faces will be given
the opacity (alpha) determined by the depth-to-opacity function and
projected with the given angles. Projection angles of 0 will render
2D regions normally.

Params
  • regions as item()*: items to render
  • min-opacity as xs:double: desired minimum opacity (default = 0.25)
  • max-opacity as xs:double: desired maximum opacity (default = 1)
  • α as xs:double: pitch degrees (default = 0)
  • β as xs:double: roll degrees (default = 0)
Returns
  • item()*: regions ordered from back to front and projected with depth-sensitive opacity
declare function this:render3d(
  $regions as item()*,
  $min-opacity as xs:double,
  $max-opacity as xs:double,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  let $zs := (
    this:process-regions($regions, this:zpoints#1, false(), "slot")
  )!point:pz(.)
  let $min-z := min($zs)
  let $max-z := max($zs)
  let $z-to-alpha := (
    function($z as xs:double) as xs:double {
      $z=>mmath:remap($max-z, $min-z, $min-opacity, $max-opacity)=>mmath:clamp(0.0, 1.0)=>mmath:decimal(3)
    }
  )
  return (
    if ($min-z = $max-z) then $regions
    else $regions=>this:render3d($z-to-alpha, $α, $β)
  )
}

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

Params
  • regions as item()*
  • α as xs:double
  • β as xs:double
Returns
  • item()*
declare function this:render3d(
  $regions as item()*,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  $regions=>this:render3d(0.25, 1.0, $α, $β)
}

Function: render3d
declare function render3d($regions as item()*) as item()*

Params
  • regions as item()*
Returns
  • item()*
declare function this:render3d(
  $regions as item()*
) as item()*
{
  $regions=>this:render3d(0.25, 1.0, 0, 0)
}

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

Params
  • items as item()*
  • function as function(map(xs:string,item()*))asmap(xs:string,item()*)*
  • keep as xs:boolean
  • exceptions as xs:string*
Returns
  • item()*
declare function this:process-regions(
  $items as item()*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean,
  $exceptions as xs:string*
) as item()*
{
  for $item in $items return (
    if ($item instance of map(xs:string,item()*))
    then geom:delegate($item, $function, $keep, $exceptions)
    else if ($keep) then $item else ()
  )
}

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

Params
  • items as item()*
  • function as function(map(xs:string,item()*))asmap(xs:string,item()*)*
  • keep as xs:boolean
Returns
  • item()*
declare function this:process-regions(
  $items as item()*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean
) as item()*
{
  for $item in $items return (
    if ($item instance of map(xs:string,item()*))
    then geom:delegate($item, $function, $keep, ())
    else if ($keep) then $item else ()
  )
}

Original Source Code

xquery version "3.1";
(:~
 : A drawing paradigm for 3d objects that does distance-based opacity
 : with projection.
 :
 : Copyright© Mary Holstege 2024
 : CC-BY (https://creativecommons.org/licenses/by/4.0/)
 : @since May 2024
 : @custom:Status Bleeding edge
 :)
module namespace this="http://mathling.com/geometric/render3d";

import module namespace config="http://mathling.com/core/config"
       at "../core/config.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace mmath="http://mathling.com/math"
       at "../math/math.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace def="http://mathling.com/type/defref"
       at "../types/defref.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 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 edge="http://mathling.com/geometric/edge"
       at "../geo/edge.xqy";
import module namespace cpoly="http://mathling.com/geometric/complex-polygon"
       at "../geo/complex-polygon.xqy";
import module namespace ellipse="http://mathling.com/geometric/ellipse"
       at "../geo/ellipse.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 project="http://mathling.com/geometric/projection"
       at "../geo/projection.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.xqy";
import module namespace gradient="http://mathling.com/svg/gradients"
       at "../svg/gradients.xqy";

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

(:~
 : render3d()
 : 3D rendering adjustment to the shapes. Edges and faces will be given
 : the opacity (alpha) determined by the depth-to-opacity function and
 : projected with the given angles. 2D regions will be left alone. If you
 : want to include them in the mix then $regions=>translate(0,0,0) will
 : do the trick.
 : @param $regions: items to render
 : @param $z-to-alpha: function mapping depth (z) to opacity
 : @param $α: pitch degrees
 : @param $β: roll degrees
 : @return regions ordered from back to front and projected with depth-sensitive opacity
 :)
declare function this:render3d(
  $regions as item()*,
  $z-to-alpha as function(xs:double) as xs:double,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  for $region in $regions return (
    if ($region instance of map(xs:string,item()*)) then (
      let $kind := util:kind($region)
      return (
        if ($kind=("sphere","ellipsoid")) then (
          let $ellipse := ellipse:ellipse(solid:center($region), solid:rx($region), solid:ry($region))
          let $min-opacity := $z-to-alpha(point:pz(solid:center($region)) - solid:rz($region))
          let $max-opacity := $z-to-alpha(point:pz(solid:center($region)) + solid:rz($region))
          let $fill := ($region("fill"), $region("colour"))[1]
          let $fill-gradient := (
            if (empty($fill)) then ()
            else if ($fill="none") then ()
            else (
              let $def := gradient:gradient($fill)
              return (
                if (exists($def)) then (
                  $def=>
                    gradient:gradient-fade($max-opacity, $min-opacity)=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")=>
                    gradient:gradient-name(rand:id($fill))
                ) else (
                  gradient:gradient-definition(
                    rand:id($fill), "outflow", ($max-opacity, $min-opacity), $fill
                  )=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")
                )
              )
            )
          )
          let $fill-ref := if (empty($fill-gradient)) then $fill else gradient:id($fill-gradient)
          let $stroke := ($region("stroke"), $region("colour"))[1]
          let $stroke-gradient := (
            if (empty($stroke)) then ()
            else if ($stroke="none") then ()
            else (
              let $def := gradient:gradient($stroke)
              return (
                if (exists($def)) then (
                  $def=>
                    gradient:gradient-fade($max-opacity, $min-opacity)=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")=>
                    gradient:gradient-name(rand:id($stroke))
                ) else (
                  gradient:gradient-definition(
                    rand:id($stroke), "normal", $min-opacity, $stroke
                  )=>
                    gradient:gradient-transform("skewX("||$α||") skewY("||$β||")")
                )
              )
            )
          )
          let $stroke-ref := if (empty($stroke-gradient)) then $stroke else gradient:id($stroke-gradient)
          return (
            def:def($fill-gradient),
            def:def($stroke-gradient),
            slot:slot(
              $ellipse=>
              ellipse:interpolate(
                ellipse:perimeter($ellipse) idiv 10 (:fineness:)
              )=>
              path:polygon()=>
              project:projection($α, $β)=>
              path:decimal(1)=>
              path:with-properties(
                util:merge-into(
                  solid:property-map($region)=>util:exclude(("opacity","colour","fill","stroke")),
                  map {
                    "colour": $fill-ref,
                    "stroke": $stroke-ref
                  }
                )
              )
            )
          )
        ) else if ($kind=solid:solids()) then (
          slot:slot(
            $region=>
              solid:ordered-translucent-faces($z-to-alpha)=>
              project:projection($α, $β)=>
              solid:as-polygon()=>
              path:decimal(1)=>
              path:with-properties(
                solid:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            solid:property-map($region)=>util:include("opacity")
          )
        ) else if (
          point:max-dimension((point:point(0,0),this:process-regions($region, this:zpoints#1, false(), "slot"))) < 3
        ) then (
          $region
        ) else if ($kind=("path","polygon")) then (
          slot:slot(
            $region=>
              path:ordered-translucent-edges($z-to-alpha)=>
              project:projection($α, $β)=>
              path:consolidate-by-colour-and-opacity()=>
              path:decimal(1)=>
              path:with-properties(
                path:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            path:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind=("edge","quad","cubic","arc","ellipse-arc")) then (
          slot:slot(
            $region=>
              path:ordered-translucent-edges($z-to-alpha)=>
              project:projection($α, $β)=>
              edge:decimal(1)=>
              edge:with-properties(
                edge:property-map($region)=>util:exclude("opacity")
              )
          )=>slot:with-properties(
            edge:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind=("complex-polygon")) then (
          slot:slot(
            for $poly in (cpoly:inners($region), cpoly:outer($region))
            return (
              slot:slot(
                $poly=>
                  path:ordered-translucent-edges($z-to-alpha)=>
                  project:projection($α, $β)=>
                  path:consolidate-by-colour-and-opacity()=>
                  path:decimal(1)=>
                  path:with-properties(
                    path:property-map($poly)=>util:exclude("opacity")
                  )
              )=>slot:with-properties(
                path:property-map($poly)=>util:include("opacity")
              )
            )
          )=>slot:with-properties(cpoly:property-map($region))
        ) else if ($kind="graph") then (
          slot:slot(
            graph:edges($region)=>
            path:ordered-translucent-edges($z-to-alpha)=>
            project:projection($α, $β)=>
            edge:decimal(1)=>
            path:with-properties(
              graph:property-map($region)=>util:exclude("opacity")
            )
          )=>slot:with-properties(
            graph:property-map($region)=>util:include("opacity")
          )
        ) else if ($kind="point") then (
          $region=>
            project:projection($α, $β)=>
            point:decimal(1)=>
            point:with-properties(
              if (exists($region("opacity")))
              then map {"opacity": $region("opacity")*$z-to-alpha(point:pz($region))}
              else map {"opacity": $z-to-alpha(point:pz($region))}
            )
        ) else if ($kind="ellipse") then (
            $region=>
              project:projection($α, $β)=>
              ellipse:decimal(1)=>
              ellipse:with-properties(
                if (exists($region("opacity")))
                then map {"opacity": $region("opacity")*$z-to-alpha(point:pz(ellipse:center($region)))}
                else map {"opacity": $z-to-alpha(point:pz(ellipse:center($region)))}
              )
        ) else if ($kind=("space","box")) then (
          path:polygon(box:edges($region))=>
            path:with-properties($region=>box:property-map())=>
            this:render3d($z-to-alpha, $α, $β)
        ) else if ($kind="wrapper") then (
          $region=>wrapper:body(
            $region=>wrapper:body()=>this:render3d($z-to-alpha, $α, $β)
          )
        ) else if ($kind="slot") then (
          (: XYZZY FIX ME: handling slot transforms, locations :)
          if ($region=>slot:body() instance of map(xs:string,item()*)*) then (
            $region=>slot:body(
              $region=>slot:body()=>this:render3d($z-to-alpha, $α, $β)
            )
          ) else (
            $region
          )
        ) else if ($kind="def") then (
          if ($region=>def:body() instance of map(xs:string,item()*)*) then (
            $region=>def:body(
              $region=>def:body()=>this:render3d($z-to-alpha, $α, $β)
            )
          ) else (
            $region
          )
        ) else if ($kind="ref") then (
          if (point:dimension(def:location($region)) < 3) then (
            $region
          ) else (
            $region=>
              def:location(
                def:location($region)=>
                  project:projection($α, $β)=>
                  geom:decimal(1)
              )=>map:put("opacity", $z-to-alpha(point:pz(def:location($region))))
          )
        ) else if ($kind=("mask","reach")) then (
          (: Bad idea to mess with opacities of reaches and masks :)
          $region=>project:projection($α, $β)=>geom:decimal(1)
        ) else (
          $region=>
            project:projection($α, $β)=>
            geom:decimal(1)=>
            geom:with-properties(
              if (exists($region("opacity")))
              then map {"opacity": $region("opacity")*$z-to-alpha(0)}
              else map {"opacity": $z-to-alpha(0)}
            )
        )
      )
    ) else (
      $region (: pass-through literal elements etc. :)
    )
  )
};

declare %private function this:zpoints(
  $regions as map(xs:string,item()*)*
) as map(xs:string,item()*)*
{
  geom:delegate($regions,
    function($region as map(xs:string,item()*)) as map(xs:string,item()*)* {
      let $kind := util:kind($region)
      return (
        if ($kind=("sphere","ellipsoid")) then (
          solid:center($region)=>point:sub(point:point(0,0,solid:rz($region))),
          solid:center($region)=>point:add(point:point(0,0,solid:rz($region)))
        ) else if ($kind=solid:solids()) then (
          solid:vertices($region)
        ) else if ($kind="ellipse") then (
          ellipse:center($region)
        ) else if ($kind="slot") then (
          (: Slots can hold things like draw point maps (XML) :)
          if (slot:body($region) instance of map(xs:string,item()*)*) then (
            this:zpoints(slot:body($region))
          ) else ()
        ) else (
          geom:vertices($region)
        )
      )
    },
    false(),
    "slot"
  )
};

(:~
 : render3d()
 : 3D rendering adjustment to the shapes. Edges and faces will be given
 : the opacity (alpha) determined by the depth-to-opacity function and
 : projected with the given angles. Projection angles of 0 will render
 : 2D regions normally.
 :
 : @param $regions: items to render
 : @param $min-opacity: desired minimum opacity (default = 0.25)
 : @param $max-opacity: desired maximum opacity (default = 1)
 : @param $α: pitch degrees (default = 0)
 : @param $β: roll degrees (default = 0)
 : @return regions ordered from back to front and projected with depth-sensitive opacity
 :)
declare function this:render3d(
  $regions as item()*,
  $min-opacity as xs:double,
  $max-opacity as xs:double,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  let $zs := (
    this:process-regions($regions, this:zpoints#1, false(), "slot")
  )!point:pz(.)
  let $min-z := min($zs)
  let $max-z := max($zs)
  let $z-to-alpha := (
    function($z as xs:double) as xs:double {
      $z=>mmath:remap($max-z, $min-z, $min-opacity, $max-opacity)=>mmath:clamp(0.0, 1.0)=>mmath:decimal(3)
    }
  )
  return (
    if ($min-z = $max-z) then $regions
    else $regions=>this:render3d($z-to-alpha, $α, $β)
  )
};

declare function this:render3d(
  $regions as item()*,
  $α as xs:double,
  $β as xs:double
) as item()*
{
  $regions=>this:render3d(0.25, 1.0, $α, $β)
};

declare function this:render3d(
  $regions as item()*
) as item()*
{
  $regions=>this:render3d(0.25, 1.0, 0, 0)
};

declare function this:process-regions(
  $items as item()*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean,
  $exceptions as xs:string*
) as item()*
{
  for $item in $items return (
    if ($item instance of map(xs:string,item()*))
    then geom:delegate($item, $function, $keep, $exceptions)
    else if ($keep) then $item else ()
  )
};

declare function this:process-regions(
  $items as item()*,
  $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*,
  $keep as xs:boolean
) as item()*
{
  for $item in $items return (
    if ($item instance of map(xs:string,item()*))
    then geom:delegate($item, $function, $keep, ())
    else if ($keep) then $item else ()
  )
};