Clamping numeric values

What’s the canonical way to clamp a numeric value to a particular min/max range? E.G., if I want to set Xpos programmatically but constrain it to be within [0,N] for some fixed N.

I see the min() and max() functions, and assume they are the tools to use. It appears they take a string rather than individual numeric arguments; that seems to introduce enough awkwardness to make me question if I’m on the right track, since if I read the docs correctly, they imply that I have to concatenate my numeric arts into a semicolon-delimited string. Is this correct?

You can pass numbers to min and max.

Are the restrictions the same for all notes or do different (individual?) notes have different limits?

Assuming for the moment we want to ‘clamp’ so notes must lie within +/- 5 map units on the X axis. Then the core of this is to check like so:

if ($Xpos<= -5 | $Xpos>= 5) {
   if($Xpos<= -5){$Xpos=-5;};
   if($Xpos>= 5){$Xpos=5;};
}

Of course if notes are moved by this rule they will shift. But $Xpos is the top left corner of the note. So if we want the note to always be fully in bounds, we need to allow for the width when at positive X.

Try this rule:

if ($Xpos<= -10 | ($Xpos+$Width)>=10) {
   if($Xpos<= -10){$Xpos= -9.90;};
   if(($Xpos+$Width)>=10){$Xpos=(9.90-$Width);};
};
// subtitle is only for visual tell-back during testing
$Subtitle = "X is "+$Xpos.precision(3)+", "+"Y is "+$Ypos.precision(3);

Now, I’ve made an adornment (locked to the background) 20 map units wide. If I move my test note even slightly left of X=-10 units (left dashed line), it is moved back in:

clamp-test.tbx 2022-02-15 21-49-31

Same if I go the other way:

clamp-test.tbx 2022-02-15 21-51-02

There is may be a slight delay before movement as the rule has to cycle and the map re-draw. Note: I very much doubt the map drawing routines were designed with this sort of trick in mind.

Note that is you want to ‘clamp’ on the Y axis, thre directions are flipped from normal cartesian settings. It won’t change the maths but it might surprise if unexpected. Here is my test document: clamp-test.tbx (83.6 KB)

In the test doc the rule is set in a prototype—see the outline tab, note ‘pNote’.

Hmm, it doesn’t work that way for me.

This sets $Xpos as I want:

$DaysTilNext = day($NextDate) - day(today);
$Xpos = 3*max("0;"+$DaysTilNext);

This (which is what I take “pass numbers to max” to mean) sets $Xpos to zero:

$DaysTilNext = day($NextDate) - day(today);
$Xpos = 3*max(0, $DaysTilNext);

Thanks for the reply, Mark. I understand the branching logic; I’m used to just writing the equivalent of

clamp(x, lo, hi) = min(hi, max(lo, x))

in whatever language I’m working in, so writing out if-blocks longhand would be a step backwards.

I appreciate the info about positioning, and the thorough example! Thanks!

That sounds like a good use case for the new ‘function’ feature: you should be able to write your own function clamp(…) so that you only have to write out the nested ifs once.

Sorry, I’m on the iPad so can’t test this at the moment….

That sounds like a good use case for the new ‘function’ feature: you should be able to write your own function clamp(…) so that you only have to write out the nested ifs once.

Yes, that’s the ticket! :grinning:

A relevant docs page for anyone stumbling across this later: Functions

Thanks!!

2 Likes

Function? Sure. Aside: I’ll avoid the term ‘clamp’ in the function which has no meaning for Tinderbox users and would only confuse.

So here is a specimen function (for those not used to Discourse forum software, the forum default syntax colouring below is not marking Tinderbox syntax but gets close to it):

function fXReset(iXpos,iWidth,iMin,iMax,iShim,iReport){
   // inputs arrive as strings so coerce to correct type
   // passing inputs to typed variables confirms data type
   // *ALL* inputs must be supplied
   // iXpos is source note $Xpos
   var:number vX = iXpos;
   // iWidth is source notes $Width
   var:number vW = iWidth;
   // iMin is left-most allowed X-axis value
   var:number vMin = iMin;
   // iMax is right-most allowed X-axis value
   var:number vMax = iMax;
   // iShim is buffer so moved note is visibly inside limit
   var:number vShim = iShim;
   // iReport toggle postion report in $Subtitle, for testing
   var:boolean vReport = vReport;

   if (vX<=vMin | (vX+vW)>=vMax ) {
      if(vX<=vMin){
         vX=(vMin+vShim);
      };
      // as $Xpos is note top-left corner, postive axis
      // value must allow for $Width to remain fully in bounds
      if((vX+vW)>=vMax){
         vX=((vMax-vShim)-vW);
      };
   };

   // report current $Xpos
   if(vReport){
      $Subtitle = "X co-ord: "+vX.precision(2);
   } else {
      $Subtitle =;  // reset $Subtitle to defualt, i.e. no text
   }

   // return new $Xpos
   return vX;
}

The function is invoked with an action like this:

$Xpos = fXReset($Xpos,$Width,-10,10,0.1,true);

We can also call the function via an agent. But, noting that $Xpos is an intrinsic attribute we must use $Xpos(original) otherwise we set the $Xpos of the alias in the agent and not the alias’ original note.

$Xpos(original) = fXReset($Xpos(original),$Width,-10,10,0.1,true);

Both methods work. In my very small test doc, calling from a rule resulted in faster update than an agent. As both methods work, there is no ‘correct’ method of calling the function.

Notes:

  • Tinderbox functions fail if any defined terms are not passed from the calling action
  • Currently function input arguments are all received as String-type data. This may change, but until then it is advisable to use typed variables (see var operator).
  • I’m not a programmer so others may see a more efficient way of doing this, but the code is tested in v9.1.0)
  • The ‘shim’ is just a visual artifice for the test so we can see the note and test boundary (simulated by the adornments).
  • Adjustment is made for positive X because in order for the whole note to be inbounds, we need to allowed for the note icon width ($Xpos is the position of the top-left corner of the note). This may not be a necessary
  • The function cares nothing about Y-axis position. If the user drags a note out of bounds at the same Y positions several times, the moved notes will stack. In the demo as the test notes are the same size, the oldest notes in such a stack will appear to vanish as they are hidden.
    • Tinderbox has no user-available ‘collision-avoidance’ routine, before anyone asks.
  • It is perfectly possible to track Y-axis position too. If needed, I’d suggest using discrete function so you don’t have a vast number of arguments. Anyway this is left as an exercise for those who need it.
  • Another possibility is to set some marker (visible or just a general user attribute) that a note has passed out of bounds and been moved. But you will also need to consider how/when that flag is reset, if needs be.
  • The function arguments could be passed as a list, but I don’t think it helps any. The individual arguments inputs still have to found and mapped to correct data type. In addition, unless a function is building the list, I think the apparent concision achieved just adds complexity and chance of error.
  • Bear in mind that once function and calling code are written, you don’t need to touch it again. But clear layout of code and comments will help future self understand it weeks/months/years later. for instance, if type-support for arguments arrives, lots of the casting to typed variables can be removed.
  • Not used here but possibly helpful when building is to use a logging function, e.g. to debug position & shimming. Using rules or agents, beware of logging that accretes input (i.e. $Text += ...) as this will add a lot of entries quickly (and keeps going when you step away to take a call or grab a drink!). In such circumstances consider using edicts, low priority agents or stamps for initial testing so the logging is actually useful.
  • Although disabled by default, when using map-based behaviours like this, be aware of composites: how they work and how to disable remove them if they somehow get invoked.
  • Beware issues of scale with processes like this. What works for a few test notes might not work so well with 100s of such notes. My suggestion, think defensive and assume there will be scale problems. You may either be happily surprised or at least not disappointed. Otherwise, as seen often in the past, the user ends up blaming the app for their own error and lack of foresight. The sort of thing we are doing here can be done, but I strongly doubt many of the processes used were designed for (fast-updating) interactive maps. So, don’t over-demand of the app. :slight_smile:

I did note in an earlier post that @jxxmxxj is actually using the map X-axis as a timeline, in which case do look at Tinderbox’s timeline view. It might help, though I’ve insufficient use case detail to be able to say. Also, if using the above ideas be aware date arithmetic in action code has some idiosyncrasies > Again, Tinderbox’s action code is not [your favourite formal code language], so expect syntax to differ from your assumed norms rather than that action code should be like some unknown (to Tinderbox) other system…

I’ve updated my earlier TBX to use 3 pairs of notes, each based on a discrete prototype. Two prototype’s use a rule to call the function, and one is worked via and agent. This is simply to chow both approaches could be used.

I’ve run out of time to read a read-me, but the test part is seen below, in the first (left-most) tab which uses map view. The doc is saved with this tab selected (you may need to scroll the map a bit to see what you want:

The second (right-most) tab is set to a root-level outline view giving access to everything:

…and finally the test TBX: x-reset-test.tbx (120.4 KB)

1 Like