/* * * * (c) 2010-2019 Torstein Honsi * * Extensions to the SVGRenderer class to enable 3D shapes * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var _Globals = require('../parts/Globals.js'); var _Globals2 = _interopRequireDefault(_Globals); var _Utilities = require('../parts/Utilities.js'); var _Utilities2 = _interopRequireDefault(_Utilities); require('../parts/Color.js'); require('../parts/SvgRenderer.js'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var defined = _Utilities2.default.defined, objectEach = _Utilities2.default.objectEach; var cos = Math.cos, PI = Math.PI, sin = Math.sin; var animObject = _Globals2.default.animObject, charts = _Globals2.default.charts, color = _Globals2.default.color, deg2rad = _Globals2.default.deg2rad, extend = _Globals2.default.extend, merge = _Globals2.default.merge, perspective = _Globals2.default.perspective, pick = _Globals2.default.pick, SVGElement = _Globals2.default.SVGElement, SVGRenderer = _Globals2.default.SVGRenderer, // internal: dFactor, element3dMethods, cuboidMethods; /* EXTENSION TO THE SVG-RENDERER TO ENABLE 3D SHAPES */ // HELPER METHODS dFactor = 4 * (Math.sqrt(2) - 1) / 3 / (PI / 2); /* eslint-disable no-invalid-this, valid-jsdoc */ /** * Method to construct a curved path. Can 'wrap' around more then 180 degrees. * @private */ function curveTo(cx, cy, rx, ry, start, end, dx, dy) { var result = [], arcAngle = end - start; if (end > start && end - start > Math.PI / 2 + 0.0001) { result = result.concat(curveTo(cx, cy, rx, ry, start, start + Math.PI / 2, dx, dy)); result = result.concat(curveTo(cx, cy, rx, ry, start + Math.PI / 2, end, dx, dy)); return result; } if (end < start && start - end > Math.PI / 2 + 0.0001) { result = result.concat(curveTo(cx, cy, rx, ry, start, start - Math.PI / 2, dx, dy)); result = result.concat(curveTo(cx, cy, rx, ry, start - Math.PI / 2, end, dx, dy)); return result; } return ['C', cx + rx * Math.cos(start) - rx * dFactor * arcAngle * Math.sin(start) + dx, cy + ry * Math.sin(start) + ry * dFactor * arcAngle * Math.cos(start) + dy, cx + rx * Math.cos(end) + rx * dFactor * arcAngle * Math.sin(end) + dx, cy + ry * Math.sin(end) - ry * dFactor * arcAngle * Math.cos(end) + dy, cx + rx * Math.cos(end) + dx, cy + ry * Math.sin(end) + dy]; } SVGRenderer.prototype.toLinePath = function (points, closed) { var result = []; // Put "L x y" for each point points.forEach(function (point) { result.push('L', point.x, point.y); }); if (points.length) { // Set the first element to M result[0] = 'M'; // If it is a closed line, add Z if (closed) { result.push('Z'); } } return result; }; SVGRenderer.prototype.toLineSegments = function (points) { var result = [], m = true; points.forEach(function (point) { result.push(m ? 'M' : 'L', point.x, point.y); m = !m; }); return result; }; // A 3-D Face is defined by it's 3D vertexes, and is only visible if it's // vertexes are counter-clockwise (Back-face culling). It is used as a // polyhedron Element SVGRenderer.prototype.face3d = function (args) { var renderer = this, ret = this.createElement('path'); ret.vertexes = []; ret.insidePlotArea = false; ret.enabled = true; ret.attr = function (hash) { if ((typeof hash === 'undefined' ? 'undefined' : _typeof(hash)) === 'object' && (defined(hash.enabled) || defined(hash.vertexes) || defined(hash.insidePlotArea))) { this.enabled = pick(hash.enabled, this.enabled); this.vertexes = pick(hash.vertexes, this.vertexes); this.insidePlotArea = pick(hash.insidePlotArea, this.insidePlotArea); delete hash.enabled; delete hash.vertexes; delete hash.insidePlotArea; var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = _Globals2.default.shapeArea(vertexes2d), visibility = this.enabled && area > 0 ? 'visible' : 'hidden'; hash.d = path; hash.visibility = visibility; } return SVGElement.prototype.attr.apply(this, arguments); }; ret.animate = function (params) { if ((typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object' && (defined(params.enabled) || defined(params.vertexes) || defined(params.insidePlotArea))) { this.enabled = pick(params.enabled, this.enabled); this.vertexes = pick(params.vertexes, this.vertexes); this.insidePlotArea = pick(params.insidePlotArea, this.insidePlotArea); delete params.enabled; delete params.vertexes; delete params.insidePlotArea; var chart = charts[renderer.chartIndex], vertexes2d = perspective(this.vertexes, chart, this.insidePlotArea), path = renderer.toLinePath(vertexes2d, true), area = _Globals2.default.shapeArea(vertexes2d), visibility = this.enabled && area > 0 ? 'visible' : 'hidden'; params.d = path; this.attr('visibility', visibility); } return SVGElement.prototype.animate.apply(this, arguments); }; return ret.attr(args); }; // A Polyhedron is a handy way of defining a group of 3-D faces. It's only // attribute is `faces`, an array of attributes of each one of it's Face3D // instances. SVGRenderer.prototype.polyhedron = function (args) { var renderer = this, result = this.g(), destroy = result.destroy; if (!this.styledMode) { result.attr({ 'stroke-linejoin': 'round' }); } result.faces = []; // destroy all children result.destroy = function () { for (var i = 0; i < result.faces.length; i++) { result.faces[i].destroy(); } return destroy.call(this); }; result.attr = function (hash, val, complete, continueAnimation) { if ((typeof hash === 'undefined' ? 'undefined' : _typeof(hash)) === 'object' && defined(hash.faces)) { while (result.faces.length > hash.faces.length) { result.faces.pop().destroy(); } while (result.faces.length < hash.faces.length) { result.faces.push(renderer.face3d().add(result)); } for (var i = 0; i < hash.faces.length; i++) { if (renderer.styledMode) { delete hash.faces[i].fill; } result.faces[i].attr(hash.faces[i], null, complete, continueAnimation); } delete hash.faces; } return SVGElement.prototype.attr.apply(this, arguments); }; result.animate = function (params, duration, complete) { if (params && params.faces) { while (result.faces.length > params.faces.length) { result.faces.pop().destroy(); } while (result.faces.length < params.faces.length) { result.faces.push(renderer.face3d().add(result)); } for (var i = 0; i < params.faces.length; i++) { result.faces[i].animate(params.faces[i], duration, complete); } delete params.faces; } return SVGElement.prototype.animate.apply(this, arguments); }; return result.attr(args); }; // Base, abstract prototype member for 3D elements element3dMethods = { /** * The init is used by base - renderer.Element * @private */ initArgs: function initArgs(args) { var elem3d = this, renderer = elem3d.renderer, paths = renderer[elem3d.pathType + 'Path'](args), zIndexes = paths.zIndexes; // build parts elem3d.parts.forEach(function (part) { elem3d[part] = renderer.path(paths[part]).attr({ 'class': 'highcharts-3d-' + part, zIndex: zIndexes[part] || 0 }).add(elem3d); }); elem3d.attr({ 'stroke-linejoin': 'round', zIndex: zIndexes.group }); // store original destroy elem3d.originalDestroy = elem3d.destroy; elem3d.destroy = elem3d.destroyParts; }, /** * Single property setter that applies options to each part * @private */ singleSetterForParts: function singleSetterForParts(prop, val, values, verb, duration, complete) { var elem3d = this, newAttr = {}, optionsToApply = [null, null, verb || 'attr', duration, complete], hasZIndexes = values && values.zIndexes; if (!values) { newAttr[prop] = val; optionsToApply[0] = newAttr; } else { objectEach(values, function (partVal, part) { newAttr[part] = {}; newAttr[part][prop] = partVal; // include zIndexes if provided if (hasZIndexes) { newAttr[part].zIndex = values.zIndexes[part] || 0; } }); optionsToApply[1] = newAttr; } return elem3d.processParts.apply(elem3d, optionsToApply); }, /** * Calls function for each part. Used for attr, animate and destroy. * @private */ processParts: function processParts(props, partsProps, verb, duration, complete) { var elem3d = this; elem3d.parts.forEach(function (part) { // if different props for different parts if (partsProps) { props = _Globals2.default.pick(partsProps[part], false); } // only if something to set, but allow undefined if (props !== false) { elem3d[part][verb](props, duration, complete); } }); return elem3d; }, /** * Destroy all parts * @private */ destroyParts: function destroyParts() { this.processParts(null, null, 'destroy'); return this.originalDestroy(); } }; // CUBOID cuboidMethods = _Globals2.default.merge(element3dMethods, { parts: ['front', 'top', 'side'], pathType: 'cuboid', attr: function attr(args, val, complete, continueAnimation) { // Resolve setting attributes by string name if (typeof args === 'string' && typeof val !== 'undefined') { var key = args; args = {}; args[key] = val; } if (args.shapeArgs || defined(args.x)) { return this.singleSetterForParts('d', null, this.renderer[this.pathType + 'Path'](args.shapeArgs || args)); } return SVGElement.prototype.attr.call(this, args, undefined, complete, continueAnimation); }, animate: function animate(args, duration, complete) { if (defined(args.x) && defined(args.y)) { var paths = this.renderer[this.pathType + 'Path'](args); this.singleSetterForParts('d', null, paths, 'animate', duration, complete); this.attr({ zIndex: paths.zIndexes.group }); } else { SVGElement.prototype.animate.call(this, args, duration, complete); } return this; }, fillSetter: function fillSetter(fill) { this.singleSetterForParts('fill', null, { front: fill, top: color(fill).brighten(0.1).get(), side: color(fill).brighten(-0.1).get() }); // fill for animation getter (#6776) this.color = this.fill = fill; return this; } }); // set them up SVGRenderer.prototype.elements3d = { base: element3dMethods, cuboid: cuboidMethods }; /** * return result, generalization * @private */ SVGRenderer.prototype.element3d = function (type, shapeArgs) { // base var ret = this.g(); // extend _Globals2.default.extend(ret, this.elements3d[type]); // init ret.initArgs(shapeArgs); // return return ret; }; // generelized, so now use simply SVGRenderer.prototype.cuboid = function (shapeArgs) { return this.element3d('cuboid', shapeArgs); }; // Generates a cuboid path and zIndexes _Globals2.default.SVGRenderer.prototype.cuboidPath = function (shapeArgs) { var x = shapeArgs.x, y = shapeArgs.y, z = shapeArgs.z, h = shapeArgs.height, w = shapeArgs.width, d = shapeArgs.depth, chart = charts[this.chartIndex], front, back, top, bottom, left, right, shape, path1, path2, path3, isFront, isTop, isRight, options3d = chart.options.chart.options3d, alpha = options3d.alpha, // Priority for x axis is the biggest, // because of x direction has biggest influence on zIndex incrementX = 10000, // y axis has the smallest priority in case of our charts // (needs to be set because of stacking) incrementY = 10, incrementZ = 100, zIndex = 0, // The 8 corners of the cube pArr = [{ x: x, y: y, z: z }, { x: x + w, y: y, z: z }, { x: x + w, y: y + h, z: z }, { x: x, y: y + h, z: z }, { x: x, y: y + h, z: z + d }, { x: x + w, y: y + h, z: z + d }, { x: x + w, y: y, z: z + d }, { x: x, y: y, z: z + d }], pickShape; // apply perspective pArr = perspective(pArr, chart, shapeArgs.insidePlotArea); /** * helper method to decide which side is visible * @private */ function mapPath(i) { return pArr[i]; } /** * First value - path with specific side * Second value - added information about side for later calculations. * Possible second values are 0 for path1, 1 for path2 and -1 for no path * chosen. * @private */ pickShape = function pickShape(path1, path2) { var ret = [[], -1]; path1 = path1.map(mapPath); path2 = path2.map(mapPath); if (_Globals2.default.shapeArea(path1) < 0) { ret = [path1, 0]; } else if (_Globals2.default.shapeArea(path2) < 0) { ret = [path2, 1]; } return ret; }; // front or back front = [3, 2, 1, 0]; back = [7, 6, 5, 4]; shape = pickShape(front, back); path1 = shape[0]; isFront = shape[1]; // top or bottom top = [1, 6, 7, 0]; bottom = [4, 5, 2, 3]; shape = pickShape(top, bottom); path2 = shape[0]; isTop = shape[1]; // side right = [1, 2, 5, 6]; left = [0, 7, 4, 3]; shape = pickShape(right, left); path3 = shape[0]; isRight = shape[1]; /* New block used for calculating zIndex. It is basing on X, Y and Z position of specific columns. All zIndexes (for X, Y and Z values) are added to the final zIndex, where every value has different priority. The biggest priority is in X and Z directions, the lowest index is for stacked columns (Y direction and the same X and Z positions). Big differences between priorities is made because we need to ensure that even for big changes in Y and Z parameters all columns will be drawn correctly. */ if (isRight === 1) { zIndex += incrementX * (1000 - x); } else if (!isRight) { zIndex += incrementX * x; } zIndex += incrementY * (!isTop || // Numbers checked empirically alpha >= 0 && alpha <= 180 || alpha < 360 && alpha > 357.5 ? chart.plotHeight - y : 10 + y); if (isFront === 1) { zIndex += incrementZ * z; } else if (!isFront) { zIndex += incrementZ * (1000 - z); } return { front: this.toLinePath(path1, true), top: this.toLinePath(path2, true), side: this.toLinePath(path3, true), zIndexes: { group: Math.round(zIndex) }, // additional info about zIndexes isFront: isFront, isTop: isTop }; // #4774 }; // SECTORS // _Globals2.default.SVGRenderer.prototype.arc3d = function (attribs) { var wrapper = this.g(), renderer = wrapper.renderer, customAttribs = ['x', 'y', 'r', 'innerR', 'start', 'end']; /** * Get custom attributes. Don't mutate the original object and return an * object with only custom attr. * @private */ function suckOutCustom(params) { var hasCA = false, ca = {}, key; params = merge(params); // Don't mutate the original object for (key in params) { if (customAttribs.indexOf(key) !== -1) { ca[key] = params[key]; delete params[key]; hasCA = true; } } return hasCA ? ca : false; } attribs = merge(attribs); attribs.alpha = (attribs.alpha || 0) * deg2rad; attribs.beta = (attribs.beta || 0) * deg2rad; // Create the different sub sections of the shape wrapper.top = renderer.path(); wrapper.side1 = renderer.path(); wrapper.side2 = renderer.path(); wrapper.inn = renderer.path(); wrapper.out = renderer.path(); // Add all faces wrapper.onAdd = function () { var parent = wrapper.parentGroup, className = wrapper.attr('class'); wrapper.top.add(wrapper); // These faces are added outside the wrapper group because the z index // relates to neighbour elements as well ['out', 'inn', 'side1', 'side2'].forEach(function (face) { wrapper[face].attr({ 'class': className + ' highcharts-3d-side' }).add(parent); }); }; // Cascade to faces ['addClass', 'removeClass'].forEach(function (fn) { wrapper[fn] = function () { var args = arguments; ['top', 'out', 'inn', 'side1', 'side2'].forEach(function (face) { wrapper[face][fn].apply(wrapper[face], args); }); }; }); /** * Compute the transformed paths and set them to the composite shapes * @private */ wrapper.setPaths = function (attribs) { var paths = wrapper.renderer.arc3dPath(attribs), zIndex = paths.zTop * 100; wrapper.attribs = attribs; wrapper.top.attr({ d: paths.top, zIndex: paths.zTop }); wrapper.inn.attr({ d: paths.inn, zIndex: paths.zInn }); wrapper.out.attr({ d: paths.out, zIndex: paths.zOut }); wrapper.side1.attr({ d: paths.side1, zIndex: paths.zSide1 }); wrapper.side2.attr({ d: paths.side2, zIndex: paths.zSide2 }); // show all children wrapper.zIndex = zIndex; wrapper.attr({ zIndex: zIndex }); // Set the radial gradient center the first time if (attribs.center) { wrapper.top.setRadialReference(attribs.center); delete attribs.center; } }; wrapper.setPaths(attribs); /** * Apply the fill to the top and a darker shade to the sides * @private */ wrapper.fillSetter = function (value) { var darker = color(value).brighten(-0.1).get(); this.fill = value; this.side1.attr({ fill: darker }); this.side2.attr({ fill: darker }); this.inn.attr({ fill: darker }); this.out.attr({ fill: darker }); this.top.attr({ fill: value }); return this; }; // Apply the same value to all. These properties cascade down to the // children when set to the composite arc3d. ['opacity', 'translateX', 'translateY', 'visibility'].forEach(function (setter) { wrapper[setter + 'Setter'] = function (value, key) { wrapper[key] = value; ['out', 'inn', 'side1', 'side2', 'top'].forEach(function (el) { wrapper[el].attr(key, value); }); }; }); // Override attr to remove shape attributes and use those to set child paths wrapper.attr = function (params) { var ca; if ((typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') { ca = suckOutCustom(params); if (ca) { extend(wrapper.attribs, ca); wrapper.setPaths(wrapper.attribs); } } return SVGElement.prototype.attr.apply(wrapper, arguments); }; // Override the animate function by sucking out custom parameters related to // the shapes directly, and update the shapes from the animation step. wrapper.animate = function (params, animation, complete) { var ca, from = this.attribs, to, anim, randomProp = 'data-' + Math.random().toString(26).substring(2, 9); // Attribute-line properties connected to 3D. These shouldn't have been // in the attribs collection in the first place. delete params.center; delete params.z; delete params.depth; delete params.alpha; delete params.beta; anim = animObject(pick(animation, this.renderer.globalAnimation)); if (anim.duration) { ca = suckOutCustom(params); // Params need to have a property in order for the step to run // (#5765, #7097, #7437) wrapper[randomProp] = 0; params[randomProp] = 1; wrapper[randomProp + 'Setter'] = _Globals2.default.noop; if (ca) { to = ca; anim.step = function (a, fx) { /** * @private */ function interpolate(key) { return from[key] + (pick(to[key], from[key]) - from[key]) * fx.pos; } if (fx.prop === randomProp) { fx.elem.setPaths(merge(from, { x: interpolate('x'), y: interpolate('y'), r: interpolate('r'), innerR: interpolate('innerR'), start: interpolate('start'), end: interpolate('end') })); } }; } animation = anim; // Only when duration (#5572) } return SVGElement.prototype.animate.call(this, params, animation, complete); }; // destroy all children wrapper.destroy = function () { this.top.destroy(); this.out.destroy(); this.inn.destroy(); this.side1.destroy(); this.side2.destroy(); return SVGElement.prototype.destroy.call(this); }; // hide all children wrapper.hide = function () { this.top.hide(); this.out.hide(); this.inn.hide(); this.side1.hide(); this.side2.hide(); }; wrapper.show = function (inherit) { this.top.show(inherit); this.out.show(inherit); this.inn.show(inherit); this.side1.show(inherit); this.side2.show(inherit); }; return wrapper; }; // Generate the paths required to draw a 3D arc SVGRenderer.prototype.arc3dPath = function (shapeArgs) { var cx = shapeArgs.x, // x coordinate of the center cy = shapeArgs.y, // y coordinate of the center start = shapeArgs.start, // start angle end = shapeArgs.end - 0.00001, // end angle r = shapeArgs.r, // radius ir = shapeArgs.innerR || 0, // inner radius d = shapeArgs.depth || 0, // depth alpha = shapeArgs.alpha, // alpha rotation of the chart beta = shapeArgs.beta; // beta rotation of the chart // Derived Variables var cs = Math.cos(start), // cosinus of the start angle ss = Math.sin(start), // sinus of the start angle ce = Math.cos(end), // cosinus of the end angle se = Math.sin(end), // sinus of the end angle rx = r * Math.cos(beta), // x-radius ry = r * Math.cos(alpha), // y-radius irx = ir * Math.cos(beta), // x-radius (inner) iry = ir * Math.cos(alpha), // y-radius (inner) dx = d * Math.sin(beta), // distance between top and bottom in x dy = d * Math.sin(alpha); // distance between top and bottom in y // TOP var top = ['M', cx + rx * cs, cy + ry * ss]; top = top.concat(curveTo(cx, cy, rx, ry, start, end, 0, 0)); top = top.concat(['L', cx + irx * ce, cy + iry * se]); top = top.concat(curveTo(cx, cy, irx, iry, end, start, 0, 0)); top = top.concat(['Z']); // OUTSIDE var b = beta > 0 ? Math.PI / 2 : 0, a = alpha > 0 ? 0 : Math.PI / 2; var start2 = start > -b ? start : end > -b ? -b : start, end2 = end < PI - a ? end : start < PI - a ? PI - a : end, midEnd = 2 * PI - a; // When slice goes over bottom middle, need to add both, left and right // outer side. Additionally, when we cross right hand edge, create sharp // edge. Outer shape/wall: // // ------- // / ^ \ // 4) / / \ \ 1) // / / \ \ // / / \ \ // (c)=> ==== ==== <=(d) // \ \ / / // \ \<=(a)/ / // \ \ / / <=(b) // 3) \ v / 2) // ------- // // (a) - inner side // (b) - outer side // (c) - left edge (sharp) // (d) - right edge (sharp) // 1..n - rendering order for startAngle = 0, when set to e.g 90, order // changes clockwise (1->2, 2->3, n->1) and counterclockwise for negative // startAngle var out = ['M', cx + rx * cos(start2), cy + ry * sin(start2)]; out = out.concat(curveTo(cx, cy, rx, ry, start2, end2, 0, 0)); // When shape is wide, it can cross both, (c) and (d) edges, when using // startAngle if (end > midEnd && start < midEnd) { // Go to outer side out = out.concat(['L', cx + rx * cos(end2) + dx, cy + ry * sin(end2) + dy]); // Curve to the right edge of the slice (d) out = out.concat(curveTo(cx, cy, rx, ry, end2, midEnd, dx, dy)); // Go to the inner side out = out.concat(['L', cx + rx * cos(midEnd), cy + ry * sin(midEnd)]); // Curve to the true end of the slice out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end, 0, 0)); // Go to the outer side out = out.concat(['L', cx + rx * cos(end) + dx, cy + ry * sin(end) + dy]); // Go back to middle (d) out = out.concat(curveTo(cx, cy, rx, ry, end, midEnd, dx, dy)); out = out.concat(['L', cx + rx * cos(midEnd), cy + ry * sin(midEnd)]); // Go back to the left edge out = out.concat(curveTo(cx, cy, rx, ry, midEnd, end2, 0, 0)); // But shape can cross also only (c) edge: } else if (end > PI - a && start < PI - a) { // Go to outer side out = out.concat(['L', cx + rx * Math.cos(end2) + dx, cy + ry * Math.sin(end2) + dy]); // Curve to the true end of the slice out = out.concat(curveTo(cx, cy, rx, ry, end2, end, dx, dy)); // Go to the inner side out = out.concat(['L', cx + rx * Math.cos(end), cy + ry * Math.sin(end)]); // Go back to the artifical end2 out = out.concat(curveTo(cx, cy, rx, ry, end, end2, 0, 0)); } out = out.concat(['L', cx + rx * Math.cos(end2) + dx, cy + ry * Math.sin(end2) + dy]); out = out.concat(curveTo(cx, cy, rx, ry, end2, start2, dx, dy)); out = out.concat(['Z']); // INSIDE var inn = ['M', cx + irx * cs, cy + iry * ss]; inn = inn.concat(curveTo(cx, cy, irx, iry, start, end, 0, 0)); inn = inn.concat(['L', cx + irx * Math.cos(end) + dx, cy + iry * Math.sin(end) + dy]); inn = inn.concat(curveTo(cx, cy, irx, iry, end, start, dx, dy)); inn = inn.concat(['Z']); // SIDES var side1 = ['M', cx + rx * cs, cy + ry * ss, 'L', cx + rx * cs + dx, cy + ry * ss + dy, 'L', cx + irx * cs + dx, cy + iry * ss + dy, 'L', cx + irx * cs, cy + iry * ss, 'Z']; var side2 = ['M', cx + rx * ce, cy + ry * se, 'L', cx + rx * ce + dx, cy + ry * se + dy, 'L', cx + irx * ce + dx, cy + iry * se + dy, 'L', cx + irx * ce, cy + iry * se, 'Z']; // correction for changed position of vanishing point caused by alpha and // beta rotations var angleCorr = Math.atan2(dy, -dx), angleEnd = Math.abs(end + angleCorr), angleStart = Math.abs(start + angleCorr), angleMid = Math.abs((start + end) / 2 + angleCorr); /** * set to 0-PI range * @private */ function toZeroPIRange(angle) { angle = angle % (2 * Math.PI); if (angle > Math.PI) { angle = 2 * Math.PI - angle; } return angle; } angleEnd = toZeroPIRange(angleEnd); angleStart = toZeroPIRange(angleStart); angleMid = toZeroPIRange(angleMid); // *1e5 is to compensate pInt in zIndexSetter var incPrecision = 1e5, a1 = angleMid * incPrecision, a2 = angleStart * incPrecision, a3 = angleEnd * incPrecision; return { top: top, // max angle is PI, so this is always higher zTop: Math.PI * incPrecision + 1, out: out, zOut: Math.max(a1, a2, a3), inn: inn, zInn: Math.max(a1, a2, a3), side1: side1, zSide1: a3 * 0.99, side2: side2, zSide2: a2 * 0.99 }; };