jsroot
Version:
JavaScript ROOT
359 lines (289 loc) • 12.5 kB
JavaScript
import { isStr, isFunc, nsREX, settings, isNodeJs, isBatchMode } from '../core.mjs';
import { FontHandler } from './FontHandler.mjs';
import { ObjectPainter } from './ObjectPainter.mjs';
import { color as d3_color } from '../d3.mjs';
const kNormal = 1, /* kLessTraffic = 2, */ kOffline = 3;
class RObjectPainter extends ObjectPainter {
#pending_request;
constructor(dom, obj, opt, csstype) {
super(dom, obj, opt);
this.csstype = csstype;
}
/** @summary Add painter to pad list of painters
* @desc For RCanvas also handles common style
* @protected */
addToPadPrimitives() {
const pp = super.addToPadPrimitives();
if (pp && !this.rstyle && pp.next_rstyle)
this.rstyle = pp.next_rstyle;
return pp;
}
/** @summary Evaluate v7 attributes using fAttr storage and configured RStyle */
v7EvalAttr(name, dflt) {
const obj = this.getObject();
if (!obj) return dflt;
if (this.cssprefix) name = this.cssprefix + name;
const type_check = res => {
if (dflt === undefined) return res;
const typ1 = typeof dflt, typ2 = typeof res;
if (typ1 === typ2) return res;
if (typ1 === 'boolean') {
if (typ2 === 'string')
return (res !== '') && (res !== '0') && (res !== 'no') && (res !== 'off');
return Boolean(res);
}
if ((typ1 === 'number') && (typ2 === 'string'))
return parseFloat(res);
return res;
};
if (obj.fAttr?.m) {
const value = obj.fAttr.m[name];
if (value) return type_check(value.v); // found value direct in attributes
}
if (this.rstyle?.fBlocks) {
const blks = this.rstyle.fBlocks;
for (let k = 0; k < blks.length; ++k) {
const block = blks[k],
match = (this.csstype && (block.selector === this.csstype)) ||
(obj.fId && (block.selector === ('#' + obj.fId))) ||
(obj.fCssClass && (block.selector === ('.' + obj.fCssClass)));
if (match && block.map?.m) {
const value = block.map.m[name.toLowerCase()];
if (value) return type_check(value.v);
}
}
}
return dflt;
}
/** @summary Set v7 attributes value */
v7SetAttr(name, value) {
const obj = this.getObject();
if (this.cssprefix) name = this.cssprefix + name;
if (obj?.fAttr?.m)
obj.fAttr.m[name] = { v: value };
}
/** @summary Decode pad length from string, return pixel value */
v7EvalLength(name, sizepx, dflt) {
if (sizepx <= 0) sizepx = 1;
const value = this.v7EvalAttr(name);
if (value === undefined)
return Math.round(dflt*sizepx);
if (typeof value === 'number')
return Math.round(value*sizepx);
if (value === null)
return 0;
let norm = 0, px = 0, val = value, operand = 0, pos = 0;
while (val) {
// skip empty spaces
while ((pos < val.length) && ((val[pos] === ' ') || (val[pos] === '\t')))
++pos;
if (pos >= val.length)
break;
if ((val[pos] === '-') || (val[pos] === '+')) {
if (operand) {
console.log(`Fail to parse RPadLength ${value}`);
return dflt;
}
operand = (val[pos] === '-') ? -1 : 1;
pos++;
continue;
}
if (pos > 0) { val = val.slice(pos); pos = 0; }
while ((pos < val.length) && (((val[pos] >= '0') && (val[pos] <= '9')) || (val[pos] === '.'))) pos++;
const v = parseFloat(val.slice(0, pos));
if (!Number.isFinite(v)) {
console.log(`Fail to parse RPadLength ${value}`);
return Math.round(dflt*sizepx);
}
val = val.slice(pos);
pos = 0;
if (!operand) operand = 1;
if (val && (val[0] === '%')) {
val = val.slice(1);
norm += operand*v*0.01;
} else if ((val.length > 1) && (val[0] === 'p') && (val[1] === 'x')) {
val = val.slice(2);
px += operand*v;
} else
norm += operand*v;
operand = 0;
}
return Math.round(norm*sizepx + px);
}
/** @summary Evaluate RColor using attribute storage and configured RStyle */
v7EvalColor(name, dflt) {
let val = this.v7EvalAttr(name, '');
if (!val || !isStr(val)) return dflt;
if (val === 'auto') {
const pp = this.getPadPainter();
if (pp?._auto_color_cnt !== undefined) {
const pal = pp.getHistPalette(),
cnt = pp._auto_color_cnt++;
let num = pp._num_primitives - 1;
if (num < 2) num = 2;
val = pal ? pal.getColorOrdinal((cnt % num) / num) : 'blue';
if (!this._auto_colors) this._auto_colors = {};
this._auto_colors[name] = val;
} else if (this._auto_colors && this._auto_colors[name])
val = this._auto_colors[name];
else {
console.error(`Autocolor ${name} not defined yet - please check code`);
val = '';
}
} else if (val[0] === '[') {
const ordinal = parseFloat(val.slice(1, val.length - 1));
val = 'black';
if (Number.isFinite(ordinal)) {
const pal = this.getPadPainter()?.getHistPalette();
if (pal) val = pal.getColorOrdinal(ordinal);
}
}
// to make colors similar in node and in pupperteer
if ((val[0] === '#') && (isNodeJs() || (isBatchMode() && settings.ApproxTextSize))) {
const col = d3_color(val);
if (col.opacity !== 1)
col.opacity = col.opacity.toFixed(2);
return col.formatRgb();
}
return val;
}
/** @summary Evaluate RAttrText properties
* @return {Object} FontHandler, can be used directly for the text drawing */
v7EvalFont(name, dflts, fontScale) {
if (!dflts)
dflts = {};
else if (typeof dflts === 'number')
dflts = { size: dflts };
const pp = this.getPadPainter(),
rfont = pp?._dfltRFont || { fFamily: 'Arial', fStyle: '', fWeight: '' },
text_angle = this.v7EvalAttr(name + '_angle', 0),
text_align = this.v7EvalAttr(name + '_align', dflts.align || 'none'),
text_color = this.v7EvalColor(name + '_color', dflts.color || 'none'),
font_family = this.v7EvalAttr(name + '_font_family', rfont.fFamily || 'Arial'),
font_style = this.v7EvalAttr(name + '_font_style', rfont.fStyle || ''),
font_weight = this.v7EvalAttr(name + '_font_weight', rfont.fWeight || '');
let text_size = this.v7EvalAttr(name + '_size', dflts.size || 12);
if (isStr(text_size)) text_size = parseFloat(text_size);
if (!Number.isFinite(text_size) || (text_size <= 0)) text_size = 12;
if (!fontScale) fontScale = pp?.getPadHeight() || 100;
const handler = new FontHandler(null, text_size, fontScale);
handler.setNameStyleWeight(font_family, font_style, font_weight);
if (text_angle) handler.setAngle(360 - text_angle);
if (text_align !== 'none') handler.setAlign(text_align);
if (text_color !== 'none') handler.setColor(text_color);
return handler;
}
/** @summary Create this.fillatt object based on v7 fill attributes */
createv7AttFill(prefix) {
if (!prefix || !isStr(prefix)) prefix = 'fill_';
const color = this.v7EvalColor(prefix + 'color', ''),
pattern = this.v7EvalAttr(prefix + 'style', 0);
this.createAttFill({ pattern, color, color_as_svg: true });
}
/** @summary Create this.lineatt object based on v7 line attributes */
createv7AttLine(prefix) {
if (!prefix || !isStr(prefix)) prefix = 'line_';
const color = this.v7EvalColor(prefix + 'color', 'black'),
width = this.v7EvalAttr(prefix + 'width', 1),
style = this.v7EvalAttr(prefix + 'style', 1);
let pattern = this.v7EvalAttr(prefix + 'pattern');
if (pattern && isNodeJs())
pattern = pattern.split(',').join(', ');
this.createAttLine({ color, width, style, pattern });
if (prefix === 'border_')
this.lineatt.setBorder(this.v7EvalAttr(prefix + 'rx', 0), this.v7EvalAttr(prefix + 'ry', 0));
}
/** @summary Create this.markeratt object based on v7 attributes */
createv7AttMarker(prefix) {
if (!prefix || !isStr(prefix)) prefix = 'marker_';
const color = this.v7EvalColor(prefix + 'color', 'black'),
size = this.v7EvalAttr(prefix + 'size', 0.01),
style = this.v7EvalAttr(prefix + 'style', 1),
refsize = (size >= 1) ? 1 : (this.getPadPainter()?.getPadHeight() || 100);
this.createAttMarker({ color, size, style, refsize });
}
/** @summary Create RChangeAttr, which can be applied on the server side
* @private */
v7AttrChange(req, name, value, kind) {
if (!this.snapid)
return false;
if (!req._typename) {
req._typename = `${nsREX}RChangeAttrRequest`;
req.ids = [];
req.names = [];
req.values = [];
req.update = true;
}
if (this.cssprefix) name = this.cssprefix + name;
req.ids.push(this.snapid);
req.names.push(name);
if ((value === null) || (value === undefined)) {
if (!kind) kind = 'none';
if (kind !== 'none') console.error(`Trying to set ${kind} for none value`);
}
if (!kind) {
switch (typeof value) {
case 'number': kind = 'double'; break;
case 'boolean': kind = 'boolean'; break;
}
}
const obj = { _typename: `${nsREX}RAttrMap::` };
switch (kind) {
case 'none': obj._typename += 'NoValue_t'; break;
case 'boolean': obj._typename += 'BoolValue_t'; obj.v = Boolean(value); break;
case 'int': obj._typename += 'IntValue_t'; obj.v = parseInt(value); break;
case 'double': obj._typename += 'DoubleValue_t'; obj.v = parseFloat(value); break;
default: obj._typename += 'StringValue_t'; obj.v = isStr(value) ? value : JSON.stringify(value); break;
}
req.values.push(obj);
return true;
}
/** @summary Sends accumulated attribute changes to server */
v7SendAttrChanges(req, do_update) {
const canp = this.getCanvPainter();
if (canp && req?._typename) {
if (do_update !== undefined)
req.update = Boolean(do_update);
canp.v7SubmitRequest('', req);
}
}
/** @summary Submit request to server-side drawable
* @param kind defines request kind, only single request a time can be submitted
* @param req is object derived from DrawableRequest, including correct _typename
* @param method is method of painter object which will be called when getting reply */
v7SubmitRequest(kind, req, method) {
const canp = this.getCanvPainter();
if (!isFunc(canp?.submitDrawableRequest)) return null;
// special situation when snapid not yet assigned - just keep ref until snapid is there
// maybe keep full list - for now not clear if really needed
if (!this.snapid) {
this.#pending_request = { kind, req, method };
return req;
}
return canp.submitDrawableRequest(kind, req, this, method);
}
/** @summary Assign snapid to the painter
* @desc Overwrite default method */
assignSnapId(id) {
this.snapid = id;
if (this.snapid && this.#pending_request) {
const p = this.#pending_request;
this.#pending_request = undefined;
this.v7SubmitRequest(p.kind, p.req, p.method);
}
}
/** @summary Return communication mode with the server
* @desc
* kOffline means no server there,
* kLessTraffic advise not to send commands if offline functionality available
* kNormal is standard functionality with RCanvas on server side */
v7CommMode() {
const canp = this.getCanvPainter();
if (!canp || !canp.submitDrawableRequest || !canp._websocket)
return kOffline;
return kNormal;
}
v7NormalMode() { return this.v7CommMode() === kNormal; }
v7OfflineMode() { return this.v7CommMode() === kOffline; }
} // class RObjectPainter
export { RObjectPainter };