/* tabulated.inc
 *
 * create row/column data tables from array data.
 *
 * Persistence of Vision Raytracer version 3.8 or later.
 *
 *
 * macro Tabulated(dict)
 *
 *   dict - a 'dictionary {}'. all keys optional, except
 *          '.DataColumns' and '.DataTable'.
 *
 *     .DataTable   - array [r] or array [r][c].  provide data
 *                    inline, or use '#declare'd identifier.
 *
 *     .DataColumns - array [c] string.  chosen from list:
 *                      "B"   - boolean values.
 *                      "I"   - integer values.
 *                      "Fp"  - float values.  'p' is the precision,
 *                              range: 0 <= p <= 16.
 *                      "G"   - alias for "F6".
 *                      "Vnp" - vector values.  'n' is the number of
 *                              components (2 <= n <= 5), and 'p' the
 *                              precision, as for floats.
 *
 *     .ColLabels   - array [c] string.  names for the data columns.
 *
 *     .RowLabels   - array [r] string.  names for the data rows.
 *
 *     .ColAlign    - array [c] int.  override the alignment of data
 *                    items.  -1 to left-align, 0 to centre, and
 *                    1 to right-align.  defaults are strings and
 *                    vectors left-aligned, numbers right-aligned,
 *                    and booleans centred.
 *
 *     .ColWidths   - array [c] float.  legal range 1,..,40 POV-Ray
 *                    units, or -1 for "don't care".  default is to
 *                    make cells just wide enough for content.
 *
 *     .Padding     - 2-vector.  the two components represent percent
 *                    of largest glyph (of font used), where the '.x'
 *                    value is added left and right of label/data,
 *                    and '.y' above and below.  default, when omitted,
 *                    is '<.35,.2>'.
 *
 *     .Caption     - array mixed.  three elements, or four:
 *                    {caption, alignment, above table[, fg index]}
 *
 *     .Hilite      - array mixed.  two elements, or three:
 *                    {from, to[, rgbf]}, where 'from' and 'to' are
 *                    2-vectors giving start and end of range
 *                    ('<col,row>').
 *
 *     .Note        - reserved for future use.
 *
 *     .Fonts       - array [3] string.  the TrueType font names to use
 *                    for labels, data, and the caption, in that order.
 *
 *     .Bg          - array [3] pigment.  override the backgrounds for
 *                    labels, data, and alt-data (cf '.Ledger').
 *
 *     .Fg          - array [3] int.  override the foreground colours
 *                    for labels, data, and alt-data (cf '.Ledger').
 *                      0 - black    4 - red
 *                      1 - blue     5 - magenta
 *                      2 - green    6 - yellow
 *                      3 - cyan     7 - white
 *
 *     .Borders     - bool.  whether or not to display the table "grid".
 *                    default, when omitted, 'true'.
 *
 *     .Lines       - array [2] float.  the two values represent the
 *                    diameter of the (cylinder) lines, and a fg colour
 *                    index, for the "grid".  default, when omitted,
 *                    is '{.025,4}'.
 *
 *     .Emissive    - bool.  whether or not to use an emissive style
 *                    finish for the table's various components.
 *                    default, when omitted, 'false'.
 *
 *     .Finishes    - array [2] finish.  the first, regular 'finish{}'
 *                    for lit scenes, the second for poorly or unlit
 *                    scenes (using 'emission' keyword).
 *
 *     .UseBool     - array [2] string.  the words used to display
 *                    boolean type values, default, when omitted,
 *                    is '{"no","yes"}'.
 *
 *     .Transpose   - bool.  whether or not to transpose table's rows
 *                    and columns.  default, when omitted, 'false'.
 *
 *     .Ledger      - bool.  whether or not to draw alternating rows
 *                    with different fg/bg.  requires min 4 rows data.
 *                    default, when omitted, 'false'.
 *
 *     .Logo        - bool.  whether or not to draw a small POV-Ray logo
 *                    as part of table.  requires both row and column
 *                    labels, and 'logo.inc' sourced.  default, when
 *                    omitted, 'true'.
 *
 *     .Verbose     - bool.  whether or not to output summary info
 *                    to POV-Ray's '#debug' stream.  default, when
 *                    omitted, 'false'.
 *
 *
 * variables Tabulated_{min,max}_extent
 *
 *   these are 3-vector variables, as returned by '{min,max}_extent'
 *   function calls, both are (re-)created every time the macro runs.
 *
 *
 * version 202106.2
 */

#ifndef (tabu_temp_include)

#declare tabu_temp_include = version;
#version 3.8;

#ifdef (View_POV_Include_Stack)
#debug "including tabulated.inc\n"
#end

/* -----[helpers + stuff]---------------------------------------------------- *
 * colours  - black, blue, green, cyan, red, magenta, yellow, white.
 * finishes - all objects {regular, emissive}.
 * fonts    - ttf names {label, data, caption}.
 * pigments - backgrounds {label, data, alt-data, transparent}.
 * scales   - of strings {label, data, caption}.
 * eps      - epsilon.
 * hyphen   - hyphens.
 * roundup  - to nearest 5/1000th.
 */

#declare tabu__data_colours_ = array [8] {
  <.01,.01,.01>, <0,0,1>, <0,1,0>, <0,1,1>, <1,0,0>, <1,0,1>, <1,1,0>, <1,1,1>
};

#declare tabu__data_finishes_ = array [2] {
  finish {ambient .1 diffuse .8 specular .1},
  finish {ambient 0 emission .85}
};

#declare tabu__data_fonts_ = array [3] {
  "timrom.ttf", "cyrvetic.ttf", "timrom.ttf"
};

#declare tabu__data_pigments_ = array [4] {
  pigment {color rgb .60},
  pigment {color rgb .85},
  pigment {color rgb .75},
  pigment {color rgbt 1}
};

#declare tabu__data_scales_ = array [3] {.85, .9, .8};

#declare tabu__eps_ = 1e-4;

#macro tabu__hyphen(n_)
  #local s_ = "";
  #for (i_,1,n_)
    #local s_ = concat(s_,"-");
  #end
  s_
#end

#macro tabu__roundup(v_)
  #local k1_ = 1e6;
  #local k2_ = 5e3;
  #local a_ = int(k1_*v_);
  #if (mod(a_,k2_))
    #local a_ = k2_ * int((k2_+a_)/k2_);
  #end
  (a_/k1_)
#end

/* -----[formats + pretty printing]----------------------------------------- */

#macro tabu__boolFmt(f_)
  #local s_ = idict_.bool_[0];
  #if (f_)
    #local s_ = idict_.bool_[1];
  #end
  s_
#end

#macro tabu__strFmt(s_)
  #local w_ = 20;
  #local n_ = strlen(s_);
  #if (w_ > n_)
    #local s_ = concat(substr("                    ",1,(w_-n_)),s_);
  #end
  s_
#end

#macro tabu__vectFmt(n_,v_,p_)
  concat("<",vstr(n_,v_,",",0,p_),">")
#end

#macro tabu__emitBool(s_,v_)
  #debug concat(tabu__strFmt(s_),": ",tabu__boolFmt(v_),"\n")
#end

#macro tabu__emitFlt(s_,v_,p_)
  #debug concat(tabu__strFmt(s_),": ",str(v_,0,p_),"\n")
#end

#macro tabu__emitInt(s_,v_)
  #debug concat(tabu__strFmt(s_),": ",str(v_,0,0),"\n")
#end

#macro tabu__emitStr(s_,v_)
  #debug concat(tabu__strFmt(s_),": ",v_,"\n")
#end

#macro tabu__emitVect(s_,v_,n_,p_)
  #debug concat(tabu__strFmt(s_),": ",tabu__vectFmt(n_,v_,p_),"\n")
#end

#macro tabu__emitHeader()
  #debug concat(tabu__hyphen(5),"[Tabulated]",tabu__hyphen(56),"\n")
  #local i_ = dimension_size(idict_.gc_,1) - (idict_.ucl_ ? 0 : 1);
  #local j_ = dimension_size(idict_.gc_,2) - (idict_.url_ ? 0 : 1);
  #local s_ = concat("rows ", str(i_,0,0), ", columns ", str(j_,0,0));
  tabu__emitStr("table dimensions",s_)
  tabu__emitBool("transposed",idict_.transpose_)
  tabu__emitBool("column widths",defined(idict_.ucw_))
  tabu__emitBool("column align",idict_.ual_)
  tabu__emitBool("column labels",idict_.ucl_)
  tabu__emitBool("row labels",idict_.url_)
  tabu__emitBool("ledger style",idict_.ledger_)
  tabu__emitBool("highlight range",idict_.hilite_)
  tabu__emitBool("table borders",idict_.borders_)
  tabu__emitBool("emissive texture",idict_.emissive_)
  #local s_ = concat("radius ",str(idict_.lines_[0],0,4),
                    ", colour ",str(idict_.lines_[1],0,0));
  tabu__emitStr("grid lines",s_)
  #local s_ = concat("'",idict_.fonts_[0],"', '",
          idict_.fonts_[1],"', '",idict_.fonts_[2],"'");
  tabu__emitStr("fonts",s_)
  #debug concat(tabu__hyphen(72),"\n")
#end

/* -----[column type coding]------------------------------------------------- *
 * array 'ct_' elements {type, align, precision, Nvector}.
 * align -1 left, 0 centre, 1 right.
 * type 0 "S"   string, regular quoted.
 *      1 "B"   boolean, becomes 'yes'/'no'.
 *      2 "I"   integer, convert/display with 0 precision.
 *      3 "Fn"  float, convert to given precision, eg "F2" --> "n.nn",
 *              or use "G" for fixed six decimals.
 *      4 "Vcn" vector, where 2<=c<=5, and n as for float.
 */

#macro tabu__ctcSBIG(s_)
  #if ("S" = s_)
    #local el_ = array [4] {0, -1, 0, 0};
  #elseif ("B" = s_)
    #local el_ = array [4] {1, 0, 0, 0};
  #elseif ("I" = s_)
    #local el_ = array [4] {2, 1, 0, 0};
  #elseif ("G" = s_)
    #local el_ = array [4] {3, 1, 6, 0};
  #else
    #error concat("oops, unknown/bad column data type '",s_,"'.")
  #end
  el_
#end

#macro tabu__ctcFV(s_)
  #if ("F" = substr(s_,1,1))
    #local tt_ = 3;
    #local ta_ = 1;
    #local tn_ = 0;
    #local tp_ = val(substr(s_,2,strlen(s_)-1));
  #elseif ("V" = substr(s_,1,1))
    #local tt_ = 4;
    #local ta_ = -1;
    #local tn_ = val(substr(s_,2,1));
    #if (2 > tn_ | 5 < tn_)
      #error "oops, bad vector spec, expected 2,..,5 components."
    #end
    #local tp_ = 0;
    #if (2 < strlen(s_))
      #local tp_ = val(substr(s_,3,strlen(s_)-2));
    #end
  #else
    #error concat("oops, unknown/bad column data type '",s_,"'.")
  #end
  #if (0 > tp_ | 16 < tp_)
    #error "oops, (float) precision out-of-range (0,..16)."
  #end
  array [4] {tt_, ta_, tp_, tn_}
#end

#macro tabu__ctcmks(t_,v_)
  #switch (t_[0])
    #case (0) #local s_ = v_;                            #break
    #case (1) #local s_ = tabu__boolFmt(v_);             #break
    #case (2) #local s_ = str(v_,0,0);                   #break
    #case (3) #local s_ = str(v_,0,t_[2]);               #break
    #case (4) #local s_ = tabu__vectFmt(t_[3],v_,t_[2]); #break
    #else
      #error "oops, \"cannot happen\" error in 'ctcmks'."
  #end
  s_
#end

/* -----[mandatory dictionary keys]----------------------------------------- */

/* widen columns, if requested */
#macro tabu__setColWidths(d_)
  #if (defined(d_.ucw_))
    #for (i_,0,d_.ctn_-1)
      #local j_ = 1 + i_;
      #if (0 < d_.ucw_[i_])
        #if (d_.ucw_[i_] < d_.dcw_[j_])
          #error "oops, column(s) wider than given width(s)."
        #else
          #local d_.dcw_[j_] = d_.ucw_[i_];
        #end
      #end
    #end
  #end
#end

/* calc width + height requirements, incl. padding */
#macro tabu__rcDims(d_,nr_,nc_)
  #local d_.dcw_ = array [nc_];
  #for (i_,0,nc_-1) #local d_.dcw_[i_] = 0; #end
  #for (j_,0,nc_-1)
    #for (i_,0,nr_-1)
      #if (strlen(d_.gc_[i_][j_][3]))
        #if (d_.dcw_[j_] < d_.gc_[i_][j_][1])
          #local d_.dcw_[j_] = d_.gc_[i_][j_][1];
        #end
      #end
    #end
  #end
  #for (i_,0,nc_-1)
    #if (d_.dcw_[i_])
      #local d_.dcw_[i_] = d_.dcw_[i_] + 2 * idict_.ipad_[(i_ ? 1 : 0)].x;
    #end
  #end
  tabu__setColWidths(d_)
  #local d_.drh_ = array [nr_];
  #for (i_,0,nr_-1) #local d_.drh_[i_] = 0; #end
  #for (i_,0,nr_-1)
    #if (!i_)
      #if (d_.ucl_)
        #local d_.drh_[i_] = d_.glysz_[0][0].y
                + 2 * idict_.ipad_[(i_ ? 1 : 0)].y;
      #end
    #else
      #local d_.drh_[i_] = d_.glysz_[1][0].y + idict_.glysz_[1][1].y
              + 2 * idict_.ipad_[(i_ ? 1 : 0)].y;
    #end
  #end
#end

/* transpose table's rows + columns.
 * and label indicators, and highlight range.
 */
#macro tabu__swapGC(d_,nr_,nc_)
  #local ta_ = array [nc_][nr_];
  #for (j_, 0, nc_ - 1)
    #for (i_, 0, nr_ - 1)
      #local ta_[j_][i_] = d_.gc_[i_][j_];
    #end
  #end
  #undef d_.gc_
  #local d_.gc_ = ta_;
  #local tf_ = d_.ucl_;
  #local d_.ucl_ = d_.url_;
  #local d_.url_ = tf_;
  #if (defined(d_.hlarr_))
    #local d_.hlarr_[0] = <d_.hlarr_[0].y,d_.hlarr_[0].x>;
    #local d_.hlarr_[1] = <d_.hlarr_[1].y,d_.hlarr_[1].x>;
  #end
#end

/* net extents of scaled string (no Z) */
#macro tabu__calcSnet(s_,scl_,fnt_)
  #local t_ = text {ttf fnt_,s_,1,0 scale scl_};
  #local x_ = max_extent(t_) - min_extent(t_);
  #undef t_
  <tabu__roundup(x_.x),tabu__roundup(x_.y)>
#end

/* args: type, string, align, fg, bg, hilite */
#macro tabu__mkCell(typ_,s_,alg_,fg_,bg_,hl_)
  #local sz_ = tabu__calcSnet(s_,tabu__data_scales_[typ_],idict_.fonts_[typ_]);
  array mixed [8] {typ_,sz_.x,sz_.y,s_,alg_,fg_,bg_,hl_}
#end

/* update backgrounds, unless de-selected or table too small
 * (at least four data rows).
 */
#macro tabu__mkLedger(d_)
  #if (d_.ledger_ & 4 < dimension_size(d_.drh_,1))
    #for (i_,2,dimension_size(d_.drh_,1)-1,2)
      #for (j_,1,dimension_size(d_.dcw_,1)-1)
        #local d_.gc_[i_][j_][5] = d_.fg_[2];
        #local d_.gc_[i_][j_][6] = d_.bg_[2];
      #end
    #end
  #end
#end

/* transfer array data to cells */
#macro tabu__readArr(d_,nr_,nc_)
  #if (2 = d_.dtdims_)
    #if ((1 + dimension_size(dict_.DataTable,2)) != nc_)
      #error "oops, table's 2nd dimension does not match column count."
    #end
    #for (i_,1,nr_-1)
      #local ki_ = i_ - 1;
      #for (j_,1,nc_-1)
        #local kj_ = j_ - 1;
        #local s_ = tabu__ctcmks(d_.ct_[j_],dict_.DataTable[ki_][kj_]);
        #if (d_.ual_)
          #local al_ = dict_.ColAlign[kj_];
        #else
          #local al_ = d_.ct_[j_][1];
        #end
        #local d_.gc_[i_][j_] =
                tabu__mkCell(1,s_,al_,d_.fg_[1],d_.bg_[1],false);
      #end
    #end
  #elseif (2 = nc_)
    #for (i_,1,nr_-1)
      #local s_ = tabu__ctcmks(d_.ct_[1],dict_.DataTable[(i_ - 1)]);
      #if (d_.ual_)
        #local al_ = dict_.ColAlign[0];
      #else
        #local al_ = d_.ct_[1][1];
      #end
      #local d_.gc_[i_][1] =
              tabu__mkCell(1,s_,al_,d_.fg_[1],d_.bg_[1],false);
    #end
  #else
    #for (i_,0,nr_-2)
      #if ((1 + dimension_size(dict_.DataTable[i_],1)) != nc_)
        #error "oops, table's 2nd dimension does not match column count."
      #end
    #end
    #for (i_,1,nr_-1)
      #local ki_ = i_ - 1;
      #for (j_,1,nc_-1)
        #local kj_ = j_ - 1;
        #if (d_.ual_)
          #local al_ = dict_.ColAlign[kj_];
        #else
          #local al_ = d_.ct_[j_][1];
        #end
        #local s_ = tabu__ctcmks(d_.ct_[j_],dict_.DataTable[ki_][kj_]);
        #local d_.gc_[i_][j_] =
                tabu__mkCell(1,s_,al_,d_.fg_[1],d_.bg_[1],false);
      #end
    #end
  #end
#end

#macro tabu__dictMandatory(d_)
  /* initialise column types array, incl row label column */
  #local nc_ = 1 + d_.ctn_;
  #local d_.ct_ = array [nc_];
  /* row label column right-aligned */
  #local d_.ct_[0] = tabu__ctcSBIG("S");
  #local d_.ct_[0][1] = 1;
  #for (j_,1,nc_-1)
    #local i_ = j_ - 1;
    #local tn_ = strlen(dict_.DataColumns[i_]);
    #if (1 = tn_)
      #local d_.ct_[j_] = tabu__ctcSBIG(dict_.DataColumns[i_]);
    #elseif (1 < tn_ | 5 > tn_)
      #local d_.ct_[j_] = tabu__ctcFV(dict_.DataColumns[i_]);
    #else
      #error concat("oops, bad column type spec '",dict_.DataColumns[i_],"'.")
    #end
  #end
  /* set up grid of cells, incl column label row.
   * create row + column label cells, including empty.
   */
  #local nr_ = dimension_size(dict_.DataTable,1) + 1;
  #local d_.gc_ = array [nr_][nc_];

  #local d_.gc_[0][0] = tabu__mkCell(0,"",0,0,tabu__data_pigments_[3],false);

  #local tal_ = (d_.transpose_ ? 0 : 1);
  #for (j_,1,nr_-1)
    #local i_ = j_ - 1;
    #if (d_.url_)
      #local d_.gc_[j_][0] =
              tabu__mkCell(0,dict_.RowLabels[i_],tal_,
              d_.fg_[0],d_.bg_[0],false);
    #else
      #local d_.gc_[j_][0] = tabu__mkCell(0,"",0,0,d_.bg_[0],false);
    #end
  #end
  #local tal_ = (d_.transpose_ ? 1 : 0);
  #for (j_,1,nc_-1)
    #local i_ = j_ - 1;
    #if (d_.ucl_)
      #local d_.gc_[0][j_] =
              tabu__mkCell(0,dict_.ColLabels[i_],tal_,
              d_.fg_[0],d_.bg_[0],false);
    #else
      #local d_.gc_[0][j_] = tabu__mkCell(0,"",0,0,d_.bg_[0],false);
    #end
  #end

  /* transfer table data to cells, can be single column.
   * label default alignments change when transpose.
   */
  tabu__readArr(d_,nr_,nc_)
  #if (d_.transpose_)
    tabu__swapGC(d_,nr_,nc_)
    tabu__rcDims(d_,nc_,nr_)
  #else
    tabu__rcDims(d_,nr_,nc_)
  #end
  tabu__mkLedger(d_)
#end

/* -----[optional dictionary keys]------------------------------------------- *
 * check all optional keys, supply defaults where useful.
 */

#macro tabu__chkAlign()
  #for (i_,0,dimension_size(dict_.ColAlign,1)-1)
    #switch (dict_.ColAlign[i_])
      #case (-1)  /* fall-through, no-op */
      #case (0)
      #case (1)
        #break
      #else
        #error "oops, bad 'ColAlign' array."
    #end
  #end
#end

#macro tabu__chkCaption(d_)
  #if (1 != dimensions(dict_.Caption)
         | !(3 = dimension_size(dict_.Caption,1)
           | 4 = dimension_size(dict_.Caption,1)))
    #error "oops, bad 'Caption' array."
  #end
  #local sz_ = tabu__calcSnet(dict_.Caption[0],tabu__data_scales_[2],
          d_.fonts_[2]);
  #if (3 = dimension_size(dict_.Caption,1))
    #local fg_ = d_.lines_[1];
  #else
    #local fg_ = dict_.Caption[3];
  #end
  /* sx, sy, ss, alg, top, fg */
  #local d_.capt_ = array mixed [6]
      {sz_.x,sz_.y,dict_.Caption[0],dict_.Caption[1],dict_.Caption[2],fg_};
#end

/* column widths.
 * each in range 1,..,40 units, or -1 for don't care.
 */
#macro tabu__chkColWidths(d_)
  #if (d_.transpose_)
    #error "oops, cannot use 'ColWidths' and 'Transpose' together."
  #elseif (1 != dimensions(dict_.ColWidths))
    #error "oops, bad 'ColWidths' array."
  #elseif (dimension_size(dict_.ColWidths,1) != d_.ctn_)
    #error "oops, bad 'ColWidths' array."
  #end
  #local d_.ucw_ = array [d_.ctn_];
  #for (i_, 0, d_.ctn_ - 1)
    #local a_ = dict_.ColWidths[i_];
    #if (-1 != a_)
      #if (1 > a_ | 40 < a_)
        #error "oops, bad value(s) in 'ColWidths' array."
      #end
    #end
    #local d_.ucw_[i_] = a_;
  #end
#end

/* fonts + glyphs.
 * create 'fonts_' from input or default.
 * create 'glysz_', calculate widest and tallest glyph in range,
 * average width, and an offset (1/5 of tallest, cf descenders);
 * nb not asc(0), get negative values for some fonts.
 * store, per font, as 2-vectors:
 *   [n][0] - <xmax, ymax>.
 *      [1] - <xavg, yofs>.
 */
#macro tabu__glyphXY(fnt_,scl_)
  #local a_ = 0;
  #local x_ = 0;
  #local y_ = 0;
  #for (i_,1,255)
    #local t_ = text {ttf fnt_,chr(i_),.1,0 scale scl_};
    #local sz_ = max_extent(t_) - min_extent(t_);
    #undef t_
    #local a_ = a_ + sz_.x;
    #if (x_ < sz_.x) #local x_ = sz_.x; #end
    #if (y_ < sz_.y) #local y_ = sz_.y; #end
  #end
  (<tabu__roundup(x_),tabu__roundup(y_)>,
   <tabu__roundup(a_/255),tabu__roundup(.2*y_)>)
#end
#macro tabu__chkFonts(d_)
  #if (!defined(dict_.Fonts))
    #local d_.fonts_ = tabu__data_fonts_;
  #elseif (!(1 = dimensions(dict_.Fonts) & 3 = dimension_size(dict_.Fonts,1)))
    #error "oops, bad 'Fonts' array."
  #else
    #local d_.fonts_ = dict_.Fonts;
  #end
  #local d_.glysz_ = array [3][2];
  #for (i_,0,2)
    #local (d_.glysz_[i_][0], d_.glysz_[i_][1]) =
            tabu__glyphXY(d_.fonts_[i_],tabu__data_scales_[i_]);
  #end
#end

/* highlighting. hlarr_ {from, to, rgbf}
 * TODO test whether [1].y > #data rows.
 */
#macro tabu__chkHilite(d_)
  #if (1 != dimensions(dict_.Hilite)
         | !(3 = dimension_size(dict_.Hilite,1)
           | 2 = dimension_size(dict_.Hilite,1)))
    #error "oops, bad 'Hilite' array."
  #end
  #if (dict_.Hilite[0].x != int(dict_.Hilite[0].x)
     | dict_.Hilite[0].y != int(dict_.Hilite[0].y)
     | dict_.Hilite[1].x != int(dict_.Hilite[1].x)
     | dict_.Hilite[1].y != int(dict_.Hilite[1].y))
    #error "oops, bad highlight value(s), expect integers."
  #elseif (0 > dict_.Hilite[0].x | 0 > dict_.Hilite[0].y
     | dict_.Hilite[0].x > dict_.Hilite[1].x
     | dict_.Hilite[0].y > dict_.Hilite[1].y
     | dict_.Hilite[1].x > idict_.ctn_)
    #error "oops, bad highlight range value(s)."
  #elseif (!(dict_.Hilite[0].x + dict_.Hilite[0].y))
    #error "oops, cannot highlight non-existing cell '<0,0>'."
  #end
  #if (2 = dimension_size(dict_.Hilite,1))
    #local v_ = <.65,.65,.90,.975>;  /* blue-ish */
  #else
    #local v_ = dict_.Hilite[2];
  #end
  #local d_.hlarr_ = array mixed [3] {dict_.Hilite[0],dict_.Hilite[1],v_};
  #local d_.hilite_ = true;
#end

/* lines.  {radius, colour index}.
 * create 'lines_' from input or default.
 * TODO check radius for "reasonable" range?
 */
#macro tabu__chkLines(d_)
  #if (!defined(dict_.Lines))
    #local d_.lines_ = array [2] {.025,4};
  #elseif (!(1 = dimensions(dict_.Lines) & 2 = dimension_size(dict_.Lines,1)))
    #error "oops, bad 'Lines' array."
  #elseif (dict_.Lines[1] != int(dict_.Lines[1])
          | 0 > dict_.Lines[1] | 7 < dict_.Lines[1])
    #error "oops, bad 'Lines' colour index."
  #else
    #local d_.lines_ = dict_.Lines;
  #end
#end

/* logo, if already sourced */
#macro tabu__chkLogo(d_,f_)
  #if (defined(Povray_Logo_Prism))
    #local d_.logo_ = f_;
  #else
    #local d_.logo_ = off;
  #end
#end

/* padding
 * create 'ipad_' as % of widest/tallest glyph in respective font.
 */
#macro tabu__chkPadding(d_,v_)
  #if (.01 > v_.x | .01 > v_.y | .99 < v_.x | .99 < v_.y)
    #error "oops, bad 'Padding' value(s)."
  #end
  #local d_.ipad_ = array [3];
  #for (i_,0,2)
    #local d_.ipad_[i_] =
            <tabu__roundup(v_.x * d_.glysz_[i_][0].x),
             tabu__roundup(v_.y * d_.glysz_[i_][0].y)>;
  #end
#end

/* order of checks matters in a few cases */
#macro tabu__dictOptional(ud_,d_)
  /* .Fonts */
  tabu__chkFonts(d_)
  /* .Padding */
  #if (defined(ud_.Padding))
    tabu__chkPadding(d_,ud_.Padding)
  #else
    tabu__chkPadding(d_,<.35,.2>)
  #end
  /* .Transpose */
  #if (defined(ud_.Transpose))
    #local d_.transpose_ = ud_.Transpose;
  #else
    #local d_.transpose_ = off;
  #end
  /* .ColWidths */
  #if (defined(ud_.ColWidths))
    tabu__chkColWidths(d_)
  #end
  /* .ColLabels */
  #if (!defined(ud_.ColLabels))
    #local d_.ucl_ = false;
  #elseif (1 != dimensions(ud_.ColLabels))
    #error "oops, bad 'ColLabels' array."
  #elseif (dimension_size(ud_.ColLabels,1) != d_.ctn_)
    #error "oops, bad 'ColLabels' array."
  #else
    #local d_.ucl_ = true;
  #end
  /* .RowLabels */
  #if (!defined(ud_.RowLabels))
    #local d_.url_ = false;
  #elseif (1 != dimensions(ud_.RowLabels))
    #error "oops, bad 'RowLabels' array."
  #elseif (dimension_size(ud_.RowLabels,1) != dimension_size(ud_.DataTable,1))
    #error "oops, bad 'RowLabels' array."
  #else
    #local d_.url_ = true;
  #end
  /* .ColAlign */
  #if (!defined(ud_.ColAlign))
    #local d_.ual_ = false;
  #elseif (1 != dimensions(ud_.ColAlign))
    #error "oops, bad 'ColAlign' array."
  #elseif (dimension_size(ud_.ColAlign,1) != d_.ctn_)
    #error "oops, bad 'ColAlign' array."
  #else
    tabu__chkAlign()
    #local d_.ual_ = true;
  #end
  /* .Fg */
  #if (!defined(ud_.Fg))
    #local d_.fg_ = array [3] {4, 0, 0};
  #elseif (!(1 = dimensions(ud_.Fg) & 3 = dimension_size(ud_.Fg,1)))
    #error "oops, bad 'Fg' colour index array."
  #elseif (ud_.Fg[0] != int(ud_.Fg[0]) | 0 > ud_.Fg[0] | 7 < ud_.Fg[0])
    #error "oops, bad 'Fg[0]' colour index."
  #elseif (ud_.Fg[1] != int(ud_.Fg[1]) | 0 > ud_.Fg[1] | 7 < ud_.Fg[1])
    #error "oops, bad 'Fg[1]' colour index."
  #elseif (ud_.Fg[2] != int(ud_.Fg[2]) | 0 > ud_.Fg[2] | 7 < ud_.Fg[2])
    #error "oops, bad 'Fg[2]' colour index."
  #else
    #local d_.fg_ = ud_.Fg;
  #end
  /* .Bg */
  #if (!defined(ud_.Bg))
    #local d_.bg_ = array [3] {tabu__data_pigments_[0],
                               tabu__data_pigments_[1],
                               tabu__data_pigments_[2]};
  #elseif (!(1 = dimensions(ud_.Bg) & 3 = dimension_size(ud_.Bg,1)))
    #error "oops, bad 'Bg' pigment array."
  #else
    #local d_.bg_ = ud_.Bg;
  #end
  /* .Borders */
  #if (defined(ud_.Borders))
    #local d_.borders_ = ud_.Borders;
  #else
    #local d_.borders_ = on;
  #end
  /* .Lines */
  tabu__chkLines(d_)
  /* .UseBool */
  #if (!defined(ud_.UseBool))
    #local d_.bool_ = array [2] {"no","yes"};
  #elseif (!(1 = dimensions(ud_.UseBool) & 2 = dimension_size(ud_.UseBool,1)))
    #error "oops, bad 'UseBool' array."
  #else
    #local d_.bool_ = ud_.UseBool;
  #end
  /* .Caption */
  #if (defined(ud_.Caption))
    tabu__chkCaption(d_)
  #end
  /* .Logo */
  #if (defined(ud_.Logo))
    tabu__chkLogo(d_,ud_.Logo)
  #else
    tabu__chkLogo(d_,on)
  #end
  /* .Emissive */
  #if (defined(ud_.Emissive))
    #local d_.emissive_ = ud_.Emissive;
  #else
    #local d_.emissive_ = off;
  #end
  /* .Finishes */
  #if (!defined(ud_.Finishes))
    #local d_.finishes_ = tabu__data_finishes_;
  #elseif (!(1 = dimensions(ud_.Finishes) & 2 = dimension_size(ud_.Finishes,1)))
    #error "oops, bad 'Finishes' array."
  #else
    #local d_.finishes_ = ud_.Finishes;
  #end
  /* .Ledger */
  #if (!defined(ud_.Ledger))
    #local d_.ledger_ = off;
  #else
    #local d_.ledger_ = ud_.Ledger;
  #end
  /* .Hilite */
  #if (!defined(ud_.Hilite))
    #local d_.hilite_ = off;
  #else
    tabu__chkHilite(d_)
  #end
  /* TODO annotation */
  #if (defined(ud_.Note))
    #warning "Tabulated() - the '.Note' feature remains T.B.I."
  #end
  /* .Verbose */
  #if (defined(ud_.Verbose))
    #local d_.verbose_ = ud_.Verbose;
  #else
    #local d_.verbose_ = off;
  #end
#end

/* ------------------------------------------------------------------------- */
/* draw table.
 */

/* remx_ goes negative when caption wider than table */
// TODO can net height check ever fail?  remove?
#macro tabu__calcAlign(n_,x_,y_,a_,w_,h_)
  #local remx_ = w_ - x_;
  #if (0 > remx_) #error "oops, grid cell too narrow." #end
  #if (0 > (h_ - y_)) #error "oops, grid cell too low." #end
  #local j_ = idict_.ipad_[n_].y + idict_.glysz_[n_][1].y;
  #switch (a_)
    #case (-1) #local i_ = idict_.ipad_[n_].x;          #break
    #case (0)  #local i_ = remx_ / 2;                   #break
    #case (1)  #local i_ = remx_ - idict_.ipad_[n_].x;  #break
    #else
      #error "oops, \"cannot happen\" error in 'calcAlign'."
  #end
  <i_,j_>
#end

/* highlight box corner point(s) */
#macro tabu__calcPt(v_,f_)
  #local x_ = 0;
  #for (i_,0,v_.x-1)
    #local x_ = x_ + idict_.dcw_[i_];
  #end
  #if (f_) #local x_ = x_ + idict_.dcw_[i_]; #end
  #local y_ = 0;
  #for (i_,0,v_.y-1)
    #local y_ = y_ + idict_.drh_[i_];
  #end
  #if (f_) #local y_ = y_ + idict_.drh_[i_]; #end
  #local z_ = idict_.lines_[0] + tabu__eps_;
  #if (!f_) #local z_ = z_ + tabu__eps_; #end
  <x_,-y_,-z_>
#end

// TODO y offset top?
#macro tabu__mkCaption(tw_,th_)
  #local tmp_ =
    tabu__calcAlign(2,idict_.capt_[0],idict_.capt_[1],idict_.capt_[3],tw_,th_);
  #if (idict_.capt_[4])
    #local y_ = .15 +  tmp_.y;
  #else
    #local y_ = -(tmp_.y + idict_.glysz_[2][0].y + th_);
  #end
  text {
    ttf idict_.fonts_[2], idict_.capt_[2], .1, 0
    scale tabu__data_scales_[2]
    translate <tmp_.x,y_,0>
    texture {
      pigment {color rgb tabu__data_colours_[idict_.capt_[5]]}
      finish {tabu__mkFinish()}
    }
  }
#end

#macro tabu__mkCorner(pt_)
  sphere {
    pt_, (idict_.lines_[0] + tabu__eps_)
    texture {
      pigment {color rgb tabu__data_colours_[idict_.lines_[1]]}
      finish {tabu__mkFinish()}
    }
  }
#end

#macro tabu__mkFinish()
  idict_.finishes_[(idict_.emissive_ ? 1 : 0)]
#end

#macro tabu__mkHL(pt1_,pt2_,rgbf_)
  box {
    pt1_, pt2_
    hollow
    pigment {color rgbf rgbf_}
    /* cf 'glass_old.inc:F_Glass4' */
    finish {
      ambient .1
      diffuse .1
      reflection .25
      specular 1
      roughness tabu__eps_
    }
  }
#end

#macro tabu__mkLogo(x_,y_,w_,h_)
  object {
    Povray_Logo_Prism
    texture {
      pigment {
        gradient y
        color_map {
          [ 0, color rgb <0,1,0>]
          [.5  color rgb <1,0,0>]
          [ 1  color rgb <1,1,0>]
        }
        scale 2.5
      }
      normal {dents .0005}
      finish {tabu__mkFinish()}
    }
    scale .45
    translate <(x_ + (w_/2)),(y_ + (h_/1.65)),0>
  }
#end

#macro tabu__mkGrid(nh_,nv_,lenh_,lenv_)
  #local zo_ = (idict_.lines_[0] / 2);
  #local cylv_ = array [nv_][2];
  #local cylv_[0][0] = <0,0,zo_>;
  #local cylv_[0][1] = <0,-lenv_,zo_>;
  #local curr_ = 0;
  #for (i_,1,nv_-1)
    #local j_ = i_ - 1;
    #local curr_ = curr_ + idict_.dcw_[j_];
    #local cylv_[i_][0] = <curr_,0,zo_>;
    #local cylv_[i_][1] = <curr_,-lenv_,zo_>;
  #end
  #local cylh_ = array [nh_][2];
  #local cylh_[0][0] = <0,0,zo_>;
  #local cylh_[0][1] = <lenh_,0,zo_>;
  #local curr_ = 0;
  #for (i_,1,nh_-1)
    #local j_ = i_ - 1;
    #local curr_ = curr_ + idict_.drh_[j_];
    #local cylh_[i_][0] = <0,-curr_,zo_>;
    #local cylh_[i_][1] = <lenh_,-curr_,zo_>;
  #end
  /* no frame for gc_[0][0] */
  #if (idict_.ucl_ & idict_.url_)
    #local cylh_[0][0] = cylv_[1][0];
    #local cylv_[0][0] = cylh_[1][0];
  #end
  #for (i_,0,nh_-1)
    cylinder {
      cylh_[i_][0], cylh_[i_][1], idict_.lines_[0]
      texture {
        pigment {color rgb tabu__data_colours_[idict_.lines_[1]]}
        finish {tabu__mkFinish()}
      }
    }
  #end
  #for (i_,0,nv_-1)
    cylinder {
      cylv_[i_][0], cylv_[i_][1], idict_.lines_[0]
      texture {
        pigment {color rgb tabu__data_colours_[idict_.lines_[1]]}
        finish {tabu__mkFinish()}
      }
    }
  #end
  tabu__mkCorner(cylh_[0][0])
  tabu__mkCorner(cylh_[0][1])
  tabu__mkCorner(cylh_[nh_-1][0])
  tabu__mkCorner(cylh_[nh_-1][1])
  #if (idict_.ucl_ & idict_.url_)
    tabu__mkCorner(cylv_[0][0])
  #end
#end

#macro tabu__gridLines()
  #local nv_ = dimension_size(idict_.dcw_,1);
  #local lenh_ = 0;
  #for (i_,0,nv_-1) #local lenh_ = lenh_ + idict_.dcw_[i_]; #end
  #local nv_ = nv_ + 1;
  #local nh_ = dimension_size(idict_.drh_,1);
  #local lenv_ = 0;
  #for (i_,0,nh_-1) #local lenv_ = lenv_ + idict_.drh_[i_]; #end
  #local nh_ = nh_ + 1;

  #if (defined(idict_.capt_))
    tabu__mkCaption(lenh_,lenv_)
  #end

  #if (!idict_.borders_) #break #end

  tabu__mkGrid(nh_,nv_,lenh_,lenv_)
#end

/* ------------------------------------------------------------------------- */

#macro tabu__mkGcell(i_,j_,blx_,bly_,w_,h_)
  #local zo_  = -.005;
  #local clb_ = <blx_, bly_, 0> + tabu__eps_;
  #local crt_ = <(blx_ + w_), (bly_ + h_), .1> - tabu__eps_;
  #if (strlen(idict_.gc_[i_][j_][3]))
    #local at_ = tabu__calcAlign(idict_.gc_[i_][j_][0],
                                 idict_.gc_[i_][j_][1],
                                 idict_.gc_[i_][j_][2],
                                 idict_.gc_[i_][j_][4],
                                  w_,h_);
    union {
      text {
        ttf idict_.fonts_[idict_.gc_[i_][j_][0]],
        idict_.gc_[i_][j_][3], .1, 0
        scale tabu__data_scales_[idict_.gc_[i_][j_][0]]
        translate <(blx_ + at_.x),(bly_ + at_.y),zo_>
        texture {
          pigment {color rgb tabu__data_colours_[idict_.gc_[i_][j_][5]]}
          finish {tabu__mkFinish()}
        }
      }
      box {
        clb_, crt_
        hollow
        texture {
          pigment {idict_.gc_[i_][j_][6]}
          finish {tabu__mkFinish()}
        }
      }
    }
  #else
    box {
      clb_, crt_
      hollow
      pigment {idict_.gc_[i_][j_][6]}
    }
  #end
#end

/* make cells, place logo in gc_[0][0] space and highlight */
#macro tabu__gridCells()
  #local curw_ = 0;
  #for (j_,0,dimension_size(idict_.dcw_,1)-1)
    #local curh_ = 0;
    #local w_ = idict_.dcw_[j_];
    #if (w_)
      #for (i_,0,dimension_size(idict_.drh_,1)-1)
        #local h_ = idict_.drh_[i_];
        #if (h_)
          #local curh_ = curh_ + h_;
          #if (!(i_ + j_))
            #if (idict_.logo_)
              tabu__mkLogo(curw_,-curh_,w_,h_)
            #end
          #end
          tabu__mkGcell(i_,j_,curw_,-curh_,w_,h_)
        #end
      #end
    #end
    #local curw_ = curw_ + w_;
  #end
  #if (idict_.hilite_)
    tabu__mkHL(tabu__calcPt(idict_.hlarr_[0],false),
               tabu__calcPt(idict_.hlarr_[1],true),
               idict_.hlarr_[2])
  #end
#end


/* ------------------------------------------------------------------------- */
/* the "public interface".
 * check and pre-process arguments, adding defaults as required,
 * provide feedback when requested, build a table, as a 'union {}'
 * of the "lines" and the "cells" representing the data, adjust
 * position when top caption, get/set extents.
 */

#macro Tabulated(dict_)

  #if (defined(Tabulated_min_extent)) #undef Tabulated_min_extent #end
  #if (defined(Tabulated_max_extent)) #undef Tabulated_max_extent #end

  #if (!(defined(dict_.DataColumns) & defined(dict_.DataTable)))
    #error "oops, missing mandatory key(s)."
  #end

  #local idict_ = dictionary {.dtdims_: dimensions(dict_.DataTable)};
  #if (!(2 = idict_.dtdims_ | 1 = idict_.dtdims_))
    #error "oops, require 2D (or single column) data table."
  #end

  #if (1 != dimensions(dict_.DataColumns))
    #error "oops, bad 'DataColumns' array."
  #end

  #local idict_.ctn_ = dimension_size(dict_.DataColumns,1);
  #if (!idict_.ctn_ | 20 < idict_.ctn_)
    #error "oops, require one or more (to twenty) data columns."
  #end

  tabu__dictOptional(dict_,idict_)

  tabu__dictMandatory(idict_)

  #if (idict_.verbose_)
    tabu__emitHeader()
  #end

  #local tabu_tbl_ = union {
    tabu__gridLines()
    tabu__gridCells()
    #if (defined(idict_.capt_))
      #if (idict_.capt_[4])
        translate <0,-1,0>
      #end
    #end
    no_shadow
  };

  #declare Tabulated_min_extent = min_extent(tabu_tbl_);
  #declare Tabulated_max_extent = max_extent(tabu_tbl_);

  tabu_tbl_

#end

#version tabu_temp_include;

#end

/* -------------------------------------------------------------------- *
  * the content above is covered by the GNU General Public License v3+ * 
  * copyright (c) 2021 jr <creature.eternal@gmail.com>.                * 
  * all rights reserved.                                               * 
 * -------------------------------------------------------------------- */
