http://mathling.com/wfc/image-tiled-model  library module

http://mathling.com/wfc/image-tiled-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.
inferred-model() infers a simple tiling model from a source image.
Larger tile sizes on inferred model => more constraints => prettier result,
but slower.

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

July 2022
Status: Stable

Imports

http://mathling.com/core/array
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.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/wfc/simple-tiled-model
import module namespace simple="http://mathling.com/wfc/simple-tiled-model"
       at "simple-tiled-model.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/core/random
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy"
http://mathling.com/art/core
import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy"
http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.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: 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()*)

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()*)
{
  if (not($model-def=>modeldef:image-type() = ("png","ppm")))
  then errors:error("WFC-BADFORMAT", $model-def=>modeldef:image-type())
  else
  let $image-type as xs:string := $model-def=>modeldef:image-type()
  (: Define these as no-ops because all rotations/reflections done already :)
  let $rotate as function(map(*), xs:integer) as map(*) :=
    function($pattern as map(*), $tilesize as xs:integer) as map(*) {$pattern}
  let $reflect as function(map(*), xs:integer) as map(*) := 
    function($pattern as map(*), $tilesize as xs:integer) as map(*) {$pattern}
  let $make-tile as function(xs:string, xs:integer) as map(*) :=
    function ($tilename as xs:string, $tilesize as xs:integer) as map(*) {
      this:read-tile($options("source-dir"), $tilename, $image-type="png")
    }
  return (
    simple:base-model(
      $model-def,
      $width,
      $height,
      $make-tile,
      $rotate,
      $reflect,
      $options
    )
  )
}

Function: model
declare function model($source-dir as xs:string, $subset-name 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
  • subset-name 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,
  $subset-name as xs:string?,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $model-node as element() := doc($source-dir||"/data.xml")/set
  let $model-def as map(xs:string,item()*) := modeldef:parse($model-node, $subset-name)
  return (
    this:model(
      $model-def, $width, $height, $options=>map:put("source-dir", $source-dir)
    )
  )
}

Function: edge-aligns
declare function edge-aligns($m1 as map(*), $m2 as map(*)) as xs:boolean


edge-aligns()
Tiles are same along given row or column, e.g.
o x o o x x
x o o aligns o x x with direction=4 (right)
x x o o x x

Params
  • m1 as map(*)
  • m2 as map(*)
Returns
  • xs:boolean
declare function this:edge-aligns(
  $m1 as map(*),
  $m2 as map(*)
) 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 $left-column-m1 as xs:double* := $m1=>arr:column($N)
  let $right-column-m2 as xs:double* := $m2=>arr:column($N)
  return (
    every $i in 1 to $N
    satisfies $left-column-m1[$i] = $right-column-m2[$i]
  )
}

Function: infer-neighbours
declare function infer-neighbours($patterns as map(*)*, $keep as xs:integer) as map(xs:string,item()*)*

Params
  • patterns as map(*)*
  • keep as xs:integer
Returns
  • map(xs:string,item()*)*
declare function this:infer-neighbours(
  $patterns as map(*)*,
  $keep as xs:integer
) as map(xs:string,item()*)*
{
  for $p1 at $i in $patterns
  for $p2 at $j in $patterns
  where this:edge-aligns($p1, $p2) and rand:flip($keep)
  return modeldef:neighbour(string($i), string($j))
}

Function: base-inferred-model
declare function base-inferred-model($model-def as map(xs:string,item()*), $patterns as map(*)*, $colours as xs:integer*, $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()*)
  • patterns as map(*)*
  • colours 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:base-inferred-model(
  $model-def as map(xs:string,item()*),
  $patterns as map(*)*,
  $colours as xs:integer*,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (not($model-def=>modeldef:image-type() = ("png","ppm")))
  then errors:error("WFC-BADFORMAT", $model-def=>modeldef:image-type())
  else
  let $rotate as function(map(*), xs:integer) as map(*) := this:rotate-pattern#2
  let $reflect as function(map(*), xs:integer) as map(*) := this:reflect-pattern#2
  let $make-tile as function(xs:string, xs:integer) as map(*) :=
    function ($tilename as xs:string, $tilesize as xs:integer) as map(*) {
      let $ix as xs:integer := xs:integer(substring-before($tilename," "))
      let $pattern as map(*) := $patterns[$ix]
      let $data := (
        for $v in $pattern=>arr:data()
        return rgb:from-int($colours[xs:integer($v)])
      )
      return matrix:array($tilesize, $tilesize, $data)
    }
  return (
    simple:base-model(
      $model-def,
      $width,
      $height,
      $make-tile,
      $rotate,
      $reflect,
      $options
    )
  )
}

Function: sample-tiles
declare function sample-tiles($sample as map(*), $N as xs:integer, $periodic-input as xs:boolean, $sample-percent as xs:integer) as map(*)*

Params
  • sample as map(*)
  • N as xs:integer
  • periodic-input as xs:boolean
  • sample-percent as xs:integer
Returns
  • map(*)*
declare function this:sample-tiles(
  $sample as map(*),
  $N as xs:integer,
  $periodic-input as xs:boolean,
  $sample-percent as xs:integer
) as map(*)*
{
  let $SX := $sample=>arr:columns()
  let $SY := $sample=>arr:rows()
  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 (
    if (rand:flip($sample-percent))
    then $sample=>this:pattern-from-sample($x, $y, $N)
    else ()
  )
}

Function: inferred-model
declare function inferred-model($sampled-modeldef 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
  • sampled-modeldef 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:inferred-model(
  $sampled-modeldef 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 as map(*) := $sampled-modeldef=>modeldef:source-image()
  let $image-type as xs:string := $sampled-modeldef=>modeldef:image-type()
  let $symmetry as xs:integer := $sampled-modeldef=>modeldef:symmetry()
  let $periodic-input as xs:boolean := $sampled-modeldef=>modeldef:periodic-input()
  let $image as map(*) := $sampled-modeldef=>modeldef:source-image()
  let $N as xs:integer := $sampled-modeldef=>modeldef:tilesize()
  let $keep as xs:integer := $sampled-modeldef=>modeldef:keep-percent()

  let $colours as xs:integer* := 
   distinct-values(($image=>matrix:data())!rgb:to-int(.))
  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 (
        util:assert(
          rgb:from-int($colour)=>rgb:to-string()=$image=>matrix:get($y, $x)=>rgb:to-string(),
          $colour||" "||(rgb:from-int($colour)=>rgb:to-string())||($image=>matrix:get($y, $x)=>rgb:to-string())
        ),
        ($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-pattern($p0, $N)
          let $p2 as map(*) := this:rotate-pattern($p0, $N)
          let $p3 as map(*) := this:reflect-pattern($p2, $N)
          let $p4 as map(*) := this:rotate-pattern($p2, $N)
          let $p5 as map(*) := this:reflect-pattern($p4, $N)
          let $p6 as map(*) := this:rotate-pattern($p4, $N)
          let $p7 as map(*) := this:reflect-pattern($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 $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 $tiles as map(xs:string,item()*)* :=
    for $w at $i in $ordering return (
      modeldef:tile(string($i), "X", $weights[$i])
    )
  let $neighbours as map(xs:string,item()*)* :=
    this:infer-neighbours($patterns, $keep)
  return (
    (: util:log(count($patterns)||" "||util:quote($patterns)),
    util:log(util:quote($neighbours)), :)
    this:base-inferred-model(
      modeldef:modeldef(
        $N,
        $tiles,
        $neighbours,
        $image-type,
        true() (: every tile unique :)
      ),
      $patterns,
      $colours,
      $width,
      $height,
      $options
    )
  )
}

Function: inferred-model
declare function inferred-model($source-dir as xs:string, $tilename as xs:string, $tilesize as xs:integer, $image-type as xs:string, $symmetry as xs:integer, $periodic-input as xs:boolean, $keep-percent 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
  • tilename as xs:string
  • tilesize as xs:integer
  • image-type as xs:string
  • symmetry as xs:integer
  • periodic-input as xs:boolean
  • keep-percent 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:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $periodic-input as xs:boolean,
  $keep-percent 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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry, $periodic-input, $keep-percent)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
}

Function: inferred-model
declare function inferred-model($source-dir as xs:string, $tilename as xs:string, $tilesize as xs:integer, $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
  • tilename as xs:string
  • tilesize as xs:integer
  • 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:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry, $periodic-input)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
}

Function: inferred-model
declare function inferred-model($source-dir as xs:string, $tilename as xs:string, $tilesize as xs:integer, $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
  • tilename as xs:string
  • tilesize as xs:integer
  • 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:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
}

Function: inferred-model
declare function inferred-model($source-dir as xs:string, $tilename as xs:string, $tilesize as xs:integer, $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
  • tilename as xs:string
  • tilesize as xs:integer
  • 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:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
}

Function: inferred-model
declare function inferred-model($source-dir as xs:string, $tilename as xs:string, $tilesize 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
  • tilename as xs:string
  • tilesize 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:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize 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, $tilename, true())
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
}

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()*)

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()*)

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()*)

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()*)

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()*)

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()*)

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()*)

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()*)

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()*)

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(*)

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 :)
{
  simple:graphics($model, $run, $canvas)
}

Function: draw
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()*
{
  simple:draw($model, $run, $canvas)
}

Function: draw
declare function draw($model as map(*), $run as map(*), $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*), $edge-width as function(map(xs:string,item()*)) as xs:double?, $canvas as map(xs:string,item()*)) as item()*

Params
  • model as map(*)
  • run as map(*)
  • mutation as function(map(xs:string,item()*))asmap(xs:string,item()*)
  • edge-width as function(map(xs:string,item()*))asxs:double?
  • canvas as map(xs:string,item()*)
Returns
  • item()*
declare function this:draw(
  $model as map(*),
  $run as map(*),
  $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*),
  $edge-width as function(map(xs:string,item()*)) as xs:double?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  simple:draw($model, $run, $mutation, $edge-width, $canvas)
}

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.
 : inferred-model() infers a simple tiling model from a source image.
 : Larger tile sizes on inferred model => more constraints => prettier result,
 : but slower.
 :
 : 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/image-tiled-model";

import module namespace core="http://mathling.com/art/core"
       at "../art/core.xqy";
import module namespace errors="http://mathling.com/core/errors"
       at "../core/errors.xqy";
import module namespace util="http://mathling.com/core/utilities"
       at "../core/utilities.xqy";
import module namespace arr="http://mathling.com/core/array"
       at "../core/array.xqy";
import module namespace rand="http://mathling.com/core/random"
       at "../core/random.xqy";
import module namespace geom="http://mathling.com/geometric"
       at "../geo/euclidean.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 simple="http://mathling.com/wfc/simple-tiled-model"
       at "simple-tiled-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 %private function this:rotate(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colours := $matrix=>matrix:data()
  return $matrix=>matrix:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colours[$tilesize - $y + 1 + ($x - 1) * $tilesize]
  )
};

declare %private function this:reflect(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colours := $matrix=>matrix:data()
  return $matrix=>matrix:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colours[$tilesize - $x + 1 + ($y - 1) * $tilesize]
  )
};

declare %private function this:rotate-pattern(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colours := $matrix=>arr:data()
  return $matrix=>arr:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colours[$tilesize - $y + 1 + ($x - 1) * $tilesize]
  )
};

declare %private function this:reflect-pattern(
  $matrix as map(*),
  $tilesize as xs:integer
) as map(*)
{
  let $colours := $matrix=>arr:data()
  return $matrix=>arr:data(
    for $y in 1 to $tilesize
    for $x in 1 to $tilesize
    return $colours[$tilesize - $x + 1 + ($y - 1) * $tilesize]
  )
};

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()*)
{
  if (not($model-def=>modeldef:image-type() = ("png","ppm")))
  then errors:error("WFC-BADFORMAT", $model-def=>modeldef:image-type())
  else
  let $image-type as xs:string := $model-def=>modeldef:image-type()
  (: Define these as no-ops because all rotations/reflections done already :)
  let $rotate as function(map(*), xs:integer) as map(*) :=
    function($pattern as map(*), $tilesize as xs:integer) as map(*) {$pattern}
  let $reflect as function(map(*), xs:integer) as map(*) := 
    function($pattern as map(*), $tilesize as xs:integer) as map(*) {$pattern}
  let $make-tile as function(xs:string, xs:integer) as map(*) :=
    function ($tilename as xs:string, $tilesize as xs:integer) as map(*) {
      this:read-tile($options("source-dir"), $tilename, $image-type="png")
    }
  return (
    simple:base-model(
      $model-def,
      $width,
      $height,
      $make-tile,
      $rotate,
      $reflect,
      $options
    )
  )
};

declare function this:model(
  $source-dir as xs:string,
  $subset-name as xs:string?,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  let $model-node as element() := doc($source-dir||"/data.xml")/set
  let $model-def as map(xs:string,item()*) := modeldef:parse($model-node, $subset-name)
  return (
    this:model(
      $model-def, $width, $height, $options=>map:put("source-dir", $source-dir)
    )
  )
};

(:~
 : edge-aligns()
 : Tiles are same along given row or column, e.g.
 : o x o        o x x
 : x o o aligns o x x with direction=4 (right)
 : x x o        o x x
 :)
declare function this:edge-aligns(
  $m1 as map(*),
  $m2 as map(*)
) 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 $left-column-m1 as xs:double* := $m1=>arr:column($N)
  let $right-column-m2 as xs:double* := $m2=>arr:column($N)
  return (
    every $i in 1 to $N
    satisfies $left-column-m1[$i] = $right-column-m2[$i]
  )
};

declare function this:infer-neighbours(
  $patterns as map(*)*,
  $keep as xs:integer
) as map(xs:string,item()*)*
{
  for $p1 at $i in $patterns
  for $p2 at $j in $patterns
  where this:edge-aligns($p1, $p2) and rand:flip($keep)
  return modeldef:neighbour(string($i), string($j))
};

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]))
      }
    )
  )
};

declare function this:base-inferred-model(
  $model-def as map(xs:string,item()*),
  $patterns as map(*)*,
  $colours as xs:integer*,
  $width as xs:integer,
  $height as xs:integer,
  $options as map(xs:string,item()*)
) as map(xs:string,item()*)
{
  if (not($model-def=>modeldef:image-type() = ("png","ppm")))
  then errors:error("WFC-BADFORMAT", $model-def=>modeldef:image-type())
  else
  let $rotate as function(map(*), xs:integer) as map(*) := this:rotate-pattern#2
  let $reflect as function(map(*), xs:integer) as map(*) := this:reflect-pattern#2
  let $make-tile as function(xs:string, xs:integer) as map(*) :=
    function ($tilename as xs:string, $tilesize as xs:integer) as map(*) {
      let $ix as xs:integer := xs:integer(substring-before($tilename," "))
      let $pattern as map(*) := $patterns[$ix]
      let $data := (
        for $v in $pattern=>arr:data()
        return rgb:from-int($colours[xs:integer($v)])
      )
      return matrix:array($tilesize, $tilesize, $data)
    }
  return (
    simple:base-model(
      $model-def,
      $width,
      $height,
      $make-tile,
      $rotate,
      $reflect,
      $options
    )
  )
};

declare function this:sample-tiles(
  $sample as map(*),
  $N as xs:integer,
  $periodic-input as xs:boolean,
  $sample-percent as xs:integer
) as map(*)*
{
  let $SX := $sample=>arr:columns()
  let $SY := $sample=>arr:rows()
  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 (
    if (rand:flip($sample-percent))
    then $sample=>this:pattern-from-sample($x, $y, $N)
    else ()
  )
};

declare function this:inferred-model(
  $sampled-modeldef 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 as map(*) := $sampled-modeldef=>modeldef:source-image()
  let $image-type as xs:string := $sampled-modeldef=>modeldef:image-type()
  let $symmetry as xs:integer := $sampled-modeldef=>modeldef:symmetry()
  let $periodic-input as xs:boolean := $sampled-modeldef=>modeldef:periodic-input()
  let $image as map(*) := $sampled-modeldef=>modeldef:source-image()
  let $N as xs:integer := $sampled-modeldef=>modeldef:tilesize()
  let $keep as xs:integer := $sampled-modeldef=>modeldef:keep-percent()

  let $colours as xs:integer* := 
   distinct-values(($image=>matrix:data())!rgb:to-int(.))
  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 (
        util:assert(
          rgb:from-int($colour)=>rgb:to-string()=$image=>matrix:get($y, $x)=>rgb:to-string(),
          $colour||" "||(rgb:from-int($colour)=>rgb:to-string())||($image=>matrix:get($y, $x)=>rgb:to-string())
        ),
        ($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-pattern($p0, $N)
          let $p2 as map(*) := this:rotate-pattern($p0, $N)
          let $p3 as map(*) := this:reflect-pattern($p2, $N)
          let $p4 as map(*) := this:rotate-pattern($p2, $N)
          let $p5 as map(*) := this:reflect-pattern($p4, $N)
          let $p6 as map(*) := this:rotate-pattern($p4, $N)
          let $p7 as map(*) := this:reflect-pattern($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 $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 $tiles as map(xs:string,item()*)* :=
    for $w at $i in $ordering return (
      modeldef:tile(string($i), "X", $weights[$i])
    )
  let $neighbours as map(xs:string,item()*)* :=
    this:infer-neighbours($patterns, $keep)
  return (
    (: util:log(count($patterns)||" "||util:quote($patterns)),
    util:log(util:quote($neighbours)), :)
    this:base-inferred-model(
      modeldef:modeldef(
        $N,
        $tiles,
        $neighbours,
        $image-type,
        true() (: every tile unique :)
      ),
      $patterns,
      $colours,
      $width,
      $height,
      $options
    )
  )
};

declare function this:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $image-type as xs:string,
  $symmetry as xs:integer,
  $periodic-input as xs:boolean,
  $keep-percent 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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry, $periodic-input, $keep-percent)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
};

declare function this:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry, $periodic-input)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
};

declare function this:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type, $symmetry)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
};

declare function this:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize as xs:integer,
  $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, $tilename, $image-type="png")
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize, $image-type)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
};

declare function this:inferred-model(
  $source-dir as xs:string,
  $tilename as xs:string,
  $tilesize 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, $tilename, true())
  let $model-def as map(xs:string,item()*) :=
    modeldef:sampled-modeldef($image, $tilesize)
  return (
    this:inferred-model($model-def, $width, $height, $options)
  )
};

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 :)
{
  simple:graphics($model, $run, $canvas)
};

declare function this:draw(
  $model as map(*),
  $run as map(*),
  $canvas as map(xs:string,item()*)
) as item()*
{
  simple:draw($model, $run, $canvas)
};

declare function this:draw(
  $model as map(*),
  $run as map(*),
  $mutation as function(map(xs:string,item()*)) as map(xs:string,item()*),
  $edge-width as function(map(xs:string,item()*)) as xs:double?,
  $canvas as map(xs:string,item()*)
) as item()*
{
  simple:draw($model, $run, $mutation, $edge-width, $canvas)
};