MrDraw/src/octoprint/plugins/svgtogcode/static/js/matrix_oven.js
2016-02-16 12:53:20 +01:00

555 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Matrix Oven - a snapsvg.io plugin to apply & remove transformations from svg files.
// Copyright (C) 2015 Teja Philipp <osd@tejaphilipp.de>
//
// based on work by https://gist.github.com/timo22345/9413158
// and https://github.com/duopixel/Method-Draw/blob/master/editor/src/svgcanvas.js
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
Snap.plugin(function (Snap, Element, Paper, global) {
/**
* bakes transformations of the element and all sub-elements into coordinates
*
* @param {boolean} toCubics : use only cubic path segments
* @param {integer} dec : number of digits after decimal separator. defaults to 5
* @returns {undefined}
*/
Element.prototype.bake = function (toCubics, dec) {
var elem = this;
if (!elem || !elem.paper) // don't handle unplaced elements. this causes double handling.
return;
if (typeof (toCubics) === 'undefined')
toCubics = false;
if (typeof (dec) === 'undefined')
dec = 5;
//var children = elem.selectAll('*')
var children = elem.children();
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
child.bake(toCubics, dec);
}
elem.attr({transform: ''});
return;
}
if (elem.type !== "circle" &&
elem.type !== "rect" &&
elem.type !== "ellipse" &&
elem.type !== "line" &&
elem.type !== "polygon" &&
elem.type !== "polyline" &&
elem.type !== "image" &&
elem.type !== "path"){
// if(elem.type !== 'g' && elem.type !== 'desc' && elem.type !== 'defs')
// console.log('skipping unsupported element ', elem.type);
return;
}
if (elem.type == 'image'){
// TODO ...
var x = parseFloat(elem.attr('x')),
y = parseFloat(elem.attr('y')),
w = parseFloat(elem.attr('width')),
h = parseFloat(elem.attr('height'));
// Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
// If 'x' and 'y' are not specified, then set both to 0. // CorelDraw is creating that sometimes
if (!isFinite(x)) {
console.log('No attribute "x" in image tag. Assuming 0.')
x = 0;
}
if (!isFinite(y)) {
console.log('No attribute "y" in image tag. Assuming 0.')
y = 0;
}
var transform = elem.transform();
var matrix = transform['totalMatrix'];
var transformedX = matrix.x(x, y);
var transformedY = matrix.y(x, y);
var transformedW = matrix.x(x+w, y+h) - transformedX;
var transformedH = matrix.y(x+w, y+h) - transformedY;
elem.attr({x: transformedX, y: transformedY, width: transformedW, height: transformedH});
return;
}
//if(elem.type !== 'path') console.log("bake: converting " + elem.type + " to path");
var path_elem = elem.convertToPath();
if (!path_elem || path_elem.attr('d') === '' || path_elem.attr('d') === null)
path_elem.attr('d', 'M 0 0');
// Rounding coordinates to dec decimals
if (dec || dec === 0) {
dec = Math.min(Math.max(0,Math.floor(dec)), 15);
} else {
dec = false;
}
function r(num) {
if (dec !== false) {
return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
} else {
return num;
}
}
var arr;
var d = path_elem.attr('d');
d = (d || "").trim();
var arr_orig;
arr = Snap.parsePathString(d);
if (!toCubics) {
arr_orig = arr;
arr = Snap.path.toAbsolute(arr);
} else {
arr = Snap.path.toCubic(arr); // implies absolute coordinates
arr_orig = arr;
}
// Get the transformation matrix between SVG root element and current element
var transform = path_elem.transform();
var matrix = transform['totalMatrix'];
// apply the matrix transformation on the path segments
var j;
var m = arr.length;
var letter = '';
var letter_orig = '';
var x = 0;
var y = 0;
var new_segments = [];
var pt = {x: 0, y: 0};
var pt_baked = {};
var subpath_start = {};
var prevX = 0;
var prevY = 0;
subpath_start.x = null;
subpath_start.y = null;
for (var i=0; i < m; i++) {
letter = arr[i][0].toUpperCase();
letter_orig = arr_orig[i][0];
new_segments[i] = [];
new_segments[i][0] = arr[i][0];
if (letter === 'A') {
x = arr[i][6];
y = arr[i][7];
pt.x = arr[i][6];
pt.y = arr[i][7];
new_segments[i] = _arc_transform(arr[i][1], arr[i][2], arr[i][3], arr[i][4], arr[i][5], pt, matrix);
} else if (letter !== 'Z') {
// parse other segs than Z and A
for (j = 1; j < arr[i].length; j = j + 2) {
if (letter === 'V') {
y = arr[i][j];
} else if (letter === 'H') {
x = arr[i][j];
} else {
x = arr[i][j];
y = arr[i][j + 1];
}
pt.x = x;
pt.y = y;
pt_baked.x = matrix.x(pt.x, pt.y);
pt_baked.y = matrix.y(pt.x, pt.y);
if (letter === 'V' || letter === 'H') {
new_segments[i][0] = 'L';
new_segments[i][j] = pt_baked.x;
new_segments[i][j + 1] = pt_baked.y;
} else {
new_segments[i][j] = pt_baked.x;
new_segments[i][j + 1] = pt_baked.y;
}
}
}
if ((letter !== 'Z' && subpath_start.x === null) || letter === 'M') {
subpath_start.x = x;
subpath_start.y = y;
}
if (letter === 'Z') {
x = subpath_start.x;
y = subpath_start.y;
}
}
// Convert all that was relative back to relative
// This could be combined to above, but to make code more readable
// this is made separately.
var prevXtmp = 0;
var prevYtmp = 0;
subpath_start.x = '';
for (i = 0; i < new_segments.length; i++) {
letter_orig = arr_orig[i][0];
if (letter_orig === 'A' || letter_orig === 'M' || letter_orig === 'L' || letter_orig === 'C' || letter_orig === 'S' || letter_orig === 'Q' || letter_orig === 'T' || letter_orig === 'H' || letter_orig === 'V') {
var len = new_segments[i].length;
var lentmp = len;
if (letter_orig === 'A') {
// rounding arc parameters
// only x,y are rounded,
// other parameters are left as they are
// because they are more sensitive to rounding
new_segments[i][6] = r(new_segments[i][6]);
new_segments[i][7] = r(new_segments[i][7]);
} else {
lentmp--;
while (--lentmp){
new_segments[i][lentmp] = r(new_segments[i][lentmp]);
}
}
prevX = new_segments[i][len - 2];
prevY = new_segments[i][len - 1];
} else {
if (letter_orig === 'a') {
// same rounding treatment as above for arcs
prevXtmp = new_segments[i][6];
prevYtmp = new_segments[i][7];
new_segments[i][0] = letter_orig;
new_segments[i][6] = r(new_segments[i][6] - prevX);
new_segments[i][7] = r(new_segments[i][7] - prevY);
prevX = prevXtmp;
prevY = prevYtmp;
} else if (letter_orig === 'm' || letter_orig === 'l' || letter_orig === 'c' || letter_orig === 's' || letter_orig === 'q' || letter_orig === 't' || letter_orig === 'h' || letter_orig === 'v') {
var len = new_segments[i].length;
prevXtmp = new_segments[i][len - 2];
prevYtmp = new_segments[i][len - 1];
for (j = 1; j < len; j = j + 2) {
if (letter_orig === 'h' || letter_orig === 'v') {
new_segments[i][0] = 'l';
} else {
new_segments[i][0] = letter_orig;
}
new_segments[i][j] = r(new_segments[i][j] - prevX);
new_segments[i][j + 1] = r(new_segments[i][j + 1] - prevY);
}
prevX = prevXtmp;
prevY = prevYtmp;
}
}
if ((letter_orig.toLowerCase() !== 'z' && subpath_start.x === '') || letter_orig.toLowerCase() === 'm') {
subpath_start.x = prevX;
subpath_start.y = prevY;
}
if (letter_orig.toLowerCase() === 'z') {
prevX = subpath_start.x;
prevY = subpath_start.y;
}
}
var d_str = _convertToString(new_segments);
path_elem.attr({d: d_str});
path_elem.attr({transform: ''});
//console.log("baked matrix ", matrix, " of ", path_elem.attr('id'));
};
/**
* Helper to apply matrix transformations to arcs.
* From flatten.js (https://gist.github.com/timo22345/9413158), modified a bit.
*
* @param {type} a_rh : r1 of the ellipsis in degree
* @param {type} a_rv : r2 of the ellipsis in degree
* @param {type} a_offsetrot : x-axis rotation in degree
* @param {type} large_arc_flag : 0 or 1
* @param {int} sweep_flag : 0 or 1
* @param {object} endpoint with properties x and y
* @param {type} matrix : transformation matrix
* @returns {Array} : representing the transformed path segment
*/
function _arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix) {
function NEARZERO(B) {
return Math.abs(B) < 0.0000000000000001;
}
var m = []; // matrix representation of transformed ellipse
var A; var B; var C; // ellipse implicit equation:
var ac; var A2; var C2; // helpers for angle and halfaxis-extraction.
var rh = a_rh;
var rv = a_rv;
a_offsetrot = a_offsetrot * (Math.PI / 180); // deg->rad
var rot = a_offsetrot;
// sin/cos helper (the former offset rotation)
var s = Math.sin(rot);
var c = Math.cos(rot);
// build ellipse representation matrix (unit circle transformation).
// the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined.
m[0] = matrix.a * +rh * c + matrix.c * rh * s;
m[1] = matrix.b * +rh * c + matrix.d * rh * s;
m[2] = matrix.a * -rv * s + matrix.c * rv * c;
m[3] = matrix.b * -rv * s + matrix.d * rv * c;
// to implict equation (centered)
A = (m[0] * m[0]) + (m[2] * m[2]);
C = (m[1] * m[1]) + (m[3] * m[3]);
B = (m[0] * m[1] + m[2] * m[3]) * 2.0;
// precalculate distance A to C
ac = A - C;
// convert implicit equation to angle and halfaxis:
// disabled intentionally
if (false && NEARZERO(B)) { // there is a bug in this optimization: does not work for path below
a_offsetrot = 0;
// d="M0,350 l 50,-25
// a25,25 -30 0,1 50,-25 l 50,-25
// a25,50 -30 0,1 50,-25 l 50,-25
// a25,75 -30 0,1 50,-25 l 50,-25
// a25,100 -30 0,1 50,-25 l 50,-25"
// with matrix transform="scale(0.5,2.0)"
A2 = A;
C2 = C;
} else {
if (NEARZERO(ac)) {
A2 = A + B * 0.5;
C2 = A - B * 0.5;
a_offsetrot = Math.PI / 4.0;
} else {
// Precalculate radical:
var K = 1 + B * B / (ac * ac);
// Clamp (precision issues might need this.. not likely, but better save than sorry)
K = (K < 0) ? 0 : Math.sqrt(K);
A2 = 0.5 * (A + C + K * ac);
C2 = 0.5 * (A + C - K * ac);
a_offsetrot = 0.5 * Math.atan2(B, ac);
}
}
// This can get slightly below zero due to rounding issues.
// it's save to clamp to zero in this case (this yields a zero length halfaxis)
A2 = (A2 < 0) ? 0 : Math.sqrt(A2);
C2 = (C2 < 0) ? 0 : Math.sqrt(C2);
// now A2 and C2 are half-axis:
if (ac <= 0){
a_rv = A2;
a_rh = C2;
} else {
a_rv = C2;
a_rh = A2;
}
// If the transformation matrix contain a mirror-component
// winding order of the ellise needs to be changed.
if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0){
sweep_flag = !sweep_flag ? 1 : 0;
}
// Finally, transform arc endpoint. This takes care about the
// translational part which we ignored at the whole math-showdown above.
var baked_x = matrix.x(endpoint.x, endpoint.y);
var baked_y = matrix.y(endpoint.x, endpoint.y);
// Radians back to degrees
a_offsetrot = a_offsetrot * 180 / Math.PI;
var r = ['A', a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, baked_x, baked_y];
return r;
}
// just a helper
var _p2s = /,?([achlmqrstvxz]),?/gi;
var _convertToString = function (arr) {
return arr.join(',').replace(_p2s, '$1');
};
/**
* Replaces an element with a path of same shape.
* Supports rect, ellipse, circle, line, polyline, polygon and of course path
* The element will be replaced by the path with same id.
*
* @returns {path}
*/
Element.prototype.convertToPath = function(){
var old_element = this;
var path = old_element.toPath();
old_element.before(path);
old_element.remove();
return path;
};
/**
* Creates a path in the same shape as the origin element
* Supports rect, ellipse, circle, line, polyline, polygon and of course path
*
* based on
* https://github.com/duopixel/Method-Draw/blob/master/editor/src/svgcanvas.js
* Modifications: Timo (https://github.com/timo22345)
*
* @returns {path} path element
*/
Element.prototype.toPath = function () {
var old_element = this;
// Create new path element
var pathAttr = {};
// All attributes that path element can have
var attrs = ['requiredFeatures', 'requiredExtensions', 'systemLanguage', 'id', 'xml:base', 'xml:lang', 'xml:space', 'onfocusin', 'onfocusout', 'onactivate', 'onclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onload', 'alignment-baseline', 'baseline-shift', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'unicode-bidi', 'visibility', 'word-spacing', 'writing-mode', 'class', 'style', 'externalResourcesRequired', 'transform', 'd', 'pathLength'];
// Copy attributes of old_element to path
for(var attrIdx in attrs){
var attrName = attrs[attrIdx];
var attrValue;
if(attrName === 'transform') {
attrValue = old_element.transform()['localMatrix'];
} else {
attrValue = old_element.attr(attrName);
}
if (attrValue) {
pathAttr[attrName] = attrValue;
}
}
var d = '';
var validRadius = function (val) {
return (isFinite(val) && (val >= 0));
};
var validCoordinate = function (val) {
return (isFinite(val));
};
// Possibly the cubed root of 6, but 1.81 works best
var num = 1.81;
var tag = old_element.type;
switch (tag) {
case 'ellipse':
case 'circle':
var rx = +parseFloat(old_element.attr('rx')),
ry = +parseFloat(old_element.attr('ry')),
cx = +parseFloat(old_element.attr('cx')),
cy = +old_element.attr('cy');
if (tag === 'circle') {
rx = ry = +old_element.attr('r');
}
// If 'x' and 'y' are not specified, then set both to 0. // CorelDraw is creating that sometimes
if (!validCoordinate(cx))
cx = 0;
if (!validCoordinate(cy))
cy = 0;
d += _convertToString([
['M', (cx - rx), (cy)],
['C', (cx - rx), (cy - ry / num), (cx - rx / num), (cy - ry), (cx), (cy - ry)],
['C', (cx + rx / num), (cy - ry), (cx + rx), (cy - ry / num), (cx + rx), (cy)],
['C', (cx + rx), (cy + ry / num), (cx + rx / num), (cy + ry), (cx), (cy + ry)],
['C', (cx - rx / num), (cy + ry), (cx - rx), (cy + ry / num), (cx - rx), (cy)],
['Z']
]);
break;
case 'path':
d = old_element.attr('d');
break;
case 'line':
var x1 = parseFloat(old_element.attr('x1')),
y1 = parseFloat(old_element.attr('y1')),
x2 = parseFloat(old_element.attr('x2')),
y2 = old_element.attr('y2');
d = 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2;
break;
case 'polyline':
d = 'M' + old_element.attr('points');
break;
case 'polygon':
d = 'M' + old_element.attr('points') + 'Z';
break;
case 'rect':
// TODO ...
var rx = parseFloat(old_element.attr('rx')),
ry = parseFloat(old_element.attr('ry')),
x = parseFloat(old_element.attr('x')),
y = parseFloat(old_element.attr('y')),
w = parseFloat(old_element.attr('width')),
h = parseFloat(old_element.attr('height'));
// Validity checks from http://www.w3.org/TR/SVG/shapes.html#RectElement:
// If 'x' and 'y' are not specified, then set both to 0. // CorelDraw is creating that sometimes
if (!validCoordinate(x))
x = 0;
if (!validCoordinate(y))
y = 0;
// If neither rx nor ry are properly specified, then set both rx and ry to 0. (This will result in square corners.)
if (!validRadius(rx) && !validRadius(ry)) {
rx = ry = 0;
// Otherwise, if a properly specified value is provided for rx, but not for ry, then set both rx and ry to the value of rx.
} else if (validRadius(rx) && !validRadius(ry)) {
ry = rx;
// Otherwise, if a properly specified value is provided for ry, but not for rx, then set both rx and ry to the value of ry.
} else if (validRadius(ry) && !validRadius(rx)) {
rx = ry;
} else { // cap values for rx/ry to half of w/h
rx = Math.min(rx, w/2);
ry = Math.min(ry, h/2);
}
if (!rx && !ry) {
d += _convertToString([
['M', x, y],
['L', x + w, y],
['L', x + w, y + h],
['L', x, y + h],
['L', x, y],
['Z']
]);
} else {
var num = 2.19;
if (!ry){
ry = rx;
}
d += _convertToString([
['M', x, y + ry],
['C', x, y + ry / num, x + rx / num, y, x + rx, y],
['L', x + w - rx, y],
['C', x + w - rx / num, y, x + w, y + ry / num, x + w, y + ry],
['L', x + w, y + h - ry],
['C', x + w, y + h - ry / num, x + w - rx / num, y + h, x + w - rx, y + h],
['L', x + rx, y + h],
['C', x + rx / num, y + h, x, y + h - ry / num, x, y + h - ry],
['L', x, y + ry],
['Z']
]);
}
break;
default:
break;
}
if (d){
pathAttr.d = d;
}
var path = old_element.paper.path(pathAttr);
return path;
};
});