http://mathling.com/wfc/overlapping-model library module
http://mathling.com/wfc/overlapping-model
Wave Function Collapse
This is a port and rework of WFC
See https://github.com/mxgmn/WaveFunctionCollapse
Requires png/ppm modules, which require Saxon Java extension, EXPath file
respectively.
Copyright© Mary Holstege 2022-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)
Status: Stable
Imports
http://mathling.com/core/arrayimport module namespace arr="http://mathling.com/core/array" at "../core/array.xqy"http://mathling.com/type/defref
import module namespace def="http://mathling.com/type/defref" at "../types/defref.xqy"http://mathling.com/wfc/modeldef
import module namespace modeldef="http://mathling.com/wfc/modeldef" at "modeldef.xqy"http://mathling.com/image/png
import module namespace png="http://mathling.com/image/png" at "../image/png.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/image/matrix
import module namespace matrix="http://mathling.com/image/matrix" at "../image/matrix.xqy"http://mathling.com/image/ppm
import module namespace ppm="http://mathling.com/image/ppm" at "../image/ppm.xqy"http://mathling.com/colour/rgb
import module namespace rgb="http://mathling.com/colour/rgb" at "../colourspace/rgb.xqy"http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"http://mathling.com/core/random
import module namespace rand="http://mathling.com/core/random" at "../core/random.xqy"http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities" at "../core/utilities.xqy"http://mathling.com/core/vector
import module namespace v="http://mathling.com/core/vector" at "../core/vector.xqy"http://mathling.com/wfc/model
import module namespace model="http://mathling.com/wfc/model" at "model.xqy"http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"
Functions
Function: sparse
declare function sparse($default as item()*) as map(*)
declare function sparse($default as item()*) as map(*)
Params
- default as item()*
Returns
- map(*)
declare function this:sparse($default as item()*) as map(*) { map { "kind": "sparse-propagator", "default": $default } }
Function: set
declare function set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*)
declare function set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*)
Params
- sparse3 as map(*)
- x as xs:integer
- y as xs:integer
- z as xs:integer
- val as item()*
Returns
- map(*)
declare function this:set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*) { let $xmap as map(*) := head(($sparse3($x), map{})) let $ymap as map(*) := head(($xmap($y), map{})) return $sparse3=>map:put($x, $xmap=>map:put($y, $ymap=>map:put($z, $val) ) ) }
Function: set
declare function set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*)
declare function set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*)
Params
- sparse2 as map(*)
- x as xs:integer
- y as xs:integer
- val as item()*
Returns
- map(*)
declare function this:set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*) { $sparse2=>map:put($x, head(($sparse2($x),map{}))=>map:put($y, $val) ) }
Function: get
declare function get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()*
declare function get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()*
Params
- sparse3 as map(*)
- x as xs:integer
- y as xs:integer
- z as xs:integer
Returns
- item()*
declare function this:get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()* { let $xmap := $sparse3($x) return ( if (empty($xmap)) then $sparse3("default") else ( let $ymap := $xmap($y) return ( if (empty($ymap)) then $sparse3("default") else ( head(($ymap($z), $sparse3("default"))) ) ) ) ) }
Function: get
declare function get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()*
declare function get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()*
Params
- sparse2 as map(*)
- x as xs:integer
- y as xs:integer
Returns
- item()*
declare function this:get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()* { let $xmap := $sparse2($x) return ( if (empty($xmap)) then $sparse2("default") else ( head(($xmap($y), $sparse2("default"))) ) ) }
Function: flatten
declare function flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()*
declare function flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()*
Params
- sparse3 as map(*)
- x as xs:integer
- y as xs:integer
- n as xs:integer
Returns
- item()*
declare function this:flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()* { let $xmap := $sparse3($x) return ( if (empty($xmap)) then $sparse3("default") else ( let $ymap := $xmap($y) return ( if (empty($ymap)) then $sparse3("default") else ( for $z in 1 to $n return head(($ymap($z), $sparse3("default"))) ) ) ) ) }
Function: agrees
declare function agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean
declare function agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean
agrees()
Tiles are same if we overlap offset by a row or column in the given
direction, e.g.
x x o o x x
x x o agrees o x x with direction=4 (right)
x x o o x x
Params
- m1 as map(*)
- m2 as map(*)
- dx as xs:integer
- dy as xs:integer
Returns
- xs:boolean
declare function this:agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean { let $N as xs:integer := $m1=>arr:rows() let $N as xs:integer := ( util:assert($m1=>arr:rows() = $m1=>arr:columns(), "m1.rows!=m1.columns"), util:assert($m2=>arr:rows() = $m2=>arr:columns(), "m2.rows!=m2.columns"), util:assert($m1=>arr:rows() = $m2=>arr:rows(), "m1.rows!=m2.rows"), $N ) let $xmin as xs:integer := if ($dx < 0) then 1 else $dx + 1 let $xmax as xs:integer := if ($dx < 0) then $dx + $N else $N let $ymin as xs:integer := if ($dy < 0) then 1 else $dy + 1 let $ymax as xs:integer := if ($dy < 0) then $dy + $N else $N return ( every $y in $ymin to $ymax satisfies ( every $x in $xmin to $xmax satisfies ( $m1=>arr:get($y, $x) = $m2=>arr:get($y - $dy, $x - $dx) ) ) ) }
Function: model
declare function model($model-def as map(xs:string,item()*),
$width as xs:integer,
$height as xs:integer,
$options as map(xs:string,item()*)) as map(xs:string,item()*)
declare function model($model-def as map(xs:string,item()*), $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- model-def as map(xs:string,item()*)
- width as xs:integer
- height as xs:integer
- options as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:model( $model-def as map(xs:string,item()*), $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image-type as xs:string := $model-def=>modeldef:image-type() let $symmetry as xs:integer := $model-def=>modeldef:symmetry() let $periodic-input as xs:boolean := $model-def=>modeldef:periodic-input() let $image as map(*) := $model-def=>modeldef:source-image() let $N as xs:integer := $model-def=>modeldef:tilesize() let $colours as xs:integer* := distinct-values(($image=>matrix:data())!rgb:to-int(.))=>trace("colours") let $C as xs:integer := count($colours) let $W as xs:integer := math:pow($C, $N*$N) cast as xs:integer let $SX as xs:integer := $image=>matrix:columns() let $SY as xs:integer := $image=>matrix:rows() let $sample as map(*) := let $data := for $y in 1 to $SY for $x in 1 to $SX let $colour as xs:integer := $image=>matrix:get($y, $x)=>rgb:to-int() let $cix as xs:integer := for $col at $i in $colours where $colour = $col return $i (: : -1 because we are going to represent sample arrays as integers : base C, so we need 0s :) return ($cix - 1) cast as xs:double return ( arr:array($SY (:rows:), $SX(:columns:), $data) ) let $pattern-from-index as function(xs:integer) as map(*) := function($index as xs:integer) as map(*) { let $data as xs:integer* := ( ($index=>util:as-base($C))!(. + 1) ) let $data as xs:integer* := ( for $i in 1 to $N*$N - count($data) return 1, $data ) return ( arr:array($N, $N, $data) ) } let $weights as map(*) := ( fold-left( for $y in 1 to (if ($periodic-input) then $SY else $SY - $N + 1) for $x in 1 to (if ($periodic-input) then $SX else $SX - $N + 1) return [$x, $y], map {}, function($weights as map(*), $pair as array(xs:integer)) as map(*) { let $x as xs:integer := $pair(1) let $y as xs:integer := $pair(2) let $ps as map(*)* := let $p0 as map(*) := $sample=>this:pattern-from-sample($x, $y, $N) let $p1 as map(*) := this:reflect($p0, $N) let $p2 as map(*) := this:rotate($p0, $N) let $p3 as map(*) := this:reflect($p2, $N) let $p4 as map(*) := this:rotate($p2, $N) let $p5 as map(*) := this:reflect($p4, $N) let $p6 as map(*) := this:rotate($p4, $N) let $p7 as map(*) := this:reflect($p6, $N) return ($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7) return ( fold-left(1 to $symmetry, $weights, function($weights as map(*), $k as xs:integer) as map(*) { let $index as xs:integer := this:index($ps[$k], $C) return ( if ($weights=>map:contains($index)) then $weights=>util:map-increment($index) else ( $weights=> map:put($index, 1)=> util:map-append("ordering", $index) ) ) } ) ) } ) ) let $ordering as xs:integer* := $weights("ordering") let $T as xs:integer := $weights=>map:size() - 1 let $weights as xs:double* := ( for $w at $i in $ordering return xs:double($weights($w)) ) let $patterns as map(*)* := for $w in $ordering return $pattern-from-index($w) let $propagator as array(*)* := ( for $d in 1 to 4 return array { for $t1 in 1 to $T return array { let $list as xs:integer* := for $t2 in 1 to $T where this:agrees($patterns[$t1], $patterns[$t2], $model:dx[$d], $model:dy[$d]) return $t2 return ( $list ) } } ) let $weightLogWeights as xs:double* := for $t in 1 to $T return $weights[$t] * math:log($weights[$t]) return ( (: util:log("T="||$T), util:log("propagator="||util:quote($propagator)), :) util:merge-into(( model:model($width, $height, $options), map { "kind": "overlapping-model", "colours": $colours, "patterns": $patterns, "weights": $weights, "weightLogWeights": $weightLogWeights, "T": $T, "propagator": $propagator, "on-boundary": this:on-boundary#3 } )) ) }
Function: model
declare function model($source-dir as xs:string,
$imagename as xs:string,
$image-type as xs:string,
$symmetry as xs:integer,
$periodic-input as xs:boolean,
$width as xs:integer,
$height as xs:integer,
$options as map(xs:string,item()*)) as map(xs:string,item()*)
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $periodic-input as xs:boolean, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- source-dir as xs:string
- imagename as xs:string
- image-type as xs:string
- symmetry as xs:integer
- periodic-input as xs:boolean
- width as xs:integer
- height as xs:integer
- options as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $periodic-input as xs:boolean, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := ( modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry, $periodic-input) ) return ( this:model($model-def, $width, $height, $options) ) }
Function: model
declare function model($source-dir as xs:string,
$imagename as xs:string,
$image-type as xs:string,
$symmetry as xs:integer,
$width as xs:integer,
$height as xs:integer,
$options as map(xs:string,item()*)) as map(xs:string,item()*)
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- source-dir as xs:string
- imagename as xs:string
- image-type as xs:string
- symmetry as xs:integer
- width as xs:integer
- height as xs:integer
- options as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry) return ( this:model($model-def, $width, $height, $options) ) }
Function: model
declare function model($source-dir as xs:string,
$imagename as xs:string,
$image-type as xs:string,
$width as xs:integer,
$height as xs:integer,
$options as map(xs:string,item()*)) as map(xs:string,item()*)
declare function model($source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- source-dir as xs:string
- imagename as xs:string
- image-type as xs:string
- width as xs:integer
- height as xs:integer
- options as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n"), $image-type) return ( this:model($model-def, $width, $height, $options) ) }
Function: model
declare function model($source-dir as xs:string,
$imagename as xs:string,
$width as xs:integer,
$height as xs:integer,
$options as map(xs:string,item()*)) as map(xs:string,item()*)
declare function model($source-dir as xs:string, $imagename as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*)) as map(xs:string,item()*)
Params
- source-dir as xs:string
- imagename as xs:string
- width as xs:integer
- height as xs:integer
- options as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:model( $source-dir as xs:string, $imagename as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, true()) let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n")) return ( this:model($model-def, $width, $height, $options) ) }
Function: on-boundary
declare function on-boundary($model as map(xs:string,item()*),
$x as xs:integer,
$y as xs:integer) as xs:boolean
declare function on-boundary($model as map(xs:string,item()*), $x as xs:integer, $y as xs:integer) as xs:boolean
Params
- model as map(xs:string,item()*)
- x as xs:integer
- y as xs:integer
Returns
- xs:boolean
declare function this:on-boundary( $model as map(xs:string,item()*), $x as xs:integer, $y as xs:integer ) as xs:boolean { let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $N as xs:integer := $model("N") let $periodic-output as xs:boolean := $model("periodic-output") return ( not($periodic-output) and ( $x - 1 < 0 or $y - 1 < 0 or $x - 1 + $N > $MX or $y - 1 + $N > $MY ) ) }
Function: options
declare function options($n as xs:integer,
$periodic-output as xs:boolean,
$background as xs:string,
$ground as xs:integer,
$heuristic as xs:string) as map(xs:string,item()*)
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer, $heuristic as xs:string) as map(xs:string,item()*)
Params
- n as xs:integer
- periodic-output as xs:boolean
- background as xs:string
- ground as xs:integer
- heuristic as xs:string
Returns
- map(xs:string,item()*)
declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer, $heuristic as xs:string ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background, $ground, $heuristic) }
Function: options
declare function options($n as xs:integer,
$periodic-output as xs:boolean,
$background as xs:string,
$ground as xs:integer) as map(xs:string,item()*)
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer) as map(xs:string,item()*)
Params
- n as xs:integer
- periodic-output as xs:boolean
- background as xs:string
- ground as xs:integer
Returns
- map(xs:string,item()*)
declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background, $ground) }
Function: options
declare function options($n as xs:integer,
$periodic-output as xs:boolean,
$background as xs:string) as map(xs:string,item()*)
declare function options($n as xs:integer, $periodic-output as xs:boolean, $background as xs:string) as map(xs:string,item()*)
Params
- n as xs:integer
- periodic-output as xs:boolean
- background as xs:string
Returns
- map(xs:string,item()*)
declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background) }
Function: options
declare function options($n as xs:integer,
$periodic-output as xs:boolean) as map(xs:string,item()*)
declare function options($n as xs:integer, $periodic-output as xs:boolean) as map(xs:string,item()*)
Params
- n as xs:integer
- periodic-output as xs:boolean
Returns
- map(xs:string,item()*)
declare function this:options( $n as xs:integer, $periodic-output as xs:boolean ) as map(xs:string,item()*) { model:options($n, $periodic-output) }
Function: options
declare function options($n as xs:integer) as map(xs:string,item()*)
declare function options($n as xs:integer) as map(xs:string,item()*)
Params
- n as xs:integer
Returns
- map(xs:string,item()*)
declare function this:options( $n as xs:integer ) as map(xs:string,item()*) { model:options($n) }
Function: options
declare function options() as map(xs:string,item()*)
declare function options() as map(xs:string,item()*)
Returns
- map(xs:string,item()*)
declare function this:options( ) as map(xs:string,item()*) { model:options() }
Function: run
declare function run($model as map(xs:string,item()*),
$limit as xs:integer) as map(xs:string,item()*)
declare function run($model as map(xs:string,item()*), $limit as xs:integer) as map(xs:string,item()*)
Params
- model as map(xs:string,item()*)
- limit as xs:integer
Returns
- map(xs:string,item()*)
declare function this:run( $model as map(xs:string,item()*), $limit as xs:integer ) as map(xs:string,item()*) { $model=>model:run($limit, false()) }
Function: run
declare function run($model as map(xs:string,item()*),
$limit as xs:integer,
$forced as xs:boolean) as map(xs:string,item()*)
declare function run($model as map(xs:string,item()*), $limit as xs:integer, $forced as xs:boolean) as map(xs:string,item()*)
Params
- model as map(xs:string,item()*)
- limit as xs:integer
- forced as xs:boolean
Returns
- map(xs:string,item()*)
declare function this:run( $model as map(xs:string,item()*), $limit as xs:integer, $forced as xs:boolean ) as map(xs:string,item()*) { $model=>model:run($limit, $forced) }
Function: continue
declare function continue($model as map(xs:string,item()*),
$run as map(xs:string,item()*),
$limit as xs:integer) as map(xs:string,item()*)
declare function continue($model as map(xs:string,item()*), $run as map(xs:string,item()*), $limit as xs:integer) as map(xs:string,item()*)
Params
- model as map(xs:string,item()*)
- run as map(xs:string,item()*)
- limit as xs:integer
Returns
- map(xs:string,item()*)
declare function this:continue( $model as map(xs:string,item()*), $run as map(xs:string,item()*), $limit as xs:integer ) as map(xs:string,item()*) { $model=>model:continue($run, $limit) }
Function: graphics
declare function graphics($model as map(*),
$run as map(*),
$canvas as map(xs:string,item()*)) as map(*)
declare function graphics($model as map(*), $run as map(*), $canvas as map(xs:string,item()*)) as map(*)
Params
- model as map(*)
- run as map(*)
- canvas as map(xs:string,item()*)
Returns
- map(*)
declare function this:graphics( $model as map(*), $run as map(*), $canvas as map(xs:string,item()*) ) as map(*) (: colour map :) { (: util:log("model="||util:quote($model)), util:log("run="||util:quote($run)), :) let $bg as map(xs:string,item()*) := $model("background") let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $T as xs:integer := $model("T") let $N as xs:integer := $model("N") let $patterns as map(*)* := $model("patterns") let $colours as xs:integer* := $model("colours") let $weights as xs:double* := $model("weights") let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary") let $nodes as array(*) := $run("nodes") let $observed as array(*)? := $run("observed") let $forced as xs:boolean := $run("forced") return ( if (exists($observed)) then ( util:log("Collapsed"), let $colours as map(xs:string,item()*)* := for $y as xs:integer in 1 to $MY let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1 for $x as xs:integer in 1 to $MX let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1 let $pattern as map(*) := $patterns[$observed($x - $dx + ($y - $dy - 1)*$MX)] return ( rgb:from-int($colours[$pattern=>arr:get($dy + 1, $dx + 1)]) ) return matrix:array($MY, $MX, $colours) ) else ( util:log("Uncollapsed"), let $colours as map(xs:string,item()*)* := let $pairs as array(xs:integer)* := for $dy as xs:integer in 0 to $N - 1 for $dx as xs:integer in 0 to $N - 1 return array {$dy, $dx} for $y as xs:integer in 0 to $MY - 1 for $x as xs:integer in 0 to $MX - 1 let $cva as item()* := fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0), function ($cva as item()*, $pair as array(xs:integer)) as item()* { let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) let $dy as xs:integer := $pair(1) let $dx as xs:integer := $pair(2) let $sx as xs:integer := $x - $dx let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx let $sy as xs:integer := $y - $dy let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy return ( if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva else ( let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?* let $amount as xs:integer := count($a[.=true()]) let $ts as xs:integer* := if ($forced) then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t)) else for $t in 1 to $T where $a[$t] return $t let $cv as xs:double* := fold-left($ts, $cv, function($cv as xs:double*, $t as xs:integer) as xs:double* { util:assert($a[$t], "Bad t"), let $pattern as map(*) := $patterns[$t] let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer let $colour as map(*) := rgb:from-int($colours[$index]) let $tcv := $colour=>rgb:raw-coordinates() return $cv=>v:add($tcv) } ) return ( if ($forced) then $contributors + 1 else $contributors + $amount, $cv ) ) ) } ) let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) return ( if ($contributors = 0) then $bg else ( $cv=>v:times(1 div $contributors)=>rgb:to-rgb() ) ) return ( matrix:array($MY, $MX, $colours) ) ) ) }
Function: draw
declare function draw($model as map(*),
$run as map(*),
$canvas as map(xs:string,item()*)) as item()*
declare function draw($model as map(*), $run as map(*), $canvas as map(xs:string,item()*)) as item()*
Params
- model as map(*)
- run as map(*)
- canvas as map(xs:string,item()*)
Returns
- item()*
declare function this:draw( $model as map(*), $run as map(*), $canvas as map(xs:string,item()*) ) as item()* { let $bg as map(xs:string,item()*) := $model("background") let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $T as xs:integer := $model("T") let $patterns as map(*)* := $model("patterns") let $N as xs:integer := $model("N") let $colours as xs:integer* := $model("colours") let $weights as xs:double* := $model("weights") let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary") let $nodes as array(*) := $run("nodes") let $observed as array(*)? := $run("observed") let $forced as xs:boolean := $run("forced") let $scale-x as xs:double := util:decimal(box:width($canvas) div $MX, 2) let $scale-y as xs:double := util:decimal(box:height($canvas) div $MY, 2) return ( $canvas=>map:put("fill", rgb:to-string($bg)), if (exists($observed)) then ( util:log("Collapsed"), (: We only use 1,1 N,1 1,N and N,N of the patterns :) for $colour-id as xs:integer in distinct-values( for $i in distinct-values($observed?*) let $pattern := $patterns[$i] return ( for $y in (1, $pattern=>arr:rows()) for $x in (1, $pattern=>arr:columns()) return $pattern=>arr:get($y, $x) cast as xs:integer ) ) let $colour as xs:string := $colours[$colour-id]=>rgb:from-int()=>rgb:to-string() return ( def:def("px-"||$colour-id, point:point(0, 0)=>geom:with-properties(map {"colour": $colour}) ) ) , slot:slot( for $y in 1 to $MY let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1 for $x in 1 to $MX let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1 let $tile-id as xs:integer := $observed($x - $dx + ($y - $dy - 1)*$MX) let $colour-ix as xs:integer := $patterns[$tile-id]=>arr:get($dy + 1, $dx + 1) cast as xs:integer return ( def:ref("px-"||$colour-ix, point:point($x - 1, $y - 1), map {}) ) )=>slot:scale($scale-x, $scale-y) ) else ( util:log("Uncollapsed"), slot:slot( let $pairs as array(xs:integer)* := for $dy as xs:integer in 0 to $N - 1 for $dx as xs:integer in 0 to $N - 1 return array {$dy, $dx} for $y as xs:integer in 0 to $MY - 1 for $x as xs:integer in 0 to $MX - 1 let $cva as item()* := fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0), function ($cva as item()*, $pair as array(xs:integer)) as item()* { let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) let $dy as xs:integer := $pair(1) let $dx as xs:integer := $pair(2) let $sx as xs:integer := $x - $dx let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx let $sy as xs:integer := $y - $dy let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy return ( if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva else ( let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?* let $amount as xs:integer := count($a[.=true()]) let $ts as xs:integer* := if ($forced) then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t)) else for $t in 1 to $T where $a[$t] return $t let $cv as xs:double* := fold-left($ts, $cv, function($cv as xs:double*, $t as xs:integer) as xs:double* { util:assert($a[$t], "Bad t"), let $pattern as map(*) := $patterns[$t] let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer let $colour as map(*) := rgb:from-int($colours[$index]) let $tcv := $colour=>rgb:raw-coordinates() return $cv=>v:add($tcv) } ) return ( if ($forced) then $contributors + 1 else $contributors + $amount, $cv ) ) ) } ) let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) return ( if ($contributors = 0) then () (: background :) else ( let $colour as xs:string := $cv=>v:times(1 div $contributors)=>rgb:to-rgb()=>rgb:to-string() return ( point:point($x, $y)=>geom:with-properties(map {"colour": $colour}) ) ) ) )=>slot:scale($scale-x, $scale-y) ) ) }
Original Source Code
xquery version "3.1"; (:~ : Wave Function Collapse : This is a port and rework of WFC : See https://github.com/mxgmn/WaveFunctionCollapse : Requires png/ppm modules, which require Saxon Java extension, EXPath file : respectively. : : Copyright© Mary Holstege 2022-2023 : CC-BY (https://creativecommons.org/licenses/by/4.0/) : @since July 2022 : @custom:Status Stable :) module namespace this="http://mathling.com/wfc/overlapping-model"; import module namespace rand="http://mathling.com/core/random" at "../core/random.xqy"; import module namespace errors="http://mathling.com/core/errors" at "../core/errors.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 v="http://mathling.com/core/vector" at "../core/vector.xqy"; import module namespace arr="http://mathling.com/core/array" at "../core/array.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 box="http://mathling.com/geometric/rectangle" at "../geo/rectangle.xqy"; import module namespace matrix="http://mathling.com/image/matrix" at "../image/matrix.xqy"; import module namespace ppm="http://mathling.com/image/ppm" at "../image/ppm.xqy"; import module namespace png="http://mathling.com/image/png" at "../image/png.xqy"; import module namespace rgb="http://mathling.com/colour/rgb" at "../colourspace/rgb.xqy"; import module namespace model="http://mathling.com/wfc/model" at "model.xqy"; import module namespace modeldef="http://mathling.com/wfc/modeldef" at "modeldef.xqy"; 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"; declare function this:sparse($default as item()*) as map(*) { map { "kind": "sparse-propagator", "default": $default } }; declare function this:set($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer, $val as item()*) as map(*) { let $xmap as map(*) := head(($sparse3($x), map{})) let $ymap as map(*) := head(($xmap($y), map{})) return $sparse3=>map:put($x, $xmap=>map:put($y, $ymap=>map:put($z, $val) ) ) }; declare function this:set($sparse2 as map(*), $x as xs:integer, $y as xs:integer, $val as item()*) as map(*) { $sparse2=>map:put($x, head(($sparse2($x),map{}))=>map:put($y, $val) ) }; declare function this:get($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $z as xs:integer) as item()* { let $xmap := $sparse3($x) return ( if (empty($xmap)) then $sparse3("default") else ( let $ymap := $xmap($y) return ( if (empty($ymap)) then $sparse3("default") else ( head(($ymap($z), $sparse3("default"))) ) ) ) ) }; declare function this:get($sparse2 as map(*), $x as xs:integer, $y as xs:integer) as item()* { let $xmap := $sparse2($x) return ( if (empty($xmap)) then $sparse2("default") else ( head(($xmap($y), $sparse2("default"))) ) ) }; declare function this:flatten($sparse3 as map(*), $x as xs:integer, $y as xs:integer, $n as xs:integer) as item()* { let $xmap := $sparse3($x) return ( if (empty($xmap)) then $sparse3("default") else ( let $ymap := $xmap($y) return ( if (empty($ymap)) then $sparse3("default") else ( for $z in 1 to $n return head(($ymap($z), $sparse3("default"))) ) ) ) ) }; declare %private function this:rotate( $matrix as map(*), $tilesize as xs:integer ) as map(*) { let $colour-idxs := $matrix=>arr:data() return $matrix=>arr:data( for $y in 1 to $tilesize for $x in 1 to $tilesize return $colour-idxs[$tilesize - $y + 1 + ($x - 1) * $tilesize] ) }; declare %private function this:reflect( $matrix as map(*), $tilesize as xs:integer ) as map(*) { let $colour-idxs := $matrix=>arr:data() return $matrix=>arr:data( for $y in 1 to $tilesize for $x in 1 to $tilesize return $colour-idxs[$tilesize - $x + 1 + ($y - 1) * $tilesize] ) }; declare %private function this:pattern-from-sample( $matrix as map(*), $x as xs:integer, $y as xs:integer, $tilesize as xs:integer ) as map(*) { let $data := let $SX := $matrix=>arr:columns() let $SY := $matrix=>arr:rows() for $dy in 0 to $tilesize - 1 for $dx in 0 to $tilesize - 1 return $matrix=>arr:get(util:modix($y + $dy, $SY), util:modix($x + $dx, $SX)) return arr:array($tilesize, $tilesize, $data) }; declare %private function this:index( $matrix as map(*), $C as xs:integer ) as xs:integer { (: Turn the array into a number base C, essentially :) let $data := reverse($matrix=>arr:data()) let $l as xs:integer := count($data) let $powers as xs:integer* := fold-left(1 to count($data), 1, function($powers as xs:integer*, $i as xs:integer) as xs:integer* { $powers, $powers[last()] * $C } ) return ( fold-left(1 to $l, 0, function($result as xs:integer, $i as xs:integer) as xs:integer { ($result + $powers[$i] * xs:integer($data[$i])) } ) ) }; (:~ : agrees() : Tiles are same if we overlap offset by a row or column in the given : direction, e.g. : x x o o x x : x x o agrees o x x with direction=4 (right) : x x o o x x :) declare function this:agrees($m1 as map(*), $m2 as map(*), $dx as xs:integer, $dy as xs:integer) as xs:boolean { let $N as xs:integer := $m1=>arr:rows() let $N as xs:integer := ( util:assert($m1=>arr:rows() = $m1=>arr:columns(), "m1.rows!=m1.columns"), util:assert($m2=>arr:rows() = $m2=>arr:columns(), "m2.rows!=m2.columns"), util:assert($m1=>arr:rows() = $m2=>arr:rows(), "m1.rows!=m2.rows"), $N ) let $xmin as xs:integer := if ($dx < 0) then 1 else $dx + 1 let $xmax as xs:integer := if ($dx < 0) then $dx + $N else $N let $ymin as xs:integer := if ($dy < 0) then 1 else $dy + 1 let $ymax as xs:integer := if ($dy < 0) then $dy + $N else $N return ( every $y in $ymin to $ymax satisfies ( every $x in $xmin to $xmax satisfies ( $m1=>arr:get($y, $x) = $m2=>arr:get($y - $dy, $x - $dx) ) ) ) }; (: agrees :) declare %private function this:read-tile( $source-dir as xs:string, $tilename as xs:string, $png as xs:boolean ) as map(*) { if ($png) then ( png:png-array($source-dir||"/"||$tilename||".png") ) else ( ppm:p3-array($source-dir||"/"||$tilename||".ppm") ) }; declare function this:model( $model-def as map(xs:string,item()*), $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image-type as xs:string := $model-def=>modeldef:image-type() let $symmetry as xs:integer := $model-def=>modeldef:symmetry() let $periodic-input as xs:boolean := $model-def=>modeldef:periodic-input() let $image as map(*) := $model-def=>modeldef:source-image() let $N as xs:integer := $model-def=>modeldef:tilesize() let $colours as xs:integer* := distinct-values(($image=>matrix:data())!rgb:to-int(.))=>trace("colours") let $C as xs:integer := count($colours) let $W as xs:integer := math:pow($C, $N*$N) cast as xs:integer let $SX as xs:integer := $image=>matrix:columns() let $SY as xs:integer := $image=>matrix:rows() let $sample as map(*) := let $data := for $y in 1 to $SY for $x in 1 to $SX let $colour as xs:integer := $image=>matrix:get($y, $x)=>rgb:to-int() let $cix as xs:integer := for $col at $i in $colours where $colour = $col return $i (: : -1 because we are going to represent sample arrays as integers : base C, so we need 0s :) return ($cix - 1) cast as xs:double return ( arr:array($SY (:rows:), $SX(:columns:), $data) ) let $pattern-from-index as function(xs:integer) as map(*) := function($index as xs:integer) as map(*) { let $data as xs:integer* := ( ($index=>util:as-base($C))!(. + 1) ) let $data as xs:integer* := ( for $i in 1 to $N*$N - count($data) return 1, $data ) return ( arr:array($N, $N, $data) ) } let $weights as map(*) := ( fold-left( for $y in 1 to (if ($periodic-input) then $SY else $SY - $N + 1) for $x in 1 to (if ($periodic-input) then $SX else $SX - $N + 1) return [$x, $y], map {}, function($weights as map(*), $pair as array(xs:integer)) as map(*) { let $x as xs:integer := $pair(1) let $y as xs:integer := $pair(2) let $ps as map(*)* := let $p0 as map(*) := $sample=>this:pattern-from-sample($x, $y, $N) let $p1 as map(*) := this:reflect($p0, $N) let $p2 as map(*) := this:rotate($p0, $N) let $p3 as map(*) := this:reflect($p2, $N) let $p4 as map(*) := this:rotate($p2, $N) let $p5 as map(*) := this:reflect($p4, $N) let $p6 as map(*) := this:rotate($p4, $N) let $p7 as map(*) := this:reflect($p6, $N) return ($p0, $p1, $p2, $p3, $p4, $p5, $p6, $p7) return ( fold-left(1 to $symmetry, $weights, function($weights as map(*), $k as xs:integer) as map(*) { let $index as xs:integer := this:index($ps[$k], $C) return ( if ($weights=>map:contains($index)) then $weights=>util:map-increment($index) else ( $weights=> map:put($index, 1)=> util:map-append("ordering", $index) ) ) } ) ) } ) ) let $ordering as xs:integer* := $weights("ordering") let $T as xs:integer := $weights=>map:size() - 1 let $weights as xs:double* := ( for $w at $i in $ordering return xs:double($weights($w)) ) let $patterns as map(*)* := for $w in $ordering return $pattern-from-index($w) let $propagator as array(*)* := ( for $d in 1 to 4 return array { for $t1 in 1 to $T return array { let $list as xs:integer* := for $t2 in 1 to $T where this:agrees($patterns[$t1], $patterns[$t2], $model:dx[$d], $model:dy[$d]) return $t2 return ( $list ) } } ) let $weightLogWeights as xs:double* := for $t in 1 to $T return $weights[$t] * math:log($weights[$t]) return ( (: util:log("T="||$T), util:log("propagator="||util:quote($propagator)), :) util:merge-into(( model:model($width, $height, $options), map { "kind": "overlapping-model", "colours": $colours, "patterns": $patterns, "weights": $weights, "weightLogWeights": $weightLogWeights, "T": $T, "propagator": $propagator, "on-boundary": this:on-boundary#3 } )) ) }; declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $periodic-input as xs:boolean, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := ( modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry, $periodic-input) ) return ( this:model($model-def, $width, $height, $options) ) }; declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $symmetry as xs:integer, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n"), $image-type, $symmetry) return ( this:model($model-def, $width, $height, $options) ) }; declare function this:model( $source-dir as xs:string, $imagename as xs:string, $image-type as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, $image-type="png") let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n"), $image-type) return ( this:model($model-def, $width, $height, $options) ) }; declare function this:model( $source-dir as xs:string, $imagename as xs:string, $width as xs:integer, $height as xs:integer, $options as map(xs:string,item()*) ) as map(xs:string,item()*) { let $image as map(*) := this:read-tile($source-dir, $imagename, true()) let $model-def as map(xs:string,item()*) := modeldef:sampled-modeldef($image, $options("n")) return ( this:model($model-def, $width, $height, $options) ) }; declare function this:on-boundary( $model as map(xs:string,item()*), $x as xs:integer, $y as xs:integer ) as xs:boolean { let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $N as xs:integer := $model("N") let $periodic-output as xs:boolean := $model("periodic-output") return ( not($periodic-output) and ( $x - 1 < 0 or $y - 1 < 0 or $x - 1 + $N > $MX or $y - 1 + $N > $MY ) ) }; declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer, $heuristic as xs:string ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background, $ground, $heuristic) }; declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string, $ground as xs:integer ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background, $ground) }; declare function this:options( $n as xs:integer, $periodic-output as xs:boolean, $background as xs:string ) as map(xs:string,item()*) { model:options($n, $periodic-output, $background) }; declare function this:options( $n as xs:integer, $periodic-output as xs:boolean ) as map(xs:string,item()*) { model:options($n, $periodic-output) }; declare function this:options( $n as xs:integer ) as map(xs:string,item()*) { model:options($n) }; declare function this:options( ) as map(xs:string,item()*) { model:options() }; declare function this:run( $model as map(xs:string,item()*), $limit as xs:integer ) as map(xs:string,item()*) { $model=>model:run($limit, false()) }; declare function this:run( $model as map(xs:string,item()*), $limit as xs:integer, $forced as xs:boolean ) as map(xs:string,item()*) { $model=>model:run($limit, $forced) }; declare function this:continue( $model as map(xs:string,item()*), $run as map(xs:string,item()*), $limit as xs:integer ) as map(xs:string,item()*) { $model=>model:continue($run, $limit) }; declare function this:graphics( $model as map(*), $run as map(*), $canvas as map(xs:string,item()*) ) as map(*) (: colour map :) { (: util:log("model="||util:quote($model)), util:log("run="||util:quote($run)), :) let $bg as map(xs:string,item()*) := $model("background") let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $T as xs:integer := $model("T") let $N as xs:integer := $model("N") let $patterns as map(*)* := $model("patterns") let $colours as xs:integer* := $model("colours") let $weights as xs:double* := $model("weights") let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary") let $nodes as array(*) := $run("nodes") let $observed as array(*)? := $run("observed") let $forced as xs:boolean := $run("forced") return ( if (exists($observed)) then ( util:log("Collapsed"), let $colours as map(xs:string,item()*)* := for $y as xs:integer in 1 to $MY let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1 for $x as xs:integer in 1 to $MX let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1 let $pattern as map(*) := $patterns[$observed($x - $dx + ($y - $dy - 1)*$MX)] return ( rgb:from-int($colours[$pattern=>arr:get($dy + 1, $dx + 1)]) ) return matrix:array($MY, $MX, $colours) ) else ( util:log("Uncollapsed"), let $colours as map(xs:string,item()*)* := let $pairs as array(xs:integer)* := for $dy as xs:integer in 0 to $N - 1 for $dx as xs:integer in 0 to $N - 1 return array {$dy, $dx} for $y as xs:integer in 0 to $MY - 1 for $x as xs:integer in 0 to $MX - 1 let $cva as item()* := fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0), function ($cva as item()*, $pair as array(xs:integer)) as item()* { let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) let $dy as xs:integer := $pair(1) let $dx as xs:integer := $pair(2) let $sx as xs:integer := $x - $dx let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx let $sy as xs:integer := $y - $dy let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy return ( if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva else ( let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?* let $amount as xs:integer := count($a[.=true()]) let $ts as xs:integer* := if ($forced) then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t)) else for $t in 1 to $T where $a[$t] return $t let $cv as xs:double* := fold-left($ts, $cv, function($cv as xs:double*, $t as xs:integer) as xs:double* { util:assert($a[$t], "Bad t"), let $pattern as map(*) := $patterns[$t] let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer let $colour as map(*) := rgb:from-int($colours[$index]) let $tcv := $colour=>rgb:raw-coordinates() return $cv=>v:add($tcv) } ) return ( if ($forced) then $contributors + 1 else $contributors + $amount, $cv ) ) ) } ) let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) return ( if ($contributors = 0) then $bg else ( $cv=>v:times(1 div $contributors)=>rgb:to-rgb() ) ) return ( matrix:array($MY, $MX, $colours) ) ) ) }; (: graphics :) declare function this:draw( $model as map(*), $run as map(*), $canvas as map(xs:string,item()*) ) as item()* { let $bg as map(xs:string,item()*) := $model("background") let $MX as xs:integer := $model("MX") let $MY as xs:integer := $model("MY") let $T as xs:integer := $model("T") let $patterns as map(*)* := $model("patterns") let $N as xs:integer := $model("N") let $colours as xs:integer* := $model("colours") let $weights as xs:double* := $model("weights") let $on-boundary as function(map(xs:string,item()*), xs:integer, xs:integer) as xs:boolean := $model("on-boundary") let $nodes as array(*) := $run("nodes") let $observed as array(*)? := $run("observed") let $forced as xs:boolean := $run("forced") let $scale-x as xs:double := util:decimal(box:width($canvas) div $MX, 2) let $scale-y as xs:double := util:decimal(box:height($canvas) div $MY, 2) return ( $canvas=>map:put("fill", rgb:to-string($bg)), if (exists($observed)) then ( util:log("Collapsed"), (: We only use 1,1 N,1 1,N and N,N of the patterns :) for $colour-id as xs:integer in distinct-values( for $i in distinct-values($observed?*) let $pattern := $patterns[$i] return ( for $y in (1, $pattern=>arr:rows()) for $x in (1, $pattern=>arr:columns()) return $pattern=>arr:get($y, $x) cast as xs:integer ) ) let $colour as xs:string := $colours[$colour-id]=>rgb:from-int()=>rgb:to-string() return ( def:def("px-"||$colour-id, point:point(0, 0)=>geom:with-properties(map {"colour": $colour}) ) ) , slot:slot( for $y in 1 to $MY let $dy as xs:integer := if ($y < $MY - $N + 1) then 0 else $N - 1 for $x in 1 to $MX let $dx as xs:integer := if ($x < $MX - $N + 1) then 0 else $N - 1 let $tile-id as xs:integer := $observed($x - $dx + ($y - $dy - 1)*$MX) let $colour-ix as xs:integer := $patterns[$tile-id]=>arr:get($dy + 1, $dx + 1) cast as xs:integer return ( def:ref("px-"||$colour-ix, point:point($x - 1, $y - 1), map {}) ) )=>slot:scale($scale-x, $scale-y) ) else ( util:log("Uncollapsed"), slot:slot( let $pairs as array(xs:integer)* := for $dy as xs:integer in 0 to $N - 1 for $dx as xs:integer in 0 to $N - 1 return array {$dy, $dx} for $y as xs:integer in 0 to $MY - 1 for $x as xs:integer in 0 to $MX - 1 let $cva as item()* := fold-left($pairs, (0, 0.0E0, 0.0E0, 0.0E0), function ($cva as item()*, $pair as array(xs:integer)) as item()* { let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) let $dy as xs:integer := $pair(1) let $dx as xs:integer := $pair(2) let $sx as xs:integer := $x - $dx let $sx as xs:integer := if ($sx < 0) then $sx + $MX else $sx let $sy as xs:integer := $y - $dy let $sy as xs:integer := if ($sy < 0) then $sy + $MY else $sy return ( if ($model=>$on-boundary($sx + 1, $sy + 1)) then $cva else ( let $a as xs:boolean* := $nodes($sx + 1 + ($sy + 1 - 1)*$MX)("wave")?* let $amount as xs:integer := count($a[.=true()]) let $ts as xs:integer* := if ($forced) then head(rand:shuffle(for $t in 1 to $T where $a[$t] return $t)) else for $t in 1 to $T where $a[$t] return $t let $cv as xs:double* := fold-left($ts, $cv, function($cv as xs:double*, $t as xs:integer) as xs:double* { util:assert($a[$t], "Bad t"), let $pattern as map(*) := $patterns[$t] let $index as xs:integer := $pattern=>arr:get($dy + 1, $dx + 1) cast as xs:integer let $colour as map(*) := rgb:from-int($colours[$index]) let $tcv := $colour=>rgb:raw-coordinates() return $cv=>v:add($tcv) } ) return ( if ($forced) then $contributors + 1 else $contributors + $amount, $cv ) ) ) } ) let $contributors as xs:integer := head($cva) let $cv as xs:double* := tail($cva) return ( if ($contributors = 0) then () (: background :) else ( let $colour as xs:string := $cv=>v:times(1 div $contributors)=>rgb:to-rgb()=>rgb:to-string() return ( point:point($x, $y)=>geom:with-properties(map {"colour": $colour}) ) ) ) )=>slot:scale($scale-x, $scale-y) ) ) };