
! https://graphviz.org/Gallery/directed/git.html

! (note) https://www.w3schools.com/graphics/svg_intro.asp
! (note) https://www.w3schools.com/graphics/svg_path.asp
! (note) https://www.w3.org/TR/SVG2/paths.html

! combobulate [To compose (one's self); to compose, organize, design, or arrange; to reverse the effect of discombobulation]

* TODO directed graph toolkit / markup language
   ! 26Feb2024

* IDEA based on tkui layout engine + cascading style sheets
   ! 26Feb2024
   - ';' or '\n' terminate current directive

style myncbase {
  fgcolor: #fff
  bgcolor: #234
  shape: rect
}

style mync1 {
  myncbase  // inherit attributes from myncbase class
  fgcolor: #f00
  sgx: hgrp1  // horizontal size group
}

style mync2 {
  myncbase  // inherit attributes from myncbase class
  fgcolor: #0f0
}

style mync3 {
  myncbase  // inherit attributes from myncbase class
  bgcolor: #123
}

size="1024;1024"  // logical canvas size

graph main {  // optional?

  flow=lr                    // layout from left to right
  style class=mync1          // set default node class
  a                          // inherit style attributes from mync1
  a2 "Label A2" class=mync1  // inherit style attributes from mync1
  b                          // inherit style attributes from mync1
  //c#mync2 "Long label"
  c "Long label" class=mync2
  d {                        // node with extended (possibly multi-line) attributes
    class=mync3  // set
    ports {
      freq:e  // port "freq" on east side
      pan:e   // port "pan" on east side
      pd:n    // port "pd" on north side
      pm:n    // port "pm" on north side
      outl:w  // port "outl" on west side
      outr:w  // port "outr" on west side
    }
  }
  graph sub1 {
    flow=tb           /* layout from top to bottom */
    nodeclass=mync2
    a; b              // two nodes declared in one line
    a->b class=myec1  /* from a to b with solid line */
  }
  // node connections
  b.>a                /* from b to a with dotted line */
  a->sub1             // connect (global) node a to(wards) subgraph (center)
  c->sub1.b           // connect (global) node c to subgraph node b
  a2:e->d:freq        // connect east-side of a2 to (east-side) "freq" port of d
}

* NOTE graph, node, edge attributes
   ! 26Feb2024
   ! C:03Mar2024
   ! (note) !! redeclaring a node updates its style class and, if followed by [] or (), its GR / XFM attributes !!
   + common:
      + c32   tint                     colorize (affects all colours). alpha=tint amount
   + graph:
      + int   col_spacing              initial horizontal cell spacing [def=5]
      + int   row_spacing              initial vertical cell spacing [def=5]
      + int   cell_spacing             sets both col and row spacing [def=5]
      x int   node_spacing             <removed>
      + bool  rotate                   true=rotate graph (swap x / y)
      + bool  flip_x                   true=flip graph horizontally
      + bool  flip_y                   true=flip graph horizontally
      + bool  flip_x_opt               true=try to optimize space by flipping subgraphs horizontally
      + bool  flip_y_opt               true=try to optimize space by flipping subgraphs vertically
      + bool  cell_slant               true= set cell shift x to cell y (e.g. test17.gr) [def=0]
      + bool  slant                    true= set layer shift x to cell y * slant_factor  [def=false]
      + int   slant_factor             pixel shift multiplier [def=16(px)]
      x int   pad_col                  <removed>
      + int   col_padding              horizontal column padding (in pixel layout pass) [def=16]
      x int   pad_row                  <removed>
      + int   row_padding              vertical row padding (in pixel layout pass) [def=12]
      + bool  zero_margin              auto-zero padding at graph borders [def=true]
      + bool  flatten                  (main graph) true=flatten layer hierarchy (but break event propagation) false=keep nested layers and don't add labels [def=true]
      + id    spacer_class             set default style class for '#' helper nodes in fixed layouts (def=none)
      + id    palette                  set currently actively default palette. palette=default sets palette 'default' or first available palette.
      + id    style                    set currently actively default style. style=default sets style 'default' or first available style.
   + graph/edge common
      + bool  xmajor                   force xmajor layouting (prefer left/right side connection instead of top/bottom) [def=auto (xmajor=(w>h)]
      + bool  xmajor_tail              force xmajor layouting for tail connection (prefer left/right side instead of top/bottom) [def=maybe(revert to 'xmajor')]
      + bool  xmajor_head              force xmajor layouting for head connection (prefer left/right side instead of top/bottom) [def=maybe(revert to 'xmajor')]
   + graph/node common
      + c24   bgcolor                  palette color name or #rgb or #rrggbb  [def graph bg=#f9f9f9]
      + c32   bgtint                   alpha=tint amount
      + bool  bgfill   NOINHERIT       0=transparent 1=solid
      + id    bg_pattern               checker_1|dot_[1..4]|lines_[1..4]|stars_1|zigzag_[1..4] [def=checker_1]
      + amt   bg_pattern_intensity     [def=15%]
      + id    bg_gradient              gradient id
      + int   bg_gradient_shape        h|v|r  [def=v]
      + float pad      NOINHERIT       (margin) (setPadding4f()) (scalar=apply to t/l/b/r, ';' separated value list otherwise)  "auto" = auto-zero padding at graph borders
      + float pad_t    NOINHERIT       (top    margin) (setPadTop())
      + float pad_l    NOINHERIT       (left   margin) (setPadLeft())
      + float pad_b    NOINHERIT       (bottom margin) (setPadBottom())
      + float pad_r    NOINHERIT       (right  margin) (setPadRight())
      + float pad_v                    virtual attribute: sets both pad_t and pad_b
      + float pad_h                    virtual attribute: sets both pad_l and pad_r
      + float ipad     NOINHERIT       (inner margin / distance from content to border) (scalar=apply to t/l/b/r, ';' separated value list otherwise)
      + float ipad_t   NOINHERIT       (top    inner margin)
      + float ipad_l   NOINHERIT       (left   inner margin)
      + float ipad_b   NOINHERIT       (bottom inner margin)
      + float ipad_r   NOINHERIT       (right  inner margin)
      + float ipad_v                   virtual attribute: sets both ipad_t and ipad_b
      + float ipad_h                   virtual attribute: sets both ipad_l and ipad_r
      + csv   align                    (alignment) (top|bottom|left|right|center|centerx|centery|expand|expandx|expandy|baseliney|baseline)
                                         ! (note) when set, panel inherits alignment. default alignment for nodes is expandx|baseliney and top|left for sub-graphs
                                         ! (note) "baseline" combines centery|baseliney (commonly used shortcut)
      + int   shape                    ((todo)none|rect|round|(todo)ellipse|(todo)circle|(todo)oval?|(todo)triangle|rhomb) [def=rect]
      + amt   shape_round_amount       (0..100%) [def=12.5%]
      + float shape_round_aspect       [def=16/10]
      + float shape_round_limit        [def=12]
      + float shape_rhomb_tx           [def=12]
      + float shape_rhomb_ty           [def=10]
      ? float shape_slant_x
      ? bool  shape_flip_x
      ? bool  shape_flip_y
      + int   border_shape  NOINHERIT  (rect|round) [def=rect]
      o int   border_style  NOINHERIT  (none|solid|dashed|dotted)  [def=none]
      + float border_width  NOINHERIT  [def=0.5]
      + c24   border_color  NOINHERIT  #rgb or #rrggbb color [def=#000]
      + c32   border_tint
      + int   border_dir    NOINHERIT  (top|left|bottom|right|all)  [def=all]
      + int   border_blend             (default/off/none|srcover/normal/1/true|additive/add) [def=off]
      + byte  border_alpha             (border alpha, when "border_blend" != off)
      + id    border_pattern           checker_1|dot_[1..4]|lines_[1..4]|stars_1|zigzag_[1..4] [def=checker_1]
      + amt   border_pattern_intensity [def=15%]
      + id    border_gradient          gradient id
      + int   border_gradient_shape    h|v|r  [def=v]
      + id    sgx           NOINHERIT  SizeGroupX assignment
      + id    sgy           NOINHERIT  SizeGroupY assignment
      + id    sgxy          NOINHERIT  SizeGroupXY assignment
   + node/edge common (fallback):
      + c24   fgcolor                  palette color name or #rgb or #rrggbb
      + c32   fgtint                   [def=0]
      + bool  helper                   (for manually adding edge helper nodes)
      + text  tooltip                  tooltip caption
      + text  label                    (alternative caption declaration)
   + node:
      + c24   node_fgcolor             palette color name or #rgb or #rrggbb
      + c32   node_fgtint              [def=0]
      + int   text_pos                 (top|left|bottom|right|center|centerx|centery|baseliney) (setTextPlacement())
      o str   rotate                   rotate content (text+icon) (cw|90|true|1|ccw|-90|-1) [def=0]
      o file  icon                     (todo) lazy-add local icon font resource
      + int   icon_pos                 (top|left|bottom|right|center|centerx|centery) (setIconPlacement())
      + float icon_pad                 (setIconPadding4f()) (scalar=apply to t/l/b/r, ';' separated value list otherwise)
      + float icon_pad_t               (setIconPadTop())
      + float icon_pad_l               (setIconPadLeft())
      + float icon_pad_b               (setIconPadBottom())
      + float icon_pad_r               (setIconPadRight())
      + float w                        forced width (absolute px or layout weight in %)
      + float h                        forced height
      + float s                        forced size (w;h or w,h) (absolute px or layout weights in %) (virtual attrib, sets both "w" and "h")
      + int   sw                       cell span width (when pixel_layout=grid)
      + int   sh                       cell span height (when pixel_layout=grid)
      + amt   headpoint_amount         percentage of width / height used for distributing (multiple) edge connections to head node [def=12.5%]
      + float headpoint_mindist        minimum pixel distance between edge connections to head node [def=8]
   + edge:
      + c24   edge_fgcolor             palette color name or #rgb or #rrggbb
      + c32   edge_fgtint              [def=0]
      + int   edge_shape               [none|line|curve|rcurve]  (rcurve=right-angled curve)
      + amt   edge_bend/bend           [0..1 or 0%..100%]
      + float edge_width               [def=0.5 / 1.5(bold/>>/<<)]
      + float edge_width_start         [def=use edge_width]
      + float edge_width_end           [def=use edge_width]
      + float edge_width_exp           variable line width bias towards head or tail (-f..+f) [def=1.0)
      + float edge_width_left
      + float edge_width_right
      + bool  edge_width_adaptive      true=use edge_width as reference width and scale left/right edge widths [def=false]
      + int   edge_fgcolor_left        [def=use edge_fgcolor]
      + int   edge_fgtint_left         [def=use edge_fgtint]
      + int   edge_fgcolor_right       [def=use edge_fgcolor]
      + int   edge_fgtint_right        [def=use edge_fgtint]
      - byte  edge_alpha               [def=255]
      - str   edge_blend               off|srcover|additive|srccolor|dstcolor [def=srcover]  (todo) usecase ?
      + float tail_snap_x              snap tail position to edge x within this threshold [def=8(px)]
      + float tail_snap_y              snap tail position to edge y within this threshold [def=5(px)]
      + id    label_class              label style class
      x int   label_pos                (top|left|bottom|right|center|centerx|centery)  (position of label relative to edge) (TODO) REMOVE, handled by "align" and "pad*"
      + amt   label_relpos             relative position along edge (0%=tail, 100%=head)
      + c32   label_fgcolor
      + c32   label_fgtint             [def=0]
      + int   label_style              [none|default]
      + c24   arrow_fgcolor            palette color name or #rgb or #rrggbb
      + c24   arrow_fgcolor_rev
      + c32   arrow_fgtint             [def=0]
      + c32   arrow_fgtint_rev         [def=0]
      + int   arrow_style              [solid|line]
      + int   arrow_style_rev          [solid|line]
      + int   arrow_shape              [none|tri]
      - int   arrow_shape_rev          [none|tri]
      + float arrow_linewidth          [def=1]
      + float arrow_linewidth_rev      [def=1]
      + float arrow_angle              [def=35(deg)]
      + float arrow_angle_rev          [def=35(deg)]
      + float arrow_len                [def=6(px)]
      + float arrow_len_rev            [def=6(px)]
      + amt   arrow_ilen               [def=0%]
      + amt   arrow_ilen_rev           [def=0%]
      + amt   arrow_snap               [0..1 or 0%..100%] (snap angle amount)   (test23b.gr, test24.gr)  [def=0%]
      + amt   arrow_snap_rev           [0..1 or 0%..100%] (snap angle amount)   (test23b.gr, test24.gr)  [def=0%]
      + c24   arrow_border_color       palette color name or #rgb or #rrggbb
      + c32   arrow_border_color_rev
      + float arrow_border_width       [def=0]
      + float arrow_border_width_rev   [def=0]
   + ports
      + amt   margin                   percentage of edge that is _not_ used for ports [def=0%,0%,0%,0%]

* NOTE alg
   ! 27Feb2024
   ! Sugiyama (Fujitsu, JAIST) (book "Graph drawing and applications for software and knowledge engineers" 1992?)
      ! https://en.wikipedia.org/wiki/Kozo_Sugiyama
   - identify cyclic edges
   o test setup: add nodes
       - order of declaration determines priority ?
   o test setup: add edges
   - test setup: add ranks (layout constraints between nodes)
   + iterate (non-cyclic) edges and create initial grid layout
      + from left to right  (later: top-to-bottom, right-to-left, bottom-to-top)
   o loop: remove node<>edge collisions, add helper nodes+edges so that nodes always connect to their direct neighbours
      o in case of collision, insert helper node (+edges) below previous gridx cell
         o move head node down to helper node y
            ! not necessarily => (integer) weights (later)
   o loop: minimize node distances
      o iterate grid rows
         o move grid nodes to left-hand side empty cells, eliminate helper nodes+edges if possible
            - apply rank constraints
      o iterate grid columns
         o move grid nodes to top side empty cells, eliminate helper nodes+edges if possible
            - apply rank constraints

* TEST sub-graphs
   ! 28Feb2024
   ! C:29Feb2024
   ! C:01Mar2024
   + layout independently
   + insert into parent graph
   ! must have single (or at least a "main") "i/o" node which determines where the sub-graph is connected
      + setAnchor(), getAnchor()
   + flip sub-graph about anchor node when subgraph.b_flip_x=true
      + test with anchor != (0,0)
   x collapseEmptyCellsH: collapse towards right when edge points to the right
      + OR: optimize in final pass ? (?)
         + collapseGapsBetweenEdgeNodesH()

* DONE node ports
   ! 28Feb2024
   ! C:27Mar2024
   ! C:28Mar2024
   + ports { [margin=50%] portid : t|l|b|r [relpos] }
      + or n|w|s|e
   + edge target port, e.g. >>a->b:portid<<
   + edge dir fallback, e.g. >>a->b:e<<
   + autoAssignPortPositions()
   + applyPortMargins()
   + PortBase class

* ACTV dot-like graph parser
   ! 28Feb2024
   ! C:01Mar2024
   ! C:02Mar2024
   ! C:03Mar2024
   + cascading style sheets / classes
   + nodes
      + labels
         ! a "my node a"
   - node ports
   + graphs and subgraphs
   + edges
      + implicit node declaration
      + labels
         ! a->b "a to b"
   + scanner/parser
      + iterate lines
      + tokenize
      + (continue) parsing next tokens
   + keywords
      + "style"
         + outside of graph: style declaration
         + inside graph: set default style for next nodes
      + "graph"
         + outside of graph: begin main graph
         + inside graph: add subgraph
   + "[..]" defines attribute hashtable for last object
   + "/*..*/" starts/ends inline comment
   + "//" starts comment until end of line
   + file suffix: ".gr"
   + convert test cases to .gr files
   + sub graph short form
      ! a { 1->2 }
      ! a->b { 1->2 }
      + replace prev_node by sub graph
   + C-syntax-highlighter compatible
   + normal edges: -> and <-
   + dotted edges: .> and <.
   + dashed edges: => and <=
   + bold edges: >> and <<
   + invisible/hidden edges: ~> and <~
   + undirected edges: --
   + bidirectional edges: <>
   + node_spacing / cell_spacing
      + set col_spacing and row_spacing
   + col_spacing
   + row_spacing
   + line break directive
      + during initial layout
      + move curY forward by <row_spacing> after adding node that has line break flag attached to it
      + line break ('+') must occur on same source line as prev_node declaration
         ! e.g. >>a;b;c;d +<<
         + CAUTION: '+' must occur before ';' (since ';' resets prev_node)
      + see test26.gr

* IGNR svg output v1
   ! 28Feb2024
   ! C:02Mar2024
   ! preliminary
   - simply emit fixed size grid
   x places edges on layer below nodes. connect node centers.
   o pick edge connection side (n,e,w,s) depending on grid positions
      ! similar to debugPrint() / drawEdge()
   ! ==> skipping straight to svg output v2

* ACTV tkui integration
   ! 28Feb2024
   ! C:02Mar2024
   ! C:03Mar2024
   o convert layouted grid to BorderLayout
      ? support custom-classes via node attributes (similar to XFM)
      - apply node distance / margin / padding attributes
      o import Edges (GraphEdge)
         ! (note) glLineStipple() does not work on macOS (lines disappear)
   o layout grid contents (labels, icons, ..)
   + (optionally) flatten BorderLayout to freely movable (floating) layers
   o tesselate spline edges
      - evaluate exact port connections
         ! first pass layouter only considers node connections
   o eliminate helper nodes while tesselating edge splines
      ! e.g. test13.gr
      - when edge connects to helper node which uniquely connects to other node

* ACTV svg output v2
   ! 28Feb2024
   ! C:18Mar2024
   ! C:19Mar2024
   ! requires tkui integration
   + convert tkui Layer hierarchy to SVG
   + fix "double border" issue
      + e.g. test18d.gr
         + don't emit panel border in label pass
      + test16.gr
         ! "Node C" looks ok, "Node A" has double border (even though both are using the same style class)
   - icons
   + dotted/dashed edges
   + dotted/dashed border shapes
      ! (note) stroke-dasharray does not work with 'rect' element (=> must use 'path')
   o multi-line text
      ! https://wiki.selfhtml.org/wiki/SVG/Tutorials/Text/mehrzeiliger_Text
         ! <text class="mehrzeilig" x="20" y="130">
             <tspan x="10" dy="2em">Mehrzeiliger Text ist mglich, wenn Sie</tspan>
             <tspan x="10" dy="2em">die einzelnen Zeilen in &lt;tspan>-Elemente notieren.</tspan>
             <tspan x="10" dy="2em">Allerdings sind diese Zeilen nicht responsiv.</tspan>
           </text>
   + force minimal main graph padding (2;2;2;2)
   - gradients
   - patterns

* TEST optimize improve vertical empty space optimization
   ! 01Mar2024
   ! C:02Mar2024
   ! e.g. k>>b<<C sub graph in test22.gr
   + at end of layout() pass (when all sub graphs have been unfolded)
   + find edges that connect to "leaf" sub trees
      ! e.g. "*vv1", "1>>x" or "*vvB"
      + iterate edges and find nodes that are only connected via that edge
         + greedy: try to move larger subtrees, first
            + find all subtrees and sort by size / move largest areas first
      + check if the tree can be y-flipped
         ! e.g. "1>>x"
      + check if the tree can be moved up
         ! e.g. "*vvB"

* TEST add record/struct/table like sub graphs
   ! 01Mar2024
   ! C:04Mar2024
   ! C:05Mar2024
   ! C:06Mar2024
   ! C:10Mar2024
   ! C:11Mar2024
   ! tightly packed with no spaces in between the cells
   + looks like a single cell to the rest of the graph
   + see test23.gr
   + identify spans
   + split spans
   + add span panels and rows
   + reverse child layer order and replace Layout.RIGHT with Layout.LEFT
      + reverseChildLayers()
   x create/assign size groups
      + ==> use custom layouter and lock cell widths (requiredSizeX)
   + !!! distinguish extra_w after node and span_w
      ! i.e. when clipping x-spans ("defg" != "i   ")
   + (optionally) separate table cells with '|' (arbitrary id lengths)
   + test23b.gr: "Header Cell B", "Cell F", "Small Cell G" too wide
   + nested subgraphs
      + test23c.gr
         ! embeds test17.gr
   + add edges in final layout pass (use correct positions)
   o don't store absolute edge positions (or update them, e.g. when main graphwidget position changes after window resize)
   + test23d.gr
      o table layout not working in nested table in nested subgraph / node geo=(0,0,0,0)
      o also: why does table cell position differs from regular cell
         ? don't insert helper node for table subgraphs ?
            ! 1->2, 1->3 edges
            ! helper node is inserted for regular cell as well but eliminated later on
              ! collapseCellsAndRedundantEdgesH() removes helper node in non-subgraph test (but not when "3"

* TEST fixed / manual node placement
   ! 02Mar2024
   ! for smaller, non-autogenerated graphs
   + see test24.gr / test25.gr

* DONE split into graph.tks, test.tks
   ! 02Mar2024

* TODO test18c  "*" and "a" are placed on same cell (edge not drawn in testui)
   ! 02Mar2024

* TEST align num_edges with pass1 (via)
   ! 03Mar2024

* TODO look at major direction when calculating dirFlagsVia  (e.g. not both BOTTOM+LEFT)
   ! 03Mar2024

* DONE add option to snap arrow angle to 90
   ! 04Mar2024
   ! C:14Mar2024
   + <amt> arrow_snap (0..100%)
      + correct/move destination position
         + update curve / line destination vertex

* DONE add border options
   ! 04Mar2024
   ! C:15Mar2024
   + border_dir=top/left/bottom/right or all
   + border_width
   + border_style=solid|dotted|dashed|bold
   + border_shape=rect|round
   + border follows node shape
   x add inner padding
   + add outer panel(s)
   + implement in tkui

* DONE add palettes (named colors)
   ! 05Mar2024
   ! C:11Mar2024
   ! C:12Mar2024
   + palette [mypalette] { .. }
   + (colors) e.g. bgcolor=red
      ! use default palette (first declared)
   + (colors) e.g. bgcolor=dark.red
      ! use specific palette
   + test28.gr
   + set/change default palette with "palette=<id>"
      + implicit main graph start

* TEST tesselate lines
   ! 05Mar2024
   - reuse line pattern alg from SW rasterizer
      ! dashed / dotted lines
   + aa?
      ! => multisampling

* ACTV node shapes
   ! 05Mar2024
   o "shape" attribute
      + rect
         ! "a rectangle represents a process"
      + round (arc)
      - mrect/squircle
         ? shape_dist
            - set squircle bendiness
      + ellipse
         - circle
      ? oval
         ! "an oval represents a start or end point"
      + rhomb
         ! "a parallelogram represents input or output"
      + diamond
         ! "a diamond indicates a decision"
      + hexagon
         ! "represents a setup to another step in the process"
      - triangle (upside-down)
         ! (merge) "indicates a step where two or more processes become one"
   ! https://www.smartdraw.com/flowchart/flowchart-symbols.htm
   ? shape_slant_x (e.g. create parallelogram from rect)
   ? shape_flip_x
   ? shape_flip_y
   ! (note) https://venngage.com/blog/flowchart-symbols/

* TODO add edge target paths
   ! 05Mar2024
   - a->G.B.c

* ACTV add icons
   ! 05Mar2024
   ! C:16Mar2024
   + "icon", "icon_pad*", "icon_pos" attributes
   - add PNGIcon resource

* ACTV add dividers
   ! 05Mar2024
   ! C:13Mar2024
   ! e.g. for table layouts
   + add Label instead of Button (minSizeY)
   + set border_dir=top
   x set border_style=bold
   + set border_style=solid
   ? '-----' row = add bottom side border to previous row cells

* DONE includes
   ! 05Mar2024
   ! C:26Mar2024
   ! C:29Mar2024
   + include <file>
      + beginIncludeFile()
   x `<file>`
     ! problematic due to tokenization
   ! e.g. for style sheets
   + parsecontext stack
      + copy from Cycle parser
   + include paths (cmdline)

* TODO drop shadows
   ! 05Mar2024

* TODO detect edge crossings / swap edges when there are multiple connections to the same node
   ! 11Mar2024
   ! occured in a test (test17.gr?) due to an another issue. currently not occurs in any test, though-
   ! low prio though, workaround: swap edge decl order

* DONE default/fixed layout: pad nodes more tightly by default
   ! 11Mar2024
   + pad to the left but not in the first column
   + pad to the right but not in the last column
   + pad to the top but not in the first row
   + pad to the bottom but not in the last row
   ! defPaddingX / defPaddingY

* TEST add "auto" pad attrib value and "zero_margin" graph attrib (def=true)
   ! 11Mar2024
   + auto-zero margin at graph borders
   + add graph border-color / border-width / border-style attributes
   ! 11Mar2024

* DONT replace "-" in attribute name by "_"
   ! 11Mar2024
   ! e.g. allow both border_color and border-color
   x change default to "-" and update test cases
   ! ==>  nope, "-" is a separator token

* DONE change default cell spacing from 1 to 5
   ! 11Mar2024
   + but not for "fixed" layout
   + and also not if col/row spacing has already been changed (via cell/col/row spacing attribs)
      ! e.g. test24.gr

* DONE fix edge connection position in test13.gr (left-shifted)
   ! 11Mar2024
   ! C:12Mar2024
   ! helper cell is Label (" ") -> width is 0 (==> no, it's determined by the sizegroup!)
      + use width of connected node (/ui_layer) when connected vertically
         + don't connected to left/top/right/bottom of helper nodes, connect to their centers
      + smoothen multi-segment curves in pass 3

* TEST tkui: copy (max) sizegroup padding to layers in sizegroup
   ! 11Mar2024
   ! e.g. test29.gr
   ! in Panel::invalidateSizeGroupMemberSizes()
   o and retest all dialogs etc to see if this breaks anythings / requires additional changes in xfm
      ! ==> a quick look at it indicates that things still seem to work
   x need b_pad_auto flag in Layer class
      - initially true
      - set to false when default padding is overriden

* DONE fix edge positions after resize
   ! 12Mar2024
   ! i.e. to fix scrollbar issue and also for subgraph target node paths
   + add Layer::recursiveBeginResize()
   + add Graph::recursiveBeginUIInit()
   + add Node::beginUIInit()
   + change GraphWidget::addTableSubgraph() call to UI.LayoutRootLayer() to relayout()

* DONE split testui code and add Graph[Form?] to ui lib
   ! 12Mar2024

* DONE support "anonymous" style class
   ! 12Mar2024
   ! for simple graphs
   ! (note) first declared class is the default one

* TEST fix missing (rev) graph:d->b edge in test18c.gr (subgraph anchor is '2')
   ! 12Mar2024
   - and there's more wrong here: subgraph edge goes from 'd' (subgraph) to 'b', not a<-d('2')
   ? is this even a valid use case ? (postpone?)

* DONE add unfold graph attribute (def=true)
   ! 12Mar2024
   + false=keep subgraph contained in single cell (like a table subgraph)

* IGNR using (1;1) spcHelper causes "curve" in (helper-node) connected sub graph edge (test17.gr and test22.gr)
   ! 12Mar2024
   - labels are a workaround but the edge abs_*_pos_* should be centered instead
   ! since this isn't exactly a valid use case, postpone this for now
   ! ==> resolved in the meantime

* TODO eliminate pRow that consists only of spacers
   ! 12Mar2024
   - but NOT after adding edges (!)
   ! test13.gr

* DONT table sub graphs: separate layer creating and sizegroup fixations (setRequiredSize*()) (e.g. test23d.gr)
   ! 12Mar2024
   ! when not unfolding graph 'b' (unfold=false)
   ? or don't use sizegroups for tables but arrange them in columns instead of rows ?
      ! ==> no. see test29.gr. or yes after all ? hmmmmmmmm :) (=> problem would probably only "rotate" ?!)

* TEST fix sizegroup-related sub-graph grid cell width issue in test23e.gr
   ! 12Mar2024
             ? rewrite layouting ?? (and open up a whole new can of worms..??)
                1) recursively iterate layers (inner layers first) and
                    - invalidate pref/min size caches (unless forced)
                    - invalidate sizegroup-forced sizes (and set to unforced)
                       - align x and/or y padding of layers in same sizegroup
                    - invalidate absolute positions
                    - calc minimum sizes
                    - calc preferred sizes
                    - set (initial) size to minimum size
                2) recursively iterate layers (inner panels first) and
                    - layoutSizeGroups() (first 'min/req' size pass)
                        - setRequiredSizeX/Y()
                3) ??recursively iterate layers (inner layers first) and
                    ? recalc/update min/preferred sizes of layers/panels that are not in a sizegroup
                loop 2x:
                  - ??loop because sizegroups may (and usually will) link layers from different panels (and may also be nested)
                     ! but: layers in the same sizegroup must also have a common parent (Panel.size_groups / findSizeGroupMembers())
                     - after pass 1, the layout may not fit
                        ! e.g. a panel in the same sizegroup with a label is too wide and the new label size exceeds the total available space
                3) recursively iterate layers (outer layers first) and
                    - layoutSizeGroups() (next 'min/req' size pass)
                3) recursively iterate layers (outer layers first) and
                    ! most-outer layer is RootLayer, which is assigned the viewport size
                    - call Layer::layoutHierarchy()
                       - (when there is no layouter set, this simply assigns the parent size to all child layers (is this used somewhere..?))
                       - call layout.layoutChildLayersOf()
                          - set the direct child layer sizes and positions according to layout hints / alignment / layout weights
                    - in bFinalLayout pass, apply baseline adjustments
   + add (simplified) test23f.gr (worked already)
   ! basic problem is:
      - "sub 1" and "sub 2" (RIGHT in the same panel, part of the sub-Graphwidget) initially have the same size (80px w/o padding, 112 px with 32px of padding)
      - the (required)size of the sub-Graph is set to 2*112=224 px when applying its parent graph grid column sizegroupx constraints
      - "sub 1" is in a sizegroupx with a 115px wide TableGraphPanel (in the grid row below).
         - that now makes it (115+32=147x wide. the total sub-Graph width becomes 147+112=259px)
      ? apply the inner panel sizegroups first
      ! note: using setMinimumSizeX() instead of setRequiredSizeX() when applying the sgx fixes the issue in test23e.gr but breaks e.g. test29.gr
   o in Panel::layoutHierarchy(), add recursiveUpdatePreferredSizeOf() after size group layout. that seems to fix it (and works with all other tests, too :-))
      - retest the "bigger" apps !
         - it _does_ break some things (like ColorBoxes and load indicators) but totally seems salvagable
            ! turning off the "experimental" switch in Panel::layoutHierarchy() also makes everything go back to normal ("worst" case fallback)
         + this results in "huge" (pref 100% height) colors bars (PagePipeMap.xfm):
              <Panel dir=TOP align=expandx reqSizeY=1 tint=#40000000 alpha=255/>
              <Panel dir=TOP align=expandx reqSizeY=1 tint=#a0000000 alpha=255/>
           + => restricting recursiveUpdatePreferredPanelSizeOf() to a) just Panels and b) only those with sub-panels fixed it
   ! 5:45..23:30.............

* IDEA render small circle at edge tails (and add GR attribs for customization of colors, diameters, ..)
   ! 12Mar2024

* IDEA font declarations
   ! 13Mar2024
   ! similar to palettes
   - pak file or local file
   - basename and size
   - aliases (csv)
   ! ==> low prio (not used for svg export)
      ==> alias definitions would be handy, though

* IDEA icon declarations
   ! 13Mar2024
   ! similar to palettes / fonts
   - pak file or local file
   - alias (string)
   ? OR: simply fallback to local file when name cannot be resolved to a pak file resource
   ! also rather low prio for SVG export

* DONE test29.gr: one of the top>center edges connects to the same position
   ! 13Mar2024
   ! C:14Mar2024
   + there's also an inconsistency between left/right connections (right ones connect to the bottom edge, left ones to the left of the center node)

* IDEA (sub-)graph subtitles
   ! 13Mar2024

* DONE :tkui: flatten-layer-stop flags / layer groups
   ! 14Mar2024
   ! C:16Mar2024
   ! stop recursion, e.g. at outer node layer
   ! e.g. when nodes should be freely movable but not their "innards"
   + Layer.setEnableLayerGroup()
   + add bIgnoreLayerGroups param to Layer.flattenHierarchy()

* DONE :gr: gradients
   ! 14Mar2024
   ! C:23Mar2024
   + gradient mygradient {
        #20ff0000         // start at 0.0
        0.25 = #1000ffff  // explicit position = 0.25
        #18208040          // implicit position = 0.5  (half-way between 0.25 and 0.75)
        0.75 = #1000ff00
        #18000000         // implicit position = 1.0
     }
   + interpolate missing gradient starts
   x add Panel::setPanelBackgroundGradient(int _shape, FloatArray _starts, IntArray _colors)
   x add Panel::setPanelBorderGradient(int _shape, FloatArray _starts, IntArray _colors)
   + add Panel::setPanelBackgroundGradientShape(int _shape)
      + add "panelBgGradientShape" XFM attrib
   + add Panel::setPanelBackgroundGradientTexture(Texture _tex)
   + add Panel::setPanelBorderGradientShape(int _shape)
      + add "panelBorderGradientShape" XFM attrib
   + add Panel::setPanelBorderGradientTexture(Texture _tex)

* IDEA :gr: add gradient HSV mode
   ! 23Mar2024
   - "hsv" keyword

* IDEA :tkui: render/FX layer plugins (backgrounds, post-processing)
   ! 14Mar2024

* DONE arrow_border_color / arrow_border_width attribs
   ! 14Mar2024
   + def width=0 (no border)
   + when arrow_shape=curve

* DONE apply 'tint' to _all_ colors
   ! 15Mar2024
   + bgcolor
   + node_fgcolor
   + border_color
   + edge_fgcolor
   + arrow_fgcolor
   + arrow_border_color
   + inherit from parent graph

* DONE bgfill attribute
   ! 15Mar2024
   + call setEnableFillBackground()
   + set panel alpha in createPanelForNodeLayer() when border_width>0
   + copy panel alpha in copyBgColorFromGRAttribsToPanel()

* DONE need additional arrow_shape_rev, arrow_len_rev, arrow_angle_rev, arrow_linewidth_rev, arrow_fgcolor_rev, arrow_border_color_rev, arrow_border_width_rev settings
   ! 15Mar2024
   + test30.gr
   + GraphEdgeArrow

* DONE fix bidir arrow snap in test30.gr
   ! 15Mar2024
   + add (and check) Edge.b_ui_xmajor

* DONE connect top/right tail to right when xmajor=1
   ! 15Mar2024
   ! test30.gr

* DONE implement node/button ipad attrib
   ! 15Mar2024
   ! test30.gr

* DONE add (pixel) optimization pass that moves layers down to straighten horizontal edges (breaks baseliney alignment, though)
   ! 15Mar2024
   ! OR: move tail position
      + add "tail_snap_y" edge attribute (def=5)
         ! don't increase default beyond 5 (looks weird otherwise, e.g. test18d.gr)
   ! e.g. test30.gr

* DONE rename form => shape
   ! 15Mar2024
   + update test cases

* DONE "border_blend" attribute
   ! 15Mar2024
   ! C:16Mar2024
   + "off"/"none", "srcover", "add"/"additive", "srccolor", "dstcolor"

* DONT short-form border_dir "tlbr"
   ! 15Mar2024

* DONE "round_amount", "round_aspect", "round_limit" attributes
   ! 16Mar2024
   + shape_round_amount def=0.125 (12.5%)
   + shape_round_aspect def=1.6 (16:10)
   + shape_round_limit  def=12

* DONE in Label class, add ref to outer (border) layer and shrink fill area in Label by 2*arcW/H
   ! 16Mar2024
   + OR: (if panel has bg_shape set to != rect): don't fill at all and delegate to Panel instead
   x OR move border handling to Layer class and don't create outer panel
      ! ==> but this would require more changes (in _all_ widget classes)
   ! e.g. test24.gr and test29.gr

* DONE subgraph shape=round overdraw issue: pGraph
   ! 16Mar2024
   + copyPanelShapeAndBorderAttribs()

* IDEA import (complex) graph layout from graphviz-generated SVG output
   ! 16Mar2024

* DONE quietly merge '.' char in style class attribute values (forgotten "")
   ! 16Mar2024
   + style class attribs
   + graph attribs
   + node attribs
   + edge attribs

* DONE lazy-replace ',' by ';' in style/graph/node/edge attrib values
   ! 16Mar2024
   ! e.g.  pad=1,2,3,4  => pad="1;2;3;4"

* IDEA after flattenHierarchy(), traverse nodes and apply final pixel-shift
   ! 16Mar2024
   - add "offset" attribute
   - re-run addEdgesFromGraph() afterwards

* DONE labels
   ! 16Mar2024
   ! C:17Mar2024
   x add GraphLabel class (derived from ui::Label)
      ! BUT: label may have outer border panel. add a layer flag instead or simply keep a list ?
   + add labels after adding edges + flattenHierarchy()
   + add "label_class" attribute
   + call getMinimumSize() and layoutContent() after adding label
   + label px-pos: along edge
      + tail..<next_helper_node>..head (follow SLL)
      + add "label_relpos" attrib (0..100%, 0=tail, 100%=head) (def=50%)
   + draw labels after edges
      x add "dummy" layer that invokes drawEdges() ?
      + OR: add flag to last layer before first label layer that causes drawHierarchy() to invoke parent.onDrawPostChild(Layer _childLayer)
         ! with 'parent' being the GraphWidget
         + Layer.b_invoke_parent_ondrawpostchild / Layer.setEnableInvokeParentOnDrawPostChild()
   + set default label class
      + initially style class "label" or "lb" (if it exists)
      ! >>label_class=my_default_label_style_class<<
      ! (note) graph attrib is inherited by nodes/edges/subgraphs

* IDEA add short-form attribute aliases for "class" and "label_class" ("c" and "lc" ? or "cl" and "lcl" ?)
   ! 16Mar2024

* DONE "tooltip" attribute (nodes / edges)
   ! 16Mar2024

* DONE translate GraphEdge positions when/after flattening layers
   ! 17Mar2024
   ! C:18Mar2024

* DONE attrib priorities
   ! 17Mar2024
   + copy initial node style from cur_node_style
   + inherit (ifnotexists) attributes from parent graph
   + edges: copy (overwrite) attributes from "label_class" style
   + when adding 'class'/'label_class' attribute via [], overwrite existing attributes
      + order of attrib declaration matters

* DONE :tkui: move fg/bg color+tint setters/getters and setEnableFillBackground() to Layer base class
   ! 17Mar2024
   ! C:22Mar2024
   + rename c32_tint to bg32_tint
   + rename setTint() to setBackgroundTint() / update apps
   + rename getTint() to getBackgroundTint() / update apps
   + update TableViewData
   + update Label
   + update Button
   + update CheckBox
   + update ColorButton
   + update RepeatButton
   + update PopupMenuButton
   + update TextField
   + update FloatParam
   + update XYPad
   + update BezierEdit
   + update ComboBox

* TODO :tkui: add Layer.c32_all_tint
   ! 17Mar2024
   ! C:22Mar2024
   - modulate c32_fg and c32_bg
   - setTint(), getTint()

* DONE :tkui: implement Slider, Scroller and Dial tint
   ! 17Mar2024
   ! C:22Mar2024

* IDEA instantiate other widget classes via ()
   ! 17Mar2024
   ! >>a(Slider min=0 max=100 step=5)<<
      - parse class name and XFM attribs within (..) and pass to beginXFMTag()
      ? but what about classes with child layers (e.g. ScrollPane, TabbedView, ..?)
         ? allow graphs to be embedded in XFM ?

* DONE "arrow_ilen" attribute (def = 0)
   ! 18Mar2024
   + add "dent" along LR edge

* DONE :tkui: add headless mode
   ! 18Mar2024
   + UI.SetEnableHeadless(boolean)

* DONT :tkui: skip second (non-final) layout pass when panel has no sizegroups
   ! 18Mar2024
   ! reduces test23d total run time (UI Init+Graph parsing/layout + SVG emit) from 115ms to 81ms
   ! look _almost_ perfect but breaks some layer alignments ==> keep second pass

* NOTE graph v0.1 total run time (UI Init / load .gr file / parse graph / layout / SVG emit / save .html file)
   ! 18Mar2024
   !  test24.gr:  16ms (62.5 graphs/second)
   !  test30.gr:  14ms (71.4 graphs/second)
   ! test23d.gr: 109ms ( 9.2 graphs/second)

* DONE :tkui: ConfigureSupersampling(int _numSamples)
   ! 19Mar2024
   + multiply FBO width/height
   + use bilinear filter when compositing screen
   + scale glLineWidth
   + scale glViewport position/size
   + scale glScissor position/size
   ! ==> ok but probably not worth the huge resolution increase. text becomes a bit blurry at numSamples=2 (numSamples=4 looks sharper/better)

* IDEA row-major table layout mode
   ! 18Mar2024
   ! ==> use pixel_layout=grid instead

* DONE cell width+height / link table-cells / GridLayout
   ! 18Mar2024
   ! C:19Mar2024
   ! C:20Mar2024
   ! C:21Mar2024
   ! C:22Mar2024
   ! C:26Mar2024
   ! e.g. for tall cell next to (vertically placed) smaller cells
   !  a  b
      a  c
      a  d
   !  aa  b
      aa  c
      aa  d
   + find first (top/left) cell for node
   + at beginning of graph layout:
      + find cell dimensions ("sw", "sh")
      - eliminate/unset cells except for top/left
         - need to move edges ? (probably not)
   + in pixel (grid) layout pass:
      - find max cell_h for current row
      - when cell spans multiple rows:
         -
   ! problem: BorderLayout works with nested row/column panels/layers => hard to implement row/col spans
      - implement new (single-pass?) GridLayout
         x no sizegroups ?
            - OR: just limit them to single grid cells (e.g. labels)
         - assign grid_x / grid_y to child layer
            - Layer.addGridLayer(Layer _l, int _gridX, int _gridY, int _gridW, int _gridH)
         - GridLayout.layoutChildLayersOf(..):
            - calc min+preferred sizes of each grid cell / layer
            - apply size groups
            - iterate rows and cols
               - find min row span > 1 that starts in current cell
               - calc total height of other cells within row-span
                  - when height > current cell height: increase height
                  - else evenly distribute extra space to other cells
                     - spanHeightPx * (numOtherCells / row_span_height_in_cells)
               - mark cell as "done"
            - repeat until all row_span cells are done
            - now repeat for hspans
   + add node attribs "sw" and "sh" (span width / height)
   + support sizegroups
      ? might already be working ?
         ? may need to set outer panel alignent to BASELINE
         ! ==> no it didn't but now it does
   + implement baseline alignment
      + layoutApplyCompositeBaselineAdjustments_GridLayout()
   + support relative node widths/heights
      + 'w'/'h' < 0.1 or string contains '%'
      + availW/H * weightX/Y
   + implement slant / slant_factor
      + Layer.layout_grid_slant_factor

* DONE implement invisible nodes
   ! 18Mar2024
   ! C:19Mar2024
   ! C:20Mar2024
   ! C:22Mar2024
   ! e.g. for creating connection "ports" on larger nodes (composed of joined cells)
   x size=(1;1) during initial layout pass
      x then reset to (0;0) after flattening layers
      + ==> allow w=0 / h=0
   + skip when exporting to SVG
   + test32e.gr

* TODO allow edges between table nodes/cells
   ! 18Mar2024

* TEST add right-angled (single and multi-segment) curves (edge_shape=rcurve)
   ! 19Mar2024
   ! C:23Mar2024

* DONE (main) graph ipad attribute support
   ! 19Mar2024
   + copy root GraphWidget bgcolor to parent Panel and apply padding to GraphWidget
   + SVG: translateFlattenedLayers()
      + undo after export when not in headless mode

* TEST name ? (diagraph ?)
   ! 20Mar2024

* DONE (background) fill patterns (e.g. for print)
   ! 20Mar2024
   ! C:22Mar2024
   ! C:23Mar2024
   + bind shader that uses gl_FragCoord to stencil pattern onto background shape
   + "bg_pattern", "bg_pattern_intensity" attributes

* TEST tkui integration
   ! 20Mar2024
   ! C:21Mar2024
   ! C:22Mar2024
   + move GR* / Graph* classes to tkui
      + rename GraphWidget to GraphForm and change base class to Form
      + add Form.onResizeFormPost() and call in GraphForm::onResize() after adding edges+labels
         + override in MyGraphForm (testui.tks) and call exportSVG()
   + GraphForm:
      + parse flow text between <GraphForm>..</GraphForm>
         + String.parseXML: allow stray '<' and '>' in flow text
      + parse XFM attributes in ()
         ! see 17Mar2024 "instantiate other widget classes via ()"
            ! ! >>a(Slider min=0 max=100 step=5)<<
      + add findLayerByPath(String _sPath)
         ! e.g. mysubgraph.othersubgraph.my_slider
      + add "sgx" / "sgy" attributes (size groups)
         + lazy-create sizegroup and add to parent GraphForm
      x add Layer "linkedRedraw" XFM attribute (linked_redraw member)
         x layer id or special value "parent"
         + OR: add boolean "redrawParent" attrib ? (==> yes)
   + support new border / shape attributes in XFM
      + panelStyle
      + panelBgStyle
      + panelBorderStyle
      + panelShape
      + panelBgShape
      + panelBorderShape
      + panelBorderWidth
      + panelBorderColor
      + panelBorderDir
      + panelBorderBlend=off|srcover|additive|srccolor|dstcolor
   x add "xfm_class" directive (set default class)
      ! ==> useless. each widget class needs its own default style class
         + try to resolve <widget_class> style class instead, e.g. "style Slider { .. }"

* DONE rename node and graph ids via attribute "id"
   ! 20Mar2024
   ! C:21Mar2024
   ! C:22Mar2024
   ! e.g.
       row="abc"
       a "Button A" [id=bt_a]
       b "Button B" [id=bt_b]
       c "Button C" [id=bt_c]
       bt_a->bt_b->bt_c
   + also rename ids in table_layout_rows
   + test34.gr

* DONE parse '-' in attrib value
   ! 21Mar2024

* IDEA shape=rect_fractal
   ! 21Mar2024

* IDEA shape=ellipse_fractal
   ! 21Mar2024

* IDEA edge_shape=paper
   ! 21Mar2024
   ! "torn piece of paper" look

* DONE mouseover not working in test23d.gr (innermost table subgraph)
   ! 21Mar2024
   x linked_redraw issue ? (=> no. or rather yes: a bug in Panel.onDraw() (did not eval panel_tint2 in all cases))

* DONE :tkui: add CheckBox "iconSuffix" XFM attribute
   ! 22Mar2024
   ! e.g. "_dark"

* DONE :tkui: add Shader_GradientH / Shader_PatternGradientH
   ! 23Mar2024
   x special case when gradient has two colors with start=0 and start=1
   x need gradient or palette id
      - color 0 = top/left + bottom/left
      - color 1 = top/right + bottom/right
   + fit to object
      + pass abs_x1/x2 to shader
   + support arbitrary number of gradient starts/stops
   + [bg|border]_gradient_shape=h
   + test35.gr

* DONT :tkui: add Shader_GradientHStops
   ! 23Mar2024
   - when gradient either has more than two colors or first start!=0 or last start!=1
   - need gradient or palette id
      - arbitrary number of colors / stops
   - create texture
   - fit to object
      - pass abs_x1/x2 to shader
   + [bg|border]_gradient_shape=h
   ! ==> DON'T. simply use Shader_GradientH in all cases

* DONE :tkui: add Shader_GradientV / Shader_PatternGradientV
   ! 23Mar2024
   x special case when gradient has two colors with start=0 and start=1
   x need gradient or palette id
      - color 0 = top/left + top/right
      - color 1 = bottom/left + bottom/right
   + fit to object
      + pass abs_y1/y2 to shader
   + support arbitrary number of gradient starts/stops
   + [bg|border]_gradient_shape=v
   + test35.gr

* DONT :tkui: add Shader_GradientVStops
   ! 23Mar2024
   - when gradient either has more than two colors or first start!=0 or last start!=1
   - need gradient or palette id
      - arbitrary number of colors / stops
   - create texture
   - fit to object
      - pass abs_x1/y1/x2/y2 to shader
   - gradient_shape=v
   ! ==> DON'T. simply use Shader_GradientV in all cases

* DONE :tkui: add Shader_GradientR / Shader_PatternGradientR
   ! 23Mar2024
   x special case when gradient has two colors with start=0 and start=1
   x need gradient or palette id
      - color 0 = top/left + top/right
      - color 1 = bottom/left + bottom/right
   + [bg|border]_gradient_shape=r
   + fit to object
      + pass abs_ctr + rx/y to shader
   + support arbitrary number of gradient starts/stops
   + [bg|border]_gradient_shape=r
   + test35.gr

* IDEA :tkui: add Shader_GradientHV
   ! 23Mar2024
   - need gradient or palette id
      - color 0 = top/left
      - color 1 = top/right
      - color 2 = bottom/right
      - color 3 = bottom/left
   - fit to object
      - pass abs_x1/y1/x2/y2 to shader
   ? use _two_ gradients to create the texture (one per axis)

* DONE add "xmajor_tail", "xmajor_head", "xmajor" graph and edge attributes
   ! 23Mar2024
   ! for finetuning edge connection placement
   ! e.g. test30.gr, test31.gr, test32f.gr

* DONE :tkui:Label: support CW/CCW rotated content (text+icon)
   ! 24Mar2024
   + "rotate" XFM and GR attributes
      + 1/true/cw/90 = rotate 90 CW
      + ccw/-90 = rotate 90 CCW
   + swap w/h in layoutContent()
   ? return center y in getBaselineY()

* DONE test32f: gradient breaks when supersampling is enabled
   ! 24Mar2024

* DONE add virtual pad_h and pad_v attributes
   ! 24Mar2024
   + set both pad_l/r and pad_t/b

* DONE add headpoint_amount and headpoint_mindist node attributes
   ! 24Mar2024
   ! for calcHeadPoint()
   ! e.g. connections to 'e' in test32f.gr
   + headpoint_amount  def=12.5%
   + headpoint_mindist def=8  (internally scaled to 16 in HiDPI)

* DONE :tkui: fix align=centerx|right for multi-line text
   ! 24Mar2024
   + pass availW / align to UIRenderer.DrawText()
      + update apps
   + also remove glPushMatrix/glTranslate/glScale/glPopMatrix calls

* DONE findLayerByPath: match sub-path when absolute path could not be resolved
   ! 25Mar2024
   + iterate all nodes that match last path element id
      + return node that matches remaining sub-path

* TODO :tkui: FlowLayout
   ! 25Mar2024
   - add regular words as Labels or LabelLinks
      - linktarget attribute
   - linefeed inserts line break

! NOTE https://kroki.io/
   ! "provides a unified API with support for BlockDiag (BlockDiag, SeqDiag, ActDiag, NwDiag, PacketDiag, RackDiag), BPMN, Bytefield, C4 (with PlantUML),
       D2, DBML, Ditaa, Erd, Excalidraw, GraphViz, Mermaid, Nomnoml, Pikchr, PlantUML, Structurizr, SvgBob, Symbolator, TikZ, UMLet, Vega, Vega-Lite, WaveDrom, WireViz.."
   ! online service. send http request with graph source, recv output png/svg/pdf/..

* DONE multi-layer panel stack mode
   ! 25Mar2024
   ! C:26Mar2024
   ! C:29Mar2024
   ! e.g. for hw_toplevel diagram
   + XFM:
      + panel_stack=3
      + panel_stack_off [8,8]
   + GR:
      + stack=3
      + stack_off=8[,8]
   + draw multiple, offset panels
   + test37.gr
   + svg export

* ACTV calcHeadPoint(): calc point on shape
   ! 25Mar2024
   - also implement calcTailPoint()
   o eval stack extraW/H

* TODO fix default shape=default (fills Label background, shape=rect does not)
   ! 26Mar2024

* DONE allow include or style / palette / gradient declaration or update in graph body
   ! 26Mar2024
   ! C:29Mar2024
   ! e.g. after implicit main graph start in diagram.gi include

* TODO subgraph must currently be declared before it's used in a (fixed) layout (test38.gr)
   ! 26Mar2024

* TODO calcDirFlags: add min threshold for direction (e.g. 16px)
   ! 26Mar2024

* TODO remove all Spacers after adding edges+labels
   ! 26Mar2024

* TEST test38.gr: "mmmmm" colspan above "MBI" (and invis port-helpers) distributes (644-522)/5=24.4 to columns below
   ! 26Mar2024
   ! ==> "MBI" column (content w=96px/100px padded) width is increased to 124.4px
      ! ==> left-alignment causes gap between MBI and port-helper
         ! ====> workaround: disable zero_margin for 'm' subgraph and manually pad_l MBI node so it aligns with SBI
                  ! (note) proper solution: add ports {} declaration and get rid of port-helper nodes

* DONE support fractional numbers, e.g. "1/3"
   ! 26Mar2024
   ! C:28Mar2024

* DONE (virtual) ipad_h, ipad_v attributes
   ! 26Mar2024
   ! C:28Mar2024

* DONE graph38.gr: bg_pattern not working
   ! 26Mar2024
   ! >>f "Dispatcher"           [h=75% pad_h=0 align=center ipad_l=6 ipad_r=6 bg_pattern=line_4 bg_pattern_intensity=30%] //shape=rhomb<<
   + parse bg_pattern_intensity %

* DONE add "anonymous" layout helper '#' (may occur multiple times)
   ! 27Mar2024
   ! C:28Mar2024
   + spacer_class=spacer

* DONE :ports: port margin (%)
   ! 27Mar2024
   ! C:28Mar2024
   + shift/scale port relpos
   + separate margins for top/left/bottom/right edges
   + test38d.gr
   + test39.gr

* DONE :ports: make relpos optional (and auto-assign it)
   ! 28Mar2024
   + test39.gr

* IDEA word frequency chart (font sizes)
   ! 28Mar2024

* DONE test24.gr tooltip defined in label_class=lb_gi not working anymore
   ! 28Mar2024
   + when evaluating fractions, check if (de-)nominator can be converted to int/float

* DONE apply GR panel/bg/fg/border attribs to XFM layer
   ! 28Mar2024

* ACTV :tkui: Shape class
   ! 28Mar2024
   ! C:29Mar2024
   ! C:30Mar2024
   ! C:01Apr2024
   o Shape base class
      + color
      + width
         ! for non-filled shapes
      - calcBBox()
   - ShapeEllipse
   - ShapeFilledEllipse
   - ShapePolygon
      - solid filled (inconvex) polygon
      - use tktriangulate
   + ShapePolyline
      + tks-projects/polyline_v2_2024/polyline.tks / polyline_mesh.tks test cases
         + reuse polyline code from SW rasterizer research
            + remove AA planes
      + closed flag
      + left / right borders
   - ShapeBezierCurve : ShapePolyline
      ! cubic bezier curve
   - ShapeFactory
      - create from SVG path

* DONE add test38e.gr (hspan aa/bb/cccc test)     
   ! 28Mar2024
   + GridLayout: loop totalNumHSpans/VSpans
   + fix '#' in char-based row layout

* DONE style=default / palette=default  return first available style/palette when 'default' does not exist
   ! 29Mar2024

* DONE add ParseCmdLine() (and multi-file processing)
   ! 29Mar2024

* DONE :tkui: remove Vector2f, Point2f, Size2f script classes and replace by tkmath C++ classes
   ! 29Mar2024

* DONE :tkui: implement GL_EXT_framebuffer_multisample (also supported on macOS)
   ! 29Mar2024
   ! https://www.khronos.org/opengl/wiki/GL_EXT_framebuffer_multisample
   + for each FBO, create another (multisampled) FBO
   + when rendering, bind the msaa FBO instead of the regular FBO
   + when compositing layers, call glBlitFramebuffer to resolve the multisampled FB into the regular FB (then composite as usual)

* IDEA :shape: z_pos
   ! 31Mar2024
   - add 'z' arg to draw()
      ! e.g. for overlapping lines with borders

* DONE replace line edges (curve + rcurve) with ShapePolyline
   ! 01Apr2024
   + line patterns

* DONE add edge_width_start, edge_width_end attributes
   ! 01Apr2024
   ! C:02Apr2024
   + when either is unset, fall back to 'edge_width'
   + interpolate line widths along curve
      x calc "t" array
      x calc line width array using "t" array and start/end widths
      + OR: simply use normalized vertex index (may be good enough already)
      + call curve_shape.setLineWidthArray()
   + update test22.gr
   + update test24.gr
   + update test25.gr
   + update test25b.gr
   + update test29.gr

* DONE add edge_width_left, edge_fgcolor_left, edge_fgtint_left, edge_width_right, edge_fgcolor_right, edge_fgtint_right, edge_width_adaptive attributes
   ! 01Apr2024
   ! C:02Apr2024

* TODO :tkui: don't create multisampled window when using multisampled FBOs (?)
   ! 02Apr2024

* TODO class_solid|dotted|dashed|bold|invisible|undirected=<class>
   ! 02Apr2024

* DONE node rotation broken in test32f.gr exported SVG
   ! 02Apr2024
   ! C:03Apr2024
   ! works in preview

* DONE move SVG edge tail position by border width (currently overlaps)
   ! 02Apr2024
   ! C:03Apr2024

* DONE $(inc!./res/simple_ab_edge.svg)(SVG) not working (underscore)
   ! 03Apr2024 

* DONE set curve distB to high value when using line patterns
   ! 03Apr2024
   ! => equal distance

* DONE test32f.gr: rectangular edges connect to ??? instead of helper node (padding?)
   ! 03Apr2024

* DONE border line patterns (in preview mode)
   ! 03Apr2024
   ! 04Apr2024
   ! 05Apr2024
   ! 06Apr2024
   + lazy-create ShapePolyline in Panel
   ! e.g. test24.gr
   + either don't use VBOs or properly clean them up before deleting Layer
      + Layer::freeLayer()
   + shapes
      + rect
      + round
      + rhomb
      + diamond
         + test43.gr
         + SVG
      + ellipse
         + test42.gr
         + SVG
   + stacks

* DONE edge_width_exp
   ! 03Apr2024
   + line width bias towards tail or end

* DONE stack alpha decay and exponent
   ! 04Apr2024
   + stack_alpha_dcy attribute
      + 0..100%
   + stack_alpha_exp attribute
      + -f..+f

* TEST arrow distance (from edge)
   ! 04Apr2024
   + GraphForm::addEdgesFromGraph: tailDistX / tailDistY
   + GraphForm::addEdgesFromGraph: headDistX / headDistY
   - add attribute ?

* NOTE 2.3s for all 76 test graphs => ~32ms per graph (.svg)
   ! 05Apr2024
   ! % (cd ../../../tkui ; m install) ; (cd ../ ; make ; make install) ; time tks app:diagraph -cli -svg -o svg/ *.gr

* DONE add overall UI scaling factor cmdline option
   ! 05Apr2024
   + update UI.pad_scaling, font_scaling, ..

* DONT add -textoff cmdline option (workaround for slightly misaligned (scaling-factor dependent) text label positions
   ! 05Apr2024
   + ==> add built-in tweaks insteada

* IDEA multi-threaded batch processing ?
   ! 05Apr2024
   - split file list and spawn additional processes
      - psplit cmdline option (number of files per process)

* DONE add screenshot / save tkui graph as png option
   ! 05Apr2024
   + Layer::exportPNG()
   + 'p'

* DONE :svg: add maingraph edges before nodes and subgraph edges afterwards
   ! 06Apr2024

* DONE :md: fix image base64 link issue (screenhots)
   ! 08Apr2024
   ! C:09Apr2024

* DONE add hexagon shape
   ! 08Apr2024
   + tkui preview
   + svg export
   + doc update

* TODO add command line option that turns certain errors into warnings (e.g. undeclared style classes/palettes/gradients)
   ! 08Apr2024

* DONE :doc: replace embedded <svg> by <img>
   ! 09Apr2024

* TEST add tri shape
   ! 09Apr2024
   + tkui preview
   + svg export
   + doc update

* TEST fix line patterns
   ! 09Apr2024
   ! C:10Apr2024
   ! in ShapePolyline::SubdividePolylineVertices()
   + traverse interpolated position on path and generate new vertices
      ! _curveDistP
   + calc total path length and adjust curveDistP to create seamless pattern

* TEST change graph 'unfold' attrib default value to false
   ! 23Feb2025
   + update tests 16,17,18,18b,18c,22

* TEST test47.gr: 'node_class' should not set/update graph attribs (e.g. bgcolor)
   ! 23Feb2025
   ! node_class is set in subgraph and later applied when setting subgraph geometry in main graph
      ! i.e. "it stuck"
   + push cur_style/cur_node_style/cur_edge_style/cur_palette/cur_gradient onto stack when starting new subgraph
   + pop from stack when subgraph is closed

* TEST select (implicit) "default" style by default (instead of first declared style class)
   ! 23Feb2025
   + update tests (e.g. test32e.gr)

* TEST fix ports {} section in (sub-)graph
   ! 23Feb2025
   + add ports and port attribs to graph.getIONode() instead of graph

* TODO add syntax for copying graph attribs from style class / change 'class' to 'style'
   ! 23Feb2025
   ! (note) 'class' (/'edge_class','node_class') sets default class for _next_ nodes and edges
       ! BUT: when using 'class' attrib in node/edge attrib lists, it sets the style class for the current item
          ! ==> inconsistent behaviour
   - change 'class*' behaviour and add 'style*' keywords
      - make 'class' set style class for current entity (copy attribs)
      - add 'style*' for setting default style class for next entities (what 'class*' currently does)
      - update all tests
