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/)
Status: Bleeding edge
Function Index
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()*process-regions($items as item()*, $function as function(map(xs:string,item()*)) as map(xs:string,item()*)*, $keep as xs:boolean) as item()*render3d($regions as item()*, $z-to-alpha as function(xs:double) as xs:double, $α as xs:double, $β as xs:double) as item()*render3d($regions as item()*, $min-opacity as xs:double, $max-opacity as xs:double, $α as xs:double, $β as xs:double) as item()*render3d($regions as item()*, $α as xs:double, $β as xs:double) as item()*render3d($regions as item()*) as item()*zpoints($regions as map(xs:string,item()*)*) as map(xs:string,item()*)*
Imports
http://mathling.com/geometric/solidimport module namespace solid="http://mathling.com/geometric/solid"
at "../geo/solid.xqy"http://mathling.com/type/defrefimport module namespace def="http://mathling.com/type/defref"
at "../types/defref.xqy"http://mathling.com/mathimport module namespace mmath="http://mathling.com/math"
at "../math/math.xqy"http://mathling.com/type/reachimport module namespace reach="http://mathling.com/type/reach"
at "../types/reach.xqy"http://mathling.com/svg/gradientsimport module namespace gradient="http://mathling.com/svg/gradients"
at "../svg/gradients.xqy"http://mathling.com/geometric/complex-polygonimport module namespace cpoly="http://mathling.com/geometric/complex-polygon"
at "../geo/complex-polygon.xqy"http://mathling.com/geometric/projectionimport module namespace project="http://mathling.com/geometric/projection"
at "../geo/projection.xqy"http://mathling.com/type/wrapperimport module namespace wrapper="http://mathling.com/type/wrapper"
at "../types/wrapper.xqy"http://mathling.com/geometric/rectangleimport module namespace box="http://mathling.com/geometric/rectangle"
at "../geo/rectangle.xqy"http://mathling.com/type/slotimport module namespace slot="http://mathling.com/type/slot"
at "../types/slot.xqy"http://mathling.com/geometricimport module namespace geom="http://mathling.com/geometric"
at "../geo/euclidean.xqy"http://mathling.com/geometric/pointimport module namespace point="http://mathling.com/geometric/point"
at "../geo/point.xqy"http://mathling.com/geometric/graphimport module namespace graph="http://mathling.com/geometric/graph"
at "../geo/graph.xqy"http://mathling.com/core/randomimport module namespace rand="http://mathling.com/core/random"
at "../core/random.xqy"http://mathling.com/geometric/pathimport module namespace path="http://mathling.com/geometric/path"
at "../geo/path.xqy"http://mathling.com/geometric/edgeimport module namespace edge="http://mathling.com/geometric/edge"
at "../geo/edge.xqy"http://mathling.com/core/utilitiesimport module namespace util="http://mathling.com/core/utilities"
at "../core/utilities.xqy"http://mathling.com/geometric/ellipseimport module namespace ellipse="http://mathling.com/geometric/ellipse"
at "../geo/ellipse.xqy"http://mathling.com/core/configimport module namespace config="http://mathling.com/core/config"
at "../core/config.xqy"http://mathling.com/core/errorsimport module namespace errors="http://mathling.com/core/errors"
at "../core/errors.xqy"http://mathling.com/type/maskimport 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()*
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()*
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()*
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()*
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()*
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()*
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 ()
)
};