base/TAttFillHandler.mjs

import { gStyle, isObject, isStr } from '../core.mjs';
import { color as d3_color, rgb as d3_rgb, select as d3_select } from '../d3.mjs';
import { getColor, findColor, clTLinearGradient, clTRadialGradient, toColor } from './colors.mjs';


/**
  * @summary Handle for fill attributes
  * @private
  */

class TAttFillHandler {

   /** @summary constructor
     * @param {object} args - arguments see {@link TAttFillHandler#setArgs} for more info
     * @param {number} [args.kind = 2] - 1 means object drawing where combination fillcolor == 0 and fillstyle == 1001 means no filling,  2 means all other objects where such combination is white-color filling */
   constructor(args) {
      this.color = 'none';
      this.colorindx = 0;
      this.pattern = 0;
      this.used = true;
      this.kind = args.kind || 2;
      this.changed = false;
      this.func = this.apply.bind(this);
      this.setArgs(args);
      this.changed = false; // unset change property
   }

   /** @summary Set fill style as arguments
     * @param {object} args - different arguments to set fill attributes
     * @param {object} [args.attr] - TAttFill object
     * @param {number} [args.color] - color id
     * @param {number} [args.pattern] - fill pattern id
     * @param {object} [args.svg] - SVG element to store newly created patterns
     * @param {string} [args.color_as_svg] - color in SVG format */
   setArgs(args) {
      if (isObject(args.attr)) {
         args.pattern ??= args.attr.fFillStyle;
         args.color ??= args.attr.fFillColor;
      }

      if (args.enable !== undefined)
         this.enable(args.enable);

      const was_changed = this.changed; // preserve changed state
      this.change(args.color, args.pattern, args.svg, args.color_as_svg, args.painter);
      this.changed = was_changed;
   }

   /** @summary Apply fill style to selection */
   apply(selection) {
      if (this._disable) {
         selection.style('fill', 'none');
         return;
      }

      this.used = true;

      selection.style('fill', this.getFillColor());

      if ('opacity' in this)
         selection.style('opacity', this.opacity);

      if ('antialias' in this)
         selection.style('antialias', this.antialias);
   }

   /** @summary Returns fill color (or pattern url) */
   getFillColor() { return this.pattern_url || this.color; }

   /** @summary Returns fill color without pattern url.
     * @desc If empty, alternative color will be provided
     * @param {string} [alt] - alternative color which returned when fill color not exists
     * @private */
   getFillColorAlt(alt) { return this.color && (this.color !== 'none') ? this.color : alt; }

   /** @summary Returns true if color not specified or fill style not specified */
   empty() {
      const fill = this.getFillColor();
      return !fill || (fill === 'none');
   }

   /** @summary Enable or disable fill usage - if disabled only 'fill: none' will be applied */
   enable(on) {
      if ((on === undefined) || on)
         delete this._disable;
      else
         this._disable = true;
   }

   /** @summary Set usage flag of attribute */
   setUsed(flag) {
      this.used = flag;
   }

   /** @summary Returns true if fill attributes has real color */
   hasColor() {
      return this.color && (this.color !== 'none');
   }

   /** @summary Set solid fill color as fill pattern
     * @param {string} col - solid color */
   setSolidColor(col) {
      delete this.pattern_url;
      this.color = col;
      this.pattern = 1001;
   }

   /** @summary Set fill color opacity */
   setOpacity(o) {
      this.opacity = o;
   }

   /** @summary Check if solid fill is used, also color can be checked
     * @param {string} [solid_color] - when specified, checks if fill color matches */
   isSolid(solid_color) {
      if ((this.pattern !== 1001) || this.gradient) return false;
      return !solid_color || (solid_color === this.color);
   }

   /** @summary Method used when color or pattern were changed with OpenUi5 widgets
     * @private */
   verifyDirectChange(painter) {
      if (isStr(this.pattern))
         this.pattern = parseInt(this.pattern);
      if (!Number.isInteger(this.pattern))
         this.pattern = 0;

      this.change(this.color, this.pattern, painter ? painter.getCanvSvg() : null, true, painter);
   }

   /** @summary Method to change fill attributes.
     * @param {number} color - color index
     * @param {number} pattern - pattern index
     * @param {selection} svg - top canvas element for pattern storages
     * @param {string} [color_as_svg] - when color is string, interpret as normal SVG color
     * @param {object} [painter] - when specified, used to extract color by index */
   change(color, pattern, svg, color_as_svg, painter) {
      delete this.pattern_url;
      delete this.gradient;
      this.changed = true;

      if ((color !== undefined) && Number.isInteger(parseInt(color)) && !color_as_svg)
         this.colorindx = parseInt(color);

      if ((pattern !== undefined) && Number.isInteger(parseInt(pattern))) {
         this.pattern = parseInt(pattern);
         delete this.opacity;
         delete this.antialias;
      }

      if ((this.pattern === 1000) && (this.colorindx === 0)) {
         this.pattern_url = 'white';
         return true;
      }

      if (this.pattern === 1000)
         this.pattern = 1001;

      if (this.pattern < 1001) {
         this.pattern_url = 'none';
         return true;
      }

      if (this.isSolid() && (this.colorindx === 0) && (this.kind === 1) && !color_as_svg) {
         this.pattern_url = 'none';
         return true;
      }

      let indx = this.colorindx;

      if (color_as_svg) {
         this.color = color;
         if (color !== 'none') indx = d3_color(color).hex().slice(1); // fictional index produced from color code
      } else
         this.color = painter ? painter.getColor(indx) : getColor(indx);

      if (!isStr(this.color)) {
         if (isObject(this.color) && (this.color?._typename === clTLinearGradient || this.color?._typename === clTRadialGradient))
            this.gradient = this.color;
         this.color = 'none';
      }

      if (this.isSolid()) return true;

      if (!this.gradient) {
         if ((this.pattern >= 4000) && (this.pattern <= 4100)) {
            // special transparent colors (use for sub-pads)
            this.opacity = (this.pattern - 4000) / 100;
            return true;
         }
         if ((this.pattern < 3000) || (this.color === 'none'))
            return false;
      }

      if (!svg || svg.empty()) return false;

      let id, lines = '', lfill = null, fills = '', fills2 = '', w = 2, h = 2;

      if (this.gradient)
         id = `grad_${this.gradient.fNumber}`;
      else {
         id = `pat_${this.pattern}_${indx}`;

         switch (this.pattern) {
            case 3001: w = h = 2; fills = 'M0,0h1v1h-1zM1,1h1v1h-1z'; break;
            case 3002: w = 4; h = 2; fills = 'M1,0h1v1h-1zM3,1h1v1h-1z'; break;
            case 3003: w = h = 4; fills = 'M2,1h1v1h-1zM0,3h1v1h-1z'; break;
            case 3004: w = h = 8; lines = 'M8,0L0,8'; break;
            case 3005: w = h = 8; lines = 'M0,0L8,8'; break;
            case 3006: w = h = 4; lines = 'M1,0v4'; break;
            case 3007: w = h = 4; lines = 'M0,1h4'; break;
            case 3008:
               w = h = 10;
               fills = 'M0,3v-3h3ZM7,0h3v3ZM0,7v3h3ZM7,10h3v-3ZM5,2l3,3l-3,3l-3,-3Z';
               lines = 'M0,3l5,5M3,10l5,-5M10,7l-5,-5M7,0l-5,5';
               break;
            case 3009: w = 12; h = 12; lines = 'M0,0A6,6,0,0,0,12,0M6,6A6,6,0,0,0,12,12M6,6A6,6,0,0,1,0,12'; lfill = 'none'; break;
            case 3010: w = h = 10; lines = 'M0,2h10M0,7h10M2,0v2M7,2v5M2,7v3'; break; // bricks
            case 3011: w = 9; h = 18; lines = 'M5,0v8M2,1l6,6M8,1l-6,6M9,9v8M6,10l3,3l-3,3M0,9v8M3,10l-3,3l3,3'; lfill = 'none'; break;
            case 3012: w = 10; h = 20; lines = 'M5,1A4,4,0,0,0,5,9A4,4,0,0,0,5,1M0,11A4,4,0,0,1,0,19M10,11A4,4,0,0,0,10,19'; lfill = 'none'; break;
            case 3013: w = h = 7; lines = 'M0,0L7,7M7,0L0,7'; lfill = 'none'; break;
            case 3014: w = h = 16; lines = 'M0,0h16v16h-16v-16M0,12h16M12,0v16M4,0v8M4,4h8M0,8h8M8,4v8'; lfill = 'none'; break;
            case 3015: w = 6; h = 12; lines = 'M2,1A2,2,0,0,0,2,5A2,2,0,0,0,2,1M0,7A2,2,0,0,1,0,11M6,7A2,2,0,0,0,6,11'; lfill = 'none'; break;
            case 3016: w = 12; h = 7; lines = 'M0,1A3,2,0,0,1,3,3A3,2,0,0,0,9,3A3,2,0,0,1,12,1'; lfill = 'none'; break;
            case 3017: w = h = 4; lines = 'M3,1l-2,2'; break;
            case 3018: w = h = 4; lines = 'M1,1l2,2'; break;
            case 3019:
               w = h = 12;
               lines = 'M1,6A5,5,0,0,0,11,6A5,5,0,0,0,1,6h-1h1A5,5,0,0,1,6,11v1v-1A5,5,0,0,1,11,6h1h-1A5,5,0,0,1,6,1v-1v1A5,5,0,0,1,1,6';
               lfill = 'none';
               break;
            case 3020: w = 7; h = 12; lines = 'M1,0A2,3,0,0,0,3,3A2,3,0,0,1,3,9A2,3,0,0,0,1,12'; lfill = 'none'; break;
            case 3021: w = h = 8; lines = 'M8,2h-2v4h-4v2M2,0v2h-2'; lfill = 'none'; break; // left stairs
            case 3022: w = h = 8; lines = 'M0,2h2v4h4v2M6,0v2h2'; lfill = 'none'; break; // right stairs
            case 3023: w = h = 8; fills = 'M4,0h4v4zM8,4v4h-4z'; fills2 = 'M4,0L0,4L4,8L8,4Z'; break;
            case 3024: w = h = 16; fills = 'M0,8v8h2v-8zM8,0v8h2v-8M4,14v2h12v-2z'; fills2 = 'M0,2h8v6h4v-6h4v12h-12v-6h-4z'; break;
            case 3025: w = h = 18; fills = 'M5,13v-8h8ZM18,0v18h-18l5,-5h8v-8Z'; break;
            default: {
               if ((this.pattern > 3025) && (this.pattern < 3100)) {
                  // same as 3002, see TGX11.cxx, line 2234
                  w = 4; h = 2; fills = 'M1,0h1v1h-1zM3,1h1v1h-1z'; break;
               }

               const code = this.pattern % 1000,
                     k = code % 10,
                     j = ((code - k) % 100) / 10,
                     i = (code - j * 10 - k) / 100;
               if (!i) break;

               // use flexible hatches only possible when single pattern is used,
               // otherwise it is not possible to adjust pattern dimension that both hatches match with each other
               const use_new = (j === k) || (j === 0) || (j === 5) || (j === 9) || (k === 0) || (k === 5) || (k === 9),
                     pp = painter?.getPadPainter(),
                     scale_size = pp ? Math.max(pp.getPadWidth(), pp.getPadHeight()) : 600,
                     spacing_original = Math.max(0.1, gStyle.fHatchesSpacing * scale_size * 0.001),
                     hatches_spacing = Math.max(1, Math.round(spacing_original)) * 6,
                     sz = i * hatches_spacing; // axis distance between lines

               id += use_new ? `_hn${Math.round(spacing_original*100)}` : `_ho${hatches_spacing}`;

               w = h = 6 * sz; // we use at least 6 steps

               const produce_old = (dy, swap) => {
                  const pos = [];
                  let step = sz, y1 = 0, max = h, y2, x1, x2;

                  // reduce step for smaller angles to keep normal distance approx same
                  if (Math.abs(dy) < 3)
                     step = Math.round(sz / 12 * 9);
                  if (dy === 0) {
                     step = Math.round(sz / 12 * 8);
                     y1 = step / 2;
                  } else if (dy > 0)
                     max -= step;
                  else
                     y1 = step;

                  while (y1 <= max) {
                     y2 = y1 + dy * step;
                     if (y2 < 0) {
                        x2 = Math.round(y1 / (y1 - y2) * w);
                        pos.push(0, y1, x2, 0);
                        pos.push(w, h - y1, w - x2, h);
                     } else if (y2 > h) {
                        x2 = Math.round((h - y1) / (y2 - y1) * w);
                        pos.push(0, y1, x2, h);
                        pos.push(w, h - y1, w - x2, 0);
                     } else
                        pos.push(0, y1, w, y2);
                     y1 += step;
                  }
                  for (let b = 0; b < pos.length; b += 4) {
                     if (swap) {
                        x1 = pos[b+1];
                        y1 = pos[b];
                        x2 = pos[b+3];
                        y2 = pos[b+2];
                     } else {
                        x1 = pos[b];
                        y1 = pos[b+1];
                        x2 = pos[b+2];
                        y2 = pos[b+3];
                     }
                     lines += `M${x1},${y1}`;
                     if (y2 === y1)
                        lines += `h${x2-x1}`;
                     else if (x2 === x1)
                        lines += `v${y2-y1}`;
                     else
                        lines += `L${x2},${y2}`;
                  }
               },

               produce_new = (_aa, _bb, angle, swapx) => {
                  if ((angle === 0) || (angle === 90)) {
                     const dy = i*spacing_original*3,
                           nsteps = Math.round(h / dy),
                           dyreal = h / nsteps;
                     let yy = dyreal/2;

                     while (yy < h) {
                        if (angle === 0)
                           lines += `M0,${Math.round(yy)}h${w}`;
                        else
                           lines += `M${Math.round(yy)},0v${h}`;
                        yy += dyreal;
                     }

                     return;
                  }

                  const a = angle/180*Math.PI,
                        dy = i*spacing_original*3/Math.cos(a),
                        hside = Math.tan(a) * w,
                        hside_steps = Math.round(hside / dy),
                        dyreal = hside / hside_steps,
                        nsteps = Math.floor(h / dyreal);

                  h = Math.round(nsteps * dyreal);

                  let yy = nsteps * dyreal;

                  while (Math.abs(yy-h) < 0.1) yy -= dyreal;

                  while (yy + hside > 0) {
                     let x1 = 0, y1 = yy, x2 = w, y2 = yy + hside;

                     if (y1 < -0.00001) {
                        // cut at the begin
                        x1 = -y1 / hside * w;
                        y1 = 0;
                     } else if (y2 > h) {
                        // cut at the end
                        x2 = (h - y1) / hside * w;
                        y2 = h;
                     }

                     if (swapx) {
                        x1 = w - x1;
                        x2 = w - x2;
                     }

                     lines += `M${Math.round(x1)},${Math.round(y1)}L${Math.round(x2)},${Math.round(y2)}`;
                     yy -= dyreal;
                  }
               },

               func = use_new ? produce_new : produce_old;

               let horiz = false, vertical = false;

               switch (j) {
                  case 0: horiz = true; break;
                  case 1: func(1, false, 10); break;
                  case 2: func(2, false, 20); break;
                  case 3: func(3, false, 30); break;
                  case 4: func(6, false, 45); break;
                  case 6: func(3, true, 60); break;
                  case 7: func(2, true, 70); break;
                  case 8: func(1, true, 80); break;
                  case 9: vertical = true; break;
               }

               switch (k) {
                  case 0: horiz = true; break;
                  case 1: func(-1, false, 10, true); break;
                  case 2: func(-2, false, 20, true); break;
                  case 3: func(-3, false, 30, true); break;
                  case 4: func(-6, false, 45, true); break;
                  case 6: func(-3, true, 60, true); break;
                  case 7: func(-2, true, 70, true); break;
                  case 8: func(-1, true, 80, true); break;
                  case 9: vertical = true; break;
               }

               if (horiz) func(0, false, 0);
               if (vertical) func(0, true, 90);

               break;
            }
         }

         if (!fills && !lines) return false;
      }

      this.pattern_url = `url(#${id})`;
      this.antialias = false;

      let defs = svg.selectChild('.canvas_defs');
      if (defs.empty())
         defs = svg.insert('svg:defs', ':first-child').attr('class', 'canvas_defs');

      if (defs.selectChild('.' + id).empty()) {
         if (this.gradient) {
            const is_linear = this.gradient._typename === clTLinearGradient,
                  grad = defs.append(is_linear ? 'svg:linearGradient' : 'svg:radialGradient')
                             .attr('id', id).attr('class', id),
                  conv = v => { return v === Math.round(v) ? v.toFixed(0) : v.toFixed(2); };
            if (is_linear) {
               grad.attr('x1', conv(this.gradient.fStart.fX))
                   .attr('y1', conv(1 - this.gradient.fStart.fY))
                   .attr('x2', conv(this.gradient.fEnd.fX))
                   .attr('y2', conv(1 - this.gradient.fEnd.fY));
            } else {
               grad.attr('cx', conv(this.gradient.fStart.fX))
                   .attr('cy', conv(1 - this.gradient.fStart.fY))
                   .attr('cr', conv(this.gradient.fR1));
            }
            for (let n = 0; n < this.gradient.fColorPositions.length; ++n) {
               const pos = this.gradient.fColorPositions[n],
                     col = toColor(this.gradient.fColors[n*4], this.gradient.fColors[n*4+1], this.gradient.fColors[n*4+2]);
               grad.append('svg:stop').attr('offset', `${Math.round(pos*100)}%`)
                                      .attr('stop-color', col)
                                      .attr('stop-opacity', `${Math.round(this.gradient.fColors[n*4+3]*100)}%`);
            }
         } else {
            const patt = defs.append('svg:pattern')
                             .attr('id', id).attr('class', id).attr('patternUnits', 'userSpaceOnUse')
                             .attr('width', w).attr('height', h);

            if (fills2) {
               const col = d3_rgb(this.color);
               col.r = Math.round((col.r + 255) / 2); col.g = Math.round((col.g + 255) / 2); col.b = Math.round((col.b + 255) / 2);
               patt.append('svg:path').attr('d', fills2).style('fill', col);
            }
            if (fills) patt.append('svg:path').attr('d', fills).style('fill', this.color);
            if (lines) patt.append('svg:path').attr('d', lines).style('stroke', this.color).style('stroke-width', gStyle.fHatchesLineWidth || 1).style('fill', lfill);
         }
      }

      return true;
   }

   /** @summary Create sample of fill pattern inside SVG
     * @private */
   createSample(svg, width, height, plain) {
      // we need to create extra handle to change
      if (plain) svg = d3_select(svg);

      const sample = new TAttFillHandler({ svg, pattern: this.pattern, color: this.color, color_as_svg: true });

      svg.append('path')
         .attr('d', `M0,0h${width}v${height}h${-width}z`)
         .call(sample.func);
   }

   /** @summary Save fill attributes to style
     * @private */
   saveToStyle(name_color, name_pattern) {
      if (name_color) {
         const indx = this.colorindx ?? findColor(this.color);
         if (indx >= 0) gStyle[name_color] = indx;
      }
      if (name_pattern)
         gStyle[name_pattern] = this.pattern;
   }

} // class TAttFillHandler

export { TAttFillHandler };