From 272554c994d2836489cfdbf94168d2d72d847852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 17:32:10 -0400 Subject: [PATCH 1/9] Rebuild drawing primitives PR scope --- Makefile | 7 +- README.md | 3 +- builtin-programs/connections.folk | 69 ----- builtin-programs/decorations/label.folk | 62 ++-- builtin-programs/decorations/outline.folk | 37 ++- builtin-programs/demos.folk | 3 +- builtin-programs/display/arc.folk | 39 --- builtin-programs/display/curve.folk | 135 -------- builtin-programs/draw/arc.folk | 79 +++++ builtin-programs/draw/circle.folk | 47 ++- builtin-programs/draw/curve.folk | 79 +++++ builtin-programs/draw/fill.folk | 56 ++-- builtin-programs/draw/image.folk | 68 ++++- builtin-programs/draw/line.folk | 77 ++++- builtin-programs/draw/shapes.folk | 338 ++++++++++++++++++++ builtin-programs/draw/text.folk | 6 +- builtin-programs/editor.folk | 11 +- builtin-programs/group.folk | 42 --- builtin-programs/intersect.folk | 25 -- builtin-programs/regions.folk | 8 - builtin-programs/shapes.folk | 357 ---------------------- builtin-programs/shapes/region.folk | 92 ------ builtin-programs/title.folk | 45 ++- builtin-programs/web/new.folk | 5 +- lib/math.tcl | 97 ++++++ test/decorations.folk | 67 ++++ test/draw-fill.folk | 37 +++ test/draw-image-url.folk | 60 ++++ test/draw-shapes.folk | 81 +++++ test/draw-text-anchor.folk | 64 ++++ 30 files changed, 1221 insertions(+), 875 deletions(-) delete mode 100644 builtin-programs/connections.folk delete mode 100644 builtin-programs/display/arc.folk delete mode 100644 builtin-programs/display/curve.folk create mode 100644 builtin-programs/draw/arc.folk create mode 100644 builtin-programs/draw/curve.folk create mode 100644 builtin-programs/draw/shapes.folk delete mode 100644 builtin-programs/group.folk delete mode 100644 builtin-programs/intersect.folk delete mode 100644 builtin-programs/regions.folk delete mode 100644 builtin-programs/shapes.folk delete mode 100644 builtin-programs/shapes/region.folk create mode 100644 test/decorations.folk create mode 100644 test/draw-fill.folk create mode 100644 test/draw-image-url.folk create mode 100644 test/draw-shapes.folk create mode 100644 test/draw-text-anchor.folk 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..4f69d3e2 100644 --- a/README.md +++ b/README.md @@ -517,7 +517,8 @@ Use it in an animation: ``` When the clock time is /t/ { - Wish $this draws a circle with offset [list [expr {sin($t) * 50}] 0] + set x [format "%.3fcm" [expr {sin($t) * 5.0}]] + Wish $this draws a circle with offset [list $x 0cm] radius 1.2cm } ``` 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..6b040d6d 100644 --- a/builtin-programs/decorations/label.folk +++ b/builtin-programs/decorations/label.folk @@ -1,34 +1,50 @@ +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 [list [expr {$width / 2.0}] [expr {$height / 2.0}]] + dict create \ + position $position \ + scale $scale \ + anchor center \ + font "PTSans-Regular" +} + When /thing/ has resolved geometry /geom/ { 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 width [dict get $geom width] + set height [dict get $geom height] + set options [drawLabelDefaultOptions $text $width $height] + if {[dict exists $geom top] && + [dict exists $geom tagSize] && + [dict exists $geom bottom]} { + dict set options position \ + [list [expr {$width / 2.0}] \ + [expr {[dict get $geom top] + [dict get $geom tagSize] + [dict get $geom bottom] / 2.0}]] } - - # 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] + 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 } } diff --git a/builtin-programs/decorations/outline.folk b/builtin-programs/decorations/outline.folk index c663af26..94699c69 100644 --- a/builtin-programs/decorations/outline.folk +++ b/builtin-programs/decorations/outline.folk @@ -1,13 +1,30 @@ -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} { + set points [list [list 0 0] \ + [list $width 0] \ + [list $width $height] \ + [list 0 $height]] + lappend points [lindex $points 0] + return $points +} + +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 /thing/ has resolved geometry /geom/ &\ + /someone/ wishes /thing/ is outlined with /...options/ { + if {![info exists options]} { set options [dict create] } + set color [dict getdef $options color white] + set outlineWidth [drawPhysicalLength \ + [dict getdef $options width [dict getdef $options thickness 0.1cm]]] + set layer [dict getdef $options layer 2] Wish to draw a line onto $thing with \ - points $points width 0.01 color $color + points [drawOutlinePoints [dict get $geom width] [dict get $geom height]] \ + width $outlineWidth color $color layer $layer } diff --git a/builtin-programs/demos.folk b/builtin-programs/demos.folk index 192afd23..67810ecc 100644 --- a/builtin-programs/demos.folk +++ b/builtin-programs/demos.folk @@ -24,7 +24,8 @@ 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] + set x [format "%.3fcm" [expr {sin($t) * 5.0}]] + Wish $this draws a circle with offset [list $x 0cm] radius 1.2cm } } 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/circle.folk b/builtin-programs/draw/circle.folk index a67b3e4e..9963b143 100644 --- a/builtin-programs/draw/circle.folk +++ b/builtin-programs/draw/circle.folk @@ -29,19 +29,36 @@ Wish the GPU compiles pipeline "circle" { When the color map is /colorMap/ &\ /p/ has canvas /id/ with /...wiOptions/ &\ - /p/ has canvas projection /surfaceToClip/ &\ - /someone/ wishes to draw a circle 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 color [dict get $options color] - set color [dict getdef $colorMap $color $color] - set filled [dict getdef $options filled false] - - set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] - Wish the GPU draws pipeline "circle" onto canvas $id with arguments \ - [list $wiResolution $surfaceToClip \ - $center $radius $thickness $color [expr {$filled eq false ? 0 : 1}]] + /p/ has canvas projection /surfaceToClip/ { + + set query [list /someone/ wishes to draw a circle onto $p with /...options/] + When the collected results for $query are /results/ { + set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] + set instancesByLayer [dict create] + + foreach result $results { + set options [dict get $result 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 getdef $options thickness 0] + set color [dict get $options color] + set color [dict getdef $colorMap $color $color] + set filled [dict getdef $options filled false] + set layer [dict getdef $options layer 0] + + dict lappend instancesByLayer $layer \ + [list $wiResolution $surfaceToClip \ + $center $radius $thickness $color \ + [expr {$filled eq false ? 0 : 1}]] + } + + dict for {layer instances} $instancesByLayer { + Wish the GPU draws pipeline "circle" onto canvas $id \ + with instances $instances layer $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..eb1c773c 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -10,27 +10,35 @@ 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/ { dict with options { if {![info exists layer]} { set layer 0 } set color [dict getdef $colorMap $color $color] - Wish the GPU draws pipeline "fillTriangle" onto canvas $id with arguments \ - [list $surfaceToClip $p1 $p2 $p3 $color] layer $layer - Wish the GPU draws pipeline "fillTriangle" onto canvas $id with arguments \ - [list $surfaceToClip $p0 $p1 $p3 $color] layer $layer + Wish the GPU draws pipeline "fillTriangle" onto canvas $id with instances \ + [list \ + [list $surfaceToClip $p1 $p2 $p3 $color] \ + [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 +47,45 @@ 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 resolved geometry /geom/ { + if {![info exists options]} { set options {color white} } + 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 } +When /someone/ wishes /page/ draws fill with /...options/ { + if {![info exists options]} { set options {color white} } + Wish $page is filled with {*}$options } -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 } diff --git a/builtin-programs/draw/image.folk b/builtin-programs/draw/image.folk index c159c006..373896d5 100644 --- a/builtin-programs/draw/image.folk +++ b/builtin-programs/draw/image.folk @@ -35,6 +35,37 @@ 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 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,10 +76,7 @@ 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 im [imageDownloadUrl $impath] } set path [expr {[string index $im 0] eq "/" ? $im : "$::env(HOME)/folk-images/$im"}] @@ -70,6 +98,19 @@ When the collected results for {/loader/ is an image loader} are /loaders/ { Claim the image loader is [fn loadImage] } +fn drawImageScalar {value geom axis} { + drawRelativePhysicalLength $value \ + [dict get $geom width] [dict get $geom height] $axis draw/image +} + +fn drawImagePoint {point geom} { + if {[llength $point] != 2} { + error "draw/image: expected a 2D point, got $point" + } + list [drawImageScalar [lindex $point 0] $geom x] \ + [drawImageScalar [lindex $point 1] $geom y] +} + Wish the GPU compiles pipeline "image" { {vec2 viewport mat3 surfaceToClip sampler2D image vec2 a vec2 b vec2 c vec2 d} { @@ -173,13 +214,24 @@ When the image library is /imageLib/ &\ if {$derivedHeight > $geomHeight} { set width $($geomHeight / $derivedHeight * $width) } - Wish to draw an image onto $p with \ + set drawOptions [dict create \ image $im \ - position [dict getdef $options position [list 0 0]] \ - anchor topleft width [dict getdef $options width $width] + position [drawImagePoint [dict getdef $options position {0cm 0cm}] $geom] \ + anchor [dict getdef $options anchor topleft]] + if {[dict exists $options width]} { + dict set drawOptions width [drawImageScalar [dict get $options width] $geom width] + } + if {[dict exists $options height]} { + dict set drawOptions height [drawImageScalar [dict get $options height] $geom height] + } + if {![dict exists $drawOptions width] && ![dict exists $drawOptions height]} { + dict set drawOptions width $width + } + + Wish to draw an image onto $p with {*}$drawOptions } When /someone/ wishes /p/ displays image /im/ { - Wish $p displays image $im with scale 1.0 + Wish $p displays image $im with position {0cm 0cm} } diff --git a/builtin-programs/draw/line.folk b/builtin-programs/draw/line.folk index 96215309..3da3fa7f 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,17 +25,47 @@ 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); } } +fn drawLineClosed {points} { + expr {[llength $points] > 2 && [lindex $points 0] eq [lindex $points end]} +} + +fn drawLineCapFlags {index segmentCount closed capStyle} { + switch -- $capStyle { + square - flat - butt { + return {1.0 1.0} + } + round - rounded { + if {$closed} { + return {0.0 0.0} + } + + return [list [expr {$index == 0 ? 1.0 : 0.0}] \ + [expr {$index == ($segmentCount - 1) ? 1.0 : 0.0}]] + } + default { + error "draw/line: unknown cap style $capStyle" + } + } +} + When the color map is /colorMap/ &\ /p/ has canvas /id/ with /...wiOptions/ &\ /p/ has canvas projection /surfaceToClip/ &\ @@ -41,13 +76,23 @@ When the color map is /colorMap/ &\ set width [dict get $options width] set color [dict get $options color] set color [dict getdef $colorMap $color $color] + set caps [dict getdef $options caps [dict getdef $options cap round]] 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] + set segmentCount [expr {$numPoints - 1}] + set closed [drawLineClosed $points] + + for {set i 0} {$i < $segmentCount} {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 + lassign [drawLineCapFlags $i $segmentCount $closed $caps] capFrom capTo + + 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..8b002a59 --- /dev/null +++ b/builtin-programs/draw/shapes.folk @@ -0,0 +1,338 @@ +set drawShapeSides [dict create \ + triangle 3 square 4 pentagon 5 hexagon 6 septagon 7 octagon 8 nonagon 9] + +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 width height axis} { + drawRelativePhysicalLength $value $width $height $axis draw/shapes +} + +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" + } + set width [dict get $geom width] + set height [dict get $geom height] + list [drawShapeScalar [lindex $point 0] $width $height x] \ + [drawShapeScalar [lindex $point 1] $width $height y] +} + +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 $height x] 0] + } + } + } + + if {[llength $offset] == 2} { + set dir [lindex $offset 0] + set amount [lindex $offset 1] + switch -- $dir { + right { return [list [drawShapeScalar $amount $width $height x] 0] } + left { + set value [drawShapeScalar $amount $width $height x] + return [list [expr {-$value}] 0] + } + down { return [list 0 [drawShapeScalar $amount $width $height y]] } + up { + set value [drawShapeScalar $amount $width $height y] + return [list 0 [expr {-$value}]] + } + default { + return [list [drawShapeScalar $dir $width $height x] \ + [drawShapeScalar $amount $width $height y]] + } + } + } + + 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 width [dict get $geom width] + set height [dict get $geom height] + set x [drawShapeScalar [dict getdef $options x 50%] $width $height x] + set y [drawShapeScalar [dict getdef $options y 50%] $width $height y] + 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 drawShapeLengthOption {options key default width height axis} { + if {[dict exists $options $key]} { + return [drawShapeScalar [dict get $options $key] $width $height $axis] + } + return $default +} + +fn drawShapeRadius {options default width height} { + if {[dict exists $options diameter]} { + return [expr {[drawShapeScalar [dict get $options diameter] $width $height min] / 2.0}] + } + drawShapeLengthOption $options radius $default $width $height min +} + +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 +} + +When /someone/ wishes /p/ draws a /shape/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + if {![info exists options]} { return } + set shape [drawShapeCanonical $shape $options] + set center [drawShapePosition $options $geom] + set color [dict getdef $options color white] + set filled [drawTruthy [dict getdef $options filled false]] + set geomWidth [dict get $geom width] + set geomHeight [dict get $geom height] + set thickness [drawShapeLengthOption $options thickness 0.002 \ + $geomWidth $geomHeight min] + set layer [dict getdef $options layer 1] + set radians [drawShapeRadians $options] + + if {$shape eq "circle"} { + set radius [drawShapeRadius $options 0.025 $geomWidth $geomHeight] + 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.025 $geomWidth $geomHeight] + set size [drawShapeLengthOption $options size [expr {$radius * 2.0}] \ + $geomWidth $geomHeight min] + set rectWidth [drawShapeLengthOption $options width $size \ + $geomWidth $geomHeight width] + set rectHeight [drawShapeLengthOption $options height $rectWidth \ + $geomWidth $geomHeight height] + 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.025 $geomWidth $geomHeight] + 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/ { + if {![info exists options]} { return } + 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/ { + if {![info exists options]} { return } + set position [drawShapePosition $options $geom] + set color [dict getdef $options color white] + set scale [drawShapeLengthOption $options scale 0.01 \ + [dict get $geom width] [dict get $geom height] min] + 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/ { + if {![info exists options]} { return } + set points [drawShapePathPoints $points $geom $options] + set color [dict getdef $options color white] + set geomWidth [dict get $geom width] + set geomHeight [dict get $geom height] + set width [drawShapeLengthOption $options thickness 0.002 \ + $geomWidth $geomHeight min] + set width [drawShapeLengthOption $options width $width \ + $geomWidth $geomHeight min] + set layer [dict getdef $options layer 1] + set dashed [drawTruthy [dict getdef $options dashed false]] + + if {$dashed} { + set dashlength [drawShapeLengthOption $options dashlength 0.01 \ + $geomWidth $geomHeight min] + set dashoffset [drawShapeLengthOption $options dashoffset 0 \ + $geomWidth $geomHeight min] + 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 a polyline /points/ { + Wish $p draws a polyline $points with color white +} + +When /someone/ wishes /p/ draws points /points/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + if {![info exists options]} { return } + set points [drawShapePathPoints $points $geom $options] + set geomWidth [dict get $geom width] + set geomHeight [dict get $geom height] + set radius [drawShapeRadius $options 0.003 $geomWidth $geomHeight] + set thickness [drawShapeLengthOption $options thickness 0.001 \ + $geomWidth $geomHeight min] + set color [dict getdef $options color white] + set filled [drawTruthy [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 points /points/ { + Wish $p draws points $points with color white +} + +When /someone/ wishes /p/ draws a set of points /points/ with /...options/ { + if {![info exists options]} { return } + Wish $p draws points $points with {*}$options +} + +When /someone/ wishes /p/ draws a set of points /points/ { + Wish $p draws points $points +} + +# Keep the builtin demo tiny and self-contained so the public shapes API +# stays reviewable without dragging along a large stress gallery. +Hold! -on builtin-programs/draw/shapes.folk -key draw-shapes-demo-code \ + Claim builtin-programs/draw/shapes.folk has demo code { +Wish $this draws a rect with width 6cm height 4cm color {0.10 0.16 0.22 0.92} filled true +Wish $this draws a circle with position {50% 50%} radius 1.2cm color {0.92 0.74 0.28 1.0} filled false thickness 0.25cm +Wish $this draws a polyline {{20% 75%} {50% 25%} {80% 75%}} with color {0.52 0.85 1.0 1.0} width 0.18cm +Wish $this draws points {{20% 75%} {50% 25%} {80% 75%}} with color white radius 0.22cm thickness 0cm filled true +Wish $this draws text "shapes" with position {50% 88%} anchor center color white scale 1.1cm +} 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..8c674659 100644 --- a/builtin-programs/editor.folk +++ b/builtin-programs/editor.folk @@ -14,15 +14,19 @@ When /k/ is a keyboard with /...opts/ &\ Claim $editor has resolved geometry $geom } When the quad library is /quadLib/ & $k has quad /q/ { - Claim $editor has quad [$quadLib move $q up 105%] + set editorQuad [$quadLib move $q up 105%] + Claim $editor has quad $editorQuad + Claim $editor has drawing quad $editorQuad } 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 +77,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/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/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/title.folk b/builtin-programs/title.folk index 223bf6a7..2de5e9cc 100644 --- a/builtin-programs/title.folk +++ b/builtin-programs/title.folk @@ -4,11 +4,29 @@ # Wish $this is footnoted "This is a footnote" # Wish $this is right-margined "This is right-margined text" # Wish $this is left-margined "This is left-margined text" +# Any of those can also take text drawing options: +# Wish $this is left-margined "code" with font CourierPrimeCode padding 1cm When /thing/ has quad /quad/ { Claim -keep 50ms $thing has a quad } +When /someone/ wishes /thing/ is titled /text/ { + Wish $thing is titled $text with scale 36.0 +} + +When /someone/ wishes /thing/ is footnoted /text/ { + Wish $thing is footnoted $text with scale 36.0 +} + +When /someone/ wishes /thing/ is right-margined /text/ { + Wish $thing is right-margined $text with scale 36.0 +} + +When /someone/ wishes /thing/ is left-margined /text/ { + Wish $thing is left-margined $text with scale 36.0 +} + When the quad library is /quadLib/ &\ the pose library is /poseLib/ &\ the quad changer is /quadChange/ &\ @@ -24,7 +42,7 @@ When the quad library is /quadLib/ &\ right-margined right left left-margined left right } { - When the collected results for [list /someone/ wishes $thing is $label /text/] are /results/ { + When the collected results for [list /someone/ wishes $thing is $label /text/ with /...options/] are /results/ { set text [join [lmap result $results {dict get $result text}] "\n"] if {$text eq ""} { return } @@ -58,7 +76,19 @@ When the quad library is /quadLib/ &\ } } - set paddingMeters 0.02 + set drawOptions [dict create \ + scale 36.0 \ + anchor $textAnchor \ + layer 100] + foreach result $results { + set drawOptions [dict merge $drawOptions [dict getdef $result options [dict create]]] + } + + set padding [dict getdef $drawOptions padding 2cm] + if {[dict exists $drawOptions padding]} { + dict unset drawOptions padding + } + set paddingMeters [drawPhysicalLength $padding] set offset [scale $paddingMeters [unitLengthVector $physicalDir]] set physicalPos [add $physicalPos $offset] @@ -70,11 +100,12 @@ When the quad library is /quadLib/ &\ set dispTop [vec2 sub $dispTopRight $dispTopLeft] set dispRadians [expr {-atan2([lindex $dispTop 1], [lindex $dispTop 0])}] - Wish to draw text onto $disp with \ - position $dispPosition \ - scale 36.0 radians $dispRadians anchor $textAnchor \ - text $text + dict set drawOptions position $dispPosition + dict set drawOptions radians $dispRadians + dict set drawOptions text $text + + Wish to draw text onto $disp with {*}$drawOptions } } } -} \ No newline at end of file +} diff --git a/builtin-programs/web/new.folk b/builtin-programs/web/new.folk index 2294956b..ba6eb496 100644 --- a/builtin-programs/web/new.folk +++ b/builtin-programs/web/new.folk @@ -237,8 +237,9 @@ Wish the web server handles route "/new" with nav "" [add [matmul $R [sub $v4 $centroid]] $centroid] \ ] - Claim ${program} has quad \ - [$quadLib create "display $disp" $rotated_vertices] + set programQuad [$quadLib create "display $disp" $rotated_vertices] + Claim ${program} has quad $programQuad + Claim ${program} has drawing quad $programQuad `); } function handleSave() { diff --git a/lib/math.tcl b/lib/math.tcl index 300e1b04..0ffb4220 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -3,6 +3,103 @@ # This file provides global math datatypes and utilities. # +set ::PI 3.142 +set ::TAU 6.283 + +proc drawTruthy {value} { + expr {$value in {1 true yes on}} +} + +proc drawPhysicalLength {value} { + if {[llength $value] != 1} { + error "draw: 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 {$unit eq ""} { + error "draw: physical length $value must include a unit: mm, cm, or m" + } + if {![string is double -strict $amount]} { + error "draw: 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: invalid physical unit $unit" } + } +} + +proc drawPhysicalPoint {point} { + if {[llength $point] != 2} { + error "draw: expected a 2D physical point, got $point" + } + list [drawPhysicalLength [lindex $point 0]] \ + [drawPhysicalLength [lindex $point 1]] +} + +proc drawPhysicalPoints {points} { + lmap point $points { + drawPhysicalPoint $point + } +} + +proc drawPhysicalAxisExtent {width height axis} { + switch -- $axis { + x - width - horizontal { return $width } + y - height - vertical { return $height } + max { return [expr {$width > $height ? $width : $height}] } + min - radius - scale - thickness - default { + return [expr {$width < $height ? $width : $height}] + } + } +} + +proc drawRelativePhysicalLength {value width height axis {context draw}} { + if {[llength $value] != 1} { + error "$context: expected a scalar length, got $value" + } + + set override "" + if {[regexp {^([-+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+))%(min|max|x|y|width|height)?$} \ + $value -> pct override]} { + if {$override ne ""} { + set axis $override + } + return [expr {double($pct) / 100.0 * [drawPhysicalAxisExtent $width $height $axis]}] + } + + drawPhysicalLength $value +} + +proc drawMeterLength {value} { + format "%sm" $value +} + +proc drawMeterPoint {point} { + lmap value $point { + drawMeterLength $value + } +} + +proc drawMeterRectPoints {x y width height} { + list \ + [drawMeterPoint [list $x $y]] \ + [drawMeterPoint [list [expr {$x + $width}] $y]] \ + [drawMeterPoint [list [expr {$x + $width}] [expr {$y + $height}]]] \ + [drawMeterPoint [list $x [expr {$y + $height}]]] +} + namespace eval ::vec2 { proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] diff --git a/test/decorations.folk b/test/decorations.folk new file mode 100644 index 00000000..95ed1360 --- /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/line.folk + +assert {[drawOutlinePoints 0.2 0.1] eq {{0 0} {0.2 0} {0.2 0.1} {0 0.1} {0 0}}} +assert {[drawLineClosed {{0 0} {1 0} {0 0}}]} +assert {![drawLineClosed {{0 0} {1 0}}]} +assert {[drawLineCapFlags 0 4 true round] eq {0.0 0.0}} +assert {[drawLineCapFlags 0 4 false round] eq {1.0 0.0}} +assert {[drawLineCapFlags 3 4 false round] eq {0.0 1.0}} +assert {[drawLineCapFlags 1 4 true square] eq {1.0 1.0}} +assert {[drawMeterPoint {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.1 0.05}} +assert {abs([dict get $options scale] - 0.02) < 1e-9} +assert {[dict get $options anchor] eq "center"} +assert {[dict get $options font] eq "PTSans-Regular"} + +set thing test-thing +set plainThing test-plain-outline + +Assert! $thing has resolved geometry {width 0.2 height 0.1} +Assert! $plainThing has resolved geometry {width 0.3 height 0.2} + +Wish $thing is labelled "hello" with color cyan +Wish $thing is outlined red with thickness 0.5cm 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 $thing 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 $thing 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 exists $drawOptions caps]} +assert {[dict get $drawOptions points] eq {{0 0} {0.2 0} {0.2 0.1} {0 0.1} {0 0}}} + +set lineDraws [Query! /someone/ wishes to draw a line onto $plainThing 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.001) < 1e-9} +assert {![dict exists $drawOptions caps]} +assert {[dict get $drawOptions points] eq {{0 0} {0.3 0} {0.3 0.2} {0 0.2} {0 0}}} + +Exit! 0 diff --git a/test/draw-fill.folk b/test/draw-fill.folk new file mode 100644 index 00000000..3999b658 --- /dev/null +++ b/test/draw-fill.folk @@ -0,0 +1,37 @@ +source builtin-programs/draw/fill.folk + +Assert! the color map is {white {1 1 1 1} teal {0 0.5 0.5 1}} +sleep 0.2 + +set geom {width 0.2 height 0.1} +set filledPage draw-fill-default-page +set aliasPage draw-fill-alias-page +set colorPage draw-fill-color-page + +Assert! $filledPage has resolved geometry $geom +Assert! $aliasPage has resolved geometry $geom +Assert! $colorPage has resolved geometry $geom + +Wish $filledPage is filled +Wish $aliasPage draws fill +Wish $colorPage draws fill with color teal layer 3 +sleep 0.5 + +set filledDraws [Query! /someone/ wishes to draw a polygon onto $filledPage with /...drawOptions/] +assert {[llength $filledDraws] == 1} +set drawOptions [dict get [lindex $filledDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "white"} +assert {[dict get $drawOptions points] eq {{0 0} {0.2 0} {0.2 0.1} {0 0.1}}} + +set aliasDraws [Query! /someone/ wishes to draw a polygon onto $aliasPage with /...drawOptions/] +assert {[llength $aliasDraws] == 1} +set drawOptions [dict get [lindex $aliasDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "white"} + +set colorDraws [Query! /someone/ wishes to draw a polygon onto $colorPage with /...drawOptions/] +assert {[llength $colorDraws] == 1} +set drawOptions [dict get [lindex $colorDraws 0] drawOptions] +assert {[dict get $drawOptions color] eq "teal"} +assert {[dict get $drawOptions layer] == 3} + +Exit! 0 diff --git a/test/draw-image-url.folk b/test/draw-image-url.folk new file mode 100644 index 00000000..8fd499a5 --- /dev/null +++ b/test/draw-image-url.folk @@ -0,0 +1,60 @@ +source builtin-programs/collect.folk +source builtin-programs/draw/color-map.folk +source builtin-programs/draw/image.folk +source builtin-programs/image/image-lib.folk + +fn testImageLoader {path} { + if {[string match "*.png" $path]} { + return [$imageLib imageNew 2 1 3 123] + } + return "" +} +Claim [fn testImageLoader] is an image loader + +When /someone/ wishes the GPU loads image /im/ as texture { + Claim the GPU has loaded image $im as texture 99 +} + +proc testImageUrlCachePath {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 +} + +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} +assert {[catch {drawImagePoint {0 0} {width 0.2 height 0.1}} unitlessError]} +assert {[string match {*must include a unit*} $unitlessError]} +assert {[drawImagePoint {1cm 2cm} {width 0.2 height 0.1}] eq {0.01 0.02}} + +sleep 0.2 +set url "https://example.invalid/folk-test-image.png" +set cachedPath [testImageUrlCachePath $url] +set cachedFile [open $cachedPath w] +puts -nonewline $cachedFile "x" +close $cachedFile + +Wish image-page displays image $url + +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 {abs([lindex $arguments 3 0]) < 1e-9} +assert {abs([lindex $arguments 3 1]) < 1e-9} + +Exit! 0 diff --git a/test/draw-shapes.folk b/test/draw-shapes.folk new file mode 100644 index 00000000..8a0e74f6 --- /dev/null +++ b/test/draw-shapes.folk @@ -0,0 +1,81 @@ +source builtin-programs/draw/shapes.folk + +proc nearly {actual expected} { + expr {abs($actual - $expected) < 1e-9} +} + +set geom {width 0.2 height 0.1} +assert {[catch {drawShapeScalar 5 0.2 0.1 min} unitlessError]} +assert {[string match {*must include a unit*} $unitlessError]} +assert {[nearly [drawShapeScalar 5cm 0.2 0.1 min] 0.05]} +assert {[nearly [drawShapeScalar 50% 0.2 0.1 x] 0.1]} + +foreach page { + draw-shapes-default-circle + draw-shapes-default-rect + draw-shapes-default-octagon + draw-shapes-default-text + draw-shapes-default-polyline + draw-shapes-default-points + draw-shapes-explicit-circle +} { + Assert! $page has resolved geometry $geom +} + +Wish draw-shapes-default-circle draws a circle +Wish draw-shapes-default-rect draws a rect +Wish draw-shapes-default-octagon draws an octagon +Wish draw-shapes-default-text draws text "hello" +Wish draw-shapes-default-polyline draws a polyline {{-2.5cm 0cm} {2.5cm 0cm}} +Wish draw-shapes-default-points draws points {{0cm 0cm}} +Wish draw-shapes-explicit-circle draws a circle with \ + position {25% 75%} diameter 5cm color cyan filled false + +sleep 0.5 + +set circleDraws [Query! /someone/ wishes to draw a circle onto draw-shapes-default-circle with /...drawOptions/] +assert {[llength $circleDraws] == 1} +set drawOptions [dict get [lindex $circleDraws 0] drawOptions] +assert {[dict get $drawOptions center] eq {0.1 0.05}} +assert {[nearly [dict get $drawOptions radius] 0.025]} +assert {[dict get $drawOptions color] eq "white"} +assert {[drawTruthy [dict get $drawOptions filled]]} + +set rectDraws [Query! /someone/ wishes to draw a polygon onto draw-shapes-default-rect with /...drawOptions/] +assert {[llength $rectDraws] == 1} +set rectPoints [dict get [dict get [lindex $rectDraws 0] drawOptions] points] +assert {[nearly [lindex $rectPoints 0 0] 0.075]} +assert {[nearly [lindex $rectPoints 2 0] 0.125]} + +set octagonDraws [Query! /someone/ wishes to draw a polygon onto draw-shapes-default-octagon with /...drawOptions/] +assert {[llength $octagonDraws] == 1} +assert {[llength [dict get [dict get [lindex $octagonDraws 0] drawOptions] points]] == 8} + +set textDraws [Query! /someone/ wishes to draw text onto draw-shapes-default-text with /...drawOptions/] +assert {[llength $textDraws] == 1} +set drawOptions [dict get [lindex $textDraws 0] drawOptions] +assert {[dict get $drawOptions position] eq {0.1 0.05}} +assert {[dict get $drawOptions color] eq "white"} + +set polylineDraws [Query! /someone/ wishes to draw a line onto draw-shapes-default-polyline with /...drawOptions/] +assert {[llength $polylineDraws] == 1} +set drawOptions [dict get [lindex $polylineDraws 0] drawOptions] +assert {[nearly [dict get $drawOptions width] 0.002]} +assert {[dict get $drawOptions points] eq {{0.075 0.05} {0.125 0.05}}} + +set pointDraws [Query! /someone/ wishes to draw a circle onto draw-shapes-default-points with /...drawOptions/] +assert {[llength $pointDraws] == 1} +set drawOptions [dict get [lindex $pointDraws 0] drawOptions] +assert {[dict get $drawOptions center] eq {0.1 0.05}} +assert {[nearly [dict get $drawOptions radius] 0.003]} +assert {[drawTruthy [dict get $drawOptions filled]]} + +set explicitDraws [Query! /someone/ wishes to draw a circle onto draw-shapes-explicit-circle with /...drawOptions/] +assert {[llength $explicitDraws] == 1} +set drawOptions [dict get [lindex $explicitDraws 0] drawOptions] +assert {[dict get $drawOptions center] eq {0.05 0.075}} +assert {[nearly [dict get $drawOptions radius] 0.025]} +assert {[dict get $drawOptions color] eq "cyan"} +assert {![drawTruthy [dict get $drawOptions filled]]} + +Exit! 0 diff --git a/test/draw-text-anchor.folk b/test/draw-text-anchor.folk new file mode 100644 index 00000000..10d99509 --- /dev/null +++ b/test/draw-text-anchor.folk @@ -0,0 +1,64 @@ +source builtin-programs/collect.folk +source builtin-programs/image/image-lib.folk +source builtin-programs/draw/color-map.folk + +When the image library is /imageLib/ { + fn fakeLoadImage {path} { + $imageLib imageNew 8 8 4 1 + } + Claim the image loader is [fn fakeLoadImage] +} + +source builtin-programs/draw/text.folk + +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 From 710085a486c846d1d211e5a037a2a59e22bb4698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 17:38:50 -0400 Subject: [PATCH 2/9] Shrink points-at drawing helper --- builtin-programs/points-at.folk | 215 +++++++++++++++++++------------- test/points-at.folk | 36 ++++++ 2 files changed, 164 insertions(+), 87 deletions(-) create mode 100644 test/points-at.folk diff --git a/builtin-programs/points-at.folk b/builtin-programs/points-at.folk index 5de70d0d..46f6d947 100644 --- a/builtin-programs/points-at.folk +++ b/builtin-programs/points-at.folk @@ -1,3 +1,96 @@ +fn pointsAtVecAdd {a b} { + list [expr {[lindex $a 0] + [lindex $b 0]}] \ + [expr {[lindex $a 1] + [lindex $b 1]}] \ + [expr {[lindex $a 2] + [lindex $b 2]}] +} + +fn pointsAtVecScale {s v} { + list [expr {$s * [lindex $v 0]}] \ + [expr {$s * [lindex $v 1]}] \ + [expr {$s * [lindex $v 2]}] +} + +fn pointsAtVecMix {a b t} { + pointsAtVecAdd $a [pointsAtVecScale $t [list \ + [expr {[lindex $b 0] - [lindex $a 0]}] \ + [expr {[lindex $b 1] - [lindex $a 1]}] \ + [expr {[lindex $b 2] - [lindex $a 2]}]]] +} + +fn pointsAtLength {value geom axis} { + set width [dict get $geom width] + set height [dict get $geom height] + if {![catch { + drawRelativePhysicalLength $value $width $height $axis points-at + } meters]} { + return $meters + } + + if {![string is double -strict $value]} { + error "points-at: expected length as a multiplier, unit length, or percentage, got $value" + } + expr {double($value) * [drawPhysicalAxisExtent $width $height $axis]} +} + +fn pointsAtLocalSegment {geom direction length} { + set width [dict get $geom width] + set height [dict get $geom height] + + switch -- $direction { + up { + set reach [pointsAtLength $length $geom height] + return [dict create \ + from [list [expr {$width / 2.0}] 0] \ + to [list [expr {$width / 2.0}] [expr {-$reach}]] \ + color blue] + } + down { + set reach [pointsAtLength $length $geom height] + return [dict create \ + from [list [expr {$width / 2.0}] $height] \ + to [list [expr {$width / 2.0}] [expr {$height + $reach}]] \ + color white] + } + left { + set reach [pointsAtLength $length $geom width] + return [dict create \ + from [list 0 [expr {$height / 2.0}]] \ + to [list [expr {-$reach}] [expr {$height / 2.0}]] \ + color gold] + } + right { + set reach [pointsAtLength $length $geom width] + return [dict create \ + from [list $width [expr {$height / 2.0}]] \ + to [list [expr {$width + $reach}] [expr {$height / 2.0}]] \ + color red] + } + default { + error "points-at: invalid direction $direction" + } + } +} + +fn pointsAtQuadPoint {vertices geom point} { + lassign $vertices topLeft topRight bottomRight bottomLeft + set width [dict get $geom width] + set height [dict get $geom height] + set u [expr {[lindex $point 0] / $width}] + set v [expr {[lindex $point 1] / $height}] + + set top [pointsAtVecMix $topLeft $topRight $u] + set bottom [pointsAtVecMix $bottomLeft $bottomRight $u] + pointsAtVecMix $top $bottom $v +} + +fn pointsAtDisplayPoint {poseLib displayIntrinsics displayWidth displayHeight point} { + $poseLib project $displayIntrinsics $displayWidth $displayHeight $point +} + +fn pointsAtPointerKey {rect direction disp} { + list points-at pointer $disp $rect $direction +} + 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 @@ -13,112 +106,60 @@ When the quad library is /quadLib/ &\ the quad changer is /quadChange/ &\ display /disp/ has width /displayWidth/ height /displayHeight/ &\ display /disp/ has intrinsics /displayIntrinsics/ &\ - /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 + /someone/ wishes /rect/ points /direction/ with length /l/ &\ + /rect/ has resolved geometry /geom/ &\ + /rect/ has quad /quad/ { fn quadChange - set scale $l - - 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]] - - set from $topCenter - set to [add $topCenter $up] - set color blue - - } 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 color gold - - } 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 color red - - } 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 color white - - } 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 segment [pointsAtLocalSegment $geom $direction $l] + set displayQuad [quadChange $quad "display $disp"] + set displayVertices [$quadLib vertices $displayQuad] + + set from [pointsAtDisplayPoint $poseLib $displayIntrinsics \ + $displayWidth $displayHeight \ + [pointsAtQuadPoint $displayVertices $geom [dict get $segment from]]] + set to [pointsAtDisplayPoint $poseLib $displayIntrinsics \ + $displayWidth $displayHeight \ + [pointsAtQuadPoint $displayVertices $geom [dict get $segment to]]] + set color [dict get $segment color] - # Downproject the whisker to screen-space. - set from [$poseLib project $displayIntrinsics \ - $displayWidth $displayHeight \ - $from] - set to [$poseLib project $displayIntrinsics \ - $displayWidth $displayHeight \ - $to] + Claim -keep 16ms $rect points $direction on $disp from $from to $to color $color - When /target/ has quad /q2/ { + When /target/ has quad /targetQuad/ { if {$target eq $rect} { return } - set displayVertices [lmap v [$quadLib vertices [quadChange $q2 "display $disp"]] { - $poseLib project $displayIntrinsics \ - $displayWidth $displayHeight $v + set targetDisplayVertices [lmap vertex [$quadLib vertices [quadChange $targetQuad "display $disp"]] { + pointsAtDisplayPoint $poseLib $displayIntrinsics \ + $displayWidth $displayHeight $vertex }] - - if {[::math::geometry::pointInsidePolygon $to $displayVertices]} { + if {[::math::geometry::pointInsidePolygon $to $targetDisplayVertices]} { + Claim -keep 16ms $rect points $direction at $target on $disp Claim -keep 50ms $rect points $direction at $target Claim -keep 50ms $rect points $direction with length $l at $target + } + } +} +When /rect/ points /direction/ on /disp/ from /from/ to /to/ color /color/ { + When the collected results for [list /someone/ claims $rect points $direction at /target/ on $disp] \ + with settle 16ms are /hits/ { + set filled [expr {[llength $hits] > 0}] + if {$filled} { set color green - Hold! -keep 16ms -key [list $rect pointer] { - Wish to draw a line onto $disp with \ - points [list $from $to] width 4 \ - color $color - Wish to draw a circle onto $disp with \ - center $to radius 10 thickness 5 \ - color $color filled true - } } - } - When /nobody/ claims $rect points /anything/ at /anything/ { - Hold! -keep 16ms -key [list $rect pointer] { + Hold! -keep 16ms -key [pointsAtPointerKey $rect $direction $disp] { Wish to draw a line onto $disp with \ points [list $from $to] width 4 \ - color $color + color $color layer 200 Wish to draw a circle onto $disp with \ center $to radius 10 thickness 5 \ - color $color filled false + color $color filled $filled layer 201 } } -} - -On unmatch { - Hold! -key [list $rect pointer] {} -} + On unmatch { + Hold! -key [pointsAtPointerKey $rect $direction $disp] {} + } } diff --git a/test/points-at.folk b/test/points-at.folk new file mode 100644 index 00000000..ded4e630 --- /dev/null +++ b/test/points-at.folk @@ -0,0 +1,36 @@ +source builtin-programs/points-at.folk + +proc nearly {actual expected} { + expr {abs($actual - $expected) < 1e-9} +} + +proc nearlyPoint {actual expected} { + foreach a $actual e $expected { + if {![nearly $a $e]} { + return false + } + } + return true +} + +set geom {width 0.2 height 0.1} + +assert {[nearly [pointsAtLength 1 $geom height] 0.1]} +assert {[nearly [pointsAtLength 50% $geom width] 0.1]} +assert {[nearly [pointsAtLength 5cm $geom height] 0.05]} + +set segment [pointsAtLocalSegment $geom up 1] +assert {[dict get $segment color] eq "blue"} +assert {[nearlyPoint [dict get $segment from] {0.1 0}]} +assert {[nearlyPoint [dict get $segment to] {0.1 -0.1}]} + +set segment [pointsAtLocalSegment $geom right 50%] +assert {[dict get $segment color] eq "red"} +assert {[nearlyPoint [dict get $segment from] {0.2 0.05}]} +assert {[nearlyPoint [dict get $segment to] {0.3 0.05}]} + +set vertices {{0 0 0} {0.2 0 0} {0.2 0.1 0} {0 0.1 0}} +assert {[nearlyPoint [pointsAtQuadPoint $vertices $geom {0.1 0.05}] {0.1 0.05 0}]} +assert {[nearlyPoint [pointsAtQuadPoint $vertices $geom {0.1 -0.1}] {0.1 -0.1 0}]} + +Exit! 0 From 1073f29ccad9f54c4f47e7cb1a2b8d786352b888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 17:42:25 -0400 Subject: [PATCH 3/9] Simplify title drawing helper --- builtin-programs/title.folk | 146 ++++++++++++++++++++---------------- test/title.folk | 44 +++++++++++ 2 files changed, 126 insertions(+), 64 deletions(-) create mode 100644 test/title.folk diff --git a/builtin-programs/title.folk b/builtin-programs/title.folk index 2de5e9cc..ff4acb10 100644 --- a/builtin-programs/title.folk +++ b/builtin-programs/title.folk @@ -7,10 +7,6 @@ # Any of those can also take text drawing options: # Wish $this is left-margined "code" with font CourierPrimeCode padding 1cm -When /thing/ has quad /quad/ { - Claim -keep 50ms $thing has a quad -} - When /someone/ wishes /thing/ is titled /text/ { Wish $thing is titled $text with scale 36.0 } @@ -27,12 +23,78 @@ When /someone/ wishes /thing/ is left-margined /text/ { Wish $thing is left-margined $text with scale 36.0 } +fn titleDrawOptions {results textAnchor} { + set text [join [lmap result $results {dict get $result text}] "\n"] + if {$text eq ""} { return "" } + + set drawOptions [dict create \ + scale 36.0 \ + anchor $textAnchor \ + layer 100 \ + text $text] + foreach result $results { + set drawOptions [dict merge $drawOptions \ + [dict getdef $result options [dict create]]] + } + + set padding [dict getdef $drawOptions padding 2cm] + if {[dict exists $drawOptions padding]} { + dict unset drawOptions padding + } + + list [drawPhysicalLength $padding] $drawOptions +} + +fn titleEdgePosition {vertices edge paddingMeters} { + package require linalg + namespace import \ + ::math::linearalgebra::add \ + ::math::linearalgebra::sub \ + ::math::linearalgebra::scale \ + ::math::linearalgebra::unitLengthVector + + lassign $vertices 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] + } + default { + error "title: invalid edge $edge" + } + } + + set offset [scale $paddingMeters [unitLengthVector $physicalDir]] + add $physicalPos $offset +} + +fn titleDisplayRadians {poseLib displayIntrinsics displayWidth displayHeight topLeft topRight} { + set dispTopLeft [$poseLib project $displayIntrinsics \ + $displayWidth $displayHeight $topLeft] + set dispTopRight [$poseLib project $displayIntrinsics \ + $displayWidth $displayHeight $topRight] + set dispTop [vec2 sub $dispTopRight $dispTopLeft] + expr {-atan2([lindex $dispTop 1], [lindex $dispTop 0])} +} + 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 { + display /disp/ has intrinsics /displayIntrinsics/ { fn quadChange @@ -42,67 +104,23 @@ When the quad library is /quadLib/ &\ right-margined right left left-margined left right } { - When the collected results for [list /someone/ wishes $thing is $label /text/ with /...options/] are /results/ { - 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] - } - } - - set drawOptions [dict create \ - scale 36.0 \ - anchor $textAnchor \ - layer 100] - foreach result $results { - set drawOptions [dict merge $drawOptions [dict getdef $result options [dict create]]] - } - - set padding [dict getdef $drawOptions padding 2cm] - if {[dict exists $drawOptions padding]} { - dict unset drawOptions padding - } - set paddingMeters [drawPhysicalLength $padding] - 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])}] + When -atomically /thing/ has quad /q/ { + When the collected results for [list /someone/ wishes $thing is $label /text/ with /...options/] are /results/ { + set optionPair [titleDrawOptions $results $textAnchor] + if {$optionPair eq ""} { return } + lassign $optionPair paddingMeters drawOptions + + set vertices [$quadLib vertices [quadChange $q "display $disp"]] + lassign $vertices topLeft topRight bottomRight bottomLeft + + set physicalPos [titleEdgePosition $vertices $edge $paddingMeters] + set dispPosition [$poseLib project $displayIntrinsics \ + $displayWidth $displayHeight $physicalPos] + set dispRadians [titleDisplayRadians $poseLib $displayIntrinsics \ + $displayWidth $displayHeight $topLeft $topRight] dict set drawOptions position $dispPosition dict set drawOptions radians $dispRadians - dict set drawOptions text $text Wish to draw text onto $disp with {*}$drawOptions } diff --git a/test/title.folk b/test/title.folk new file mode 100644 index 00000000..9d4257e8 --- /dev/null +++ b/test/title.folk @@ -0,0 +1,44 @@ +source builtin-programs/title.folk + +proc nearly {actual expected} { + expr {abs($actual - $expected) < 1e-9} +} + +proc nearlyPoint {actual expected} { + foreach a $actual e $expected { + if {![nearly $a $e]} { + return false + } + } + return true +} + +set results [list \ + [dict create text hello options [dict create color cyan padding 1cm]] \ + [dict create text world options [dict create layer 7]]] +lassign [titleDrawOptions $results bottom] paddingMeters drawOptions + +assert {[nearly $paddingMeters 0.01]} +assert {[dict get $drawOptions text] eq "hello\nworld"} +assert {[dict get $drawOptions anchor] eq "bottom"} +assert {[dict get $drawOptions color] eq "cyan"} +assert {[dict get $drawOptions layer] == 7} +assert {![dict exists $drawOptions padding]} + +set vertices {{0 0 0} {0.2 0 0} {0.2 0.1 0} {0 0.1 0}} +assert {[nearlyPoint [titleEdgePosition $vertices top 0.02] {0.1 -0.02 0}]} +assert {[nearlyPoint [titleEdgePosition $vertices bottom 0.02] {0.1 0.12 0}]} +assert {[nearlyPoint [titleEdgePosition $vertices right 0.02] {0.22 0.05 0}]} +assert {[nearlyPoint [titleEdgePosition $vertices left 0.02] {-0.02 0.05 0}]} + +proc fakePoseLib {method intrinsics width height point} { + if {$method ne "project"} { + error "unexpected fake pose method $method" + } + lrange $point 0 1 +} + +assert {[nearly [titleDisplayRadians fakePoseLib {} 100 100 {0 0 0} {1 0 0}] 0.0]} +assert {[nearly [titleDisplayRadians fakePoseLib {} 100 100 {0 0 0} {0 1 0}] [expr {-atan2(1, 0)}]]} + +Exit! 0 From 5f6b5523fd2a08654517464ab6dd2566cf745fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 17:45:04 -0400 Subject: [PATCH 4/9] Trim drawing math helpers --- builtin-programs/points-at.folk | 19 ++++++++++- lib/math.tcl | 57 --------------------------------- test/decorations.folk | 2 -- test/points-at.folk | 2 ++ 4 files changed, 20 insertions(+), 60 deletions(-) diff --git a/builtin-programs/points-at.folk b/builtin-programs/points-at.folk index 46f6d947..0fe27d1e 100644 --- a/builtin-programs/points-at.folk +++ b/builtin-programs/points-at.folk @@ -91,6 +91,23 @@ fn pointsAtPointerKey {rect direction disp} { list points-at pointer $disp $rect $direction } +fn pointsAtPointInsidePolygon {point polygon} { + lassign $point testx testy + set inside 0 + set n [llength $polygon] + for {set i 0; set j [expr {$n - 1}]} {$i < $n} {set j $i; incr i} { + lassign [lindex $polygon $i] xi yi + lassign [lindex $polygon $j] xj yj + if { + (($yi > $testy) != ($yj > $testy)) && + ($testx < ($xj - $xi) * ($testy - $yi) / ($yj - $yi) + $xi) + } { + set inside [expr {!$inside}] + } + } + return $inside +} + 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 @@ -133,7 +150,7 @@ When the quad library is /quadLib/ &\ pointsAtDisplayPoint $poseLib $displayIntrinsics \ $displayWidth $displayHeight $vertex }] - if {[::math::geometry::pointInsidePolygon $to $targetDisplayVertices]} { + if {[pointsAtPointInsidePolygon $to $targetDisplayVertices]} { Claim -keep 16ms $rect points $direction at $target on $disp Claim -keep 50ms $rect points $direction at $target Claim -keep 50ms $rect points $direction with length $l at $target diff --git a/lib/math.tcl b/lib/math.tcl index 0ffb4220..28cbf822 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -40,20 +40,6 @@ proc drawPhysicalLength {value} { } } -proc drawPhysicalPoint {point} { - if {[llength $point] != 2} { - error "draw: expected a 2D physical point, got $point" - } - list [drawPhysicalLength [lindex $point 0]] \ - [drawPhysicalLength [lindex $point 1]] -} - -proc drawPhysicalPoints {points} { - lmap point $points { - drawPhysicalPoint $point - } -} - proc drawPhysicalAxisExtent {width height axis} { switch -- $axis { x - width - horizontal { return $width } @@ -82,24 +68,6 @@ proc drawRelativePhysicalLength {value width height axis {context draw}} { drawPhysicalLength $value } -proc drawMeterLength {value} { - format "%sm" $value -} - -proc drawMeterPoint {point} { - lmap value $point { - drawMeterLength $value - } -} - -proc drawMeterRectPoints {x y width height} { - list \ - [drawMeterPoint [list $x $y]] \ - [drawMeterPoint [list [expr {$x + $width}] $y]] \ - [drawMeterPoint [list [expr {$x + $width}] [expr {$y + $height}]]] \ - [drawMeterPoint [list $x [expr {$y + $height}]]] -} - namespace eval ::vec2 { proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] @@ -155,31 +123,6 @@ namespace eval ::vec2 { namespace ensemble create } -# From tcllib ::math::geometry -# Original code found at: https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html -# Thanks to Christian Gollwitzer, Peter Lewerin and Eduard Zozuly -proc ::math::geometry::pointInsidePolygon {point polygon} { - lassign $point testx testy - foreach p $polygon { - lassign $p x y - lappend vertx $x - lappend verty $y - } - set c 0 - set nvert [llength $vertx] - for {set i 0 ; set j [expr {$nvert-1}]} {$i < $nvert} {set j $i ; incr i} { - if { - (([lindex $verty $i]>$testy) != ([lindex $verty $j]>$testy)) && - ($testx < ([lindex $vertx $j] - [lindex $vertx $i]) * - ($testy - [lindex $verty $i]) / - ([lindex $verty $j] - [lindex $verty $i]) + [lindex $vertx $i]) - } { - set c [expr {!$c}] - } - } - return $c -} - proc lsort-indices {itemL} { set pairL [list] foreach item $itemL { diff --git a/test/decorations.folk b/test/decorations.folk index 95ed1360..ead61d1b 100644 --- a/test/decorations.folk +++ b/test/decorations.folk @@ -10,8 +10,6 @@ assert {[drawLineCapFlags 0 4 true round] eq {0.0 0.0}} assert {[drawLineCapFlags 0 4 false round] eq {1.0 0.0}} assert {[drawLineCapFlags 3 4 false round] eq {0.0 1.0}} assert {[drawLineCapFlags 1 4 true square] eq {1.0 1.0}} -assert {[drawMeterPoint {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} diff --git a/test/points-at.folk b/test/points-at.folk index ded4e630..3e6b7b5d 100644 --- a/test/points-at.folk +++ b/test/points-at.folk @@ -32,5 +32,7 @@ assert {[nearlyPoint [dict get $segment to] {0.3 0.05}]} set vertices {{0 0 0} {0.2 0 0} {0.2 0.1 0} {0 0.1 0}} assert {[nearlyPoint [pointsAtQuadPoint $vertices $geom {0.1 0.05}] {0.1 0.05 0}]} assert {[nearlyPoint [pointsAtQuadPoint $vertices $geom {0.1 -0.1}] {0.1 -0.1 0}]} +assert {[pointsAtPointInsidePolygon {0.1 0.05} $vertices]} +assert {![pointsAtPointInsidePolygon {0.3 0.05} $vertices]} Exit! 0 From fb90e346997153e11420eb3b7cce11afbf16c23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 19:11:19 -0400 Subject: [PATCH 5/9] Set default outline width to 1cm --- builtin-programs/decorations/outline.folk | 2 +- test/decorations.folk | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin-programs/decorations/outline.folk b/builtin-programs/decorations/outline.folk index 94699c69..eebac060 100644 --- a/builtin-programs/decorations/outline.folk +++ b/builtin-programs/decorations/outline.folk @@ -21,7 +21,7 @@ When /thing/ has resolved geometry /geom/ &\ if {![info exists options]} { set options [dict create] } set color [dict getdef $options color white] set outlineWidth [drawPhysicalLength \ - [dict getdef $options width [dict getdef $options thickness 0.1cm]]] + [dict getdef $options width [dict getdef $options thickness 1cm]]] set layer [dict getdef $options layer 2] Wish to draw a line onto $thing with \ diff --git a/test/decorations.folk b/test/decorations.folk index ead61d1b..c93718c2 100644 --- a/test/decorations.folk +++ b/test/decorations.folk @@ -58,7 +58,7 @@ 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.001) < 1e-9} +assert {abs([dict get $drawOptions width] - 0.01) < 1e-9} assert {![dict exists $drawOptions caps]} assert {[dict get $drawOptions points] eq {{0 0} {0.3 0} {0.3 0.2} {0 0.2} {0 0}}} From e5e18deac9f3134d91773078858624591fe49296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 19:39:00 -0400 Subject: [PATCH 6/9] Restore resolved-geometry connections --- builtin-programs/connections.folk | 285 ++++++++++++++++++++++++++++++ test/draw-connections.folk | 49 +++++ 2 files changed, 334 insertions(+) create mode 100644 builtin-programs/connections.folk create mode 100644 test/draw-connections.folk diff --git a/builtin-programs/connections.folk b/builtin-programs/connections.folk new file mode 100644 index 00000000..c558556a --- /dev/null +++ b/builtin-programs/connections.folk @@ -0,0 +1,285 @@ +# Connection wish fulfillment for wishes of the form: +# Wish $source is connected to $sink +# Wish $source is dynamically connected to $sink + +set ::drawConnectionClockFrame "" + +fn drawConnectionVecAdd {a b} { + list [expr {[lindex $a 0] + [lindex $b 0]}] \ + [expr {[lindex $a 1] + [lindex $b 1]}] +} + +fn drawConnectionVecSub {a b} { + list [expr {[lindex $a 0] - [lindex $b 0]}] \ + [expr {[lindex $a 1] - [lindex $b 1]}] +} + +fn drawConnectionVecScale {v s} { + list [expr {[lindex $v 0] * $s}] \ + [expr {[lindex $v 1] * $s}] +} + +fn drawConnectionVecLength {v} { + expr {sqrt(pow([lindex $v 0], 2) + pow([lindex $v 1], 2))} +} + +fn drawConnectionVecNormalize {v} { + set length [drawConnectionVecLength $v] + if {$length == 0.0} { return "" } + drawConnectionVecScale $v [expr {1.0 / $length}] +} + +fn drawConnectionVecMix3 {a b t} { + list [expr {[lindex $a 0] + ([lindex $b 0] - [lindex $a 0]) * $t}] \ + [expr {[lindex $a 1] + ([lindex $b 1] - [lindex $a 1]) * $t}] \ + [expr {[lindex $a 2] + ([lindex $b 2] - [lindex $a 2]) * $t}] +} + +fn drawConnectionLocalLength {value width height axis} { + drawRelativePhysicalLength $value $width $height $axis connections +} + +fn drawConnectionLocalPoint {geom selector} { + set width [dict get $geom width] + set height [dict get $geom height] + + switch -- $selector { + centroid - center { return [list [expr {$width / 2.0}] [expr {$height / 2.0}]] } + top { return [list [expr {$width / 2.0}] 0] } + bottom { return [list [expr {$width / 2.0}] $height] } + left { return [list 0 [expr {$height / 2.0}]] } + right { return [list $width [expr {$height / 2.0}]] } + top-left - topleft { return {0 0} } + top-right - topright { return [list $width 0] } + bottom-right - bottomright { return [list $width $height] } + bottom-left - bottomleft { return [list 0 $height] } + } + + if {[llength $selector] != 2} { + error "connections: expected endpoint selector or 2D point, got $selector" + } + list [drawConnectionLocalLength [lindex $selector 0] $width $height width] \ + [drawConnectionLocalLength [lindex $selector 1] $width $height height] +} + +fn drawConnectionQuadPoint {vertices geom point} { + lassign $vertices topLeft topRight bottomRight bottomLeft + set u [expr {[lindex $point 0] / [dict get $geom width]}] + set v [expr {[lindex $point 1] / [dict get $geom height]}] + + set top [drawConnectionVecMix3 $topLeft $topRight $u] + set bottom [drawConnectionVecMix3 $bottomLeft $bottomRight $u] + drawConnectionVecMix3 $top $bottom $v +} + +fn drawConnectionProject {poseLib displayIntrinsics displayWidth displayHeight point} { + $poseLib project $displayIntrinsics $displayWidth $displayHeight $point +} + +fn drawConnectionPixelOption {options key default context} { + set value [dict getdef $options $key $default] + if {![string is double -strict $value]} { + error "connections: $context must be a numeric display-pixel value, got $value" + } + expr {double($value)} +} + +fn drawConnectionSegment { + quadLib poseLib quadChange displayIntrinsics displayWidth displayHeight disp + sourceGeom sourceQuad sinkGeom sinkQuad options +} { + set fromSelector [dict getdef $options from centroid] + set toSelector [dict getdef $options to centroid] + + set sourceVertices [$quadLib vertices [quadChange $sourceQuad "display $disp"]] + set sinkVertices [$quadLib vertices [quadChange $sinkQuad "display $disp"]] + + set sourcePoint [drawConnectionQuadPoint $sourceVertices $sourceGeom \ + [drawConnectionLocalPoint $sourceGeom $fromSelector]] + set sinkPoint [drawConnectionQuadPoint $sinkVertices $sinkGeom \ + [drawConnectionLocalPoint $sinkGeom $toSelector]] + + set from [drawConnectionProject $poseLib $displayIntrinsics \ + $displayWidth $displayHeight $sourcePoint] + set to [drawConnectionProject $poseLib $displayIntrinsics \ + $displayWidth $displayHeight $sinkPoint] + set delta [drawConnectionVecSub $to $from] + set distance [drawConnectionVecLength $delta] + if {$distance == 0.0} { return "" } + + dict create \ + from $from \ + to $to \ + direction [drawConnectionVecScale $delta [expr {1.0 / $distance}]] \ + distance $distance +} + +fn drawConnectionArrowPoints {center direction radius} { + if {$radius <= 0.0} { return "" } + set perp [list [expr {-[lindex $direction 1]}] [lindex $direction 0]] + set tip [drawConnectionVecAdd $center [drawConnectionVecScale $direction $radius]] + set rear [drawConnectionVecAdd $center [drawConnectionVecScale $direction [expr {-$radius}]]] + set spread [expr {$radius * 0.8}] + list $tip \ + [drawConnectionVecAdd $rear [drawConnectionVecScale $perp $spread]] \ + [drawConnectionVecAdd $rear [drawConnectionVecScale $perp [expr {-$spread}]]] +} + +fn drawConnectionFrameTime {t} { + expr {floor($t * 30.0) / 30.0} +} + +fn drawConnectionFrameIndex {t} { + expr {int(floor($t * 30.0))} +} + +When -serially connections have dynamic animation demand &\ + the clock time is /t/ { + set frame [drawConnectionFrameIndex $t] + if {$::drawConnectionClockFrame eq $frame} { + return + } + set ::drawConnectionClockFrame $frame + Claim -keep 50ms connections animation frame $frame time [expr {$frame / 30.0}] +} + +fn drawConnectionDynamicArrows {from direction distance options t} { + set speed [drawConnectionPixelOption $options speed 75 speed] + set spacing [drawConnectionPixelOption $options spacing 50 spacing] + set maxSize [drawConnectionPixelOption $options maxsize \ + [dict getdef $options maxSize 25] maxsize] + set maxArrows [expr {int([drawConnectionPixelOption $options maxArrows 48 maxArrows])}] + + if {$spacing <= 0.0} { error "connections: spacing must be positive" } + if {$maxArrows < 1} { error "connections: maxArrows must be at least 1" } + + set effectiveSpacing $spacing + if {ceil($distance / $effectiveSpacing) > $maxArrows} { + set effectiveSpacing [expr {$distance / double($maxArrows)}] + } + + set frameT [drawConnectionFrameTime $t] + set offset [expr {fmod($frameT * $speed, $effectiveSpacing)}] + if {$offset < 0.0} { set offset [expr {$offset + $effectiveSpacing}] } + + set arrows [list] + for {set p $offset} {$p < $distance && [llength $arrows] < $maxArrows} \ + {set p [expr {$p + $effectiveSpacing}]} { + set farEdgeDistance [expr {$distance - $p}] + set edgeDistance [expr {$p < $farEdgeDistance ? $p : $farEdgeDistance}] + set radius [expr {$maxSize < 0.20 * $edgeDistance ? $maxSize : 0.20 * $edgeDistance}] + if {$radius <= 0.25} { continue } + + lappend arrows [dict create \ + center [drawConnectionVecAdd $from [drawConnectionVecScale $direction $p]] \ + radius $radius] + } + return $arrows +} + +fn drawConnectionKey {kind disp source sink options} { + list connections $kind $disp $source $sink $options +} + +fn drawConnectionWish {disp key from to direction color layer width arrowSpecs} { + Hold! -keep 50ms -key $key { + Wish to draw a line onto $disp with \ + points [list $from $to] width $width color $color layer $layer + foreach arrowSpec $arrowSpecs { + set arrowPoints [drawConnectionArrowPoints \ + [dict get $arrowSpec center] $direction [dict get $arrowSpec radius]] + if {$arrowPoints eq ""} { continue } + lassign $arrowPoints tip rearLeft rearRight + Wish to draw a triangle onto $disp with \ + p0 $tip p1 $rearLeft p2 $rearRight 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 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/ &\ + /source/ has resolved geometry /sourceGeom/ &\ + /source/ has quad /sourceQuad/ &\ + /sink/ has resolved geometry /sinkGeom/ &\ + /sink/ has quad /sinkQuad/ { + if {$source eq $sink} { return } + if {![info exists options]} { set options [dict create] } + set segment [drawConnectionSegment \ + $quadLib $poseLib $quadChange $displayIntrinsics $displayWidth $displayHeight $disp \ + $sourceGeom $sourceQuad $sinkGeom $sinkQuad $options] + if {$segment eq ""} { return } + + set color [dict getdef $options color grey] + set layer [dict getdef $options layer 0] + set width [drawConnectionPixelOption $options width 2 width] + set arrowRadius [drawConnectionPixelOption $options arrowRadius 30 arrowRadius] + set mid [drawConnectionVecAdd [dict get $segment from] \ + [drawConnectionVecScale \ + [drawConnectionVecSub [dict get $segment to] [dict get $segment from]] 0.5]] + set key [drawConnectionKey static $disp $source $sink $options] + + drawConnectionWish $disp $key \ + [dict get $segment from] [dict get $segment to] [dict get $segment direction] \ + $color $layer $width [list [dict create center $mid radius $arrowRadius]] + + On unmatch { + Hold! -key $key {} + } +} + +When -atomically /anyone/ wishes /source/ is dynamically connected to /sink/ with /...options/ &\ + 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/ &\ + /source/ has resolved geometry /sourceGeom/ &\ + /source/ has quad /sourceQuad/ &\ + /sink/ has resolved geometry /sinkGeom/ &\ + /sink/ has quad /sinkQuad/ { + if {$source eq $sink} { return } + if {![info exists options]} { set options [dict create] } + set segment [drawConnectionSegment \ + $quadLib $poseLib $quadChange $displayIntrinsics $displayWidth $displayHeight $disp \ + $sourceGeom $sourceQuad $sinkGeom $sinkQuad $options] + if {$segment eq ""} { return } + + set color [dict getdef $options color white] + set layer [dict getdef $options layer 0] + set width [drawConnectionPixelOption $options width 1 width] + set key [drawConnectionKey dynamic $disp $source $sink $options] + Claim -keep 100ms connections have dynamic animation demand + + When -serially -atomicallyWithKey $key connections animation frame /frame/ time /frameT/ { + set arrows [drawConnectionDynamicArrows \ + [dict get $segment from] [dict get $segment direction] \ + [dict get $segment distance] $options $frameT] + drawConnectionWish $disp $key \ + [dict get $segment from] [dict get $segment to] [dict get $segment direction] \ + $color $layer $width $arrows + } + + On unmatch { + Hold! -key $key {} + } +} diff --git a/test/draw-connections.folk b/test/draw-connections.folk new file mode 100644 index 00000000..e7dea243 --- /dev/null +++ b/test/draw-connections.folk @@ -0,0 +1,49 @@ +source builtin-programs/connections.folk + +proc nearly {actual expected} { + expr {abs($actual - $expected) < 1e-9} +} + +proc nearlyPoint {actual expected} { + foreach a $actual e $expected { + if {![nearly $a $e]} { + return false + } + } + return true +} + +set geom {width 0.2 height 0.1} + +assert {[drawConnectionLocalPoint $geom centroid] eq {0.1 0.05}} +assert {[drawConnectionLocalPoint $geom top] eq {0.1 0}} +assert {[drawConnectionLocalPoint $geom right] eq {0.2 0.05}} +assert {[nearlyPoint [drawConnectionLocalPoint $geom {50% 25%}] {0.1 0.025}]} +assert {[nearlyPoint [drawConnectionLocalPoint $geom {2cm 3cm}] {0.02 0.03}]} + +set vertices {{0 0 0} {0.2 0 0} {0.2 0.1 0} {0 0.1 0}} +assert {[nearlyPoint [drawConnectionQuadPoint $vertices $geom {0.1 0.05}] {0.1 0.05 0}]} +assert {[nearlyPoint [drawConnectionQuadPoint $vertices $geom {0.2 0.05}] {0.2 0.05 0}]} + +lassign [drawConnectionArrowPoints {5 1} {1 0} 2] tip rearLeft rearRight +assert {[nearlyPoint $tip {7 1}]} +assert {[nearlyPoint $rearLeft {3 2.6}]} +assert {[nearlyPoint $rearRight {3 -0.6}]} + +assert {[nearly [drawConnectionFrameTime 1.001] 1.0]} +assert {[nearly [drawConnectionFrameTime 1.032] 1.0]} +assert {[nearly [drawConnectionFrameTime 1.034] [expr {31.0 / 30.0}]]} +assert {[drawConnectionFrameIndex 1.001] == 30} +assert {[drawConnectionFrameIndex 1.032] == 30} +assert {[drawConnectionFrameIndex 1.034] == 31} + +set arrows [drawConnectionDynamicArrows {0 0} {1 0} 1000 {spacing 10 maxArrows 20 maxsize 25 speed 75} 1.0] +assert {[llength $arrows] <= 20} +assert {[llength $arrows] > 0} + +set first [lindex $arrows 0] +assert {[dict exists $first center]} +assert {[dict exists $first radius]} +assert {[dict get $first radius] <= 25} + +Exit! 0 From 189e59a5c8622321047edcc482a3d56f11922dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Wed, 20 May 2026 20:41:33 -0400 Subject: [PATCH 7/9] Harden connection animation frame initialization --- builtin-programs/connections.folk | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builtin-programs/connections.folk b/builtin-programs/connections.folk index c558556a..8d1eceec 100644 --- a/builtin-programs/connections.folk +++ b/builtin-programs/connections.folk @@ -136,7 +136,8 @@ fn drawConnectionFrameIndex {t} { When -serially connections have dynamic animation demand &\ the clock time is /t/ { set frame [drawConnectionFrameIndex $t] - if {$::drawConnectionClockFrame eq $frame} { + if {[info exists ::drawConnectionClockFrame] && + $::drawConnectionClockFrame eq $frame} { return } set ::drawConnectionClockFrame $frame From 095b56ea602558834ffa0131b6e30bc3437ec6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Thu, 21 May 2026 09:44:07 -0400 Subject: [PATCH 8/9] Add drawing demo code claims --- builtin-programs/draw/apriltags.folk | 10 ++++++++++ builtin-programs/draw/arc.folk | 13 +++++++++++++ builtin-programs/draw/circle.folk | 15 ++++++++++++++ builtin-programs/draw/curve.folk | 13 +++++++++++++ builtin-programs/draw/dashed-line.folk | 13 +++++++++++++ builtin-programs/draw/fill.folk | 12 ++++++++++++ builtin-programs/draw/gif.folk | 8 ++++++++ builtin-programs/draw/image.folk | 10 ++++++++++ builtin-programs/draw/line.folk | 14 +++++++++++++ builtin-programs/draw/shapes.folk | 18 ++++++++++------- builtin-programs/draw/text.folk | 18 +++++++++++++++++ builtin-programs/programs.folk | 27 +++++++++++++++++++++++++- 12 files changed, 163 insertions(+), 8 deletions(-) diff --git a/builtin-programs/draw/apriltags.folk b/builtin-programs/draw/apriltags.folk index 8cfff0ef..150fa864 100644 --- a/builtin-programs/draw/apriltags.folk +++ b/builtin-programs/draw/apriltags.folk @@ -51,3 +51,13 @@ When the image library is /imageLib/ & the print library is /printLib/ &\ $tagBitsVec {*}$corners] \ layer $layer } + +Hold! -on builtin-programs/draw/apriltags.folk -key draw-apriltags-demo-code \ + Claim builtin-programs/draw/apriltags.folk has demo code { +Wish $this is filled with color {0.05 0.05 0.05 1.0} +Wish $this draws text "AprilTag" with position {50% 16%} scale 0.85cm color white anchor center layer 8 +Wish to draw an AprilTag onto $this with \ + id 204 corners {{0.065 0.045} {0.145 0.045} {0.145 0.125} {0.065 0.125}} \ + background {1.0 1.0 1.0 1.0} layer 4 +Wish $this draws text "id 204" with position {50% 82%} scale 0.65cm color white anchor center layer 8 +} diff --git a/builtin-programs/draw/arc.folk b/builtin-programs/draw/arc.folk index 39e3e255..e3ef2165 100644 --- a/builtin-programs/draw/arc.folk +++ b/builtin-programs/draw/arc.folk @@ -77,3 +77,16 @@ When the color map is /colorMap/ &\ $center $radius $thickness $start $arclen $color] \ layer $layer } + +Hold! -on builtin-programs/draw/arc.folk -key draw-arc-demo-code \ + Claim builtin-programs/draw/arc.folk has demo code { +Wish $this is filled with color {0.04 0.05 0.07 1.0} +Wish $this draws text "arc" with position {50% 18%} scale 0.9cm color white anchor center layer 8 +When the clock time is /t/ { + set spin [expr {fmod($t, $::TAU)}] + Wish to draw an arc onto $this with \ + center {0.105 0.085} radius 0.045 thickness 0.008 \ + start $spin arclen [expr {$::PI * 1.3}] \ + color {0.30 0.86 1.0 1.0} layer 4 +} +} diff --git a/builtin-programs/draw/circle.folk b/builtin-programs/draw/circle.folk index 9963b143..df72f26e 100644 --- a/builtin-programs/draw/circle.folk +++ b/builtin-programs/draw/circle.folk @@ -62,3 +62,18 @@ When the color map is /colorMap/ &\ } } } + +Hold! -on builtin-programs/draw/circle.folk -key draw-circle-demo-code \ + Claim builtin-programs/draw/circle.folk has demo code { +Wish $this is filled with color {0.03 0.04 0.055 1.0} +Wish $this draws text "circle" with position {50% 18%} scale 0.9cm color white anchor center layer 8 +Wish to draw a circle onto $this with \ + center {0.135 0.095} radius 0.026 thickness 0.006 \ + color {1.0 0.78 0.28 1.0} filled false layer 4 +When the clock time is /t/ { + set radius [expr {0.020 + 0.006 * (0.5 + 0.5 * sin($t * 2.0))}] + Wish to draw a circle onto $this with \ + center {0.075 0.095} radius $radius thickness 0 \ + color {0.92 0.20 0.55 0.92} filled true layer 3 +} +} diff --git a/builtin-programs/draw/curve.folk b/builtin-programs/draw/curve.folk index 475ea1d3..9f245da7 100644 --- a/builtin-programs/draw/curve.folk +++ b/builtin-programs/draw/curve.folk @@ -77,3 +77,16 @@ When the color map is /colorMap/ &\ $p0 $p1 $p2 $p3 $thickness $color] \ layer $layer } + +Hold! -on builtin-programs/draw/curve.folk -key draw-curve-demo-code \ + Claim builtin-programs/draw/curve.folk has demo code { +Wish $this is filled with color {0.035 0.04 0.055 1.0} +Wish $this draws text "curve" with position {50% 18%} scale 0.9cm color white anchor center layer 8 +When the clock time is /t/ { + set lift [expr {0.018 * sin($t * 1.4)}] + Wish to draw a curve onto $this with \ + p0 {0.035 0.125} p1 [list 0.070 [expr {0.030 + $lift}]] \ + p2 [list 0.140 [expr {0.165 - $lift}]] p3 {0.175 0.070} \ + thickness 0.006 color {0.55 0.88 1.0 1.0} layer 4 +} +} diff --git a/builtin-programs/draw/dashed-line.folk b/builtin-programs/draw/dashed-line.folk index c6fd2ad2..b5025a89 100644 --- a/builtin-programs/draw/dashed-line.folk +++ b/builtin-programs/draw/dashed-line.folk @@ -61,3 +61,16 @@ When the color map is /colorMap/ &\ Wish the GPU draws pipeline "dashed-line" onto canvas $id \ with instances $instances layer $layer } + +Hold! -on builtin-programs/draw/dashed-line.folk -key draw-dashed-line-demo-code \ + Claim builtin-programs/draw/dashed-line.folk has demo code { +Wish $this is filled with color {0.035 0.04 0.05 1.0} +Wish $this draws text "dashed line" with position {50% 18%} scale 0.85cm color white anchor center layer 8 +When the clock time is /t/ { + set dashoffset [expr {fmod(-$t * 0.035, 0.030)}] + Wish to draw a dashed line onto $this with \ + points {{0.035 0.115} {0.080 0.060} {0.130 0.125} {0.175 0.070}} \ + width 0.006 color {1.0 0.82 0.22 1.0} \ + dashlength 0.012 dashoffset $dashoffset layer 4 +} +} diff --git a/builtin-programs/draw/fill.folk b/builtin-programs/draw/fill.folk index eb1c773c..26716950 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -89,3 +89,15 @@ When /someone/ wishes /page/ draws fill with /...options/ { } } + +Hold! -on builtin-programs/draw/fill.folk -key draw-fill-demo-code \ + Claim builtin-programs/draw/fill.folk has demo code { +Wish $this is filled with color {0.02 0.04 0.06 1.0} +Wish $this draws text "fill" with position {50% 18%} scale 0.9cm color white anchor center layer 8 +Wish to draw a polygon onto $this with \ + points {{0.040 0.135} {0.070 0.055} {0.130 0.045} {0.175 0.120} {0.105 0.155}} \ + color {0.20 0.78 0.62 0.82} layer 3 +Wish to draw a triangle onto $this with \ + p0 {0.060 0.130} p1 {0.105 0.075} p2 {0.150 0.130} \ + color {1.0 0.72 0.18 0.85} layer 4 +} diff --git a/builtin-programs/draw/gif.folk b/builtin-programs/draw/gif.folk index 8124256d..4a081dbf 100644 --- a/builtin-programs/draw/gif.folk +++ b/builtin-programs/draw/gif.folk @@ -34,3 +34,11 @@ When the gif library is /gifLib/ { } } } + +Hold! -on builtin-programs/draw/gif.folk -key draw-gif-demo-code \ + Claim builtin-programs/draw/gif.folk has demo code { +Wish $this is filled with color {0.035 0.035 0.045 1.0} +Wish $this draws text "gif" with position {50% 16%} scale 0.9cm color white anchor center layer 8 +Wish $this displays image "https://blob.gifcities.org/gifcities/EAAJDOTZGLUOERVAUXNUITVS64CZ57MG.gif" with \ + position {50% 56%} anchor center width 8cm +} diff --git a/builtin-programs/draw/image.folk b/builtin-programs/draw/image.folk index 373896d5..846b22fa 100644 --- a/builtin-programs/draw/image.folk +++ b/builtin-programs/draw/image.folk @@ -235,3 +235,13 @@ When the image library is /imageLib/ &\ When /someone/ wishes /p/ displays image /im/ { Wish $p displays image $im with position {0cm 0cm} } + +Hold! -on builtin-programs/draw/image.folk -key draw-image-demo-code \ + Claim builtin-programs/draw/image.folk has demo code { +Wish $this is filled with color {0.035 0.035 0.045 1.0} +Wish $this draws text "image" with position {50% 16%} scale 0.9cm color white anchor center layer 8 +Wish $this draws a rect with position {50% 56%} width 10cm height 7cm \ + color {0.25 0.36 0.48 1.0} filled false thickness 0.18cm layer 2 +Wish $this displays image "https://folk.computer/_media/logo.png" with \ + position {50% 56%} anchor center width 8cm +} diff --git a/builtin-programs/draw/line.folk b/builtin-programs/draw/line.folk index 3da3fa7f..79749b40 100644 --- a/builtin-programs/draw/line.folk +++ b/builtin-programs/draw/line.folk @@ -98,3 +98,17 @@ When the color map is /colorMap/ &\ Wish the GPU draws pipeline "line" onto canvas $id \ with instances $instances layer $layer } + +Hold! -on builtin-programs/draw/line.folk -key draw-line-demo-code \ + Claim builtin-programs/draw/line.folk has demo code { +Wish $this is filled with color {0.03 0.04 0.055 1.0} +Wish $this draws text "line caps" with position {50% 18%} scale 0.85cm color white anchor center layer 8 +Wish to draw a line onto $this with \ + points {{0.035 0.075} {0.175 0.075}} width 0.010 \ + color {0.35 0.86 1.0 1.0} caps round layer 4 +Wish to draw a line onto $this with \ + points {{0.035 0.120} {0.175 0.120}} width 0.010 \ + color {1.0 0.72 0.22 1.0} caps square layer 4 +Wish $this draws points {{20% 38%} {80% 38%} {20% 61%} {80% 61%}} with \ + radius 0.25cm color white filled true layer 6 +} diff --git a/builtin-programs/draw/shapes.folk b/builtin-programs/draw/shapes.folk index 8b002a59..1e10ba5c 100644 --- a/builtin-programs/draw/shapes.folk +++ b/builtin-programs/draw/shapes.folk @@ -326,13 +326,17 @@ When /someone/ wishes /p/ draws a set of points /points/ { Wish $p draws points $points } -# Keep the builtin demo tiny and self-contained so the public shapes API -# stays reviewable without dragging along a large stress gallery. Hold! -on builtin-programs/draw/shapes.folk -key draw-shapes-demo-code \ Claim builtin-programs/draw/shapes.folk has demo code { -Wish $this draws a rect with width 6cm height 4cm color {0.10 0.16 0.22 0.92} filled true -Wish $this draws a circle with position {50% 50%} radius 1.2cm color {0.92 0.74 0.28 1.0} filled false thickness 0.25cm -Wish $this draws a polyline {{20% 75%} {50% 25%} {80% 75%}} with color {0.52 0.85 1.0 1.0} width 0.18cm -Wish $this draws points {{20% 75%} {50% 25%} {80% 75%}} with color white radius 0.22cm thickness 0cm filled true -Wish $this draws text "shapes" with position {50% 88%} anchor center color white scale 1.1cm +Wish $this draws a rect with width 7cm height 4.5cm color {0.10 0.16 0.22 0.92} filled true layer 1 +Wish $this draws a circle with position {50% 50%} radius 1.2cm color {0.92 0.74 0.28 1.0} filled false thickness 0.25cm layer 3 +Wish $this draws an octagon with position {75% 38%} radius 1.0cm color {0.76 0.50 1.0 1.0} filled true layer 2 +Wish $this draws a polyline {{20% 75%} {50% 25%} {80% 75%}} with color {0.52 0.85 1.0 1.0} width 0.18cm layer 4 +Wish $this draws points {{20% 75%} {50% 25%} {80% 75%}} with color white radius 0.22cm thickness 0cm filled true layer 5 +Wish $this draws text "shapes" with position {50% 88%} anchor center color white scale 1.1cm layer 8 +When the clock time is /t/ { + set x [format "%.2fcm" [expr {sin($t * 1.5) * 2.0}]] + Wish $this draws a circle with offset [list $x 0cm] radius 0.65cm \ + color {0.95 0.35 0.62 0.90} filled true layer 6 +} } diff --git a/builtin-programs/draw/text.folk b/builtin-programs/draw/text.folk index b3c75602..83030f67 100644 --- a/builtin-programs/draw/text.folk +++ b/builtin-programs/draw/text.folk @@ -322,3 +322,21 @@ When the color map is /colorMap/ &\ } } + +Hold! -on builtin-programs/draw/text.folk -key draw-text-demo-code \ + Claim builtin-programs/draw/text.folk has demo code { +Wish $this is filled with color {0.035 0.035 0.05 1.0} +Wish $this draws text "text" with position {50% 16%} scale 0.95cm color white anchor center layer 8 +Wish to draw text onto $this with \ + position {0.040 0.065} scale 0.012 text "top left" \ + anchor topleft color {0.52 0.86 1.0 1.0} layer 5 +Wish to draw text onto $this with \ + position {0.175 0.140} scale 0.012 text "bottom right" \ + anchor bottomright color {1.0 0.78 0.32 1.0} layer 5 +When the clock time is /t/ { + set radians [expr {sin($t * 1.2) * 0.20}] + Wish to draw text onto $this with \ + position {0.105 0.105} scale 0.016 text "rotating" \ + anchor center radians $radians color {0.95 0.42 0.75 1.0} layer 6 +} +} diff --git a/builtin-programs/programs.folk b/builtin-programs/programs.folk index b38cef46..778eed55 100644 --- a/builtin-programs/programs.folk +++ b/builtin-programs/programs.folk @@ -6,6 +6,32 @@ When tag /tag/ has detection /any/ on camera /any/ at timestamp /any/ { Claim -keep 100ms tag $tag is a tag } +When /someone/ wishes /thing/ runs demo code from /demoSource/ &\ + /demoSource/ has demo code /code/ { + set marginKey [list demo-code-margin-for $thing] + set codeKey [list demo-code-program-for $thing] + set demoProgram [list demo-code-for $thing] + set wrappedCode [format "set this %s\n%s" [list $thing] $code] + + Hold! -on builtin-programs/programs.folk -key $marginKey \ + Wish $thing is left-margined [string trim $code] with \ + font CourierPrimeCode \ + anchor {1.0 0.5 0 0.5} \ + scale 18.0 + + Hold! -on builtin-programs/programs.folk -key $codeKey \ + Claim $demoProgram has program code $wrappedCode + + On unmatch { + Hold! -on builtin-programs/programs.folk -key $marginKey {} + Hold! -on builtin-programs/programs.folk -key $codeKey {} + } +} + +When /someone/ wishes /thing/ runs demo code { + Wish $thing runs demo code from $thing +} + When -noncapturing the program save directory is /saveDir/ { When /type/ /obj/ has a program { puts stderr "Added $type $obj" @@ -68,4 +94,3 @@ When -noncapturing the program save directory is /saveDir/ { } } } - From 19aa70a6dc9007ce3889a0e64eda1636f168d92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Thu, 21 May 2026 11:57:32 -0400 Subject: [PATCH 9/9] Serialize retired texture destruction --- builtin-programs/gpu/textures.folk | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/builtin-programs/gpu/textures.folk b/builtin-programs/gpu/textures.folk index 6321c8ac..c1625910 100644 --- a/builtin-programs/gpu/textures.folk +++ b/builtin-programs/gpu/textures.folk @@ -815,7 +815,37 @@ $gpuc code { block->alive = false; } + static bool waitForTextureRetireIdleLocked() { + VkResult res = vkQueueWaitIdle(*graphicsQueue_ptr()); + + if (res != VK_SUCCESS) { + fprintf(stderr, "gpu/textures: Failed vkQueueWaitIdle before texture retire: %s (%d)\n", + VkResultToString(res), res); + return false; + } + + return true; + } + static void destroyRetiredGpuTextures() { + bool hasRetiredTexture = false; + for (GpuTextureHandle gim = 1; gim < getMaxTextures(); gim++) { + GpuTextureBlock* block = &gpuTextures[gim]; + if (block->alive && block->retiring && + block->retireAfterFrame <= textureFrameEpoch) { + hasRetiredTexture = true; + break; + } + } + + if (!hasRetiredTexture) return; + + pthread_mutex_lock(graphicsQueueMutex_ptr()); + if (!waitForTextureRetireIdleLocked()) { + pthread_mutex_unlock(graphicsQueueMutex_ptr()); + return; + } + for (GpuTextureHandle gim = 1; gim < getMaxTextures(); gim++) { GpuTextureBlock* block = &gpuTextures[gim]; if (block->alive && block->retiring && @@ -823,6 +853,7 @@ $gpuc code { destroyGpuTextureResources(gim); } } + pthread_mutex_unlock(graphicsQueueMutex_ptr()); } }