diff --git a/Makefile b/Makefile index 30882ab8..15a7566b 100644 --- a/Makefile +++ b/Makefile @@ -115,16 +115,17 @@ kill-folk: fi FOLK_REMOTE_NODE ?= folk-live +FOLK_SYNC_IGNORES ?= $(shell git rev-parse --git-path ignores.tmp 2>/dev/null || printf '%s\n' .git/ignores.tmp) sync: ssh $(FOLK_REMOTE_NODE) -t \ 'cd ~/folk && git init > /dev/null && git ls-files --exclude-standard -oi --directory' \ - > .git/ignores.tmp || true - git ls-files --exclude-standard -oi --directory >> .git/ignores.tmp + > '$(FOLK_SYNC_IGNORES)' || true + git ls-files --exclude-standard -oi --directory >> '$(FOLK_SYNC_IGNORES)' rsync --timeout=15 -e "ssh -o StrictHostKeyChecking=no" \ --archive --delete --itemize-changes \ --exclude='/.git' \ - --exclude-from='.git/ignores.tmp' \ + --exclude-from='$(FOLK_SYNC_IGNORES)' \ --exclude='vendor/tracy/public/TracyClient.o' \ --include='vendor/tracy/public/***' \ --exclude='vendor/tracy/*' \ diff --git a/README.md b/README.md index ec371e8f..91af04b2 100644 --- a/README.md +++ b/README.md @@ -517,7 +517,7 @@ Use it in an animation: ``` When the clock time is /t/ { - Wish $this draws a circle with offset [list [expr {sin($t) * 50}] 0] + Wish $this draws a circle with offset [list [expr {sin($t) * 0.05}] 0] radius 0.012 } ``` diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..a14866a1 Binary files /dev/null and b/assets/logo.png differ diff --git a/builtin-programs/connections.folk b/builtin-programs/connections.folk deleted file mode 100644 index 8efa359a..00000000 --- a/builtin-programs/connections.folk +++ /dev/null @@ -1,69 +0,0 @@ -# Connection wish fulfillment -# for wishes of the form: -# "Wish $tag is connected to $tag2" or "Wish $tag is dynamically connected to $tag2" - -When /anyone/ wishes /source/ is connected to /sink/ { - Wish $source is connected to $sink from centroid to centroid -} -When /anyone/ wishes /source/ is dynamically connected to /sink/ { - Wish $source is dynamically connected to $sink from centroid to centroid -} - -When /anyone/ wishes /source/ is connected to /sink/ /...options/ & \ - /source/ has region /source_region/ & \ - /sink/ has region /sink_region/ { - if {$source == $sink} {return} - - set p1 [dict_getdef $options from centroid] - set p2 [dict_getdef $options to centroid] - set source [region $p1 $source_region] - set sink [region $p2 $sink_region] - - set direction [vec2 sub $sink $source] - set color [dict_getdef $options color grey] - set layer [dict_getdef $options layer 0] - - set c [vec2 scale [vec2 add $source $sink] 0.5] - set angle [expr {atan2(-[lindex $direction 1], [lindex $direction 0]) - 3.14159/2}] - - Wish to draw a stroke with points [list $source $sink] width 2 color $color layer $layer - Wish to draw a shape with sides 3 center $c radius 30 radians $angle color $color filled true layer $layer -} - -set speed 75 -set spacing 50 -set maxsize 25 - -When /anyone/ wishes /source/ is dynamically connected to /sink/ /...options/ & \ - /source/ has region /source_region/ & \ - /sink/ has region /sink_region/ { - - if {$source == $sink} {return} - - set p1 [dict_getdef $options from centroid] - set p2 [dict_getdef $options to centroid] - set source [region $p1 $source_region] - set sink [region $p2 $sink_region] - - set direction [vec2 normalize [vec2 sub $sink $source]] - set distance [vec2 distance $sink $source] - set angle [expr {atan2(-[lindex $direction 1], [lindex $direction 0]) - 3.14159/2}] - - set color [dict_getdef $options color white] - set layer [dict_getdef $options layer 0] - - lassign [vec2 scale [vec2 add $source $sink] 0.5] cx cy - - Wish to draw a stroke with points [list $source $sink] width 1 color $color layer $layer - - When the clock time is /t/ { - set offset [expr {round($t*$speed) % $spacing}] - set count [expr {round($distance / $spacing)}] - - 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))}] - Wish to draw a shape with sides 3 center $c radius $s radians $angle color $color filled true layer $layer - } - } -} diff --git a/builtin-programs/decorations/label.folk b/builtin-programs/decorations/label.folk index 03341d6d..5d1149f0 100644 --- a/builtin-programs/decorations/label.folk +++ b/builtin-programs/decorations/label.folk @@ -1,35 +1,46 @@ -When /thing/ has resolved geometry /geom/ { +fn drawLabelMaxLineLength {text} { + set maxLength 0 + foreach line [split $text "\n"] { + set lineLength [string length $line] + if {$lineLength > $maxLength} { + set maxLength $lineLength + } + } + return $maxLength +} + +fn drawLabelDefaultScale {text} { + set maxLength [drawLabelMaxLineLength $text] + if {$maxLength == 0} { return 0.02 } + ::math::min 0.02 [/ 0.45 $maxLength] +} + +fn drawLabelDefaultOptions {text width height} { + set scale [drawLabelDefaultScale $text] + set position [lmap value [list [expr {$width / 2.0}] [expr {$height / 2.0}]] { + format "%sm" $value + }] + dict create \ + position $position \ + scale [format "%sm" $scale] \ + anchor center \ + font "PTSans-Regular" +} + +When display /disp/ has width /displayWidth/ height /displayHeight/ &\ + /thing/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ { When the collected results for [list /someone/ wishes $thing is labelled /text/ with /...options/] are /results/ { set text [join [lmap result $results {dict get $result text}] "\n"] if {$text eq ""} { return } - # Split text into lines and find the longest line. - set lines [split $text "\n"] - set maxLength 0 - foreach line $lines { - set lineLength [string length $line] - if {$lineLength > $maxLength} { - set maxLength $lineLength - } - } - - # Set default scale based on longest line length. - # Scale inversely with length to keep text readable. - set defaultScale [::math::min 0.02 [/ 0.45 $maxLength]] - - set x [/ $geom(width) 2.0] - try { - set y $($geom(top) + $geom(tagSize) + $geom(bottom)/2.0) - } on error e { - set y [/ $geom(height) 2.0] + set options [drawLabelDefaultOptions $text $width $height] + foreach result $results { + set options [dict merge $options [dict get $result options]] } - set options [dict create x $x y $y scale $defaultScale] - # FIXME: support per-label options; right now, this just - # applies an arbitrary label's options to all of them - # together. - set options [dict merge $options [dict get $result options]] dict set options text $text - Wish to draw text onto $thing with {*}$options + + Wish to draw text onto $disp in surface $surface with {*}$options } } diff --git a/builtin-programs/decorations/outline.folk b/builtin-programs/decorations/outline.folk index c663af26..6fd0b0f5 100644 --- a/builtin-programs/decorations/outline.folk +++ b/builtin-programs/decorations/outline.folk @@ -1,13 +1,35 @@ -When /someone/ wishes /thing/ is outlined /color/ &\ - /thing/ has resolved geometry /geom/ { - dict with geom { - set points [list [list 0 0] \ - [list $width 0] \ - [list $width $height] \ - [list 0 $height] \ - [list 0 0]] +fn drawOutlinePoints {width height} { + lmap point [list \ + {0 0} \ + [list $width 0] \ + [list $width $height] \ + [list 0 $height] \ + {0 0}] { + lmap value $point { + format "%sm" $value + } } +} + +When /someone/ wishes /thing/ is outlined /color/ { + Wish $thing is outlined with color $color +} + +When /someone/ wishes /thing/ is outlined /color/ with /...options/ { + if {![info exists options]} { set options [dict create] } + Wish $thing is outlined with color $color {*}$options +} + +When display /disp/ has width /displayWidth/ height /displayHeight/ &\ + /thing/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ &\ + /someone/ wishes /thing/ is outlined with /...options/ { + if {![info exists options]} { set options [dict create] } + set color [dict getdef $options color white] + set outlineWidth [dict getdef $options width [dict getdef $options thickness [format "%sm" 0.01]]] + set layer [dict getdef $options layer 2] - Wish to draw a line onto $thing with \ - points $points width 0.01 color $color + Wish to draw a line onto $disp in surface $surface with \ + points [drawOutlinePoints $width $height] \ + width $outlineWidth color $color layer $layer } diff --git a/builtin-programs/demos.folk b/builtin-programs/demos.folk index 192afd23..4a28aee5 100644 --- a/builtin-programs/demos.folk +++ b/builtin-programs/demos.folk @@ -24,7 +24,7 @@ Claim 45004 has demo code { } Claim 45005 has demo code { When the clock time is /t/ { - Wish $this draws a circle offset [list expr {sin($t) * 50} 0] + Wish $this draws a circle with offset [list [expr {sin($t) * 0.05}] 0] radius 0.012 } } Claim 45006 has demo code { diff --git a/builtin-programs/display/arc.folk b/builtin-programs/display/arc.folk deleted file mode 100644 index f6a0c678..00000000 --- a/builtin-programs/display/arc.folk +++ /dev/null @@ -1,39 +0,0 @@ -# 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 vec4(vertices[gl_VertexIndex], 0.0, 1.0); -} { - #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/builtin-programs/display/curve.folk b/builtin-programs/display/curve.folk deleted file mode 100644 index 9082d117..00000000 --- a/builtin-programs/display/curve.folk +++ /dev/null @@ -1,135 +0,0 @@ - -# Bezier implementation from https://www.shadertoy.com/view/XdVBWd - -Wish the GPU compiles function "bboxBezier" {{vec2 p0 vec2 p1 vec2 p2 vec2 p3} vec4 { - // Exact BBox to a quadratic bezier - // extremes - vec2 mi = min(p0,p3); - vec2 ma = max(p0,p3); - - vec2 k0 = -1.0*p0 + 1.0*p1; - vec2 k1 = 1.0*p0 - 2.0*p1 + 1.0*p2; - vec2 k2 = -1.0*p0 + 3.0*p1 - 3.0*p2 + 1.0*p3; - - vec2 h = k1*k1 - k0*k2; - - if( h.x>0.0 ) - { - h.x = sqrt(h.x); - //float t = (-k1.x - h.x)/k2.x; - float t = k0.x/(-k1.x-h.x); - if( t>0.0 && t<1.0 ) - { - float s = 1.0-t; - float q = s*s*s*p0.x + 3.0*s*s*t*p1.x + 3.0*s*t*t*p2.x + t*t*t*p3.x; - mi.x = min(mi.x,q); - ma.x = max(ma.x,q); - } - //t = (-k1.x + h.x)/k2.x; - t = k0.x/(-k1.x+h.x); - if( t>0.0 && t<1.0 ) - { - float s = 1.0-t; - float q = s*s*s*p0.x + 3.0*s*s*t*p1.x + 3.0*s*t*t*p2.x + t*t*t*p3.x; - mi.x = min(mi.x,q); - ma.x = max(ma.x,q); - } - } - - if( h.y>0.0) - { - h.y = sqrt(h.y); - //float t = (-k1.y - h.y)/k2.y; - float t = k0.y/(-k1.y-h.y); - if( t>0.0 && t<1.0 ) - { - float s = 1.0-t; - float q = s*s*s*p0.y + 3.0*s*s*t*p1.y + 3.0*s*t*t*p2.y + t*t*t*p3.y; - mi.y = min(mi.y,q); - ma.y = max(ma.y,q); - } - //t = (-k1.y + h.y)/k2.y; - t = k0.y/(-k1.y+h.y); - if( t>0.0 && t<1.0 ) - { - float s = 1.0-t; - float q = s*s*s*p0.y + 3.0*s*s*t*p1.y + 3.0*s*t*t*p2.y + t*t*t*p3.y; - mi.y = min(mi.y,q); - ma.y = max(ma.y,q); - } - } - - return vec4( mi, ma ); -}} - -Wish the GPU compiles function sdSegmentSq {{vec2 p vec2 a vec2 b} float { - vec2 pa = p-a, ba = b-a; - float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 ); - vec2 d = pa - ba*h; - return dot(d, d); -}} - -Wish the GPU compiles function udBezier {{vec2 p0 vec2 p1 vec2 p2 vec2 p3 vec2 pos} vec2 { - const int kNum = 50; - vec2 res = vec2(1e10,0.0); - vec2 a = p0; - for( int i=1; i 0.0) { + if ((end < TAU && angle > c_start && angle < end) || + (end >= TAU && (angle > c_start || angle < end - TAU))) { + return color; + } + } + + return vec4(0.0); + }]] + +When the color map is /colorMap/ &\ + /p/ has canvas /id/ with /...wiOptions/ &\ + /p/ has canvas projection /surfaceToClip/ &\ + /someone/ wishes to draw an arc onto /p/ 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 start [dict get $options start] + set arclen [dict get $options arclen] + + set color [dict get $options color] + set color [dict getdef $colorMap $color $color] + set layer [dict getdef $options layer 0] + + set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] + + Wish the GPU draws pipeline "arc" onto canvas $id with arguments \ + [list $wiResolution $surfaceToClip \ + $center $radius $thickness $start $arclen $color] \ + layer $layer +} diff --git a/builtin-programs/draw/connections.folk b/builtin-programs/draw/connections.folk new file mode 100644 index 00000000..65c84e53 --- /dev/null +++ b/builtin-programs/draw/connections.folk @@ -0,0 +1,163 @@ +# Connection wish fulfillment for wishes of the form: +# Wish $tag is connected to $tag2 +# Wish $tag is dynamically connected to $tag2 + +fn drawConnectionArrowPoints {x y radius} { + set baseX [expr {$x - $radius}] + set tipX [expr {$x + $radius}] + set spread [expr {$radius * 0.8}] + list [list $tipX $y] \ + [list $baseX [expr {$y - $spread}]] \ + [list $baseX [expr {$y + $spread}]] +} + +fn drawConnectionPhysicalLength {value} { + if {[llength $value] != 1} { + error "draw/connections: expected a scalar physical length, got $value" + } + + set unit "" + set amount $value + foreach suffix {mm cm m} { + if {[string match *$suffix $value]} { + set unit $suffix + set amount [string range $value 0 end-[string length $suffix]] + break + } + } + + if {![string is double -strict $amount]} { + error "draw/connections: invalid physical length $value" + } + + switch -- $unit { + "" - cm { return [expr {double($amount) * 0.01}] } + mm { return [expr {double($amount) * 0.001}] } + m { return [expr {double($amount)}] } + default { error "draw/connections: invalid physical unit $unit" } + } +} + +fn drawConnectionMeterPoint {point} { + lmap value $point { + format "%sm" $value + } +} + +fn drawConnectionVectorDistance {a b} { + set sum 0.0 + foreach av $a bv $b { + set d [expr {$av - $bv}] + set sum [expr {$sum + $d * $d}] + } + expr {sqrt($sum)} +} + +fn drawConnectionDrawArrow {disp surface x y radius color layer} { + if {$radius <= 0.0} { return } + lassign [drawConnectionArrowPoints $x $y $radius] p0 p1 p2 + Wish to draw a triangle onto $disp in surface $surface with \ + p0 [drawConnectionMeterPoint $p0] \ + p1 [drawConnectionMeterPoint $p1] \ + p2 [drawConnectionMeterPoint $p2] \ + color $color layer $layer +} + +When /anyone/ wishes /source/ is connected to /sink/ { + Wish $source is connected to $sink with from centroid to centroid +} + +When /anyone/ wishes /source/ is connected to /sink/ from /from/ to /to/ { + Wish $source is connected to $sink with from $from to $to +} + +When /anyone/ wishes /source/ is dynamically connected to /sink/ { + Wish $source is dynamically connected to $sink with from centroid to centroid +} + +When /anyone/ wishes /source/ is dynamically connected to /sink/ from /from/ to /to/ { + Wish $source is dynamically connected to $sink with from $from to $to +} + +When -atomically /anyone/ wishes /source/ is connected to /sink/ with /...options/ &\ + the draw space library is /drawSpaceLib/ &\ + /source/ has quad /sourceQuad/ &\ + /sink/ has quad /sinkQuad/ { + if {$source eq $sink} { return } + + set fromSelector [dict getdef $options from centroid] + set toSelector [dict getdef $options to centroid] + + set from [$drawSpaceLib quadPoint $sourceQuad $fromSelector] + set to [$drawSpaceLib quadPoint $sinkQuad $toSelector] + set distance [drawConnectionVectorDistance $from $to] + if {$distance == 0.0} { return } + + set surfaceHeight [drawConnectionPhysicalLength [dict getdef $options surfaceHeight 6]] + set disp [$drawSpaceLib display] + set connection [list connection $source $sink $fromSelector $toSelector $disp] + Claim -keep 50ms $connection has quad \ + [$drawSpaceLib surfaceQuadBetween $from $to $surfaceHeight] + + When -atomically $connection has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + $disp has canvas projection for surface /surface/ /surfaceToClip/ { + set color [dict getdef $options color grey] + set layer [dict getdef $options layer 0] + set lineWidth [dict getdef $options width 0.2] + set arrowRadius [drawConnectionPhysicalLength [dict getdef $options arrowRadius 2]] + set y [expr {$height / 2.0}] + set mid [expr {$width / 2.0}] + + Wish to draw a line onto $disp in surface $surface with \ + points [list [drawConnectionMeterPoint [list 0 $y]] \ + [drawConnectionMeterPoint [list $width $y]]] \ + width $lineWidth color $color layer $layer + drawConnectionDrawArrow $disp $surface $mid $y $arrowRadius $color $layer + } +} + +When -atomically /anyone/ wishes /source/ is dynamically connected to /sink/ with /...options/ &\ + the draw space library is /drawSpaceLib/ &\ + /source/ has quad /sourceQuad/ &\ + /sink/ has quad /sinkQuad/ { + if {$source eq $sink} { return } + + set fromSelector [dict getdef $options from centroid] + set toSelector [dict getdef $options to centroid] + + set from [$drawSpaceLib quadPoint $sourceQuad $fromSelector] + set to [$drawSpaceLib quadPoint $sinkQuad $toSelector] + set distance [drawConnectionVectorDistance $from $to] + if {$distance == 0.0} { return } + + set surfaceHeight [drawConnectionPhysicalLength [dict getdef $options surfaceHeight 6]] + set disp [$drawSpaceLib display] + set connection [list dynamic-connection $source $sink $fromSelector $toSelector $disp] + Claim -keep 50ms $connection has quad \ + [$drawSpaceLib surfaceQuadBetween $from $to $surfaceHeight] + + When -atomically $connection has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + $disp has canvas projection for surface /surface/ /surfaceToClip/ { + set color [dict getdef $options color white] + set layer [dict getdef $options layer 0] + set lineWidth [dict getdef $options width 0.1] + set speed [drawConnectionPhysicalLength [dict getdef $options speed 12]] + set spacing [drawConnectionPhysicalLength [dict getdef $options spacing 5]] + set maxSize [drawConnectionPhysicalLength [dict getdef $options maxsize 2.5]] + set y [expr {$height / 2.0}] + + Wish to draw a line onto $disp in surface $surface with \ + points [list [drawConnectionMeterPoint [list 0 $y]] \ + [drawConnectionMeterPoint [list $width $y]]] \ + width $lineWidth color $color layer $layer + + When the clock time is /t/ { + set offset [expr {fmod($t * $speed, $spacing)}] + for {set p $offset} {$p < $width} {set p [expr {$p + $spacing}]} { + set edgeDistance [expr {min($p, $width - $p)}] + set radius [expr {min($maxSize, 0.20 * $edgeDistance)}] + drawConnectionDrawArrow $disp $surface $p $y $radius $color $layer + } + } + } +} diff --git a/builtin-programs/draw/curve.folk b/builtin-programs/draw/curve.folk new file mode 100644 index 00000000..475ea1d3 --- /dev/null +++ b/builtin-programs/draw/curve.folk @@ -0,0 +1,79 @@ +# Bezier implementation adapted from https://www.shadertoy.com/view/XdVBWd + +Wish the GPU compiles function "curveSegmentDistance" {{vec2 p vec2 a vec2 b} float { + vec2 pa = p - a; + vec2 ba = b - a; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + vec2 d = pa - ba * h; + return dot(d, d); +}} + +Wish the GPU compiles function "curveBezierDistance" {{vec2 p0 vec2 p1 vec2 p2 vec2 p3 vec2 pos fn curveSegmentDistance} float { + const int kNumSamples = 50; + float distance = 1e10; + vec2 a = p0; + for (int i = 1; i < kNumSamples; i++) { + float t = float(i) / float(kNumSamples - 1); + float s = 1.0 - t; + vec2 b = p0 * s * s * s + + p1 * 3.0 * s * s * t + + p2 * 3.0 * s * t * t + + p3 * t * t * t; + distance = min(distance, curveSegmentDistance(pos, a, b)); + a = b; + } + return sqrt(distance); +}} + +Wish the GPU compiles pipeline "curve" { + {vec2 viewport mat3 surfaceToClip + vec2 p0 vec2 p1 vec2 p2 vec2 p3 float thickness vec4 color} { + vec2 from = min(min(p0, p1), min(p2, p3)) - thickness; + vec2 to = max(max(p0, p1), max(p2, p3)) + thickness; + + vec2 vertices[6] = vec2[6]( + from, + vec2(to.x, from.y), + vec2(from.x, to.y), + vec2(to.x, from.y), + to, + vec2(from.x, to.y) + ); + + vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); + return vec4(v.xy / v.z, 0.0, 1.0); + } {fn curveBezierDistance} { + vec2 clipXy = (gl_FragCoord.xy / viewport) * 2.0 - 1.0; + vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); + surfaceXy /= surfaceXy.z; + + float distance = curveBezierDistance(p0, p1, p2, p3, surfaceXy.xy); + float edge = max(fwidth(distance), thickness * 0.05); + float alpha = 1.0 - smoothstep(thickness, thickness + edge, distance); + + return (alpha < 0.01) ? vec4(0.0) : vec4(color.rgb, color.a * alpha); + } +} + +When the color map is /colorMap/ &\ + /p/ has canvas /id/ with /...wiOptions/ &\ + /p/ has canvas projection /surfaceToClip/ &\ + /someone/ wishes to draw a curve onto /p/ with /...options/ { + + set p0 [dict get $options p0] + set p1 [dict get $options p1] + set p2 [dict get $options p2] + set p3 [dict get $options p3] + set thickness [dict get $options thickness] + + set color [dict get $options color] + set color [dict getdef $colorMap $color $color] + set layer [dict getdef $options layer 0] + + set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] + + Wish the GPU draws pipeline "curve" onto canvas $id with arguments \ + [list $wiResolution $surfaceToClip \ + $p0 $p1 $p2 $p3 $thickness $color] \ + layer $layer +} diff --git a/builtin-programs/draw/fill.folk b/builtin-programs/draw/fill.folk index 4e977c1e..a54e5f48 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -10,14 +10,17 @@ Wish the GPU compiles pipeline "fillTriangle" { When the color map is /colorMap/ { -When /someone/ wishes to draw a triangle with /...options/ { +When /someone/ wishes to draw a triangle onto /p/ with /...options/ &\ + /p/ has canvas /id/ with /...wiOptions/ &\ + /p/ has canvas projection /surfaceToClip/ { dict with options { if {![info exists layer]} { set layer 0 } set color [dict getdef $colorMap $color $color] - Wish the GPU draws pipeline "fillTriangle" with arguments \ - [list $p0 $p1 $p2 $color] layer $layer + Wish the GPU draws pipeline "fillTriangle" onto canvas $id with arguments \ + [list $surfaceToClip $p0 $p1 $p2 $color] layer $layer } } + When /someone/ wishes to draw a quad onto /p/ with /...options/ &\ /p/ has canvas /id/ with /...wiOptions/ &\ /p/ has canvas projection /surfaceToClip/ { @@ -30,7 +33,11 @@ When /someone/ wishes to draw a quad onto /p/ with /...options/ &\ [list $surfaceToClip $p0 $p1 $p3 $color] layer $layer } } -When /someone/ wishes to draw a polygon with /...options/ { + +When /someone/ wishes to draw a polygon onto /p/ with /...options/ &\ + /p/ has canvas /id/ with /...wiOptions/ &\ + /p/ has canvas projection /surfaceToClip/ { + set points [dict get $options points] set color [dict get $options color] set layer [dict getdef $options layer 0] @@ -39,31 +46,39 @@ When /someone/ wishes to draw a polygon with /...options/ { 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 \ + Wish to draw a triangle onto $p with \ p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \ color $color layer $layer } elseif {$num_points == 4} { - Wish to draw a quad with \ + Wish to draw a quad onto $p with \ p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] p3 [lindex $points 3] \ color $color layer $layer } else { # Get the first point in the list as the "base" point of the triangles set p0 [lindex $points 0] - set color [dict getdef $colorMap $color $color] + + # Batch the fanned-out triangles into a single GPU instance list + set instances [list] 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 $color] layer $layer + lappend instances [list $surfaceToClip $p0 $p1 $p2 $color] } + Wish the GPU draws pipeline "fillTriangle" onto canvas $id \ + with instances $instances layer $layer } } -} - When /someone/ wishes /page/ is filled with /...options/ &\ - /page/ has region /region/ { - set points [region vertices $region] - Wish to draw a polygon with points $points {*}$options + /page/ has resolved geometry /geom/ { + dict with geom { + set points [list [list 0 0] \ + [list $width 0] \ + [list $width $height] \ + [list 0 $height]] + } + Wish to draw a polygon onto $page with points $points {*}$options } + +} \ No newline at end of file diff --git a/builtin-programs/draw/hit-targets.folk b/builtin-programs/draw/hit-targets.folk new file mode 100644 index 00000000..6d784244 --- /dev/null +++ b/builtin-programs/draw/hit-targets.folk @@ -0,0 +1,310 @@ +# Child surfaces and hit targets are small physical sub-quads of a parent +# quad. They are real quad-backed objects, so the normal drawing-space and +# pointing APIs can see them. + +fn drawHitTargetTruthy {value} { + expr {$value in {1 true yes on}} +} + +fn drawHitTargetName {options} { + if {[dict exists $options name]} { + return [dict get $options name] + } + if {[dict exists $options index]} { + return [dict get $options index] + } + return 0 +} + +fn drawHitTargetId {parent options} { + if {[dict exists $options id]} { + return [dict get $options id] + } + list hit target of $parent [drawHitTargetName $options] +} + +fn drawHitTargetPhysicalLength {value} { + if {[llength $value] != 1} { + error "draw/hit-targets: expected a scalar physical length, got $value" + } + + set unit "" + set amount $value + foreach suffix {mm cm m} { + if {[string match *$suffix $value]} { + set unit $suffix + set amount [string range $value 0 end-[string length $suffix]] + break + } + } + + if {![string is double -strict $amount]} { + error "draw/hit-targets: invalid physical length $value" + } + + switch -- $unit { + "" - cm { return [expr {double($amount) * 0.01}] } + mm { return [expr {double($amount) * 0.001}] } + m { return [expr {double($amount)}] } + default { error "draw/hit-targets: invalid physical unit $unit" } + } +} + +fn drawHitTargetMeterPoint {point} { + lmap value $point { + format "%sm" $value + } +} + +fn drawHitTargetVectorAdd {a b} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av + $bv}] + } + return $out +} + +fn drawHitTargetVectorDistance {a b} { + set sum 0.0 + foreach av $a bv $b { + set d [expr {$av - $bv}] + set sum [expr {$sum + $d * $d}] + } + expr {sqrt($sum)} +} + +fn drawHitTargetVectorMix {a b t} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av + ($bv - $av) * $t}] + } + return $out +} + +fn drawHitTargetQuadSize {quadLib quad} { + lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft + + set topWidth [drawHitTargetVectorDistance $topLeft $topRight] + set bottomWidth [drawHitTargetVectorDistance $bottomLeft $bottomRight] + set rightHeight [drawHitTargetVectorDistance $topRight $bottomRight] + set leftHeight [drawHitTargetVectorDistance $topLeft $bottomLeft] + + list [expr {($topWidth + $bottomWidth) / 2.0}] \ + [expr {($rightHeight + $leftHeight) / 2.0}] +} + +fn drawHitTargetQuadSurfacePoint {quadLib quad point} { + lassign [drawHitTargetQuadSize $quadLib $quad] width height + if {$width == 0.0 || $height == 0.0} { + error "draw/hit-targets: cannot map point through zero-sized quad" + } + + lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft + lassign $point x y + set tx [expr {$x / $width}] + set ty [expr {$y / $height}] + + set top [drawHitTargetVectorMix $topLeft $topRight $tx] + set bottom [drawHitTargetVectorMix $bottomLeft $bottomRight $tx] + drawHitTargetVectorMix $top $bottom $ty +} + +fn drawHitTargetQuadSurfaceRect {quadLib quad x y width height} { + $quadLib create [$quadLib space $quad] [list \ + [drawHitTargetQuadSurfacePoint $quadLib $quad [list $x $y]] \ + [drawHitTargetQuadSurfacePoint $quadLib $quad [list [expr {$x + $width}] $y]] \ + [drawHitTargetQuadSurfacePoint $quadLib $quad [list [expr {$x + $width}] [expr {$y + $height}]]] \ + [drawHitTargetQuadSurfacePoint $quadLib $quad [list $x [expr {$y + $height}]]]] +} + +fn drawHitTargetScalar {value extent} { + if {[string match *% $value]} { + set pct [string range $value 0 end-1] + if {![string is double -strict $pct]} { + error "draw/hit-targets: invalid percentage $value" + } + return [expr {double($pct) / 100.0 * $extent}] + } + drawHitTargetPhysicalLength $value +} + +fn drawHitTargetPoint {point parentWidth parentHeight} { + if {[llength $point] != 2} { + error "draw/hit-targets: expected a 2D point, got $point" + } + list [drawHitTargetScalar [lindex $point 0] $parentWidth] \ + [drawHitTargetScalar [lindex $point 1] $parentHeight] +} + +fn drawHitTargetOffset {offset parentWidth parentHeight} { + if {$offset eq "" || $offset eq "center"} { + return {0 0} + } + + if {[llength $offset] == 1} { + set token [lindex $offset 0] + switch -- $token { + right { return [list [expr {$parentWidth / 2.0}] 0] } + left { return [list [expr {-$parentWidth / 2.0}] 0] } + down { return [list 0 [expr {$parentHeight / 2.0}]] } + up { return [list 0 [expr {-$parentHeight / 2.0}]] } + default { + return [list [drawHitTargetScalar $token $parentWidth] 0] + } + } + } + + if {[llength $offset] == 2} { + set dir [lindex $offset 0] + set amount [lindex $offset 1] + switch -- $dir { + right { return [list [drawHitTargetScalar $amount $parentWidth] 0] } + left { + set value [drawHitTargetScalar $amount $parentWidth] + return [list [expr {-$value}] 0] + } + down { return [list 0 [drawHitTargetScalar $amount $parentHeight]] } + up { + set value [drawHitTargetScalar $amount $parentHeight] + return [list 0 [expr {-$value}]] + } + default { + return [drawHitTargetPoint $offset $parentWidth $parentHeight] + } + } + } + + error "draw/hit-targets: expected offset like {x y} or {right 50%}, got $offset" +} + +fn drawHitTargetRect {options parentWidth parentHeight} { + set defaultSize [dict getdef $options size 5] + set rectWidth [drawHitTargetScalar [dict getdef $options width $defaultSize] $parentWidth] + set rectHeight [drawHitTargetScalar \ + [dict getdef $options height [dict getdef $options width $defaultSize]] \ + $parentHeight] + + if {[dict exists $options topleft]} { + set topLeft [drawHitTargetPoint [dict get $options topleft] \ + $parentWidth $parentHeight] + lassign $topLeft x y + return [list $x $y $rectWidth $rectHeight] + } + + if {[dict exists $options top-left]} { + set topLeft [drawHitTargetPoint [dict get $options top-left] \ + $parentWidth $parentHeight] + lassign $topLeft x y + return [list $x $y $rectWidth $rectHeight] + } + + if {[dict exists $options center]} { + set center [drawHitTargetPoint [dict get $options center] \ + $parentWidth $parentHeight] + } elseif {[dict exists $options position]} { + set center [drawHitTargetPoint [dict get $options position] \ + $parentWidth $parentHeight] + } elseif {[dict exists $options x] || [dict exists $options y]} { + set center [list \ + [drawHitTargetScalar [dict getdef $options x 50%] $parentWidth] \ + [drawHitTargetScalar [dict getdef $options y 50%] $parentHeight]] + } else { + set center [list [expr {$parentWidth / 2.0}] [expr {$parentHeight / 2.0}]] + } + + if {[dict exists $options offset]} { + set center [drawHitTargetVectorAdd $center \ + [drawHitTargetOffset [dict get $options offset] $parentWidth $parentHeight]] + } + + lassign $center cx cy + list [expr {$cx - $rectWidth / 2.0}] \ + [expr {$cy - $rectHeight / 2.0}] \ + $rectWidth $rectHeight +} + +fn drawHitTargetDrawHighlight {disp surface width height options} { + set points [list \ + [drawHitTargetMeterPoint {0 0}] \ + [drawHitTargetMeterPoint [list $width 0]] \ + [drawHitTargetMeterPoint [list $width $height]] \ + [drawHitTargetMeterPoint [list 0 $height]] \ + [drawHitTargetMeterPoint {0 0}]] + + set color [dict getdef $options highlightColor [dict getdef $options color yellow]] + set thickness [dict getdef $options thickness [dict getdef $options outlineWidth 0.2]] + set layer [dict getdef $options layer 4] + + if {[drawHitTargetTruthy [dict getdef $options dashed false]]} { + set dashlength [dict getdef $options dashlength 1] + set dashoffset [dict getdef $options dashoffset 0] + Wish to draw a dashed line onto $disp in surface $surface with \ + points $points width $thickness color $color \ + dashlength $dashlength dashoffset $dashoffset layer $layer + } else { + Wish to draw a line onto $disp in surface $surface with \ + points $points width $thickness color $color layer $layer + } +} + +fn drawHitTargetClaim {quadLib parent parentQuad options} { + set target [drawHitTargetId $parent $options] + set name [drawHitTargetName $options] + set index [dict getdef $options index $name] + + lassign [drawHitTargetQuadSize $quadLib $parentQuad] parentWidth parentHeight + lassign [drawHitTargetRect $options $parentWidth $parentHeight] x y width height + + set targetQuad [drawHitTargetQuadSurfaceRect $quadLib $parentQuad $x $y $width $height] + Claim -keep 50ms $target has quad $targetQuad + Claim -keep 50ms $parent has child surface $target \ + with name $name index $index x $x y $y width $width height $height + Claim -keep 50ms $parent has hit target $target \ + with name $name index $index x $x y $y width $width height $height + Claim -keep 50ms $target is child surface of $parent \ + with name $name index $index x $x y $y width $width height $height + Claim -keep 50ms $target is hit target of $parent \ + with name $name index $index x $x y $y width $width height $height + + if {[drawHitTargetTruthy [dict getdef $options highlight false]]} { + When $target has physical drawing surface /surface/ with width /surfaceWidth/ height /surfaceHeight/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ { + drawHitTargetDrawHighlight $disp $surface $surfaceWidth $surfaceHeight $options + } + } +} + +When the quad library is /quadLib/ &\ + /parent/ has quad /parentQuad/ &\ + /someone/ wishes /parent/ adds child surface with /...options/ { + drawHitTargetClaim $quadLib $parent $parentQuad $options +} + +When the quad library is /quadLib/ &\ + /parent/ has quad /parentQuad/ &\ + /someone/ wishes /parent/ adds hit target with /...options/ { + drawHitTargetClaim $quadLib $parent $parentQuad $options +} + +When /someone/ wishes /parent/ adds child surface /name/ with /...options/ { + Wish $parent adds child surface with name $name {*}$options +} + +When /someone/ wishes /parent/ adds hit target /name/ with /...options/ { + Wish $parent adds hit target with name $name {*}$options +} + +Claim $this has demo { + Wish $this adds hit target with name left-button width 4 height 3 offset {left 25%} \ + highlight true color yellow dashed true + Wish $this adds hit target with name right-button width 4 height 3 offset {right 25%} \ + highlight true color cyan + + When $this has hit target /target/ with name /name/ /...options/ &\ + /target/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ { + Wish to draw text onto $disp in surface $surface with \ + position [drawHitTargetMeterPoint [list [expr {$width / 2.0}] [expr {$height / 2.0}]]] \ + scale 0.35 anchor center color white text $name + } +} diff --git a/builtin-programs/draw/image.folk b/builtin-programs/draw/image.folk index c159c006..432fa013 100644 --- a/builtin-programs/draw/image.folk +++ b/builtin-programs/draw/image.folk @@ -35,6 +35,45 @@ When the gif library is /gifLib/ { When the collected results for {/loader/ is an image loader} are /loaders/ { + fn imageUrlCachePath {url} { + set cleanUrl [regsub {[?#].*$} $url ""] + set ext [file extension $cleanUrl] + set cachePath /tmp/[regsub -all {\W+} $url "_"] + if {$ext ne "" && ![string match "*$ext" $cachePath]} { + append cachePath $ext + } + return $cachePath + } + + fn imageUrlLocalFallback {url} { + if {[regexp {^https?://folk[.]computer/_media/logo[.]png([?#].*)?$} $url]} { + set path [file join [pwd] assets logo.png] + if {[file exists $path]} { return $path } + } + return "" + } + + fn imageDownloadUrl {url} { + set path [imageUrlCachePath $url] + if {[file exists $path] && [file size $path] > 0} { + return $path + } + + file delete -force $path + set tmp "$path.[pid].tmp" + try { + exec curl -fsSL --connect-timeout 10 --retry 2 -o $tmp $url + if {![file exists $tmp] || [file size $tmp] == 0} { + error "Downloaded empty image from $url" + } + file rename -force $tmp $path + } on error {e opts} { + file delete -force $tmp + return -options $opts $e + } + return $path + } + # Pass coerceToImage = 0 if the caller is willing to handle a Gif # object, not just a normal Image. fn loadImage {im {coerceToImage 1}} { @@ -45,9 +84,11 @@ When the collected results for {/loader/ is an image loader} are /loaders/ { set impath $im if {[string match "http*://*" $impath]} { - set im /tmp/[regsub -all {\W+} $impath "_"] - if {![file exists $im]} { - exec curl -s -L -o$im $impath + set fallback [imageUrlLocalFallback $impath] + if {$fallback ne ""} { + set im $fallback + } else { + set im [imageDownloadUrl $impath] } } set path [expr {[string index $im 0] eq "/" ? diff --git a/builtin-programs/draw/line.folk b/builtin-programs/draw/line.folk index 96215309..673f412e 100644 --- a/builtin-programs/draw/line.folk +++ b/builtin-programs/draw/line.folk @@ -1,18 +1,23 @@ Wish the GPU compiles pipeline "line" { {vec2 viewport mat3 surfaceToClip - vec2 from vec2 to float thickness vec4 color} { + vec2 from vec2 to float thickness vec4 color float capFrom float capTo} { + vec2 dir = normalize(to - from); - vec2 perp = vec2(-dir.y, dir.x) * thickness/2.0; + vec2 perp = vec2(-dir.y, dir.x) * (thickness / 2.0); + + // Push the quad outward so the rounded caps don't get clipped by the geometry bounds + vec2 ext = dir * (thickness / 2.0); vec2 vertices[6] = vec2[6]( - from + perp, - from - perp, - to - perp, + (from - ext) + perp, + (from - ext) - perp, + (to + ext) - perp, - from + perp, - to - perp, - to + perp + (from - ext) + perp, + (to + ext) - perp, + (to + ext) + perp ); + vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); return vec4(v.xy/v.z, 0.0, 1.0); } { @@ -20,12 +25,19 @@ Wish the GPU compiles pipeline "line" { vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); surfaceXy /= surfaceXy.z; - float l = length(to - from); - vec2 d = (to - from) / l; - vec2 q = (surfaceXy.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); + vec2 pa = surfaceXy.xy - from; + vec2 ba = to - from; + + // Calculate where the pixel projects along the line segment + float h_unclamped = dot(pa, ba) / dot(ba, ba); + + // Dynamically slice off the rounded ends based on our Tcl flags + if (capFrom > 0.5 && h_unclamped < 0.0) return vec4(0.0); + if (capTo > 0.5 && h_unclamped > 1.0) return vec4(0.0); + + // Clamp the remainder to calculate the capsule distance + float h = clamp(h_unclamped, 0.0, 1.0); + float dist = length(pa - ba * h) - (thickness / 2.0); return (dist < 0.0) ? color : vec4(0.0); } @@ -44,10 +56,18 @@ When the color map is /colorMap/ &\ set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] set instances [list] - for {set i 0} {$i < [llength $points] - 1} {incr i} { + set numPoints [llength $points] + + for {set i 0} {$i < $numPoints - 1} {incr i} { set from [lindex $points $i] set to [lindex $points [+ $i 1]] - lappend instances [list $wiResolution $surfaceToClip $from $to $width $color] + + # 1.0 = flat + # 0.0 = round + set capFrom [expr {$i == 0 ? 1.0 : 0.0}] + set capTo [expr {$i == ($numPoints - 2) ? 1.0 : 0.0}] + + lappend instances [list $wiResolution $surfaceToClip $from $to $width $color $capFrom $capTo] } Wish the GPU draws pipeline "line" onto canvas $id \ diff --git a/builtin-programs/draw/shapes.folk b/builtin-programs/draw/shapes.folk new file mode 100644 index 00000000..7c5a5b41 --- /dev/null +++ b/builtin-programs/draw/shapes.folk @@ -0,0 +1,341 @@ +set drawShapeSides [dict create \ + triangle 3 square 4 pentagon 5 hexagon 6 septagon 7 octagon 8 nonagon 9] + +fn drawShapeTruthy {value} { + expr {$value in {1 true yes on}} +} + +fn drawShapeCanonical {shape options} { + if {[dict exists $options type]} { + set shape [dict get $options type] + } + if {[dict exists $options shape]} { + set shape [dict get $options shape] + } + switch -- $shape { + rectangle - box { return rect } + default { return $shape } + } +} + +fn drawShapeScalar {value extent} { + if {[string match *% $value]} { + set pct [string range $value 0 end-1] + return [expr {double($pct) / 100.0 * $extent}] + } + return $value +} + +fn drawShapePageCenter {geom} { + list [expr {[dict get $geom width] / 2.0}] \ + [expr {[dict get $geom height] / 2.0}] +} + +fn drawShapePoint {point geom} { + if {$point eq "" || $point eq "center"} { + return [drawShapePageCenter $geom] + } + if {[llength $point] != 2} { + error "draw/shapes: expected a 2D point, got $point" + } + list [drawShapeScalar [lindex $point 0] [dict get $geom width]] \ + [drawShapeScalar [lindex $point 1] [dict get $geom height]] +} + +fn drawShapeOffset {offset geom} { + if {$offset eq "" || $offset eq "center"} { + return {0 0} + } + + set width [dict get $geom width] + set height [dict get $geom height] + + if {[llength $offset] == 1} { + set token [lindex $offset 0] + switch -- $token { + right { return [list [expr {$width / 2.0}] 0] } + left { return [list [expr {-$width / 2.0}] 0] } + down { return [list 0 [expr {$height / 2.0}]] } + up { return [list 0 [expr {-$height / 2.0}]] } + default { + return [list [drawShapeScalar $token $width] 0] + } + } + } + + if {[llength $offset] == 2} { + set dir [lindex $offset 0] + set amount [lindex $offset 1] + switch -- $dir { + right { return [list [drawShapeScalar $amount $width] 0] } + left { + set value [drawShapeScalar $amount $width] + return [list [expr {-$value}] 0] + } + down { return [list 0 [drawShapeScalar $amount $height]] } + up { + set value [drawShapeScalar $amount $height] + return [list 0 [expr {-$value}]] + } + default { + return [list [drawShapeScalar $dir $width] \ + [drawShapeScalar $amount $height]] + } + } + } + + error "draw/shapes: expected offset like {x y} or {right 50%}, got $offset" +} + +fn drawShapePosition {options geom} { + if {[dict exists $options position]} { + return [drawShapePoint [dict get $options position] $geom] + } + if {[dict exists $options center]} { + return [drawShapePoint [dict get $options center] $geom] + } + if {[dict exists $options x] || [dict exists $options y]} { + set x [drawShapeScalar [dict getdef $options x 50%] [dict get $geom width]] + set y [drawShapeScalar [dict getdef $options y 50%] [dict get $geom height]] + return [list $x $y] + } + + set pos [drawShapePageCenter $geom] + if {[dict exists $options offset]} { + set pos [vec2 add $pos [drawShapeOffset [dict get $options offset] $geom]] + } + return $pos +} + +fn drawShapeRadians {options} { + dict getdef $options radians [dict getdef $options angle 0] +} + +fn drawShapeRadius {options default} { + if {[dict exists $options diameter]} { + return [expr {[dict get $options diameter] / 2.0}] + } + dict getdef $options radius $default +} + +fn drawShapeRegularPolygon {center radius sides radians} { + lassign $center cx cy + set points [list] + for {set i 0} {$i < $sides} {incr i} { + set theta [expr {$radians + $i * $::TAU / $sides - $::PI / 2.0}] + lappend points [list [expr {$cx + $radius * cos($theta)}] \ + [expr {$cy + $radius * sin($theta)}]] + } + return $points +} + +fn drawShapeRectPoints {center width height radians} { + set hw [expr {$width / 2.0}] + set hh [expr {$height / 2.0}] + set points [list \ + [list [expr {-$hw}] [expr {-$hh}]] \ + [list $hw [expr {-$hh}]] \ + [list $hw $hh] \ + [list [expr {-$hw}] $hh]] + lmap point $points { + vec2 add $center [vec2 rotate $point $radians] + } +} + +fn drawShapePathPoints {points geom options} { + set radians [drawShapeRadians $options] + set origin [dict getdef $options origin center] + set absolute [expr {$origin in {absolute local topleft top-left}}] + if {$absolute} { + set base {0 0} + } else { + set base [drawShapePosition $options $geom] + } + + set transformed [list] + foreach point $points { + if {$absolute} { + set point [drawShapePoint $point $geom] + } else { + set point [drawShapeOffset $point $geom] + } + lappend transformed [vec2 add $base [vec2 rotate $point $radians]] + } + return $transformed +} + +fn process_offset {offset regionOrGeom} { + if {[catch { + dict create width [dict get $regionOrGeom width] height [dict get $regionOrGeom height] + } geom]} { + set geom [dict create width [region width $regionOrGeom] height [region height $regionOrGeom]] + } + drawShapeOffset $offset $geom +} + +When /someone/ wishes /p/ draws a /shape/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + set shape [drawShapeCanonical $shape $options] + set center [drawShapePosition $options $geom] + set color [dict getdef $options color white] + set filled [drawShapeTruthy [dict getdef $options filled false]] + set thickness [dict getdef $options thickness 0.002] + set layer [dict getdef $options layer 1] + set radians [drawShapeRadians $options] + + if {$shape eq "circle"} { + set radius [drawShapeRadius $options 0.02] + Wish to draw a circle onto $p with \ + center $center radius $radius thickness $thickness \ + color $color filled $filled layer $layer + return + } + + if {$shape eq "rect"} { + set radius [drawShapeRadius $options 0.02] + set size [dict getdef $options size [expr {$radius * 2.0}]] + set rectWidth [dict getdef $options width $size] + set rectHeight [dict getdef $options height [dict getdef $options width $size]] + set points [drawShapeRectPoints $center $rectWidth $rectHeight $radians] + } else { + if {[dict exists $options sides]} { + set sides [dict get $options sides] + } elseif {[dict exists $drawShapeSides $shape]} { + set sides [dict get $drawShapeSides $shape] + } else { + error "draw/shapes: unknown shape $shape" + } + set radius [drawShapeRadius $options 0.02] + set points [drawShapeRegularPolygon $center $radius $sides $radians] + } + + if {$filled} { + Wish to draw a polygon onto $p with points $points color $color layer $layer + } else { + lappend points [lindex $points 0] + Wish to draw a line onto $p with \ + points $points width $thickness color $color layer $layer + } +} + +When /someone/ wishes /p/ draws a /shape/ { + Wish $p draws a $shape with color white filled true +} + +When /someone/ wishes /p/ draws an /shape/ { + Wish $p draws a $shape +} + +When /someone/ wishes /p/ draws an /shape/ with /...options/ { + Wish $p draws a $shape with {*}$options +} + +When /someone/ wishes /p/ draws a rect with width /width/ height /height/ { + Wish $p draws a rect with width $width height $height +} + +When /someone/ wishes /p/ draws a /shape/ with radius /radius/ { + Wish $p draws a $shape with radius $radius +} + +When /someone/ wishes /p/ draws text /text/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + set position [drawShapePosition $options $geom] + set color [dict getdef $options color white] + set scale [dict getdef $options scale 0.01] + set layer [dict getdef $options layer 0] + set anchor [dict getdef $options anchor center] + set font [dict getdef $options font "PTSans-Regular"] + set radians [drawShapeRadians $options] + + Wish to draw text onto $p with \ + position $position scale $scale text $text \ + color $color radians $radians anchor $anchor font $font layer $layer +} + +When /someone/ wishes /p/ draws text /text/ { + Wish $p draws text $text with color white +} + +When /someone/ wishes /p/ draws a polyline /points/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + set points [drawShapePathPoints $points $geom $options] + set color [dict getdef $options color white] + set width [dict getdef $options width [dict getdef $options thickness 0.002]] + set layer [dict getdef $options layer 1] + set dashed [drawShapeTruthy [dict getdef $options dashed false]] + + if {$dashed} { + set dashlength [dict getdef $options dashlength 0.01] + set dashoffset [dict getdef $options dashoffset 0] + Wish to draw a dashed line onto $p with \ + points $points width $width color $color \ + dashlength $dashlength dashoffset $dashoffset layer $layer + } else { + Wish to draw a line onto $p with \ + points $points width $width color $color layer $layer + } +} + +When /someone/ wishes /p/ draws points /points/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + set points [drawShapePathPoints $points $geom $options] + set radius [drawShapeRadius $options 0.003] + set thickness [dict getdef $options thickness 0.001] + set color [dict getdef $options color white] + set filled [drawShapeTruthy [dict getdef $options filled true]] + set layer [dict getdef $options layer 1] + + foreach point $points { + Wish to draw a circle onto $p with \ + center $point radius $radius thickness $thickness \ + color $color filled $filled layer $layer + } +} + +When /someone/ wishes /p/ draws a set of points /points/ with /...options/ { + Wish $p draws points $points with {*}$options +} + +Claim $this has demo { + Wish $this draws a circle with radius 0.018 color white filled true + + set baseX -0.055 + set baseY -0.035 + set dx 0.037 + set dy 0.03 + + Wish $this draws text "triangle" with color skyblue offset [list $baseX [expr {$baseY - 0.018}]] scale 0.004 + Wish $this draws text "square" with color green offset [list [expr {$baseX + $dx}] [expr {$baseY - 0.018}]] scale 0.004 + Wish $this draws text "pentagon" with color gold offset [list [expr {$baseX + $dx * 2}] [expr {$baseY - 0.018}]] scale 0.004 + Wish $this draws text "rect" with color cyan offset [list [expr {$baseX + $dx * 3}] [expr {$baseY - 0.018}]] scale 0.004 + + Wish $this draws a triangle with color skyblue radius 0.012 thickness 0.001 offset [list $baseX $baseY] + Wish $this draws a square with color green radius 0.012 thickness 0.0015 radians [expr {$::PI / 4.0}] offset [list [expr {$baseX + $dx}] $baseY] + Wish $this draws a pentagon with color gold radius 0.012 filled true offset [list [expr {$baseX + $dx * 2}] $baseY] + Wish $this draws a rect with width 0.026 height 0.014 color cyan radians 0.4 offset [list [expr {$baseX + $dx * 3}] $baseY] + + Wish $this draws a polyline [list {-0.055 0.01} {-0.035 0.025} {-0.015 0.008} {0.005 0.025}] \ + with color magenta width 0.0015 + Wish $this draws a polyline [list {0.02 0.012} {0.04 0.025} {0.06 0.012}] \ + with color orange width 0.001 dashed true dashlength 0.006 + Wish $this draws a set of points [list {-0.052 0.045} {-0.038 0.047} {-0.024 0.043} {-0.010 0.047}] \ + with color palegoldenrod radius 0.0025 + + When $this has resolved geometry /geom/ & the clock time is /t/ { + set x [expr {sin($t) * 0.028}] + set y [expr {cos($t * 1.5) * 0.018}] + Wish $this draws a circle with \ + radius 0.004 color palegoldenrod filled true offset [list $x $y] layer 4 + } + + When $this has resolved geometry /geom/ & the clock time is /t/ { + set filled [expr {round($t * 2) % 2 == 0}] + Wish $this draws a square with \ + radius 0.014 color white filled $filled offset {0.05 0.045} + Wish $this draws text $filled with \ + offset {0.05 0.045} scale 0.005 color red layer 5 + } + + Wish $this is outlined white +} diff --git a/builtin-programs/draw/spaces.folk b/builtin-programs/draw/spaces.folk new file mode 100644 index 00000000..6f47a2c2 --- /dev/null +++ b/builtin-programs/draw/spaces.folk @@ -0,0 +1,641 @@ +fn drawSpacePhysicalLength {value} { + if {[llength $value] != 1} { + error "draw/spaces: expected a scalar physical length, got $value" + } + + set unit "" + set amount $value + foreach suffix {mm cm m} { + if {[string match *$suffix $value]} { + set unit $suffix + set amount [string range $value 0 end-[string length $suffix]] + break + } + } + + if {![string is double -strict $amount]} { + error "draw/spaces: invalid physical length $value" + } + + switch -- $unit { + "" - cm { return [expr {double($amount) * 0.01}] } + mm { return [expr {double($amount) * 0.001}] } + m { return [expr {double($amount)}] } + default { error "draw/spaces: invalid physical unit $unit" } + } +} + +fn drawSpacePhysicalPoint {point} { + if {[llength $point] != 2} { + error "draw/spaces: expected a 2D physical point, got $point" + } + list [drawSpacePhysicalLength [lindex $point 0]] \ + [drawSpacePhysicalLength [lindex $point 1]] +} + +fn drawSpacePhysicalPoints {points} { + lmap point $points { + drawSpacePhysicalPoint $point + } +} + +fn drawSpaceSetLength {options key} { + if {[dict exists $options $key]} { + dict set options $key [drawSpacePhysicalLength [dict get $options $key]] + } + return $options +} + +fn drawSpaceSetPoint {options key} { + if {[dict exists $options $key]} { + set point [dict get $options $key] + if {$point ne ""} { + dict set options $key [drawSpacePhysicalPoint $point] + } + } + return $options +} + +fn drawSpaceSetPoints {options key} { + if {[dict exists $options $key]} { + dict set options $key [drawSpacePhysicalPoints [dict get $options $key]] + } + return $options +} + +fn drawSpaceNormalizeOptions {shape options} { + switch -- $shape { + line { + set options [drawSpaceSetPoints $options points] + set options [drawSpaceSetLength $options width] + } + dashed-line { + set options [drawSpaceSetPoints $options points] + set options [drawSpaceSetLength $options width] + set options [drawSpaceSetLength $options dashlength] + set options [drawSpaceSetLength $options dashoffset] + } + circle { + set options [drawSpaceSetPoint $options center] + set options [drawSpaceSetLength $options x] + set options [drawSpaceSetLength $options y] + set options [drawSpaceSetLength $options radius] + set options [drawSpaceSetLength $options thickness] + } + arc { + set options [drawSpaceSetPoint $options center] + set options [drawSpaceSetLength $options x] + set options [drawSpaceSetLength $options y] + set options [drawSpaceSetLength $options radius] + set options [drawSpaceSetLength $options thickness] + } + curve { + foreach key {p0 p1 p2 p3} { + set options [drawSpaceSetPoint $options $key] + } + set options [drawSpaceSetLength $options thickness] + } + text { + set options [drawSpaceSetPoint $options position] + set options [drawSpaceSetLength $options x] + set options [drawSpaceSetLength $options y] + set options [drawSpaceSetLength $options scale] + } + image { + set options [drawSpaceSetPoint $options position] + set options [drawSpaceSetLength $options x] + set options [drawSpaceSetLength $options y] + set options [drawSpaceSetLength $options width] + set options [drawSpaceSetLength $options height] + } + triangle { + foreach key {p0 p1 p2} { + set options [drawSpaceSetPoint $options $key] + } + } + quad { + foreach key {p0 p1 p2 p3} { + set options [drawSpaceSetPoint $options $key] + } + } + polygon { + set options [drawSpaceSetPoints $options points] + } + default { + error "draw/spaces: unknown primitive $shape" + } + } + return $options +} + +fn drawSpaceSurfaceTarget {target surface} { + list $target surface $surface +} + +fn drawSpaceVectorAdd {a b} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av + $bv}] + } + return $out +} + +fn drawSpaceVectorSub {a b} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av - $bv}] + } + return $out +} + +fn drawSpaceVectorScale {v s} { + lmap x $v { + expr {$x * $s} + } +} + +fn drawSpaceVectorDistance {a b} { + set sum 0.0 + foreach av $a bv $b { + set d [expr {$av - $bv}] + set sum [expr {$sum + $d * $d}] + } + expr {sqrt($sum)} +} + +fn drawSpaceDistance {a b} { + drawSpaceVectorDistance $a $b +} + +fn drawSpaceVectorUnit {v} { + set zero [lmap _ $v { expr {0.0} }] + set n [drawSpaceVectorDistance $v $zero] + if {$n == 0.0} { return "" } + drawSpaceVectorScale $v [expr {1.0 / $n}] +} + +fn drawSpaceVectorAverage {points} { + set first [lindex $points 0] + set sum [lmap _ $first { expr {0.0} }] + foreach point $points { + set sum [drawSpaceVectorAdd $sum $point] + } + drawSpaceVectorScale $sum [expr {1.0 / [llength $points]}] +} + +fn drawSpaceVectorMidpoint {a b} { + drawSpaceVectorScale [drawSpaceVectorAdd $a $b] 0.5 +} + +fn drawSpaceVectorMix {a b t} { + drawSpaceVectorAdd $a \ + [drawSpaceVectorScale [drawSpaceVectorSub $b $a] $t] +} + +fn drawSpaceQuadPoint {quadLib quad selector} { + lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft + + switch -- [string tolower $selector] { + centroid - center { + return [drawSpaceVectorAverage [list $topLeft $topRight $bottomRight $bottomLeft]] + } + top { + return [drawSpaceVectorMidpoint $topLeft $topRight] + } + right { + return [drawSpaceVectorMidpoint $topRight $bottomRight] + } + bottom { + return [drawSpaceVectorMidpoint $bottomLeft $bottomRight] + } + left { + return [drawSpaceVectorMidpoint $topLeft $bottomLeft] + } + topleft - top-left { + return $topLeft + } + topright - top-right { + return $topRight + } + bottomright - bottom-right { + return $bottomRight + } + bottomleft - bottom-left { + return $bottomLeft + } + default { + error "draw/spaces: unknown quad point selector $selector" + } + } +} + +fn drawSpaceQuadSize {quadLib quad} { + lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft + + set topWidth [drawSpaceVectorDistance $topLeft $topRight] + set bottomWidth [drawSpaceVectorDistance $bottomLeft $bottomRight] + set rightHeight [drawSpaceVectorDistance $topRight $bottomRight] + set leftHeight [drawSpaceVectorDistance $topLeft $bottomLeft] + + list [expr {($topWidth + $bottomWidth) / 2.0}] \ + [expr {($rightHeight + $leftHeight) / 2.0}] +} + +fn drawSpaceQuadSurfacePoint {quadLib quad point} { + lassign [drawSpaceQuadSize $quadLib $quad] width height + if {$width == 0.0 || $height == 0.0} { + error "draw/spaces: cannot map point through zero-sized quad" + } + + lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft + lassign $point x y + set tx [expr {$x / $width}] + set ty [expr {$y / $height}] + + set top [drawSpaceVectorMix $topLeft $topRight $tx] + set bottom [drawSpaceVectorMix $bottomLeft $bottomRight $tx] + drawSpaceVectorMix $top $bottom $ty +} + +fn drawSpaceQuadSurfaceRect {quadLib quad x y width height} { + $quadLib create [$quadLib space $quad] [list \ + [drawSpaceQuadSurfacePoint $quadLib $quad [list $x $y]] \ + [drawSpaceQuadSurfacePoint $quadLib $quad [list [expr {$x + $width}] $y]] \ + [drawSpaceQuadSurfacePoint $quadLib $quad [list [expr {$x + $width}] [expr {$y + $height}]]] \ + [drawSpaceQuadSurfacePoint $quadLib $quad [list $x [expr {$y + $height}]]]] +} + +fn drawSpacePerpendicularBetween {from to} { + set dx [expr {[lindex $to 0] - [lindex $from 0]}] + set dy [expr {[lindex $to 1] - [lindex $from 1]}] + set perp [list [expr {-$dy}] $dx] + if {[llength $from] > 2} { + lappend perp 0.0 + } + + set unit [drawSpaceVectorUnit $perp] + if {$unit ne ""} { return $unit } + + set fallback {0.0 1.0} + if {[llength $from] > 2} { + lappend fallback 0.0 + } + return $fallback +} + +fn drawSpaceSurfaceQuadBetween {quadLib space from to height} { + set halfHeight [expr {$height / 2.0}] + set perp [drawSpaceVectorScale [drawSpacePerpendicularBetween $from $to] $halfHeight] + + set topLeft [drawSpaceVectorSub $from $perp] + set topRight [drawSpaceVectorSub $to $perp] + set bottomRight [drawSpaceVectorAdd $to $perp] + set bottomLeft [drawSpaceVectorAdd $from $perp] + + $quadLib create $space [list $topLeft $topRight $bottomRight $bottomLeft] +} + +fn drawSpaceDisplayPixelToClip {displayWidth displayHeight point} { + lassign $point x y + list [expr {2.0 * $x / $displayWidth - 1.0}] \ + [expr {2.0 * $y / $displayHeight - 1.0}] +} + +fn drawSpaceHomography {pointPairs} { + package require linalg + namespace import ::math::linearalgebra::solvePGauss + + set A [list] + set b [list] + foreach pair $pointPairs { + lassign $pair x y u v + lappend A [list $x $y 1 0 0 0 [expr {-$u * $x}] [expr {-$u * $y}]] + lappend b $u + lappend A [list 0 0 0 $x $y 1 [expr {-$v * $x}] [expr {-$v * $y}]] + lappend b $v + } + + set h [solvePGauss $A $b] + list [list [lindex $h 0] [lindex $h 1] [lindex $h 2]] \ + [list [lindex $h 3] [lindex $h 4] [lindex $h 5]] \ + [list [lindex $h 6] [lindex $h 7] 1.0] +} + +fn drawSpaceApplyHomography {H point} { + lassign $point x y + lassign [lindex $H 0] h00 h01 h02 + lassign [lindex $H 1] h10 h11 h12 + lassign [lindex $H 2] h20 h21 h22 + set hx [expr {$h00 * $x + $h01 * $y + $h02}] + set hy [expr {$h10 * $x + $h11 * $y + $h12}] + set hw [expr {$h20 * $x + $h21 * $y + $h22}] + list [expr {$hx / $hw}] [expr {$hy / $hw}] +} + +fn drawSpacePointInsidePolygon {point polygon} { + if {[llength $polygon] < 3} { return false } + + lassign $point x y + set inside false + set j [expr {[llength $polygon] - 1}] + for {set i 0} {$i < [llength $polygon]} {incr i} { + lassign [lindex $polygon $i] xi yi + lassign [lindex $polygon $j] xj yj + if {(($yi > $y) != ($yj > $y)) && + ($x < (($xj - $xi) * ($y - $yi) / ($yj - $yi)) + $xi)} { + set inside [expr {!$inside}] + } + set j $i + } + return $inside +} + +fn drawSpaceMeterPoint {point} { + lmap value $point { + format "%sm" $value + } +} + +fn drawSpaceMakeLib {quadLib poseLib quadChange disp displayWidth displayHeight displayIntrinsics} { + set drawSpaceQuadPoint [fn drawSpaceQuadPoint] + set drawSpaceQuadSize [fn drawSpaceQuadSize] + set drawSpaceQuadSurfacePoint [fn drawSpaceQuadSurfacePoint] + set drawSpaceQuadSurfaceRect [fn drawSpaceQuadSurfaceRect] + set drawSpaceSurfaceQuadBetween [fn drawSpaceSurfaceQuadBetween] + set drawSpaceDisplayPixelToClip [fn drawSpaceDisplayPixelToClip] + set drawSpacePointInsidePolygon [fn drawSpacePointInsidePolygon] + + library create drawSpaceLib { + quadLib poseLib quadChange disp displayWidth displayHeight displayIntrinsics + drawSpaceQuadPoint drawSpaceQuadSize drawSpaceQuadSurfacePoint + drawSpaceQuadSurfaceRect drawSpaceSurfaceQuadBetween + drawSpaceDisplayPixelToClip drawSpacePointInsidePolygon + } { + proc display {} { + variable disp + return $disp + } + + proc displaySpace {} { + variable disp + return "display $disp" + } + + proc displaySize {} { + variable displayWidth + variable displayHeight + list $displayWidth $displayHeight + } + + proc quad {q} { + variable quadChange + fn quadChange + quadChange $q [displaySpace] + } + + proc quadVertices {q} { + variable quadLib + $quadLib vertices [quad $q] + } + + proc quadSpace {q} { + variable quadLib + $quadLib space [quad $q] + } + + proc quadPoint {q selector} { + variable quadLib + variable drawSpaceQuadPoint + fn drawSpaceQuadPoint + drawSpaceQuadPoint $quadLib [quad $q] $selector + } + + proc quadSize {q} { + variable quadLib + variable drawSpaceQuadSize + fn drawSpaceQuadSize + drawSpaceQuadSize $quadLib [quad $q] + } + + proc quadSurfacePoint {q point} { + variable quadLib + variable drawSpaceQuadSurfacePoint + fn drawSpaceQuadSurfacePoint + drawSpaceQuadSurfacePoint $quadLib [quad $q] $point + } + + proc quadSurfaceRect {q x y width height} { + variable quadLib + variable drawSpaceQuadSurfaceRect + fn drawSpaceQuadSurfaceRect + drawSpaceQuadSurfaceRect $quadLib [quad $q] $x $y $width $height + } + + proc surfaceQuadBetween {from to height} { + variable quadLib + variable drawSpaceSurfaceQuadBetween + fn drawSpaceSurfaceQuadBetween + drawSpaceSurfaceQuadBetween $quadLib [displaySpace] $from $to $height + } + + proc project {point} { + variable poseLib + variable displayIntrinsics + variable displayWidth + variable displayHeight + $poseLib project $displayIntrinsics $displayWidth $displayHeight $point + } + + proc clipPoint {point} { + variable drawSpaceDisplayPixelToClip + fn drawSpaceDisplayPixelToClip + lassign [displaySize] width height + drawSpaceDisplayPixelToClip $width $height [project $point] + } + + proc quadPixelVertices {q} { + lmap vertex [quadVertices $q] { + project $vertex + } + } + + proc containsPixelPoint {q point} { + variable drawSpacePointInsidePolygon + fn drawSpacePointInsidePolygon + drawSpacePointInsidePolygon $point [quadPixelVertices $q] + } + } +} + +When the quad library is /quadLib/ &\ + the pose library is /poseLib/ &\ + the quad changer is /quadChange/ &\ + display /disp/ has width /displayWidth/ height /displayHeight/ &\ + display /disp/ has intrinsics /displayIntrinsics/ { + set drawSpaceLib [drawSpaceMakeLib $quadLib $poseLib $quadChange $disp \ + $displayWidth $displayHeight $displayIntrinsics] + Claim the draw space library is $drawSpaceLib + Claim the draw space library for display $disp is $drawSpaceLib +} + +When /target/ has canvas /id/ with /...wiOptions/ &\ + /target/ has canvas projection for surface /surface/ /surfaceToClip/ { + set surfaceTarget [drawSpaceSurfaceTarget $target $surface] + Claim $surfaceTarget has canvas $id with {*}$wiOptions + Claim $surfaceTarget has canvas projection $surfaceToClip +} + +When /target/ has resolved geometry /geom/ &\ + /target/ has canvas projection /surfaceToClip/ { + Claim $target has canvas projection for surface local $surfaceToClip +} + +When the draw space library is /drawSpaceLib/ &\ + /thing/ has quad /quad/ { + set surface [list surface of $thing] + lassign [$drawSpaceLib quadVertices $quad] topLeft topRight bottomRight bottomLeft + lassign [$drawSpaceLib quadSize $quad] width height + + set surfacePoints [list \ + [list 0 0] \ + [list $width 0] \ + [list $width $height] \ + [list 0 $height]] + + set displayVertices [list $topLeft $topRight $bottomRight $bottomLeft] + set clipPoints [lmap vertex $displayVertices { + $drawSpaceLib clipPoint $vertex + }] + + set pointPairs [list] + foreach surfacePoint $surfacePoints clipPoint $clipPoints { + lassign $surfacePoint x y + lassign $clipPoint u v + lappend pointPairs [list $x $y $u $v] + } + + Claim $thing has physical drawing surface $surface \ + with width $width height $height space [$drawSpaceLib quadSpace $quad] + Claim [$drawSpaceLib display] has canvas projection for surface $surface \ + [drawSpaceHomography $pointPairs] +} + +fn drawSpaceWishPrimitive {article shape target surface options} { + set normalized [drawSpaceNormalizeOptions $shape $options] + Wish to draw $article $shape onto [drawSpaceSurfaceTarget $target $surface] \ + with {*}$normalized +} + +When /someone/ wishes to draw a line onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a line $target $surface $options +} +When /someone/ wishes to draw a line onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a line $target $surface $options +} +When /someone/ wishes to draw a line onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a line $target $surface $options +} + +fn drawSpaceWishDashedLine {target surface options} { + set normalized [drawSpaceNormalizeOptions dashed-line $options] + Wish to draw a dashed line onto [drawSpaceSurfaceTarget $target $surface] \ + with {*}$normalized +} + +When /someone/ wishes to draw a dashed line onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishDashedLine $target $surface $options +} +When /someone/ wishes to draw a dashed line onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishDashedLine $target $surface $options +} +When /someone/ wishes to draw a dashed line onto /target/ in space /surface/ with /...options/ { + drawSpaceWishDashedLine $target $surface $options +} + +When /someone/ wishes to draw a circle onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a circle $target $surface $options +} +When /someone/ wishes to draw a circle onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a circle $target $surface $options +} +When /someone/ wishes to draw a circle onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a circle $target $surface $options +} + +When /someone/ wishes to draw an arc onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive an arc $target $surface $options +} +When /someone/ wishes to draw an arc onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive an arc $target $surface $options +} +When /someone/ wishes to draw an arc onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive an arc $target $surface $options +} + +When /someone/ wishes to draw a curve onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a curve $target $surface $options +} +When /someone/ wishes to draw a curve onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a curve $target $surface $options +} +When /someone/ wishes to draw a curve onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a curve $target $surface $options +} + +When /someone/ wishes to draw an image onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive an image $target $surface $options +} +When /someone/ wishes to draw an image onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive an image $target $surface $options +} +When /someone/ wishes to draw an image onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive an image $target $surface $options +} + +When /someone/ wishes to draw a triangle onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a triangle $target $surface $options +} +When /someone/ wishes to draw a triangle onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a triangle $target $surface $options +} +When /someone/ wishes to draw a triangle onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a triangle $target $surface $options +} + +When /someone/ wishes to draw a quad onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a quad $target $surface $options +} +When /someone/ wishes to draw a quad onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a quad $target $surface $options +} +When /someone/ wishes to draw a quad onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a quad $target $surface $options +} + +When /someone/ wishes to draw a polygon onto /target/ in surface /surface/ with /...options/ { + drawSpaceWishPrimitive a polygon $target $surface $options +} +When /someone/ wishes to draw a polygon onto /target/ in physical surface /surface/ with /...options/ { + drawSpaceWishPrimitive a polygon $target $surface $options +} +When /someone/ wishes to draw a polygon onto /target/ in space /surface/ with /...options/ { + drawSpaceWishPrimitive a polygon $target $surface $options +} + +When /someone/ wishes to draw text onto /target/ in surface /surface/ with /...options/ { + set normalized [drawSpaceNormalizeOptions text $options] + Wish to draw text onto [drawSpaceSurfaceTarget $target $surface] \ + with {*}$normalized +} + +When /someone/ wishes to draw text onto /target/ in physical surface /surface/ with /...options/ { + set normalized [drawSpaceNormalizeOptions text $options] + Wish to draw text onto [drawSpaceSurfaceTarget $target $surface] \ + with {*}$normalized +} + +When /someone/ wishes to draw text onto /target/ in space /surface/ with /...options/ { + set normalized [drawSpaceNormalizeOptions text $options] + Wish to draw text onto [drawSpaceSurfaceTarget $target $surface] \ + with {*}$normalized +} diff --git a/builtin-programs/draw/text.folk b/builtin-programs/draw/text.folk index f9dfcf63..b3c75602 100644 --- a/builtin-programs/draw/text.folk +++ b/builtin-programs/draw/text.folk @@ -113,8 +113,10 @@ $cc proc textShape {Jim_Obj* viewport Jim_Obj* surfaceToClip ch = charOrFallback(font, ch); GlyphInfo* glyphInfo = &font->glyphInfos[ch]; if (ch != ' ') { - // Calculate the absolute glyph position. - float lineOffsetX = -(lineAnchorX * lineWidth) - blockOffsetX; + // Calculate the absolute glyph position. The block anchor places the + // text block relative to the requested position; the line anchor then + // aligns each individual line inside that block. + float lineOffsetX = lineAnchorX * (extent.x - lineWidth); // `lineOffsetY` doesn't exist, since it's already included in the `blockOffsetY` calculation. vec2f rotatedLineOffset = vec2f_rotate((vec2f) { lineOffsetX, 0 }, radians); vec2f combinedOffset = vec2f_add(blockStart, rotatedLineOffset); diff --git a/builtin-programs/editor.folk b/builtin-programs/editor.folk index 952b6b58..f4462902 100644 --- a/builtin-programs/editor.folk +++ b/builtin-programs/editor.folk @@ -17,12 +17,14 @@ When /k/ is a keyboard with /...opts/ &\ Claim $editor has quad [$quadLib move $q up 105%] } Claim $k has created editor $editor - Claim $k is typing into $editor + When /nobody/ claims $k has focused input target /anything/ { + Claim $k is typing into $editor + } } When /k/ is a keyboard with /...opts/ &\ - /nobody/ claims /k/ has created editor /any/ &\ /k/ points up at /editor/ & /editor/ is an editor with /...opts/ { + Claim $k has focused input target $editor Claim $k is typing into $editor } @@ -73,6 +75,7 @@ set editorLib [library create editorLib {margin defaults} { When /someone/ claims /editor/ is an editor { Claim $editor is an editor with {*}$defaults + Claim $editor accepts keyboard input } When /editor/ is an editor with /...options/ { diff --git a/builtin-programs/gpu/gpu.folk b/builtin-programs/gpu/gpu.folk index c762f176..830f8125 100644 --- a/builtin-programs/gpu/gpu.folk +++ b/builtin-programs/gpu/gpu.folk @@ -50,6 +50,11 @@ fn gpuInit {useGlfw} { } set macos [expr {$::tcl_platform(os) eq "darwin"}] + set enableValidation false + if {[info exists ::env(FOLK_VULKAN_VALIDATION)]} { + set validationSetting [string tolower $::env(FOLK_VULKAN_VALIDATION)] + set enableValidation [expr {$validationSetting ni {0 false no off ""}}] + } if {$macos} { $gpuc cflags -I/opt/homebrew/include -L/opt/homebrew/lib } @@ -145,11 +150,16 @@ fn gpuInit {useGlfw} { VkInstanceCreateInfo createInfo = {0}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + $[if {$enableValidation} {subst -nocommands { const char* validationLayers[] = { "VK_LAYER_KHRONOS_validation" }; createInfo.enabledLayerCount = sizeof(validationLayers)/sizeof(validationLayers[0]); createInfo.ppEnabledLayerNames = validationLayers; + }} else {subst -nocommands { + createInfo.enabledLayerCount = 0; + createInfo.ppEnabledLayerNames = NULL; + }}] $[if {$macos} {subst -nocommands { const char* enabledExtensions[] = { diff --git a/builtin-programs/gpu/textures.folk b/builtin-programs/gpu/textures.folk index e74e4074..cef1bee5 100644 --- a/builtin-programs/gpu/textures.folk +++ b/builtin-programs/gpu/textures.folk @@ -14,12 +14,11 @@ $gpuc code { #include "vk_mem_alloc.h" - void vmaInit(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, - PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr, - PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr, - PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties, - PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties); - VmaAllocator vmaGetAllocator(); + VmaAllocator vmaCreateFolkAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr, + PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr, + PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties, + PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties); } $gpuc include $gpuc include @@ -77,11 +76,27 @@ defineVulkanHandleType $gpuc VkDescriptorSet $gpuc code { VkDevice device; + VmaAllocator textureAllocator = VK_NULL_HANDLE; + static PFN_vkCreateFence textureVkCreateFence = NULL; + static PFN_vkResetFences textureVkResetFences = NULL; + + static void loadTextureFenceProcs(void) { + if (vkGetDeviceProcAddr == NULL) { + volkInitialize(); + volkLoadInstanceOnly(*instance_ptr()); + } + FOLK_ENSURE(device != VK_NULL_HANDLE); + + textureVkCreateFence = (PFN_vkCreateFence)vkGetDeviceProcAddr(device, "vkCreateFence"); + textureVkResetFences = (PFN_vkResetFences)vkGetDeviceProcAddr(device, "vkResetFences"); + } + static void initPlaceholderTexture(); } $gpuc typedef int GpuTextureHandle defineVulkanHandleType $gpuc VkImage +defineVulkanHandleType $gpuc VkBuffer defineVulkanHandleType $gpuc VkDeviceMemory defineVulkanHandleType $gpuc VkImageView defineVulkanHandleType $gpuc VkSampler @@ -178,12 +193,12 @@ $gpuc proc textureManagerInit {} void { $[vktry {vkAllocateDescriptorSets(device, &allocInfo, textureDescriptorSet_ptr())}] } - // Initialize VMA allocator - vmaInit(*instance_ptr(), *physicalDevice_ptr(), device, - vkGetInstanceProcAddr, - vkGetDeviceProcAddr, - vkGetPhysicalDeviceProperties, - vkGetPhysicalDeviceMemoryProperties); + // Initialize this texture library's VMA allocator. + textureAllocator = vmaCreateFolkAllocator(*instance_ptr(), *physicalDevice_ptr(), device, + vkGetInstanceProcAddr, + vkGetDeviceProcAddr, + vkGetPhysicalDeviceProperties, + vkGetPhysicalDeviceMemoryProperties); initPlaceholderTexture(); } @@ -224,7 +239,7 @@ $gpuc proc createBuffer {VkDeviceSize size VkBufferUsageFlags usage VkMemoryProp allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT; } - VkResult res = vmaCreateBuffer(vmaGetAllocator(), &bufferInfo, &allocInfo, buffer, allocation, NULL); + VkResult res = vmaCreateBuffer(textureAllocator, &bufferInfo, &allocInfo, buffer, allocation, NULL); if (res != VK_SUCCESS) { fprintf(stderr, "Failed to create buffer with VMA: %d\\n", res); exit(1); @@ -232,11 +247,25 @@ $gpuc proc createBuffer {VkDeviceSize size VkBufferUsageFlags usage VkMemoryProp #ifdef TRACY_ENABLE VmaAllocationInfo vmaInfo; - vmaGetAllocationInfo(vmaGetAllocator(), *allocation, &vmaInfo); + vmaGetAllocationInfo(textureAllocator, *allocation, &vmaInfo); TracyCAlloc(*allocation, vmaInfo.size); #endif } +$gpuc proc copyTextureStagingBufferToImage {VmaAllocation allocation Image im size_t size} void { + void* data; + vmaMapMemory(textureAllocator, allocation, &data); + memcpy(im.data, data, size); + vmaUnmapMemory(textureAllocator, allocation); +} + +$gpuc proc destroyTextureBuffer {VkBuffer buffer VmaAllocation allocation} void { +#ifdef TRACY_ENABLE + TracyCFree(allocation); +#endif + vmaDestroyBuffer(textureAllocator, buffer, allocation); +} + # Texture allocation: $gpuc code [csubst { void createImage(uint32_t width, uint32_t height, @@ -267,7 +296,7 @@ $gpuc code [csubst { allocInfo.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; } - VkResult res = vmaCreateImage(vmaGetAllocator(), &imageInfo, &allocInfo, image, allocation, NULL); + VkResult res = vmaCreateImage(textureAllocator, &imageInfo, &allocInfo, image, allocation, NULL); if (res != VK_SUCCESS) { fprintf(stderr, "Failed to create image with VMA: %d\\n", res); exit(1); @@ -275,7 +304,7 @@ $gpuc code [csubst { #ifdef TRACY_ENABLE VmaAllocationInfo vmaInfo; - vmaGetAllocationInfo(vmaGetAllocator(), *allocation, &vmaInfo); + vmaGetAllocationInfo(textureAllocator, *allocation, &vmaInfo); TracyCAlloc(*allocation, vmaInfo.size); #endif } @@ -311,12 +340,18 @@ $gpuc code { static __thread VkFence _fence = VK_NULL_HANDLE; } $gpuc proc getFence {} VkFence { + if (textureVkCreateFence == NULL || textureVkResetFences == NULL) { + loadTextureFenceProcs(); + } + FOLK_ENSURE(textureVkCreateFence != NULL); + FOLK_ENSURE(textureVkResetFences != NULL); + if (_fence == VK_NULL_HANDLE) { VkFenceCreateInfo fenceInfo = {0}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - $[vktry {vkCreateFence(device, &fenceInfo, NULL, &_fence)}] + $[vktry {textureVkCreateFence(device, &fenceInfo, NULL, &_fence)}] } else { - vkResetFences(device, 1, &_fence); + textureVkResetFences(device, 1, &_fence); } return _fence; } @@ -603,16 +638,19 @@ $gpuc code { TracyCZoneEnd(ctx); TracyCFree(slot->stagingBufferAllocation); #endif - vmaDestroyBuffer(vmaGetAllocator(), + vmaDestroyBuffer(textureAllocator, slot->stagingBuffer, slot->stagingBufferAllocation); slot->inUse = false; } if (slot->fence == VK_NULL_HANDLE) { + if (textureVkCreateFence == NULL || textureVkResetFences == NULL) { + loadTextureFenceProcs(); + } VkFenceCreateInfo fenceInfo = {0}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - vkCreateFence(device, &fenceInfo, NULL, &slot->fence); + textureVkCreateFence(device, &fenceInfo, NULL, &slot->fence); } else { - vkResetFences(device, 1, &slot->fence); + textureVkResetFences(device, 1, &slot->fence); } if (slot->cmdBuffer == VK_NULL_HANDLE) { VkCommandBufferAllocateInfo allocInfo = {0}; @@ -638,7 +676,7 @@ $gpuc proc copyImageToGpuTexture {Image im} GpuTextureHandle { // Copy im to stagingBuffer: { - void* data; vmaMapMemory(vmaGetAllocator(), upload->stagingBufferAllocation, &data); + void* data; vmaMapMemory(textureAllocator, upload->stagingBufferAllocation, &data); Image stagingIm = (Image) { .width = im.width, .height = im.height, .components = 4, @@ -646,7 +684,7 @@ $gpuc proc copyImageToGpuTexture {Image im} GpuTextureHandle { .data = data }; copyImageToRgba(im, stagingIm); - vmaUnmapMemory(vmaGetAllocator(), upload->stagingBufferAllocation); + vmaUnmapMemory(textureAllocator, upload->stagingBufferAllocation); } // Allocate a texture and texture block: @@ -783,7 +821,7 @@ $gpuc code { #ifdef TRACY_ENABLE TracyCFree(block->textureImageAllocation); #endif - vmaDestroyImage(vmaGetAllocator(), block->textureImage, block->textureImageAllocation); + vmaDestroyImage(textureAllocator, block->textureImage, block->textureImageAllocation); vkDestroySampler(device, block->textureSampler, NULL); vkDestroyImageView(device, block->textureImageView, NULL); diff --git a/builtin-programs/gpu/vma.folk b/builtin-programs/gpu/vma.folk index c8c416e6..7360b23c 100644 --- a/builtin-programs/gpu/vma.folk +++ b/builtin-programs/gpu/vma.folk @@ -23,17 +23,36 @@ defineVulkanHandleType $vmac VkPhysicalDevice defineVulkanHandleType $vmac VkDevice $vmac code { extern "C" { +VmaAllocator vmaCreateFolkAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr, + PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr, + PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties, + PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties); + void vmaInit(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr, PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr, PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties, PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties) { if (allocator != VK_NULL_HANDLE) { return; } + allocator = vmaCreateFolkAllocator(instance, physicalDevice, device, + vkGetInstanceProcAddr, + vkGetDeviceProcAddr, + vkGetPhysicalDeviceProperties, + vkGetPhysicalDeviceMemoryProperties); +} +VmaAllocator vmaCreateFolkAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device, + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr, + PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr, + PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties, + PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties) { volkInitialize(); volkLoadInstanceOnly(instance); volkLoadDevice(device); + VmaAllocator newAllocator = VK_NULL_HANDLE; + VmaAllocatorCreateInfo allocatorInfo = {0}; allocatorInfo.physicalDevice = physicalDevice; allocatorInfo.device = device; @@ -56,11 +75,13 @@ void vmaInit(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice devi allocatorInfo.pVulkanFunctions = &vulkanFunctions; - res = vmaCreateAllocator(&allocatorInfo, &allocator); + res = vmaCreateAllocator(&allocatorInfo, &newAllocator); if (res != VK_SUCCESS) { fprintf(stderr, "Failed to create VMA allocator: %d\\n", res); exit(1); } + + return newAllocator; } VmaAllocator vmaGetAllocator() { diff --git a/builtin-programs/group.folk b/builtin-programs/group.folk deleted file mode 100644 index 72394306..00000000 --- a/builtin-programs/group.folk +++ /dev/null @@ -1,42 +0,0 @@ -return -# FIXME: re-enable group.folk - -# load all programs -When group /group/ contains /...programs/ { - Wish tag $group is stabilized - foreach program $programs { - # HACK: claim 'tag' specifically so it doesn't run twice - Claim tag $program has a program - } -} - -# figure out the text to display below -When group /group/ contains /...programs/ &\ - the collected results for [list /someone/ wishes /program/ is titled /title/] are /results/ { - set programTitles [dict create] - - foreach result $results { - set programId [dict get $result program] - - if {[lsearch $programs $programId] != -1} { - dict set programTitles $programId [dict get $result title] - } - } - - set programTitleText "" - - foreach program $programs { - set title [dict_getdef $programTitles $program "(no title)"] - append programTitleText \n $program ": " $title - } - - Claim group $group has program titles $programTitleText -} - -# display said text -When group /group/ has program titles /programTitles/ &\ - /group/ has region /r/ { - set radians [region angle $r] - set pos [region topleft [region move $r down 40px right 15px]] - Wish to draw text with position $pos text $programTitles scale 0.7 radians $radians anchor topleft -} diff --git a/builtin-programs/intersect.folk b/builtin-programs/intersect.folk deleted file mode 100644 index 18704da5..00000000 --- a/builtin-programs/intersect.folk +++ /dev/null @@ -1,25 +0,0 @@ - -When /someone/ wishes /p/ has neighbors & /p/ has region /r/ & /p2/ has region /r2/ { - if {$p eq $p2} { return } - lassign [regionToBbox $r] bMinX bMinY bMaxX bMaxY - lassign [regionToBbox $r2] b2MinX b2MinY b2MaxX b2MaxY - - set hasIntersections [rectanglesOverlap [list $bMinX $bMinY] \ - [list $bMaxX $bMaxY]\ - [list $b2MinX $b2MinY]\ - [list $b2MaxX $b2MaxY]\ - false ] - #Display::stroke [list [list $bMinX $bMinY] {500 500}] 3 blue - #Display::stroke [list [list $bMaxX $bMaxY] {500 500}] 3 red - - if {$hasIntersections} { - Claim $p has neighbor $p2 - #Display::stroke [list [list $b2MinX $b2MinY] {500 500}] 3 red - #Display::stroke [list [list $b2MaxX $b2MaxY] {500 500}] 3 white - #Display::stroke [list [list $b2MinX $b2MinY] [list $b2MaxX $b2MaxY]] 10 blue - } -} - -When when /p/ has neighbor /n/ /lambda/ with environment /e/ { - Wish $p has neighbors -} diff --git a/builtin-programs/mask-tags.folk b/builtin-programs/mask-tags.folk index f3cabc79..509b1607 100644 --- a/builtin-programs/mask-tags.folk +++ b/builtin-programs/mask-tags.folk @@ -1,20 +1,21 @@ -When the quad library is /quadLib/ &\ - the pose library is /poseLib/ &\ - the quad changer is /quadChange/ &\ - display /proj/ has width /projWidth/ height /projHeight/ &\ - display /proj/ has intrinsics /projectorIntrinsics/ { - - fn quadChange - +When the quad library is /quadLib/ { When -atomically tag /id/ has quad /q/ { - set scaledQuad [$quadLib scale $q 2.25] + Claim -keep 50ms [list tag-mask $id] has quad [$quadLib scale $q 2.25] + } +} - lassign [lmap v [$quadLib vertices [quadChange $scaledQuad "display $proj"]] { - $poseLib project $projectorIntrinsics $projWidth $projHeight $v - }] p0 p1 p2 p3 +When display /proj/ has width /projWidth/ height /projHeight/ &\ + /mask/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /proj/ has canvas projection for surface /surface/ /surfaceToClip/ { + if {[lindex $mask 0] ne "tag-mask"} { return } - Wish to draw a quad onto $proj with \ - p0 $p0 p1 $p1 p2 $p2 p3 $p3 \ - color black layer 99 - } -} \ No newline at end of file + set widthM [format "%sm" $width] + set heightM [format "%sm" $height] + + Wish to draw a quad onto $proj in surface $surface with \ + p0 {0m 0m} \ + p1 [list $widthM 0m] \ + p2 [list $widthM $heightM] \ + p3 [list 0m $heightM] \ + color black layer 99 +} diff --git a/builtin-programs/points-at.folk b/builtin-programs/points-at.folk index 5de70d0d..740c92bb 100644 --- a/builtin-programs/points-at.folk +++ b/builtin-programs/points-at.folk @@ -1,3 +1,31 @@ +fn pointsAtMeterPoint {point} { + lmap value $point { + format "%sm" $value + } +} + +fn pointsAtVectorAdd {a b} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av + $bv}] + } + return $out +} + +fn pointsAtVectorSub {a b} { + set out [list] + foreach av $a bv $b { + lappend out [expr {$av - $bv}] + } + return $out +} + +fn pointsAtVectorScale {v s} { + lmap x $v { + expr {$x * $s} + } +} + When when /rect/ points /direction/ with length /l/ at /someone/ /lambda/ with environment /e/ { if {[string match "/*" $rect]} { return } Wish $rect points $direction with length $l @@ -8,98 +36,78 @@ When when /rect/ points /direction/ at /someone/ /lambda/ with environment /e/ { Wish $rect points $direction with length 1 } -When the quad library is /quadLib/ &\ - the pose library is /poseLib/ &\ - the quad changer is /quadChange/ &\ - display /disp/ has width /displayWidth/ height /displayHeight/ &\ - display /disp/ has intrinsics /displayIntrinsics/ &\ +When the draw space library is /drawSpaceLib/ &\ /someone/ wishes /rect/ points /direction/ with length /l/ { -When $rect has quad /quad/ { - - package require linalg - namespace import \ - ::math::linearalgebra::add \ - ::math::linearalgebra::sub \ - ::math::linearalgebra::scale + When $rect has quad /quad/ { - fn quadChange set scale $l + set disp [$drawSpaceLib display] - set quad [quadChange $quad "display $disp"] - lassign [$quadLib vertices $quad] topLeft topRight bottomRight bottomLeft - if {$direction eq "up"} { - set topCenter [scale 0.5 [add $topLeft $topRight]] - set bottomCenter [scale 0.5 [add $bottomLeft $bottomRight]] - set up [scale $scale [sub $topCenter $bottomCenter]] + lassign [$drawSpaceLib quadSize $quad] width height - set from $topCenter - set to [add $topCenter $up] + if {$direction eq "up"} { + set from [$drawSpaceLib quadPoint $quad top] + set opposite [$drawSpaceLib quadPoint $quad bottom] + set to [pointsAtVectorAdd $from \ + [pointsAtVectorScale [pointsAtVectorSub $from $opposite] $scale]] set color blue + set fromSurface [list [expr {$width / 2.0}] 0] + set toSurface [list [expr {$width / 2.0}] [expr {-$height * $scale}]] } elseif {$direction eq "left"} { - set leftCenter [scale 0.5 [add $topLeft $bottomLeft]] - set rightCenter [scale 0.5 [add $topRight $bottomRight]] - set left [scale $scale [sub $leftCenter $rightCenter]] - - set from $leftCenter - set to [add $leftCenter $left] + set from [$drawSpaceLib quadPoint $quad left] + set opposite [$drawSpaceLib quadPoint $quad right] + set to [pointsAtVectorAdd $from \ + [pointsAtVectorScale [pointsAtVectorSub $from $opposite] $scale]] set color gold + set fromSurface [list 0 [expr {$height / 2.0}]] + set toSurface [list [expr {-$width * $scale}] [expr {$height / 2.0}]] } elseif {$direction eq "right"} { - set leftCenter [scale 0.5 [add $topLeft $bottomLeft]] - set rightCenter [scale 0.5 [add $topRight $bottomRight]] - set right [scale $scale [sub $rightCenter $leftCenter]] - - set from $rightCenter - set to [add $rightCenter $right] + set from [$drawSpaceLib quadPoint $quad right] + set opposite [$drawSpaceLib quadPoint $quad left] + set to [pointsAtVectorAdd $from \ + [pointsAtVectorScale [pointsAtVectorSub $from $opposite] $scale]] set color red + set fromSurface [list $width [expr {$height / 2.0}]] + set toSurface [list [expr {$width * (1.0 + $scale)}] [expr {$height / 2.0}]] } elseif {$direction eq "down"} { - set topCenter [scale 0.5 [add $topLeft $topRight]] - set bottomCenter [scale 0.5 [add $bottomLeft $bottomRight]] - set down [scale $scale [sub $bottomCenter $topCenter]] - - set from $bottomCenter - set to [add $bottomCenter $down] + set from [$drawSpaceLib quadPoint $quad bottom] + set opposite [$drawSpaceLib quadPoint $quad top] + set to [pointsAtVectorAdd $from \ + [pointsAtVectorScale [pointsAtVectorSub $from $opposite] $scale]] set color white + set fromSurface [list [expr {$width / 2.0}] $height] + set toSurface [list [expr {$width / 2.0}] [expr {$height * (1.0 + $scale)}]] } else { error "points-at: Invalid direction $direction" } - # HACK: This implementation is sort of inelegant in that it - # happens entirely in screen-space, because we need to draw right - # to the screen right now, and we don't have a surface-to-clip for - # that. + set surface [list surface of $rect] - # Downproject the whisker to screen-space. - set from [$poseLib project $displayIntrinsics \ - $displayWidth $displayHeight \ - $from] - set to [$poseLib project $displayIntrinsics \ - $displayWidth $displayHeight \ - $to] + # The hit test still happens in display pixels, but drawing now + # stays in the rect's extended physical surface. + set toPixel [$drawSpaceLib project $to] When /target/ has quad /q2/ { if {$target eq $rect} { return } - set displayVertices [lmap v [$quadLib vertices [quadChange $q2 "display $disp"]] { - $poseLib project $displayIntrinsics \ - $displayWidth $displayHeight $v - }] - - if {[::math::geometry::pointInsidePolygon $to $displayVertices]} { + if {[$drawSpaceLib containsPixelPoint $q2 $toPixel]} { Claim -keep 50ms $rect points $direction at $target Claim -keep 50ms $rect points $direction with length $l at $target set color green Hold! -keep 16ms -key [list $rect pointer] { - Wish to draw a line onto $disp with \ - points [list $from $to] width 4 \ + Wish to draw a line onto $disp in surface $surface with \ + points [list [pointsAtMeterPoint $fromSurface] \ + [pointsAtMeterPoint $toSurface]] width 0.4 \ color $color - Wish to draw a circle onto $disp with \ - center $to radius 10 thickness 5 \ + Wish to draw a circle onto $disp in surface $surface with \ + center [pointsAtMeterPoint $toSurface] \ + radius 1 thickness 0.4 \ color $color filled true } } @@ -107,11 +115,13 @@ When $rect has quad /quad/ { When /nobody/ claims $rect points /anything/ at /anything/ { Hold! -keep 16ms -key [list $rect pointer] { - Wish to draw a line onto $disp with \ - points [list $from $to] width 4 \ + Wish to draw a line onto $disp in surface $surface with \ + points [list [pointsAtMeterPoint $fromSurface] \ + [pointsAtMeterPoint $toSurface]] width 0.4 \ color $color - Wish to draw a circle onto $disp with \ - center $to radius 10 thickness 5 \ + Wish to draw a circle onto $disp in surface $surface with \ + center [pointsAtMeterPoint $toSurface] \ + radius 1 thickness 0.4 \ color $color filled false } } diff --git a/builtin-programs/regions.folk b/builtin-programs/regions.folk deleted file mode 100644 index 945fcd20..00000000 --- a/builtin-programs/regions.folk +++ /dev/null @@ -1,8 +0,0 @@ -When when the distance between /p1/ and /p2/ is /distanceVar/ /body/ with environment /e/ & /p1/ has region /r1/ & /p2/ has region /r2/ { - Claim the distance between $p1 and $p2 is [region distance $r1 $r2] -} - -When /someone/ wishes region /r/ is /verbed/ /x/ { - Claim $r has region $r - Wish $r is $verbed $x -} diff --git a/builtin-programs/shapes.folk b/builtin-programs/shapes.folk deleted file mode 100644 index c67c7e43..00000000 --- a/builtin-programs/shapes.folk +++ /dev/null @@ -1,357 +0,0 @@ -set shapes [dict create triangle 3 square 4 pentagon 5 hexagon 6 septagon 7 octagon 8 nonagon 9] - -proc process_offset {offset region} { - if {![info exists region]} { - return $offset - } - - set w [region width $region] - set h [region height $region] - - if {[llength $offset] == 2 && - ![string match *%* $offset] && - ![string is alpha -strict [lindex $offset 0]]} { - return $offset - } - - # Handle simple percentage string: "50%" - if {[string match *%* $offset] && [llength $offset] == 1} { - set pct [expr {[string map {% ""} $offset] / 100.0}] - return [list [expr {$w * $pct}] 0] # Default to horizontal offset - } - - # Handle directional strings: "right", "left", "up", "down" - if {$offset eq "right"} { - return [list [expr {$w * 0.5}] 0] - } elseif {$offset eq "left"} { - return [list [expr {-$w * 0.5}] 0] - } elseif {$offset eq "up"} { - return [list 0 [expr {-$h * 0.5}]] - } elseif {$offset eq "down"} { - return [list 0 [expr {$h * 0.5}]] - } - - # Handle directional percentage: "right 50%", "left 25%", etc. - if {[llength $offset] == 2 && [string is alpha -strict [lindex $offset 0]]} { - set direction [lindex $offset 0] - set amount [lindex $offset 1] - - if {[string match *%* $amount]} { - set pct [expr {[string map {% ""} $amount] / 100.0}] - - switch $direction { - "right" { return [list [expr {$w * $pct}] 0] } - "left" { return [list [expr {-$w * $pct}] 0] } - "up" { return [list 0 [expr {-$h * $pct}]] } - "down" { return [list 0 [expr {$h * $pct}]] } - default { return [list 0 0] } - } - } - } - - # Handle x y vector where one or both components have percentage notation - if {[llength $offset] == 2} { - lassign $offset ox oy - - if {[string match *%* $ox]} { - set pct [expr {[string map {% ""} $ox] / 100.0}] - set ox [expr {$w * $pct}] - } - - if {[string match *%* $oy]} { - set pct [expr {[string map {% ""} $oy] / 100.0}] - set oy [expr {$h * $pct}] - } - - return [list $ox $oy] - } - - # Default fallback - return $offset -} - -When /someone/ wishes to draw a shape with /...options/ { - set isRect 0 - if {[dict exists $options type] && [dict get $options type] eq "rect"} { - set isRect 1 - } - - set c [dict_getdef $options center {0 0}] - - set color [dict_getdef $options color white] - set filled [dict_getdef $options filled false] - set thickness [dict_getdef $options thickness 1] - set layer [dict_getdef $options layer 0] - set angle [dict_getdef $options angle 0] - - if {$isRect} { - set w [dict_getdef $options width 100] - set h [dict_getdef $options height 100] - - set hw [expr {$w / 2.0}] - set hh [expr {$h / 2.0}] - - set points [lmap v [list \ - [list [expr {-$hw}] [expr {-$hh}]] \ - [list [expr {$hw}] [expr {-$hh}]] \ - [list [expr {$hw}] [expr {$hh}]] \ - [list [expr {-$hw}] [expr {$hh}]] \ - [list [expr {-$hw}] [expr {-$hh}]] \ - ] { - vec2 add [vec2 rotate $v $angle] $c - }] - } else { - set numPoints [dict_getdef $options sides 4] - if {[dict exists $options shape] && [dict exists $shapes [dict get $options shape]]} { - set numPoints [dict get $shapes [dict get $options shape]] - } - set r [dict_getdef $options radius 50] - - set points {{0 0}} - set centerPoint {0 0} - set polyAngle [expr {2 * 3.14159 / $numPoints + 3.14159}] - set angleIncr [expr {2 * 3.14159 / $numPoints}] - - for {set i 0} {$i < $numPoints} {incr i} { - set p [vec2 add [lindex $points end] [vec2 scale [list [expr {cos($polyAngle)}] [expr {sin($polyAngle)}]] $r]] - lappend points $p - set centerPoint [vec2 add $centerPoint $p] - set polyAngle [expr {$polyAngle + $angleIncr}] - } - - set points [lmap v $points { - vec2 add [vec2 rotate [vec2 sub $v [vec2 scale $centerPoint [expr {1.0/$numPoints}]]] $angle] $c - }] - } - - if {$filled} { - Wish to draw a polygon with points $points color $color layer $layer - } else { - Wish to draw a stroke with points $points width $thickness color $color layer $layer - } -} - -When /someone/ wishes /p/ draws a /shape/ { - Wish $p draws a $shape with color white -} - -# Handle "a" vs "an" grammar variations -When /someone/ wishes /p/ draws an /shape/ { - Wish $p draws a $shape -} - -When /someone/ wishes /p/ draws text /text/ with /...options/ & /p/ has region /r/ { - # As shapes.folk but for text. - lassign [region centroid $r] cx cy - set pageAngle [region angle $r] - - # Use the page's angle unless explicitly overwritten - set defaults [dict create \ - color white \ - scale 1.0 \ - layer 0 \ - angle $pageAngle \ - anchor center \ - font "PTSans-Regular" - ] - - set options [dict merge $defaults $options] - - set color [dict get $options color] - set scale [dict get $options scale] - set layer [dict get $options layer] - set angle [dict get $options angle] - set anchor [dict get $options anchor] - set font [dict get $options font] - - set offset [dict_getdef $options offset {0 0}] - set offset [::process_offset $offset $r] - set center [vec2 add [list $cx $cy] [vec2 rotate $offset $pageAngle]] - - Wish to draw text with position $center scale $scale text $text\ - color $color radians $angle anchor $anchor font $font -} - -When /someone/ wishes /p/ draws text /text/ { - Wish $p draws text $text with color white -} - -When /someone/ wishes /p/ draws a /shape/ with /...options/ & /p/ has region /r/ { - lassign [region centroid $r] cx cy - set angle [region angle $r] - - set color [dict_getdef $options color white] - set filled [dict_getdef $options filled false] - set thickness [dict_getdef $options thickness 5] - set layer [dict_getdef $options layer 0] - - set offset [dict_getdef $options offset {0 0}] - set offset [process_offset $offset $r] - - set center [vec2 add [list $cx $cy] [vec2 rotate $offset $angle]] - - if {$shape eq "circle"} { - set radius [dict_getdef $options radius 50] - - Wish to draw a circle with center $center radius $radius thickness $thickness \ - color $color filled $filled layer $layer - - } elseif {$shape eq "rect"} { - set w [dict_getdef $options width [region width $r]] - set h [dict_getdef $options height [region height $r]] - - Wish to draw a shape with type rect center $center width $w height $h angle $angle \ - color $color filled $filled thickness $thickness layer $layer - - } elseif {[dict exists $shapes $shape]} { - set radius [dict_getdef $options radius 50] - - Wish to draw a shape with sides [dict get $shapes $shape] center $center radius $radius \ - angle $angle color $color filled $filled thickness $thickness layer $layer - - } else { - set radius [dict_getdef $options radius 50] - - Wish to draw a shape with sides 4 center $center radius $radius \ - angle $angle color $color filled $filled thickness $thickness layer $layer - } -} - -# Pass through options for "an" version -When /someone/ wishes /p/ draws an /shape/ with /...options/ { - Wish $p draws a $shape with {*}$options -} - -When /someone/ wishes /p/ draws a rect with width /w/ height /h/ { - Wish $p draws a rect with width $w height $h -} - -When /someone/ wishes /p/ draws a /shape/ with radius /rad/ { - Wish $p draws a $shape with radius $rad -} - -When /someone/ wishes /page/ draws a set of points /points/ with /...options/ & /page/ has region /r/ { - set radius [dict_getdef $options radius 5] - set color [dict_getdef $options color white] - set filled [dict_getdef $options filled true] - set thickness [dict_getdef $options thickness 2] - set layer [dict_getdef $options layer 0] - - lassign [region centroid $r] cx cy - set angle [region angle $r] - set center [list $cx $cy] - - if {[dict exists $options offset]} { - set offset [dict get $options offset] - set offset [process_offset $offset $r] - set center [vec2 add $center [vec2 rotate $offset $angle]] - } - - foreach point $points { - set pointPos [vec2 add $center [vec2 rotate $point $angle]] - - Wish to draw a circle with center $pointPos radius $radius thickness $thickness \ - color $color filled $filled layer $layer - } -} - -When /someone/ wishes /page/ draws a polyline /points/ with /...options/ & /page/ has region /r/ { - set color [dict_getdef $options color white] - set thickness [dict_getdef $options thickness 2] - set layer [dict_getdef $options layer 0] - set dashed [dict_getdef $options dashed false] - set dashlength [dict_getdef $options dashlength 20] - set dashoffset [dict_getdef $options dashoffset 0] - - lassign [region centroid $r] cx cy - set angle [region angle $r] - set center [list $cx $cy] - - if {[dict exists $options offset]} { - set offset [dict get $options offset] - set offset [process_offset $offset $r] - set center [vec2 add $center [vec2 rotate $offset $angle]] - } - - set transformedPoints {} - foreach point $points { - lappend transformedPoints [vec2 add $center [vec2 rotate $point $angle]] - } - - if {$dashed} { - Wish to draw a dashed stroke with points $transformedPoints color $color width $thickness \ - dashlength $dashlength dashoffset $dashoffset layer $layer - } else { - Wish to draw a stroke with points $transformedPoints color $color width $thickness layer $layer - } -} - -Claim $this has demo { - # Center circle - Wish $this draws a circle - - # Grid of shapes with varying thickness - set baseX -850 - set baseY -200 - set gridSpacing 130 - - # Row 0: Title - Wish $this draws text "triangle" with color skyblue offset [list $baseX [expr {$baseY - ($gridSpacing / 2.0)}]] scale 0.9 - Wish $this draws text "square" with color green offset [list [expr {$baseX + $gridSpacing}] [expr {$baseY - ($gridSpacing / 2.0)}]] scale 0.9 - Wish $this draws text "pentagon" with color gold offset [list [expr {$baseX + $gridSpacing*2}] [expr {$baseY - ($gridSpacing / 2.0)}]] scale 0.9 - Wish $this draws text "hexagon" with color orange offset [list [expr {$baseX + $gridSpacing*3}] [expr {$baseY - ($gridSpacing / 2.0)}]] scale 0.9 - - # Row 1: Regular polygons with different colors and thickness - Wish $this draws a triangle with color skyblue thickness 2 offset [list $baseX [expr {$baseY}]] - Wish $this draws a square with color green thickness 4 offset [list [expr {$baseX + $gridSpacing}] [expr {$baseY}]] - Wish $this draws a pentagon with color gold thickness 6 offset [list [expr {$baseX + $gridSpacing*2}] [expr {$baseY}]] - Wish $this draws a hexagon with color orange thickness 8 offset [list [expr {$baseX + $gridSpacing*3}] [expr {$baseY}]] - - # Row 2: Filled shapes - Wish $this draws a triangle with color skyblue filled true offset [list $baseX [expr {$baseY + $gridSpacing}]] - Wish $this draws a square with color green filled true offset [list [expr {$baseX + $gridSpacing}] [expr {$baseY + $gridSpacing}]] - Wish $this draws a pentagon with color gold filled true offset [list [expr {$baseX + $gridSpacing*2}] [expr {$baseY + $gridSpacing}]] - Wish $this draws a hexagon with color orange filled true offset [list [expr {$baseX + $gridSpacing*3}] [expr {$baseY + $gridSpacing}]] - - # Row 3: Directional offset examples (replacing shift) - Wish $this draws a triangle with radius 40 offset "right 50%" color skyblue - Wish $this draws a square with radius 40 offset "left 50%" color green - Wish $this draws a pentagon with radius 40 offset "up 50%" color gold - Wish $this draws a hexagon with radius 40 offset "down 50%" color orange - - # Row 4: Rectangles with different properties - Wish $this draws a rect with width 80 height 50 color cyan thickness 3 offset [list $baseX [expr {$baseY + $gridSpacing*3}]] - Wish $this draws a rect with width 80 height 50 color magenta filled true offset [list [expr {$baseX + $gridSpacing}] [expr {$baseY + $gridSpacing*3}]] - Wish $this draws a rect with width 80 height 50 offset "right 50%" - Wish $this draws a rect with width 80 height 50 offset "left 50%" - -# Animated elements - When $this has region /r/ & the clock time is /t/ { - lassign [region angle $r] angle - for {set i 0} {$i < 8} {incr i} { - set offsetVector [list [sin [+ [- $i $t] $angle]] [* 2 [cos [+ [- $i $t] $angle]]]] - set vector [::vec2::scale $offsetVector [+ [* $i $i] 15]] - Wish $this draws a circle with radius $i color palegoldenrod offset $vector - } - } - - When $this has region /r/ & the clock time is /t/ { - lassign [region centroid $r] x y - set fillVal [expr {round(sin($t) * 2)}] - set fill [expr {$fillVal % 2 == 0}] - set y [- $y 150] - Wish to draw a shape with sides 4 center [list [- $x 200] $y] radius 60 color white filled $fill - Wish to draw text with position [list [- $x 200] [+ $y 14]] scale 1.5 text "$fillVal" color red - } - - When $this has region /r/ & the clock time is /t/ { - lassign [region centroid $r] x y - set fillVal [expr {round($t * 2)}] - set fill [expr {$fillVal % 2 == 0}] - set y [- $y 150] - Wish to draw a shape with sides 4 center [list [+ $x 200] $y] radius 60 color white filled $fill - Wish to draw text with position [list [+ $x 200] [+ $y 14]] scale 1.5 text "$fill" color red - } - - Wish $this is outlined white -} diff --git a/builtin-programs/shapes/region.folk b/builtin-programs/shapes/region.folk deleted file mode 100644 index 492a268d..00000000 --- a/builtin-programs/shapes/region.folk +++ /dev/null @@ -1,92 +0,0 @@ -# Creates an id "${p}:${index}" and assigns region. -# Extra regions can be used to create sensitive areas other pages can collect. -When /someone/ wishes /p/ adds region with /...options/ & /p/ has region /r/ { - lassign [region centroid $r] cx cy - set angle [region angle $r] - - set defaults { - index 0 \ - height 55 \ - width 55 \ - highlight false \ - color red \ - } - - set index [dict get $options index] - set height [dict get $options height] - set width [dict get $options width] - set highlight [dict get $options highlight] - set color [dict get $options color] - - set offset [dict_getdef $options offset {0 0}] - set offset [::process_offset $offset $r] - set center [vec2 add [list $cx $cy] [vec2 rotate $offset $angle]] - - # compute points offset from $p - set hw [expr {$width / 2.0}] - set hh [expr {$height / 2.0}] - - # compute points in table coordinates - set tablePoints [lmap v [list \ - [list [expr {-$hw}] [expr {-$hh}]] \ - [list [expr {$hw}] [expr {-$hh}]] \ - [list [expr {$hw}] [expr {$hh}]] \ - [list [expr {-$hw}] [expr {$hh}]] \ - [list [expr {-$hw}] [expr {-$hh}]] \ - ] { - vec2 add $center [vec2 rotate $v $angle] - }] - - set edges [list] - for {set i 0} {$i < [llength $tablePoints]} {incr i} { - if {$i > 0} { lappend edges [list [expr {$i - 1}] $i] } - } - lappend edges [list [expr {[llength $tablePoints] - 1}] [lindex $tablePoints 0]] - - # Create new region in table points - set indexedRegion [region create $tablePoints $edges $angle] - Claim $p has indexedRegion with index $index region $indexedRegion - Claim "${p}:${index}" has region $indexedRegion - - # debug: display dashed line around the points - if {$highlight} { - Wish region $indexedRegion has highlight $highlight with color $color - } -} - -When /someone/ wishes region /r/ has highlight /highlighted/ with /...options/ { - - set color [dict_getdef $options color white] - set thickness [dict_getdef $options thickness 2] - set layer [dict_getdef $options layer 0] - set dashed [dict_getdef $options dashed false] - set dashlength [dict_getdef $options dashlength 20] - set dashoffset [dict_getdef $options dashoffset 0] - - if {$highlighted} { - set verts [region vertices $r] - set edges [region edges $r] - lappend verts [lindex $verts 0] - Wish to draw a dashed stroke with points $verts color $color width $thickness dashlength $dashlength dashoffset $dashoffset layer $layer - } -} - -Claim $this has demo { - # How to use - # When builtin-programs/shapes/region.folk has demo /code/ & \ - # $this has region /r/ { - # Claim $this has program code $code - # set angle [region angle $r] - # set pos [region bottom $r] - # Wish to draw text with position $pos scale 0.6 text $code radians $angle anchor topright - # } - - When $this has region /r/ { - Wish region $r has highlight true with color yellow thickness 1 dashed true - - Wish $this adds region with index 0 width 50 height 50 offset [list -250 0] highlight true color yellow - Wish $this draws text "Region 0" with offset [list -250 -50] scale 0.6 color yellow - Wish $this adds region with index 1 width 50 height 50 offset [list 250 0] highlight true color yellow - Wish $this draws text "Region 1" with offset [list 250 -50] scale 0.6 color yellow - } -} diff --git a/builtin-programs/terminal.folk b/builtin-programs/terminal.folk index 1fd9c042..fffb7c79 100644 --- a/builtin-programs/terminal.folk +++ b/builtin-programs/terminal.folk @@ -2,64 +2,218 @@ # # Spawn terminals with any command (default "bash"): # Wish $this is a terminal -# Wish $this is a terminal spawning "any command" +# Wish $this is a terminal spawning "vim" +# Wish $this is a terminal spawning "bash" with rows 24 cols 80 # -# Send keyboard events to the terminal: -# Claim $thing has keyboard input -# -# Optionally, draw the terminal on an arbitrary region: -# Claim $thing has terminal region $region -# -# -# Example program: Tie it all together with a simple vim editor... -# -# When $this points up at /target/ & /target/ has program /anything/ { -# Wish $this is a terminal spawning "vim ~/folk-printed-programs/$target.folk" -# When $this has region /r/ { -# Claim $this has terminal region [region move $r right 350px] -# } -# Claim $this has keyboard input +# Terminals draw onto their own physical drawing surface. To attach one to +# another quad, create a child surface and make that child the terminal: +# Wish $this adds child surface terminal with width 16 height 9 offset {right 60%} +# When $this has child surface /term/ with name terminal /...opts/ { +# Wish $term is a terminal spawning "bash" # } # -# - -error "FIXME: terminal.folk not currently working." +# Keyboard input follows the same focus relation as editors: a keyboard that +# points at a terminal types into that terminal. +# The old "has keyboard input" API is still accepted as a fallback for keyboards +# that do not currently have a focused input target. source lib/terminal.tcl -# WIP: Needs to finish being fixed for folk2. +fn terminalDefaultOptions {} { + dict create \ + rows 12 \ + cols 43 \ + padding 0.4 \ + textScale "" \ + font NeomatrixCode \ + foreground green \ + background {0 0 0 0.82} \ + activeColor green \ + layer 0 +} + +fn terminalOptionsWithDefaults {options} { + dict merge [terminalDefaultOptions] $options +} + +fn terminalInstanceKey {thing cmd options} { + list terminal $thing $cmd $options +} + +fn terminalKeyboardPathHasFocusedTarget {kbPath} { + foreach result [Query! /keyboard/ is a keyboard with path $kbPath /...keyboardOptions/] { + set keyboard [dict get $result keyboard] + if {[llength [Query! /someone/ claims $keyboard has focused input target /target/]] > 0} { + return 1 + } + } + return 0 +} + +fn terminalPhysicalLength {value} { + if {[llength $value] != 1} { + error "terminal: expected a scalar physical length, got $value" + } + + set unit "" + set amount $value + foreach suffix {mm cm m} { + if {[string match *$suffix $value]} { + set unit $suffix + set amount [string range $value 0 end-[string length $suffix]] + break + } + } + + if {![string is double -strict $amount]} { + error "terminal: invalid physical length $value" + } + + switch -- $unit { + "" - cm { return [expr {double($amount) * 0.01}] } + mm { return [expr {double($amount) * 0.001}] } + m { return [expr {double($amount)}] } + default { error "terminal: invalid physical unit $unit" } + } +} + +fn terminalTextScale {width height options} { + set explicit [dict getdef $options textScale ""] + if {$explicit ne ""} { + return [terminalPhysicalLength $explicit] + } + + set rows [dict get $options rows] + set cols [dict get $options cols] + set padding [terminalPhysicalLength [dict get $options padding]] + set usableWidth [expr {$width - 2.0 * $padding}] + set usableHeight [expr {$height - 2.0 * $padding}] + if {$usableWidth < 0.001} { set usableWidth 0.001 } + if {$usableHeight < 0.001} { set usableHeight 0.001 } + + if {$rows < 1} { set rows 1 } + if {$cols < 1} { set cols 1 } + set rowScale [expr {$usableHeight / $rows}] + set colScale [expr {$usableWidth / $cols / 0.5859375}] + if {$rowScale < $colScale} { + return $rowScale + } + return $colScale +} + +fn terminalSurfaceCorners {width height} { + lmap point [list \ + {0 0} \ + [list $width 0] \ + [list $width $height] \ + [list 0 $height]] { + lmap value $point { + format "%sm" $value + } + } +} + +fn terminalDrawSurface {disp surface width height text options isActive} { + set options [terminalOptionsWithDefaults $options] + lassign [terminalSurfaceCorners $width $height] p0 p1 p2 p3 + + set background [dict get $options background] + set layer [dict get $options layer] + Wish to draw a quad onto $disp in surface $surface with \ + p0 $p0 p1 $p1 p2 $p2 p3 $p3 \ + color $background layer [expr {$layer - 10}] + + set padding [terminalPhysicalLength [dict get $options padding]] + set scale [terminalTextScale $width $height $options] + set textPosition [lmap value [list $padding $padding] { + format "%sm" $value + }] + Wish to draw text onto $disp in surface $surface with \ + position $textPosition \ + scale [format "%sm" $scale] \ + anchor topleft \ + font [dict get $options font] \ + color [dict get $options foreground] \ + layer $layer \ + text $text + + if {$isActive} { + set points [list $p0 $p1 $p2 $p3 $p0] + Wish to draw a line onto $disp in surface $surface with \ + points $points width 0.2 color [dict get $options activeColor] \ + layer [expr {$layer + 1}] + } +} When /anyone/ wishes /thing/ is a terminal { - Wish $thing is a terminal spawning bash + Wish $thing is a terminal spawning bash } -When /thing/ has terminal region /r/ & /r/ has keyboard input { - Claim $thing has keyboard input +When /anyone/ wishes /thing/ is a terminal with /...options/ { + Wish $thing is a terminal spawning bash with {*}$options } When /anyone/ wishes /thing/ is a terminal spawning /cmd/ { - set term [$terminalLib create 12 43 $cmd] - # Keep for 10 minutes. - Claim -keep [expr {10*60*1000}]ms \ - -destructor [list $terminalLib destroy $term] - $thing has terminal $term spawning $cmd - - When the clock time is /t/ { - set body { - Wish region $region is labelled [$terminalLib read $term] + Wish $thing is a terminal spawning $cmd with {*}[terminalDefaultOptions] +} + +When /anyone/ wishes /thing/ is a terminal spawning /cmd/ with /...options/ { + set options [terminalOptionsWithDefaults $options] + set rows [dict get $options rows] + set cols [dict get $options cols] + if {$rows < 2 || $cols < 2} { + error "terminal: rows and cols must both be at least 2" } - When $thing has terminal region /region/ $body - When /nobody/ claims $thing has terminal region /x/ & $thing has region /region/ $body - } - - When /anyone/ claims $thing has keyboard input \ - & keyboard /anyone/ claims key /key/ is /direction/ with /...options/ { - if {$direction != "up"} { - if {[dict exists $options printable]} { - $terminalLib write $term [dict get $options printable] - } else { - $terminalLib handleKey $term $key - } + + set existing [Query! /someone/ claims $thing has terminal /term/ spawning $cmd with {*}$options] + if {[llength $existing] > 0} { + Claim $thing is a terminal with {*}$options + Claim $thing accepts keyboard input + return } - } + + set term [$terminalLib createForKey [terminalInstanceKey $thing $cmd $options] $rows $cols $cmd] + if {$term eq ""} { + error "terminal: failed to spawn $cmd" + } + + Claim -keep [expr {10 * 60 * 1000}]ms \ + -destructor [list $terminalLib destroy $term] \ + $thing has terminal $term spawning $cmd with {*}$options + Claim $thing is a terminal with {*}$options + Claim $thing accepts keyboard input +} + +When /keyboard/ is a keyboard with path /kbPath/ /...keyboardOptions/ &\ + /keyboard/ points up at /thing/ &\ + /thing/ is a terminal with /...terminalOptions/ { + Claim $keyboard has focused input target $thing + Claim $keyboard is typing into $thing +} + +When /keyboard/ is a keyboard with path /kbPath/ /...keyboardOptions/ &\ + /keyboard/ is typing into /thing/ &\ + /thing/ has terminal /term/ spawning /cmd/ with /...terminalOptions/ { + Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { + $terminalLib handleEvent $term $key $keyState $options + } +} + +# Backwards compatibility with the old global-input API. Prefer pointing a +# keyboard at the terminal quad, which uses the focus relation above. +When /anyone/ claims /thing/ has keyboard input &\ + /thing/ has terminal /term/ spawning /cmd/ with /...terminalOptions/ { + Subscribe: keyboard /kbPath/ claims key /key/ is /keyState/ with /...options/ { + if {[terminalKeyboardPathHasFocusedTarget $kbPath]} { return } + $terminalLib handleEvent $term $key $keyState $options + } +} + +When display /disp/ has width /displayWidth/ height /displayHeight/ &\ + /thing/ has terminal /term/ spawning /cmd/ with /...options/ &\ + /thing/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ &\ + the clock time is /t/ { + set isActive [expr {[llength [Query! /someone/ claims /keyboard/ is typing into $thing]] > 0}] + terminalDrawSurface $disp $surface $width $height [$terminalLib read $term] $options $isActive } diff --git a/builtin-programs/title.folk b/builtin-programs/title.folk index 223bf6a7..4f623ae4 100644 --- a/builtin-programs/title.folk +++ b/builtin-programs/title.folk @@ -9,14 +9,12 @@ When /thing/ has quad /quad/ { Claim -keep 50ms $thing has a quad } -When the quad library is /quadLib/ &\ - the pose library is /poseLib/ &\ - the quad changer is /quadChange/ &\ - display /disp/ has width /displayWidth/ height /displayHeight/ &\ - display /disp/ has intrinsics /displayIntrinsics/ &\ - /thing/ has a quad { +When display /disp/ has width /displayWidth/ height /displayHeight/ &\ + /thing/ has physical drawing surface /surface/ with width /width/ height /height/ space /space/ &\ + /disp/ has canvas projection for surface /surface/ /surfaceToClip/ { - fn quadChange + set paddingMeters 0.02 + set textScaleCm 2 foreach {label edge textAnchor} { titled top bottom @@ -28,53 +26,27 @@ When the quad library is /quadLib/ &\ set text [join [lmap result $results {dict get $result text}] "\n"] if {$text eq ""} { return } - When -atomically $thing has quad /q/ { - package require linalg - namespace import \ - ::math::linearalgebra::add \ - ::math::linearalgebra::sub \ - ::math::linearalgebra::scale \ - ::math::linearalgebra::unitLengthVector - - lassign [$quadLib vertices [quadChange $q "display $disp"]] \ - topLeft topRight bottomRight bottomLeft - - switch $edge { - top { - set physicalPos [scale 0.5 [add $topLeft $topRight]] - set physicalDir [sub $topLeft $bottomLeft] - } - bottom { - set physicalPos [scale 0.5 [add $bottomLeft $bottomRight]] - set physicalDir [sub $bottomLeft $topLeft] - } - right { - set physicalPos [scale 0.5 [add $topRight $bottomRight]] - set physicalDir [sub $topRight $topLeft] - } - left { - set physicalPos [scale 0.5 [add $topLeft $bottomLeft]] - set physicalDir [sub $topLeft $topRight] - } + switch $edge { + top { + set position [list [expr {$width / 2.0}] [expr {-$paddingMeters}]] } + bottom { + set position [list [expr {$width / 2.0}] [expr {$height + $paddingMeters}]] + } + right { + set position [list [expr {$width + $paddingMeters}] [expr {$height / 2.0}]] + } + left { + set position [list [expr {-$paddingMeters}] [expr {$height / 2.0}]] + } + } - set paddingMeters 0.02 - set offset [scale $paddingMeters [unitLengthVector $physicalDir]] - set physicalPos [add $physicalPos $offset] - - set dispPosition [$poseLib project $displayIntrinsics $displayWidth $displayHeight $physicalPos] - - set dispTopLeft [$poseLib project $displayIntrinsics $displayWidth $displayHeight $topLeft] - set dispTopRight [$poseLib project $displayIntrinsics $displayWidth $displayHeight $topRight] - - set dispTop [vec2 sub $dispTopRight $dispTopLeft] - set dispRadians [expr {-atan2([lindex $dispTop 1], [lindex $dispTop 0])}] + set meterPosition [lmap value $position { format "%sm" $value }] - Wish to draw text onto $disp with \ - position $dispPosition \ - scale 36.0 radians $dispRadians anchor $textAnchor \ - text $text - } + Wish to draw text onto $disp in surface $surface with \ + position $meterPosition \ + scale $textScaleCm anchor $textAnchor \ + text $text } } -} \ No newline at end of file +} diff --git a/builtin-programs/web/textures.folk b/builtin-programs/web/textures.folk index 756830cc..87ef8a20 100644 --- a/builtin-programs/web/textures.folk +++ b/builtin-programs/web/textures.folk @@ -13,8 +13,6 @@ When the GPU library is /gpuLib/ & the image library is /imageLib/ &\ #include "vk_mem_alloc.h" - VmaAllocator vmaGetAllocator(); - VkDevice device; } @@ -134,13 +132,10 @@ When the GPU library is /gpuLib/ & the image library is /imageLib/ &\ vkWaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX); // Copy staging buffer back to CPU - void* data; - vmaMapMemory(vmaGetAllocator(), stagingBufferAllocation, &data); - memcpy(im.data, data, stagingBufferSize); - vmaUnmapMemory(vmaGetAllocator(), stagingBufferAllocation); + copyTextureStagingBufferToImage(stagingBufferAllocation, im, stagingBufferSize); // Cleanup staging buffer - vmaDestroyBuffer(vmaGetAllocator(), stagingBuffer, stagingBufferAllocation); + destroyTextureBuffer(stagingBuffer, stagingBufferAllocation); return im; } diff --git a/lib/math.tcl b/lib/math.tcl index 300e1b04..78adf90c 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -3,6 +3,9 @@ # This file provides global math datatypes and utilities. # +set ::PI 3.14159 +set ::TAU 6.28318 + namespace eval ::vec2 { proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] diff --git a/lib/terminal.tcl b/lib/terminal.tcl index 9c72d103..5c56314d 100644 --- a/lib/terminal.tcl +++ b/lib/terminal.tcl @@ -10,7 +10,7 @@ $cc endcflags -lutil ./vendor/libtmt/tmt.c $cc include $cc include $cc include -if {$::tcl_platform(os) eq "darwin"} { +if {[string tolower $::tcl_platform(os)] eq "darwin"} { $cc include } else { $cc include @@ -19,24 +19,31 @@ $cc include $cc include $cc include $cc include +$cc include $cc include "tmt.h" -$cc struct VTerminal { - TMT* tmt; - int pty_fd; - int pid; - - // Note: display has 1 more column than tmt screen to hold newlines between each line - char* display; - int curs_r; - int curs_c; - int ncols; -}; - $cc code { #define PTYBUF 4096 char iobuf[PTYBUF]; + typedef struct VTerminal { + TMT* tmt; + int pty_fd; + int pid; + + // Note: display has 1 more column than tmt screen to hold newlines between each line. + char* display; + int curs_r; + int curs_c; + int ncols; + char* key; + int refCount; + struct VTerminal* next; + } VTerminal; + + pthread_mutex_t terminalRegistryMutex = PTHREAD_MUTEX_INITIALIZER; + VTerminal* terminalRegistry = NULL; + char* charAt(VTerminal *vt, int r, int c) { int i = r * (vt->ncols + 1) + c; return &vt->display[i]; @@ -75,51 +82,117 @@ $cc code { *charAt(vt, vt->curs_r, vt->curs_c) = 0xDB; // block char: █ } } + + VTerminal* termCreateRaw(int rows, int cols, char** cmd) { + int i = 0; + while (true) { + // execvp requires cmd array to be terminated by null pointer. + if (strlen(cmd[i]) == 0) { cmd[i] = NULL; break; } + i++; + } + + VTerminal *vt = malloc(sizeof(VTerminal)); + vt->curs_r = 0; + vt->curs_c = 0; + vt->ncols = cols; + vt->key = NULL; + vt->refCount = 1; + vt->next = NULL; + + vt->display = malloc(sizeof(char[rows][cols + 1])); + memset(vt->display, ' ', sizeof(char[rows][cols + 1])); + for (int r = 0; r < rows - 1; r++) { + *charAt(vt, r, cols) = '\n'; + } + *charAt(vt, rows - 1, cols) = '\0'; + + vt->tmt = tmt_open(rows, cols, tmtEvent, vt, NULL); + if (vt->tmt == NULL) { + free(vt->display); + free(vt); + return NULL; + } + + struct winsize ws = {.ws_row = rows, .ws_col = cols}; + pid_t pid = forkpty(&vt->pty_fd, NULL, NULL, &ws); + if (pid < 0){ + tmt_close(vt->tmt); + free(vt->display); + free(vt); + return NULL; + } else if (pid == 0){ + setenv("TERM", "ansi", 1); + if (execvp(cmd[0], cmd) == -1) { + fprintf(stderr, "execvp(%s, ...) failed: %m\n", cmd[0]); + } + _exit(127); + } + + vt->pid = pid; + fcntl(vt->pty_fd, F_SETFL, O_NONBLOCK); + return vt; + } + + void termDestroyRaw(VTerminal* vt) { + kill(vt->pid, SIGTERM); + close(vt->pty_fd); + tmt_close(vt->tmt); + free(vt->display); + free(vt); + } } $cc proc termCreate {int rows int cols char* cmd[]} VTerminal* { - int i = 0; - while (true) { - // execvp requires cmd array to be terminated by null pointer - if (strlen(cmd[i]) == 0) { cmd[i] = NULL; break; } - i++; - } - - VTerminal *vt = malloc(sizeof(VTerminal)); - vt->curs_r = 0; - vt->curs_c = 0; - vt->ncols = cols; - - vt->display = malloc(sizeof(char[rows][cols + 1])); - for (int r = 0; r < rows - 1; r++) { - *charAt(vt, r, cols) = '\n'; - } - *charAt(vt, rows - 1, cols) = '\0'; - - vt->tmt = tmt_open(rows, cols, tmtEvent, vt, NULL); - - struct winsize ws = {.ws_row = rows, .ws_col = cols}; - pid_t pid = forkpty(&vt->pty_fd, NULL, NULL, &ws); - if (pid < 0){ - return NULL; - } else if (pid == 0){ - setenv("TERM", "ansi", 1); - if (execvp(cmd[0], cmd) == -1) { - fprintf(stderr, "execvp(%s, ...) failed: %m\n", cmd[0]); + return termCreateRaw(rows, cols, cmd); +} + +$cc proc termCreateForKey {char* key int rows int cols char* cmd[]} VTerminal* { + pthread_mutex_lock(&terminalRegistryMutex); + for (VTerminal* vt = terminalRegistry; vt != NULL; vt = vt->next) { + if (strcmp(vt->key, key) == 0) { + vt->refCount++; + pthread_mutex_unlock(&terminalRegistryMutex); + return vt; } - return NULL; } - vt->pid = pid; - fcntl(vt->pty_fd, F_SETFL, O_NONBLOCK); + VTerminal* vt = termCreateRaw(rows, cols, cmd); + if (vt != NULL) { + vt->key = strdup(key); + vt->next = terminalRegistry; + terminalRegistry = vt; + } + pthread_mutex_unlock(&terminalRegistryMutex); return vt; } $cc proc termDestroy {VTerminal* vt} void { - kill(vt->pid, SIGTERM); - close(vt->pty_fd); - free(vt->display); - free(vt); + if (vt == NULL) { return; } + + if (vt->key == NULL) { + termDestroyRaw(vt); + return; + } + + pthread_mutex_lock(&terminalRegistryMutex); + vt->refCount--; + if (vt->refCount > 0) { + pthread_mutex_unlock(&terminalRegistryMutex); + return; + } + + VTerminal** cursor = &terminalRegistry; + while (*cursor != NULL) { + if (*cursor == vt) { + *cursor = vt->next; + break; + } + cursor = &(*cursor)->next; + } + pthread_mutex_unlock(&terminalRegistryMutex); + + free(vt->key); + termDestroyRaw(vt); } $cc proc termRead {VTerminal* vt} char* { @@ -166,25 +239,21 @@ set terminalLib [library create terminalLib {impl} { dict append keymap "Control_$char" $charCode } - proc _remap {key} { + proc keyBytes {key {options {}}} { variable keymap - if {[string length $key] == 1} { - # Convert ctrl-A through ctrl-Z and others to terminal control characters - if {$ctrlPressed} { - set charCode [scan [string toupper $key] %c] - if {$charCode >= 64 && $charCode <= 95} { - set charCode [expr {$charCode - 64}] - return [format %c $charCode] - } - } - # All other single char keys can be passed through - return $key + if {[dict exists $options printable]} { + return [dict get $options printable] } if {[dict exists $keymap $key]} { return [dict get $keymap $key] } + + if {[string length $key] == 1} { + return $key + } + return "" } @@ -193,6 +262,11 @@ set terminalLib [library create terminalLib {impl} { $impl termCreate $rows $cols [list bash -c $cmd ""] } + proc createForKey {key rows cols cmd} { + variable impl + $impl termCreateForKey $key $rows $cols [list bash -c $cmd ""] + } + proc destroy {term} { variable impl $impl termDestroy $term @@ -203,18 +277,22 @@ set terminalLib [library create terminalLib {impl} { $impl termWrite $term $char } - proc handleKey {term key} { + proc handleKey {term key {options {}}} { variable impl - set key [_remap $key] - if {$key ne ""} { - $impl termWrite $term $key + set bytes [keyBytes $key $options] + if {$bytes ne ""} { + $impl termWrite $term $bytes } } + proc handleEvent {term key keyState options} { + if {$keyState eq "up"} { return } + handleKey $term $key $options + } + # Returns a newline separated string of terminal lines proc read {term} { variable impl $impl termRead $term } }] - diff --git a/test/decorations.folk b/test/decorations.folk new file mode 100644 index 00000000..e0e1353a --- /dev/null +++ b/test/decorations.folk @@ -0,0 +1,67 @@ +source builtin-programs/collect.folk +source builtin-programs/decorations/label.folk +source builtin-programs/decorations/outline.folk +source builtin-programs/draw/spaces.folk + +assert {[drawOutlinePoints 0.2 0.1] eq {{0m 0m} {0.2m 0m} {0.2m 0.1m} {0m 0.1m} {0m 0m}}} +assert {[drawSpaceMeterPoint {0.2 0.1}] eq {0.2m 0.1m}} + +assert {[drawLabelMaxLineLength "hi\nthere"] == 5} +assert {abs([drawLabelDefaultScale "hello"] - 0.02) < 1e-9} +assert {abs([drawLabelDefaultScale [string repeat x 100]] - 0.0045) < 1e-9} + +set options [drawLabelDefaultOptions "hello" 0.2 0.1] +assert {[dict get $options position] eq {0.1m 0.05m}} +assert {[dict get $options scale] eq "0.02m"} +assert {[dict get $options anchor] eq "center"} +assert {[dict get $options font] eq "PTSans-Regular"} + +set thing test-thing +set plainThing test-plain-outline +set disp test-display +set surface test-surface +set plainSurface test-plain-surface +set surfaceTarget [drawSpaceSurfaceTarget $disp $surface] +set plainSurfaceTarget [drawSpaceSurfaceTarget $disp $plainSurface] + +Assert! display $disp has width 100 height 100 +Assert! $thing has physical drawing surface $surface with width 0.2 height 0.1 space test-space +Assert! $plainThing has physical drawing surface $plainSurface with width 0.3 height 0.2 space test-space +Assert! $disp has canvas projection for surface $surface {{1 0 0} {0 1 0} {0 0 1}} +Assert! $disp has canvas projection for surface $plainSurface {{1 0 0} {0 1 0} {0 0 1}} + +Wish $thing is labelled "hello" with color cyan +Wish $thing is outlined red with thickness 0.5 layer 7 +Wish $plainThing is outlined blue + +sleep 1 + +set errors [Query! /program/ has error /err/ with info /info/] +assert {[llength $errors] == 0} + +set textDraws [Query! /someone/ wishes to draw text onto $surfaceTarget with /...drawOptions/] +assert {[llength $textDraws] == 1} +set drawOptions [dict get [lindex $textDraws 0] drawOptions] +assert {[dict get $drawOptions text] eq "hello"} +assert {[dict get $drawOptions color] eq "cyan"} +assert {[dict get $drawOptions anchor] eq "center"} +assert {abs([dict get $drawOptions scale] - 0.02) < 1e-9} +assert {[dict get $drawOptions position] eq {0.1 0.05}} + +set lineDraws [Query! /someone/ wishes to draw a line onto $surfaceTarget with /...drawOptions/] +assert {[llength $lineDraws] == 1} +set drawOptions [dict get [lindex $lineDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "red"} +assert {[dict get $drawOptions layer] == 7} +assert {abs([dict get $drawOptions width] - 0.005) < 1e-9} +assert {[dict get $drawOptions points] eq {{0.0 0.0} {0.2 0.0} {0.2 0.1} {0.0 0.1} {0.0 0.0}}} + +set lineDraws [Query! /someone/ wishes to draw a line onto $plainSurfaceTarget with /...drawOptions/] +assert {[llength $lineDraws] == 1} +set drawOptions [dict get [lindex $lineDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "blue"} +assert {[dict get $drawOptions layer] == 2} +assert {abs([dict get $drawOptions width] - 0.01) < 1e-9} +assert {[dict get $drawOptions points] eq {{0.0 0.0} {0.3 0.0} {0.3 0.2} {0.0 0.2} {0.0 0.0}}} + +Exit! 0 diff --git a/test/draw-arc.folk b/test/draw-arc.folk new file mode 100644 index 00000000..75037021 --- /dev/null +++ b/test/draw-arc.folk @@ -0,0 +1,25 @@ +source builtin-programs/draw/arc.folk +source builtin-programs/draw/spaces.folk + +assert {$::PI == 3.14159} +assert {$::TAU == 6.28318} + +set options [drawSpaceNormalizeOptions arc { + center {3 4cm} + radius 3 + thickness 4mm + start 0.5 + arclen 1.5 + color cyan +}] + +set center [dict get $options center] +assert {abs([lindex $center 0] - 0.03) < 1e-9} +assert {abs([lindex $center 1] - 0.04) < 1e-9} +assert {abs([dict get $options radius] - 0.03) < 1e-9} +assert {abs([dict get $options thickness] - 0.004) < 1e-9} +assert {[dict get $options start] == 0.5} +assert {[dict get $options arclen] == 1.5} +assert {[dict get $options color] eq "cyan"} + +Exit! 0 diff --git a/test/draw-connections.folk b/test/draw-connections.folk new file mode 100644 index 00000000..91b6506a --- /dev/null +++ b/test/draw-connections.folk @@ -0,0 +1,9 @@ +source builtin-programs/draw/spaces.folk +source builtin-programs/draw/connections.folk + +lassign [drawConnectionArrowPoints 5 1 2] tip rearLeft rearRight +assert {$tip eq {7 1}} +assert {$rearLeft eq {3 -0.6}} +assert {$rearRight eq {3 2.6}} + +Exit! 0 diff --git a/test/draw-hit-targets.folk b/test/draw-hit-targets.folk new file mode 100644 index 00000000..c7c9f673 --- /dev/null +++ b/test/draw-hit-targets.folk @@ -0,0 +1,29 @@ +source builtin-programs/draw/spaces.folk +source builtin-programs/draw/hit-targets.folk + +set rect [drawHitTargetRect { + width 4 + height 2cm + offset {right 25%} +} 0.2 0.1] +lassign $rect x y width height +assert {abs($x - 0.13) < 1e-9} +assert {abs($y - 0.04) < 1e-9} +assert {abs($width - 0.04) < 1e-9} +assert {abs($height - 0.02) < 1e-9} + +set rect [drawHitTargetRect { + size 10% + top-left {1 2} +} 0.2 0.1] +lassign $rect x y width height +assert {abs($x - 0.01) < 1e-9} +assert {abs($y - 0.02) < 1e-9} +assert {abs($width - 0.02) < 1e-9} +assert {abs($height - 0.01) < 1e-9} + +assert {[drawHitTargetId parent {name button}] eq {hit target of parent button}} +assert {[drawHitTargetName {index 2}] == 2} +assert {[drawHitTargetOffset {left 50%} 0.2 0.1] eq {-0.1 0}} + +Exit! 0 diff --git a/test/draw-image-url.folk b/test/draw-image-url.folk new file mode 100644 index 00000000..e637dc5e --- /dev/null +++ b/test/draw-image-url.folk @@ -0,0 +1,32 @@ +source builtin-programs/collect.folk +source builtin-programs/image/image-lib.folk +source builtin-programs/image/png-lib.folk +source builtin-programs/draw/color-map.folk +source builtin-programs/draw/image.folk + +When /someone/ wishes the GPU loads image /im/ as texture { + Claim the GPU has loaded image $im as texture 99 +} + +Assert! image-page has resolved geometry {width 0.2 height 0.1 left 0.2} +Assert! image-page has canvas canvas-id with width 400 height 300 +Assert! image-page has canvas projection {1 0 0 0 1 0 0 0 1} + +Wish image-page displays image "https://folk.computer/_media/logo.png" + +sleep 1 + +set errors [Query! /program/ has error /err/ with info /info/] +assert {[llength $errors] == 0} + +set loads [Query! /someone/ wishes the GPU loads image /im/ as texture] +assert {[llength $loads] == 1} + +set draws [Query! /someone/ wishes the GPU draws pipeline "image" onto canvas canvas-id with arguments /arguments/] +assert {[llength $draws] == 1} + +set arguments [dict get [lindex $draws 0] arguments] +assert {[lindex $arguments 2] == 99} +assert {[lindex $arguments 3] eq {0 0}} + +Exit! 0 diff --git a/test/draw-shapes.folk b/test/draw-shapes.folk new file mode 100644 index 00000000..b8106d71 --- /dev/null +++ b/test/draw-shapes.folk @@ -0,0 +1,10 @@ +source builtin-programs/draw/shapes.folk + +set points [drawShapeRegularPolygon {0 0} 1 4 0] + +assert {abs([lindex $points 0 0]) < 0.00001} +assert {abs([lindex $points 0 1] + 1.0) < 0.00001} +assert {abs([lindex $points 1 0] - 1.0) < 0.00001} +assert {abs([lindex $points 1 1]) < 0.00001} + +Exit! 0 diff --git a/test/draw-spaces.folk b/test/draw-spaces.folk new file mode 100644 index 00000000..20ff5b74 --- /dev/null +++ b/test/draw-spaces.folk @@ -0,0 +1,109 @@ +source builtin-programs/draw/spaces.folk + +proc fakeQuadLib {cmd args} { + switch -- $cmd { + vertices { + lindex [lindex $args 0] 1 + } + space { + lindex [lindex $args 0] 0 + } + create { + list [lindex $args 0] [lindex $args 1] + } + default { + error "unknown fakeQuadLib command $cmd" + } + } +} + +proc fakePoseLib {cmd intrinsics width height point} { + switch -- $cmd { + project { + list [lindex $point 0] [lindex $point 1] + } + default { + error "unknown fakePoseLib command $cmd" + } + } +} + +proc fakeQuadChange {q targetSpace} { + fakeQuadLib create $targetSpace [fakeQuadLib vertices $q] +} + +set l [drawSpacePhysicalLength 3] +assert {abs($l - 0.03) < 1e-9} + +set l [drawSpacePhysicalLength 4mm] +assert {abs($l - 0.004) < 1e-9} + +set l [drawSpacePhysicalLength 0.5m] +assert {abs($l - 0.5) < 1e-9} + +set point [drawSpacePhysicalPoint {3 -2cm}] +assert {abs([lindex $point 0] - 0.03) < 1e-9} +assert {abs([lindex $point 1] + 0.02) < 1e-9} + +set options [drawSpaceNormalizeOptions circle { + center {3 4cm} + radius 3 + thickness 4mm + color green +}] +set center [dict get $options center] +assert {abs([lindex $center 0] - 0.03) < 1e-9} +assert {abs([lindex $center 1] - 0.04) < 1e-9} +assert {abs([dict get $options radius] - 0.03) < 1e-9} +assert {abs([dict get $options thickness] - 0.004) < 1e-9} + +set H [drawSpaceHomography {{0 0 -1 -1} {1 0 1 -1} {1 1 1 1} {0 1 -1 1}}] +set projected [drawSpaceApplyHomography $H {0.5 0.5}] +assert {abs([lindex $projected 0]) < 1e-9} +assert {abs([lindex $projected 1]) < 1e-9} + +set distance [drawSpaceVectorDistance {0 0 0} {3 4 12}] +assert {abs($distance - 13.0) < 1e-9} + +set q [list "display test" {{0 0 0} {4 0 0} {4 2 0} {0 2 0}}] +assert {[drawSpaceQuadPoint fakeQuadLib $q centroid] eq {2.0 1.0 0.0}} +assert {[drawSpaceQuadPoint fakeQuadLib $q top] eq {2.0 0.0 0.0}} +assert {[drawSpaceQuadPoint fakeQuadLib $q bottom-right] eq {4 2 0}} + +lassign [drawSpaceQuadSize fakeQuadLib $q] width height +assert {abs($width - 4.0) < 1e-9} +assert {abs($height - 2.0) < 1e-9} + +set surfaceQuad [drawSpaceSurfaceQuadBetween fakeQuadLib "display test" {0 0 0} {4 0 0} 2] +lassign [fakeQuadLib vertices $surfaceQuad] topLeft topRight bottomRight bottomLeft +assert {$topLeft eq {0.0 -1.0 0.0}} +assert {$topRight eq {4.0 -1.0 0.0}} +assert {$bottomRight eq {4.0 1.0 0.0}} +assert {$bottomLeft eq {0.0 1.0 0.0}} + +assert {[drawSpaceQuadSurfacePoint fakeQuadLib $q {2 1}] eq {2.0 1.0 0.0}} + +set rectQuad [drawSpaceQuadSurfaceRect fakeQuadLib $q 1 0.5 2 1] +lassign [fakeQuadLib vertices $rectQuad] topLeft topRight bottomRight bottomLeft +assert {$topLeft eq {1.0 0.5 0.0}} +assert {$topRight eq {3.0 0.5 0.0}} +assert {$bottomRight eq {3.0 1.5 0.0}} +assert {$bottomLeft eq {1.0 1.5 0.0}} + +assert {[drawSpaceMeterPoint {0 1.5}] eq {0m 1.5m}} + +set drawSpaceLib [drawSpaceMakeLib fakeQuadLib fakePoseLib fakeQuadChange test 100 50 {}] +assert {[$drawSpaceLib display] eq {test}} +assert {[$drawSpaceLib displaySpace] eq {display test}} +assert {[$drawSpaceLib quadPoint $q top] eq {2.0 0.0 0.0}} +lassign [$drawSpaceLib quadSize $q] width height +assert {abs($width - 4.0) < 1e-9} +assert {abs($height - 2.0) < 1e-9} +assert {[$drawSpaceLib project {50 25 0}] eq {50 25}} +set clipPoint [$drawSpaceLib clipPoint {50 25 0}] +assert {abs([lindex $clipPoint 0]) < 1e-9} +assert {abs([lindex $clipPoint 1]) < 1e-9} +assert {[$drawSpaceLib containsPixelPoint $q {2 1}]} +assert {![$drawSpaceLib containsPixelPoint $q {5 1}]} + +Exit! 0 diff --git a/test/draw-text-anchor.folk b/test/draw-text-anchor.folk new file mode 100644 index 00000000..b53e9d04 --- /dev/null +++ b/test/draw-text-anchor.folk @@ -0,0 +1,63 @@ +source builtin-programs/collect.folk +source builtin-programs/image/image-lib.folk +source builtin-programs/draw/color-map.folk +source builtin-programs/draw/text.folk + +When the image library is /imageLib/ { + fn fakeLoadImage {path} { + $imageLib imageNew 8 8 4 1 + } + Claim the image loader is [fn fakeLoadImage] +} + +When /someone/ wishes the GPU loads image /im/ as texture { + Claim the GPU has loaded image $im as texture 17 +} + +fn drawTextAnchorBounds {instances} { + set minX 1000000.0 + set maxX -1000000.0 + foreach instance $instances { + foreach point [list \ + [lindex $instance 4] \ + [lindex $instance 5] \ + [lindex $instance 6] \ + [lindex $instance 7]] { + set x [lindex $point 0] + if {$x < $minX} { set minX $x } + if {$x > $maxX} { set maxX $x } + } + } + dict create minX $minX maxX $maxX +} + +Assert! text-page has canvas canvas-id with width 400 height 300 +Assert! text-page has canvas projection {1 0 0 0 1 0 0 0 1} + +set fonts [list] +for {set i 0} {$i < 100 && [llength $fonts] == 0} {incr i} { + sleep 0.1 + set fonts [Query! the GPU has font CourierPrimeCode with data /fontData/] +} +assert {[llength $fonts] == 1} + +Wish to draw text onto text-page with \ + position {100 100} \ + scale 10.0 \ + font CourierPrimeCode \ + anchor {1.0 0.5 0.0 0.5} \ + text "A\nAA" \ + color white + +set draws [list] +for {set i 0} {$i < 100 && [llength $draws] == 0} {incr i} { + sleep 0.1 + set draws [Query! /someone/ wishes the GPU draws pipeline "glyph" onto canvas canvas-id with instances /instances/ layer /layer/] +} +assert {[llength $draws] == 1} + +set bounds [drawTextAnchorBounds [dict get [lindex $draws 0] instances]] +assert {[dict get $bounds minX] < 95.0} +assert {[dict get $bounds maxX] < 101.0} + +Exit! 0 diff --git a/test/terminal.folk b/test/terminal.folk new file mode 100644 index 00000000..85fcfd49 --- /dev/null +++ b/test/terminal.folk @@ -0,0 +1,80 @@ +source builtin-programs/collect.folk +source builtin-programs/terminal.folk +source builtin-programs/draw/spaces.folk +source builtin-programs/editor.folk + +assert {abs([terminalPhysicalLength 3] - 0.03) < 1e-9} +assert {abs([terminalPhysicalLength 4mm] - 0.004) < 1e-9} +assert {[drawSpaceMeterPoint {0.2 0.1}] eq {0.2m 0.1m}} + +set options [terminalOptionsWithDefaults {rows 2 cols 10 padding 0.2 foreground white}] +assert {[dict get $options rows] == 2} +assert {[dict get $options cols] == 10} +assert {[dict get $options foreground] eq "white"} +assert {[dict get $options font] eq "NeomatrixCode"} + +assert {[$terminalLib keyBytes x {printable x}] eq "x"} +assert {[$terminalLib keyBytes Return {}] eq "\r"} +assert {[$terminalLib keyBytes Control_c {}] eq "\x03"} +assert {[$terminalLib keyBytes NotARealKey {}] eq ""} + +set disp test-display +set surface test-surface +set surfaceTarget [drawSpaceSurfaceTarget $disp $surface] + +terminalDrawSurface $disp $surface 0.2 0.1 "hello" $options true +sleep 0.5 + +set textDraws [Query! /someone/ wishes to draw text onto $surfaceTarget with /...drawOptions/] +assert {[llength $textDraws] == 1} +set drawOptions [dict get [lindex $textDraws 0] drawOptions] +assert {[dict get $drawOptions text] eq "hello"} +assert {[dict get $drawOptions color] eq "white"} +assert {[dict get $drawOptions anchor] eq "topleft"} + +set lineDraws [Query! /someone/ wishes to draw a line onto $surfaceTarget with /...drawOptions/] +assert {[llength $lineDraws] == 1} +set drawOptions [dict get [lindex $lineDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "green"} + +Wish spawned-terminal is a terminal spawning "printf hi" with rows 2 cols 10 +sleep 0.5 + +set spawned [Query! /someone/ claims spawned-terminal has terminal /term/ spawning /cmd/ with /...spawnOptions/] +assert {[llength $spawned] == 1} +set spawnedTerm [dict get [lindex $spawned 0] term] +set output "" +for {set i 0} {$i < 10} {incr i} { + set output [$terminalLib read $spawnedTerm] + if {[string first hi $output] >= 0} { + break + } + sleep 0.1 +} +assert {[string first hi $output] >= 0} + +Assert! first-wisher wishes shared-terminal is a terminal spawning "printf shared" with rows 2 cols 10 +Assert! second-wisher wishes shared-terminal is a terminal spawning "printf shared" with rows 2 cols 10 +sleep 0.5 + +set shared [Query! /someone/ claims shared-terminal has terminal /term/ spawning /cmd/ with /...sharedOptions/] +assert {[llength $shared] == 1} + +set keyboard keyboard-page +set terminal terminal-page +set syntheticEditor [list $keyboard editor] + +Assert! $keyboard is a keyboard with path keyboard-path locale us +Assert! $terminal is a terminal with {*}[terminalDefaultOptions] +Assert! $keyboard points up at $terminal + +sleep 1 + +assert {[llength [Query! /someone/ claims $keyboard has focused input target $terminal]] == 1} +assert {[llength [Query! /someone/ claims $keyboard is typing into $terminal]] == 1} +assert {[llength [Query! /someone/ claims $keyboard is typing into $syntheticEditor]] == 0} +assert {[terminalKeyboardPathHasFocusedTarget keyboard-path] == 1} +assert {[terminalKeyboardPathHasFocusedTarget unknown-keyboard-path] == 0} +assert {[llength [Query! /program/ has error /err/ with info /info/]] == 0} + +Exit! 0