summaryrefslogtreecommitdiffstats
path: root/virtual-programs
diff options
context:
space:
mode:
authorAndrés Cuervo <andrescuervor@gmail.com>2023-10-27 16:50:16 +0000
committerAndrés Cuervo <andrescuervor@gmail.com>2023-10-27 16:50:16 +0000
commitccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff (patch)
tree9980a884f927312bf6e64a28f3066fb1dd146ce6 /virtual-programs
parentRemove extraneous debug info (diff)
parentoutline: Slight optimization (diff)
downloadfolk-ccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff.tar.gz
folk-ccbcc1e7bf7dbf3fb118a4a06371fe4d0f2147ff.zip
Merge branch 'main' into ac/editor
Diffstat (limited to 'virtual-programs')
-rw-r--r--virtual-programs/_archive/calibration-overlays.folk (renamed from virtual-programs/archive/calibration-overlays.folk)0
-rw-r--r--virtual-programs/_archive/hello-name.folk (renamed from virtual-programs/archive/hello-name.folk)0
-rw-r--r--virtual-programs/_archive/highlight.folk (renamed from virtual-programs/archive/highlight.folk)0
-rw-r--r--virtual-programs/_archive/log.folk (renamed from virtual-programs/archive/log.folk)0
-rw-r--r--virtual-programs/_archive/metrics.folk (renamed from virtual-programs/archive/metrics.folk)0
-rw-r--r--virtual-programs/_archive/points-up-test.folk (renamed from virtual-programs/archive/points-up-test.folk)0
-rw-r--r--virtual-programs/_archive/points-up.folk (renamed from virtual-programs/archive/points-up.folk)0
-rw-r--r--virtual-programs/_archive/test.folk (renamed from virtual-programs/archive/test.folk)0
-rw-r--r--virtual-programs/_archive/welcome.folk (renamed from virtual-programs/archive/welcome.folk)0
-rw-r--r--virtual-programs/apriltags.folk20
-rw-r--r--virtual-programs/camera.folk5
-rw-r--r--virtual-programs/connections.folk8
-rw-r--r--virtual-programs/display.folk642
-rw-r--r--virtual-programs/display/arc.folk39
-rw-r--r--virtual-programs/display/circle.folk31
-rw-r--r--virtual-programs/display/fill.folk51
-rw-r--r--virtual-programs/display/fns.folk0
-rw-r--r--virtual-programs/display/image.folk118
-rw-r--r--virtual-programs/display/stroke.folk34
-rw-r--r--virtual-programs/display/text.folk207
-rw-r--r--virtual-programs/errors.folk10
-rw-r--r--virtual-programs/images.folk10
-rw-r--r--virtual-programs/label.folk5
-rw-r--r--virtual-programs/mask-tags.folk2
-rw-r--r--virtual-programs/new-program-web-editor.folk8
-rw-r--r--virtual-programs/outline.folk17
-rw-r--r--virtual-programs/shapes.folk106
-rw-r--r--virtual-programs/sprites.folk2
-rw-r--r--virtual-programs/tags-and-calibration.folk13
-rw-r--r--virtual-programs/title.folk4
-rw-r--r--virtual-programs/web-editor.folk9
31 files changed, 732 insertions, 609 deletions
diff --git a/virtual-programs/archive/calibration-overlays.folk b/virtual-programs/_archive/calibration-overlays.folk
index 6575168d..6575168d 100644
--- a/virtual-programs/archive/calibration-overlays.folk
+++ b/virtual-programs/_archive/calibration-overlays.folk
diff --git a/virtual-programs/archive/hello-name.folk b/virtual-programs/_archive/hello-name.folk
index be31ba5c..be31ba5c 100644
--- a/virtual-programs/archive/hello-name.folk
+++ b/virtual-programs/_archive/hello-name.folk
diff --git a/virtual-programs/archive/highlight.folk b/virtual-programs/_archive/highlight.folk
index 768e4bc9..768e4bc9 100644
--- a/virtual-programs/archive/highlight.folk
+++ b/virtual-programs/_archive/highlight.folk
diff --git a/virtual-programs/archive/log.folk b/virtual-programs/_archive/log.folk
index 872f3fe2..872f3fe2 100644
--- a/virtual-programs/archive/log.folk
+++ b/virtual-programs/_archive/log.folk
diff --git a/virtual-programs/archive/metrics.folk b/virtual-programs/_archive/metrics.folk
index e5389512..e5389512 100644
--- a/virtual-programs/archive/metrics.folk
+++ b/virtual-programs/_archive/metrics.folk
diff --git a/virtual-programs/archive/points-up-test.folk b/virtual-programs/_archive/points-up-test.folk
index c4fcff4d..c4fcff4d 100644
--- a/virtual-programs/archive/points-up-test.folk
+++ b/virtual-programs/_archive/points-up-test.folk
diff --git a/virtual-programs/archive/points-up.folk b/virtual-programs/_archive/points-up.folk
index e5c12126..e5c12126 100644
--- a/virtual-programs/archive/points-up.folk
+++ b/virtual-programs/_archive/points-up.folk
diff --git a/virtual-programs/archive/test.folk b/virtual-programs/_archive/test.folk
index 9b5c75f0..9b5c75f0 100644
--- a/virtual-programs/archive/test.folk
+++ b/virtual-programs/_archive/test.folk
diff --git a/virtual-programs/archive/welcome.folk b/virtual-programs/_archive/welcome.folk
index 6c08ed47..6c08ed47 100644
--- a/virtual-programs/archive/welcome.folk
+++ b/virtual-programs/_archive/welcome.folk
diff --git a/virtual-programs/apriltags.folk b/virtual-programs/apriltags.folk
index 7e190a5b..49b83e5a 100644
--- a/virtual-programs/apriltags.folk
+++ b/virtual-programs/apriltags.folk
@@ -3,21 +3,13 @@ if {$::isLaptop} return
set tagfamily "tagStandard52h13"
# Plain detector. Runs on entire camera frame.
-set mainDetectorProcess [On process {
+set mainDetectorProcess [Start process {
source pi/AprilTags.tcl
- # TODO: Clean this up. We retract these so that we don't bounce
- # statements back to the main Folk process that it sends us.
- Retract /anyone/ wishes $::thisProcess shares all wishes
- Retract /anyone/ wishes $::thisProcess shares all claims
Wish $::thisProcess receives statements like \
[list /someone/ claims the camera frame is /grayFrame/ at /timestamp/]
Wish $::thisProcess shares statements like \
- [list /someone/ wishes /process/ receives statements like /pattern/]
- Wish $::thisProcess shares statements like \
[list /someone/ claims /process/ detects tags /tags/ at /timestamp/ in time /aprilTime/]
- Wish $::thisProcess shares statements like \
- [list /someone/ claims $::thisProcess has pid /pid/]
set detector [AprilTags new $tagfamily]
When the camera frame is /grayFrame/ at /timestamp/ {
@@ -30,22 +22,14 @@ set mainDetectorProcess [On process {
# Incremental detector. Looks at regions where there were tags in the
# old camera frame.
-On process {
+Start process {
source pi/AprilTags.tcl
- # TODO: Clean this up. We retract these so that we don't bounce
- # statements back to the main Folk process that it sends us.
- Retract /anyone/ wishes $::thisProcess shares all wishes
- Retract /anyone/ wishes $::thisProcess shares all claims
Wish $::thisProcess receives statements like \
[list /someone/ claims the camera frame is /grayFrame/ at /timestamp/]
Wish $::thisProcess receives statements like \
[list /someone/ claims $mainDetectorProcess detects tags /tags/ at /timestamp/ in time /aprilTime/]
Wish $::thisProcess shares statements like \
- [list /someone/ wishes /process/ receives statements like /pattern/]
- Wish $::thisProcess shares statements like \
- [list /someone/ claims $::thisProcess has pid /pid/]
- Wish $::thisProcess shares statements like \
[list /someone/ claims $::thisProcess detects tags /tags/ at /timestamp/ in time /aprilTime/]
Wish $::thisProcess shares statements like \
[list /someone/ wishes /something/ is labelled /text/]
diff --git a/virtual-programs/camera.folk b/virtual-programs/camera.folk
index 0e1710e7..f997d419 100644
--- a/virtual-programs/camera.folk
+++ b/virtual-programs/camera.folk
@@ -14,7 +14,10 @@ if {$::thisNode eq "folk-interact"} {
set width $::Camera::WIDTH
set height $::Camera::HEIGHT
-On process {
+Start process "camera" {
+ Wish $::thisProcess shares statements like \
+ [list /someone/ claims the camera /...anything/]
+
source pi/Camera.tcl
Camera::init $width $height
diff --git a/virtual-programs/connections.folk b/virtual-programs/connections.folk
index c66fba1d..4e002910 100644
--- a/virtual-programs/connections.folk
+++ b/virtual-programs/connections.folk
@@ -17,8 +17,8 @@ When /anyone/ wishes /source/ is connected to /sink/ & \
set angle [expr {atan2(-[lindex $direction 1], [lindex $direction 0]) - 3.14159/2}]
set color grey
- Display::stroke [list $source $sink ] 2 $color
- shape 3 $c 30 $angle $color true
+ Wish to draw a stroke with points [list $source $sink] width 2 color $color
+ Wish to draw a shape with sides 3 center $c radius 30 radians $angle color $color filled true
}
set speed 75
@@ -40,7 +40,7 @@ When /anyone/ wishes /source/ is dynamically connected to /sink/ & \
lassign [vec2 scale [vec2 add $source $sink] 0.5] cx cy
- Display::stroke [list $source $sink ] 1 white
+ Wish to draw a stroke with points [list $source $sink] width 1 color white
When the clock time is /t/ {
@@ -50,7 +50,7 @@ When /anyone/ wishes /source/ is dynamically connected to /sink/ & \
for {set p $offset} {$p < $distance} {incr p $spacing} {
set c [vec2 add $source [vec2 scale $direction $p]]
set s [expr {min($maxsize, 0.20*min($p, $distance - $p))}]
- shape 3 $c $s $angle white true
+ Wish to draw a shape with sides 3 center $c radius $s radians $angle color white filled true
}
}
}
diff --git a/virtual-programs/display.folk b/virtual-programs/display.folk
index da12bc83..2853c065 100644
--- a/virtual-programs/display.folk
+++ b/virtual-programs/display.folk
@@ -1,529 +1,185 @@
-namespace eval ::Display {
- variable WIDTH
- variable HEIGHT
- variable LAYER 0
- if {$::isLaptop} {
- set WIDTH 640; set HEIGHT 480
- } else {
- regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT
- }
-
- proc drawOnTop {func args} {
- set ::Display::LAYER 1
- uplevel [list $func {*}$args]
- set ::Display::LAYER 0
- }
-
- # Create proxy versions of drawing primitives for the main Folk
- # process (that will forward those draw commands to the display
- # subprocess).
- foreach func {stroke circle text fillTriangle fillQuad fillPolygon} {
- proc $func args {
- set func [lindex [info level 0] 0]
- uplevel [list Wish display runs [list $func {*}$args] on layer $::Display::LAYER]
- }
- }
-
- Claim the display Display has width $::Display::WIDTH height $::Display::HEIGHT
+namespace eval Colors { source "pi/Colors.tcl" }
+proc ::getColor {color} {
+ if {[info exists Colors::$color]} { return [set Colors::$color] } \
+ else { return $Colors::white }
}
-On process {
+Start process "display" {
puts "Display pid: [pid]"
source pi/Gpu.tcl
Gpu::init
Gpu::ImageManager::imageManagerInit
- namespace eval Display {
- namespace eval Colors { source "pi/Colors.tcl" }
-
- variable rotate [Gpu::fn {vec2 v float a} vec2 {
- float s = sin(a);
- float c = cos(a);
- mat2 m = mat2(c, s, -s, c);
- return m * v;
- }]
- variable cross2d [Gpu::fn {vec2 a vec2 b} float {
- return a.x*b.y - a.y*b.x;
- }]
- # See https://www.shadertoy.com/view/lsBSDm
- variable invBilinear [Gpu::fn {vec2 p vec2 a vec2 b vec2 c vec2 d fn cross2d} vec2 {
- vec2 res = vec2(-1.0);
-
- vec2 e = b-a;
- vec2 f = d-a;
- vec2 g = a-b+c-d;
- vec2 h = p-a;
-
- float k2 = cross2d( g, f );
- float k1 = cross2d( e, f ) + cross2d( h, g );
- float k0 = cross2d( h, e );
-
- // if edges are parallel, this is a linear equation
- k2 /= k0; k1 /= k0; k0 = 1.0;
- if( abs(k2)<0.001*abs(k0) )
+ # TODO: Share these functions through the database somehow,
+ # instead of keeping them in globals.
+
+ set ::rotate [Gpu::fn {vec2 v float a} vec2 {
+ float s = sin(a);
+ float c = cos(a);
+ mat2 m = mat2(c, s, -s, c);
+ return m * v;
+ }]
+ set ::cross2d [Gpu::fn {vec2 a vec2 b} float {
+ return a.x*b.y - a.y*b.x;
+ }]
+ # See https://www.shadertoy.com/view/lsBSDm
+ set ::invBilinear [Gpu::fn {vec2 p vec2 a vec2 b vec2 c vec2 d fn ::cross2d} vec2 {
+ vec2 res = vec2(-1.0);
+
+ vec2 e = b-a;
+ vec2 f = d-a;
+ vec2 g = a-b+c-d;
+ vec2 h = p-a;
+
+ float k2 = cross2d( g, f );
+ float k1 = cross2d( e, f ) + cross2d( h, g );
+ float k0 = cross2d( h, e );
+
+ // if edges are parallel, this is a linear equation
+ k2 /= k0; k1 /= k0; k0 = 1.0;
+ if( abs(k2)<0.001*abs(k0) )
+ {
+ res = vec2( (h.x*k1+f.x*k0)/(e.x*k1-g.x*k0), -k0/k1 );
+ }
+ // otherwise, it's a quadratic
+ else
+ {
+ float w = k1*k1 - 4.0*k0*k2;
+ if( w<0.0 ) return vec2(-1.0);
+ w = sqrt( w );
+
+ float ik2 = 0.5/k2;
+ float v = (-k1 - w)*ik2;
+ float u = (h.x - f.x*v)/(e.x + g.x*v);
+
+ if( u<0.0 || u>1.0 || v<0.0 || v>1.0 )
{
- res = vec2( (h.x*k1+f.x*k0)/(e.x*k1-g.x*k0), -k0/k1 );
- }
- // otherwise, it's a quadratic
- else
- {
- float w = k1*k1 - 4.0*k0*k2;
- if( w<0.0 ) return vec2(-1.0);
- w = sqrt( w );
-
- float ik2 = 0.5/k2;
- float v = (-k1 - w)*ik2;
- float u = (h.x - f.x*v)/(e.x + g.x*v);
-
- if( u<0.0 || u>1.0 || v<0.0 || v>1.0 )
- {
- v = (-k1 + w)*ik2;
- u = (h.x - f.x*v)/(e.x + g.x*v);
- }
- res = vec2( u, v );
- }
- return res;
- }]
-
- namespace eval font {
- proc load {name} {
- set csvFd [open "vendor/fonts/$name.csv" r]; set csv [read $csvFd]; close $csvFd
- set glyphInfos [dict create]
- foreach line [split $csv "\n"] {
- set info [lassign [split $line ,] glyph]
- lassign $info advance \
- planeLeft planeBottom planeRight planeTop \
- atlasLeft atlasBottom atlasRight atlasTop
- dict set glyphInfos $glyph \
- [list $advance \
- [list $planeLeft $planeBottom $planeRight $planeTop] \
- [list $atlasLeft $atlasBottom $atlasRight $atlasTop]]
- }
-
- set im [image load "[pwd]/vendor/fonts/$name.png"]
- set gim [Gpu::ImageManager::copyImageToGpu $im]
-
- return [list $glyphInfos $im $gim]
- }
- proc hasGlyphInfo {font charCode} { dict exists [lindex $font 0] $charCode }
- proc glyphInfo {font charCode} { dict get [lindex $font 0] $charCode }
- proc atlasImage {font} { lindex $font 1 }
- proc gpuAtlasImage {font} { lindex $font 2 }
-
- namespace export *
- namespace ensemble create
- }
- variable glyphMsd [Gpu::fn {sampler2D atlas vec4 atlasGlyphBounds vec2 glyphUv} vec4 {
- vec2 atlasUv = mix(atlasGlyphBounds.xw, atlasGlyphBounds.zy, glyphUv);
- return texture(atlas, vec2(atlasUv.x, 1.0-atlasUv.y));
- }]
- variable median [Gpu::fn {float r float g float b} float {
- return max(min(r, g), min(max(r, g), b));
- }]
- variable glyph [Gpu::pipeline {sampler2D atlas vec2 atlasSize
- vec4 atlasGlyphBounds
- vec4 planeGlyphBounds
- vec2 pos float radians float em
- fn rotate} {
- float left = planeGlyphBounds[0] * em;
- float bottom = planeGlyphBounds[1] * em;
- float right = planeGlyphBounds[2] * em;
- float top = planeGlyphBounds[3] * em;
- vec2 a = pos + rotate(vec2(left, -top), -radians);
- vec2 b = pos + rotate(vec2(right, -top), -radians);
- vec2 c = pos + rotate(vec2(right, -bottom), -radians);
- vec2 d = pos + rotate(vec2(left, -bottom), -radians);
-
- vec2 vertices[4] = vec2[4](a, b, d, c);
- return vertices[gl_VertexIndex];
- } {fn rotate fn invBilinear fn glyphMsd fn median} {
- float left = planeGlyphBounds[0] * em;
- float bottom = planeGlyphBounds[1] * em;
- float right = planeGlyphBounds[2] * em;
- float top = planeGlyphBounds[3] * em;
- vec2 a = pos + rotate(vec2(left, -top), -radians);
- vec2 b = pos + rotate(vec2(right, -top), -radians);
- vec2 c = pos + rotate(vec2(right, -bottom), -radians);
- vec2 d = pos + rotate(vec2(left, -bottom), -radians);
-
- vec2 glyphUv = invBilinear(gl_FragCoord.xy, a, b, c, d);
- if( max( abs(glyphUv.x-0.5), abs(glyphUv.y-0.5))>=0.5 ) {
- return vec4(0, 0, 0, 0);
- }
- vec3 msd = glyphMsd(atlas, atlasGlyphBounds/atlasSize.xyxy, glyphUv).rgb;
- float sd = median(msd.r, msd.g, msd.b);
- float screenPxDistance = 4.5*(sd - 0.5);
- float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
- return mix(vec4(0, 0, 0, 0), vec4(1, 1, 1, 1), opacity);
- }]
-
- proc start {} { Gpu::drawStart }
-
- proc getColor {color} {
- if {[info exists Colors::$color]} { return [set Colors::$color] } \
- else { return $Colors::white }
- }
-
- variable line [Gpu::pipeline {vec2 from vec2 to float thickness vec4 color} {
- vec2 vertices[4] = vec2[4](
- min(from, to) - thickness,
- vec2(max(from.x, to.x) + thickness, min(from.y, to.y) - thickness),
- vec2(min(from.x, to.x) - thickness, max(from.y, to.y) + thickness),
- max(from, to) + thickness
- );
- return vertices[gl_VertexIndex];
- } {
- float l = length(to - from);
- vec2 d = (to - from) / l;
- vec2 q = (gl_FragCoord.xy - (from + to)*0.5);
- q = mat2(d.x, -d.y, d.y, d.x) * q;
- q = abs(q) - vec2(l, thickness)*0.5;
- float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
-
- return dist < 0.0 ? color : vec4(0, 0, 0, 0);
- }]
- proc stroke {points width color} {
- variable line
- for {set i 0} {$i < [expr {[llength $points] - 1}]} {incr i} {
- set from [lindex $points $i]
- set to [lindex $points [expr $i+1]]
- Gpu::draw $line $from $to $width [getColor $color]
- }
- }
-
- variable arc [Gpu::pipeline {vec2 center float start float arclen float radius float thickness vec4 color} {
- float r = radius + thickness;
- vec2 vertices[4] = vec2[4](
- center - r,
- vec2(center.x + r, center.y - r),
- vec2(center.x - r, center.y + r),
- center + r
- );
- return vertices[gl_VertexIndex];
- } {
- #define M_TWO_PI 6.283185307179586
- start = clamp(start, 0, M_TWO_PI);
- arclen = clamp(arclen, 0, M_TWO_PI);
-
- float dist = length(gl_FragCoord.xy - center) - radius;
- float angle = atan(-(gl_FragCoord.y - center.y), gl_FragCoord.x - center.x);
-
- // Shift angle from [-pi, pi) to [0, 2*pi]
- angle = (angle < 0) ? (angle + M_TWO_PI) : angle;
- float end = start + arclen;
-
- return ((dist < thickness && dist > 0.0) &&
- ((end < M_TWO_PI && angle > start && angle < end) ||
- (end >= M_TWO_PI && (angle > start || angle < end-M_TWO_PI)))) ? color : vec4(0, 0, 0, 0);
-
- }]
-
- proc arc {x y start arclen radius thickness color} {
- variable arc
- Gpu::draw $arc [list $x $y] $start $arclen $radius $thickness [getColor $color]
- }
-
- # TODO: Fix bool support.
- variable circle [Gpu::pipeline {vec2 center float radius float thickness vec4 color int filled} {
- float r = radius + thickness;
- vec2 vertices[4] = vec2[4](
- center - r,
- vec2(center.x + r, center.y - r),
- vec2(center.x - r, center.y + r),
- center + r
- );
- return vertices[gl_VertexIndex];
- } {
- float dist = length(gl_FragCoord.xy - center) - radius;
- if (filled == 1) {
- return (dist < thickness) ? color : vec4(0, 0, 0, 0);
- } else {
- return (dist < thickness && dist > 0.0) ? color : vec4(0, 0, 0, 0);
- }
- }]
- proc circle {x y radius thickness color {filled false}} {
- variable circle
- Gpu::draw $circle [list $x $y] $radius $thickness [getColor $color] [ne $filled "false"]
- }
-
- set ::FontCache [dict create]
-
- # load all fonts into the fontCache
- foreach fontPath [list {*}[glob {*}vendor/fonts/*.png]] {
- set fontName ""
- regexp {vendor/fonts/(.*).png} $fontPath whole_match fontName
- if {!($fontName eq "")} {
- puts "Loaded $fontName into font cache"
- set fontdata [font load $fontName]
- dict set ::FontCache $fontName $fontdata
- }
- }
-
- proc textExtent {text scale {font "PTSans-Regular"}} {
- if {!([dict exists $::FontCache $font])} {
- throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist"
- return
- }
- set font [dict get $::FontCache $font]
-
- set em [* $scale 25.0]
- set x 0; set y 0
- set width 0
- for {set i 0} {$i < [string length $text]} {incr i} {
- set char [string index $text $i]
- if {$char eq "\n"} {
- set y [+ $y $em]; set x 0; continue
- }
- set charCode [scan $char %c]
- if {[font hasGlyphInfo $font $charCode]} {
- set glyphInfo [font glyphInfo $font $charCode]
- } else {
- set glyphInfo [font glyphInfo $font [scan ? %c]]
- }
- lassign $glyphInfo advance planeBounds atlasBounds
- set x [+ $x [* $advance $em]]
- if {$x > $width} { set width $x }
- }
- return [list $width [+ $y $em]]
- }
- proc text {x0 y0 scale text radians {font "PTSans-Regular"}} {
- if {!([dict exists $::FontCache $font])} {
- throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist"
- return
- }
- set font [dict get $::FontCache $font]
-
-
- variable glyph
- set fontAtlas [font gpuAtlasImage $font]
- set fontAtlasSize [list [::image width [font atlasImage $font]] \
- [::image height [font atlasImage $font]]]
-
- set extent [vec2 rotate [textExtent $text $scale] $radians]
-
- set em [* $scale 25.0]
-
- # TODO: Add text alignment/anchor options (right now, this
- # setup centers the text).
- set x0 [expr {$x0 - [lindex $extent 0]/2}]
- set y0 [expr {$y0 - [lindex $extent 1]/2}]
- set x $x0; set y $y0
-
- set lineNum 0
- for {set i 0} {$i < [string length $text]} {incr i} {
- set char [string index $text $i]
- if {$char eq "\n"} {
- incr lineNum
- lassign [vec2 add [list $x0 $y0] \
- [vec2 rotate [list 0 [* $lineNum $em]] $radians]] x y
- continue
- }
- set charCode [scan $char %c]
- if {[font hasGlyphInfo $font $charCode]} {
- set glyphInfo [font glyphInfo $font $charCode]
- } else {
- set glyphInfo [font glyphInfo $font [scan ? %c]]
- }
- lassign $glyphInfo advance planeBounds atlasBounds
- if {$char ne " "} {
- Gpu::draw $glyph $fontAtlas $fontAtlasSize \
- $atlasBounds $planeBounds [list $x $y] $radians $em
- }
- lassign [vec2 add [list $x $y] \
- [vec2 rotate [list [* $advance $em] 0] $radians]] x y
+ v = (-k1 + w)*ik2;
+ u = (h.x - f.x*v)/(e.x + g.x*v);
}
+ res = vec2( u, v );
}
+ return res;
+ }]
- variable fillTriangle [Gpu::pipeline {vec2 p0 vec2 p1 vec2 p2 vec4 color} {
- vec2 vertices[4] = vec2[4](p0, p1, p2, p0);
- return vertices[gl_VertexIndex];
- } {
- return color;
- }]
- proc fillTriangle {p0 p1 p2 color} {
- variable fillTriangle
- Gpu::draw $fillTriangle $p0 $p1 $p2 [getColor $color]
- }
-
- proc fillQuad {p0 p1 p2 p3 color} {
- fillTriangle $p1 $p2 $p3 $color
- fillTriangle $p0 $p1 $p3 $color
- }
-
- proc fillPolygon {points color} {
- set num_points [llength $points]
- if {$num_points < 3} {
- error "At least 3 points are required to form a polygon."
- } elseif {$num_points == 3} {
- eval fillTriangle $points $color
- } elseif {$num_points == 4} {
- eval fillQuad $points $color
- } else {
- # Get the first point in the list as the "base" point of the triangles
- set p0 [lindex $points 0]
-
- for {set i 1} {$i < $num_points - 1} {incr i} {
- set p1 [lindex $points $i]
- set p2 [lindex $points [expr {$i+1}]]
- fillTriangle $p0 $p1 $p2 $color
- }
- }
- }
-
- variable image [Gpu::pipeline {sampler2D image vec2 imageSize
- vec2 pos float radians float scale
- fn rotate} {
- vec2 a = pos + rotate(-imageSize/2, -radians);
- vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians);
- vec2 c = pos + rotate(imageSize/2, -radians);
- vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians);
- vec2 vertices[4] = vec2[4](a, b, d, c);
- return vertices[gl_VertexIndex];
- } {fn invBilinear fn rotate} {
- vec2 a = pos + rotate(-imageSize/2, -radians);
- vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians);
- vec2 c = pos + rotate(imageSize/2, -radians);
- vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians);
- vec2 p = gl_FragCoord.xy;
- vec2 uv = invBilinear(p, a, b, c, d);
- if( max( abs(uv.x-0.5), abs(uv.y-0.5))<0.5 ) {
- return texture(image, uv);
- }
- return vec4(0.0, 0.0, 0.0, 0.0);
- }]
- variable imCache [dict create]
- variable IMCACHE_MAX_IMAGES [- $Gpu::ImageManager::GPU_MAX_IMAGES [dict size $::FontCache]]
- proc checkImCacheAndCopyIfNeeded {imDrawSet} {
- variable imCache
- variable IMCACHE_MAX_IMAGES
-
- dict for {im v} $imDrawSet {
- if {![dict exists $imCache $im]} { continue }
- # Check for staleness and remove from cache if so.
- lassign [dict get $imCache $im] gim expectedVersion
- set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
- if {$expectedVersion != $version} {
- Gpu::ImageManager::freeGpuImage $gim
- dict unset imCache $im
- }
- }
-
- set notInCache [dictset difference $imDrawSet $imCache]
- set notInDrawSet [dictset difference $imCache $imDrawSet]
-
- set numImagesToCopy [dictset size $notInCache]
- if {$numImagesToCopy > 0} {
- if {[dictset size $imCache] + $numImagesToCopy > $IMCACHE_MAX_IMAGES} {
- set numImagesToEvict \
- [expr {[dictset size $imCache] + $numImagesToCopy - $IMCACHE_MAX_IMAGES}]
-
- # What can we safely evict?
- # - Anything that's stale
- # - Anything that's not in use
- set numImagesEvicted 0
- dict for {im v} $imCache {
- if {$numImagesEvicted == $numImagesToEvict} { break }
-
- lassign $v gim expectedVersion
- set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
- if {$expectedVersion != $version} {
- Gpu::ImageManager::freeGpuImage $gim
- dict unset imCache $im
- incr numImagesEvicted
- continue
- }
- if {![dict exists $imDrawSet $im]} {
- Gpu::ImageManager::freeGpuImage $gim
- dict unset imCache $im
- incr numImagesEvicted
- }
- }
- }
-
- dict for {im _} $notInCache {
- # TODO: This is unsafe (has a race condition) -- we're
- # not locking the image, so version and gim may diverge.
- set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
- set gim [Gpu::ImageManager::copyImageToGpu $im]
- dict set imCache $im [list $gim $version]
- }
- }
- }
- proc image {x y im radians {scale 1.0}} {
- # TODO: Implement scale.
- variable image
- variable imCache
- lassign [dict get $imCache $im] gim
- Gpu::draw $image $gim [list [::image width $im] [::image height $im]] \
- [list $x $y] $radians $scale
- }
-
- proc end {} { Gpu::drawEnd; Gpu::poll }
+ namespace eval Colors { source "pi/Colors.tcl" }
+ proc ::getColor {color} {
+ if {[info exists Colors::$color]} { return [set Colors::$color] } \
+ else { return $Colors::white }
}
- # TODO: Clean this up. We retract these so that we don't bounce
- # statements back to the main Folk process that it sends us.
- Retract /anyone/ wishes $::thisProcess shares all wishes
- Retract /anyone/ wishes $::thisProcess shares all claims
- Wish $::thisProcess shares statements like \
- [list /someone/ wishes /process/ receives statements like /pattern/]
- Wish $::thisProcess shares statements like \
- [list /someone/ claims $::thisProcess has pid /pid/]
Wish $::thisProcess receives statements like \
- [list /someone/ wishes display runs /command/ on layer /layer/]
- Wish $::thisProcess receives statements like \
- [list /someone/ wishes display runs /command/]
+ [list /someone/ wishes the GPU /...anything/]
Wish $::thisProcess shares statements like \
[list /someone/ claims the display time is /displayTime/]
+ Wish $::thisProcess shares statements like \
+ [list /someone/ has error /err/ with info /errorInfo/]
- while true {
- set displayList [list]
- foreach match [Statements::findMatches {/someone/ wishes display runs /command/ on layer /layer/}] {
- lappend displayList [list [dict get $match layer] [dict get $match command]]
- }
- foreach match [Statements::findMatches {/someone/ wishes display runs /command/}] {
- lappend displayList [list 0 [dict get $match command]]
- }
-
- proc lcomp {a b} {
- set layerA [lindex $a 0]
- set layerB [lindex $b 0]
- if {$layerA == $layerB} {
- expr {[lindex $a 1 0] == "Display::text"}
- } else {
- expr {$layerA - $layerB}
- }
+ set ::pipelines [dict create]
+ When /wisher/ wishes the GPU compiles pipeline /name/ /source/ {
+ try {
+ dict set ::pipelines $name [Gpu::pipeline {*}$source]
+ } on error e {
+ puts stderr $::errorInfo
+ Say $wisher has error $e with info $::errorInfo
}
+ }
- set displayCommands [lmap sublist [lsort -command lcomp $displayList] {lindex $sublist 1}]
-
- set imDrawSet [dictset create]
- foreach command $displayCommands {
- if {[lindex $command 0] eq "Display::image"} {
- set im [lindex $command 3]
- dictset add imDrawSet $im
+ while true {
+ set ::displayList [dict create] ;# Keys are layer numbers; values are lists of commands
+ Step
+ foreach match [Statements::findMatches {/wisher/ wishes the GPU draws pipeline /name/ with /...options/}] {
+ try {
+ set name [dict get $match name]
+ set pipeline [dict get $::pipelines [dict get $match name]]
+
+ set options [dict get $match options]
+ set layer [dict_getdef $options layer 0]
+ if {[dict exists $options instances]} {
+ set instances [dict get $options instances]
+ } else {
+ set instances [list [dict get $options arguments]]
+ }
+ foreach instance $instances {
+ dict lappend ::displayList $layer [list Gpu::draw $pipeline {*}$instance]
+ }
+ } on error e {
+ Say [dict get $match wisher] has error $e with info $::errorInfo
}
}
- Display::checkImCacheAndCopyIfNeeded $imDrawSet
set renderTime [baretime {
- Display::start
- foreach command $displayCommands {
- try { {*}$command } \
- on error e { puts stderr $::errorInfo }
+ Gpu::drawStart
+ foreach layer [lsort -real [dict keys $::displayList]] {
+ set layerDisplayList [dict get $::displayList $layer]
+ foreach displayCommand $layerDisplayList {
+ try { {*}$displayCommand } \
+ on error e { puts stderr $::errorInfo }
+ }
}
- Display::end
+ Gpu::drawEnd
}]
Commit { Claim the display time is "render $renderTime us ($::stepTime)" }
- Step
}
}
-set ::Display::displayTime none
set ::displayTime none
When the display time is /displayTime/ {
set ::Display::displayTime $displayTime
set ::displayTime $displayTime
}
+
+namespace eval ::Display {
+ variable displayTime none
+
+ variable WIDTH
+ variable HEIGHT
+ variable LAYER 0
+ if {$::isLaptop} {
+ set WIDTH 640; set HEIGHT 480
+ } else {
+ regexp {mode "(\d+)x(\d+)"} [exec fbset] -> WIDTH HEIGHT
+ }
+
+ # TODO: Remove these / expel them to a shim page; these are only
+ # for backward compatibility now.
+ proc warnDeprecated {} {
+ upvar 2 this this
+ if {![info exists this]} {set this "<unknown>"}
+ set func [lindex [info level -1] 0]
+ set w "$func was deprecated in Oct 2023; use GPU wishes/shaders"
+ Claim $this has warning $w with info $w
+ }
+ proc stroke {points width color} {
+ warnDeprecated
+ Wish to draw a stroke with points $points width $width color $color
+ }
+ proc circle {x y radius thickness color {filled false}} {
+ warnDeprecated
+ Wish to draw a circle with x $x y $y radius $radius thickness $thickness color $color filled $filled
+ }
+ proc text {x0 y0 scale text radians {font "PTSans-Regular"}} {
+ warnDeprecated
+ Wish to draw text with x $x0 y $y0 scale $scale text $text radians $radians font $font
+ }
+ proc fillTriangle {p0 p1 p2 color} {
+ warnDeprecated
+ Wish to draw a triangle with p0 $p0 p1 $p1 p2 $p2 color $color
+ }
+ proc fillQuad {p0 p1 p2 p3 color} {
+ warnDeprecated
+ Wish to draw a quad with p0 $p0 p1 $p1 p2 $p2 p3 $p3 color $color
+ }
+ proc fillPolygon {points color} {
+ warnDeprecated
+ Wish to draw a polygon with points $points color $color
+ }
+ proc image {x y im radians {scale 1.0}} {
+ warnDeprecated
+ Wish to draw an image with x $x y $y image $im radians $radians scale $scale
+ }
+
+ Claim the display Display has width $::Display::WIDTH height $::Display::HEIGHT
+}
diff --git a/virtual-programs/display/arc.folk b/virtual-programs/display/arc.folk
new file mode 100644
index 00000000..e2412bf0
--- /dev/null
+++ b/virtual-programs/display/arc.folk
@@ -0,0 +1,39 @@
+# Example:
+# When $this has region /r/ {
+# lassign [region centroid $r] x y
+# Wish to draw an arc with x $x y $y start 0 arclen 1 thickness 3 radius 100 color green
+# }
+
+Wish the GPU compiles pipeline "arc" {{vec2 center float start float arclen float radius float thickness vec4 color} {
+ float r = radius + thickness;
+ vec2 vertices[4] = vec2[4](
+ center - r,
+ vec2(center.x + r, center.y - r),
+ vec2(center.x - r, center.y + r),
+ center + r
+ );
+ return vertices[gl_VertexIndex];
+} {
+ #define M_TWO_PI 6.283185307179586
+ start = clamp(start, 0, M_TWO_PI);
+ arclen = clamp(arclen, 0, M_TWO_PI);
+
+ float dist = length(gl_FragCoord.xy - center) - radius;
+ float angle = atan(-(gl_FragCoord.y - center.y), gl_FragCoord.x - center.x);
+
+ // Shift angle from [-pi, pi) to [0, 2*pi]
+ angle = (angle < 0) ? (angle + M_TWO_PI) : angle;
+ float end = start + arclen;
+
+ return ((dist < thickness && dist > 0.0) &&
+ ((end < M_TWO_PI && angle > start && angle < end) ||
+ (end >= M_TWO_PI && (angle > start || angle < end-M_TWO_PI)))) ? color : vec4(0, 0, 0, 0);
+
+}}
+
+When /someone/ wishes to draw an arc with /...options/ {
+ dict with options {
+ Wish the GPU draws pipeline "arc" with arguments \
+ [list [list $x $y] $start $arclen $radius $thickness [getColor $color]]
+ }
+}
diff --git a/virtual-programs/display/circle.folk b/virtual-programs/display/circle.folk
new file mode 100644
index 00000000..03e4def7
--- /dev/null
+++ b/virtual-programs/display/circle.folk
@@ -0,0 +1,31 @@
+Wish the GPU compiles pipeline "circle" {
+ {vec2 center float radius float thickness vec4 color int filled} {
+ float r = radius + thickness;
+ vec2 vertices[4] = vec2[4](
+ center - r,
+ vec2(center.x + r, center.y - r),
+ vec2(center.x - r, center.y + r),
+ center + r
+ );
+ return vertices[gl_VertexIndex];
+ } {
+ float dist = length(gl_FragCoord.xy - center) - radius;
+ if (filled == 1) {
+ return (dist < thickness) ? color : vec4(0, 0, 0, 0);
+ } else {
+ return (dist < thickness && dist > 0.0) ? color : vec4(0, 0, 0, 0);
+ }
+ }
+}
+
+When /someone/ wishes to draw a circle with /...options/ {
+ set center [dict_getdef $options center ""]
+ if {$center eq ""} { set center [list [dict get $options x] [dict get $options y]] }
+ set radius [dict get $options radius]
+ set thickness [dict get $options thickness]
+ set color [getColor [dict get $options color]]
+ set filled [dict_getdef $options filled false]
+
+ Wish the GPU draws pipeline "circle" with arguments \
+ [list $center $radius $thickness $color [expr {$filled eq false ? 0 : 1}]]
+}
diff --git a/virtual-programs/display/fill.folk b/virtual-programs/display/fill.folk
new file mode 100644
index 00000000..f3057068
--- /dev/null
+++ b/virtual-programs/display/fill.folk
@@ -0,0 +1,51 @@
+Wish the GPU compiles pipeline "fillTriangle" {
+ {vec2 p0 vec2 p1 vec2 p2 vec4 color} {
+ vec2 vertices[4] = vec2[4](p0, p1, p2, p0);
+ return vertices[gl_VertexIndex];
+ } {
+ return color;
+ }
+}
+
+When /someone/ wishes to draw a triangle with /...options/ {
+ dict with options {
+ Wish the GPU draws pipeline "fillTriangle" with arguments \
+ [list $p0 $p1 $p2 [getColor $color]]
+ }
+}
+When /someone/ wishes to draw a quad with /...options/ {
+ dict with options {
+ if {![info exists layer]} { set layer 0 }
+ Wish the GPU draws pipeline "fillTriangle" with arguments \
+ [list $p1 $p2 $p3 [getColor $color]] layer $layer
+ Wish the GPU draws pipeline "fillTriangle" with arguments \
+ [list $p0 $p1 $p3 [getColor $color]] layer $layer
+ }
+}
+When /someone/ wishes to draw a polygon with /...options/ {
+ set points [dict get $options points]
+ set color [dict get $options color]
+
+ set num_points [llength $points]
+ if {$num_points < 3} {
+ error "At least 3 points are required to form a polygon."
+ } elseif {$num_points == 3} {
+ Wish to draw a triangle with \
+ p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \
+ color $color
+ } elseif {$num_points == 4} {
+ Wish to draw a quad with \
+ p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] p3 [lindex $points 3] \
+ color $color
+ } else {
+ # Get the first point in the list as the "base" point of the triangles
+ set p0 [lindex $points 0]
+
+ for {set i 1} {$i < $num_points - 1} {incr i} {
+ set p1 [lindex $points $i]
+ set p2 [lindex $points [expr {$i+1}]]
+ Wish the GPU draws pipeline "fillTriangle" with arguments \
+ [list $p0 $p1 $p2 [getColor $color]]
+ }
+ }
+}
diff --git a/virtual-programs/display/fns.folk b/virtual-programs/display/fns.folk
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/virtual-programs/display/fns.folk
diff --git a/virtual-programs/display/image.folk b/virtual-programs/display/image.folk
new file mode 100644
index 00000000..23246d45
--- /dev/null
+++ b/virtual-programs/display/image.folk
@@ -0,0 +1,118 @@
+On process "display" {
+ set invBilinear $::invBilinear
+ set rotate $::rotate
+ # TODO: Do this with a wish, instead of hard-coding the global dict.
+ dict set ::pipelines "image" [Gpu::pipeline {sampler2D image vec2 imageSize
+ vec2 pos float radians float scale
+ fn rotate} {
+ vec2 a = pos + rotate(-imageSize/2, -radians);
+ vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians);
+ vec2 c = pos + rotate(imageSize/2, -radians);
+ vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians);
+ vec2 vertices[4] = vec2[4](a, b, d, c);
+ return vertices[gl_VertexIndex];
+ } {fn invBilinear fn rotate} {
+ vec2 a = pos + rotate(-imageSize/2, -radians);
+ vec2 b = pos + rotate(vec2(imageSize.x, -imageSize.y)/2, -radians);
+ vec2 c = pos + rotate(imageSize/2, -radians);
+ vec2 d = pos + rotate(vec2(-imageSize.x, imageSize.y)/2, -radians);
+ vec2 p = gl_FragCoord.xy;
+ vec2 uv = invBilinear(p, a, b, c, d);
+ if( max( abs(uv.x-0.5), abs(uv.y-0.5))<0.5 ) {
+ return texture(image, uv);
+ }
+ return vec4(0.0, 0.0, 0.0, 0.0);
+ }]
+
+ When the GPU has loaded /nfonts/ fonts {
+ namespace eval ::ImageCache {
+ # Backing store: stores pairs of (GPU image handle, heap slot version).
+ variable cache [dict create]
+ variable CACHE_MAX_SIZE [- $Gpu::ImageManager::GPU_MAX_IMAGES [uplevel {set nfonts}]]
+
+ proc getOrInsert {im} {
+ variable cache
+ variable CACHE_MAX_SIZE
+ if {[dict exists $cache $im]} {
+ lassign [dict get $cache $im] gim cachedVersion
+ set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
+ if {$version == $cachedVersion} {
+ # Bump this image to end of cache since it's
+ # most-recently-accessed.
+ dict unset cache $im
+ dict set cache $im [list $gim $cachedVersion]
+ return $gim
+ } else {
+ # This image is stale. Don't retain it.
+ remove $im
+ }
+ }
+ if {[dict size $cache] >= $CACHE_MAX_SIZE} {
+ evict
+ }
+ if {[dict size $cache] >= $CACHE_MAX_SIZE} {
+ puts stderr "image: Warning: Out of slots in GPU image cache."
+ }
+ set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
+ set gim [Gpu::ImageManager::copyImageToGpu $im]
+ dict set cache $im [list $gim $version]
+ return $gim
+ }
+
+ proc evict {} {
+ variable cache
+ variable CACHE_MAX_SIZE
+ set numToEvict [expr {([dict size $cache] + 1) - $CACHE_MAX_SIZE}]
+ set numEvicted [list]
+ # Evict stale.
+ dict for {im v} $cache {
+ lassign $v gim expectedVersion
+ set version [Heap::folkHeapGetVersion [string map {uint8_t void} [::image_t data_ptr $im]]]
+ if {$expectedVersion != $version} {
+ Gpu::ImageManager::freeGpuImage $gim
+ lappend numEvicted $im
+ }
+ }
+ foreach im $numEvicted { dict unset cache $im }
+ # Evict old.
+ dict for {im v} $cache {
+ if {$numToEvict - [llength $numEvicted] <= 0} {
+ break
+ }
+ lassign $v gim
+ Gpu::ImageManager::freeGpuImage $gim
+ lappend numEvicted $im
+ }
+ foreach im $numEvicted { dict unset cache $im }
+ }
+
+ proc remove {im} {
+ variable cache
+ if {[dict exists $cache $im]} {
+ lassign [dict get $cache $im] gim
+ Gpu::ImageManager::freeGpuImage $gim
+ }
+ dict unset cache $im
+ }
+ }
+ }
+
+ Wish $::thisProcess receives statements like \
+ [list /someone/ wishes to draw an image with /...options/]
+ When /someone/ wishes to draw an image with /...options/ {
+ if {[dict exists $options center]} {
+ set center [dict get $options center]
+ } else {
+ set center [list [dict get $options x] [dict get $options y]]
+ }
+ set im [dict get $options image]
+ set radians [dict get $options radians]
+ set scale [dict_getdef $options scale 1.0]
+
+ set gim [ImageCache::getOrInsert $im]
+
+ Wish the GPU draws pipeline "image" with arguments \
+ [list $gim [list [image_t width $im] [image_t height $im]] \
+ $center $radians $scale]
+ }
+}
diff --git a/virtual-programs/display/stroke.folk b/virtual-programs/display/stroke.folk
new file mode 100644
index 00000000..f212f471
--- /dev/null
+++ b/virtual-programs/display/stroke.folk
@@ -0,0 +1,34 @@
+Wish the GPU compiles pipeline "line" {
+ {vec2 from vec2 to float thickness vec4 color} {
+ vec2 vertices[4] = vec2[4](
+ min(from, to) - thickness,
+ vec2(max(from.x, to.x) + thickness, min(from.y, to.y) - thickness),
+ vec2(min(from.x, to.x) - thickness, max(from.y, to.y) + thickness),
+ max(from, to) + thickness
+ );
+ return vertices[gl_VertexIndex];
+ } {
+ float l = length(to - from);
+ vec2 d = (to - from) / l;
+ vec2 q = (gl_FragCoord.xy - (from + to)*0.5);
+ q = mat2(d.x, -d.y, d.y, d.x) * q;
+ q = abs(q) - vec2(l, thickness)*0.5;
+ float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0);
+
+ return dist < 0.0 ? color : vec4(0, 0, 0, 0);
+ }
+}
+
+When /someone/ wishes to draw a stroke with /...options/ {
+ set points [dict get $options points]
+ set width [dict get $options width]
+ set color [getColor [dict get $options color]]
+
+ set instances [list]
+ for {set i 0} {$i < [expr {[llength $points] - 1}]} {incr i} {
+ set from [lindex $points $i]
+ set to [lindex $points [expr $i+1]]
+ lappend instances [list $from $to $width $color]
+ }
+ Wish the GPU draws pipeline "line" with instances $instances
+}
diff --git a/virtual-programs/display/text.folk b/virtual-programs/display/text.folk
new file mode 100644
index 00000000..858a9dc7
--- /dev/null
+++ b/virtual-programs/display/text.folk
@@ -0,0 +1,207 @@
+On process "display" {
+ namespace eval font {
+ set cc [c create]
+ $cc include <math.h>
+ defineImageType $cc
+ $cc struct Font {
+ image_t atlasImage;
+ int gpuAtlasImage;
+ // TODO: This only handles ASCII, obviously.
+ Tcl_Obj* glyphInfos[128];
+ }
+
+ proc load {name} {
+ set csvFd [open "vendor/fonts/$name.csv" r]; set csv [read $csvFd]; close $csvFd
+ set fields [list ]
+ # HACK: Create list of null glyphs to initialize.
+ set glyphInfos [list]
+ for {set i 0} {$i < 128} {incr i} {
+ lappend glyphInfos {}
+ }
+
+ foreach line [split $csv "\n"] {
+ set values [lassign [split $line ,] glyph]
+ if {![string is integer -strict $glyph]} { continue }
+
+ lassign $values advance \
+ planeLeft planeBottom planeRight planeTop \
+ atlasLeft atlasBottom atlasRight atlasTop
+ lset glyphInfos $glyph \
+ [list $advance \
+ [list $planeLeft $planeBottom $planeRight $planeTop] \
+ [list $atlasLeft $atlasBottom $atlasRight $atlasTop]]
+ }
+
+ set im [image load "[pwd]/vendor/fonts/$name.png"]
+ set gim [Gpu::ImageManager::copyImageToGpu $im]
+
+ return [dict create atlasImage $im gpuAtlasImage $gim glyphInfos $glyphInfos]
+ }
+ $cc struct vec2f { float x; float y; }
+ $cc proc vec2f_add {vec2f a vec2f b} vec2f {
+ return (vec2f) { a.x + b.x, a.y + b.y };
+ }
+ $cc proc vec2f_rotate {vec2f a float radians} vec2f {
+ return (vec2f) {
+ a.x*cosf(radians) + a.y*sinf(radians),
+ -a.x*sinf(radians) + a.y*cosf(radians)
+ };
+ }
+ $cc proc textExtent {Font* font char* text float scale} vec2f {
+ float em = scale * 25.0;
+ float x = 0; float y = 0;
+ float width = 0;
+ for (int i = 0; text[i] != 0; i++) {
+ int ch = text[i];
+ if (ch == '\n') {
+ y = y + em; x = 0; continue;
+ }
+ if (ch >= sizeof(font->glyphInfos)/sizeof(font->glyphInfos[0])) {
+ ch = '?';
+ }
+ Tcl_Obj* glyphInfo = font->glyphInfos[ch];
+ Tcl_Obj* advanceObj; Tcl_ListObjIndex(NULL, glyphInfo, 0, &advanceObj);
+ double advance; Tcl_GetDoubleFromObj(NULL, advanceObj, &advance);
+ x = x + advance * em;
+ if (x > width) { width = x; }
+ }
+ return (vec2f) { width, y + em };
+ }
+ $cc proc textShape {Font* font char* text
+ float x0 float y0 float scale float radians} Tcl_Obj* {
+ Tcl_Obj* gpuAtlasImageSize = Tcl_ObjPrintf("%d %d", font->atlasImage.width, font->atlasImage.height);
+
+ vec2f extent = vec2f_rotate(textExtent(font, text, scale), radians);
+ float em = scale * 25.0;
+
+ vec2f p0 = { x0 - extent.x/2.0, y0 - extent.y/2.0 };
+ vec2f p = p0;
+
+ int lineNum = 0;
+ Tcl_Obj* instances = Tcl_NewListObj(0, NULL); // List of instances.
+ for (int i = 0; text[i] != 0; i++) {
+ int ch = text[i];
+ if (ch == '\n') {
+ lineNum++;
+ p = vec2f_add(p0, vec2f_rotate((vec2f) {0, lineNum * em}, radians));
+ continue;
+ }
+ if (ch >= sizeof(font->glyphInfos)/sizeof(font->glyphInfos[0])) {
+ ch = '?';
+ }
+ Tcl_Obj* glyphInfo = font->glyphInfos[ch];
+ Tcl_Obj* advanceObj; Tcl_ListObjIndex(NULL, glyphInfo, 0, &advanceObj);
+ double advance; Tcl_GetDoubleFromObj(NULL, advanceObj, &advance);
+ if (ch != ' ') {
+ // Append to list of instances.
+ Tcl_Obj* planeBounds; Tcl_ListObjIndex(NULL, glyphInfo, 1, &planeBounds);
+ Tcl_Obj* atlasBounds; Tcl_ListObjIndex(NULL, glyphInfo, 2, &atlasBounds);
+ Tcl_Obj* pv[] = {Tcl_NewDoubleObj(p.x), Tcl_NewDoubleObj(p.y)};
+ Tcl_Obj* pObj = Tcl_NewListObj(2, pv);
+ Tcl_Obj* args[] = {
+ Tcl_NewIntObj(font->gpuAtlasImage),
+ gpuAtlasImageSize,
+ atlasBounds,
+ planeBounds,
+ pObj, Tcl_NewDoubleObj(radians), Tcl_NewDoubleObj(em)
+ };
+ Tcl_Obj* instance = Tcl_NewListObj(sizeof(args)/sizeof(args[0]), args);
+ Tcl_ListObjAppendElement(NULL, instances, instance);
+ }
+ p = vec2f_add(p, vec2f_rotate((vec2f) {advance * em, 0}, radians));
+ }
+ return instances;
+ }
+ $cc compile
+
+ namespace export *
+ namespace ensemble create
+ }
+ set ::FontCache [dict create]
+ # load all fonts into the fontCache
+ foreach fontPath [list {*}[glob vendor/fonts/*.png]] {
+ set fontName ""
+ regexp {vendor/fonts/(.*).png} $fontPath -> fontName
+ if {!($fontName eq "")} {
+ puts "Loaded $fontName into font cache"
+ set fontdata [font load $fontName]
+ dict set ::FontCache $fontName $fontdata
+ }
+ }
+ Claim the GPU has loaded [dict size $::FontCache] fonts
+
+ set rotate $::rotate
+ set invBilinear $::invBilinear
+ set glyphMsd [Gpu::fn {sampler2D atlas vec4 atlasGlyphBounds vec2 glyphUv} vec4 {
+ vec2 atlasUv = mix(atlasGlyphBounds.xw, atlasGlyphBounds.zy, glyphUv);
+ return texture(atlas, vec2(atlasUv.x, 1.0-atlasUv.y));
+ }]
+ set median [Gpu::fn {float r float g float b} float {
+ return max(min(r, g), min(max(r, g), b));
+ }]
+ # TODO: Do this with a wish, instead of hard-coding the global dict.
+ dict set ::pipelines "glyph" [Gpu::pipeline \
+ {sampler2D atlas vec2 atlasSize
+ vec4 atlasGlyphBounds
+ vec4 planeGlyphBounds
+ vec2 pos float radians float em
+ fn rotate} {
+ float left = planeGlyphBounds[0] * em;
+ float bottom = planeGlyphBounds[1] * em;
+ float right = planeGlyphBounds[2] * em;
+ float top = planeGlyphBounds[3] * em;
+ vec2 a = pos + rotate(vec2(left, -top), -radians);
+ vec2 b = pos + rotate(vec2(right, -top), -radians);
+ vec2 c = pos + rotate(vec2(right, -bottom), -radians);
+ vec2 d = pos + rotate(vec2(left, -bottom), -radians);
+
+ vec2 vertices[4] = vec2[4](a, b, d, c);
+ return vertices[gl_VertexIndex];
+ } {fn rotate fn invBilinear fn glyphMsd fn median} {
+ float left = planeGlyphBounds[0] * em;
+ float bottom = planeGlyphBounds[1] * em;
+ float right = planeGlyphBounds[2] * em;
+ float top = planeGlyphBounds[3] * em;
+ vec2 a = pos + rotate(vec2(left, -top), -radians);
+ vec2 b = pos + rotate(vec2(right, -top), -radians);
+ vec2 c = pos + rotate(vec2(right, -bottom), -radians);
+ vec2 d = pos + rotate(vec2(left, -bottom), -radians);
+
+ vec2 glyphUv = invBilinear(gl_FragCoord.xy, a, b, c, d);
+ if( max( abs(glyphUv.x-0.5), abs(glyphUv.y-0.5))>=0.5 ) {
+ return vec4(0, 0, 0, 0);
+ }
+ vec3 msd = glyphMsd(atlas, atlasGlyphBounds/atlasSize.xyxy, glyphUv).rgb;
+ float sd = median(msd.r, msd.g, msd.b);
+ float screenPxDistance = 4.5*(sd - 0.5);
+ float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
+ return mix(vec4(0, 0, 0, 0), vec4(1, 1, 1, 1), opacity);
+ }]
+
+ Wish $::thisProcess receives statements like \
+ [list /someone/ wishes to draw text with /...options/]
+
+ When (non-capturing) /someone/ wishes to draw text with /...options/ {
+ if {[dict exists $options center]} {
+ lassign [dict get $options center] x0 y0
+ } else {
+ set x0 [dict get $options x]
+ set y0 [dict get $options y]
+ }
+ set scale [dict_getdef $options scale 1.0]
+ set font [dict_getdef $options font "PTSans-Regular"]
+ set text [dict get $options text]
+ set radians [dict get $options radians]
+
+ if {!([dict exists $::FontCache $font])} {
+ throw {DISPLAY FONT {font doesn't exist}} "$font doesn't exist"
+ }
+ set font [dict get $::FontCache $font]
+
+ set instances [font textShape $font $text $x0 $y0 $scale $radians]
+
+ # We need to batch into one wish so we don't deal with n^2
+ # checks for existing statements for n glyphs.
+ Wish the GPU draws pipeline "glyph" with instances $instances
+ }
+}
diff --git a/virtual-programs/errors.folk b/virtual-programs/errors.folk
index 28028710..32e34e73 100644
--- a/virtual-programs/errors.folk
+++ b/virtual-programs/errors.folk
@@ -6,6 +6,14 @@ When /p/ has error /err/ with info /info/ {
Wish $p is outlined red
}
}
-
Wish $p is titled $err
}
+
+When /p/ has warning /w/ with info /info/ {
+ When the clock time is /t/ {
+ if {[expr {(int($t * 5)) % 2}] != 1} {
+ Wish $p is outlined yellow
+ }
+ }
+ Wish $p is titled $w
+}
diff --git a/virtual-programs/images.folk b/virtual-programs/images.folk
index e748cd11..d8bcaf55 100644
--- a/virtual-programs/images.folk
+++ b/virtual-programs/images.folk
@@ -36,8 +36,10 @@ namespace eval ::image {
$cc include <string.h>
if {[namespace exists ::Heap]} {
$cc import ::Heap::cc folkHeapAlloc as folkHeapAlloc
+ $cc import ::Heap::cc folkHeapFree as folkHeapFree
} else {
$cc code { #define folkHeapAlloc malloc }
+ $cc code { #define folkHeapFree free }
}
$cc code {
@@ -258,6 +260,10 @@ namespace eval ::image {
return ret;
}
+ $cc proc freeImage {image_t im} void {
+ folkHeapFree(im.data);
+ }
+
$cc proc freeJpeg {image_t im} void {
// TODO: Free the JPEG.
// ckfree(im.data);
@@ -334,7 +340,7 @@ When /someone/ wishes /p/ displays camera slice /slice/ & /p/ has region /r/ {
# set scale [expr {$Display::WIDTH / $Camera::WIDTH}]
# Use 1x scale instead of $scale so the projected tag doesn't redetect.
# TODO: Mask the tag out?
- Wish display runs [list Display::image {*}$center $slice 0 1]
+ Wish to draw an image with center $center image $slice radians 0 scale 1
}
When /someone/ wishes /p/ displays image /im/ {
@@ -348,7 +354,7 @@ When /someone/ wishes /p/ displays image /im/ {
# set scale [expr {min($width / [image width $im],
# $height / [image height $im])}]
# Wish $p is labelled $im
- Wish display runs [list Display::image {*}$center $im [region angle $r] 1]
+ Wish to draw an image with center $center image $im radians [region angle $r]
}
# On unmatch {
# # HACK: Leaves time for the display to finish trying to display this.
diff --git a/virtual-programs/label.folk b/virtual-programs/label.folk
index 440bbf48..88a86c18 100644
--- a/virtual-programs/label.folk
+++ b/virtual-programs/label.folk
@@ -1,15 +1,12 @@
When /thing/ has region /region/ {
lassign [region centroid $region] x y
- # set width [region width $region]
- # set height [region height $region]
set radians [region angle $region]
When the collected matches for [list /someone/ wishes $thing is labelled /text/ with font /font/] are /matches/ {
set text [join [lmap match $matches {dict get $match text}] "\n"]
if {$text eq ""} { return }
- set scale 1
- Display::text $x $y $scale $text $radians [dict get $match font]
+ Wish to draw text with x $x y $y text $text radians $radians font [dict get $match font]
}
}
diff --git a/virtual-programs/mask-tags.folk b/virtual-programs/mask-tags.folk
index 4f0ae4b2..749bb26c 100644
--- a/virtual-programs/mask-tags.folk
+++ b/virtual-programs/mask-tags.folk
@@ -12,5 +12,5 @@ When tag /something/ has corners /corners/ {
set p1 [lindex $corners 1]
set p2 [lindex $corners 2]
set p3 [lindex $corners 3]
- Display::drawOnTop Display::fillQuad $p0 $p1 $p2 $p3 black
+ Wish to draw a quad with p0 $p0 p1 $p1 p2 $p2 p3 $p3 color black layer 1
} \ No newline at end of file
diff --git a/virtual-programs/new-program-web-editor.folk b/virtual-programs/new-program-web-editor.folk
index a7c176cc..f972cfc7 100644
--- a/virtual-programs/new-program-web-editor.folk
+++ b/virtual-programs/new-program-web-editor.folk
@@ -152,12 +152,14 @@ Wish the web server handles route "/new" with handler {
Assert web claims {${program}} has program code {${code}}
if {$::isLaptop} { Step }
`);
- setTimeout(() => {
- send(`
+ [500, 1000, 3000].forEach(timeout => {
+ setTimeout(() => {
+ send(`
set errors [Statements::findMatches [list {${program}} has error /err/ with info /errorInfo/]]
::websocket::send $chan text [join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n"]
`);
- }, 500);
+ }, timeout);
+ });
}
let jobid;
function handlePrint() {
diff --git a/virtual-programs/outline.folk b/virtual-programs/outline.folk
index f62ab0ba..1d6411cb 100644
--- a/virtual-programs/outline.folk
+++ b/virtual-programs/outline.folk
@@ -1,9 +1,12 @@
-proc loopRegion {edges vertices weight color} {
- foreach edge $edges {
- set from [lindex $vertices [lindex $edge 0]]
- set to [lindex $vertices [lindex $edge 1]]
- Display::stroke [list $from $to] $weight $color
- }
+proc loopRegion {edges vertices width color} {
+ set instances [list]
+ set color [getColor $color]
+ foreach edge $edges {
+ set from [lindex $vertices [lindex $edge 0]]
+ set to [lindex $vertices [lindex $edge 1]]
+ lappend instances [list $from $to $width $color]
+ }
+ Wish the GPU draws pipeline "line" with instances $instances
}
When the collected matches for [list /someone/ wishes /thing/ is outlined /color/] are /matches/ {
@@ -27,4 +30,4 @@ When /someone/ wishes /thing/ is outlined thick /color/ & /thing/ has region /re
# FIXME: assumes path
lassign $region vertices edges
loopRegion $edges $vertices 6 $color
-} \ No newline at end of file
+}
diff --git a/virtual-programs/shapes.folk b/virtual-programs/shapes.folk
index 08106475..10589f38 100644
--- a/virtual-programs/shapes.folk
+++ b/virtual-programs/shapes.folk
@@ -1,7 +1,14 @@
-# numPoints 2 => line
-# numPoints 3 => triangle
-# numPoints 4 => square
-proc shape {numPoints c r {angle 0} {color white} {filled false} args} {
+# sides 2 => line
+# sides 3 => triangle
+# sides 4 => square
+When /someone/ wishes to draw a shape with /...options/ {
+ set numPoints [dict get $options sides]
+ set c [dict get $options center]
+ set r [dict get $options radius]
+ set radians [dict_getdef $options radians 0]
+ set color [dict_getdef $options color white]
+ set filled [dict_getdef $options filled false]
+
set p [list 0 0]
set center $p
set points [list $p]
@@ -19,24 +26,34 @@ proc shape {numPoints c r {angle 0} {color white} {filled false} args} {
set points [lmap v $points {
set v [vec2 sub $v $center]
- set v [vec2 rotate $v $angle]
+ set v [vec2 rotate $v $radians]
set v [vec2 add $v $c]
set v
}]
if {$filled} {
- Display::fillPolygon $points $color
+ Wish to draw a polygon with points $points color $color
} else {
- Display::stroke $points 1 $color
+ Wish to draw a stroke with points $points width 1 color $color
}
}
-
-When /someone/ wishes /p/ draws a /color/ /shape/ offset /offsetVector/ & /p/ has region /r/ {
+set shapes [dict create triangle 3 square 4 pentagon 5 hexagon 6 \
+ septagon 7 octagon 8 nonagon 9]
+When /someone/ wishes /p/ draws a /shape/ {
+ # TODO: This is a hack because rest pattern doesn't match empty
+ # sequence at end.
+ Wish $p draws a $shape with color white
+}
+When /someone/ wishes /p/ draws an /shape/ { Wish $p draws a $shape }
+When /someone/ wishes /p/ draws a /shape/ with /...options/ & /p/ has region /r/ {
lassign [region centroid $r] x y
set width [region width $r]
set height [region height $r]
- lassign $offsetVector offsetX offsetY
+ lassign [dict_getdef $options offset {0 0}] offsetX offsetY
+ set radius [dict_getdef $options radius 50]
+ set color [dict_getdef $options color white]
+ set filled [dict_getdef $options filled false]
if {$offsetX != 0} {
set x [expr {$x + $offsetX}]
@@ -48,74 +65,39 @@ When /someone/ wishes /p/ draws a /color/ /shape/ offset /offsetVector/ & /p/ ha
set angle [region angle $r]
set p [list $x $y]
- # puts "drawing a $shape at $x $y with width $width and height $height"
- set radius 50
- switch -nocase $shape {
- triangle {
- shape 3 $p $radius $angle $color
- }
- square {
- shape 4 $p $radius $angle $color
- }
- pentagon {
- shape 5 $p $radius $angle $color
- }
- hexagon {
- shape 6 $p $radius $angle $color
- }
- septagon {
- shape 7 $p $radius $angle $color
- }
- octagon {
- shape 8 $p $radius $angle $color
- }
- nonagon {
- shape 9 $p $radius $angle $color
- }
- circle {
- Display::circle $x $y $radius 5 $color
- }
- default {
- shape 2 $p $radius $angle $color
- }
+ if {$shape eq "circle"} {
+ Wish to draw a circle with center $p radius $radius thickness 5 color $color filled $filled
+ } elseif {[dict exists $shapes $shape]} {
+ Wish to draw a shape with sides [dict get $shapes $shape] \
+ center $p radius $radius radians $angle color $color filled $filled
+ } else {
+ Wish to draw a shape with sides 2 \
+ center $p radius $radius radians $angle color $color filled $filled
}
}
-
-set defaultColor white
-
-When /someone/ wishes /p/ draws a /color/ /shape/ & /p/ has region /r/ {
- Wish $p draws a $color $shape offset {0 0}
-}
-
-When /someone/ wishes /p/ draws a /shape/ & /p/ has region /r/ {
- Wish $p draws a $defaultColor $shape
-}
-
-When /someone/ wishes /p/ draws a /shape/ offset /offsetVector/ {
- Wish $p draws a $defaultColor $shape offset $offsetVector
+When /someone/ wishes /p/ draws an /shape/ with /...options/ {
+ Wish $p draws a $shape with {*}$options
}
Claim $this has demo {
Wish $this draws a circle
- Wish $this draws a skyblue triangle
- Wish $this draws a green triangle offset {280 0}
- Wish $this draws a gold pentagon offset {200 0}
- Wish $this draws a red octagon offset {250 80}
+ Wish $this draws a triangle with color skyblue
+ Wish $this draws a triangle with color green offset {280 0}
+ Wish $this draws a pentagon with color gold offset {200 0}
+ Wish $this draws an octagon with color red offset {250 80}
When the clock time is /t/ {
set offsetVector [list [sin $t] [cos $t]]
set offsetVector [::vec2::scale $offsetVector 105]
- Wish $this draws a palegoldenrod circle offset $offsetVector
+ Wish $this draws a circle with color palegoldenrod offset $offsetVector
}
# This toggles a square between filled and unfilled
- # TODO cwervo: Support `Wish $this draws a circle with options /optionsDict/` to make this cleaner
- # optionsDisct: {size number?: 50, filled boolean?: false, color string?: white, thickness string? white }
When $this has region /r/ & the clock time is /t/ {
lassign [region centroid $r] x y
- set fill [expr {round(sin($t) * 2) % 2 == 0 ? true : false}]
+ set fill [expr {round(sin($t) * 2) % 2 == 0}]
set y [- $y 150]
- shape 4 [list [- $x 100] $y] 0 60 white $fill
+ Wish to draw a shape with sides 4 center [list [- $x 100] $y] radius 60 color white filled $fill
}
Wish $this is outlined white
diff --git a/virtual-programs/sprites.folk b/virtual-programs/sprites.folk
index 0e4fc87e..48f8a1b1 100644
--- a/virtual-programs/sprites.folk
+++ b/virtual-programs/sprites.folk
@@ -23,7 +23,7 @@ When /anyone/ wishes /p/ draws sprite /path/ with /frameCount/ frames and /colum
set subimage [image subimage $im $x $y $spriteWidth $spriteHeight]
set center [region centroid $r]
set angle [region angle $r]
- Wish display runs [list Display::image {*}$center $subimage $angle 1]
+ Wish to draw an image with center $center image $subimage radians $angle
}
}
diff --git a/virtual-programs/tags-and-calibration.folk b/virtual-programs/tags-and-calibration.folk
index ce368f00..dbe58f16 100644
--- a/virtual-programs/tags-and-calibration.folk
+++ b/virtual-programs/tags-and-calibration.folk
@@ -9,10 +9,11 @@ namespace import ::math::linearalgebra::add \
::math::linearalgebra::solvePGauss
When the camera frame is /f/ {
- Claim camera Camera has width [dict get $f width] height [dict get $f height]
+ Claim the camera Camera has width [dict get $f width] height [dict get $f height]
}
-When camera /camera/ has width /cameraWidth/ height /cameraHeight/ {
+When the display /display/ has width /displayWidth/ height /displayHeight/ &\
+ the camera /camera/ has width /cameraWidth/ height /cameraHeight/ {
puts "Evaluating calibration for $camera ($cameraWidth x $cameraHeight)"
# camera (tag) point -> projection point
# calibration for Hex
@@ -27,10 +28,10 @@ When camera /camera/ has width /cameraWidth/ height /cameraHeight/ {
set points [list]
foreach correspondence $generatedCalibration::points {
lassign $correspondence cx cy dx dy
- lappend points [list [expr {double($cx)/$generatedCalibration::cameraWidth*$Camera::WIDTH}] \
- [expr {double($cy)/$generatedCalibration::cameraHeight*$Camera::HEIGHT}] \
- [expr {double($dx)/$generatedCalibration::displayWidth*$Display::WIDTH}] \
- [expr {double($dy)/$generatedCalibration::displayHeight*$Display::HEIGHT}]]
+ lappend points [list [expr {double($cx)/$generatedCalibration::cameraWidth*$cameraWidth}] \
+ [expr {double($cy)/$generatedCalibration::cameraHeight*$cameraHeight}] \
+ [expr {double($dx)/$generatedCalibration::displayWidth*$displayWidth}] \
+ [expr {double($dy)/$generatedCalibration::displayHeight*$displayHeight}]]
}
}
if {[llength $points] < 4} {
diff --git a/virtual-programs/title.folk b/virtual-programs/title.folk
index fccacc17..3187ed3c 100644
--- a/virtual-programs/title.folk
+++ b/virtual-programs/title.folk
@@ -14,7 +14,7 @@ When /thing/ has region /region/ {
if {$text eq ""} { return }
set scale 1
- Display::text {*}$top $scale $text $radians
+ Wish to draw text with center $top scale $scale text $text radians $radians
}
When the collected matches for [list /someone/ wishes $thing is footnoted /text/] are /matches/ {
@@ -22,6 +22,6 @@ When /thing/ has region /region/ {
if {$text eq ""} { return }
set scale 1
- Display::text {*}$bot $scale $text $radians
+ Wish to draw text with center $bot scale $scale text $text radians $radians
}
}
diff --git a/virtual-programs/web-editor.folk b/virtual-programs/web-editor.folk
index 245b6043..fb899560 100644
--- a/virtual-programs/web-editor.folk
+++ b/virtual-programs/web-editor.folk
@@ -96,13 +96,14 @@ Wish the web server handles route {/page/(.*)$} with handler {
send(`EditVirtualProgram file_name {${code}}`)
}
-
- setTimeout(() => {
- send(`
+ [500, 1000, 3000].forEach(timeout => {
+ setTimeout(() => {
+ send(`
set errors [Statements::findMatches [list program_id has error /err/ with info /errorInfo/]]
::websocket::send $chan text [join [list "Error:" {*}[lmap e $errors {dict get $e errorInfo}]] "\n"]
`);
- }, 500);
+ }, 500);
+ });
}
let jobid;