UNPKG

jsroot

Version:
588 lines (511 loc) 21.3 kB
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 { #disabled; // if fill disabled /** @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.#disabled) { 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) { this.#disabled = ((on === undefined) || on) ? undefined : 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?.getCanvSvg(), 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 = 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: // bricks w = h = 10; lines = 'M0,2h10M0,7h10M2,0v2M7,2v5M2,7v3'; break; 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: // left stairs w = h = 8; lines = 'M8,2h-2v4h-4v2M2,0v2h-2'; lfill = 'none'; break; case 3022: // right stairs w = h = 8; lines = 'M0,2h2v4h4v2M6,0v2h2'; lfill = 'none'; break; 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}`; } }; /* eslint-disable-next-line one-var */ const 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; } }; /* eslint-disable-next-line one-var */ const 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 };