From 5c5c42a2fafd96ceb97e880522fa22628c03981f Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 13 May 2026 12:12:33 -0400 Subject: [PATCH 01/45] Makefile: fix tracy build? --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 30882ab8..7211b66b 100644 --- a/Makefile +++ b/Makefile @@ -124,10 +124,10 @@ sync: rsync --timeout=15 -e "ssh -o StrictHostKeyChecking=no" \ --archive --delete --itemize-changes \ --exclude='/.git' \ - --exclude-from='.git/ignores.tmp' \ --exclude='vendor/tracy/public/TracyClient.o' \ --include='vendor/tracy/public/***' \ --exclude='vendor/tracy/*' \ + --exclude-from='.git/ignores.tmp' \ ./ $(FOLK_REMOTE_NODE):~/folk/ remote-setup: From 33119bc4bd181de0398d9ddbe3dc2b57e18e48e9 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 13 May 2026 14:14:43 -0400 Subject: [PATCH 02/45] print: Force scaling to none This doesn't seem to affect folk-hex, but it might affect other systems / require re-measuring geometry. This still doesn't provide perfectly calibrated prints: I think there are some margin issues: https://moral.net.au/writing/2023/05/19/printer_actual_size/ --- builtin-programs/print.folk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin-programs/print.folk b/builtin-programs/print.folk index e8a04e16..a7177acb 100644 --- a/builtin-programs/print.folk +++ b/builtin-programs/print.folk @@ -333,7 +333,7 @@ Subscribe: print program /id/ with /...options/ { exec ps2pdf $saveDir/$id.ps $saveDir/$id.pdf puts "Printing program $id on $::thisNode" - exec lpr {*}$args $saveDir/$id.pdf + exec lpr -o print-scaling=none {*}$args $saveDir/$id.pdf } } From c50acded50bfd4e1a94d80df027d2f9ea67cbe70 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 13 May 2026 18:12:20 -0400 Subject: [PATCH 03/45] gpu/textures: Don't hard crash if we run out of texture slots --- builtin-programs/gpu/textures.folk | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/builtin-programs/gpu/textures.folk b/builtin-programs/gpu/textures.folk index 6321c8ac..0cf2cebe 100644 --- a/builtin-programs/gpu/textures.folk +++ b/builtin-programs/gpu/textures.folk @@ -439,18 +439,21 @@ $gpuc proc copyImageToRgba {Image im Image ret} void { $gpuc code [csubst { GpuTextureHandle allocateGpuTextureHandle() { - for (int i = 0; i < getMaxTextures(); i++) { - bool notAlive = false; - if (atomic_compare_exchange_weak(&gpuTextures[i].alive, ¬Alive, true)) { - gpuTextures[i].handle = i; - return i; + for (;;) { + for (int i = 0; i < getMaxTextures(); i++) { + bool notAlive = false; + if (atomic_compare_exchange_weak(&gpuTextures[i].alive, ¬Alive, true)) { + gpuTextures[i].handle = i; + return i; + } } + fprintf(stderr, "gpu/textures: Exceeded GPU max textures (%d):\n", getMaxTextures()); + for (int i = 0; i < getMaxTextures(); i++) { + fprintf(stderr, " %d: %s\n", i, gpuTextures[i].alive ? gpuTextures[i].description : ""); + } + struct timespec ts = {0, 5000000}; + nanosleep(&ts, NULL); } - fprintf(stderr, "gpu/textures: Exceeded GPU max textures (%d):\n", getMaxTextures()); - for (int i = 0; i < getMaxTextures(); i++) { - fprintf(stderr, " %d: %s\n", i, gpuTextures[i].alive ? gpuTextures[i].description : ""); - } - exit(1); } }] From 8e38e4e2f609d62341b3ce4326caba4017f95959 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 13 May 2026 21:35:51 -0400 Subject: [PATCH 04/45] prelude: Make Folk Tracy zones reentrant --- prelude.tcl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/prelude.tcl b/prelude.tcl index 37b4238a..e8a791b8 100644 --- a/prelude.tcl +++ b/prelude.tcl @@ -779,7 +779,9 @@ if {[__isTracyEnabled]} { TracyCSetThreadName(strdup(name)); } $tracyCpp code { - __thread TracyCZoneCtx __zoneCtx; + #define __ZONE_CTX_MAX 16 + __thread TracyCZoneCtx __zoneCtxs[__ZONE_CTX_MAX]; + __thread int __zoneCtxIdx; } $tracyCpp proc zoneBegin {} void { Jim_Obj* scriptObj = interp->evalFrame->scriptObj; @@ -801,13 +803,23 @@ if {[__isTracyEnabled]} { fnName != NULL ? fnName : "", fnName != NULL ? strlen(fnName) : strlen(""), 0); - __zoneCtx = ___tracy_emit_zone_begin_alloc(loc, 1); + if (__zoneCtxIdx >= __ZONE_CTX_MAX) { + fprintf(stderr, "tracy: zone stack overflow (max %d)\n", __ZONE_CTX_MAX); + exit(1); + } + __zoneCtxs[__zoneCtxIdx++] = ___tracy_emit_zone_begin_alloc(loc, 1); } $tracyCpp proc zoneName {char* name} void { - ___tracy_emit_zone_name(__zoneCtx, name, strlen(name)); + if (__zoneCtxIdx > 0) { + ___tracy_emit_zone_name(__zoneCtxs[__zoneCtxIdx - 1], name, strlen(name)); + } } $tracyCpp proc zoneEnd {} void { - ___tracy_emit_zone_end(__zoneCtx); + if (__zoneCtxIdx <= 0) { + fprintf(stderr, "tracy: zone stack underflow\n"); + exit(1); + } + ___tracy_emit_zone_end(__zoneCtxs[--__zoneCtxIdx]); } return [$tracyCpp compile $tracyCid] } From 7aeeb11e48730cc26971094ad2824dd2e3f76744 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 14 May 2026 13:11:03 -0400 Subject: [PATCH 05/45] WIP: rename print (will add mixin next) --- builtin-programs/{ => print}/print.folk | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename builtin-programs/{ => print}/print.folk (98%) diff --git a/builtin-programs/print.folk b/builtin-programs/print/print.folk similarity index 98% rename from builtin-programs/print.folk rename to builtin-programs/print/print.folk index a7177acb..37c89844 100644 --- a/builtin-programs/print.folk +++ b/builtin-programs/print/print.folk @@ -239,7 +239,9 @@ fn nextId {} { incr id } - Hold! -save -key next-id the next program id is $id + # HACK: using old path for backward compatibility. + Hold! -save -on builtin-programs/print.folk -key next-id \ + the next program id is $id set id } @@ -333,7 +335,7 @@ Subscribe: print program /id/ with /...options/ { exec ps2pdf $saveDir/$id.ps $saveDir/$id.pdf puts "Printing program $id on $::thisNode" - exec lpr -o print-scaling=none {*}$args $saveDir/$id.pdf + exec lpr {*}$args $saveDir/$id.pdf } } From 4c491168f8535d2b3bdfe990cd737f8f815bc1f5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 11:28:30 -0400 Subject: [PATCH 06/45] db: Hopefully fix race between matchDestroy and dbMatchInsert where there was a risk we would reuse a not-yet-destroyed match and then accidentally stomp its destructorSet in the final stage of match destruction. This now uses a sentinel to distinguish 'removed' and 'fully destroyed' stages. --- db.c | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/db.c b/db.c index 368af7ad..591444fa 100644 --- a/db.c +++ b/db.c @@ -248,10 +248,18 @@ typedef struct Match { pthread_mutex_t destructorSetMutex; // ListOfEdgeTo StatementRef. Used for removal. - ListOfEdgeTo* childStatements; + // NULL means the slot is fully destroyed and ready for reuse (matchNew + // checks this). CHILD_STATEMENTS_REMOVING means matchRemoveSelf has + // claimed removal but matchDestroy hasn't finished yet. + ListOfEdgeTo* _Atomic childStatements; pthread_mutex_t childStatementsMutex; } Match; +// Sentinel: matchRemoveSelf sets childStatements to this to prevent new +// children from being added. matchDestroy then sets it to NULL (release) +// as its final step, which is what matchNew waits for. +#define CHILD_STATEMENTS_REMOVING ((ListOfEdgeTo*)1) + // Database datatypes: typedef struct Hold { @@ -750,7 +758,8 @@ static MatchRef matchNew(Db* db, match = &db->matchPool[idx]; GenRc oldGenRc = match->genRc; - if (oldGenRc.rc == 0 && !oldGenRc.alive && match->childStatements == NULL) { + if (oldGenRc.rc == 0 && !oldGenRc.alive && + atomic_load_explicit(&match->childStatements, memory_order_acquire) == NULL) { GenRc newGenRc = oldGenRc; newGenRc.alive = true; @@ -763,7 +772,7 @@ static MatchRef matchNew(Db* db, // We should have exclusive access to match right now. - match->childStatements = listOfEdgeToNew(8); + atomic_store_explicit(&match->childStatements, listOfEdgeToNew(8), memory_order_relaxed); match->parentWasRemoved = false; pthread_mutexattr_t mta; @@ -783,12 +792,17 @@ static MatchRef matchNew(Db* db, } static void matchDestroy(Match* match) { - assert(match->childStatements == NULL); + assert(atomic_load_explicit(&match->childStatements, memory_order_relaxed) + == CHILD_STATEMENTS_REMOVING); // Fire any destructors. pthread_mutex_lock(&match->destructorSetMutex); destructorSetReleaseAll(&match->destructorSet); pthread_mutex_unlock(&match->destructorSetMutex); + + // Release store: synchronizes with matchNew's acquire load so that + // all writes above are visible before the slot is reused. + atomic_store_explicit(&match->childStatements, NULL, memory_order_release); } AtomicallyVersion* matchAtomicallyVersion(Match* m) { @@ -803,8 +817,9 @@ static bool statementChecker(void* db, uint64_t ref) { } // You must call this with the childStatementsMutex held. static void matchAddChildStatement(Db* db, Match* match, StatementRef child) { - listOfEdgeToAdd(statementChecker, db, - &match->childStatements, child.val); + ListOfEdgeTo* list = atomic_load_explicit(&match->childStatements, memory_order_relaxed); + listOfEdgeToAdd(statementChecker, db, &list, child.val); + atomic_store_explicit(&match->childStatements, list, memory_order_relaxed); } void matchAddDestructor(Match* m, Destructor* d) { pthread_mutex_lock(&m->destructorSetMutex); @@ -838,8 +853,8 @@ void matchRemoveSelf(Db* db, Match* match) { // Walk through each child statement and remove this match as a // parent of that statement. pthread_mutex_lock(&match->childStatementsMutex); - ListOfEdgeTo* childStatements = match->childStatements; - if (childStatements == NULL) { + ListOfEdgeTo* childStatements = atomic_load_explicit(&match->childStatements, memory_order_relaxed); + if (childStatements == NULL || childStatements == CHILD_STATEMENTS_REMOVING) { // Someone else has done / is doing removal. Abort. pthread_mutex_unlock(&match->childStatementsMutex); return; @@ -847,7 +862,7 @@ void matchRemoveSelf(Db* db, Match* match) { // This blocks further child statements from being added to this // match (if they were added, then we wouldn't be able to remove // them). - match->childStatements = NULL; + atomic_store_explicit(&match->childStatements, CHILD_STATEMENTS_REMOVING, memory_order_relaxed); genRcMarkAsDead(&match->genRc); pthread_mutex_unlock(&match->childStatementsMutex); @@ -1187,7 +1202,8 @@ Statement* dbInsertOrReuseStatement(Db* db, Clause* clause, } pthread_mutex_lock(&parentMatch->childStatementsMutex); - if (parentMatch->childStatements == NULL) { + ListOfEdgeTo* cs = atomic_load_explicit(&parentMatch->childStatements, memory_order_relaxed); + if (cs == NULL || cs == CHILD_STATEMENTS_REMOVING) { pthread_mutex_unlock(&parentMatch->childStatementsMutex); matchRelease(db, parentMatch); From b8b00fff5bdf367a95b18ed04bc4c61e4bc50f00 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 11:38:05 -0400 Subject: [PATCH 07/45] gpu/canvases: Support canvas priority Want to use for on-top editor. --- builtin-programs/gpu/canvases.folk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builtin-programs/gpu/canvases.folk b/builtin-programs/gpu/canvases.folk index 8e0f7de0..00a2a954 100644 --- a/builtin-programs/gpu/canvases.folk +++ b/builtin-programs/gpu/canvases.folk @@ -268,8 +268,8 @@ When the GPU library is /gpuLib/ &\ When /someone/ wishes the GPU creates canvas /id/ with /...options/ { puts "Create canvas: $id" - set width [dict get $options width] - set height [dict get $options height] + set width [dict getdef $options width 1024] + set height [dict getdef $options height 1024] set settle [dict getdef $options settle 3ms] set wi [$gpuCanvasLib create $width $height] @@ -515,7 +515,8 @@ When the GPU library is /gpuLib/ &\ Wish the GPU draws pipeline "composite-canvas" with arguments \ [list [list $dispWidth $dispHeight] \ $surfaceToClip \ - [dict get $canvOpts texture] $a $b $c $d] + [dict get $canvOpts texture] $a $b $c $d] \ + priority [dict getdef $canvOpts priority 0] } } } From e521fb2f2c24e6f8658231e7647bee8aff80d86c Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 11:54:12 -0400 Subject: [PATCH 08/45] web/db-lib: Support sentinel --- builtin-programs/web/db-lib.folk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/builtin-programs/web/db-lib.folk b/builtin-programs/web/db-lib.folk index 2b5b875a..ba1d7b5d 100644 --- a/builtin-programs/web/db-lib.folk +++ b/builtin-programs/web/db-lib.folk @@ -108,12 +108,17 @@ Claim the db library is [apply {{} { matchRelease(db, match); return alive; } + $cc code { + #define CHILD_STATEMENTS_REMOVING ((ListOfEdgeTo*)1) + } $cc proc childStatements {Db* db MatchRef matchRef} Jim_Obj* { Match* match = matchAcquire(db, matchRef); if (match == NULL) { return Jim_NewStringObj(interp, "", -1); } pthread_mutex_lock(&match->childStatementsMutex); - if (match->childStatements == NULL) { + if (match->childStatements == NULL || + match->childStatements == CHILD_STATEMENTS_REMOVING) { + pthread_mutex_unlock(&match->childStatementsMutex); matchRelease(db, match); return Jim_NewEmptyStringObj(interp); From a36b3aa05e87c78a2e872195d44c5961bf2ace30 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 12:32:52 -0400 Subject: [PATCH 09/45] WIP: Editor on top of the page itself. Goal here is to be able to prototype and preview fields at scale / in context while writing a program that uses them. The detached editor wouldn't work for this. --- builtin-programs/editor.folk | 2 + builtin-programs/editor/editor.folk | 504 ++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+) create mode 100644 builtin-programs/editor/editor.folk diff --git a/builtin-programs/editor.folk b/builtin-programs/editor.folk index 952b6b58..edaff9cf 100644 --- a/builtin-programs/editor.folk +++ b/builtin-programs/editor.folk @@ -1,3 +1,5 @@ +error "Disabling the standard editor while we prototype the inline editor." + # This makes all keyboards into editors automatically, so a keyboard # doesn't need to point at a real editor. May choose to change later, or # exclude keyboards that opt out. diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk new file mode 100644 index 00000000..8e61f98d --- /dev/null +++ b/builtin-programs/editor/editor.folk @@ -0,0 +1,504 @@ +# This makes all keyboards create editors automatically. May choose to +# change later, or exclude keyboards that opt out. +When /k/ is a keyboard with /...opts/ { + Wish tag $k is stabilized + + When $k points up with length 0.3 at /program/ &\ + /nobody/ claims /program/ is an editor { + # Create a synthetic editor on top of the program being edited,. + set editor [list $k editor] + Claim $editor is an editor + Claim editor $editor has selected program $program + Wish $editor has a canvas with priority 99 + + When $program has resolved geometry /geom/ { + Claim $editor has resolved geometry $geom + } + When $program has quad /q/ { Claim $editor has quad $q } + Claim $k has created editor $editor + Claim $k is typing into $editor + } +} + +When the program save directory is /programDir/ &\ + the editor utils library is /utils/ { + +# TODO: also don't hardcode this? +set margin [list 0.01 0.005 0.005 0.01] ;# CSS order (top, right, bottom, left) +set defaults { textScale 0.01 } + +set editorLib [library create editorLib {margin defaults} { + proc getAdvance {em} { + # From NeomatrixCode.csv + return $(0.5859375 * $em) + } + + proc widthAndHeight {resolvedGeom} { + set tagSize [dict get $resolvedGeom tagSize] + set left [dict get $resolvedGeom left] + set right [dict get $resolvedGeom right] + set top [dict get $resolvedGeom top] + set bottom [dict get $resolvedGeom bottom] + + set width $($left + $tagSize + $right) + set height $($top + $tagSize + $bottom) + + return [list $width $height] + } + + # given program and the editor options, figure out how many characters can + # fit in this editor + proc editorSizeInCharacters {resolvedGeom options} { + variable margin + + set textScale [dict get $options scale] + set advance [getAdvance $textScale] + + lassign [widthAndHeight $resolvedGeom] width height + set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1]) + set height $($height - [lindex $margin 0] - [lindex $margin 2]) + + set widthInCharacters $(int($width / $advance)) + set heightInCharacters $(int($height / $textScale)) + + return [list $widthInCharacters $heightInCharacters] + } +}] + +When /someone/ claims /editor/ is an editor { + Claim $editor is an editor with {*}$defaults +} + +When /editor/ is an editor with /...options/ { + # HACK: because we partial match + if {![info exists options]} { return } + + set options [dict merge $defaults $options] + + # Initial setup + # Load initial text settings if not set + When /nobody/ claims editor $editor has font options /...anything/ &\ + the saved holds are loaded { + # Wait until all the saved holds are loaded, so we don't accidentally + # overwrite a saved setting. + set textScale [dict get $options textScale] + Hold! -save -key font-options-of:$editor \ + Claim editor $editor has font options with scale $textScale + } + + # We need the editor's resolved geometry to get its size. + # We then use its size to figure out how many characters we can fit in it, + # width-wise and height-wise. + When $editor has resolved geometry /geom/ &\ + editor $editor has font options with /...options/ { + Claim editor $editor has viewport size [$editorLib editorSizeInCharacters $geom $options] + } + + # Load in defaults for the editor if it hasn't been initialized + set results [Query! /somebody/ claims editor $editor has cursor /anything/] + if {[llength $results] == 0} { + Hold! -key editor-state-of:$editor { + Claim editor $editor has cursor 0 + Claim editor $editor has max cursor x 0 + Claim editor $editor has viewport position [list 0 0] + Claim editor $editor has selection anchor "" + Claim editor $editor has undo stack {} + Claim editor $editor has redo stack {} + Claim editor $editor has last edit type "" + } + } + + set clipResults [Query! /somebody/ claims editor $editor has clipboard /anything/] + if {[llength $clipResults] == 0} { + Hold! -key clipboard-of:$editor Claim editor $editor has clipboard "" + } + + # Load in initial program code (note, this buffer is shared between + # all editor instances) + When editor $editor has selected program /program/ &\ + /program/ has program code /programCode/ &\ + the saved holds are loaded { + # Check if the code already exists. + set results [Query! editor buffer for $program is /anything/] + if {[llength $results] == 0} { + Hold! -save -key buffer-for:$program \ + Claim editor buffer for $program is $programCode + } + } + + # Feedback: show that the editor is not active + When /nobody/ wishes $editor is outlined green { + Wish $editor is outlined blue + } +} + +Subscribe: keyboard /keyboard/ claims key Control_b is down with /...options/ { + Notify: print code "# blank" +} + +When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ + /keyboard/ is typing into /editor/ &\ + /editor/ is an editor with /...anything/ &\ + editor /editor/ has selected program /program/ { + Wish $editor is outlined green + + Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { + ForEach! editor $editor has viewport position /vpPos/ &\ + editor $editor has viewport size /vpSize/ &\ + editor buffer for $program is /code/ &\ + editor $editor has max cursor x /maxCursorX/ &\ + editor $editor has cursor /cursor/ &\ + editor $editor has selection anchor /selAnchor/ &\ + editor $editor has clipboard /clipboard/ &\ + editor $editor has undo stack /undoStack/ &\ + editor $editor has redo stack /redoStack/ &\ + editor $editor has last edit type /lastEditType/ &\ + editor $editor has font options with /...textOptions/ { + lassign $vpPos vpX vpY + lassign $vpSize vpWidth vpHeight + + if {$keyState == "up"} { return } + + # if this is true, the buffer will remove the hold for program code + # (triggering a reinitialization) + set resetBuffer false + set isUndo false + set isRedo false + set origCode $code + set origCursor $cursor + set origMaxCursorX $maxCursorX + + set hasShift [dict exists $options shift] + set isNavKey [expr {$key eq "Left" || $key eq "Right" || $key eq "Up" || $key eq "Down"}] + + # Handle selection replacement for text-modifying keys + set selectionHandled false + if {$selAnchor ne ""} { + set selStart [min $selAnchor $cursor] + set selEnd [max $selAnchor $cursor] + + if {[dict exists $options printable]} { + lassign [$utils replaceRange $code $selStart $selEnd [dict get $options printable]] code cursor maxCursorX + set selAnchor "" + set selectionHandled true + } elseif {$key eq "Delete" || $key eq "Remove"} { + lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX + set selAnchor "" + set selectionHandled true + } elseif {$key eq "Return"} { + # Delete selection, then fall through to Return handler + lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX + set selAnchor "" + } + } + + if {!$selectionHandled} { + if {$hasShift && $isNavKey} { + # Shift+Arrow: extend selection + if {$selAnchor eq ""} { set selAnchor $cursor } + lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX + } elseif {[dict exists $options printable]} { + lassign [$utils insertText $code $cursor [dict get $options printable]] code cursor maxCursorX + } else { + # Regular navigation clears selection + if {$isNavKey} { set selAnchor "" } + + # general editor functionality + lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX + lassign [$utils handleRemovalAndReturn $key $code $cursor $maxCursorX] code cursor maxCursorX + + # specific editor functionality + switch $key { + Control_c { + if {$selAnchor ne ""} { + set clipText [$utils getSelectedText $code $selAnchor $cursor] + Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText + } + } + Control_x { + if {$selAnchor ne ""} { + set clipText [$utils getSelectedText $code $selAnchor $cursor] + Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText + set selStart [min $selAnchor $cursor] + set selEnd [max $selAnchor $cursor] + lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX + set selAnchor "" + } + } + Control_v { + if {$clipboard ne ""} { + if {$selAnchor ne ""} { + set selStart [min $selAnchor $cursor] + set selEnd [max $selAnchor $cursor] + lassign [$utils replaceRange $code $selStart $selEnd $clipboard] code cursor maxCursorX + set selAnchor "" + } else { + lassign [$utils replaceRange $code $cursor $cursor $clipboard] code cursor maxCursorX + } + } + } + Control_z { + if {[llength $undoStack] > 0} { + lappend redoStack [list $code $cursor $maxCursorX] + lassign [lindex $undoStack end] code cursor maxCursorX + set undoStack [lrange $undoStack 0 end-1] + set selAnchor "" + set isUndo true + set lastEditType "" + } + } + Control_y { + if {[llength $redoStack] > 0} { + lappend undoStack [list $code $cursor $maxCursorX] + lassign [lindex $redoStack end] code cursor maxCursorX + set redoStack [lrange $redoStack 0 end-1] + set selAnchor "" + set isRedo true + set lastEditType "" + } + } + Control_r { + set resetBuffer true + } + Control_s { + Notify: save code on editor $editor + } + Control_p { + Notify: print code $code + + # Give the user some feedback + Hold! -key printing-alert:$editor \ + Wish $editor is labelled "Printing!" + sleep 0.25 + Hold! -key printing-alert:$editor {} + } + Control_underscore { + # ctrl and - (zoom out) + set textScale [dict get $textOptions scale] + set textScale [/ $textScale 1.1] + + Hold! -save -key font-options-of:$editor \ + Claim editor $editor has font options with scale $textScale + } + equal { + # Dunno why it registers as equal instead of Control_equal? It works regardless, lol + # ctrl and + (zoom in) + set textScale [dict get $textOptions scale] + set textScale [* $textScale 1.1] + + Hold! -save -key font-options-of:$editor \ + Claim editor $editor has font options with scale $textScale + } + } + } + } + + # Undo stack management with coalescing + if {$code ne $origCode && !$isUndo && !$isRedo} { + # Determine edit type for coalescing consecutive same-type edits + if {[dict exists $options printable]} { + set editType "insert" + } elseif {$key eq "Delete" || $key eq "Remove"} { + set editType "delete" + } else { + set editType "other" + } + + # Only push when edit type changes or for non-coalescable edits + if {$editType ne $lastEditType || $editType eq "other"} { + lappend undoStack [list $origCode $origCursor $origMaxCursorX] + if {[llength $undoStack] > 50} { + set undoStack [lrange $undoStack end-49 end] + } + } + set lastEditType $editType + + # Any real edit clears the redo stack + set redoStack {} + } elseif {!$isUndo && !$isRedo} { + # Navigation or no-op resets edit type (breaks coalescing) + set lastEditType "" + } + + if {$resetBuffer} { + set cursor 0 + set maxCursorX 0 + set selAnchor "" + set undoStack {} + set redoStack {} + set lastEditType "" + + # remove the edited code to restore the program to its original code + Hold! -on builtin-programs/programs.folk -key new-code-for:$program {} + file delete "$programDir/[regsub {\.folk$} $program {}].folk.edited" + + Hold! -save -key buffer-for:$program { + When $program has program code /originalCode/ { + Claim editor buffer for $program is $originalCode + } + } + } else { + Hold! -save -key buffer-for:$program \ + Claim editor buffer for $program is $code + } + + lassign [$utils cursorToXy $code $cursor] cursorX cursorY + # Be sure to have at least two characters on the left, so we can + # see what we're removing. + if {[- $cursorX 2] < $vpX} { + set vpX [max 0 [- $cursorX 2]] + } + if {$cursorX >= $vpX + $vpWidth} { + set vpX $($cursorX - $vpWidth) + } + if {$cursorY < $vpY} { set vpY $cursorY } + if {$cursorY >= $vpY + $vpHeight - 1} { + set vpY $($cursorY - $vpHeight + 1) + } + + Hold! -keep 12ms -key editor-state-of:$editor { + Claim editor $editor has viewport position [list $vpX $vpY] + Claim editor $editor has cursor $cursor + Claim editor $editor has max cursor x $maxCursorX + Claim editor $editor has selection anchor $selAnchor + Claim editor $editor has undo stack $undoStack + Claim editor $editor has redo stack $redoStack + Claim editor $editor has last edit type $lastEditType + } + } + } +} + +Subscribe: save code on editor /editor/ { + ForEach! editor $editor has selected program /program/ &\ + editor buffer for /program/ is /programCode/ { + Hold! -on builtin-programs/programs.folk -key new-code-for:$program \ + Wish program $program is replaced with \ + code $programCode editedTime [clock seconds] + + set programBase [regsub {\.folk$} $program ""] + set editedPath "$programDir/[set programBase].folk.edited" + file mkdir [file dirname $editedPath] + set fp [open $editedPath w] + puts -nonewline $fp $programCode + close $fp + + # Give the user some feedback + Hold! -key saved-alert:$editor \ + Wish $editor is labelled "Saved!" + sleep 0.25 + Hold! -key saved-alert:$editor {} + } +} + +# calculate cursor position +When /editor/ is an editor with /...anything/ &\ + editor /editor/ has cursor /cursor/ &\ + editor /editor/ has selected program /program/ &\ + editor buffer for /program/ is /code/ &\ + editor /editor/ has viewport position /vpPos/ &\ + editor /editor/ has font options with /...textOptions/ { + lassign $vpPos vpX vpY + + lassign [$utils cursorToXy $code $cursor] cursorX cursorY + + set textScale [dict get $textOptions scale] + set advance [$editorLib getAdvance $textScale] + + set offsetX $(($cursorX - $vpX) * $advance) + set offsetY $(($cursorY - $vpY) * $textScale) + + Claim editor $editor has cursor position [list $offsetX $offsetY] +} + +# Draw text and cursor +When /editor/ is an editor with /...anything/ &\ + editor /editor/ has viewport position /vpPos/ &\ + editor /editor/ has viewport size /vpSize/ &\ + editor /editor/ has font options with /...fontOptions/ { + lassign $vpPos vpX vpY + lassign $vpSize vpWidth vpHeight + + set textScale [dict get $fontOptions scale] + set advance [$editorLib getAdvance $textScale] + + When editor $editor has selected program /program/ &\ + editor buffer for /program/ is /code/ &\ + editor $editor has cursor /cursor/ &\ + editor $editor has cursor position /cursorPos/ &\ + editor $editor has selection anchor /selAnchor/ { + set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] + set lineNumbers [$utils lineNumberView $vpY $lineCount] + + set marginLeft [lindex $margin 3] + set lineNumbersRight $($marginLeft + $advance*1.5) + Wish to draw text onto $editor with \ + position [list $lineNumbersRight [lindex $margin 0]] \ + text $lineNumbers \ + scale $textScale anchor topright font NeomatrixCode + + set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] + set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] + Wish to draw text onto $editor with \ + position $pos text $text \ + scale $textScale anchor topleft font NeomatrixCode + + # Draw selection highlight + if {$selAnchor ne ""} { + set rawStart [min $selAnchor $cursor] + set rawEnd [max $selAnchor $cursor] + lassign [$utils cursorToXy $code $rawStart] selStartX selStartY + lassign [$utils cursorToXy $code $rawEnd] selEndX selEndY + + set lines [split $code "\n"] + for {set ly $selStartY} {$ly <= $selEndY} {incr ly} { + if {$ly < $vpY || $ly >= $vpY + $vpHeight} continue + + set lineLen [string length [lindex $lines $ly]] + + # Absolute column range for selection on this line + if {$ly == $selStartY} { + set absStart $selStartX + } else { + set absStart 0 + } + if {$ly == $selEndY} { + set absEnd $selEndX + } else { + set absEnd $lineLen + } + + # Clip to viewport + set absStart [max $absStart $vpX] + set absEnd [min $absEnd [+ $vpX $vpWidth]] + + if {$absStart >= $absEnd} continue + + # Convert to display coordinates + set dispStart [- $absStart $vpX] + set dispEnd [- $absEnd $vpX] + set dispRow [- $ly $vpY] + + set x0 $([lindex $pos 0] + $dispStart * $advance) + set x1 $([lindex $pos 0] + $dispEnd * $advance) + set y0 $([lindex $pos 1] + $dispRow * $textScale) + set y1 $($y0 + $textScale) + + Wish to draw a quad onto $editor with \ + p0 [list $x0 $y0] p1 [list $x1 $y0] \ + p2 [list $x1 $y1] p3 [list $x0 $y1] \ + color {0.2 0.4 0.8 0.7} layer -1 + } + } + + set p1 [vec2 add $cursorPos $pos] + set p2 [vec2 add $p1 [list 0 [* $textScale 1.2]]] + set s [/ $textScale 6] + Wish to draw a circle onto $editor with center $p1 radius $s thickness 0 color green filled true + Wish to draw a line onto $editor with points [list $p1 $p2] width $s color green + } +} + + +# end of library code +} From e723a3e5f6aaf0ae04c29600a9642f97db1d9bc9 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 12:34:41 -0400 Subject: [PATCH 10/45] db: WIP: Lock current item on kill print (to try to fix UAF) --- db.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db.c b/db.c index 591444fa..d61b983c 100644 --- a/db.c +++ b/db.c @@ -881,7 +881,11 @@ void matchRemoveSelf(Db* db, Match* match) { // execution. ThreadControlBlock *workerThread = &threads[match->workerThreadIndex]; if (timestamp_get(workerThread->clockid) - workerThread->currentItemStartTimestamp > 100000000) { - char buf[10000]; traceItem(buf, sizeof(buf), workerThread->currentItem); + mutexLock(&workerThread->currentItemMutex); + WorkQueueItem item = workerThread->currentItem; + mutexUnlock(&workerThread->currentItemMutex); + + char buf[10000]; traceItem(buf, sizeof(buf), item); fprintf(stderr, "KILL (%.150s)\n", buf); kill(workerThread->tid, SIGUSR1); } From 315e1b8efcb10e3a4c4477012d446d2fe3424110 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 13:33:02 -0400 Subject: [PATCH 11/45] Fix editor canvas layer + layers and options forwarding in general --- builtin-programs/editor/editor.folk | 2 +- builtin-programs/gpu/canvases.folk | 14 +++++++------- builtin-programs/tags-to-quads.folk | 7 ++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index 8e61f98d..983d74fd 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -9,7 +9,7 @@ When /k/ is a keyboard with /...opts/ { set editor [list $k editor] Claim $editor is an editor Claim editor $editor has selected program $program - Wish $editor has a canvas with priority 99 + Wish $editor has a canvas with layer 99 When $program has resolved geometry /geom/ { Claim $editor has resolved geometry $geom diff --git a/builtin-programs/gpu/canvases.folk b/builtin-programs/gpu/canvases.folk index 00a2a954..52ea3a90 100644 --- a/builtin-programs/gpu/canvases.folk +++ b/builtin-programs/gpu/canvases.folk @@ -268,14 +268,14 @@ When the GPU library is /gpuLib/ &\ When /someone/ wishes the GPU creates canvas /id/ with /...options/ { puts "Create canvas: $id" - set width [dict getdef $options width 1024] - set height [dict getdef $options height 1024] - set settle [dict getdef $options settle 3ms] + dict set options width [dict getdef $options width 1024] + dict set options height [dict getdef $options height 1024] + dict set options settle [dict getdef $options settle 3ms] - set wi [$gpuCanvasLib create $width $height] + set wi [$gpuCanvasLib create $options(width) $options(height)] Claim the GPU has created canvas $id with \ - width $width height $height \ + {*}$options \ texture [$gpuCanvasLib gpuTexture $wi] \ writableInfo $wi \ -destructor [list $gpuCanvasLib destroy $wi] @@ -283,7 +283,7 @@ When the GPU library is /gpuLib/ &\ Wish to collect results for \ [list /wisher/ wishes the GPU draws pipeline /name/ \ onto canvas $id with /...options/] \ - with settle $settle + with settle $options(settle) } Wish the GPU runs frame prelude handler [list apply {{gpuCanvasLib gpuTextureLib} { @@ -516,7 +516,7 @@ When the GPU library is /gpuLib/ &\ [list [list $dispWidth $dispHeight] \ $surfaceToClip \ [dict get $canvOpts texture] $a $b $c $d] \ - priority [dict getdef $canvOpts priority 0] + layer [dict getdef $canvOpts layer 0] } } } diff --git a/builtin-programs/tags-to-quads.folk b/builtin-programs/tags-to-quads.folk index 4cc651aa..fc93ae24 100644 --- a/builtin-programs/tags-to-quads.folk +++ b/builtin-programs/tags-to-quads.folk @@ -761,9 +761,10 @@ When the quad changer is /quadChange/ &\ Hold! -keep 2ms -key [list $tag image] \ Wish the GPU draws pipeline "image" with arguments \ - [list [list $displayWidth $displayHeight] \ - $displayToClip \ - [dict get $wiOptions texture] $a $b $c $d] + [list [list $displayWidth $displayHeight] \ + $displayToClip \ + [dict get $wiOptions texture] $a $b $c $d] \ + layer [dict getdef $wiOptions layer 0] } On unmatch { From 80ca5dbcaf73f573b743b4012067e7249da3f86e Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 15:33:51 -0400 Subject: [PATCH 12/45] editor: Draw translucent backdrop; draw display canvas on top so that tag masking works. --- builtin-programs/editor/editor.folk | 15 ++++++++++++++- builtin-programs/gpu/canvases.folk | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index 983d74fd..bca10a36 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -9,7 +9,7 @@ When /k/ is a keyboard with /...opts/ { set editor [list $k editor] Claim $editor is an editor Claim editor $editor has selected program $program - Wish $editor has a canvas with layer 99 + Wish $editor has a canvas with layer 98 When $program has resolved geometry /geom/ { Claim $editor has resolved geometry $geom @@ -140,8 +140,21 @@ When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ /keyboard/ is typing into /editor/ &\ /editor/ is an editor with /...anything/ &\ editor /editor/ has selected program /program/ { + Wish $editor is outlined green + When $editor has canvas /editorCanvas/ with /...wiOpts/ { + set bgColor [list 0 0 0 0.8] + Wish the GPU draws pipeline "fillTriangle" onto canvas $editorCanvas \ + with arguments [list {{1 0 0} {0 1 0} {0 0 1}} \ + [list -1 -1] [list 1 -1] [list 1 1] $bgColor] \ + layer -1 + Wish the GPU draws pipeline "fillTriangle" onto canvas $editorCanvas \ + with arguments [list {{1 0 0} {0 1 0} {0 0 1}} \ + [list -1 -1] [list 1 1] [list -1 1] $bgColor] \ + layer -1 + } + Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { ForEach! editor $editor has viewport position /vpPos/ &\ editor $editor has viewport size /vpSize/ &\ diff --git a/builtin-programs/gpu/canvases.folk b/builtin-programs/gpu/canvases.folk index 52ea3a90..207735d2 100644 --- a/builtin-programs/gpu/canvases.folk +++ b/builtin-programs/gpu/canvases.folk @@ -516,7 +516,7 @@ When the GPU library is /gpuLib/ &\ [list [list $dispWidth $dispHeight] \ $surfaceToClip \ [dict get $canvOpts texture] $a $b $c $d] \ - layer [dict getdef $canvOpts layer 0] + layer [dict getdef $canvOpts layer 99] } } } From 6c1b6d25fd4b6f4501716f2f97020287baff09b0 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 16:01:38 -0400 Subject: [PATCH 13/45] Make tag masking top-level; stabilize editor program --- builtin-programs/editor/editor.folk | 7 +++++-- builtin-programs/gpu/canvases.folk | 2 +- builtin-programs/mask-tags.folk | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index bca10a36..488c1905 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -5,6 +5,9 @@ When /k/ is a keyboard with /...opts/ { When $k points up with length 0.3 at /program/ &\ /nobody/ claims /program/ is an editor { + + Wish tag $program is stabilized + # Create a synthetic editor on top of the program being edited,. set editor [list $k editor] Claim $editor is an editor @@ -148,11 +151,11 @@ When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ Wish the GPU draws pipeline "fillTriangle" onto canvas $editorCanvas \ with arguments [list {{1 0 0} {0 1 0} {0 0 1}} \ [list -1 -1] [list 1 -1] [list 1 1] $bgColor] \ - layer -1 + layer -2 Wish the GPU draws pipeline "fillTriangle" onto canvas $editorCanvas \ with arguments [list {{1 0 0} {0 1 0} {0 0 1}} \ [list -1 -1] [list 1 1] [list -1 1] $bgColor] \ - layer -1 + layer -2 } Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { diff --git a/builtin-programs/gpu/canvases.folk b/builtin-programs/gpu/canvases.folk index 207735d2..c170b2a5 100644 --- a/builtin-programs/gpu/canvases.folk +++ b/builtin-programs/gpu/canvases.folk @@ -488,7 +488,7 @@ When the GPU library is /gpuLib/ &\ # can draw onto it using the canvas-oriented interface. set dispCanvas [list $disp canvas] Wish the GPU creates canvas $dispCanvas with \ - width $dispWidth height $dispHeight settle 0ms + width $dispWidth height $dispHeight settle 0ms layer 100 When the GPU has created canvas $dispCanvas with /...canvOpts/ { Claim $disp has canvas $dispCanvas with {*}$canvOpts diff --git a/builtin-programs/mask-tags.folk b/builtin-programs/mask-tags.folk index f3cabc79..291f7f46 100644 --- a/builtin-programs/mask-tags.folk +++ b/builtin-programs/mask-tags.folk @@ -15,6 +15,6 @@ When the quad library is /quadLib/ &\ Wish to draw a quad onto $proj with \ p0 $p0 p1 $p1 p2 $p2 p3 $p3 \ - color black layer 99 + color black layer 100 } -} \ No newline at end of file +} From efd724c4f9008b41a9d6b86ef12e919331796b5a Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 15 May 2026 17:04:03 -0400 Subject: [PATCH 14/45] WIP: Refactoring editor to factor out utils for printing Printing should share as much layout logic with draw as possible. --- builtin-programs/editor/draw-editor.folk | 92 ++++++++++ .../{ => editor}/editor-utils.folk | 40 ++++- builtin-programs/editor/editor.folk | 170 +++--------------- builtin-programs/editor/print-editor.folk | 25 +++ 4 files changed, 175 insertions(+), 152 deletions(-) create mode 100644 builtin-programs/editor/draw-editor.folk rename builtin-programs/{ => editor}/editor-utils.folk (83%) create mode 100644 builtin-programs/editor/print-editor.folk diff --git a/builtin-programs/editor/draw-editor.folk b/builtin-programs/editor/draw-editor.folk new file mode 100644 index 00000000..2cf7cb3b --- /dev/null +++ b/builtin-programs/editor/draw-editor.folk @@ -0,0 +1,92 @@ +When the editor utils library is /utils/ { + +# Draw text and cursor +When /editor/ is an editor with /...anything/ &\ + editor /editor/ has margin /margin/ &\ + editor /editor/ has viewport position /vpPos/ &\ + editor /editor/ has viewport size /vpSize/ &\ + editor /editor/ has font options with /...fontOptions/ { + lassign $vpPos vpX vpY + lassign $vpSize vpWidth vpHeight + + set textScale [dict get $fontOptions scale] + set advance [$utils getAdvance $textScale] + + When editor $editor has selected program /program/ &\ + editor buffer for /program/ is /code/ &\ + editor $editor has cursor /cursor/ &\ + editor $editor has cursor position /cursorPos/ &\ + editor $editor has selection anchor /selAnchor/ { + set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] + set lineNumbers [$utils lineNumberView $vpY $lineCount] + + set marginLeft [lindex $margin 3] + set lineNumbersRight $($marginLeft + $advance*1.5) + Wish to draw text onto $editor with \ + position [list $lineNumbersRight [lindex $margin 0]] \ + text $lineNumbers \ + scale $textScale anchor topright font NeomatrixCode + + set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] + set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] + Wish to draw text onto $editor with \ + position $pos text $text \ + scale $textScale anchor topleft font NeomatrixCode + + # Draw selection highlight + if {$selAnchor ne ""} { + set rawStart [min $selAnchor $cursor] + set rawEnd [max $selAnchor $cursor] + lassign [$utils cursorToXy $code $rawStart] selStartX selStartY + lassign [$utils cursorToXy $code $rawEnd] selEndX selEndY + + set lines [split $code "\n"] + for {set ly $selStartY} {$ly <= $selEndY} {incr ly} { + if {$ly < $vpY || $ly >= $vpY + $vpHeight} continue + + set lineLen [string length [lindex $lines $ly]] + + # Absolute column range for selection on this line + if {$ly == $selStartY} { + set absStart $selStartX + } else { + set absStart 0 + } + if {$ly == $selEndY} { + set absEnd $selEndX + } else { + set absEnd $lineLen + } + + # Clip to viewport + set absStart [max $absStart $vpX] + set absEnd [min $absEnd [+ $vpX $vpWidth]] + + if {$absStart >= $absEnd} continue + + # Convert to display coordinates + set dispStart [- $absStart $vpX] + set dispEnd [- $absEnd $vpX] + set dispRow [- $ly $vpY] + + set x0 $([lindex $pos 0] + $dispStart * $advance) + set x1 $([lindex $pos 0] + $dispEnd * $advance) + set y0 $([lindex $pos 1] + $dispRow * $textScale) + set y1 $($y0 + $textScale) + + Wish to draw a quad onto $editor with \ + p0 [list $x0 $y0] p1 [list $x1 $y0] \ + p2 [list $x1 $y1] p3 [list $x0 $y1] \ + color {0.2 0.4 0.8 0.7} layer -1 + } + } + + set p1 [vec2 add $cursorPos $pos] + set p2 [vec2 add $p1 [list 0 [* $textScale 1.2]]] + set s [/ $textScale 6] + Wish to draw a circle onto $editor with center $p1 radius $s thickness 0 color green filled true + Wish to draw a line onto $editor with points [list $p1 $p2] width $s color green + } +} + +} diff --git a/builtin-programs/editor-utils.folk b/builtin-programs/editor/editor-utils.folk similarity index 83% rename from builtin-programs/editor-utils.folk rename to builtin-programs/editor/editor-utils.folk index a27e1ba5..7b1aa5c7 100644 --- a/builtin-programs/editor-utils.folk +++ b/builtin-programs/editor/editor-utils.folk @@ -1,4 +1,4 @@ -set editorUtilsLib [library create editorUtilsLib { +Claim the editor utils library is [library create editorUtilsLib { proc applyTextViewport {originalText x y width height} { set lines [split $originalText \n] set lines [lrange $lines $y [expr {($height - 1) + $y}]] @@ -191,6 +191,40 @@ set editorUtilsLib [library create editorUtilsLib { } join $numbers "\n" } -}] -Claim the editor utils library is $editorUtilsLib + # For rendering: + + proc getAdvance {em} { + # From NeomatrixCode.csv + return $(0.5859375 * $em) + } + + proc widthAndHeight {resolvedGeom} { + set tagSize [dict get $resolvedGeom tagSize] + set left [dict get $resolvedGeom left] + set right [dict get $resolvedGeom right] + set top [dict get $resolvedGeom top] + set bottom [dict get $resolvedGeom bottom] + + set width $($left + $tagSize + $right) + set height $($top + $tagSize + $bottom) + + return [list $width $height] + } + + # given program and the editor options, figure out how many characters can + # fit in this editor + proc editorSizeInCharacters {margin resolvedGeom options} { + set textScale [dict get $options scale] + set advance [getAdvance $textScale] + + lassign [widthAndHeight $resolvedGeom] width height + set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1]) + set height $($height - [lindex $margin 0] - [lindex $margin 2]) + + set widthInCharacters $(int($width / $advance)) + set heightInCharacters $(int($height / $textScale)) + + return [list $widthInCharacters $heightInCharacters] + } +}] diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index 488c1905..46b2d0da 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -27,47 +27,7 @@ When the program save directory is /programDir/ &\ the editor utils library is /utils/ { # TODO: also don't hardcode this? -set margin [list 0.01 0.005 0.005 0.01] ;# CSS order (top, right, bottom, left) set defaults { textScale 0.01 } - -set editorLib [library create editorLib {margin defaults} { - proc getAdvance {em} { - # From NeomatrixCode.csv - return $(0.5859375 * $em) - } - - proc widthAndHeight {resolvedGeom} { - set tagSize [dict get $resolvedGeom tagSize] - set left [dict get $resolvedGeom left] - set right [dict get $resolvedGeom right] - set top [dict get $resolvedGeom top] - set bottom [dict get $resolvedGeom bottom] - - set width $($left + $tagSize + $right) - set height $($top + $tagSize + $bottom) - - return [list $width $height] - } - - # given program and the editor options, figure out how many characters can - # fit in this editor - proc editorSizeInCharacters {resolvedGeom options} { - variable margin - - set textScale [dict get $options scale] - set advance [getAdvance $textScale] - - lassign [widthAndHeight $resolvedGeom] width height - set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1]) - set height $($height - [lindex $margin 0] - [lindex $margin 2]) - - set widthInCharacters $(int($width / $advance)) - set heightInCharacters $(int($height / $textScale)) - - return [list $widthInCharacters $heightInCharacters] - } -}] - When /someone/ claims /editor/ is an editor { Claim $editor is an editor with {*}$defaults } @@ -77,6 +37,7 @@ When /editor/ is an editor with /...options/ { if {![info exists options]} { return } set options [dict merge $defaults $options] + Claim editor $editor has margin [list 0.01 0.005 0.005 0.01] ;# CSS order (top, right, bottom, left) # Initial setup # Load initial text settings if not set @@ -93,8 +54,9 @@ When /editor/ is an editor with /...options/ { # We then use its size to figure out how many characters we can fit in it, # width-wise and height-wise. When $editor has resolved geometry /geom/ &\ + editor $editor has margin /margin/ &\ editor $editor has font options with /...options/ { - Claim editor $editor has viewport size [$editorLib editorSizeInCharacters $geom $options] + Claim editor $editor has viewport size [$utils editorSizeInCharacters $margin $geom $options] } # Load in defaults for the editor if it hasn't been initialized @@ -288,23 +250,23 @@ When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ sleep 0.25 Hold! -key printing-alert:$editor {} } - Control_underscore { - # ctrl and - (zoom out) - set textScale [dict get $textOptions scale] - set textScale [/ $textScale 1.1] - - Hold! -save -key font-options-of:$editor \ - Claim editor $editor has font options with scale $textScale - } - equal { - # Dunno why it registers as equal instead of Control_equal? It works regardless, lol - # ctrl and + (zoom in) - set textScale [dict get $textOptions scale] - set textScale [* $textScale 1.1] - - Hold! -save -key font-options-of:$editor \ - Claim editor $editor has font options with scale $textScale - } + # Control_underscore { + # # ctrl and - (zoom out) + # set textScale [dict get $textOptions scale] + # set textScale [/ $textScale 1.1] + + # Hold! -save -key font-options-of:$editor \ + # Claim editor $editor has font options with scale $textScale + # } + # equal { + # # Dunno why it registers as equal instead of Control_equal? It works regardless, lol + # # ctrl and + (zoom in) + # set textScale [dict get $textOptions scale] + # set textScale [* $textScale 1.1] + + # Hold! -save -key font-options-of:$editor \ + # Claim editor $editor has font options with scale $textScale + # } } } } @@ -419,7 +381,7 @@ When /editor/ is an editor with /...anything/ &\ lassign [$utils cursorToXy $code $cursor] cursorX cursorY set textScale [dict get $textOptions scale] - set advance [$editorLib getAdvance $textScale] + set advance [$utils getAdvance $textScale] set offsetX $(($cursorX - $vpX) * $advance) set offsetY $(($cursorY - $vpY) * $textScale) @@ -427,94 +389,4 @@ When /editor/ is an editor with /...anything/ &\ Claim editor $editor has cursor position [list $offsetX $offsetY] } -# Draw text and cursor -When /editor/ is an editor with /...anything/ &\ - editor /editor/ has viewport position /vpPos/ &\ - editor /editor/ has viewport size /vpSize/ &\ - editor /editor/ has font options with /...fontOptions/ { - lassign $vpPos vpX vpY - lassign $vpSize vpWidth vpHeight - - set textScale [dict get $fontOptions scale] - set advance [$editorLib getAdvance $textScale] - - When editor $editor has selected program /program/ &\ - editor buffer for /program/ is /code/ &\ - editor $editor has cursor /cursor/ &\ - editor $editor has cursor position /cursorPos/ &\ - editor $editor has selection anchor /selAnchor/ { - set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] - set lineNumbers [$utils lineNumberView $vpY $lineCount] - - set marginLeft [lindex $margin 3] - set lineNumbersRight $($marginLeft + $advance*1.5) - Wish to draw text onto $editor with \ - position [list $lineNumbersRight [lindex $margin 0]] \ - text $lineNumbers \ - scale $textScale anchor topright font NeomatrixCode - - set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] - set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] - Wish to draw text onto $editor with \ - position $pos text $text \ - scale $textScale anchor topleft font NeomatrixCode - - # Draw selection highlight - if {$selAnchor ne ""} { - set rawStart [min $selAnchor $cursor] - set rawEnd [max $selAnchor $cursor] - lassign [$utils cursorToXy $code $rawStart] selStartX selStartY - lassign [$utils cursorToXy $code $rawEnd] selEndX selEndY - - set lines [split $code "\n"] - for {set ly $selStartY} {$ly <= $selEndY} {incr ly} { - if {$ly < $vpY || $ly >= $vpY + $vpHeight} continue - - set lineLen [string length [lindex $lines $ly]] - - # Absolute column range for selection on this line - if {$ly == $selStartY} { - set absStart $selStartX - } else { - set absStart 0 - } - if {$ly == $selEndY} { - set absEnd $selEndX - } else { - set absEnd $lineLen - } - - # Clip to viewport - set absStart [max $absStart $vpX] - set absEnd [min $absEnd [+ $vpX $vpWidth]] - - if {$absStart >= $absEnd} continue - - # Convert to display coordinates - set dispStart [- $absStart $vpX] - set dispEnd [- $absEnd $vpX] - set dispRow [- $ly $vpY] - - set x0 $([lindex $pos 0] + $dispStart * $advance) - set x1 $([lindex $pos 0] + $dispEnd * $advance) - set y0 $([lindex $pos 1] + $dispRow * $textScale) - set y1 $($y0 + $textScale) - - Wish to draw a quad onto $editor with \ - p0 [list $x0 $y0] p1 [list $x1 $y0] \ - p2 [list $x1 $y1] p3 [list $x0 $y1] \ - color {0.2 0.4 0.8 0.7} layer -1 - } - } - - set p1 [vec2 add $cursorPos $pos] - set p2 [vec2 add $p1 [list 0 [* $textScale 1.2]]] - set s [/ $textScale 6] - Wish to draw a circle onto $editor with center $p1 radius $s thickness 0 color green filled true - Wish to draw a line onto $editor with points [list $p1 $p2] width $s color green - } -} - - -# end of library code } diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk new file mode 100644 index 00000000..55850ba9 --- /dev/null +++ b/builtin-programs/editor/print-editor.folk @@ -0,0 +1,25 @@ +Subscribe: print program from editor /editor/ { + set program [dict get [QueryOne! editor $editor has selected program /program/] program] + set code [dict get [QueryOne! editor buffer for $program is /code/] code] + + set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] + set lineNumbers [$utils lineNumberView $vpY $lineCount] + + set marginLeft [lindex $margin 3] + set lineNumbersRight $($marginLeft + $advance*1.5) + + # TODO: paginate + foreach page { + # TODO: construct postscript + Wish to draw text onto $editor with \ + position [list $lineNumbersRight [lindex $margin 0]] \ + text $lineNumbers \ + scale $textScale anchor topright font NeomatrixCode + + set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] + set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] + Wish to draw text onto $editor with \ + position $pos text $text \ + scale $textScale anchor topleft font NeomatrixCode + } +} From dd5bfd111b8138541fbb32de6129d6b72d9cac6e Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Mon, 18 May 2026 23:44:09 -0400 Subject: [PATCH 15/45] WIP: Editor-based printing (emits PDF, no preview yet) --- builtin-programs/editor/editor.folk | 2 +- builtin-programs/editor/print-editor.folk | 163 +++++++++++++++++++--- 2 files changed, 145 insertions(+), 20 deletions(-) diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index 46b2d0da..4c18f387 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -242,7 +242,7 @@ When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ Notify: save code on editor $editor } Control_p { - Notify: print code $code + Notify: print program from editor $editor # Give the user some feedback Hold! -key printing-alert:$editor \ diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 55850ba9..7842ec3c 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -1,25 +1,150 @@ +When the print library is /printLib/ { + +set formats [subst { + letter { + tagSize {150 150} + pageSize {612 792} + } + a4 { + tagSize {150 150} + pageSize {595 842} + } + indexcard { + tagSize {300 300} + pageSize {612 792} + } +}] +# indexcard (really receipt) assumes fake letter/A4 size: +# https://github.com/NaitLee/Cat-Printer/discussions/8#discussioncomment-2557843 + +fn codeToPs {id code opts {mixins {}}} { + # All opts should be passed in as points (1/2834.65 of a meter). + lassign $opts(pageSize) PageWidth PageHeight + lassign $opts(tagSize) tagWidth tagHeight + lassign $opts(margin) marginTop marginRight marginBottom marginLeft + set lineHeight $opts(lineHeight) + set maxLines $(int(($PageHeight - $marginTop - $marginBottom) / $lineHeight)) + + set lineNumbersRight $($marginLeft + $opts(advance)*1.5) + + set lines [split $code "\n"] + + set image [$printLib tagPsForId $id] + + set outPages [list] + set lineIdx 0 + while {[llength $lines] > 0} { + set pageLines [lrange $lines 0 $maxLines] + set lines [lreplace $lines 0 $maxLines] + + # The typesetting here is meant to exactly duplicate the + # layout in the editor. + lappend outPages [subst { + %!PS + << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice + + /settextcolor {0 setgray} def + + /Neomatrix-Code findfont + $lineHeight scalefont + setfont + + newpath + [join [lmap line $pageLines { + set line [string map {"\\" "\\\\" ")" "\\)" "(" "\\("} $line] + incr lineIdx + subst { + $marginLeft [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto + 0.4 setgray ([format "%- 3s" [+ $lineIdx 1]]) show settextcolor ($line) show + } + }] "\n"] + + [expr {[llength $outPages] == 0 ? {} : [subst { + gsave + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$marginTop}] translate + $tagWidth $tagHeight scale + $image + grestore + + /Helvetica-Narrow findfont + [- $lineHeight 2] scalefont + setfont + newpath + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-16-$marginTop}] moveto + ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show + + [join [lmap mixin $mixins { + # We run mixins only on page 1 for now. They + # get access to everything in scope. Kind of + # hacky, but OK for now. + + subst $mixin + }] "\n"] + }] }] + + showpage + }] + } + return [join $outPages "\n"] +} + Subscribe: print program from editor /editor/ { set program [dict get [QueryOne! editor $editor has selected program /program/] program] set code [dict get [QueryOne! editor buffer for $program is /code/] code] + set lines [split $code "\n"] + + set fontOptions [dict get [QueryOne! editor $editor has font options with /...opts/] opts] + set textScale $fontOptions(scale) + set margin [dict get [QueryOne! editor $editor has margin /margin/] margin] - set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] - set lineNumbers [$utils lineNumberView $vpY $lineCount] - - set marginLeft [lindex $margin 3] - set lineNumbersRight $($marginLeft + $advance*1.5) - - # TODO: paginate - foreach page { - # TODO: construct postscript - Wish to draw text onto $editor with \ - position [list $lineNumbersRight [lindex $margin 0]] \ - text $lineNumbers \ - scale $textScale anchor topright font NeomatrixCode - - set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] - set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] - Wish to draw text onto $editor with \ - position $pos text $text \ - scale $textScale anchor topleft font NeomatrixCode + # TODO: scan for mixins (a mixin should be, like, Field detector) + set id 100 + # TODO: support other formats + set mToPt 2834.646 + set opts [dict merge $formats(letter) \ + [dict create \ + lineHeight [* $textScale $mToPt] \ + advance [* 0.5859375 $textScale $mToPt] \ + margin [lmap x $margin {* $x $mToPt}]]] + set ps [codeToPs $id $code $opts {}] + + # Wish to draw text onto $editor with \ + # position [list $lineNumbersRight [lindex $margin 0]] \ + # text $lineNumbers \ + # scale $textScale anchor topright font NeomatrixCode + + # set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] + # set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] + # # TODO: construct postscript such that the text is set down + # # from top-left by $pos + # Wish to draw text onto $editor with \ + # position $pos text $text \ + # scale $textScale anchor topleft font NeomatrixCode + + # lappend postScript {..} + + + set fp [open "/tmp/$id.ps" w]; puts $fp $ps; close $fp + exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH="vendor/fonts" \ + /tmp/$id.ps /tmp/$id.pdf + + # TODO: save to a temporary pdf, puts the temporary pdf's filename + # TODO: preview the pdf next to the editor for now + Hold! { + set preview [list $editor preview] + Wish $preview has a canvas + When $editor has resolved geometry /geom/ { + Claim $preview has resolved geometry $geom + } + When the quad library is /quadLib/ & $editor has quad /q/ { + Claim $editor has quad [$quadLib move $editor right 100%] + } + # FIXME: convert the pdf to an image, display it. + # FIXME: display without margin! + Wish $preview displays image $pdf } + sleep 10 + Hold! {} +} + } From 66d807b2950ff389dfd4bae3732c348cc5ee2e6f Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 00:05:15 -0400 Subject: [PATCH 16/45] print-editor: Fix preview --- builtin-programs/editor/print-editor.folk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 7842ec3c..0a02461b 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -137,11 +137,12 @@ Subscribe: print program from editor /editor/ { Claim $preview has resolved geometry $geom } When the quad library is /quadLib/ & $editor has quad /q/ { - Claim $editor has quad [$quadLib move $editor right 100%] + Claim $preview has quad [$quadLib move $q right 100%] } - # FIXME: convert the pdf to an image, display it. + # FIXME: convert the pdf to an image with imagemagick at 144dpi, display it. + exec gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=/tmp/$id.png /tmp/$id.pdf # FIXME: display without margin! - Wish $preview displays image $pdf + Wish $preview displays image /tmp/$id.png } sleep 10 Hold! {} From a2084542456d2e3475c12a5f17af64753e0dbd31 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 00:41:01 -0400 Subject: [PATCH 17/45] print-editor: Add font --- vendor/fonts/NeomatrixCode.ttf | Bin 0 -> 163984 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 vendor/fonts/NeomatrixCode.ttf diff --git a/vendor/fonts/NeomatrixCode.ttf b/vendor/fonts/NeomatrixCode.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c7465bd186f5279c679669d9f0b65f9b3e205814 GIT binary patch literal 163984 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 P>_lDTCF Date: Tue, 19 May 2026 05:06:52 -0400 Subject: [PATCH 18/45] WIP: To-scale preview of new printing --- builtin-programs/editor/print-editor.folk | 32 +++++++++++++---------- builtin-programs/tags-to-quads.folk | 13 +++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 0a02461b..016f955f 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -55,7 +55,7 @@ fn codeToPs {id code opts {mixins {}}} { incr lineIdx subst { $marginLeft [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto - 0.4 setgray ([format "%- 3s" [+ $lineIdx 1]]) show settextcolor ($line) show + 0.4 setgray ([format "%- 3s" $lineIdx]) show settextcolor ($line) show } }] "\n"] @@ -73,13 +73,13 @@ fn codeToPs {id code opts {mixins {}}} { [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-16-$marginTop}] moveto ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show - [join [lmap mixin $mixins { - # We run mixins only on page 1 for now. They - # get access to everything in scope. Kind of - # hacky, but OK for now. + [join [lmap mixin $mixins { + # We run mixins only on page 1 for now. They + # get access to everything in scope. Kind of + # hacky, but OK for now. - subst $mixin - }] "\n"] + subst $mixin + }] "\n"] }] }] showpage @@ -100,8 +100,9 @@ Subscribe: print program from editor /editor/ { # TODO: scan for mixins (a mixin should be, like, Field detector) set id 100 # TODO: support other formats + set fmt $formats(letter) set mToPt 2834.646 - set opts [dict merge $formats(letter) \ + set opts [dict merge $fmt \ [dict create \ lineHeight [* $textScale $mToPt] \ advance [* 0.5859375 $textScale $mToPt] \ @@ -133,16 +134,19 @@ Subscribe: print program from editor /editor/ { Hold! { set preview [list $editor preview] Wish $preview has a canvas - When $editor has resolved geometry /geom/ { - Claim $preview has resolved geometry $geom - } + set previewGeom [list width [/ [lindex $fmt(pageSize) 0] $mToPt] \ + height [/ [lindex $fmt(pageSize) 1] $mToPt]] + Claim $preview has resolved geometry $previewGeom + When the quad library is /quadLib/ & $editor has quad /q/ { - Claim $preview has quad [$quadLib move $q right 100%] + Claim $preview has quad [$quadLib alignGeometry \ + [$quadLib move $q right 100%] \ + $previewGeom] } - # FIXME: convert the pdf to an image with imagemagick at 144dpi, display it. exec gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=/tmp/$id.png /tmp/$id.pdf # FIXME: display without margin! - Wish $preview displays image /tmp/$id.png + Wish $preview is outlined white + Wish $preview displays image /tmp/$id.png with width $previewGeom(width) } sleep 10 Hold! {} diff --git a/builtin-programs/tags-to-quads.folk b/builtin-programs/tags-to-quads.folk index fc93ae24..26aff7f4 100644 --- a/builtin-programs/tags-to-quads.folk +++ b/builtin-programs/tags-to-quads.folk @@ -615,6 +615,19 @@ set quadLib [library create quadLib {} { } return [create [space $q] [list $topLeft $topRight $bottomRight $bottomLeft]] } + + # Move an existing geometry geom (object with width and height + # keys) to align onto the plane & the top edge & left edge of the + # existing quad q. + proc alignGeometry {q geom} { + lassign [vertices $q] topLeft topRight bottomRight bottomLeft + set topDisp [scaleVector $geom(width) [unitLengthVector [sub $topRight $topLeft]]] + set leftDisp [scaleVector $geom(height) [unitLengthVector [sub $bottomLeft $topLeft]]] + set topRight [add $topLeft $topDisp] + set bottomRight [add $topRight $leftDisp] + set bottomLeft [add $topLeft $leftDisp] + return [create [space $q] [list $topLeft $topRight $bottomRight $bottomLeft]] + } }] Claim the quad library is $quadLib From 0e8f6bdac2ad6fd10bd15e1a695a83245fd9493d Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 06:55:28 -0400 Subject: [PATCH 19/45] print-editor: Fix font choice and tag rendering --- builtin-programs/editor/print-editor.folk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 016f955f..4138d8e1 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -45,7 +45,7 @@ fn codeToPs {id code opts {mixins {}}} { /settextcolor {0 setgray} def - /Neomatrix-Code findfont + /NeomatrixCode findfont $lineHeight scalefont setfont @@ -59,7 +59,7 @@ fn codeToPs {id code opts {mixins {}}} { } }] "\n"] - [expr {[llength $outPages] == 0 ? {} : [subst { + [expr {[llength $outPages] > 0 ? {} : [subst { gsave [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$marginTop}] translate $tagWidth $tagHeight scale @@ -67,10 +67,10 @@ fn codeToPs {id code opts {mixins {}}} { grestore /Helvetica-Narrow findfont - [- $lineHeight 2] scalefont + 14 scalefont setfont newpath - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-16-$marginTop}] moveto + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$lineHeight-$marginTop}] moveto ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show [join [lmap mixin $mixins { From d8c5591b33bcaa01d2dd9c85c6ec3584ca9e8c1b Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 14:06:32 -0400 Subject: [PATCH 20/45] WIP: Live print preview to help me debug Turn off Hold of quad rendering -- this may cause blinking, but hopefully -atomically can handle. Make tag in print preview not detect. --- builtin-programs/editor/print-editor.folk | 28 ++++++++++++++--------- builtin-programs/tags-to-quads.folk | 16 ++++--------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 4138d8e1..719a781e 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -55,7 +55,10 @@ fn codeToPs {id code opts {mixins {}}} { incr lineIdx subst { $marginLeft [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto - 0.4 setgray ([format "%- 3s" $lineIdx]) show settextcolor ($line) show + 0.4 setgray ([format "%- 3s" $lineIdx]) show + + $lineNumbersRight [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto + settextcolor ($line) show } }] "\n"] @@ -88,9 +91,11 @@ fn codeToPs {id code opts {mixins {}}} { return [join $outPages "\n"] } -Subscribe: print program from editor /editor/ { - set program [dict get [QueryOne! editor $editor has selected program /program/] program] - set code [dict get [QueryOne! editor buffer for $program is /code/] code] +# Subscribe: print program from editor /editor/ { + # set program [dict get [QueryOne! editor $editor has selected program /program/] program] +# set code [dict get [QueryOne! editor buffer for $program is /code/] code] } +When editor /editor/ has selected program /program/ &\ + editor buffer for /program/ is /code/ { set lines [split $code "\n"] set fontOptions [dict get [QueryOne! editor $editor has font options with /...opts/] opts] @@ -98,7 +103,7 @@ Subscribe: print program from editor /editor/ { set margin [dict get [QueryOne! editor $editor has margin /margin/] margin] # TODO: scan for mixins (a mixin should be, like, Field detector) - set id 100 + set id 48700 # TODO: support other formats set fmt $formats(letter) set mToPt 2834.646 @@ -124,14 +129,14 @@ Subscribe: print program from editor /editor/ { # lappend postScript {..} - + puts stderr "Render!" set fp [open "/tmp/$id.ps" w]; puts $fp $ps; close $fp exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH="vendor/fonts" \ /tmp/$id.ps /tmp/$id.pdf # TODO: save to a temporary pdf, puts the temporary pdf's filename # TODO: preview the pdf next to the editor for now - Hold! { + # Hold! { set preview [list $editor preview] Wish $preview has a canvas set previewGeom [list width [/ [lindex $fmt(pageSize) 0] $mToPt] \ @@ -143,13 +148,14 @@ Subscribe: print program from editor /editor/ { [$quadLib move $q right 100%] \ $previewGeom] } - exec gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=/tmp/$id.png /tmp/$id.pdf + set pngFile [file tempfile /tmp/$id-XXXXXX].png + exec gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=$pngFile /tmp/$id.pdf # FIXME: display without margin! Wish $preview is outlined white - Wish $preview displays image /tmp/$id.png with width $previewGeom(width) - } + Wish $preview displays image $pngFile with width $previewGeom(width) + # } sleep 10 - Hold! {} + # Hold! {} } } diff --git a/builtin-programs/tags-to-quads.folk b/builtin-programs/tags-to-quads.folk index 26aff7f4..6248ee52 100644 --- a/builtin-programs/tags-to-quads.folk +++ b/builtin-programs/tags-to-quads.folk @@ -772,18 +772,12 @@ When the quad changer is /quadChange/ &\ $displayWidth $displayHeight $v }] a b c d - Hold! -keep 2ms -key [list $tag image] \ - Wish the GPU draws pipeline "image" with arguments \ - [list [list $displayWidth $displayHeight] \ - $displayToClip \ - [dict get $wiOptions texture] $a $b $c $d] \ - layer [dict getdef $wiOptions layer 0] + Wish the GPU draws pipeline "image" with arguments \ + [list [list $displayWidth $displayHeight] \ + $displayToClip \ + [dict get $wiOptions texture] $a $b $c $d] \ + layer [dict getdef $wiOptions layer 0] } - - On unmatch { - Hold! -key [list $tag image] {} - } - # TODO: How to prevent a race if the tag returns? } } From 490ecfc90161bbcc0fb76e0f824f4ba4b813d77b Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 15:04:30 -0400 Subject: [PATCH 21/45] print-editor: Fix font load --- builtin-programs/editor/print-editor.folk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 719a781e..2d3407de 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -70,10 +70,10 @@ fn codeToPs {id code opts {mixins {}}} { grestore /Helvetica-Narrow findfont - 14 scalefont + 11 scalefont setfont newpath - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$lineHeight-$marginTop}] moveto + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-14-$marginTop}] moveto ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show [join [lmap mixin $mixins { @@ -131,8 +131,8 @@ When editor /editor/ has selected program /program/ &\ puts stderr "Render!" set fp [open "/tmp/$id.ps" w]; puts $fp $ps; close $fp - exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH="vendor/fonts" \ - /tmp/$id.ps /tmp/$id.pdf + puts stderr [exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ + /tmp/$id.ps /tmp/$id.pdf] # TODO: save to a temporary pdf, puts the temporary pdf's filename # TODO: preview the pdf next to the editor for now From 3ed35ba5798b07951ef205c98fd199f5e5cc10ce Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 15:08:44 -0400 Subject: [PATCH 22/45] editor: Re-enable text scaling; remove old editor --- builtin-programs/editor.folk | 519 ---------------------------- builtin-programs/editor/editor.folk | 34 +- 2 files changed, 17 insertions(+), 536 deletions(-) delete mode 100644 builtin-programs/editor.folk diff --git a/builtin-programs/editor.folk b/builtin-programs/editor.folk deleted file mode 100644 index edaff9cf..00000000 --- a/builtin-programs/editor.folk +++ /dev/null @@ -1,519 +0,0 @@ -error "Disabling the standard editor while we prototype the inline editor." - -# This makes all keyboards into editors automatically, so a keyboard -# doesn't need to point at a real editor. May choose to change later, or -# exclude keyboards that opt out. -When /k/ is a keyboard with /...opts/ &\ - /nobody/ wishes /k/ does not create an editor { - - Wish tag $k is stabilized - - # Create a synthetic editor above the keyboard page. - set editor [list $k editor] - Claim $editor is an editor - Wish $editor has a canvas - When $k has resolved geometry /geom/ { - Claim $editor has resolved geometry $geom - } - When the quad library is /quadLib/ & $k has quad /q/ { - Claim $editor has quad [$quadLib move $q up 105%] - } - Claim $k has created editor $editor - Claim $k is typing into $editor -} -When /k/ is a keyboard with /...opts/ &\ - /nobody/ claims /k/ has created editor /any/ &\ - /k/ points up at /editor/ & /editor/ is an editor with /...opts/ { - - Claim $k is typing into $editor -} - -When the program save directory is /programDir/ &\ - the editor utils library is /utils/ { - -# TODO: also don't hardcode this? -set margin [list 0.01 0.005 0.005 0.01] ;# CSS order (top, right, bottom, left) -set defaults { textScale 0.01 } - -set editorLib [library create editorLib {margin defaults} { - proc getAdvance {em} { - # From NeomatrixCode.csv - return $(0.5859375 * $em) - } - - proc widthAndHeight {resolvedGeom} { - set tagSize [dict get $resolvedGeom tagSize] - set left [dict get $resolvedGeom left] - set right [dict get $resolvedGeom right] - set top [dict get $resolvedGeom top] - set bottom [dict get $resolvedGeom bottom] - - set width $($left + $tagSize + $right) - set height $($top + $tagSize + $bottom) - - return [list $width $height] - } - - # given program and the editor options, figure out how many characters can - # fit in this editor - proc editorSizeInCharacters {resolvedGeom options} { - variable margin - - set textScale [dict get $options scale] - set advance [getAdvance $textScale] - - lassign [widthAndHeight $resolvedGeom] width height - set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1]) - set height $($height - [lindex $margin 0] - [lindex $margin 2]) - - set widthInCharacters $(int($width / $advance)) - set heightInCharacters $(int($height / $textScale)) - - return [list $widthInCharacters $heightInCharacters] - } -}] - -When /someone/ claims /editor/ is an editor { - Claim $editor is an editor with {*}$defaults -} - -When /editor/ is an editor with /...options/ { - # HACK: because we partial match - if {![info exists options]} { return } - - set options [dict merge $defaults $options] - - Wish tag $editor is stabilized - - # Select which program the editor is viewing - When $editor points up with length 0.3 at /program/ { - Claim editor $editor has selected program $program - } - - # Initial setup - # Load initial text settings if not set - When /nobody/ claims editor $editor has font options /...anything/ &\ - the saved holds are loaded { - # Wait until all the saved holds are loaded, so we don't accidentally - # overwrite a saved setting. - set textScale [dict get $options textScale] - Hold! -save -key font-options-of:$editor \ - Claim editor $editor has font options with scale $textScale - } - - # We need the editor's resolved geometry to get its size. - # We then use its size to figure out how many characters we can fit in it, - # width-wise and height-wise. - When $editor has resolved geometry /geom/ &\ - editor $editor has font options with /...options/ { - Claim editor $editor has viewport size [$editorLib editorSizeInCharacters $geom $options] - } - - # Load in defaults for the editor if it hasn't been initialized - set results [Query! /somebody/ claims editor $editor has cursor /anything/] - if {[llength $results] == 0} { - Hold! -key editor-state-of:$editor { - Claim editor $editor has cursor 0 - Claim editor $editor has max cursor x 0 - Claim editor $editor has viewport position [list 0 0] - Claim editor $editor has selection anchor "" - Claim editor $editor has undo stack {} - Claim editor $editor has redo stack {} - Claim editor $editor has last edit type "" - } - } - - set clipResults [Query! /somebody/ claims editor $editor has clipboard /anything/] - if {[llength $clipResults] == 0} { - Hold! -key clipboard-of:$editor Claim editor $editor has clipboard "" - } - - # Load in initial program code (note, this buffer is shared between - # all editor instances) - When editor $editor has selected program /program/ &\ - /program/ has program code /programCode/ &\ - the saved holds are loaded { - # Check if the code already exists. - set results [Query! editor buffer for $program is /anything/] - if {[llength $results] == 0} { - Hold! -save -key buffer-for:$program \ - Claim editor buffer for $program is $programCode - } - } - - # Feedback: show that the editor is not active - When /nobody/ wishes $editor is outlined green { - Wish $editor is outlined blue - } -} - -Subscribe: keyboard /keyboard/ claims key Control_b is down with /...options/ { - Notify: print code "# blank" -} - -When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ - /keyboard/ is typing into /editor/ &\ - /editor/ is an editor with /...anything/ &\ - editor /editor/ has selected program /program/ { - Wish $editor is outlined green - - Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { - ForEach! editor $editor has viewport position /vpPos/ &\ - editor $editor has viewport size /vpSize/ &\ - editor buffer for $program is /code/ &\ - editor $editor has max cursor x /maxCursorX/ &\ - editor $editor has cursor /cursor/ &\ - editor $editor has selection anchor /selAnchor/ &\ - editor $editor has clipboard /clipboard/ &\ - editor $editor has undo stack /undoStack/ &\ - editor $editor has redo stack /redoStack/ &\ - editor $editor has last edit type /lastEditType/ &\ - editor $editor has font options with /...textOptions/ { - lassign $vpPos vpX vpY - lassign $vpSize vpWidth vpHeight - - if {$keyState == "up"} { return } - - # if this is true, the buffer will remove the hold for program code - # (triggering a reinitialization) - set resetBuffer false - set isUndo false - set isRedo false - set origCode $code - set origCursor $cursor - set origMaxCursorX $maxCursorX - - set hasShift [dict exists $options shift] - set isNavKey [expr {$key eq "Left" || $key eq "Right" || $key eq "Up" || $key eq "Down"}] - - # Handle selection replacement for text-modifying keys - set selectionHandled false - if {$selAnchor ne ""} { - set selStart [min $selAnchor $cursor] - set selEnd [max $selAnchor $cursor] - - if {[dict exists $options printable]} { - lassign [$utils replaceRange $code $selStart $selEnd [dict get $options printable]] code cursor maxCursorX - set selAnchor "" - set selectionHandled true - } elseif {$key eq "Delete" || $key eq "Remove"} { - lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX - set selAnchor "" - set selectionHandled true - } elseif {$key eq "Return"} { - # Delete selection, then fall through to Return handler - lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX - set selAnchor "" - } - } - - if {!$selectionHandled} { - if {$hasShift && $isNavKey} { - # Shift+Arrow: extend selection - if {$selAnchor eq ""} { set selAnchor $cursor } - lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX - } elseif {[dict exists $options printable]} { - lassign [$utils insertText $code $cursor [dict get $options printable]] code cursor maxCursorX - } else { - # Regular navigation clears selection - if {$isNavKey} { set selAnchor "" } - - # general editor functionality - lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX - lassign [$utils handleRemovalAndReturn $key $code $cursor $maxCursorX] code cursor maxCursorX - - # specific editor functionality - switch $key { - Control_c { - if {$selAnchor ne ""} { - set clipText [$utils getSelectedText $code $selAnchor $cursor] - Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText - } - } - Control_x { - if {$selAnchor ne ""} { - set clipText [$utils getSelectedText $code $selAnchor $cursor] - Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText - set selStart [min $selAnchor $cursor] - set selEnd [max $selAnchor $cursor] - lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX - set selAnchor "" - } - } - Control_v { - if {$clipboard ne ""} { - if {$selAnchor ne ""} { - set selStart [min $selAnchor $cursor] - set selEnd [max $selAnchor $cursor] - lassign [$utils replaceRange $code $selStart $selEnd $clipboard] code cursor maxCursorX - set selAnchor "" - } else { - lassign [$utils replaceRange $code $cursor $cursor $clipboard] code cursor maxCursorX - } - } - } - Control_z { - if {[llength $undoStack] > 0} { - lappend redoStack [list $code $cursor $maxCursorX] - lassign [lindex $undoStack end] code cursor maxCursorX - set undoStack [lrange $undoStack 0 end-1] - set selAnchor "" - set isUndo true - set lastEditType "" - } - } - Control_y { - if {[llength $redoStack] > 0} { - lappend undoStack [list $code $cursor $maxCursorX] - lassign [lindex $redoStack end] code cursor maxCursorX - set redoStack [lrange $redoStack 0 end-1] - set selAnchor "" - set isRedo true - set lastEditType "" - } - } - Control_r { - set resetBuffer true - } - Control_s { - Notify: save code on editor $editor - } - Control_p { - Notify: print code $code - - # Give the user some feedback - Hold! -key printing-alert:$editor \ - Wish $editor is labelled "Printing!" - sleep 0.25 - Hold! -key printing-alert:$editor {} - } - Control_underscore { - # ctrl and - (zoom out) - set textScale [dict get $textOptions scale] - set textScale [/ $textScale 1.1] - - Hold! -save -key font-options-of:$editor \ - Claim editor $editor has font options with scale $textScale - } - equal { - # Dunno why it registers as equal instead of Control_equal? It works regardless, lol - # ctrl and + (zoom in) - set textScale [dict get $textOptions scale] - set textScale [* $textScale 1.1] - - Hold! -save -key font-options-of:$editor \ - Claim editor $editor has font options with scale $textScale - } - } - } - } - - # Undo stack management with coalescing - if {$code ne $origCode && !$isUndo && !$isRedo} { - # Determine edit type for coalescing consecutive same-type edits - if {[dict exists $options printable]} { - set editType "insert" - } elseif {$key eq "Delete" || $key eq "Remove"} { - set editType "delete" - } else { - set editType "other" - } - - # Only push when edit type changes or for non-coalescable edits - if {$editType ne $lastEditType || $editType eq "other"} { - lappend undoStack [list $origCode $origCursor $origMaxCursorX] - if {[llength $undoStack] > 50} { - set undoStack [lrange $undoStack end-49 end] - } - } - set lastEditType $editType - - # Any real edit clears the redo stack - set redoStack {} - } elseif {!$isUndo && !$isRedo} { - # Navigation or no-op resets edit type (breaks coalescing) - set lastEditType "" - } - - if {$resetBuffer} { - set cursor 0 - set maxCursorX 0 - set selAnchor "" - set undoStack {} - set redoStack {} - set lastEditType "" - - # remove the edited code to restore the program to its original code - Hold! -on builtin-programs/programs.folk -key new-code-for:$program {} - file delete "$programDir/[regsub {\.folk$} $program {}].folk.edited" - - Hold! -save -key buffer-for:$program { - When $program has program code /originalCode/ { - Claim editor buffer for $program is $originalCode - } - } - } else { - Hold! -save -key buffer-for:$program \ - Claim editor buffer for $program is $code - } - - lassign [$utils cursorToXy $code $cursor] cursorX cursorY - # Be sure to have at least two characters on the left, so we can - # see what we're removing. - if {[- $cursorX 2] < $vpX} { - set vpX [max 0 [- $cursorX 2]] - } - if {$cursorX >= $vpX + $vpWidth} { - set vpX $($cursorX - $vpWidth) - } - if {$cursorY < $vpY} { set vpY $cursorY } - if {$cursorY >= $vpY + $vpHeight - 1} { - set vpY $($cursorY - $vpHeight + 1) - } - - Hold! -keep 12ms -key editor-state-of:$editor { - Claim editor $editor has viewport position [list $vpX $vpY] - Claim editor $editor has cursor $cursor - Claim editor $editor has max cursor x $maxCursorX - Claim editor $editor has selection anchor $selAnchor - Claim editor $editor has undo stack $undoStack - Claim editor $editor has redo stack $redoStack - Claim editor $editor has last edit type $lastEditType - } - } - } -} - -Subscribe: save code on editor /editor/ { - ForEach! editor $editor has selected program /program/ &\ - editor buffer for /program/ is /programCode/ { - Hold! -on builtin-programs/programs.folk -key new-code-for:$program \ - Wish program $program is replaced with \ - code $programCode editedTime [clock seconds] - - set programBase [regsub {\.folk$} $program ""] - set editedPath "$programDir/[set programBase].folk.edited" - file mkdir [file dirname $editedPath] - set fp [open $editedPath w] - puts -nonewline $fp $programCode - close $fp - - # Give the user some feedback - Hold! -key saved-alert:$editor \ - Wish $editor is labelled "Saved!" - sleep 0.25 - Hold! -key saved-alert:$editor {} - } -} - -# calculate cursor position -When /editor/ is an editor with /...anything/ &\ - editor /editor/ has cursor /cursor/ &\ - editor /editor/ has selected program /program/ &\ - editor buffer for /program/ is /code/ &\ - editor /editor/ has viewport position /vpPos/ &\ - editor /editor/ has font options with /...textOptions/ { - lassign $vpPos vpX vpY - - lassign [$utils cursorToXy $code $cursor] cursorX cursorY - - set textScale [dict get $textOptions scale] - set advance [$editorLib getAdvance $textScale] - - set offsetX $(($cursorX - $vpX) * $advance) - set offsetY $(($cursorY - $vpY) * $textScale) - - Claim editor $editor has cursor position [list $offsetX $offsetY] -} - -# Draw text and cursor -When /editor/ is an editor with /...anything/ &\ - editor /editor/ has viewport position /vpPos/ &\ - editor /editor/ has viewport size /vpSize/ &\ - editor /editor/ has font options with /...fontOptions/ { - lassign $vpPos vpX vpY - lassign $vpSize vpWidth vpHeight - - set textScale [dict get $fontOptions scale] - set advance [$editorLib getAdvance $textScale] - - When editor $editor has selected program /program/ &\ - editor buffer for /program/ is /code/ &\ - editor $editor has cursor /cursor/ &\ - editor $editor has cursor position /cursorPos/ &\ - editor $editor has selection anchor /selAnchor/ { - set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] - set lineNumbers [$utils lineNumberView $vpY $lineCount] - - set marginLeft [lindex $margin 3] - set lineNumbersRight $($marginLeft + $advance*1.5) - Wish to draw text onto $editor with \ - position [list $lineNumbersRight [lindex $margin 0]] \ - text $lineNumbers \ - scale $textScale anchor topright font NeomatrixCode - - set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] - set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] - Wish to draw text onto $editor with \ - position $pos text $text \ - scale $textScale anchor topleft font NeomatrixCode - - # Draw selection highlight - if {$selAnchor ne ""} { - set rawStart [min $selAnchor $cursor] - set rawEnd [max $selAnchor $cursor] - lassign [$utils cursorToXy $code $rawStart] selStartX selStartY - lassign [$utils cursorToXy $code $rawEnd] selEndX selEndY - - set lines [split $code "\n"] - for {set ly $selStartY} {$ly <= $selEndY} {incr ly} { - if {$ly < $vpY || $ly >= $vpY + $vpHeight} continue - - set lineLen [string length [lindex $lines $ly]] - - # Absolute column range for selection on this line - if {$ly == $selStartY} { - set absStart $selStartX - } else { - set absStart 0 - } - if {$ly == $selEndY} { - set absEnd $selEndX - } else { - set absEnd $lineLen - } - - # Clip to viewport - set absStart [max $absStart $vpX] - set absEnd [min $absEnd [+ $vpX $vpWidth]] - - if {$absStart >= $absEnd} continue - - # Convert to display coordinates - set dispStart [- $absStart $vpX] - set dispEnd [- $absEnd $vpX] - set dispRow [- $ly $vpY] - - set x0 $([lindex $pos 0] + $dispStart * $advance) - set x1 $([lindex $pos 0] + $dispEnd * $advance) - set y0 $([lindex $pos 1] + $dispRow * $textScale) - set y1 $($y0 + $textScale) - - Wish to draw a quad onto $editor with \ - p0 [list $x0 $y0] p1 [list $x1 $y0] \ - p2 [list $x1 $y1] p3 [list $x0 $y1] \ - color {0.2 0.4 0.8 0.7} layer -1 - } - } - - set p1 [vec2 add $cursorPos $pos] - set p2 [vec2 add $p1 [list 0 [* $textScale 1.2]]] - set s [/ $textScale 6] - Wish to draw a circle onto $editor with center $p1 radius $s thickness 0 color green filled true - Wish to draw a line onto $editor with points [list $p1 $p2] width $s color green - } -} - - -# end of library code -} diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index 4c18f387..d63f86c7 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -250,23 +250,23 @@ When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ sleep 0.25 Hold! -key printing-alert:$editor {} } - # Control_underscore { - # # ctrl and - (zoom out) - # set textScale [dict get $textOptions scale] - # set textScale [/ $textScale 1.1] - - # Hold! -save -key font-options-of:$editor \ - # Claim editor $editor has font options with scale $textScale - # } - # equal { - # # Dunno why it registers as equal instead of Control_equal? It works regardless, lol - # # ctrl and + (zoom in) - # set textScale [dict get $textOptions scale] - # set textScale [* $textScale 1.1] - - # Hold! -save -key font-options-of:$editor \ - # Claim editor $editor has font options with scale $textScale - # } + Control_underscore { + # ctrl and - (zoom out) + set textScale [dict get $textOptions scale] + set textScale [/ $textScale 1.1] + + Hold! -save -key font-options-of:$editor \ + Claim editor $editor has font options with scale $textScale + } + equal { + # Dunno why it registers as equal instead of Control_equal? It works regardless, lol + # ctrl and + (zoom in) + set textScale [dict get $textOptions scale] + set textScale [* $textScale 1.1] + + Hold! -save -key font-options-of:$editor \ + Claim editor $editor has font options with scale $textScale + } } } } From 624e2b5da924767729cd84589c8a3a6ccf31d794 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 15:21:28 -0400 Subject: [PATCH 23/45] print-editor: Line numbers and text seem to be aligned --- builtin-programs/editor/print-editor.folk | 26 +++++++---------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 2d3407de..2413db7a 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -54,10 +54,12 @@ fn codeToPs {id code opts {mixins {}}} { set line [string map {"\\" "\\\\" ")" "\\)" "(" "\\("} $line] incr lineIdx subst { - $marginLeft [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto - 0.4 setgray ([format "%- 3s" $lineIdx]) show - $lineNumbersRight [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto + 0.4 setgray ([format "% 3s" $lineIdx]) + dup stringwidth pop neg 0 rmoveto + show + + [+ $lineNumbersRight $opts(advance)] [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto settextcolor ($line) show } }] "\n"] @@ -91,6 +93,9 @@ fn codeToPs {id code opts {mixins {}}} { return [join $outPages "\n"] } +# TODO: handle Ctrl-P to print here +# TODO: separate option to enable live preview? + # Subscribe: print program from editor /editor/ { # set program [dict get [QueryOne! editor $editor has selected program /program/] program] # set code [dict get [QueryOne! editor buffer for $program is /code/] code] } @@ -113,21 +118,6 @@ When editor /editor/ has selected program /program/ &\ advance [* 0.5859375 $textScale $mToPt] \ margin [lmap x $margin {* $x $mToPt}]]] set ps [codeToPs $id $code $opts {}] - - # Wish to draw text onto $editor with \ - # position [list $lineNumbersRight [lindex $margin 0]] \ - # text $lineNumbers \ - # scale $textScale anchor topright font NeomatrixCode - - # set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] - # set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] - # # TODO: construct postscript such that the text is set down - # # from top-left by $pos - # Wish to draw text onto $editor with \ - # position $pos text $text \ - # scale $textScale anchor topleft font NeomatrixCode - - # lappend postScript {..} puts stderr "Render!" set fp [open "/tmp/$id.ps" w]; puts $fp $ps; close $fp From acfff71b09b9aa281ffdfe1e33e04b56f17997e5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Tue, 19 May 2026 18:16:57 -0400 Subject: [PATCH 24/45] WIP: Editor-based printing works (may break web printing, though) It's not perfectly aligned -- need to look into. Also still some subtleties with different paper formats to fix / make sure work. Basically, I think the editor needs to be format-aware/media-aware in order for this all to make sense. --- builtin-programs/editor/print-editor.folk | 177 ++++++----------- builtin-programs/print/print.folk | 230 ++++++++-------------- 2 files changed, 136 insertions(+), 271 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 2413db7a..5f21ef1e 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -1,5 +1,3 @@ -When the print library is /printLib/ { - set formats [subst { letter { tagSize {150 150} @@ -17,135 +15,74 @@ set formats [subst { # indexcard (really receipt) assumes fake letter/A4 size: # https://github.com/NaitLee/Cat-Printer/discussions/8#discussioncomment-2557843 -fn codeToPs {id code opts {mixins {}}} { - # All opts should be passed in as points (1/2834.65 of a meter). - lassign $opts(pageSize) PageWidth PageHeight - lassign $opts(tagSize) tagWidth tagHeight - lassign $opts(margin) marginTop marginRight marginBottom marginLeft - set lineHeight $opts(lineHeight) - set maxLines $(int(($PageHeight - $marginTop - $marginBottom) / $lineHeight)) - - set lineNumbersRight $($marginLeft + $opts(advance)*1.5) - - set lines [split $code "\n"] - - set image [$printLib tagPsForId $id] - - set outPages [list] - set lineIdx 0 - while {[llength $lines] > 0} { - set pageLines [lrange $lines 0 $maxLines] - set lines [lreplace $lines 0 $maxLines] - - # The typesetting here is meant to exactly duplicate the - # layout in the editor. - lappend outPages [subst { - %!PS - << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice - - /settextcolor {0 setgray} def - - /NeomatrixCode findfont - $lineHeight scalefont - setfont - - newpath - [join [lmap line $pageLines { - set line [string map {"\\" "\\\\" ")" "\\)" "(" "\\("} $line] - incr lineIdx - subst { - $lineNumbersRight [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto - 0.4 setgray ([format "% 3s" $lineIdx]) - dup stringwidth pop neg 0 rmoveto - show - - [+ $lineNumbersRight $opts(advance)] [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto - settextcolor ($line) show - } - }] "\n"] - - [expr {[llength $outPages] > 0 ? {} : [subst { - gsave - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$marginTop}] translate - $tagWidth $tagHeight scale - $image - grestore - - /Helvetica-Narrow findfont - 11 scalefont - setfont - newpath - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-14-$marginTop}] moveto - ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show - - [join [lmap mixin $mixins { - # We run mixins only on page 1 for now. They - # get access to everything in scope. Kind of - # hacky, but OK for now. - - subst $mixin - }] "\n"] - }] }] - - showpage - }] - } - return [join $outPages "\n"] -} - -# TODO: handle Ctrl-P to print here -# TODO: separate option to enable live preview? - -# Subscribe: print program from editor /editor/ { - # set program [dict get [QueryOne! editor $editor has selected program /program/] program] -# set code [dict get [QueryOne! editor buffer for $program is /code/] code] } -When editor /editor/ has selected program /program/ &\ - editor buffer for /program/ is /code/ { - set lines [split $code "\n"] +fn editorToPrintOptions {editor} { + set program [dict get [QueryOne! editor $editor has selected program /program/] program] + set code [dict get [QueryOne! editor buffer for $program is /code/] code] set fontOptions [dict get [QueryOne! editor $editor has font options with /...opts/] opts] set textScale $fontOptions(scale) set margin [dict get [QueryOne! editor $editor has margin /margin/] margin] - # TODO: scan for mixins (a mixin should be, like, Field detector) - set id 48700 - # TODO: support other formats + # TODO: support other formats (it should be set in the editor so + # the user gets an accurate preview.) + # if {![dict exists $options format]} { + # set defaultFormatMatches [Query! paper format /format/ is the default paper format] + # if {[llength $defaultFormatMatches] > 0} { + # dict set options format [dict get [lindex $defaultFormatMatches 0] format] + # } + # } set fmt $formats(letter) set mToPt 2834.646 - set opts [dict merge $fmt \ - [dict create \ - lineHeight [* $textScale $mToPt] \ - advance [* 0.5859375 $textScale $mToPt] \ - margin [lmap x $margin {* $x $mToPt}]]] - set ps [codeToPs $id $code $opts {}] + return [dict merge $fmt \ + [dict create \ + code $code \ + lineHeight [* $textScale $mToPt] \ + advance [* 0.5859375 $textScale $mToPt] \ + margin [lmap x $margin {* $x $mToPt}]]] +} - puts stderr "Render!" - set fp [open "/tmp/$id.ps" w]; puts $fp $ps; close $fp - puts stderr [exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ - /tmp/$id.ps /tmp/$id.pdf] +Subscribe: print program from editor /editor/ { + set options [editorToPrintOptions $editor] + Notify: print a new program with {*}$options +} + +# Print preview: +When the codeToPostScript is /codeToPostScript/ &\ + /someone/ wishes editor /editor/ has a print preview &\ + editor /editor/ has selected program /program/ { - # TODO: save to a temporary pdf, puts the temporary pdf's filename - # TODO: preview the pdf next to the editor for now - # Hold! { - set preview [list $editor preview] - Wish $preview has a canvas - set previewGeom [list width [/ [lindex $fmt(pageSize) 0] $mToPt] \ - height [/ [lindex $fmt(pageSize) 1] $mToPt]] - Claim $preview has resolved geometry $previewGeom + # I want these lines to be 4-indented, not 8-indented, + # based on the When being 0-indented that corresponds to the + # most recent opening brace. + puts "I want this line to be 4-indented, but it's 8-indented" - When the quad library is /quadLib/ & $editor has quad /q/ { - Claim $preview has quad [$quadLib alignGeometry \ - [$quadLib move $q right 100%] \ - $previewGeom] - } - set pngFile [file tempfile /tmp/$id-XXXXXX].png - exec gs -dNOPAUSE -dBATCH -sDEVICE=png16m -r300 -sOutputFile=$pngFile /tmp/$id.pdf - # FIXME: display without margin! - Wish $preview is outlined white + set preview [list $editor preview] + Wish $preview has a canvas + set fmt $formats(letter) + set mToPt 2834.646 + set previewGeom [list width [/ [lindex $fmt(pageSize) 0] $mToPt] \ + height [/ [lindex $fmt(pageSize) 1] $mToPt]] + Claim $preview has resolved geometry $previewGeom + When the quad library is /quadLib/ & $editor has quad /q/ { + Claim $preview has quad [$quadLib alignGeometry \ + [$quadLib move $q right 100%] \ + $previewGeom] + } + Wish $preview is outlined white + + fn codeToPostScript + When editor buffer for $program is /code/ { + set ps [codeToPostScript 48700 $code [editorToPrintOptions $editor]] + + set psFile [file tempfile].ps + set fp [open $psFile w]; puts $fp $ps; close $fp + set pngFile [file tempfile].png + set result [exec gs -dNOPAUSE -dBATCH -sFONTPATH=vendor/fonts \ + -sDEVICE=png16m -r300 \ + -sOutputFile=$pngFile $psFile] + puts stderr "gs to render preview: $result" Wish $preview displays image $pngFile with width $previewGeom(width) - # } - sleep 10 - # Hold! {} + } } } diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index 37c89844..7988588a 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -83,150 +83,83 @@ $cc proc tagPsForId {int id} char* { set printLib [$cc compile] Claim the print library is $printLib -fn paginate {text maxlines linelen {linelenOverrides {}}} { - set lines [split $text "\n"] - - for {set i 0} {$i < [llength $lines]} {incr i} { - # tag each line with its 1-indexed line number - lset lines $i [list [expr {$i+1}] [lindex $lines $i]] - } - - set safeline 0 - set firstline 0 - set pages "" - for {set i 0} {$i < [llength $lines]} {incr i} { # hard-wrap lines - if {$i - $firstline > $maxlines - 1} { - set pagelines [lrange $lines $firstline $safeline-1] - lappend pagelines [list "..." ""] - lappend pages $pagelines - set firstline $safeline - } - - lassign [lindex $lines $i] linenum line - set max [dict getdef $linelenOverrides $i $linelen] - if {$max == 0} { - lset lines $i [list "" ""] - set lines [linsert $lines $i+1 [list $linenum $line]] - - } elseif {[string length $line] > $max} { - lset lines $i 1 [string range $line 0 $max] - set lines [linsert $lines $i+1 [list "" [string range $line $max+1 end]]] - - } elseif {$linenum ne ""} { - set safeline $i - } - } - - lappend pages [lrange $lines $firstline end] - - return $pages -} - -fn rangeDict {from to val} { - set res "" - for {set i $from} {$i < $to} {incr i} { - lappend res $i $val - } - return $res -} - -fn programToPs {id text {format "letter"} {mixins {}}} { - set defaults { - margin 36 - fontsize 12 - tagsize {150 150} - maxcharsOverride {} - } - set formats [subst { - letter { - pagesize {612 792} - maxlines 40 - maxchars 72 - maxcharsOverride {[rangeDict 0 8 49]} - } - a4 { - pagesize {595 842} - maxlines 43 - maxchars 68 - maxcharsOverride {[rangeDict 0 8 46]} - } - indexcard { - fontsize 24 - tagsize {300 300} - pagesize {612 792} - maxlines 22 - maxchars 34 - maxcharsOverride {[rangeDict 0 9 0]} - } - }] - # indexcard (really receipt) assumes fake letter/A4 size: - # https://github.com/NaitLee/Cat-Printer/discussions/8#discussioncomment-2557843 - - set params [dict merge $defaults [dict get $formats $format]] - dict with params { - lassign $pagesize PageWidth PageHeight - lassign $tagsize tagwidth tagheight - set lineheight [expr $fontsize*1.5] - - set image [$printLib tagPsForId $id] - - set pages [paginate $text $maxlines $maxchars $maxcharsOverride] - - set out "" - set pageidx 0 - foreach lines $pages { - set lineidx 0 - append out [subst { - %!PS - << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice - - /settextcolor {0 setgray} def - - /Courier findfont - $fontsize scalefont +fn codeToPostScript {id code opts {mixins {}}} { + # All opts should be passed in as points (1/2834.65 of a meter). + lassign $opts(pageSize) PageWidth PageHeight + lassign $opts(tagSize) tagWidth tagHeight + lassign $opts(margin) marginTop marginRight marginBottom marginLeft + set lineHeight $opts(lineHeight) + set maxLines $(int(($PageHeight - $marginTop - $marginBottom) / $lineHeight)) + + set lineNumbersRight $($marginLeft + $opts(advance)*1.5) + + set lines [split $code "\n"] + + set image [$printLib tagPsForId $id] + + set outPages [list] + set lineIdx 0 + while {[llength $lines] > 0} { + set pageLines [lrange $lines 0 $maxLines] + set lines [lreplace $lines 0 $maxLines] + + # The typesetting here is meant to exactly duplicate the + # layout in the editor. + lappend outPages [subst { + %!PS + << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice + + /settextcolor {0 setgray} def + + /NeomatrixCode findfont + $lineHeight scalefont + setfont + + newpath + [join [lmap line $pageLines { + set line [string map {"\\" "\\\\" ")" "\\)" "(" "\\("} $line] + incr lineIdx + subst { + $lineNumbersRight [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto + 0.4 setgray ([format "% 3s" $lineIdx]) + dup stringwidth pop neg 0 rmoveto + show + + [+ $lineNumbersRight $opts(advance)] [expr {$PageHeight-$marginTop-$lineIdx*$lineHeight}] moveto + settextcolor ($line) show + } + }] "\n"] + + [expr {[llength $outPages] > 0 ? {} : [subst { + gsave + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$marginTop}] translate + $tagWidth $tagHeight scale + $image + grestore + + /Helvetica-Narrow findfont + 11 scalefont setfont newpath - [join [lmap lineinfo $lines { - lassign $lineinfo linenum line - set line [string map {"\\" "\\\\" ")" "\\)" "(" "\\("} $line] - incr lineidx - subst { - $margin [expr $PageHeight-$margin-$lineidx*$lineheight] moveto - 0.4 setgray ([format "%- 3s" $linenum]) show settextcolor ($line) show - } + [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-14-$marginTop}] moveto + ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show + + [join [lmap mixin $mixins { + # We run mixins only on page 1 for now. They + # get access to everything in scope. Kind of + # hacky, but OK for now. + + subst $mixin }] "\n"] + }] }] - [expr {$pageidx ? {} : [subst { - gsave - [expr $PageWidth-$tagwidth-$margin] [expr $PageHeight-$tagheight-$margin] translate - $tagwidth $tagheight scale - $image - grestore - - /Helvetica-Narrow findfont - [- $fontsize 2] scalefont - setfont - newpath - [expr $PageWidth-$tagwidth-$margin] [expr $PageHeight-$tagheight-16-$margin] moveto - ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show - - [join [lmap mixin $mixins { - # We run mixins only on page 1 for now. They - # get access to everything in scope. Kind of - # hacky, but OK for now. - - subst $mixin - }] "\n"] - }] }] - showpage - }] - incr pageidx - } + showpage + }] } - - return $out + return [join $outPages "\n"] } -Claim the programToPs is [fn programToPs] +Claim the codeToPostScript is [fn codeToPostScript] + fn nextId {} { set idResults [Query! the next program id is /id/] if {[llength $idResults] == 0} { @@ -262,18 +195,17 @@ When $::thisNode claims printer /name/ is a cups printer with /...options/ { exec {*}$command } -Subscribe: print code /code/ with /...options/ { +Subscribe: print a new program with /...options/ { if {$::thisNode eq "folk-beads" || $::thisNode eq "folk-convivial"} { # HACK: Forward the print request to folk-hex. exec curl -X POST "http://folk-hex.local:4273/" \ -H "Content-Type: text/plain" \ - -d [list Notify: print code $code with printer Canon_TR150_series]; + -d [list Notify: print a new program with {*}$options]; return } set id [nextId] - if {![info exists options]} { set options [dict create]} - Notify: print program $id with code $code {*}$options + Notify: print program $id with {*}$options } Subscribe: print program /id/ with /...options/ { set code [dict get $options code] @@ -318,21 +250,17 @@ Subscribe: print program /id/ with /...options/ { set args [list -P [dict get $options printer]] } - set ps [programToPs $id $code $format] + set ps [codeToPostScript $id $code $options] # save code and ps to disk if {[file exists "$saveDir/$id.folk"]} { error "Program $id already exists on disk. Aborting print." } - set fp [open "$saveDir/$id.folk" w] - puts $fp $code - close $fp - - set fp [open "$saveDir/$id.ps" w] - puts $fp $ps - close $fp + set fp [open "$saveDir/$id.folk" w]; puts $fp $code; close $fp - exec ps2pdf $saveDir/$id.ps $saveDir/$id.pdf + set fp [open "$saveDir/$id.ps" w]; puts $fp $ps; close $fp + exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ + $saveDir/$id.ps $saveDir/$id.pdf puts "Printing program $id on $::thisNode" exec lpr {*}$args $saveDir/$id.pdf From 1d73af9bfdc27f054c6c9ffd38b2731d605930f0 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 20 May 2026 09:59:14 -0400 Subject: [PATCH 25/45] WIP: Factor calibration pdf into separate program Next: use the calibration pdf to calibrate tag size and margins such that we can print at accurate scale after calibration, and such that we calibrate at exactly the same tag size that we print programs at. --- .../calibrate/calibrate-page.folk | 121 ++++-------------- .../calibrate/calibration-pdf.folk | 82 ++++++++++++ 2 files changed, 109 insertions(+), 94 deletions(-) create mode 100644 builtin-programs/calibrate/calibration-pdf.folk diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 7ec0e299..eaaf53f4 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -1,95 +1,28 @@ -When the print library is /printLib/ &\ - the programToPs is /programToPs/ &\ - the calibration matrix library is /matLib/ &\ - the calibration model library is /modelLib/ { - -fn programToPs - -fn makeCalibrationBoardPdf {model tagSideLengthPs} { - package require linalg - namespace import ::math::linearalgebra::add - - set marginTop 72; set marginLeft 36 - set PageWidth 612; set PageHeight 792 - - set innerToOuter 0.333333 - - set tagOuterLengthPs [expr {$tagSideLengthPs * 10/6}] - - set H_modelToPs [$matLib estimateHomography [subst { - {1 1 $tagSideLengthPs $tagSideLengthPs} - {1 0 $tagSideLengthPs 0} - {0 1 0 $tagSideLengthPs} - {0 0 0 0} - }]] - - set ps [subst { - %!PS - << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice - - % (0, 0) is bottom-left of portrait page right now. - 90 rotate 1 -1 scale - % Now (0, 0) is top-left of landscape page. - - gsave - $marginLeft [- $marginTop 18] translate - 1 -1 scale - 0 setgray /Helvetica findfont 14 scalefont setfont - newpath 0 0 moveto (Folk calibration board) show - grestore - - $marginLeft $marginTop translate - - [set tagIdx -1] - [join [lmap {id modelTag} $model { - if {![$modelLib isPrintedTag $id]} { continue } - incr tagIdx - - set modelInnerTopLeft [lindex [dict get $modelTag p] 3] - set modelOuterTopLeft [add $modelInnerTopLeft [list -$innerToOuter -$innerToOuter]] - lassign [$matLib applyHomography $H_modelToPs $modelOuterTopLeft] psX psY - subst { - gsave - $psX [+ $psY $tagOuterLengthPs] translate - $tagOuterLengthPs -$tagOuterLengthPs scale - [$printLib tagPsForId $id] - grestore - - % Label the inner side length: - [if {$tagIdx == 1} { subst { - gsave - [expr {$psX + ($tagOuterLengthPs - $tagSideLengthPs)/2}] - [expr {$psY - 15}] translate - 1 -1 scale - 0.1 0.67 0.1 setrgbcolor - newpath 0 0 moveto $tagSideLengthPs 0 lineto stroke - newpath 0 0 moveto 0 -5 lineto stroke - newpath $tagSideLengthPs 0 moveto $tagSideLengthPs -5 lineto stroke - /Helvetica findfont 7 scalefont setfont - newpath 0 5 moveto (inner side length) show - grestore - } }] - } - }] "\n"] - }] +When the codeToPostScript is /codeToPostScript/ &\ + the makeCalibrationBoardPdf is /makeCalibrationBoardPdf/ { - set fp [open [list |ps2pdf - - <<$ps] rb] - set pdf [read $fp]; close $fp - return $pdf -} +fn codeToPostScript +fn makeCalibrationBoardPdf fn makeExampleProgramPng {} { # HACK: we hard-code letter, since this is just for documentation # purposes, and we want to cut the bottom half off so we need # known dimensions. - set format letter - set ps [{*}$programToPs 0 {# This image is for illustration purposes; don't + set opts [dict create \ + tagSize {150 150} \ + pageSize {612 792} \ + lineHeight 16 \ + advance [* 0.5859375 16] \ + margin [lmap x {0.01 0.005 0.005 0.01} \ + {* 2834.646 $x}]] + set ps [codeToPostScript 0 {# This image is for illustration purposes; don't # print it. You should print a program normally -# through the Folk editor and measure that.} $format {{ +# through the Folk editor and measure that. +} $opts {{ - [set left [expr {$PageWidth-$tagwidth-$margin}]] - [set bottom [expr {$PageHeight-$tagheight-$margin}]] - [set outerToInner [expr {($tagwidth / 10.0) * 2}]] + [set left [expr {$PageWidth-$tagWidth-$marginRight}]] + [set bottom [expr {$PageHeight-$tagHeight-$marginTop}]] + [set outerToInner [expr {($tagWidth / 10.0) * 2}]] % These take in x1 y1 x2 y2 on stack. /markXDistance { @@ -108,28 +41,28 @@ fn makeExampleProgramPng {} { } def % Left - [+ $left $outerToInner] [expr {$bottom + $tagheight/2.0}] - 0 [expr {$bottom + $tagheight/2.0}] + [+ $left $outerToInner] [expr {$bottom + $tagHeight/2.0}] + 0 [expr {$bottom + $tagHeight/2.0}] 1 0 0 setrgbcolor markXDistance % Right - [expr {$left + $tagwidth - $outerToInner}] [expr {$bottom + $tagheight/2.0}] - $PageWidth [expr {$bottom + $tagheight/2.0}] + [expr {$left + $tagWidth - $outerToInner}] [expr {$bottom + $tagHeight/2.0}] + $PageWidth [expr {$bottom + $tagHeight/2.0}] 1 0 0 setrgbcolor markXDistance % Top - [expr {$left + $tagwidth/2.0}] [expr {$bottom + $tagheight - $outerToInner}] - [expr {$left + $tagwidth/2.0}] $PageHeight + [expr {$left + $tagWidth/2.0}] [expr {$bottom + $tagHeight - $outerToInner}] + [expr {$left + $tagWidth/2.0}] $PageHeight 0 0.5 1 setrgbcolor markYDistance % Bottom - [expr {$left + $tagwidth/2.0}] [expr {$bottom + $outerToInner}] - [expr {$left + $tagwidth/2.0}] [expr {$PageHeight/2.0}] + [expr {$left + $tagWidth/2.0}] [expr {$bottom + $outerToInner}] + [expr {$left + $tagWidth/2.0}] [expr {$PageHeight/2.0}] 0 0.5 1 setrgbcolor markYDistance % Tag inner [+ $left $outerToInner] [+ $bottom $outerToInner 5] - [expr {$left + $tagwidth - $outerToInner}] [+ $bottom $outerToInner 5] + [expr {$left + $tagWidth - $outerToInner}] [+ $bottom $outerToInner 5] 0 1 0 setrgbcolor markXDistance }}] @@ -141,7 +74,7 @@ fn makeExampleProgramPng {} { Wish the web server handles route "/calibrate" with hidden true handler { package require base64 - set calibrationBoardPdf [makeCalibrationBoardPdf [$modelLib unitModel] 70] + set calibrationBoardPdf [makeCalibrationBoardPdf] set exampleProgramPng [makeExampleProgramPng] diff --git a/builtin-programs/calibrate/calibration-pdf.folk b/builtin-programs/calibrate/calibration-pdf.folk new file mode 100644 index 00000000..4dcdeb3f --- /dev/null +++ b/builtin-programs/calibrate/calibration-pdf.folk @@ -0,0 +1,82 @@ +When the print library is /printLib/ &\ + the calibration model library is /modelLib/ &\ + the calibration matrix library is /matLib/ { + +fn makeCalibrationBoardPdf {} { + set model [$modelLib unitModel] + set tagSideLengthPs 70 + + package require linalg + namespace import ::math::linearalgebra::add + + set marginTop 72; set marginLeft 36 + set PageWidth 612; set PageHeight 792 + + set innerToOuter 0.333333 + + set tagOuterLengthPs [expr {$tagSideLengthPs * 10/6}] + + set H_modelToPs [$matLib estimateHomography [subst { + {1 1 $tagSideLengthPs $tagSideLengthPs} + {1 0 $tagSideLengthPs 0} + {0 1 0 $tagSideLengthPs} + {0 0 0 0} + }]] + + set ps [subst { + %!PS + << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice + + % (0, 0) is bottom-left of portrait page right now. + 90 rotate 1 -1 scale + % Now (0, 0) is top-left of landscape page. + + gsave + $marginLeft [- $marginTop 18] translate + 1 -1 scale + 0 setgray /Helvetica findfont 14 scalefont setfont + newpath 0 0 moveto (Folk calibration board) show + grestore + + $marginLeft $marginTop translate + + [set tagIdx -1] + [join [lmap {id modelTag} $model { + if {![$modelLib isPrintedTag $id]} { continue } + incr tagIdx + + set modelInnerTopLeft [lindex [dict get $modelTag p] 3] + set modelOuterTopLeft [add $modelInnerTopLeft [list -$innerToOuter -$innerToOuter]] + lassign [$matLib applyHomography $H_modelToPs $modelOuterTopLeft] psX psY + subst { + gsave + $psX [+ $psY $tagOuterLengthPs] translate + $tagOuterLengthPs -$tagOuterLengthPs scale + [$printLib tagPsForId $id] + grestore + + % Label the inner side length: + [if {$tagIdx == 1} { subst { + gsave + [expr {$psX + ($tagOuterLengthPs - $tagSideLengthPs)/2}] + [expr {$psY - 15}] translate + 1 -1 scale + 0.1 0.67 0.1 setrgbcolor + newpath 0 0 moveto $tagSideLengthPs 0 lineto stroke + newpath 0 0 moveto 0 -5 lineto stroke + newpath $tagSideLengthPs 0 moveto $tagSideLengthPs -5 lineto stroke + /Helvetica findfont 7 scalefont setfont + newpath 0 5 moveto (inner side length) show + grestore + } }] + } + }] "\n"] + }] + + set fp [open [list |ps2pdf - - <<$ps] rb] + set pdf [read $fp]; close $fp + return $pdf +} +Claim the makeCalibrationBoardPdf is [fn makeCalibrationBoardPdf] + +} From 7703e0eb75b851917a7eb4f9f7228228996d87b5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 20 May 2026 14:22:03 -0400 Subject: [PATCH 26/45] prelude: Make fn (lexical) procs local so that we can live-update them when we hotpatch a dependency program (they won't just hang around the global namespace for that interp). --- prelude.tcl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/prelude.tcl b/prelude.tcl index e8a791b8..a4bd660f 100644 --- a/prelude.tcl +++ b/prelude.tcl @@ -46,7 +46,9 @@ proc unknown {cmdName args} { # environment (probably passed through a statement) # and can just be applied to args. set fnObj [lindex $fn 0] - proc $cmdName args {fnObj} { tailcall {*}$fnObj {*}$args } + uplevel [list local proc $cmdName \ + args [list [list fnObj $fnObj]] \ + { tailcall {*}$fnObj {*}$args }] tailcall $cmdName {*}$args } @@ -75,9 +77,11 @@ proc unknown {cmdName args} { set env [dict merge {*}[lrange $envStack 0 $i]] dict set env __envStack $envStack dict set env __env $env - dict with env { - proc $cmdName $argNames [dict keys $env] $body - } + + set envPairs [list] + dict for {k v} $env { lappend envPairs [list $k $v] } + uplevel [list local proc $cmdName \ + $argNames $envPairs $body] tailcall $cmdName {*}$args } From ca12d8adb56ddf063064e65e4ddcf2a91a3bdc3a Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 20 May 2026 14:22:42 -0400 Subject: [PATCH 27/45] WIP: Start working on new calibration board Need to make everything use the same print pipeline so we can be sure that the user is actually calibrating what they'll actually use to print programs. --- .../calibrate/calibrate-page.folk | 5 ++-- ...on-pdf.folk => calibration-board-pdf.folk} | 30 +++++++++++++++++-- builtin-programs/print/print.folk | 6 +++- 3 files changed, 36 insertions(+), 5 deletions(-) rename builtin-programs/calibrate/{calibration-pdf.folk => calibration-board-pdf.folk} (71%) diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index eaaf53f4..547f18cd 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -184,12 +184,13 @@ Wish the web server handles route "/calibrate" with hidden true handler {
  • Print the calibration board.

    -

    Print this calibration board and glue/tape it to +

    We're going to print this calibration board and glue/tape it to something solid and flat (hardcover book, solid cardboard, etc):

    -

    You can download the PDF and print it yourself, or print through Folk if your printer is set up:

    +

    Make sure your printer is set up.

    +

    Print the calibration board through Folk:

    Try to keep the board from bending or warping. Printing on cardstock can help.

    -

    (During calibration, Folk will want to project AprilTags in the gaps on the grid of tags on the board. Instead of printing, you can - try just maximizing the board on your computer/tablet - screen, but you'll need to cover each gap with sticky note or something else that Folk can project - tags on.)

  • diff --git a/builtin-programs/calibrate/calibration-board-pdf.folk b/builtin-programs/calibrate/calibration-board-pdf.folk index ae83c2ec..187268e9 100644 --- a/builtin-programs/calibrate/calibration-board-pdf.folk +++ b/builtin-programs/calibrate/calibration-board-pdf.folk @@ -17,14 +17,16 @@ When the print library is /printLib/ &\ # Then we can correct for these factors in all future prints, so we # can print mm-accurate. -fn makeCalibrationBoardPdf {} { +fn makeCalibrationBoardPs {} { set model [$modelLib unitModel] set tagSideLengthPs 70 package require linalg namespace import ::math::linearalgebra::add - set marginTop 24; set marginLeft 24 + set marginTop 48; set marginLeft 48 + set measureTop [/ $marginTop 2]; set measureLeft [/ $marginLeft 2] + set PageWidth 612; set PageHeight 792 set innerToOuter 0.333333 @@ -52,15 +54,15 @@ fn makeCalibrationBoardPdf {} { 1 setlinecap 2 setlinewidth 0.67 0.1 0.1 setrgbcolor - % Short red segment at top margin, with arrow up to top edge. + % Short red segment at top measure, with arrow up to top edge. 2 setlinewidth newpath - [expr {$PageWidth/2 - 20}] [- $PageHeight $marginTop] moveto - [expr {$PageWidth/2 + 20}] [- $PageHeight $marginTop] lineto + [expr {$PageWidth/2 - 20}] [- $PageHeight $measureTop] moveto + [expr {$PageWidth/2 + 20}] [- $PageHeight $measureTop] lineto stroke 1 setlinewidth newpath - [/ $PageWidth 2] [- $PageHeight $marginTop] moveto + [/ $PageWidth 2] [- $PageHeight $measureTop] moveto [/ $PageWidth 2] [expr {$PageHeight - 2}] lineto stroke newpath @@ -71,18 +73,18 @@ fn makeCalibrationBoardPdf {} { [/ $PageWidth 2] [expr {$PageHeight - 2}] moveto [expr {$PageWidth/2 + 4}] [expr {$PageHeight - 8}] lineto stroke - newpath [expr {$PageWidth/2 + 25}] [expr {$PageHeight - $marginTop/2 - 3}] moveto + newpath [expr {$PageWidth/2 + 12}] [expr {$PageHeight - $measureTop/2 - 3}] moveto (Measure to top edge of paper) show - % Short red segment at bottom margin, with arrow down to bottom edge. + % Short red segment at bottom measure, with arrow down to bottom edge. 2 setlinewidth newpath - [expr {$PageWidth/2 - 20}] $marginTop moveto - [expr {$PageWidth/2 + 20}] $marginTop lineto + [expr {$PageWidth/2 - 20}] $measureTop moveto + [expr {$PageWidth/2 + 20}] $measureTop lineto stroke 1 setlinewidth newpath - [/ $PageWidth 2] $marginTop moveto + [/ $PageWidth 2] $measureTop moveto [/ $PageWidth 2] 2 lineto stroke newpath @@ -93,20 +95,20 @@ fn makeCalibrationBoardPdf {} { [/ $PageWidth 2] 2 moveto [expr {$PageWidth/2 + 4}] 8 lineto stroke - newpath [expr {$PageWidth/2 + 25}] [expr {$marginTop/2- 3}] moveto + newpath [expr {$PageWidth/2 + 12}] [expr {$measureTop/2- 3}] moveto (Measure to bottom edge of paper) show 0.1 0.1 0.67 setrgbcolor - % Short blue segment at left margin, with arrow left to left edge. + % Short blue segment at left measure, with arrow left to left edge. 2 setlinewidth newpath - $marginLeft [expr {$PageHeight/2 - 20}] moveto - $marginLeft [expr {$PageHeight/2 + 20}] lineto + $measureLeft [expr {$PageHeight/2 - 20}] moveto + $measureLeft [expr {$PageHeight/2 + 20}] lineto stroke 1 setlinewidth newpath - $marginLeft [/ $PageHeight 2] moveto + $measureLeft [/ $PageHeight 2] moveto 2 [/ $PageHeight 2] lineto stroke newpath @@ -117,9 +119,9 @@ fn makeCalibrationBoardPdf {} { 2 [/ $PageHeight 2] moveto 8 [expr {$PageHeight/2 + 4}] lineto stroke - newpath [/ $marginLeft 4] [expr {$PageHeight/2 - 30}] moveto + newpath [/ $measureLeft 4] [expr {$PageHeight/2 - 30}] moveto (Measure to) show - [/ $marginLeft 4] [expr {$PageHeight/2 - 37}] moveto + [/ $measureLeft 4] [expr {$PageHeight/2 - 37}] moveto (left edge of paper) show % We should flip the coordinate system to match the model coordinate system, @@ -142,11 +144,11 @@ fn makeCalibrationBoardPdf {} { [$printLib tagPsForId $id] grestore - gsave - 0 setgray /Helvetica findfont 14 scalefont setfont - 1 0 0 setrgbcolor - newpath $psX $psY moveto 1 -1 scale ($tagIdx) show - grestore + % gsave + % 0 setgray /Helvetica findfont 14 scalefont setfont + % 1 0 0 setrgbcolor + % newpath $psX $psY moveto 1 -1 scale ($tagIdx) show + % grestore % Label the inner side length: [if {$tagIdx == 1} { subst { @@ -164,14 +166,37 @@ fn makeCalibrationBoardPdf {} { } }] } }] "\n"] + + showpage }] + return $ps +} +Claim the makeCalibrationBoardPs is [fn makeCalibrationBoardPs] + +fn makeCalibrationBoardPdf {} { + set ps [makeCalibrationBoardPs] set fp [open [list |ps2pdf - - <<$ps] rb] set pdf [read $fp]; close $fp return $pdf } Claim the makeCalibrationBoardPdf is [fn makeCalibrationBoardPdf] +fn makeCalibrationBoardPng {} { + set ps [makeCalibrationBoardPs] + set psFile [file tempfile].ps + set fp [open $psFile w]; puts $fp $ps; close $fp + set pngFile [file tempfile].png + exec gs -dNOPAUSE -dBATCH -sFONTPATH=vendor/fonts \ + -sDEVICE=png16m -r144 \ + -sOutputFile=$pngFile $psFile + puts stderr $pngFile + set fp [open $pngFile rb] + set png [read $fp]; close $fp + return $png +} +Claim the makeCalibrationBoardPng is [fn makeCalibrationBoardPng] + Wish the web server handles route {/calibrate/board.pdf} with handler { dict create statusAndHeaders "HTTP/1.1 200 OK Connection: close @@ -181,4 +206,13 @@ Content-Type: application/pdf body [makeCalibrationBoardPdf] } +Wish the web server handles route {/calibrate/board.png} with handler { + dict create statusAndHeaders "HTTP/1.1 200 OK +Connection: close +Content-Type: image/png + +" \ + body [makeCalibrationBoardPng] +} + } diff --git a/builtin-programs/calibrate/model.folk b/builtin-programs/calibrate/model.folk index 0386c324..e69004df 100644 --- a/builtin-programs/calibrate/model.folk +++ b/builtin-programs/calibrate/model.folk @@ -7,8 +7,8 @@ Claim the calibration model library is [library create modelLib { namespace import ::math::linearalgebra::scale \ ::math::linearalgebra::add - variable ROWS 4 - variable COLS 3 + variable ROWS 5 + variable COLS 4 proc rows {} { variable ROWS; return $ROWS } proc cols {} { variable COLS; return $COLS } # A model is a dictionary whose keys are tag IDs and where each @@ -20,7 +20,7 @@ Claim the calibration model library is [library create modelLib { set tagSideLength 1.0 set tagOuterLength [expr {$tagSideLength * 10/6}] - set pad [expr {$tagSideLength / 2}] + set pad [expr {$tagSideLength / 3}] for {set row 0} {$row < $ROWS} {incr row} { for {set col 0} {$col < $COLS} {incr col} { set id [expr {48600 + $row*$COLS + $col}] @@ -57,9 +57,14 @@ Claim the calibration model library is [library create modelLib { return $($id >= 48600 && $id < 48600 + $ROWS*$COLS) } proc isPrintedTag {id} { + variable COLS if {![isCalibrationTag $id]} { return false } set idx [- $id 48600] - return [expr {$idx % 2 == 0}] ;# for checkerboard + set row [expr {int($idx / $COLS)}] + set col [expr {$idx % $COLS}] + return [expr {$row % 2 == 0 ? + ($col % 2 == 1) : + ($col % 2 == 0)}] ;# for checkerboard } proc isProjectedTag {id} { if {![isCalibrationTag $id]} { return false } From 6543517be071d66a7c0e37ae97ae5160680ed7d9 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 14:23:54 -0400 Subject: [PATCH 30/45] WIP: Remove interactively-refine. Add UI for new measurements Automatically check default display and camera in setup.folk for easier calibrate. --- .../calibrate/calibrate-page.folk | 87 +-- .../calibrate/interactively-refine.folk | 716 ------------------ builtin-programs/web/setup.folk | 8 +- 3 files changed, 32 insertions(+), 779 deletions(-) delete mode 100644 builtin-programs/calibrate/interactively-refine.folk diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 444d0c2f..94a2afc3 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -203,23 +203,39 @@ Wish the web server handles route "/calibrate" with hidden true handler {
  • Measure your calibration board.

    -

    On your calibration board, measure the inner side length (indicated on calibration PDF) of a tag in millimeters and enter it here: mm

    - -

    Try to be as accurate as possible, like to within half a millimeter or better -- the more accurate, the better your calibration will be.

    +

    On your calibration board, measure each indicator in millimeters and enter it here. + (Try to be as accurate as possible, like to within half a millimeter or better -- + the more accurate, the better your calibration will be.)

    +
      +
    • Tag inner side length: mm
    • +
    • Left margin: mm
    • +
    • Top margin: mm
    • +
    • Bottom margin: mm
    • +
  • Run the calibration process.

    -

    Start calibration:

    +

    Start calibration:

    How to manually override the geometry of a specific program diff --git a/builtin-programs/calibrate/interactively-refine.folk b/builtin-programs/calibrate/interactively-refine.folk deleted file mode 100644 index 1b73d35d..00000000 --- a/builtin-programs/calibrate/interactively-refine.folk +++ /dev/null @@ -1,716 +0,0 @@ -# interactively-refine.folk -- -# -# Implements table-oriented projector-camera end-to-end -# calibration step. -# - -When the pose library is /poseLib/ { - -set cc [C] -$cc extend $poseLib - -$cc cflags -I./vendor/apriltag -$cc endcflags ./vendor/apriltag/build/libapriltag.so - -$cc include -$cc include -$cc include - -# From https://courses.cs.duke.edu/cps274/fall13/notes/rodrigues.pdf: -fn rotationMatrixToRotationVector {R} { - set A [scale 0.5 [sub $R [transpose $R]]] - set rho [list [getelem $A 2 1] \ - [getelem $A 0 2] \ - [getelem $A 1 0]] - set s [norm $rho] - set c [expr {([getelem $R 0 0] + [getelem $R 1 1] + [getelem $R 2 2] - 1) / 2}] - - # If s = 0 and c = 1: - if {abs($s) < 0.0001 && abs($c - 1) < 0.0001} { - return {0 0 0} - } - # If s = 0 and c = -1: - if {abs($s) < 0.0001 && abs($c - (-1)) < 0.0001} { - # let v = a nonzero column of R + I - set v [getcol [add $R [mkIdentity 3]] 0] - set u [scale [/ 1.0 [norm $v]] $v] - set r [scale 3.14159 $u] - if {abs([norm $r] - 3.14159) < 0.0001 && - ((abs([getelem $r 0]) < 0.0001 && - abs([getelem $r 1]) < 0.0001 && - [getelem $r 2] < 0) || - (abs([getelem $r 0]) < 0.0001 && - [getelem $r 1] < 0) || - ([getelem $r 0] < 0))} { - return [scale -1 $r] - } else { - return $r - } - } - - set u [scale [/ 1.0 $s] $rho] - set theta $(atan2($s, $c)) - return [scale $theta $u] -} - -fn rotationVectorToRotationMatrix {r} { - set theta [norm $r] - if {abs($theta) < 0.0001} { - return [mkIdentity 3] - } - set u [scale [/ 1.0 $theta] $r] - set ux [list [list 0 [* -1.0 [getelem $u 2]] [getelem $u 1]] \ - [list [getelem $u 2] 0 [* -1.0 [getelem $u 0]]] \ - [list [* -1.0 [getelem $u 1]] [getelem $u 0] 0]] - return [add [scale $(cos($theta)) [mkIdentity 3]] \ - [add [scale [expr {1.0 - cos($theta)}] \ - [matmul $u [transpose $u]]] \ - [scale $(sin($theta)) $ux]]] -} - -# Used to generate the initial guess in estimateBoardPose. Kind of -# misuses the AprilTag pose estimation code to do an entire-board -# estimate (which includes multiple tags). -$cc proc baseEstimateBoardPose {Intrinsics cameraIntrinsics - double cameraWidth double cameraHeight - double[][2] modelTagCorners double[][2] detectedTagCorners - int cornersCount} TagPose { - // We'll fill this in with a .H that represents all the corners. - apriltag_detection_t det; - - // The normal tag .H homography goes from (+/-1, +/-1) to the - // camera-detected tag corners. We will instead create a - // board-wide homography from board meters position to the - // camera-detected tag corners. - float correspondences[cornersCount][4]; - for (int i = 0; i < cornersCount; i++) { - correspondences[i][0] = modelTagCorners[i][0]; - correspondences[i][1] = modelTagCorners[i][1]; - - double undistortedDetectedTagCorners[2]; - rescaleAndUndistort(cameraIntrinsics, cameraWidth, cameraHeight, - detectedTagCorners[i], - undistortedDetectedTagCorners); - correspondences[i][2] = undistortedDetectedTagCorners[0]; - correspondences[i][3] = undistortedDetectedTagCorners[1]; - } - zarray_t correspondencesArr = { - .el_sz = sizeof(float[4]), .size = cornersCount, .alloc = cornersCount, - .data = (char*) correspondences - }; - det.H = homography_compute(&correspondencesArr, - HOMOGRAPHY_COMPUTE_FLAG_SVD); - apriltag_detection_info_t info = { - .det = &det, - .tagsize = 2.0, // scale factor = 1.0 - .fx = cameraIntrinsics.fx, .fy = cameraIntrinsics.fy, - .cx = cameraIntrinsics.cx, .cy = cameraIntrinsics.cy - }; - apriltag_pose_t pose; - estimate_pose_for_tag_homography(&info, &pose); - - matd_destroy(det.H); - - TagPose ret; - memcpy(ret.R, pose.R->data, sizeof(ret.R)); - memcpy(ret.t, pose.t->data, sizeof(ret.t)); - - matd_destroy(pose.R); - matd_destroy(pose.t); - return ret; -} -$cc proc estimateBoardPose {Intrinsics cameraIntrinsics - double cameraWidth double cameraHeight - double[][2] modelTagCorners double[][2] detectedTagCorners - int cornersCount} TagPose { - TagPose baseBoardPose = - baseEstimateBoardPose(cameraIntrinsics, cameraWidth, cameraHeight, - modelTagCorners, detectedTagCorners, cornersCount); - - double wX[cornersCount][3]; - double x[cornersCount][2]; - for (int i = 0; i < cornersCount; i++) { - rescaleAndUndistort(cameraIntrinsics, cameraWidth, cameraHeight, - detectedTagCorners[i], - x[i]); - // Apply intrinsics to go from pixel coordinates to normalized - // image-plane coordinates: - x[i][0] = (x[i][0] - cameraIntrinsics.cx) / cameraIntrinsics.fx; - x[i][1] = (x[i][1] - cameraIntrinsics.cy) / cameraIntrinsics.fy; - - wX[i][0] = modelTagCorners[i][0]; - wX[i][1] = modelTagCorners[i][1]; - wX[i][2] = 0; - } - - matd_t* cRw = matd_create_data(3, 3, (double*) baseBoardPose.R); - matd_t* ctw = matd_create_data(3, 1, (double*) baseBoardPose.t); - - poseGaussNewton(wX, x, cornersCount, &cRw, &ctw, 200); - - TagPose ret; - memcpy(ret.R, cRw->data, sizeof(ret.R)); - memcpy(ret.t, ctw->data, sizeof(ret.t)); - - matd_destroy(cRw); - matd_destroy(ctw); - return ret; -} - -$cc cflags -I./vendor/cmpfit -$cc include "mpfit.h" -$cc include "mpfit.c" -$cc proc funct {int m int n double* x - double* fvec double** dvec - void* userdata} int { - Jim_Obj* jimFunct = (Jim_Obj*) userdata; - - // Build xList from x[0..n-1]. - Jim_Obj* xList = Jim_NewListObj(interp, NULL, 0); - for (int i = 0; i < n; i++) { - Jim_ListAppendElement(interp, xList, Jim_NewDoubleObj(interp, x[i])); - } - - // Expand jimFunct (already an arg list) and append xList, then eval. - int prefixLen = Jim_ListLength(interp, jimFunct); - Jim_Obj* objv[prefixLen + 1]; - for (int i = 0; i < prefixLen; i++) { - __ENSURE_OK(Jim_ListIndex(interp, jimFunct, i, &objv[i], JIM_NONE)); - } - objv[prefixLen] = xList; - __ENSURE_OK(Jim_EvalObjVector(interp, prefixLen + 1, objv)); - - // Unpack result list into fvec[0..m-1]. - Jim_Obj* result = Jim_GetResult(interp); - FOLK_ENSURE(Jim_ListLength(interp, result) == m); - for (int i = 0; i < m; i++) { - Jim_Obj* elem; - __ENSURE_OK(Jim_ListIndex(interp, result, i, &elem, JIM_NONE)); - __ENSURE_OK(Jim_GetDouble(interp, elem, &fvec[i])); - } - return 0; -} -$cc proc fit {int m int n double[] x Jim_Obj* jimFunct} Jim_Obj* { - mp_result result = {0}; - - mp_par pars[18]; // One for each parameter - memset(pars, 0, sizeof(pars)); - // Set larger relative step sizes - for (int i = 0; i < 18; i++) { - pars[i].relstep = 1e-3; // Default is ~1e-7, try 1e-3 to 1e-2 - } - - mpfit(funct, - m, // Number of residuals. - n, x, // Parameters to optimize. - pars, NULL, (void*) jimFunct, &result); - fprintf(stderr, "next niter=%d, nfev=%d, status=%d, pid=%d ;\n orignorm=%f, bestnorm=%f\n", result.niter, result.nfev, result.status, gettid(), - result.orignorm, result.bestnorm); - - Jim_Obj* xList = Jim_NewListObj(interp, NULL, 0); - for (int i = 0; i < n; i++) { - Jim_ListAppendElement(interp, xList, Jim_NewDoubleObj(interp, x[i])); - } - return xList; -} - -set boardLib [$cc compile] - -When the AprilTag detector maker is /makeAprilTagDetector/ &\ - the calibration model library is /modelLib/ &\ - the calibration matrix library is /matLib/ &\ - the printed calibration tag size is /printedSideLengthMm/ mm &\ - camera /camera/ has width /cameraWidth/ height /cameraHeight/ &\ - display /display/ has width /displayWidth/ height /displayHeight/ &\ - /someone/ wishes to interactively refine calibration from camera /camera/ to display /display/ { - - When /someone/ wishes to draw refining model /model/ onto detected tags /tags/ \ - using calibration /calibration/ &\ - the collected results for [list /someone/ wishes to draw refining label /label/] \ - are /labels/ { - package require linalg - namespace import ::math::linearalgebra::matmul \ - ::math::linearalgebra::add - - set cameraIntrinsics [dict get $calibration camera intrinsics] - set displayIntrinsics [dict get $calibration projector intrinsics] - - set modelPrintedTagCorners [list] - set detectedPrintedTagCorners [list] - dict for {id tag} $tags { - if {![$modelLib isPrintedTag $id]} { continue } - - lappend modelPrintedTagCorners {*}[dict get $model $id p] - lappend detectedPrintedTagCorners {*}[dict get $tag p] - } - if {[llength $detectedPrintedTagCorners] < 4} { return } - - # Do a single board-wide pose estimate. - set pose [$boardLib estimateBoardPose $cameraIntrinsics \ - $cameraWidth $cameraHeight \ - $modelPrintedTagCorners $detectedPrintedTagCorners \ - [llength $detectedPrintedTagCorners]] - set R_boardToCamera [dict get $pose R] - set t_boardToCamera [dict get $pose t] - - set R_cameraToDisplay [dict get $calibration R_cameraToProjector] - set t_cameraToDisplay [dict get $calibration t_cameraToProjector] - - # Compute model-to-display homography from pose via correspondences - # of the detected printed tag corners: - set correspondences [lmap mc $modelPrintedTagCorners { - set v [list {*}$mc 0] - set camPt [add [matmul $R_boardToCamera $v] $t_boardToCamera] - set dispPt [add [matmul $R_cameraToDisplay $camPt] $t_cameraToDisplay] - set dp [$poseLib project $displayIntrinsics \ - $displayWidth $displayHeight $dispPt] - list {*}$mc {*}$dp - }] - set H_modelToDisplay [$matLib estimateHomography $correspondences] - - Wish to draw calibration model $model \ - using model-to-display homography $H_modelToDisplay \ - with message [join [lmap r $labels {dict get $r label}] " "] - } - - fn AwaitNextCameraFrame! {frameStmtVar} { - upvar $frameStmtVar frameStmt - if {$frameStmt ne {}} { - StatementRelease! [dict get $frameStmt __ref] - set prevFrameTimestamp $frameStmt(frameTimestamp) - } else { - set prevFrameTimestamp 0 - } - while true { - after 8 - - set frames [Query! camera $camera has gray frame /frame/ at timestamp /frameTimestamp/] - if {[llength $frames] < 1} { continue } - set frameResult [lindex $frames end] - - if {$frameResult(frameTimestamp) <= $prevFrameTimestamp} { continue } - - try { - StatementAcquire! [dict get $frameResult __ref] - } on error e { continue } - - break - } - set frameStmt $frameResult - return $frameResult - } - - set intrNames {fx cx fy cy k1 k2} - # Calibration -> flat list of 18 parameters `x`. - fn unravel {calibration} { - list \ - {*}[lmap n $intrNames { dict get $calibration camera intrinsics $n }] \ - {*}[lmap n $intrNames { dict get $calibration projector intrinsics $n }] \ - {*}[rotationMatrixToRotationVector [dict get $calibration R_cameraToProjector]] \ - {*}[dict get $calibration t_cameraToProjector] - } - # Flat list of 18 parameters `x` -> update calibration. - fn ravelInto {calibrationVar x} { - upvar $calibrationVar calibration - foreach n $intrNames v [lrange $x 0 5] { - dict set calibration camera intrinsics $n $v - } - foreach n $intrNames v [lrange $x 6 11] { - dict set calibration projector intrinsics $n $v - } - dict set calibration R_cameraToProjector \ - [rotationVectorToRotationMatrix [lrange $x 12 14]] - dict set calibration t_cameraToProjector [lrange $x 15 17] - } - - fn makeAprilTagDetector - set tagDetector [makeAprilTagDetector "tagStandard52h13" 1.0 3] - - package require linalg - namespace import ::math::linearalgebra::scale \ - ::math::linearalgebra::sub ::math::linearalgebra::add \ - ::math::linearalgebra::transpose ::math::linearalgebra::getelem \ - ::math::linearalgebra::norm ::math::linearalgebra::getcol \ - ::math::linearalgebra::mkIdentity ::math::linearalgebra::matmul - - set printedSideLengthM [/ $printedSideLengthMm 1000.0] - set model0 [$modelLib scaleModel [$modelLib unitModel] \ - $printedSideLengthM] - - set calibration [dict get [QueryOne! a calibration from camera $camera to display $display is /calibration/] calibration] - set calibrationPoses [dict get [QueryOne! the calibration poses from camera $camera to display $display are /calibrationPoses/] calibrationPoses] - - # The actual refinement process. We will have the user hold up the - # board in a sequence of whatever poses they want. When the board - # is stable for a couple seconds, we say that's a pose, and we - # start refinement with respect to that pose. - # - # Once the calibration is refined with respect to the pose, the - # user can move the board to a different pose. They can stop - # whenever they want. - - set poses [list] - - set frameStmt {} - while true { - AwaitNextCameraFrame! frameStmt - set frame $frameStmt(frame) - set frameTimestamp $frameStmt(frameTimestamp) - - set detectedTags0 [dict create] - foreach tag [$tagDetector detect $frame] { - dict set detectedTags0 $tag(id) $tag - } - if {[dict size $detectedTags0] < 8} { - # puts stderr "Not enough tags: [dict size $detectedTags0]" - continue - } - - # Do our best (given current calibration) to draw projected - # tags on the board to match the printed tags. We wouldn't - # really need this, I guess, except this is also what'll - # display the label (set next). - Hold! -key refining-model -keep 32ms Wish to draw refining model $model0 \ - onto detected tags $detectedTags0 \ - using calibration $calibration - - # We don't want to start a new pose right on top of a pose we - # just recorded. The user should have to move the board a bit. - if {[llength $poses] > 0} { - # Are the seen $tags far enough from previous pose's - # $tags? - set lastPose [lindex $poses end] - if {[$modelLib meanTagsDifference $detectedTags0 $lastPose(detectedTags0)] < 50} { - # Not far enough from previous pose. - Hold! -key label Wish to draw refining label "Move board farther!" - continue - } - } - - # We want the user to keep the board still, so don't start - # refining over a pose until it's been stable for a few - # seconds. - if {![info exists prevDetectedTags0] || - [set diff [$modelLib meanTagsDifference $detectedTags0 $prevDetectedTags0]] > 5} { - # Not close enough to previous frame for us to trust - # that the board is held still. Reset. - set prevDetectedTags0 $detectedTags0 - set prevDetectedTags0Timestamp $frameTimestamp - - set diffLabel "" - if {[info exists diff]} { set diffLabel "(moved $diff pixels)" } - Hold! -key label Wish to draw refining label \ - "Keep still! $diffLabel" - continue - } elseif {$frameTimestamp - $prevDetectedTags0Timestamp < 4} { - set timeSincePrevDetectedTags0 $($frameTimestamp - $prevDetectedTags0Timestamp) - Hold! -key label Wish to draw refining label \ - "Keep still for $(round((4.0 - $timeSincePrevDetectedTags0))) sec" - continue - } - Hold! -key label Wish to draw refining label \ - "Keep still. Refining..." - - set idealCameraCorners [apply {{} { - upvar modelLib modelLib; upvar matLib matLib - upvar model0 model0 - upvar detectedTags0 detectedTags0 - - # Collect correspondences: list of {x0 y0 x1 y1}, pairs of - # detected model corner & camera corner for all printed - # tags seen this frame. - set printedCorrespondences [list] - dict for {id tag} $detectedTags0 { - if {![$modelLib isPrintedTag $id]} { continue } - foreach modelCorner [dict get $model0 $id p] \ - camCorner [dict get $tag p] { - lappend printedCorrespondences \ - [list {*}$modelCorner {*}$camCorner] - } - } - if {[llength $printedCorrespondences] < 4} { continue } - - # H_mc: homography from model xy to camera xy. - # Used to find ideal camera positions for projected tags. - set H_mc [$matLib estimateHomography $printedCorrespondences] - - # idealCameraCorners: tag id -> list of 4 {x y}. - set idealCameraCorners [dict create] - dict for {id modelTag} $model0 { - if {![$modelLib isProjectedTag $id]} { continue } - dict set idealCameraCorners $id \ - [lmap modelCorner $modelTag(p) { - $matLib applyHomography $H_mc $modelCorner - }] - } - return $idealCameraCorners - }}] - - set pose [dict create detectedTags0 $detectedTags0 \ - idealCameraCorners $idealCameraCorners] - - # 2 residuals (dx, dy) per projected tag corner. - set projectedTagsCount [$modelLib countProjectedTags $model0] - set oldPoseResidualCount 0 - foreach oldPose $poses { - set nPrinted 0 - dict for {id tag} [dict get $oldPose finalDetectedTags] { - if {[$modelLib isPrintedTag $id]} { incr nPrinted [llength [dict get $tag p]] } - } - if {$nPrinted < 4} { continue } - dict for {id tag} [dict get $oldPose finalDetectedTags] { - if {[$modelLib isProjectedTag $id]} { - incr oldPoseResidualCount [expr {[llength [dict get $tag p]] * 2}] - } - } - } - set m [expr {$projectedTagsCount * 4 * 2 + $oldPoseResidualCount}] - - # Each saved calibrationPose contributes 2 residuals per - # projected tag corner, comparing H_modelToDisplay * model_corner - # (the target projector pixel) against the same model corner - # reprojected through the current draft calibration. - foreach calPose $calibrationPoses { - dict for {id modelTag} [dict get $calPose model] { - if {![$modelLib isProjectedTag $id]} continue - incr m [expr {[llength [dict get $modelTag p]] * 2}] - } - } - - # Refine the parameters with respect to this pose. This will - # block for a while and run the interior function a lot (10 - # times? 100 times?). - set version 0 - set x0 [unravel $calibration] - # puts stderr "x0=($x0)" - # Track the last calibration that produced a render the - # camera could actually detect. If the optimizer steers the - # parameters somewhere that breaks the detect loop, we'll - # revert to this so the projector goes back to a visible - # state. - set lastGoodCalibration $calibration - set detectedTags {} - set x [$boardLib fit $m [llength $x0] $x0 [fn funct {x} { - # This block gets run on each iteration of the - # Levenberg-Marquardt optimization loop. - - # puts stderr "ITER ------" - # puts stderr "Test x=($x)" - - ravelInto calibration $x - incr version - set model [$modelLib updateModelVersion $model0 $version] - - # This will cause the new model version to be rendered by - # the other process. Note that this assumes that the user - # hasn't moved the calibration board from the pose $tags0. - Hold! -key refining-model Wish to draw refining model $model \ - onto detected tags $detectedTags0 \ - using calibration $calibration - - # Loop until we see the version we just wished to draw. - set expectedVersion [expr {$version % 4}] - set detectedVersion {} - set missedFrames 0 - while {$detectedVersion != $expectedVersion} { - incr missedFrames - if {$missedFrames > 120} { - # The current draft x has rendered tags so badly - # the camera can't find them. Revert the projector - # render to the last known good calibration and - # return penalty residuals to push mpfit away. - puts stderr "Failure: reverting to last known good calibration" - set calibration $lastGoodCalibration - incr version - set model [$modelLib updateModelVersion $model0 $version] - Hold! -key refining-model Wish to draw refining model $model \ - onto detected tags $detectedTags0 \ - using calibration $calibration - return [lrepeat $m 1e3] - } - - AwaitNextCameraFrame! frameStmt - set frame $frameStmt(frame) - set frameTimestamp $frameStmt(frameTimestamp) - - set detectedTags [$tagDetector detect $frame] - # Report the detections for on-page preview: - Hold! -key detected-tags \ - Claim $this detects calibration tags $detectedTags on camera $camera - - set detectedProjectedTagsCount [llength [$modelLib filterProjectedTagsInDetectedTags $detectedTags]] - if {$detectedProjectedTagsCount < 1} { - # puts stderr "Failure: Only seeing $detectedProjectedTagsCount of $projectedTagsCount projected tags" - continue - } else { - # puts stderr "Success: Seeing $detectedProjectedTagsCount of $projectedTagsCount projected tags" - } - set detectedVersion [$modelLib detectVersionFromDetectedTags $detectedTags] - } - # The render was visible to the camera, so this calibration - # is safe to revert to if a later iteration breaks things. - set lastGoodCalibration $calibration - - # Compute residuals: detected position - ideal position for - # each projected tag corner. - - # H_mp: model xy -> projector xy. - # Used to infill projected tags that we didn't detect this frame. - set projCorrespondences [list] - foreach tag $detectedTags { - if {![$modelLib isProjectedTag $tag(id)]} { continue } - foreach modelCorner [dict get $model $tag(id) p] \ - projCorner [dict get $tag p] { - lappend projCorrespondences \ - [list {*}$modelCorner {*}$projCorner] - } - } - set H_mp [$matLib estimateHomography $projCorrespondences] - - set detectedCornersById [dict create] - foreach tag $detectedTags { - dict set detectedById $tag(id) $tag(p) - } - - set residuals [list] - dict for {id modelTag} $model { - if {![$modelLib isProjectedTag $id]} { continue } - set nCorners [llength [dict get $modelTag p]] - for {set i 0} {$i < $nCorners} {incr i} { - set idealIdx [expr {[$modelLib isVersionTag $id] ? ($i + $version) % 4 : $i}] - set ideal [lindex [dict get $idealCameraCorners $id] $idealIdx] - if {[dict exists $detectedCornersById $id]} { - set det [lindex $detectedCornersById($id) $i] - } else { - set det [$matLib applyHomography $H_mp \ - [lindex [dict get $modelTag p] $i]] - } - lappend residuals \ - [- [lindex $det 0] [lindex $ideal 0]] \ - [- [lindex $det 1] [lindex $ideal 1]] - } - } - # puts stderr "Residuals: ($residuals)" - - # Old-pose residuals: for each prior pose, estimate the board - # position using the current draft calibration, compute where - # projected tags would land in display space (H_mD) and use - # the stored printed-tag camera detections to map back to - # camera space (H_dC), then compare to stored projected detections. - set curCamIntr [dict get $calibration camera intrinsics] - set curDispIntr [dict get $calibration projector intrinsics] - set curR_cDP [dict get $calibration R_cameraToProjector] - set curT_cDP [dict get $calibration t_cameraToProjector] - - foreach oldPose $poses { - set oldDet [dict get $oldPose finalDetectedTags] - - set oldModelPrCorners [list] - set oldDetPrCorners [list] - dict for {id tag} $oldDet { - if {![$modelLib isPrintedTag $id]} { continue } - lappend oldModelPrCorners {*}[dict get $model0 $id p] - lappend oldDetPrCorners {*}[dict get $tag p] - } - if {[llength $oldModelPrCorners] < 4} { continue } - - set oldBP [$boardLib estimateBoardPose $curCamIntr \ - $cameraWidth $cameraHeight \ - $oldModelPrCorners $oldDetPrCorners \ - [llength $oldModelPrCorners]] - set R_old [dict get $oldBP R] - set t_old [dict get $oldBP t] - - # H_mD: model -> display (from current calibration + pose) - # H_dC: display -> stored camera detections (from printed tags) - set oldMDCorrs [list] - set oldDCCorrs [list] - foreach mc $oldModelPrCorners detC $oldDetPrCorners { - set camPt [add [matmul $R_old [list {*}$mc 0.0]] $t_old] - set dispPt [add [matmul $curR_cDP $camPt] $curT_cDP] - set dp [$poseLib project $curDispIntr $displayWidth $displayHeight $dispPt] - lappend oldMDCorrs [list {*}$mc {*}$dp] - lappend oldDCCorrs [list {*}$dp {*}$detC] - } - set H_mD [$matLib estimateHomography $oldMDCorrs] - set H_dC [$matLib estimateHomography $oldDCCorrs] - - dict for {id tag} $oldDet { - if {![$modelLib isProjectedTag $id]} { continue } - foreach detCorner [dict get $tag p] \ - mc [dict get $model0 $id p] { - set dispPos [$matLib applyHomography $H_mD $mc] - set ideal [$matLib applyHomography $H_dC $dispPos] - lappend residuals \ - [- [lindex $detCorner 0] [lindex $ideal 0]] \ - [- [lindex $detCorner 1] [lindex $ideal 1]] - } - } - } - - # CalibrationPose residuals: for each saved calibration pose, - # pose-estimate the board using current draft camera intrinsics - # and the saved tag detections, then compare where the current - # draft calibration would project each projected tag corner in - # display space against where H_modelToDisplay says it should be. - foreach calPose $calibrationPoses { - set calModel [dict get $calPose model] - set calTags [dict get $calPose tags] - set calH_mD [dict get $calPose H_modelToDisplay] - set calCamWidth [dict get $calPose cameraWidth] - set calCamHeight [dict get $calPose cameraHeight] - - set calModelPrCorners [list] - set calDetPrCorners [list] - dict for {id tag} $calTags { - if {![$modelLib isPrintedTag $id]} { continue } - if {![dict exists $calModel $id]} { continue } - lappend calModelPrCorners {*}[dict get $calModel $id p] - lappend calDetPrCorners {*}[dict get $tag p] - } - if {[llength $calModelPrCorners] < 4} { continue } - - set calBP [$boardLib estimateBoardPose $curCamIntr \ - $calCamWidth $calCamHeight \ - $calModelPrCorners $calDetPrCorners \ - [llength $calModelPrCorners]] - set R_cal [dict get $calBP R] - set t_cal [dict get $calBP t] - - dict for {id modelTag} $calModel { - if {![$modelLib isProjectedTag $id]} { continue } - foreach mc [dict get $modelTag p] { - set target [$matLib applyHomography $calH_mD $mc] - set camPt [add [matmul $R_cal [list {*}$mc 0.0]] $t_cal] - set dispPt [add [matmul $curR_cDP $camPt] $curT_cDP] - set current [$poseLib project $curDispIntr \ - $displayWidth $displayHeight $dispPt] - lappend residuals \ - [- [lindex $target 0] [lindex $current 0]] \ - [- [lindex $target 1] [lindex $current 1]] - } - } - } - - return $residuals - }]] - # puts stderr "Old x: ($x0)" - # puts stderr "New x: ($x)" - ravelInto calibration $x - - Hold! -save -on calibration -key calibration \ - Claim a calibration from camera $camera to display $display is $calibration - - dict set pose finalDetectedTags $detectedTags - lappend poses $pose - - puts stderr "Saved calibration. Now have [llength $poses] poses." - unset prevDetectedTags0 prevDetectedTags0Timestamp - } - On unmatch { - Hold! -key label {} - Hold! -key refining-model {} - } -} - -} diff --git a/builtin-programs/web/setup.folk b/builtin-programs/web/setup.folk index ac75208d..2c2911c0 100644 --- a/builtin-programs/web/setup.folk +++ b/builtin-programs/web/setup.folk @@ -515,7 +515,7 @@ folk.watchCollected(`/someone/ wishes the web server handles route "/setup" with

    Projector-camera calibration

    -

    Select one display and one or more cameras to calibrate:

    +

    Select one display and one or more cameras to calibrate:

    Display (projector) @@ -582,6 +582,12 @@ folk.watchCollected(`/someone/ wishes the web server handles route "/setup" with window.location.href = `/calibrate?\${params.toString()}`; } + + const firstDisplay = document.querySelector('input\[name="calibration-display"]'); + if (firstDisplay) firstDisplay.checked = true; + const firstCamera = document.querySelector('input\[name="calibration-camera"]'); + if (firstCamera) firstCamera.checked = true; + calibrationSelectionChange();
    From b663b24acdc5aae785876167b7363b9788216e64 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 15:17:18 -0400 Subject: [PATCH 31/45] Makefile: Disable debuginfod because it was very annoying --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7211b66b..17b5f9dd 100644 --- a/Makefile +++ b/Makefile @@ -86,7 +86,7 @@ debug: folk if [ "$$(uname)" = "Darwin" ]; then \ lldb -o "process handle -p true -s false SIGUSR1" -- ./folk; \ else \ - gdb -ex "handle SIGUSR1 nostop" -ex "handle SIGPIPE nostop" ./folk; \ + DEBUGINFOD_URLS="" gdb -ex "handle SIGUSR1 nostop" -ex "handle SIGPIPE nostop" ./folk; \ fi clean: From 71e95a773c868509c18dff40df2693f0169b3095 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 15:18:05 -0400 Subject: [PATCH 32/45] print: WIP: Make print pdf event for calibration board --- builtin-programs/print/print.folk | 42 ++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index e42e589f..a97b08e6 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -216,6 +216,27 @@ Subscribe: print program /id/ with /...options/ { return } + set ps [codeToPostScript $id $code $options] + + # save code and ps to disk + if {[file exists "$saveDir/$id.folk"]} { + error "Program $id already exists on disk. Aborting print." + } + set fp [open "$saveDir/$id.folk" w]; puts $fp $code; close $fp + + set fp [open "$saveDir/$id.ps" w]; puts $fp $ps; close $fp + exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ + $saveDir/$id.ps $saveDir/$id.pdf + + puts "Printing program $id on $::thisNode" + Notify: print pdf $saveDir/$id.pdf with {*}$options +} + +} + +Subscribe: print pdf /pdfPath/ with /...options/ { + if {![info exists options]} { set options {} } + if {![dict exists $options printer]} { set defaultPrinterMatches [Query! printer /printer/ is the default printer] if {[llength $defaultPrinterMatches] > 0} { @@ -250,24 +271,5 @@ Subscribe: print program /id/ with /...options/ { set args [list -P [dict get $options printer]] } - set ps [codeToPostScript $id $code $options] - - # save code and ps to disk - if {[file exists "$saveDir/$id.folk"]} { - error "Program $id already exists on disk. Aborting print." - } - set fp [open "$saveDir/$id.folk" w]; puts $fp $code; close $fp - - set fp [open "$saveDir/$id.ps" w]; puts $fp $ps; close $fp - exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ - $saveDir/$id.ps $saveDir/$id.pdf - - puts "Printing program $id on $::thisNode" - Notify: print pdf $saveDir/$id.pdf with {*}$args -} - -} - -Subscribe: print pdf /pdfPath/ with /...arguments/ { - exec lpr {*}$arguments $saveDir/$id.pdf + exec lpr {*}$args $pdfPath } From f3fec9a34b7a3c3a234989122e984690a07f3bf6 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 15:18:16 -0400 Subject: [PATCH 33/45] print-editor: Cleanup / syntax fix --- builtin-programs/editor/print-editor.folk | 7 ------- 1 file changed, 7 deletions(-) diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 5f21ef1e..749c94ce 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -51,11 +51,6 @@ When the codeToPostScript is /codeToPostScript/ &\ /someone/ wishes editor /editor/ has a print preview &\ editor /editor/ has selected program /program/ { - # I want these lines to be 4-indented, not 8-indented, - # based on the When being 0-indented that corresponds to the - # most recent opening brace. - puts "I want this line to be 4-indented, but it's 8-indented" - set preview [list $editor preview] Wish $preview has a canvas set fmt $formats(letter) @@ -84,5 +79,3 @@ When the codeToPostScript is /codeToPostScript/ &\ Wish $preview displays image $pngFile with width $previewGeom(width) } } - -} From a774cf24e96ff11c4b19de9c87dd06dba39f417e Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 15:47:01 -0400 Subject: [PATCH 34/45] WIP: Print new calibration board; new statements for calib --- .../calibrate/calibrate-page.folk | 20 +++++++++---------- builtin-programs/calibrate/calibrate.folk | 4 ++-- .../calibrate/calibration-board-pdf.folk | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 94a2afc3..3bda1ce6 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -183,17 +183,17 @@ Wish the web server handles route "/calibrate" with hidden true handler {

    We're going to print this calibration board and glue/tape it to something solid and flat (hardcover book, solid cardboard, etc):

    - +

    Make sure your printer is set up for Folk to print.

    -

    Print the calibration board through Folk: (print through Folk so that we can calibrate the way you will actually print)

    +

    Print the calibration board through Folk: (print through Folk so that we can calibrate the way you will actually print)

    @@ -228,17 +228,17 @@ Wish the web server handles route "/calibrate" with hidden true handler { const measurements = { tagSideLength: boardTagSideLengthMm.value + 'mm', left: boardLeftMm.value + 'mm', - right: boardLeftMm.value + 'mm', top: boardTopMm.value + 'mm', bottom: boardBottomMm.value + 'mm' }; folk.run(tcl` -Hold! -save -on calibration -key default-program-geometry Claim the default program geometry is \${geom} + Hold! -save -on calibration -key calibration-measurements \ + Claim the calibration measurements are \${measurements} `); folk.hold('start calibration', tcl` - Claim the printed calibration tag size is \${mm} mm - Wish to calibrate camera "$camera" to display "$display" + Wish to calibrate camera "$camera" to display "$display" \ + using tag size \${boardTagSideLengthMm.value} mm `, 'builtin-programs/calibrate/calibrate.folk'); }); diff --git a/builtin-programs/calibrate/calibrate.folk b/builtin-programs/calibrate/calibrate.folk index 1427a052..52d233b8 100644 --- a/builtin-programs/calibrate/calibrate.folk +++ b/builtin-programs/calibrate/calibrate.folk @@ -29,8 +29,8 @@ When camera /camera/ has width /cameraWidth/ height /cameraHeight/ &\ the jpeg library is /jpegLib/ &\ the calibration model library is /modelLib/ &\ the calibration matrix library is /matLib/ &\ - the printed calibration tag size is /printedSideLengthMm/ mm &\ - /someone/ wishes to calibrate camera /camera/ to display /display/ { + /someone/ wishes to calibrate camera /camera/ to display /display/ \ + using tag size /printedSideLengthMm/ mm { fn makeAprilTagDetector set calibrationTagDetector [makeAprilTagDetector "tagStandard52h13" 2.0 3] diff --git a/builtin-programs/calibrate/calibration-board-pdf.folk b/builtin-programs/calibrate/calibration-board-pdf.folk index 187268e9..5f67fa6a 100644 --- a/builtin-programs/calibrate/calibration-board-pdf.folk +++ b/builtin-programs/calibrate/calibration-board-pdf.folk @@ -197,7 +197,7 @@ fn makeCalibrationBoardPng {} { } Claim the makeCalibrationBoardPng is [fn makeCalibrationBoardPng] -Wish the web server handles route {/calibrate/board.pdf} with handler { +Wish the web server handles route {/calibrate/board.pdf} with hidden true handler { dict create statusAndHeaders "HTTP/1.1 200 OK Connection: close Content-Type: application/pdf @@ -206,7 +206,7 @@ Content-Type: application/pdf body [makeCalibrationBoardPdf] } -Wish the web server handles route {/calibrate/board.png} with handler { +Wish the web server handles route {/calibrate/board.png} with hidden true handler { dict create statusAndHeaders "HTTP/1.1 200 OK Connection: close Content-Type: image/png From add13f87eaecc8216dcf513357798eaef9e6fde4 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 15:59:38 -0400 Subject: [PATCH 35/45] print: Force print-scaling=none --- builtin-programs/print/print.folk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index a97b08e6..1d02d8db 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -271,5 +271,5 @@ Subscribe: print pdf /pdfPath/ with /...options/ { set args [list -P [dict get $options printer]] } - exec lpr {*}$args $pdfPath + exec lpr -o print-scaling=none {*}$args $pdfPath } From 675cc6ee5410e99f06d01ff5b6f73aeffe53acf5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 16:23:15 -0400 Subject: [PATCH 36/45] WIP: Calibrate runs through (but doesn't converge yet) --- builtin-programs/apriltags.folk | 6 ++---- builtin-programs/calibrate/calibrate-page.folk | 6 ++---- builtin-programs/calibrate/calibration-board-pdf.folk | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/builtin-programs/apriltags.folk b/builtin-programs/apriltags.folk index 606fa28f..0405a058 100644 --- a/builtin-programs/apriltags.folk +++ b/builtin-programs/apriltags.folk @@ -120,8 +120,7 @@ set entireFrameDetector [makeAprilTagDetector $tagFamily 2.0 1] set incrementalDetector [makeAprilTagDetector $tagFamily 1.5 1] # Entire-frame tag detector: -When /nobody/ wishes to calibrate camera /any/ to display /any/ &\ - /nobody/ wishes to interactively refine calibration from camera /any/ to display /any/ &\ +When /nobody/ wishes to calibrate camera /any/ to display /any/ /...etc/ &\ -serially camera /camera/ has gray frame /frame/ at timestamp /frameTs/ { tracy zoneBegin @@ -141,8 +140,7 @@ When /nobody/ wishes to calibrate camera /any/ to display /any/ &\ # Incremental tag detector (looks at regions where there were tags # seen recently): -When /nobody/ wishes to calibrate camera /any/ to display /any/ &\ - /nobody/ wishes to interactively refine calibration from camera /any/ to display /any/ &\ +When /nobody/ wishes to calibrate camera /any/ to display /any/ /...etc/ &\ the image library is /imageLib/ &\ -serially camera /camera/ has gray frame /frame/ at timestamp /frameTs/ { tracy zoneBegin diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 3bda1ce6..ce367f3d 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -233,12 +233,10 @@ Wish the web server handles route "/calibrate" with hidden true handler { }; folk.run(tcl` - Hold! -save -on calibration -key calibration-measurements \ - Claim the calibration measurements are \${measurements} + Hold! -save -on calibration -key calibration-measurements Claim the calibration measurements are \${measurements} `); folk.hold('start calibration', tcl` - Wish to calibrate camera "$camera" to display "$display" \ - using tag size \${boardTagSideLengthMm.value} mm + Wish to calibrate camera "$camera" to display "$display" using tag size \${boardTagSideLengthMm.value} mm `, 'builtin-programs/calibrate/calibrate.folk'); }); diff --git a/builtin-programs/calibrate/calibration-board-pdf.folk b/builtin-programs/calibrate/calibration-board-pdf.folk index 5f67fa6a..3abe1809 100644 --- a/builtin-programs/calibrate/calibration-board-pdf.folk +++ b/builtin-programs/calibrate/calibration-board-pdf.folk @@ -190,7 +190,6 @@ fn makeCalibrationBoardPng {} { exec gs -dNOPAUSE -dBATCH -sFONTPATH=vendor/fonts \ -sDEVICE=png16m -r144 \ -sOutputFile=$pngFile $psFile - puts stderr $pngFile set fp [open $pngFile rb] set png [read $fp]; close $fp return $png From 7943e513c06bc1785d4898d7abb8361e6d46184a Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 16:42:10 -0400 Subject: [PATCH 37/45] WIP: Calibration completes (fixed reliance on old fn-proc behavior) Problem: the projector calibration is off, so the whole thing is fairly inaccurate. Probably something we changed has thrown it off? Some assumption no longer correct. --- builtin-programs/calibrate/calibrate.folk | 17 +++++++++-------- builtin-programs/calibrate/refine.folk | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/builtin-programs/calibrate/calibrate.folk b/builtin-programs/calibrate/calibrate.folk index 52d233b8..b8293ddc 100644 --- a/builtin-programs/calibrate/calibrate.folk +++ b/builtin-programs/calibrate/calibrate.folk @@ -538,7 +538,7 @@ fn setCameraToProjectorExtrinsics {modelLib calibrationVar calibrationPoses} { # to find the rotation and translation from 3D camera-space to 3D # projector-space. - upvar $calibrationVar calibration + upvar $calibrationVar cal # Let's take all the points for which we have a corresponding # camera frame point and projector frame point. @@ -547,12 +547,12 @@ fn setCameraToProjectorExtrinsics {modelLib calibrationVar calibrationPoses} { for {set i 0} {$i < [llength $calibrationPoses]} {incr i} { set calibrationPose [lindex $calibrationPoses $i] - - set Rc [dict get [lindex [dict get $calibration camera extrinsics] $i] R] - set tc [dict get [lindex [dict get $calibration camera extrinsics] $i] t] - set Rp [dict get [lindex [dict get $calibration projector extrinsics] $i] R] - set tp [dict get [lindex [dict get $calibration projector extrinsics] $i] t] + set Rc [dict get [lindex [dict get $cal camera extrinsics] $i] R] + set tc [dict get [lindex [dict get $cal camera extrinsics] $i] t] + + set Rp [dict get [lindex [dict get $cal projector extrinsics] $i] R] + set tp [dict get [lindex [dict get $cal projector extrinsics] $i] t] # TODO: Try using pose estimation instead? dict for {id tag} [dict get $calibrationPose model] { @@ -594,8 +594,8 @@ fn setCameraToProjectorExtrinsics {modelLib calibrationVar calibrationPoses} { set t [sub $projectorFramePointsCentroid \ [matmul $R $cameraFramePointsCentroid]] - dict set calibration R_cameraToProjector $R - dict set calibration t_cameraToProjector $t + dict set cal R_cameraToProjector $R + dict set cal t_cameraToProjector $t } # End-to-end calibrates a camera-projector pair. calibrationPoses is @@ -701,6 +701,7 @@ When the calibration model library is /modelLib/ &\ set calibration [{*}$refineCalibration \ $modelLib $matLib \ + [fn setCameraToProjectorExtrinsics] \ $calibrationPoses $calibration] puts "======== Refined calibration intrinsics =========" diff --git a/builtin-programs/calibrate/refine.folk b/builtin-programs/calibrate/refine.folk index 46d91d8e..8b9442b6 100644 --- a/builtin-programs/calibrate/refine.folk +++ b/builtin-programs/calibrate/refine.folk @@ -561,8 +561,9 @@ fn refineMonoCalibration {calibration} { return $calibration } -fn refineCalibration {modelLib matLib +fn refineCalibration {modelLib matLib setCameraToProjectorExtrinsics calibrationPoses calibration} { + fn setCameraToProjectorExtrinsics # We start by individually refining the mono calibration of # the camera and the mono calibration of the projector. @@ -641,8 +642,7 @@ fn refineCalibration {modelLib matLib }] # Reconstruct camera->projector extrinsics after refinement. - setCameraToProjectorExtrinsics $modelLib \ - calibration $calibrationPoses + setCameraToProjectorExtrinsics $modelLib calibration $calibrationPoses # Now we do stereo refinement of the reprojection error # of the entire system, including all intrinsics and From b1685e51769177f98463cd1e8f89711f3f9dda18 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Thu, 21 May 2026 17:14:28 -0400 Subject: [PATCH 38/45] calibrate-page: Move sliders up --- .../calibrate/calibrate-page.folk | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index ce367f3d..5de3c3b9 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -241,15 +241,6 @@ Wish the web server handles route "/calibrate" with hidden true handler { }); -

    Once you start calibration, you'll see some AprilTags get automatically projected on your table. Move your board to the projected tags so that at least one projected tag sits inside the gap between printed AprilTags, wait a second for the projected tags to refit into the grid, - then hold the board still for a few seconds until - the pose is recorded.

    - -

    You should be lifting your board above the table plane and tilting it in the air. Don't just keep it flat on the table!

    - - - -

    Example video of Andrés calibrating the folk0 system (2x speed)

    Are the projected tags too big to fit in the gaps between printed tags? Adjust this slider to reset & adjust the default projected tag size: @@ -276,6 +267,16 @@ Claim the calibration poses max is \${calibrationPosesMax} }); +

    Once you start calibration, you'll see some AprilTags get automatically projected on your table. Move your board to the projected tags so that at least one projected tag sits inside the gap between printed AprilTags, wait a second for the projected tags to refit into the grid, + then hold the board still for a few seconds until + the pose is recorded.

    + +

    You should be lifting your board above the table plane and tilting it in the air. Don't just keep it flat on the table!

    + + + +

    Example video of Andrés calibrating the folk0 system (2x speed)

    +

    Once you've recorded the first pose, slowly drag the board around your space, going slow enough for the projected AprilTags to catch up with the printed AprilTags and fit into the gaps on your board. When you've moved the board at least a full board-length away from the first pose, try to slant it 45 degrees or so off the table and hold it still again to capture another pose.

    Repeat this process of dragging the board around and @@ -357,18 +358,6 @@ Camera Controls -

      -
    • Tag inner side length (try to be accurate to half a millimeter or better): - mm
    • - -
    • Left: mm
    • -
    • Right: mm
    • -
    • Top: mm
    • -
    • Bottom: mm
    • -
    - -

    -
    How to manually override the geometry of a specific program From 10ffa8bef990a0fcfdb6229a102723973ef1d2f2 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 22 May 2026 13:33:48 -0400 Subject: [PATCH 39/45] WIP: Move to calibration-board.folk, stub for measurement use --- ...-board-pdf.folk => calibration-board.folk} | 60 ++++++++++++------- 1 file changed, 38 insertions(+), 22 deletions(-) rename builtin-programs/calibrate/{calibration-board-pdf.folk => calibration-board.folk} (79%) diff --git a/builtin-programs/calibrate/calibration-board-pdf.folk b/builtin-programs/calibrate/calibration-board.folk similarity index 79% rename from builtin-programs/calibrate/calibration-board-pdf.folk rename to builtin-programs/calibrate/calibration-board.folk index 3abe1809..ba79fa49 100644 --- a/builtin-programs/calibrate/calibration-board-pdf.folk +++ b/builtin-programs/calibrate/calibration-board.folk @@ -1,42 +1,58 @@ -When the print library is /printLib/ &\ - the calibration model library is /modelLib/ &\ - the calibration matrix library is /matLib/ { - -# Goal of the calibration board is to have the user do three +# Goal of the calibration board is to have the user do four # measurements: # -# - paper edge to top margin, to account for vertical margin. +# - paper edge to top margin # -# - paper edge to right margin, to account for horizontal margin. +# - paper edge to bottom margin # -# - tag inner width, to account for scaling. We want this tag inner -# width to be exactly the same as the tag inner width that we use on -# every printed program, so the calibration isn't too -# measurement dependent. +# - paper edge to left margin +# +# - tag inner width, to account for scaling. (We also want this tag +# inner width to be exactly the same as the tag inner width that we +# use on every printed program, so that if the user measures the tag +# wrong, it still looks OK on the average program.) # # Then we can correct for these factors in all future prints, so we # can print mm-accurate. +# These values are all in points (1/72 of an inch). +set marginTop 48; set marginLeft 48 + +set measureTop [/ $marginTop 2]; set measureLeft [/ $marginLeft 2] +set tagInnerSideLength 70 + +When the calibration measurements are /measurements/ { + # FIXME: use the measurements and our knowledge of + # measureTop/measureLeft/tagInnerSideLength when we did an + # unmediated calibration board print -> generate some PostScript + # that mediates (scales, transforms) future printouts so that the + # coordinate spaces starts at (0, 0) bottom-left of physical page, + # and is to-scale with 1 = 1 physical point. + + # FIXME: Claim that PostScript string out so it can be used by the + # print system later. +} + +When the print library is /printLib/ &\ + the calibration model library is /modelLib/ &\ + the calibration matrix library is /matLib/ { + fn makeCalibrationBoardPs {} { set model [$modelLib unitModel] - set tagSideLengthPs 70 package require linalg namespace import ::math::linearalgebra::add - set marginTop 48; set marginLeft 48 - set measureTop [/ $marginTop 2]; set measureLeft [/ $marginLeft 2] - set PageWidth 612; set PageHeight 792 set innerToOuter 0.333333 - set tagOuterLengthPs [expr {$tagSideLengthPs * 10/6}] + set tagOuterLengthPs [expr {$tagInnerSideLength * 10/6}] set H_modelToPs [$matLib estimateHomography [subst { - {1 1 $tagSideLengthPs $tagSideLengthPs} - {1 0 $tagSideLengthPs 0} - {0 1 0 $tagSideLengthPs} + {1 1 $tagInnerSideLength $tagInnerSideLength} + {1 0 $tagInnerSideLength 0} + {0 1 0 $tagInnerSideLength} {0 0 0 0} }]] @@ -153,13 +169,13 @@ fn makeCalibrationBoardPs {} { % Label the inner side length: [if {$tagIdx == 1} { subst { gsave - [expr {$psX + ($tagOuterLengthPs - $tagSideLengthPs)/2}] + [expr {$psX + ($tagOuterLengthPs - $tagInnerSideLength)/2}] [expr {$psY - 15}] translate 1 -1 scale 0.1 0.67 0.1 setrgbcolor 2 setlinewidth - newpath 0 0 moveto $tagSideLengthPs 0 lineto stroke + newpath 0 0 moveto $tagInnerSideLength 0 lineto stroke newpath 0 0 moveto 0 -5 lineto stroke - newpath $tagSideLengthPs 0 moveto $tagSideLengthPs -5 lineto stroke + newpath $tagInnerSideLength 0 moveto $tagInnerSideLength -5 lineto stroke /Helvetica findfont 7 scalefont setfont newpath 0 5 moveto (inner side length) show grestore From 0a157e1e1de12a2656cf98905147d36d05db3ed5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 22 May 2026 14:08:17 -0400 Subject: [PATCH 40/45] WIP: Use measurements at print time; print tag to match cal board --- .../calibrate/calibrate-page.folk | 2 +- .../calibrate/calibration-board.folk | 28 +++++++++++++------ builtin-programs/editor/print-editor.folk | 6 ++-- builtin-programs/print/print.folk | 17 ++++++++++- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 5de3c3b9..95317081 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -7,7 +7,7 @@ fn makeExampleProgramPng {} { # purposes, and we want to cut the bottom half off so we need # known dimensions. set opts [dict create \ - tagSize {150 150} \ + tagInnerSideLength 70 \ pageSize {612 792} \ lineHeight 16 \ advance [* 0.5859375 16] \ diff --git a/builtin-programs/calibrate/calibration-board.folk b/builtin-programs/calibrate/calibration-board.folk index ba79fa49..5294480f 100644 --- a/builtin-programs/calibrate/calibration-board.folk +++ b/builtin-programs/calibrate/calibration-board.folk @@ -22,15 +22,25 @@ set measureTop [/ $marginTop 2]; set measureLeft [/ $marginLeft 2] set tagInnerSideLength 70 When the calibration measurements are /measurements/ { - # FIXME: use the measurements and our knowledge of - # measureTop/measureLeft/tagInnerSideLength when we did an - # unmediated calibration board print -> generate some PostScript - # that mediates (scales, transforms) future printouts so that the - # coordinate spaces starts at (0, 0) bottom-left of physical page, - # and is to-scale with 1 = 1 physical point. - - # FIXME: Claim that PostScript string out so it can be used by the - # print system later. + set m_tag [expr {double([string trimright [dict get $measurements tagSideLength] mm])}] + set m_left [expr {double([string trimright [dict get $measurements left] mm])}] + set m_bottom [expr {double([string trimright [dict get $measurements bottom] mm])}] + + # Derive a PostScript CTM that maps calibrated space (origin at + # paper bottom-left, 1 unit = 1 physical point = 25.4/72 mm) to + # the printer's raw PS coordinate space. + # + # The calibration board was printed unmediated, so its PS coords + # are the printer's raw coords. The measurement lines were drawn + # at PS positions measureLeft and measureTop; the tag inner side + # was tagInnerSideLength PS points. From the physical measurements + # (in mm) we can recover the printer's scale and origin offset. + set scale [expr {25.4 * $tagInnerSideLength / (72.0 * $m_tag)}] + set tx [expr {$measureLeft - $m_left * $tagInnerSideLength / $m_tag}] + set ty [expr {$measureTop - $m_bottom * $tagInnerSideLength / $m_tag}] + + Claim the calibrated print preamble is "\[$scale 0 0 $scale $tx $ty\] concat" + Claim the calibrated print scale is $scale } When the print library is /printLib/ &\ diff --git a/builtin-programs/editor/print-editor.folk b/builtin-programs/editor/print-editor.folk index 749c94ce..65e4f725 100644 --- a/builtin-programs/editor/print-editor.folk +++ b/builtin-programs/editor/print-editor.folk @@ -1,14 +1,14 @@ set formats [subst { letter { - tagSize {150 150} + tagInnerSideLength 70 pageSize {612 792} } a4 { - tagSize {150 150} + tagInnerSideLength 70 pageSize {595 842} } indexcard { - tagSize {300 300} + tagInnerSideLength 70 pageSize {612 792} } }] diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index 1d02d8db..7be791bc 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -86,7 +86,9 @@ Claim the print library is $printLib fn codeToPostScript {id code opts {mixins {}}} { # All opts should be passed in as points (1/2834.65 of a meter). lassign $opts(pageSize) PageWidth PageHeight - lassign $opts(tagSize) tagWidth tagHeight + set tagInnerSideLength $opts(tagInnerSideLength) + set tagWidth [expr {$tagInnerSideLength * 10.0 / 6}] + set tagHeight $tagWidth lassign $opts(margin) marginTop marginRight marginBottom marginLeft set lineHeight $opts(lineHeight) set maxLines $(int(($PageHeight - $marginTop - $marginBottom) / $lineHeight)) @@ -109,6 +111,8 @@ fn codeToPostScript {id code opts {mixins {}}} { %!PS << /PageSize \[$PageWidth $PageHeight\] >> setpagedevice + [dict getdef $opts calibrationPreamble {}] + /settextcolor {0 setgray} def /NeomatrixCode findfont @@ -216,6 +220,17 @@ Subscribe: print program /id/ with /...options/ { return } + set calibPreambleResults [Query! the calibrated print preamble is /preamble/] + if {[llength $calibPreambleResults] > 0} { + dict set options calibrationPreamble [dict get [lindex $calibPreambleResults 0] preamble] + } + + set calibScaleResults [Query! the calibrated print scale is /scale/] + if {[llength $calibScaleResults] > 0} { + set calibScale [dict get [lindex $calibScaleResults 0] scale] + dict set options tagInnerSideLength [expr {70.0 / $calibScale}] + } + set ps [codeToPostScript $id $code $options] # save code and ps to disk From daa6fc8b78ccd4f185ae50271061c983254d2e33 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Fri, 22 May 2026 20:45:13 -0400 Subject: [PATCH 41/45] jim: Track column numbers (not that well-tested) Need this to localize fields in printed code. --- vendor/jimtcl/jim-json.c | 4 +- vendor/jimtcl/jim.c | 129 +++++++++++++++++++++++++++++---------- vendor/jimtcl/jim.h | 6 +- 3 files changed, 104 insertions(+), 35 deletions(-) diff --git a/vendor/jimtcl/jim-json.c b/vendor/jimtcl/jim-json.c index 6d4cdfa7..2964d7a0 100644 --- a/vendor/jimtcl/jim-json.c +++ b/vendor/jimtcl/jim-json.c @@ -240,7 +240,7 @@ json_decode_dump_value(Jim_Interp *interp, struct json_state *state, Jim_Obj *li } if (set_source) { /* Note we need to subtract 1 because both are 1-based values */ - Jim_SetSourceInfo(interp, elem, state->fileNameObj, state->line + t->line - 1); + Jim_SetSourceInfo(interp, elem, state->fileNameObj, state->line + t->line - 1, 0); } Jim_ListAppendElement(interp, list, elem); @@ -378,7 +378,7 @@ json_decode(Jim_Interp *interp, int argc, Jim_Obj *const argv[]) } /* Save any source information from the original string */ - state.fileNameObj = Jim_GetSourceInfo(interp, argv[argc - 1], &state.line); + state.fileNameObj = Jim_GetSourceInfo(interp, argv[argc - 1], &state.line, NULL); if ((tokens = json_decode_tokenize(interp, state.json, len)) == NULL) { goto done; diff --git a/vendor/jimtcl/jim.c b/vendor/jimtcl/jim.c index 7ddfc378..5f9aabe5 100644 --- a/vendor/jimtcl/jim.c +++ b/vendor/jimtcl/jim.c @@ -1272,9 +1272,11 @@ struct JimParserCtx const char *p; /* Pointer to the point of the program we are parsing */ int len; /* Remaining length */ int linenr; /* Current line number */ + const char *line_start; /* Pointer to the start of the current line */ const char *tstart; const char *tend; /* Returned token is at tstart-tend in 'prg'. */ int tline; /* Line number of the returned token */ + int tcol; /* Column number of the returned token (0-based) */ int tt; /* Token type */ int eof; /* Non zero if EOF condition is true. */ int inquote; /* Parsing a quoted string */ @@ -1299,17 +1301,20 @@ static Jim_Obj *JimParserGetTokenObj(Jim_Interp *interp, struct JimParserCtx *pc /* Initialize a parser context. * 'prg' is a pointer to the program text, linenr is the line * number of the first line contained in the program. */ -static void JimParserInit(struct JimParserCtx *pc, const char *prg, int len, int linenr) +static void JimParserInit(struct JimParserCtx *pc, const char *prg, int len, int linenr, int col) { pc->p = prg; pc->len = len; pc->tstart = NULL; pc->tend = NULL; pc->tline = 0; + pc->tcol = 0; pc->tt = JIM_TT_NONE; pc->eof = 0; pc->inquote = 0; pc->linenr = linenr; + /* Offset line_start so that tcol values are file-relative */ + pc->line_start = prg - col; pc->comment = 1; pc->missing.ch = ' '; pc->missing.line = linenr; @@ -1322,6 +1327,7 @@ static int JimParseScript(struct JimParserCtx *pc) pc->tstart = pc->p; pc->tend = pc->p - 1; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_EOL; if (pc->inquote) { pc->missing.ch = '"'; @@ -1358,6 +1364,7 @@ static int JimParseScript(struct JimParserCtx *pc) if (JimParseVar(pc) == JIM_ERR) { /* An orphan $. Create as a separate token */ pc->tstart = pc->tend = pc->p++; + pc->tcol = pc->tstart - pc->line_start; pc->len--; pc->tt = JIM_TT_ESC; } @@ -1380,6 +1387,7 @@ static int JimParseSep(struct JimParserCtx *pc) { pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; while (isspace(UCHAR(*pc->p)) || (*pc->p == '\\' && *(pc->p + 1) == '\n')) { if (*pc->p == '\n') { break; @@ -1388,6 +1396,7 @@ static int JimParseSep(struct JimParserCtx *pc) pc->p++; pc->len--; pc->linenr++; + pc->line_start = pc->p + 1; } pc->p++; pc->len--; @@ -1401,9 +1410,12 @@ static int JimParseEol(struct JimParserCtx *pc) { pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; while (isspace(UCHAR(*pc->p)) || *pc->p == ';') { - if (*pc->p == '\n') + if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; + } pc->p++; pc->len--; } @@ -1453,6 +1465,7 @@ static void JimParseSubBrace(struct JimParserCtx *pc) if (pc->len > 1) { if (*++pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->len--; } @@ -1473,6 +1486,7 @@ static void JimParseSubBrace(struct JimParserCtx *pc) case '\n': pc->linenr++; + pc->line_start = pc->p + 1; break; } pc->p++; @@ -1507,6 +1521,7 @@ static int JimParseSubQuote(struct JimParserCtx *pc) if (pc->len > 1) { if (*++pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->len--; tt = JIM_TT_ESC; @@ -1526,6 +1541,7 @@ static int JimParseSubQuote(struct JimParserCtx *pc) case '\n': pc->linenr++; + pc->line_start = pc->p + 1; break; case '$': @@ -1562,6 +1578,7 @@ static void JimParseSubCmd(struct JimParserCtx *pc) if (pc->len > 1) { if (*++pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->len--; } @@ -1597,6 +1614,7 @@ static void JimParseSubCmd(struct JimParserCtx *pc) case '\n': pc->linenr++; + pc->line_start = pc->p + 1; break; } startofword = isspace(UCHAR(*pc->p)); @@ -1612,6 +1630,7 @@ static int JimParseBrace(struct JimParserCtx *pc) { pc->tstart = pc->p + 1; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_STR; JimParseSubBrace(pc); return JIM_OK; @@ -1621,6 +1640,7 @@ static int JimParseCmd(struct JimParserCtx *pc) { pc->tstart = pc->p + 1; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_CMD; JimParseSubCmd(pc); return JIM_OK; @@ -1630,6 +1650,7 @@ static int JimParseQuote(struct JimParserCtx *pc) { pc->tstart = pc->p + 1; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JimParseSubQuote(pc); return JIM_OK; } @@ -1652,14 +1673,17 @@ static int JimParseVar(struct JimParserCtx *pc) pc->tstart = pc->p; pc->tt = JIM_TT_VAR; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; if (*pc->p == '{') { pc->tstart = ++pc->p; + pc->tcol = pc->tstart - pc->line_start; pc->len--; while (pc->len && *pc->p != '}') { if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->p++; pc->len--; @@ -1760,6 +1784,7 @@ static int JimParseStr(struct JimParserCtx *pc) } pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; while (1) { if (pc->len == 0) { if (pc->inquote) { @@ -1779,6 +1804,7 @@ static int JimParseStr(struct JimParserCtx *pc) if (pc->len >= 2) { if (*(pc->p + 1) == '\n') { pc->linenr++; + pc->line_start = pc->p + 2; } pc->p++; pc->len--; @@ -1826,6 +1852,7 @@ static int JimParseStr(struct JimParserCtx *pc) } else if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } break; case '"': @@ -1857,12 +1884,14 @@ static int JimParseComment(struct JimParserCtx *pc) } if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } } else if (*pc->p == '\n') { pc->p++; pc->len--; pc->linenr++; + pc->line_start = pc->p; break; } pc->p++; @@ -2130,6 +2159,7 @@ static int JimParseList(struct JimParserCtx *pc) pc->tstart = pc->tend = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_EOL; pc->eof = 1; return JIM_OK; @@ -2139,9 +2169,11 @@ static int JimParseListSep(struct JimParserCtx *pc) { pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; while (isspace(UCHAR(*pc->p))) { if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->p++; pc->len--; @@ -2158,6 +2190,7 @@ static int JimParseListQuote(struct JimParserCtx *pc) pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_STR; while (pc->len) { @@ -2173,6 +2206,7 @@ static int JimParseListQuote(struct JimParserCtx *pc) break; case '\n': pc->linenr++; + pc->line_start = pc->p + 1; break; case '"': pc->tend = pc->p - 1; @@ -2192,6 +2226,7 @@ static int JimParseListStr(struct JimParserCtx *pc) { pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; pc->tt = JIM_TT_STR; while (pc->len) { @@ -3214,13 +3249,13 @@ static const Jim_ObjType scriptLineObjType = { JIM_NONE, }; -static Jim_Obj *JimNewScriptLineObj(Jim_Interp *interp, int argc, int line) +static Jim_Obj *JimNewScriptLineObj(Jim_Interp *interp, int argc, int line, int col) { Jim_Obj *objPtr; #ifdef DEBUG_SHOW_SCRIPT char buf[100]; - snprintf(buf, sizeof(buf), "line=%d, argc=%d", line, argc); + snprintf(buf, sizeof(buf), "line=%d, col=%d, argc=%d", line, col, argc); objPtr = Jim_NewStringObj(interp, buf, -1); #else objPtr = Jim_NewEmptyStringObj(interp); @@ -3228,6 +3263,7 @@ static Jim_Obj *JimNewScriptLineObj(Jim_Interp *interp, int argc, int line) objPtr->typePtr = &scriptLineObjType; objPtr->internalRep.scriptLineValue.argc = argc; objPtr->internalRep.scriptLineValue.line = line; + objPtr->internalRep.scriptLineValue.col = col; return objPtr; } @@ -3339,6 +3375,7 @@ typedef struct ScriptObj shimmering of the currently evaluated object. */ int firstline; /* Line number of the first line */ int linenr; /* Error line number, if any */ + int colnr; /* Error column number, if any */ int missing; /* Missing char if script failed to parse, (or space or backslash if OK) */ } ScriptObj; @@ -3382,6 +3419,7 @@ typedef struct int len; /* Length of this token */ int type; /* Token type */ int line; /* Line number */ + int column; /* Column number (0-based) */ } ParseToken; /* A list of parsed tokens representing a script. @@ -3417,7 +3455,7 @@ static void ScriptTokenListFree(ParseTokenList *tokenlist) * The token list is resized as necessary. */ static void ScriptAddToken(ParseTokenList *tokenlist, const char *token, int len, int type, - int line) + int line, int column) { ParseToken *t; @@ -3440,6 +3478,7 @@ static void ScriptAddToken(ParseTokenList *tokenlist, const char *token, int len t->len = len; t->type = type; t->line = line; + t->column = column; } /* Counts the number of adjoining non-separator tokens. @@ -3465,6 +3504,7 @@ static int JimCountWordTokens(struct ScriptObj *script, ParseToken *t) /* This is a "extra characters after close-brace" error. Report the first error */ script->missing = '}'; script->linenr = t[1].line; + script->colnr = t[1].column; } } } @@ -3521,6 +3561,7 @@ static void ScriptObjAddTokens(Jim_Interp *interp, struct ScriptObj *script, ScriptToken *linefirst; int count; int linenr; + int colnr; #ifdef DEBUG_SHOW_SCRIPT_TOKENS printf("==== Tokens ====\n"); @@ -3538,6 +3579,7 @@ static void ScriptObjAddTokens(Jim_Interp *interp, struct ScriptObj *script, } } linenr = script->firstline = tokenlist->list[0].line; + colnr = tokenlist->list[0].column; token = script->token = Jim_Alloc(sizeof(ScriptToken) * count); @@ -3559,7 +3601,7 @@ static void ScriptObjAddTokens(Jim_Interp *interp, struct ScriptObj *script, /* None, so at end of line */ if (lineargs) { linefirst->type = JIM_TT_LINE; - linefirst->objPtr = JimNewScriptLineObj(interp, lineargs, linenr); + linefirst->objPtr = JimNewScriptLineObj(interp, lineargs, linenr, colnr); Jim_IncrRefCount(linefirst->objPtr); /* Reset for new line */ @@ -3584,8 +3626,9 @@ static void ScriptObjAddTokens(Jim_Interp *interp, struct ScriptObj *script, } if (lineargs == 0) { - /* First real token on the line, so record the line number */ + /* First real token on the line, so record the line and column number */ linenr = tokenlist->list[i].line; + colnr = tokenlist->list[i].column; } lineargs++; @@ -3600,7 +3643,7 @@ static void ScriptObjAddTokens(Jim_Interp *interp, struct ScriptObj *script, /* Every object is initially a string of type 'source', but the * internal type may be specialized during execution of the * script. */ - Jim_SetSourceInfo(interp, token->objPtr, script->fileNameObj, t->line); + Jim_SetSourceInfo(interp, token->objPtr, script->fileNameObj, t->line, t->column); token++; } } @@ -3683,14 +3726,16 @@ static int JimParseCheckMissing(Jim_Interp *interp, int ch) return JIM_ERR; } -Jim_Obj *Jim_GetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, int *lineptr) +Jim_Obj *Jim_GetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, int *lineptr, int *colptr) { int line; + int col = 0; Jim_Obj *fileNameObj; if (objPtr->typePtr == &sourceObjType) { fileNameObj = objPtr->internalRep.sourceValue.fileNameObj; line = objPtr->internalRep.sourceValue.lineNumber; + col = objPtr->internalRep.sourceValue.columnNumber; } else if (objPtr->typePtr == &scriptObjType) { ScriptObj *script = JimGetScript(interp, objPtr); @@ -3702,17 +3747,19 @@ Jim_Obj *Jim_GetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, int *lineptr) line = 1; } *lineptr = line; + if (colptr) *colptr = col; return fileNameObj; } void Jim_SetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, - Jim_Obj *fileNameObj, int lineNumber) + Jim_Obj *fileNameObj, int lineNumber, int columnNumber) { JimPanic((Jim_IsShared(objPtr), "Jim_SetSourceInfo called with shared object")); Jim_FreeIntRep(interp, objPtr); Jim_IncrRefCount(fileNameObj); objPtr->internalRep.sourceValue.fileNameObj = fileNameObj; objPtr->internalRep.sourceValue.lineNumber = lineNumber; + objPtr->internalRep.sourceValue.columnNumber = columnNumber; objPtr->typePtr = &sourceObjType; } @@ -3756,22 +3803,23 @@ static void JimSetScriptFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) ParseTokenList tokenlist; Jim_Obj *fileNameObj; int line; + int col; /* Try to get information about filename / line number */ - fileNameObj = Jim_GetSourceInfo(interp, objPtr, &line); + fileNameObj = Jim_GetSourceInfo(interp, objPtr, &line, &col); /* Initially parse the script into tokens (in tokenlist) */ ScriptTokenListInit(&tokenlist); - JimParserInit(&parser, scriptText, scriptTextLen, line); + JimParserInit(&parser, scriptText, scriptTextLen, line, col); while (!parser.eof) { JimParseScript(&parser); ScriptAddToken(&tokenlist, parser.tstart, parser.tend - parser.tstart + 1, parser.tt, - parser.tline); + parser.tline, parser.tcol); } /* Add a final EOF token */ - ScriptAddToken(&tokenlist, scriptText + scriptTextLen, 0, JIM_TT_EOF, 0); + ScriptAddToken(&tokenlist, scriptText + scriptTextLen, 0, JIM_TT_EOF, 0, 0); /* Create the "real" script tokens from the parsed tokens */ script = Jim_Alloc(sizeof(*script)); @@ -6061,6 +6109,16 @@ static Jim_Obj *JimProcForEvalFrame(Jim_Interp *interp, Jim_EvalFrame *frame) return NULL; } +static Jim_Obj *JimLineColObj(Jim_Interp *interp, int line, int col) +{ + if (col > 0) { + char buf[32]; + snprintf(buf, sizeof(buf), "%d:%d", line, col); + return Jim_NewStringObj(interp, buf, -1); + } + return Jim_NewIntObj(interp, line); +} + /** * Append stack trace info (proc, file, line, cmd) from the eval frame * to listObj @@ -6070,16 +6128,18 @@ static void JimAddStackFrame(Jim_Interp *interp, Jim_EvalFrame *frame, Jim_Obj * Jim_Obj *procNameObj = JimProcForEvalFrame(interp, frame); Jim_Obj *fileNameObj = interp->emptyObj; int linenr = 1; + int colnr = 0; if (frame->scriptObj) { ScriptObj *script = JimGetScript(interp, frame->scriptObj); fileNameObj = script->fileNameObj; linenr = script->linenr; + colnr = script->colnr; } Jim_ListAppendElement(interp, listObj, procNameObj ? procNameObj : interp->emptyObj); Jim_ListAppendElement(interp, listObj, fileNameObj); - Jim_ListAppendElement(interp, listObj, Jim_NewIntObj(interp, linenr)); + Jim_ListAppendElement(interp, listObj, JimLineColObj(interp, linenr, colnr)); Jim_ListAppendElement(interp, listObj, Jim_NewListObj(interp, frame->argv, frame->argc)); } @@ -6104,7 +6164,7 @@ static void JimSetErrorStack(Jim_Interp *interp, ScriptObj *script) */ Jim_ListAppendElement(interp, stackTrace, interp->emptyObj); Jim_ListAppendElement(interp, stackTrace, script->fileNameObj); - Jim_ListAppendElement(interp, stackTrace, Jim_NewIntObj(interp, script->linenr)); + Jim_ListAppendElement(interp, stackTrace, JimLineColObj(interp, script->linenr, script->colnr)); Jim_ListAppendElement(interp, stackTrace, interp->emptyObj); } else { @@ -6853,7 +6913,7 @@ static int SetListFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) } /* Try to preserve information about filename / line number */ - fileNameObj = Jim_GetSourceInfo(interp, objPtr, &linenr); + fileNameObj = Jim_GetSourceInfo(interp, objPtr, &linenr, NULL); Jim_IncrRefCount(fileNameObj); /* Get the string representation */ @@ -6869,7 +6929,7 @@ static int SetListFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) /* Convert into a list */ if (strLen) { - JimParserInit(&parser, str, strLen, linenr); + JimParserInit(&parser, str, strLen, linenr, 0); while (!parser.eof) { Jim_Obj *elementPtr; @@ -6877,7 +6937,7 @@ static int SetListFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) if (parser.tt != JIM_TT_STR && parser.tt != JIM_TT_ESC) continue; elementPtr = JimParserGetTokenObj(interp, &parser); - Jim_SetSourceInfo(interp, elementPtr, fileNameObj, parser.tline); + Jim_SetSourceInfo(interp, elementPtr, fileNameObj, parser.tline, 0); ListAppendElement(objPtr, elementPtr); } } @@ -9169,6 +9229,7 @@ static int JimParseExpression(struct JimParserCtx *pc) while (isspace(UCHAR(*pc->p)) || (*(pc->p) == '\\' && *(pc->p + 1) == '\n')) { if (*pc->p == '\n') { pc->linenr++; + pc->line_start = pc->p + 1; } pc->p++; pc->len--; @@ -9185,6 +9246,7 @@ static int JimParseExpression(struct JimParserCtx *pc) /* Common case */ pc->tline = pc->linenr; pc->tstart = pc->p; + pc->tcol = pc->tstart - pc->line_start; if (pc->len == 0) { pc->tend = pc->p; @@ -9726,7 +9788,7 @@ static int ExprTreeBuildTree(Jim_Interp *interp, struct ExprBuilder *builder, in objPtr = Jim_NewStringObj(interp, t->token, t->len); if (t->type == JIM_TT_CMD) { /* Only commands need source info */ - Jim_SetSourceInfo(interp, objPtr, builder->fileNameObj, t->line); + Jim_SetSourceInfo(interp, objPtr, builder->fileNameObj, t->line, 0); } } @@ -9826,7 +9888,7 @@ static int SetExprFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) int rc = JIM_ERR; /* Try to get information about filename / line number */ - fileNameObj = Jim_GetSourceInfo(interp, objPtr, &line); + fileNameObj = Jim_GetSourceInfo(interp, objPtr, &line, NULL); Jim_IncrRefCount(fileNameObj); exprText = Jim_GetString(objPtr, &exprTextLen); @@ -9834,7 +9896,7 @@ static int SetExprFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) /* Initially tokenise the expression into tokenlist */ ScriptTokenListInit(&tokenlist); - JimParserInit(&parser, exprText, exprTextLen, line); + JimParserInit(&parser, exprText, exprTextLen, line, 0); while (!parser.eof) { if (JimParseExpression(&parser) != JIM_OK) { ScriptTokenListFree(&tokenlist); @@ -9847,7 +9909,7 @@ static int SetExprFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr) } ScriptAddToken(&tokenlist, parser.tstart, parser.tend - parser.tstart + 1, parser.tt, - parser.tline); + parser.tline, parser.tcol); } #ifdef DEBUG_SHOW_EXPR_TOKENS @@ -11186,8 +11248,8 @@ static Jim_Obj *JimInterpolateTokens(Jim_Interp *interp, const ScriptToken * tok else if (tokens && intv[0] && intv[0]->typePtr == &sourceObjType) { /* The first interpolated token is source, so preserve the source info */ int line; - Jim_Obj *fileNameObj = Jim_GetSourceInfo(interp, intv[0], &line); - Jim_SetSourceInfo(interp, objPtr, fileNameObj, line); + Jim_Obj *fileNameObj = Jim_GetSourceInfo(interp, intv[0], &line, NULL); + Jim_SetSourceInfo(interp, objPtr, fileNameObj, line, 0); } @@ -11326,6 +11388,7 @@ int Jim_EvalObj(Jim_Interp *interp, Jim_Obj *scriptObjPtr) /* First token of the line is always JIM_TT_LINE */ argc = token[i].objPtr->internalRep.scriptLineValue.argc; script->linenr = token[i].objPtr->internalRep.scriptLineValue.line; + script->colnr = token[i].objPtr->internalRep.scriptLineValue.col; /* Allocate the arguments vector if required */ if (argc > JIM_EVAL_SARGV_LEN) @@ -11708,7 +11771,7 @@ int Jim_EvalSource(Jim_Interp *interp, const char *filename, int lineno, const c scriptObjPtr = Jim_NewStringObj(interp, script, -1); Jim_IncrRefCount(scriptObjPtr); if (filename) { - Jim_SetSourceInfo(interp, scriptObjPtr, Jim_NewStringObj(interp, filename, -1), lineno); + Jim_SetSourceInfo(interp, scriptObjPtr, Jim_NewStringObj(interp, filename, -1), lineno, 0); } retval = Jim_EvalObj(interp, scriptObjPtr); Jim_DecrRefCount(interp, scriptObjPtr); @@ -11795,7 +11858,7 @@ int Jim_EvalFile(Jim_Interp *interp, const char *filename) } filenameObj = Jim_NewStringObj(interp, filename, -1); - Jim_SetSourceInfo(interp, scriptObjPtr, filenameObj, 1); + Jim_SetSourceInfo(interp, scriptObjPtr, filenameObj, 1, 0); oldFilenameObj = JimPushInterpObj(interp->currentFilenameObj, filenameObj); @@ -11822,6 +11885,7 @@ static void JimParseSubst(struct JimParserCtx *pc, int flags) { pc->tstart = pc->p; pc->tline = pc->linenr; + pc->tcol = pc->tstart - pc->line_start; if (pc->len == 0) { pc->tend = pc->p; @@ -11839,6 +11903,7 @@ static void JimParseSubst(struct JimParserCtx *pc, int flags) } /* Not a var, so treat as a string */ pc->tstart = pc->p; + pc->tcol = pc->tstart - pc->line_start; /* Skip this $ */ pc->p++; pc->len--; @@ -11881,7 +11946,7 @@ static int SetSubstFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr, int flags /* Initially parse the subst into tokens (in tokenlist) */ ScriptTokenListInit(&tokenlist); - JimParserInit(&parser, scriptText, scriptTextLen, 1); + JimParserInit(&parser, scriptText, scriptTextLen, 1, 0); while (1) { JimParseSubst(&parser, flags); if (parser.eof) { @@ -11889,7 +11954,7 @@ static int SetSubstFromAny(Jim_Interp *interp, struct Jim_Obj *objPtr, int flags break; } ScriptAddToken(&tokenlist, parser.tstart, parser.tend - parser.tstart + 1, parser.tt, - parser.tline); + parser.tline, parser.tcol); } /* Create the "real" subst/script tokens from the initial token list */ @@ -12135,6 +12200,8 @@ static int JimInfoFrame(Jim_Interp *interp, Jim_Obj *levelObjPtr, Jim_Obj **objP ScriptObj *script = JimGetScript(interp, frame->scriptObj); Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, "line", -1)); Jim_ListAppendElement(interp, listObj, Jim_NewIntObj(interp, script->linenr)); + Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, "col", -1)); + Jim_ListAppendElement(interp, listObj, Jim_NewIntObj(interp, script->colnr)); Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, "file", -1)); Jim_ListAppendElement(interp, listObj, script->fileNameObj); } @@ -15844,11 +15911,11 @@ static int Jim_InfoCoreCommand(Jim_Interp *interp, int argc, Jim_Obj *const *arg return JIM_ERR; } resObjPtr = Jim_NewStringObj(interp, Jim_String(argv[2]), Jim_Length(argv[2])); - Jim_SetSourceInfo(interp, resObjPtr, argv[3], line); + Jim_SetSourceInfo(interp, resObjPtr, argv[3], line, 0); } else { int line; - fileNameObj = Jim_GetSourceInfo(interp, argv[2], &line); + fileNameObj = Jim_GetSourceInfo(interp, argv[2], &line, NULL); resObjPtr = Jim_NewListObj(interp, NULL, 0); Jim_ListAppendElement(interp, resObjPtr, fileNameObj); Jim_ListAppendElement(interp, resObjPtr, Jim_NewIntObj(interp, line)); diff --git a/vendor/jimtcl/jim.h b/vendor/jimtcl/jim.h index df6c0b17..f64dc9cf 100644 --- a/vendor/jimtcl/jim.h +++ b/vendor/jimtcl/jim.h @@ -340,6 +340,7 @@ typedef struct Jim_Obj { struct { struct Jim_Obj *fileNameObj; int lineNumber; + int columnNumber; } sourceValue; /* Dict substitution type */ struct { @@ -348,6 +349,7 @@ typedef struct Jim_Obj { } dictSubstValue; struct { int line; + int col; int argc; } scriptLineValue; } internalRep; @@ -701,10 +703,10 @@ JIM_EXPORT int Jim_SubstObj (Jim_Interp *interp, Jim_Obj *substObjPtr, /* source information */ JIM_EXPORT Jim_Obj *Jim_GetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, - int *lineptr); + int *lineptr, int *colptr); /* may only be called on an unshared object */ JIM_EXPORT void Jim_SetSourceInfo(Jim_Interp *interp, Jim_Obj *objPtr, - Jim_Obj *fileNameObj, int lineNumber); + Jim_Obj *fileNameObj, int lineNumber, int columnNumber); /* stack */ From 39f0f905714f30605f89f65ef48ada70d6d3437b Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Mon, 25 May 2026 15:24:17 -0400 Subject: [PATCH 42/45] editor: Wipe old Holds (not that important anyway) --- builtin-programs/editor/editor.folk | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builtin-programs/editor/editor.folk b/builtin-programs/editor/editor.folk index d63f86c7..ccdfb9aa 100644 --- a/builtin-programs/editor/editor.folk +++ b/builtin-programs/editor/editor.folk @@ -1,3 +1,6 @@ +# Old editor data is incompatible. +file delete $::env(HOME)/folk-data/hold/editor.folk + # This makes all keyboards create editors automatically. May choose to # change later, or exclude keyboards that opt out. When /k/ is a keyboard with /...opts/ { From 019d91bd1a93bcba11047d23e262f209273cb2c5 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Mon, 25 May 2026 15:24:33 -0400 Subject: [PATCH 43/45] print: Make tagInset so tag not cut off; emit per-program geometry so in future, we can change the format but still have old pages work + every page can have its own font size or weird geometry based on how you edited it. --- builtin-programs/print/print.folk | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index 7be791bc..21cf6332 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -90,6 +90,7 @@ fn codeToPostScript {id code opts {mixins {}}} { set tagWidth [expr {$tagInnerSideLength * 10.0 / 6}] set tagHeight $tagWidth lassign $opts(margin) marginTop marginRight marginBottom marginLeft + set tagInset $opts(tagInset) set lineHeight $opts(lineHeight) set maxLines $(int(($PageHeight - $marginTop - $marginBottom) / $lineHeight)) @@ -136,16 +137,16 @@ fn codeToPostScript {id code opts {mixins {}}} { [expr {[llength $outPages] > 0 ? {} : [subst { gsave - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-$marginTop}] translate + [expr {$PageWidth-$tagWidth-$marginRight-$tagInset}] [expr {$PageHeight-$tagHeight-$marginTop-$tagInset}] translate $tagWidth $tagHeight scale $image grestore /Helvetica-Narrow findfont - 11 scalefont + 8 scalefont setfont newpath - [expr {$PageWidth-$tagWidth-$marginRight}] [expr {$PageHeight-$tagHeight-14-$marginTop}] moveto + [expr {$PageWidth-$tagWidth-$marginRight-$tagInset}] [expr {$PageHeight-$tagHeight-14-$marginTop-$tagInset}] moveto ($id ([clock format [clock seconds] -format "%a, %d %b %Y, %r"])) show [join [lmap mixin $mixins { @@ -231,6 +232,8 @@ Subscribe: print program /id/ with /...options/ { dict set options tagInnerSideLength [expr {70.0 / $calibScale}] } + dict set options tagInset 16 + set ps [codeToPostScript $id $code $options] # save code and ps to disk @@ -243,6 +246,35 @@ Subscribe: print program /id/ with /...options/ { exec ps2pdf -dPDFSETTINGS=/prepress -sFONTPATH=vendor/fonts \ $saveDir/$id.ps $saveDir/$id.pdf + # Write geometry to meta.folk so the camera system can interpret + # this program's quad and map (line, col) -> physical position. + # All opts are in calibrated points; 1 calibrated pt = 25.4/72 mm. + set ptmm [expr {25.4 / 72.0}] + lassign [dict get $options pageSize] PageWidth PageHeight + set tagInn [dict get $options tagInnerSideLength] + set tagOut [expr {$tagInn * 10.0 / 6}] + lassign [dict get $options margin] marginTop marginRight marginBottom marginLeft + set lh [dict get $options lineHeight] + set adv [dict get $options advance] + + set tagInset [dict get $options tagInset] + set border [expr {($tagOut - $tagInn) / 2.0}] + set gLeft [expr {($PageWidth - $tagOut - $marginRight - $tagInset + $border) * $ptmm}] + set gRight [expr {($marginRight + $tagInset + $border) * $ptmm}] + set gTop [expr {($marginTop + $tagInset + $border) * $ptmm}] + set gBottom [expr {($PageHeight - $tagOut - $marginTop - $tagInset + $border) * $ptmm}] + + set geomStr [format \ + {tagSize %.4gmm left %.4gmm right %.4gmm top %.4gmm bottom %.4gmm lineHeight %.4gmm advance %.4gmm codeLeft %.4gmm codeTop %.4gmm} \ + [expr {$tagInn * $ptmm}] $gLeft $gRight $gTop $gBottom \ + [expr {$lh * $ptmm}] [expr {$adv * $ptmm}] \ + [expr {($marginLeft + $adv * 2.5) * $ptmm}] \ + [expr {$marginTop * $ptmm}]] + + set fp [open "$saveDir/$id.meta.folk" w] + puts $fp "Claim tag \$this has geometry \{$geomStr\}" + close $fp + puts "Printing program $id on $::thisNode" Notify: print pdf $saveDir/$id.pdf with {*}$options } From 236d3fd1f23eaee18c8eb22b8b2fc33b039fbc28 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Mon, 25 May 2026 18:06:40 -0400 Subject: [PATCH 44/45] calibrate-page: Remove example program png step --- .../calibrate/calibrate-page.folk | 97 ------------------- 1 file changed, 97 deletions(-) diff --git a/builtin-programs/calibrate/calibrate-page.folk b/builtin-programs/calibrate/calibrate-page.folk index 95317081..c5da4154 100644 --- a/builtin-programs/calibrate/calibrate-page.folk +++ b/builtin-programs/calibrate/calibrate-page.folk @@ -2,78 +2,9 @@ When the codeToPostScript is /codeToPostScript/ { fn codeToPostScript -fn makeExampleProgramPng {} { - # HACK: we hard-code letter, since this is just for documentation - # purposes, and we want to cut the bottom half off so we need - # known dimensions. - set opts [dict create \ - tagInnerSideLength 70 \ - pageSize {612 792} \ - lineHeight 16 \ - advance [* 0.5859375 16] \ - margin [lmap x {0.01 0.005 0.005 0.01} \ - {* 2834.646 $x}]] - set ps [codeToPostScript 0 {# This image is for illustration purposes; don't -# print it. You should print a program normally -# through the Folk editor and measure that. -} $opts {{ - - [set left [expr {$PageWidth-$tagWidth-$marginRight}]] - [set bottom [expr {$PageHeight-$tagHeight-$marginTop}]] - [set outerToInner [expr {($tagWidth / 10.0) * 2}]] - - % These take in x1 y1 x2 y2 on stack. - /markXDistance { - newpath moveto - 0 -15 rlineto 0 30 rmoveto 0 -15 rlineto - lineto - 0 -15 rlineto 0 30 rmoveto 0 -15 rlineto - 6 setlinewidth stroke - } def - /markYDistance { - newpath moveto - -15 0 rlineto 30 0 rmoveto -15 0 rlineto - lineto - -15 0 rlineto 30 0 rmoveto -15 0 rlineto - 6 setlinewidth stroke - } def - - % Left - [+ $left $outerToInner] [expr {$bottom + $tagHeight/2.0}] - 0 [expr {$bottom + $tagHeight/2.0}] - 1 0 0 setrgbcolor markXDistance - - % Right - [expr {$left + $tagWidth - $outerToInner}] [expr {$bottom + $tagHeight/2.0}] - $PageWidth [expr {$bottom + $tagHeight/2.0}] - 1 0 0 setrgbcolor markXDistance - - % Top - [expr {$left + $tagWidth/2.0}] [expr {$bottom + $tagHeight - $outerToInner}] - [expr {$left + $tagWidth/2.0}] $PageHeight - 0 0.5 1 setrgbcolor markYDistance - - % Bottom - [expr {$left + $tagWidth/2.0}] [expr {$bottom + $outerToInner}] - [expr {$left + $tagWidth/2.0}] [expr {$PageHeight/2.0}] - 0 0.5 1 setrgbcolor markYDistance - - % Tag inner - [+ $left $outerToInner] [+ $bottom $outerToInner 5] - [expr {$left + $tagWidth - $outerToInner}] [+ $bottom $outerToInner 5] - 0 1 0 setrgbcolor markXDistance - }}] - - set fp [open [list |gs -sDEVICE=png16m -q -dBATCH -r300 -sOutputFile=- - <<$ps] rb] - set png [read $fp]; close $fp - return $png -} - Wish the web server handles route "/calibrate" with hidden true handler { package require base64 - set exampleProgramPng [makeExampleProgramPng] - set defaultGeom [dict get [lindex [Query! /someone/ claims the default program geometry is /defaultGeom/] 0] defaultGeom] fn defaultGeomGet {key} { return [string map {mm ""} [dict get $defaultGeom $key]] } @@ -343,34 +274,6 @@ Camera Controls calibrating again.

  • -
  • -

    Measure program geometry.

    - -

    Now we need to tell Folk the exact geometry of an - average program page: what is the physical size of the - AprilTag on the page? how far is the tag from the edges - of the page?

    - -

    Print a program that outlines itself, - if you don't have one already.

    - -

    Fold the program in half if you want half-height programs. Measure the tag inner side length in millimeters, along with the distances in millimeters from the tag inner perimeter to each edge of the paper. Enter them below.

    - - -
    - How to manually override the geometry of a specific program - -

    If you've, for example, printed out program 30 at a - different size, or you manually cut and pasted the tag - 30 somewhere and want to create a specially sized region - around that, you can set tag 30's geometry manually by making a - 30.meta.folk text file in ~/folk-printed-programs, with content like this:

    - -
    Claim tag \$this has geometry {tagSize 30mm top 28mm right 28mm left 157mm bottom 80mm}
    -
    -
  • -
  • Test calibration.

    From 16732ffb9827f21bab12b7a39e6fc16866011403 Mon Sep 17 00:00:00 2001 From: Omar Rizwan Date: Wed, 27 May 2026 17:16:28 -0400 Subject: [PATCH 45/45] print: Handle half-height pages --- builtin-programs/print/print.folk | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/builtin-programs/print/print.folk b/builtin-programs/print/print.folk index 21cf6332..a6cbb0a8 100644 --- a/builtin-programs/print/print.folk +++ b/builtin-programs/print/print.folk @@ -264,6 +264,17 @@ Subscribe: print program /id/ with /...options/ { set gTop [expr {($marginTop + $tagInset + $border) * $ptmm}] set gBottom [expr {($PageHeight - $tagOut - $marginTop - $tagInset + $border) * $ptmm}] + # If the first page is sparse enough, assume the user will fold the + # page in half vertically and shrink the reported bottom geometry. + set numLines [llength [split $code "\n"]] + set maxLines [expr {int(($PageHeight - $marginTop - $marginBottom) / $lh)}] + set firstPageLines [expr {$numLines < $maxLines ? $numLines : $maxLines}] + if {$firstPageLines < $maxLines / 2.0} { + set tagSize [expr {$tagInn * $ptmm}] + set pageHeightMm [expr {$tagSize + $gTop + $gBottom}] + set gBottom [expr {$gBottom - $pageHeightMm / 2.0}] + } + set geomStr [format \ {tagSize %.4gmm left %.4gmm right %.4gmm top %.4gmm bottom %.4gmm lineHeight %.4gmm advance %.4gmm codeLeft %.4gmm codeTop %.4gmm} \ [expr {$tagInn * $ptmm}] $gLeft $gRight $gTop $gBottom \