From ab19c26eeb629d51b30ec70ecfbf9d17b32d6716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Mon, 1 Jun 2026 15:44:54 -0400 Subject: [PATCH 1/4] Scope PR down to just shape drawing --- builtin-programs/decorations/label.folk | 73 +++-- builtin-programs/display/arc.folk | 39 --- builtin-programs/display/curve.folk | 135 --------- builtin-programs/draw/arc.folk | 53 ++++ builtin-programs/draw/circle.folk | 100 ++++--- builtin-programs/draw/curve.folk | 79 ++++++ builtin-programs/draw/fill.folk | 144 ++++++---- builtin-programs/draw/line.folk | 67 +++-- builtin-programs/draw/shapes.folk | 349 +++++++++++++++++++++++ builtin-programs/draw/text.folk | 8 +- builtin-programs/regions.folk | 8 - builtin-programs/shapes.folk | 357 ------------------------ builtin-programs/shapes/region.folk | 92 ------ lib/math.tcl | 65 +++++ 14 files changed, 802 insertions(+), 767 deletions(-) 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/regions.folk delete mode 100644 builtin-programs/shapes.folk delete mode 100644 builtin-programs/shapes/region.folk diff --git a/builtin-programs/decorations/label.folk b/builtin-programs/decorations/label.folk index 03341d6d9..918cbba19 100644 --- a/builtin-programs/decorations/label.folk +++ b/builtin-programs/decorations/label.folk @@ -1,38 +1,55 @@ +# TODO: inline these functions, only used once rn anyway tbh +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]] + } + dict set options text $text + Wish to draw text onto $thing with {*}$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 } -} -When /someone/ wishes /thing/ is labelled /text/ { - Wish $thing is labelled $text with font "PTSans-Regular" -} + When /someone/ wishes /thing/ is labelled /text/ { + Wish $thing is labelled $text with font "PTSans-Regular" + } diff --git a/builtin-programs/display/arc.folk b/builtin-programs/display/arc.folk deleted file mode 100644 index f6a0c6787..000000000 --- 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 9082d117f..000000000 --- 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 a67b3e4e9..04c5b85df 100644 --- a/builtin-programs/draw/circle.folk +++ b/builtin-programs/draw/circle.folk @@ -1,8 +1,8 @@ Wish the GPU compiles pipeline "circle" { {vec2 viewport mat3 surfaceToClip - vec2 center float radius float thickness vec4 color int filled} { - float r = radius + thickness; - vec2 vertices[6] = vec2[6]( + vec2 center float radius float thickness vec4 color int filled} { + float r = radius + thickness; + vec2 vertices[6] = vec2[6]( center - r, vec2(center.x + r, center.y - r), vec2(center.x - r, center.y + r), @@ -10,38 +10,66 @@ Wish the GPU compiles pipeline "circle" { vec2(center.x + r, center.y - r), center + r, vec2(center.x - r, center.y + r) - ); - vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); - return vec4(v.xy/v.z, 0.0, 1.0); - } { - vec2 clipXy = (gl_FragCoord.xy / viewport) * 2.0 - 1.0; - vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); - surfaceXy /= surfaceXy.z; - - float dist = length(surfaceXy.xy - center) - radius; - if (filled == 1) { - return (dist < thickness) ? color : vec4(0.0); - } else { - return (dist < thickness && dist > 0.0) ? color : vec4(0.0); + ); + vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); + return vec4(v.xy/v.z, 0.0, 1.0); + } { + vec2 clipXy = (gl_FragCoord.xy / viewport) * 2.0 - 1.0; + vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); + surfaceXy /= surfaceXy.z; + + float dist = length(surfaceXy.xy - center) - radius; + if (filled == 1) { + return (dist < thickness) ? color : vec4(0.0); + } else { + return (dist < thickness && dist > 0.0) ? color : vec4(0.0); + } } } -} - -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}]] -} + + 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 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 + } + } + + 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 3cm 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.026 * (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 new file mode 100644 index 000000000..475ea1d31 --- /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 4e977c1e3..465064dc2 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -9,61 +9,99 @@ Wish the GPU compiles pipeline "fillTriangle" { } When the color map is /colorMap/ { + 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" onto canvas $id with arguments \ + [list $surfaceToClip $p0 $p1 $p2 $color] layer $layer + } + } -When /someone/ wishes to draw a triangle with /...options/ { - 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 - } -} -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 - } -} -When /someone/ wishes to draw a polygon with /...options/ { - set points [dict get $options points] - set color [dict get $options color] - set layer [dict getdef $options layer 0] + 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 instances \ + [list \ + [list $surfaceToClip $p1 $p2 $p3 $color] \ + [list $surfaceToClip $p0 $p1 $p3 $color]] \ + layer $layer + } + } - set num_points [llength $points] - if {$num_points < 3} { - error "At least 3 points are required to form a polygon." - } elseif {$num_points == 3} { - Wish to draw a triangle with \ - p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \ - color $color layer $layer - } elseif {$num_points == 4} { - Wish to draw a quad with \ - p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] p3 [lindex $points 3] \ - color $color layer $layer - } else { - # Get the first point in the list as the "base" point of the triangles - set p0 [lindex $points 0] + 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] - set color [dict getdef $colorMap $color $color] - 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 - } - } -} + set num_points [llength $points] + if {$num_points < 3} { + error "At least 3 points are required to form a polygon." + } elseif {$num_points == 3} { + Wish to draw a triangle 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 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] -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 -} + # 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}]] + 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/ { + set fillOpts [list] + if {[info exists options]} { set fillOpts $options } else { set fillOpts {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 {*}$fillOpts + } + + When /someone/ wishes /page/ draws fill with /...options/ { + set fillOpts [list] + if {[info exists options]} { set fillOpts $options } else { set fillOpts {color white} } + Wish $page is filled with {*}$fillOpts + } + + When /someone/ wishes /thing/ is filled /color/ { + Wish $thing is filled with color $color + } + + When /someone/ wishes /thing/ is filled /color/ with /optionKey/ /optionValue/ /...optionRest/ { + if {![info exists optionKey] || ![info exists optionValue]} { return } + if {![info exists optionRest]} { set optionRest [list] } + Wish $thing is filled with color $color $optionKey $optionValue {*}$optionRest + } + + When /someone/ wishes /thing/ is filled /color/ with /optionKey/ /optionValue/ { + if {![info exists optionKey] || ![info exists optionValue]} { return } + Wish $thing is filled with color $color $optionKey $optionValue + } + } diff --git a/builtin-programs/draw/line.folk b/builtin-programs/draw/line.folk index 96215309e..49a60d3d6 100644 --- a/builtin-programs/draw/line.folk +++ b/builtin-programs/draw/line.folk @@ -1,18 +1,25 @@ Wish the GPU compiles pipeline "line" { {vec2 viewport mat3 surfaceToClip - vec2 from vec2 to float thickness vec4 color} { - vec2 dir = normalize(to - from); - vec2 perp = vec2(-dir.y, dir.x) * thickness/2.0; + vec2 from vec2 to float thickness vec4 color float capFrom float capTo} { + + vec2 diff = to - from; + if (length(diff) < 0.0001) return vec4(0.0); + vec2 dir = normalize(diff); + 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 +27,38 @@ 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 drawLineCapFlags {index segmentCount closed capStyle} { + switch -- $capStyle { + square - flat - butt { + return {1.0 1.0} + } + round - rounded { + return {0.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,10 +69,17 @@ 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}] + + # 1.0 = flat, 0.0 = round + lassign [drawLineCapFlags 0 $segmentCount 0 $caps] capFrom capTo + + 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] diff --git a/builtin-programs/draw/shapes.folk b/builtin-programs/draw/shapes.folk new file mode 100644 index 000000000..718035461 --- /dev/null +++ b/builtin-programs/draw/shapes.folk @@ -0,0 +1,349 @@ +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 {$shape eq "polygon" || $shape eq "polyline"} { return } + + 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 a polygon /points/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + if {![info exists options]} { return } + set parsedPoints [drawShapePathPoints $points $geom $options] + set color [dict getdef $options color white] + set layer [dict getdef $options layer 1] + Wish to draw a polygon onto $p with points $parsedPoints color $color layer $layer +} + +When /someone/ wishes /p/ draws a polygon with points /points/ /...options/ { + # Forgiving syntax for users who write 'draws a polygon with points $points color white' + # and use ( ) instead of { } for points. + set cleanPoints [string map {"(" "{" ")" "}"} $points] + Wish $p draws a polygon $cleanPoints with {*}$options +} + +When /someone/ wishes /p/ draws a polygon /points/ { + Wish $p draws a polygon $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 +} diff --git a/builtin-programs/draw/text.folk b/builtin-programs/draw/text.folk index f9dfcf63e..3a004f36e 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); @@ -201,7 +203,7 @@ When the image loader is /loadImage/ { } set im [{*}$loadImage "[pwd]/vendor/fonts/$name.png"] - Wish the GPU loads image $im as texture + Wish the GPU loads image $im as texture with pinned true When the GPU has loaded image $im as texture /gim/ { puts "text: Loaded $name as GPU texture $gim" set font [$fontLib fontNew $im $gim $glyphInfos] diff --git a/builtin-programs/regions.folk b/builtin-programs/regions.folk deleted file mode 100644 index 945fcd20e..000000000 --- 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 c67c7e434..000000000 --- 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 492a268d8..000000000 --- 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/lib/math.tcl b/lib/math.tcl index 300e1b045..866c623c6 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -3,6 +3,71 @@ # 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 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 +} + namespace eval ::vec2 { proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] From 15b5ef627aee5826f3b3821852859fc1bf6d5436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Mon, 1 Jun 2026 18:58:59 -0400 Subject: [PATCH 2/4] Inline label fns --- builtin-programs/decorations/label.folk | 71 +++++++++++-------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/builtin-programs/decorations/label.folk b/builtin-programs/decorations/label.folk index 918cbba19..5dd71335d 100644 --- a/builtin-programs/decorations/label.folk +++ b/builtin-programs/decorations/label.folk @@ -1,31 +1,3 @@ -# TODO: inline these functions, only used once rn anyway tbh -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"] @@ -33,23 +5,42 @@ When /thing/ has resolved geometry /geom/ { set width [dict get $geom width] set height [dict get $geom height] - set options [drawLabelDefaultOptions $text $width $height] + + set maxLength 0 + foreach line [split $text "\n"] { + set lineLength [string length $line] + if {$lineLength > $maxLength} { + set maxLength $lineLength + } + } + if {$maxLength == 0} { + set scale 0.02 + } else { + set scale [::math::min 0.02 [/ 0.45 $maxLength]] + } + set position [list [expr {$width / 2.0}] [expr {$height / 2.0}]] + set options [dict create \ + position $position \ + scale $scale \ + anchor center \ + font "PTSans-Regular"] + if {[dict exists $geom top] && [dict exists $geom tagSize] && [dict exists $geom bottom]} { - dict set options position \ - [list [expr {$width / 2.0}] \ + dict set options position \ + [list [expr {$width / 2.0}] \ [expr {[dict get $geom top] + [dict get $geom tagSize] + [dict get $geom bottom] / 2.0}]] - } + } - foreach result $results { - set options [dict merge $options [dict get $result options]] - } - dict set options text $text - Wish to draw text onto $thing with {*}$options + foreach result $results { + set options [dict merge $options [dict get $result options]] } + dict set options text $text + Wish to draw text onto $thing with {*}$options } +} - When /someone/ wishes /thing/ is labelled /text/ { - Wish $thing is labelled $text with font "PTSans-Regular" - } +When /someone/ wishes /thing/ is labelled /text/ { + Wish $thing is labelled $text with font "PTSans-Regular" +} From 2c20d3d293b83ca384fe85df82a44f374d1460f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Mon, 1 Jun 2026 15:44:54 -0400 Subject: [PATCH 3/4] Scope PR down to just shape drawing --- builtin-programs/display/arc.folk | 39 --- builtin-programs/display/curve.folk | 135 ----------- builtin-programs/draw/arc.folk | 53 +++++ builtin-programs/draw/circle.folk | 78 +++--- builtin-programs/draw/curve.folk | 79 ++++++ builtin-programs/draw/fill.folk | 144 ++++++----- builtin-programs/draw/line.folk | 69 ++++-- builtin-programs/draw/shapes.folk | 349 +++++++++++++++++++++++++++ builtin-programs/regions.folk | 8 - builtin-programs/shapes.folk | 357 ---------------------------- builtin-programs/shapes/region.folk | 92 ------- lib/math.tcl | 65 +++++ 12 files changed, 735 insertions(+), 733 deletions(-) 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/regions.folk delete mode 100644 builtin-programs/shapes.folk delete mode 100644 builtin-programs/shapes/region.folk diff --git a/builtin-programs/display/arc.folk b/builtin-programs/display/arc.folk deleted file mode 100644 index f6a0c6787..000000000 --- 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 9082d117f..000000000 --- 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 a67b3e4e9..1b5365824 100644 --- a/builtin-programs/draw/circle.folk +++ b/builtin-programs/draw/circle.folk @@ -1,8 +1,8 @@ Wish the GPU compiles pipeline "circle" { {vec2 viewport mat3 surfaceToClip - vec2 center float radius float thickness vec4 color int filled} { - float r = radius + thickness; - vec2 vertices[6] = vec2[6]( + vec2 center float radius float thickness vec4 color int filled} { + float r = radius + thickness; + vec2 vertices[6] = vec2[6]( center - r, vec2(center.x + r, center.y - r), vec2(center.x - r, center.y + r), @@ -10,38 +10,52 @@ Wish the GPU compiles pipeline "circle" { vec2(center.x + r, center.y - r), center + r, vec2(center.x - r, center.y + r) - ); - vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); - return vec4(v.xy/v.z, 0.0, 1.0); - } { - vec2 clipXy = (gl_FragCoord.xy / viewport) * 2.0 - 1.0; - vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); - surfaceXy /= surfaceXy.z; + ); + vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); + return vec4(v.xy/v.z, 0.0, 1.0); + } { + vec2 clipXy = (gl_FragCoord.xy / viewport) * 2.0 - 1.0; + vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); + surfaceXy /= surfaceXy.z; - float dist = length(surfaceXy.xy - center) - radius; - if (filled == 1) { - return (dist < thickness) ? color : vec4(0.0); - } else { - return (dist < thickness && dist > 0.0) ? color : vec4(0.0); + float dist = length(surfaceXy.xy - center) - radius; + if (filled == 1) { + return (dist < thickness) ? color : vec4(0.0); + } else { + return (dist < thickness && dist > 0.0) ? color : vec4(0.0); + } } } -} -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/ { + 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 wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] + set instancesByLayer [dict create] - 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] + 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 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}]] -} + 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 000000000..475ea1d31 --- /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 4e977c1e3..465064dc2 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -9,61 +9,99 @@ Wish the GPU compiles pipeline "fillTriangle" { } When the color map is /colorMap/ { + 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" onto canvas $id with arguments \ + [list $surfaceToClip $p0 $p1 $p2 $color] layer $layer + } + } -When /someone/ wishes to draw a triangle with /...options/ { - 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 - } -} -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 - } -} -When /someone/ wishes to draw a polygon with /...options/ { - set points [dict get $options points] - set color [dict get $options color] - set layer [dict getdef $options layer 0] + 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 instances \ + [list \ + [list $surfaceToClip $p1 $p2 $p3 $color] \ + [list $surfaceToClip $p0 $p1 $p3 $color]] \ + layer $layer + } + } - set num_points [llength $points] - if {$num_points < 3} { - error "At least 3 points are required to form a polygon." - } elseif {$num_points == 3} { - Wish to draw a triangle with \ - p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \ - color $color layer $layer - } elseif {$num_points == 4} { - Wish to draw a quad with \ - p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] p3 [lindex $points 3] \ - color $color layer $layer - } else { - # Get the first point in the list as the "base" point of the triangles - set p0 [lindex $points 0] + 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] - set color [dict getdef $colorMap $color $color] - 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 - } - } -} + set num_points [llength $points] + if {$num_points < 3} { + error "At least 3 points are required to form a polygon." + } elseif {$num_points == 3} { + Wish to draw a triangle 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 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] -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 -} + # 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}]] + 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/ { + set fillOpts [list] + if {[info exists options]} { set fillOpts $options } else { set fillOpts {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 {*}$fillOpts + } + + When /someone/ wishes /page/ draws fill with /...options/ { + set fillOpts [list] + if {[info exists options]} { set fillOpts $options } else { set fillOpts {color white} } + Wish $page is filled with {*}$fillOpts + } + + When /someone/ wishes /thing/ is filled /color/ { + Wish $thing is filled with color $color + } + + When /someone/ wishes /thing/ is filled /color/ with /optionKey/ /optionValue/ /...optionRest/ { + if {![info exists optionKey] || ![info exists optionValue]} { return } + if {![info exists optionRest]} { set optionRest [list] } + Wish $thing is filled with color $color $optionKey $optionValue {*}$optionRest + } + + When /someone/ wishes /thing/ is filled /color/ with /optionKey/ /optionValue/ { + if {![info exists optionKey] || ![info exists optionValue]} { return } + Wish $thing is filled with color $color $optionKey $optionValue + } + } diff --git a/builtin-programs/draw/line.folk b/builtin-programs/draw/line.folk index 96215309e..b8fa1e861 100644 --- a/builtin-programs/draw/line.folk +++ b/builtin-programs/draw/line.folk @@ -1,18 +1,25 @@ Wish the GPU compiles pipeline "line" { {vec2 viewport mat3 surfaceToClip - vec2 from vec2 to float thickness vec4 color} { - vec2 dir = normalize(to - from); - vec2 perp = vec2(-dir.y, dir.x) * thickness/2.0; + vec2 from vec2 to float thickness vec4 color float capFrom float capTo} { + + vec2 diff = to - from; + if (length(diff) < 0.0001) return vec4(0.0); + vec2 dir = normalize(diff); + 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 +27,38 @@ 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 drawLineCapFlags {index segmentCount closed capStyle} { + switch -- $capStyle { + square - flat - butt { + return {1.0 1.0} + } + round - rounded { + return {0.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 +69,20 @@ 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}] + + # 1.0 = flat, 0.0 = round + lassign [drawLineCapFlags 0 $segmentCount 0 $caps] capFrom capTo + + 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] + 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 000000000..718035461 --- /dev/null +++ b/builtin-programs/draw/shapes.folk @@ -0,0 +1,349 @@ +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 {$shape eq "polygon" || $shape eq "polyline"} { return } + + 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 a polygon /points/ with /...options/ &\ + /p/ has resolved geometry /geom/ { + if {![info exists options]} { return } + set parsedPoints [drawShapePathPoints $points $geom $options] + set color [dict getdef $options color white] + set layer [dict getdef $options layer 1] + Wish to draw a polygon onto $p with points $parsedPoints color $color layer $layer +} + +When /someone/ wishes /p/ draws a polygon with points /points/ /...options/ { + # Forgiving syntax for users who write 'draws a polygon with points $points color white' + # and use ( ) instead of { } for points. + set cleanPoints [string map {"(" "{" ")" "}"} $points] + Wish $p draws a polygon $cleanPoints with {*}$options +} + +When /someone/ wishes /p/ draws a polygon /points/ { + Wish $p draws a polygon $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 +} diff --git a/builtin-programs/regions.folk b/builtin-programs/regions.folk deleted file mode 100644 index 945fcd20e..000000000 --- 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 c67c7e434..000000000 --- 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 492a268d8..000000000 --- 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/lib/math.tcl b/lib/math.tcl index 300e1b045..866c623c6 100644 --- a/lib/math.tcl +++ b/lib/math.tcl @@ -3,6 +3,71 @@ # 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 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 +} + namespace eval ::vec2 { proc add {a b} { list [+ [lindex $a 0] [lindex $b 0]] [+ [lindex $a 1] [lindex $b 1]] From 730172487727cca10e7efbf5d43d27e165829afe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Cuervo?= Date: Mon, 1 Jun 2026 19:45:23 -0400 Subject: [PATCH 4/4] Fix circle rendering via branchless float bounds shader and fill.folk early return on 3-point/4-point quads --- Makefile | 7 +++--- builtin-programs/draw/circle.folk | 40 +++++++++++++++++-------------- builtin-programs/draw/fill.folk | 2 ++ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 17b5f9dd6..aab0e1563 100644 --- a/Makefile +++ b/Makefile @@ -115,19 +115,20 @@ kill-folk: fi FOLK_REMOTE_NODE ?= folk-live +GIT_DIR := $(shell git rev-parse --git-dir 2>/dev/null || echo .git) 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 + > $(GIT_DIR)/ignores.tmp || true + git ls-files --exclude-standard -oi --directory >> $(GIT_DIR)/ignores.tmp rsync --timeout=15 -e "ssh -o StrictHostKeyChecking=no" \ --archive --delete --itemize-changes \ --exclude='/.git' \ --exclude='vendor/tracy/public/TracyClient.o' \ --include='vendor/tracy/public/***' \ --exclude='vendor/tracy/*' \ - --exclude-from='.git/ignores.tmp' \ + --exclude-from='$(GIT_DIR)/ignores.tmp' \ ./ $(FOLK_REMOTE_NODE):~/folk/ remote-setup: diff --git a/builtin-programs/draw/circle.folk b/builtin-programs/draw/circle.folk index 1b5365824..bc8c88ee9 100644 --- a/builtin-programs/draw/circle.folk +++ b/builtin-programs/draw/circle.folk @@ -1,15 +1,15 @@ Wish the GPU compiles pipeline "circle" { {vec2 viewport mat3 surfaceToClip - vec2 center float radius float thickness vec4 color int filled} { - float r = radius + thickness; + vec2 center float innerRadius float outerRadius vec4 color} { + float r = outerRadius; vec2 vertices[6] = vec2[6]( - center - r, - vec2(center.x + r, center.y - r), - vec2(center.x - r, center.y + r), + vec2(center.x - r, center.y - r), + vec2(center.x + r, center.y - r), + vec2(center.x - r, center.y + r), - vec2(center.x + r, center.y - r), - center + r, - vec2(center.x - r, center.y + r) + vec2(center.x + r, center.y - r), + vec2(center.x + r, center.y + r), + vec2(center.x - r, center.y + r) ); vec3 v = surfaceToClip * vec3(vertices[gl_VertexIndex], 1.0); return vec4(v.xy/v.z, 0.0, 1.0); @@ -18,23 +18,20 @@ Wish the GPU compiles pipeline "circle" { vec3 surfaceXy = inverse(surfaceToClip) * vec3(clipXy, 1.0); surfaceXy /= surfaceXy.z; - float dist = length(surfaceXy.xy - center) - radius; - if (filled == 1) { - return (dist < thickness) ? color : vec4(0.0); - } else { - return (dist < thickness && dist > 0.0) ? color : vec4(0.0); - } + float dist = length(surfaceXy.xy - center); + return (dist > innerRadius && dist < outerRadius) ? color : vec4(0.0); } } 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/ { + the collected results for [list /someone/ wishes to draw a circle onto /canvas/ with /...options/] are /results/ { set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] set instancesByLayer [dict create] foreach result $results { + if {[dict get $result canvas] ne $p} { continue } set options [dict get $result options] set center [dict getdef $options center ""] if {$center eq ""} { @@ -45,13 +42,20 @@ Wish the GPU compiles pipeline "circle" { 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 filled [drawTruthy [dict getdef $options filled false]] set layer [dict getdef $options layer 0] + if {$filled} { + set innerRadius 0.0 + } else { + set innerRadius $radius + } + set outerRadius [expr {$radius + $thickness}] + + dict lappend instancesByLayer $layer \ [list $wiResolution $surfaceToClip \ - $center $radius $thickness $color \ - [expr {$filled eq false ? 0 : 1}]] + $center $innerRadius $outerRadius $color] } dict for {layer instances} $instancesByLayer { diff --git a/builtin-programs/draw/fill.folk b/builtin-programs/draw/fill.folk index 465064dc2..cc0bb87be 100644 --- a/builtin-programs/draw/fill.folk +++ b/builtin-programs/draw/fill.folk @@ -48,10 +48,12 @@ When the color map is /colorMap/ { Wish to draw a triangle onto $p with \ p0 [lindex $points 0] p1 [lindex $points 1] p2 [lindex $points 2] \ color $color layer $layer + return } elseif {$num_points == 4} { 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 + return } else { # Get the first point in the list as the "base" point of the triangles set p0 [lindex $points 0]