2018-05-18 03:25:45 +00:00
|
|
|
/* globals jQuery */
|
2013-02-20 06:29:25 +00:00
|
|
|
/**
|
|
|
|
* Recalculate.
|
|
|
|
*
|
|
|
|
* Licensed under the MIT License
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
|
|
|
// Dependencies:
|
- Breaking change: Rename config file to `svgedit-config-iife.js` (or for the module version, `svgedit-config-es.js`);
also expect one directory higher; incorporates #207 (@iuyiuy)
- Breaking change: Separate `extIconsPath` from `extPath` (not copying over icons)
- Breaking change: Don't reference `custom.css` in HTML; can instead be referenced in JavaScript through
the config file (provided in `svgedit-config-sample-iife.js`/`svgedit-config-sample-es.js` as `svgedit-custom.css` for
better namespacing); incorporates #207 (@iuyiuy)
- Breaking change: Remove minified jgraduate/spinbtn files (minified within Rollup routine)
- Fix: Zoom when scrolled; incorporates #169 (@AndrolGenhald), adapting for conventions; also allow avoidance when shift key pressed
- Fix: Update Atom feed reference in HTML
- Fixes related to recent commits: Some path and method name fixes needed, function order, missing methods, variable scope declaration, no need for DOMContentLoaded listeners in modules, switch back to non-default export, avoid trimming nullish, deal with mock tests, fix `math.matrixMultiply`, use jquery-svg where needed for array/SVG attributes; add babel-polyfill and defer script to imagelib; other misc. fixes
- Enhancement: Move config-sample.js out of `editor` directory
- Enhancement: For `callback`-style extensions, also provide config object; add following
to that object: buildCanvgCallback, canvg, decode64, encode64, executeAfterLoads, getTypeMap, isChrome, ieIE, NS, text2xml
- Enhancement: Complete ES6 modules work (extensions, locales, tests), along with Babel;
make Node build routine for converting modular source to non-modular,
use `loadStylesheets` for modular stylehsheet defining (but parallel loading);
- Enhancement: Add `stylesheets` config for modular but parallel stylesheet loading with `@default` option for simple inclusion/exclusion of defaults (if not going with default).
- Refactoring: Clean up `svg-editor.html`: consistent indents; avoid extra lbs, avoid long lines
- Refactoring: Avoid embedded API adding inline JavaScript listener
- Refactoring: Move layers and context code to `draw.js`
- Refactoring: Move `pathActions` from `svgcanvas.js` (though preserve aliases to these methods on `canvas`) and `convertPath` from `svgutils.js` to `path.js`
- Refactoring: Move `getStrokedBBox` from `svgcanvas.js` (while keeping an alias) to `svgutils.js` (as `getStrokedBBoxDefaultVisible` to avoid conflict with existing)
- Docs: Remove "dependencies" comments in code except where summarizing role of jQuery or a non-obvious dependency
- Refactoring/Linting: Enfore `no-extra-semi` and `quote-props` rules
- Refactoring: Further avoidance of quotes on properties (as possible)
- Refactoring: Use `class` in place of functions where intended as classes
- Refactoring: Consistency and granularity in extensions imports
- Testing: Update QUnit to 2.6.1 (node_modules) and Sinon to 5.0.8 (and add sinon-test at 2.1.3) and enforce eslint-plugin-qunit linting rules; update custom extensions
- Testing: Add node-static for automating (and accessing out-of-directory contents)
- Testing: Avoid HTML attributes for styling
- Testing: Add npm `test` script
- Testing: Comment out unused jQuery SVG test
- Testing: Add test1 and svgutils_performance_test to all tests page
- Testing: Due apparently to Path having not been a formal class, the test was calling it without `new`; refactored now with sufficient mock data to take into account it is a class
- npm: Update devDeps
- npm: Add html modules and config build to test script
2018-05-22 10:03:16 +00:00
|
|
|
// 1) jquery-svg.js
|
2018-05-18 03:25:45 +00:00
|
|
|
|
- Breaking change: Rename config file to `svgedit-config-iife.js` (or for the module version, `svgedit-config-es.js`);
also expect one directory higher; incorporates #207 (@iuyiuy)
- Breaking change: Separate `extIconsPath` from `extPath` (not copying over icons)
- Breaking change: Don't reference `custom.css` in HTML; can instead be referenced in JavaScript through
the config file (provided in `svgedit-config-sample-iife.js`/`svgedit-config-sample-es.js` as `svgedit-custom.css` for
better namespacing); incorporates #207 (@iuyiuy)
- Breaking change: Remove minified jgraduate/spinbtn files (minified within Rollup routine)
- Fix: Zoom when scrolled; incorporates #169 (@AndrolGenhald), adapting for conventions; also allow avoidance when shift key pressed
- Fix: Update Atom feed reference in HTML
- Fixes related to recent commits: Some path and method name fixes needed, function order, missing methods, variable scope declaration, no need for DOMContentLoaded listeners in modules, switch back to non-default export, avoid trimming nullish, deal with mock tests, fix `math.matrixMultiply`, use jquery-svg where needed for array/SVG attributes; add babel-polyfill and defer script to imagelib; other misc. fixes
- Enhancement: Move config-sample.js out of `editor` directory
- Enhancement: For `callback`-style extensions, also provide config object; add following
to that object: buildCanvgCallback, canvg, decode64, encode64, executeAfterLoads, getTypeMap, isChrome, ieIE, NS, text2xml
- Enhancement: Complete ES6 modules work (extensions, locales, tests), along with Babel;
make Node build routine for converting modular source to non-modular,
use `loadStylesheets` for modular stylehsheet defining (but parallel loading);
- Enhancement: Add `stylesheets` config for modular but parallel stylesheet loading with `@default` option for simple inclusion/exclusion of defaults (if not going with default).
- Refactoring: Clean up `svg-editor.html`: consistent indents; avoid extra lbs, avoid long lines
- Refactoring: Avoid embedded API adding inline JavaScript listener
- Refactoring: Move layers and context code to `draw.js`
- Refactoring: Move `pathActions` from `svgcanvas.js` (though preserve aliases to these methods on `canvas`) and `convertPath` from `svgutils.js` to `path.js`
- Refactoring: Move `getStrokedBBox` from `svgcanvas.js` (while keeping an alias) to `svgutils.js` (as `getStrokedBBoxDefaultVisible` to avoid conflict with existing)
- Docs: Remove "dependencies" comments in code except where summarizing role of jQuery or a non-obvious dependency
- Refactoring/Linting: Enfore `no-extra-semi` and `quote-props` rules
- Refactoring: Further avoidance of quotes on properties (as possible)
- Refactoring: Use `class` in place of functions where intended as classes
- Refactoring: Consistency and granularity in extensions imports
- Testing: Update QUnit to 2.6.1 (node_modules) and Sinon to 5.0.8 (and add sinon-test at 2.1.3) and enforce eslint-plugin-qunit linting rules; update custom extensions
- Testing: Add node-static for automating (and accessing out-of-directory contents)
- Testing: Avoid HTML attributes for styling
- Testing: Add npm `test` script
- Testing: Comment out unused jQuery SVG test
- Testing: Add test1 and svgutils_performance_test to all tests page
- Testing: Due apparently to Path having not been a formal class, the test was calling it without `new`; refactored now with sufficient mock data to take into account it is a class
- npm: Update devDeps
- npm: Add html modules and config build to test script
2018-05-22 10:03:16 +00:00
|
|
|
import jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr`
|
2018-05-18 03:25:45 +00:00
|
|
|
import {NS} from './svgedit.js';
|
|
|
|
import {convertToNum} from './units.js';
|
|
|
|
import {isWebkit} from './browser.js';
|
|
|
|
import {getTransformList} from './svgtransformlist.js';
|
|
|
|
import {getRotationAngle, getHref, getBBox, getRefElem} from './svgutils.js';
|
|
|
|
import {BatchCommand, ChangeElementCommand} from './history.js';
|
|
|
|
import {remapElement} from './coords.js';
|
|
|
|
import {
|
|
|
|
isIdentity, matrixMultiply, transformPoint, transformListToTransform,
|
|
|
|
hasMatrixTransform
|
|
|
|
} from './math.js';
|
|
|
|
|
- Breaking change: Rename config file to `svgedit-config-iife.js` (or for the module version, `svgedit-config-es.js`);
also expect one directory higher; incorporates #207 (@iuyiuy)
- Breaking change: Separate `extIconsPath` from `extPath` (not copying over icons)
- Breaking change: Don't reference `custom.css` in HTML; can instead be referenced in JavaScript through
the config file (provided in `svgedit-config-sample-iife.js`/`svgedit-config-sample-es.js` as `svgedit-custom.css` for
better namespacing); incorporates #207 (@iuyiuy)
- Breaking change: Remove minified jgraduate/spinbtn files (minified within Rollup routine)
- Fix: Zoom when scrolled; incorporates #169 (@AndrolGenhald), adapting for conventions; also allow avoidance when shift key pressed
- Fix: Update Atom feed reference in HTML
- Fixes related to recent commits: Some path and method name fixes needed, function order, missing methods, variable scope declaration, no need for DOMContentLoaded listeners in modules, switch back to non-default export, avoid trimming nullish, deal with mock tests, fix `math.matrixMultiply`, use jquery-svg where needed for array/SVG attributes; add babel-polyfill and defer script to imagelib; other misc. fixes
- Enhancement: Move config-sample.js out of `editor` directory
- Enhancement: For `callback`-style extensions, also provide config object; add following
to that object: buildCanvgCallback, canvg, decode64, encode64, executeAfterLoads, getTypeMap, isChrome, ieIE, NS, text2xml
- Enhancement: Complete ES6 modules work (extensions, locales, tests), along with Babel;
make Node build routine for converting modular source to non-modular,
use `loadStylesheets` for modular stylehsheet defining (but parallel loading);
- Enhancement: Add `stylesheets` config for modular but parallel stylesheet loading with `@default` option for simple inclusion/exclusion of defaults (if not going with default).
- Refactoring: Clean up `svg-editor.html`: consistent indents; avoid extra lbs, avoid long lines
- Refactoring: Avoid embedded API adding inline JavaScript listener
- Refactoring: Move layers and context code to `draw.js`
- Refactoring: Move `pathActions` from `svgcanvas.js` (though preserve aliases to these methods on `canvas`) and `convertPath` from `svgutils.js` to `path.js`
- Refactoring: Move `getStrokedBBox` from `svgcanvas.js` (while keeping an alias) to `svgutils.js` (as `getStrokedBBoxDefaultVisible` to avoid conflict with existing)
- Docs: Remove "dependencies" comments in code except where summarizing role of jQuery or a non-obvious dependency
- Refactoring/Linting: Enfore `no-extra-semi` and `quote-props` rules
- Refactoring: Further avoidance of quotes on properties (as possible)
- Refactoring: Use `class` in place of functions where intended as classes
- Refactoring: Consistency and granularity in extensions imports
- Testing: Update QUnit to 2.6.1 (node_modules) and Sinon to 5.0.8 (and add sinon-test at 2.1.3) and enforce eslint-plugin-qunit linting rules; update custom extensions
- Testing: Add node-static for automating (and accessing out-of-directory contents)
- Testing: Avoid HTML attributes for styling
- Testing: Add npm `test` script
- Testing: Comment out unused jQuery SVG test
- Testing: Add test1 and svgutils_performance_test to all tests page
- Testing: Due apparently to Path having not been a formal class, the test was calling it without `new`; refactored now with sufficient mock data to take into account it is a class
- npm: Update devDeps
- npm: Add html modules and config build to test script
2018-05-22 10:03:16 +00:00
|
|
|
const $ = jqPluginSVG(jQuery);
|
2018-05-18 03:25:45 +00:00
|
|
|
|
|
|
|
let context_;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param editorContext
|
|
|
|
*/
|
|
|
|
export const init = function (editorContext) {
|
2018-05-18 04:02:30 +00:00
|
|
|
context_ = editorContext;
|
2013-02-20 06:29:25 +00:00
|
|
|
};
|
|
|
|
|
2018-05-29 12:31:24 +00:00
|
|
|
/**
|
|
|
|
* Updates a <clipPath>s values based on the given translation of an element
|
|
|
|
* @param attr - The clip-path attribute value with the clipPath's ID
|
|
|
|
* @param tx - The translation's x value
|
|
|
|
* @param ty - The translation's y value
|
|
|
|
*/
|
2018-05-18 03:25:45 +00:00
|
|
|
export const updateClipPath = function (attr, tx, ty) {
|
|
|
|
const path = getRefElem(attr).firstChild;
|
|
|
|
const cpXform = getTransformList(path);
|
|
|
|
const newxlate = context_.getSVGRoot().createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newxlate.setTranslate(tx, ty);
|
2013-02-20 06:29:25 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
cpXform.appendItem(newxlate);
|
2013-02-20 06:29:25 +00:00
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// Update clipPath's dimensions
|
2018-05-18 03:25:45 +00:00
|
|
|
recalculateDimensions(path);
|
2013-02-20 06:29:25 +00:00
|
|
|
};
|
|
|
|
|
2018-05-29 12:31:24 +00:00
|
|
|
/**
|
|
|
|
* Decides the course of action based on the element's transform list
|
|
|
|
* @param selected - The DOM element to recalculate
|
|
|
|
* @returns Undo command object with the resulting change
|
|
|
|
*/
|
2018-05-18 03:25:45 +00:00
|
|
|
export const recalculateDimensions = function (selected) {
|
2018-05-18 04:02:30 +00:00
|
|
|
if (selected == null) { return null; }
|
|
|
|
|
|
|
|
// Firefox Issue - 1081
|
2018-05-18 03:25:45 +00:00
|
|
|
if (selected.nodeName === 'svg' && navigator.userAgent.includes('Firefox/20')) {
|
2018-05-18 04:02:30 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const svgroot = context_.getSVGRoot();
|
|
|
|
const tlist = getTransformList(selected);
|
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// remove any unnecessary transforms
|
|
|
|
if (tlist && tlist.numberOfItems > 0) {
|
2018-05-18 03:25:45 +00:00
|
|
|
let k = tlist.numberOfItems;
|
|
|
|
const noi = k;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (k--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const xform = tlist.getItem(k);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (xform.type === 0) {
|
|
|
|
tlist.removeItem(k);
|
|
|
|
// remove identity matrices
|
|
|
|
} else if (xform.type === 1) {
|
2018-05-18 03:25:45 +00:00
|
|
|
if (isIdentity(xform.matrix)) {
|
2018-05-18 04:02:30 +00:00
|
|
|
if (noi === 1) {
|
|
|
|
// Overcome Chrome bug (though only when noi is 1) with
|
|
|
|
// `removeItem` preventing `removeAttribute` from
|
|
|
|
// subsequently working
|
|
|
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=843901
|
|
|
|
selected.removeAttribute('transform');
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
tlist.removeItem(k);
|
|
|
|
}
|
|
|
|
// remove zero-degree rotations
|
|
|
|
} else if (xform.type === 4) {
|
|
|
|
if (xform.angle === 0) {
|
|
|
|
tlist.removeItem(k);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// End here if all it has is a rotation
|
|
|
|
if (tlist.numberOfItems === 1 &&
|
2018-05-18 03:25:45 +00:00
|
|
|
getRotationAngle(selected)) { return null; }
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if this element had no transforms, we are done
|
|
|
|
if (!tlist || tlist.numberOfItems === 0) {
|
|
|
|
// Chrome apparently had a bug that requires clearing the attribute first.
|
|
|
|
selected.setAttribute('transform', '');
|
|
|
|
// However, this still next line currently doesn't work at all in Chrome
|
|
|
|
selected.removeAttribute('transform');
|
|
|
|
// selected.transform.baseVal.clear(); // Didn't help for Chrome bug
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Make this work for more than 2
|
|
|
|
if (tlist) {
|
2018-05-18 03:25:45 +00:00
|
|
|
let mxs = [];
|
|
|
|
let k = tlist.numberOfItems;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (k--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const xform = tlist.getItem(k);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (xform.type === 1) {
|
|
|
|
mxs.push([xform.matrix, k]);
|
|
|
|
} else if (mxs.length) {
|
|
|
|
mxs = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (mxs.length === 2) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const mNew = svgroot.createSVGTransformFromMatrix(matrixMultiply(mxs[1][0], mxs[0][0]));
|
2018-05-18 04:02:30 +00:00
|
|
|
tlist.removeItem(mxs[0][1]);
|
|
|
|
tlist.removeItem(mxs[1][1]);
|
|
|
|
tlist.insertItemBefore(mNew, mxs[1][1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// combine matrix + translate
|
|
|
|
k = tlist.numberOfItems;
|
|
|
|
if (k >= 2 && tlist.getItem(k - 2).type === 1 && tlist.getItem(k - 1).type === 2) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const mt = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const m = matrixMultiply(
|
2018-05-18 04:02:30 +00:00
|
|
|
tlist.getItem(k - 2).matrix,
|
|
|
|
tlist.getItem(k - 1).matrix);
|
|
|
|
mt.setMatrix(m);
|
|
|
|
tlist.removeItem(k - 2);
|
|
|
|
tlist.removeItem(k - 2);
|
|
|
|
tlist.appendItem(mt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If it still has a single [M] or [R][M], return null too (prevents BatchCommand from being returned).
|
|
|
|
switch (selected.tagName) {
|
|
|
|
// Ignore these elements, as they can absorb the [M]
|
|
|
|
case 'line':
|
|
|
|
case 'polyline':
|
|
|
|
case 'polygon':
|
|
|
|
case 'path':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if ((tlist.numberOfItems === 1 && tlist.getItem(0).type === 1) ||
|
|
|
|
(tlist.numberOfItems === 2 && tlist.getItem(0).type === 1 && tlist.getItem(0).type === 4)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Grouped SVG element
|
2018-05-18 03:25:45 +00:00
|
|
|
const gsvg = $(selected).data('gsvg');
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// we know we have some transforms, so set up return variable
|
2018-05-18 03:25:45 +00:00
|
|
|
const batchCmd = new BatchCommand('Transform');
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// store initial values that will be affected by reducing the transform list
|
2018-05-18 03:25:45 +00:00
|
|
|
let changes = {};
|
|
|
|
let initial = null;
|
|
|
|
let attrs = [];
|
2018-05-18 04:02:30 +00:00
|
|
|
switch (selected.tagName) {
|
|
|
|
case 'line':
|
|
|
|
attrs = ['x1', 'y1', 'x2', 'y2'];
|
|
|
|
break;
|
|
|
|
case 'circle':
|
|
|
|
attrs = ['cx', 'cy', 'r'];
|
|
|
|
break;
|
|
|
|
case 'ellipse':
|
|
|
|
attrs = ['cx', 'cy', 'rx', 'ry'];
|
|
|
|
break;
|
|
|
|
case 'foreignObject':
|
|
|
|
case 'rect':
|
|
|
|
case 'image':
|
|
|
|
attrs = ['width', 'height', 'x', 'y'];
|
|
|
|
break;
|
|
|
|
case 'use':
|
|
|
|
case 'text':
|
|
|
|
case 'tspan':
|
|
|
|
attrs = ['x', 'y'];
|
|
|
|
break;
|
|
|
|
case 'polygon':
|
2018-05-18 03:25:45 +00:00
|
|
|
case 'polyline': {
|
2018-05-18 04:02:30 +00:00
|
|
|
initial = {};
|
|
|
|
initial.points = selected.getAttribute('points');
|
2018-05-18 03:25:45 +00:00
|
|
|
const list = selected.points;
|
|
|
|
const len = list.numberOfItems;
|
2018-05-18 04:02:30 +00:00
|
|
|
changes.points = new Array(len);
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 0; i < len; ++i) {
|
|
|
|
const pt = list.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
changes.points[i] = {x: pt.x, y: pt.y};
|
|
|
|
}
|
|
|
|
break;
|
2018-05-18 03:25:45 +00:00
|
|
|
} case 'path':
|
2018-05-18 04:02:30 +00:00
|
|
|
initial = {};
|
|
|
|
initial.d = selected.getAttribute('d');
|
|
|
|
changes.d = selected.getAttribute('d');
|
|
|
|
break;
|
|
|
|
} // switch on element type to get initial values
|
|
|
|
|
|
|
|
if (attrs.length) {
|
|
|
|
changes = $(selected).attr(attrs);
|
|
|
|
$.each(changes, function (attr, val) {
|
2018-05-18 03:25:45 +00:00
|
|
|
changes[attr] = convertToNum(attr, val);
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
} else if (gsvg) {
|
|
|
|
// GSVG exception
|
|
|
|
changes = {
|
|
|
|
x: $(gsvg).attr('x') || 0,
|
|
|
|
y: $(gsvg).attr('y') || 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we haven't created an initial array in polygon/polyline/path, then
|
|
|
|
// make a copy of initial values and include the transform
|
|
|
|
if (initial == null) {
|
|
|
|
initial = $.extend(true, {}, changes);
|
|
|
|
$.each(initial, function (attr, val) {
|
2018-05-18 03:25:45 +00:00
|
|
|
initial[attr] = convertToNum(attr, val);
|
2018-05-18 04:02:30 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
// save the start transform value too
|
|
|
|
initial.transform = context_.getStartTransform() || '';
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let oldcenter, newcenter;
|
|
|
|
|
2018-05-18 04:02:30 +00:00
|
|
|
// if it's a regular group, we have special processing to flatten transforms
|
|
|
|
if ((selected.tagName === 'g' && !gsvg) || selected.tagName === 'a') {
|
2018-05-18 03:25:45 +00:00
|
|
|
const box = getBBox(selected);
|
|
|
|
|
|
|
|
oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};
|
|
|
|
newcenter = transformPoint(
|
|
|
|
box.x + box.width / 2,
|
|
|
|
box.y + box.height / 2,
|
|
|
|
transformListToTransform(tlist).matrix
|
|
|
|
);
|
|
|
|
let m = svgroot.createSVGMatrix();
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// temporarily strip off the rotate and save the old center
|
2018-05-18 03:25:45 +00:00
|
|
|
const gangle = getRotationAngle(selected);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (gangle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const a = gangle * Math.PI / 180;
|
|
|
|
let s;
|
2018-05-18 04:02:30 +00:00
|
|
|
if (Math.abs(a) > (1.0e-10)) {
|
2018-05-18 03:25:45 +00:00
|
|
|
s = Math.sin(a) / (1 - Math.cos(a));
|
2018-05-18 04:02:30 +00:00
|
|
|
} else {
|
|
|
|
// FIXME: This blows up if the angle is exactly 0!
|
2018-05-18 03:25:45 +00:00
|
|
|
s = 2 / a;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 0; i < tlist.numberOfItems; ++i) {
|
|
|
|
const xform = tlist.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (xform.type === 4) {
|
|
|
|
// extract old center through mystical arts
|
2018-05-18 03:25:45 +00:00
|
|
|
const rm = xform.matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
oldcenter.y = (s * rm.e + rm.f) / 2;
|
|
|
|
oldcenter.x = (rm.e - s * rm.f) / 2;
|
|
|
|
tlist.removeItem(i);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
const N = tlist.numberOfItems;
|
|
|
|
let tx = 0, ty = 0, operation = 0;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let firstM;
|
2018-05-18 04:02:30 +00:00
|
|
|
if (N) {
|
2018-05-18 03:25:45 +00:00
|
|
|
firstM = tlist.getItem(0).matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let oldStartTransform;
|
2018-05-18 04:02:30 +00:00
|
|
|
// first, if it was a scale then the second-last transform will be it
|
|
|
|
if (N >= 3 && tlist.getItem(N - 2).type === 3 &&
|
|
|
|
tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {
|
|
|
|
operation = 3; // scale
|
|
|
|
|
|
|
|
// if the children are unrotated, pass the scale down directly
|
|
|
|
// otherwise pass the equivalent matrix() down directly
|
2018-05-18 03:25:45 +00:00
|
|
|
const tm = tlist.getItem(N - 3).matrix,
|
2018-05-18 04:02:30 +00:00
|
|
|
sm = tlist.getItem(N - 2).matrix,
|
|
|
|
tmn = tlist.getItem(N - 1).matrix;
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const children = selected.childNodes;
|
|
|
|
let c = children.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (c--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const child = children.item(c);
|
2018-05-18 04:02:30 +00:00
|
|
|
tx = 0;
|
|
|
|
ty = 0;
|
|
|
|
if (child.nodeType === 1) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const childTlist = getTransformList(child);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// some children might not have a transform (<metadata>, <defs>, etc)
|
|
|
|
if (!childTlist) { continue; }
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const m = transformListToTransform(childTlist).matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// Convert a matrix to a scale if applicable
|
2018-05-18 03:25:45 +00:00
|
|
|
// if (hasMatrixTransform(childTlist) && childTlist.numberOfItems == 1) {
|
2018-05-18 06:41:43 +00:00
|
|
|
// if (m.b==0 && m.c==0 && m.e==0 && m.f==0) {
|
|
|
|
// childTlist.removeItem(0);
|
2018-05-18 03:25:45 +00:00
|
|
|
// const translateOrigin = svgroot.createSVGTransform(),
|
2018-05-18 06:41:43 +00:00
|
|
|
// scale = svgroot.createSVGTransform(),
|
|
|
|
// translateBack = svgroot.createSVGTransform();
|
|
|
|
// translateOrigin.setTranslate(0, 0);
|
|
|
|
// scale.setScale(m.a, m.d);
|
|
|
|
// translateBack.setTranslate(0, 0);
|
|
|
|
// childTlist.appendItem(translateBack);
|
|
|
|
// childTlist.appendItem(scale);
|
|
|
|
// childTlist.appendItem(translateOrigin);
|
|
|
|
// }
|
2018-05-18 04:02:30 +00:00
|
|
|
// }
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const angle = getRotationAngle(child);
|
|
|
|
oldStartTransform = context_.getStartTransform();
|
|
|
|
const childxforms = [];
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(child.getAttribute('transform'));
|
2018-05-18 03:25:45 +00:00
|
|
|
if (angle || hasMatrixTransform(childTlist)) {
|
|
|
|
const e2t = svgroot.createSVGTransform();
|
|
|
|
e2t.setMatrix(matrixMultiply(tm, sm, tmn, m));
|
2018-05-18 04:02:30 +00:00
|
|
|
childTlist.clear();
|
|
|
|
childTlist.appendItem(e2t);
|
|
|
|
childxforms.push(e2t);
|
|
|
|
// if not rotated or skewed, push the [T][S][-T] down to the child
|
|
|
|
} else {
|
|
|
|
// update the transform list with translate,scale,translate
|
|
|
|
|
|
|
|
// slide the [T][S][-T] from the front to the back
|
|
|
|
// [T][S][-T][M] = [M][T2][S2][-T2]
|
|
|
|
|
|
|
|
// (only bringing [-T] to the right of [M])
|
|
|
|
// [T][S][-T][M] = [T][S][M][-T2]
|
|
|
|
// [-T2] = [M_inv][-T][M]
|
2018-05-18 03:25:45 +00:00
|
|
|
const t2n = matrixMultiply(m.inverse(), tmn, m);
|
2018-05-18 04:02:30 +00:00
|
|
|
// [T2] is always negative translation of [-T2]
|
2018-05-18 03:25:45 +00:00
|
|
|
const t2 = svgroot.createSVGMatrix();
|
2018-05-18 04:02:30 +00:00
|
|
|
t2.e = -t2n.e;
|
|
|
|
t2.f = -t2n.f;
|
|
|
|
|
|
|
|
// [T][S][-T][M] = [M][T2][S2][-T2]
|
|
|
|
// [S2] = [T2_inv][M_inv][T][S][-T][M][-T2_inv]
|
2018-05-18 03:25:45 +00:00
|
|
|
const s2 = matrixMultiply(t2.inverse(), m.inverse(), tm, sm, tmn, m, t2n.inverse());
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const translateOrigin = svgroot.createSVGTransform(),
|
2018-05-18 04:02:30 +00:00
|
|
|
scale = svgroot.createSVGTransform(),
|
|
|
|
translateBack = svgroot.createSVGTransform();
|
|
|
|
translateOrigin.setTranslate(t2n.e, t2n.f);
|
|
|
|
scale.setScale(s2.a, s2.d);
|
|
|
|
translateBack.setTranslate(t2.e, t2.f);
|
|
|
|
childTlist.appendItem(translateBack);
|
|
|
|
childTlist.appendItem(scale);
|
|
|
|
childTlist.appendItem(translateOrigin);
|
|
|
|
childxforms.push(translateBack);
|
|
|
|
childxforms.push(scale);
|
|
|
|
childxforms.push(translateOrigin);
|
|
|
|
// logMatrix(translateBack.matrix);
|
|
|
|
// logMatrix(scale.matrix);
|
|
|
|
} // not rotated
|
2018-05-18 03:25:45 +00:00
|
|
|
batchCmd.addSubCommand(recalculateDimensions(child));
|
2018-05-18 04:02:30 +00:00
|
|
|
// TODO: If any <use> have this group as a parent and are
|
|
|
|
// referencing this child, then we need to impose a reverse
|
|
|
|
// scale on it so that when it won't get double-translated
|
2018-05-18 03:25:45 +00:00
|
|
|
// const uses = selected.getElementsByTagNameNS(NS.SVG, 'use');
|
|
|
|
// const href = '#' + child.id;
|
|
|
|
// let u = uses.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
// while (u--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
// const useElem = uses.item(u);
|
|
|
|
// if (href == getHref(useElem)) {
|
|
|
|
// const usexlate = svgroot.createSVGTransform();
|
2018-05-18 06:41:43 +00:00
|
|
|
// usexlate.setTranslate(-tx,-ty);
|
2018-05-18 03:25:45 +00:00
|
|
|
// getTransformList(useElem).insertItemBefore(usexlate,0);
|
|
|
|
// batchCmd.addSubCommand( recalculateDimensions(useElem) );
|
2018-05-18 06:41:43 +00:00
|
|
|
// }
|
2018-05-18 04:02:30 +00:00
|
|
|
// }
|
|
|
|
context_.setStartTransform(oldStartTransform);
|
|
|
|
} // element
|
|
|
|
} // for each child
|
|
|
|
// Remove these transforms from group
|
|
|
|
tlist.removeItem(N - 1);
|
|
|
|
tlist.removeItem(N - 2);
|
|
|
|
tlist.removeItem(N - 3);
|
|
|
|
} else if (N >= 3 && tlist.getItem(N - 1).type === 1) {
|
|
|
|
operation = 3; // scale
|
2018-05-18 03:25:45 +00:00
|
|
|
m = transformListToTransform(tlist).matrix;
|
|
|
|
const e2t = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
e2t.setMatrix(m);
|
|
|
|
tlist.clear();
|
|
|
|
tlist.appendItem(e2t);
|
|
|
|
// next, check if the first transform was a translate
|
|
|
|
// if we had [ T1 ] [ M ] we want to transform this into [ M ] [ T2 ]
|
|
|
|
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ]
|
|
|
|
} else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
|
|
|
|
tlist.getItem(0).type === 2) {
|
|
|
|
operation = 2; // translate
|
2018-05-18 03:25:45 +00:00
|
|
|
const T_M = transformListToTransform(tlist).matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
tlist.removeItem(0);
|
2018-05-18 03:25:45 +00:00
|
|
|
const mInv = transformListToTransform(tlist).matrix.inverse();
|
|
|
|
const M2 = matrixMultiply(mInv, T_M);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
tx = M2.e;
|
|
|
|
ty = M2.f;
|
|
|
|
|
|
|
|
if (tx !== 0 || ty !== 0) {
|
|
|
|
// we pass the translates down to the individual children
|
2018-05-18 03:25:45 +00:00
|
|
|
const children = selected.childNodes;
|
|
|
|
let c = children.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let clipPathsDone = [];
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
while (c--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const child = children.item(c);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (child.nodeType === 1) {
|
|
|
|
// Check if child has clip-path
|
|
|
|
if (child.getAttribute('clip-path')) {
|
|
|
|
// tx, ty
|
2018-05-18 03:25:45 +00:00
|
|
|
const attr = child.getAttribute('clip-path');
|
|
|
|
if (!clipPathsDone.includes(attr)) {
|
|
|
|
updateClipPath(attr, tx, ty);
|
2018-05-18 04:02:30 +00:00
|
|
|
clipPathsDone.push(attr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
oldStartTransform = context_.getStartTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(child.getAttribute('transform'));
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const childTlist = getTransformList(child);
|
2018-05-18 04:02:30 +00:00
|
|
|
// some children might not have a transform (<metadata>, <defs>, etc)
|
|
|
|
if (childTlist) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const newxlate = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newxlate.setTranslate(tx, ty);
|
|
|
|
if (childTlist.numberOfItems) {
|
|
|
|
childTlist.insertItemBefore(newxlate, 0);
|
|
|
|
} else {
|
|
|
|
childTlist.appendItem(newxlate);
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
batchCmd.addSubCommand(recalculateDimensions(child));
|
2018-05-18 04:02:30 +00:00
|
|
|
// If any <use> have this group as a parent and are
|
|
|
|
// referencing this child, then impose a reverse translate on it
|
|
|
|
// so that when it won't get double-translated
|
2018-05-18 03:25:45 +00:00
|
|
|
const uses = selected.getElementsByTagNameNS(NS.SVG, 'use');
|
|
|
|
const href = '#' + child.id;
|
|
|
|
let u = uses.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (u--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const useElem = uses.item(u);
|
|
|
|
if (href === getHref(useElem)) {
|
|
|
|
const usexlate = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
usexlate.setTranslate(-tx, -ty);
|
2018-05-18 03:25:45 +00:00
|
|
|
getTransformList(useElem).insertItemBefore(usexlate, 0);
|
|
|
|
batchCmd.addSubCommand(recalculateDimensions(useElem));
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
context_.setStartTransform(oldStartTransform);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
clipPathsDone = [];
|
|
|
|
context_.setStartTransform(oldStartTransform);
|
|
|
|
}
|
|
|
|
// else, a matrix imposition from a parent group
|
|
|
|
// keep pushing it down to the children
|
|
|
|
} else if (N === 1 && tlist.getItem(0).type === 1 && !gangle) {
|
|
|
|
operation = 1;
|
2018-05-18 03:25:45 +00:00
|
|
|
const m = tlist.getItem(0).matrix,
|
|
|
|
children = selected.childNodes;
|
|
|
|
let c = children.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (c--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const child = children.item(c);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (child.nodeType === 1) {
|
2018-05-18 03:25:45 +00:00
|
|
|
oldStartTransform = context_.getStartTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(child.getAttribute('transform'));
|
2018-05-18 03:25:45 +00:00
|
|
|
const childTlist = getTransformList(child);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
if (!childTlist) { continue; }
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const em = matrixMultiply(m, transformListToTransform(childTlist).matrix);
|
|
|
|
const e2m = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
e2m.setMatrix(em);
|
|
|
|
childTlist.clear();
|
|
|
|
childTlist.appendItem(e2m, 0);
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
batchCmd.addSubCommand(recalculateDimensions(child));
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(oldStartTransform);
|
|
|
|
|
|
|
|
// Convert stroke
|
|
|
|
// TODO: Find out if this should actually happen somewhere else
|
2018-05-18 03:25:45 +00:00
|
|
|
const sw = child.getAttribute('stroke-width');
|
2018-05-18 04:02:30 +00:00
|
|
|
if (child.getAttribute('stroke') !== 'none' && !isNaN(sw)) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const avg = (Math.abs(em.a) + Math.abs(em.d)) / 2;
|
2018-05-18 04:02:30 +00:00
|
|
|
child.setAttribute('stroke-width', sw * avg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
tlist.clear();
|
|
|
|
// else it was just a rotate
|
|
|
|
} else {
|
|
|
|
if (gangle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const newRot = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newRot.setRotate(gangle, newcenter.x, newcenter.y);
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(newRot, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(newRot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (tlist.numberOfItems === 0) {
|
|
|
|
selected.removeAttribute('transform');
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it was a translate, put back the rotate at the new center
|
|
|
|
if (operation === 2) {
|
|
|
|
if (gangle) {
|
|
|
|
newcenter = {
|
|
|
|
x: oldcenter.x + firstM.e,
|
|
|
|
y: oldcenter.y + firstM.f
|
|
|
|
};
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const newRot = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newRot.setRotate(gangle, newcenter.x, newcenter.y);
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(newRot, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(newRot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if it was a resize
|
|
|
|
} else if (operation === 3) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const m = transformListToTransform(tlist).matrix;
|
|
|
|
const roldt = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
roldt.setRotate(gangle, oldcenter.x, oldcenter.y);
|
2018-05-18 03:25:45 +00:00
|
|
|
const rold = roldt.matrix;
|
|
|
|
const rnew = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
rnew.setRotate(gangle, newcenter.x, newcenter.y);
|
2018-05-18 03:25:45 +00:00
|
|
|
const rnewInv = rnew.matrix.inverse(),
|
2018-05-18 04:02:30 +00:00
|
|
|
mInv = m.inverse(),
|
2018-05-18 03:25:45 +00:00
|
|
|
extrat = matrixMultiply(mInv, rnewInv, rold, m);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
tx = extrat.e;
|
|
|
|
ty = extrat.f;
|
|
|
|
|
|
|
|
if (tx !== 0 || ty !== 0) {
|
|
|
|
// now push this transform down to the children
|
|
|
|
// we pass the translates down to the individual children
|
2018-05-18 03:25:45 +00:00
|
|
|
const children = selected.childNodes;
|
|
|
|
let c = children.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (c--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const child = children.item(c);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (child.nodeType === 1) {
|
2018-05-18 03:25:45 +00:00
|
|
|
oldStartTransform = context_.getStartTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(child.getAttribute('transform'));
|
2018-05-18 03:25:45 +00:00
|
|
|
const childTlist = getTransformList(child);
|
|
|
|
const newxlate = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newxlate.setTranslate(tx, ty);
|
|
|
|
if (childTlist.numberOfItems) {
|
|
|
|
childTlist.insertItemBefore(newxlate, 0);
|
|
|
|
} else {
|
|
|
|
childTlist.appendItem(newxlate);
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
batchCmd.addSubCommand(recalculateDimensions(child));
|
2018-05-18 04:02:30 +00:00
|
|
|
context_.setStartTransform(oldStartTransform);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (gangle) {
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(rnew, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(rnew);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// else, it's a non-group
|
|
|
|
} else {
|
|
|
|
// FIXME: box might be null for some elements (<metadata> etc), need to handle this
|
2018-05-18 03:25:45 +00:00
|
|
|
const box = getBBox(selected);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// Paths (and possbly other shapes) will have no BBox while still in <defs>,
|
|
|
|
// but we still may need to recalculate them (see issue 595).
|
|
|
|
// TODO: Figure out how to get BBox from these elements in case they
|
|
|
|
// have a rotation transform
|
|
|
|
|
|
|
|
if (!box && selected.tagName !== 'path') return null;
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
let m = svgroot.createSVGMatrix();
|
|
|
|
// temporarily strip off the rotate and save the old center
|
|
|
|
const angle = getRotationAngle(selected);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (angle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
oldcenter = {x: box.x + box.width / 2, y: box.y + box.height / 2};
|
|
|
|
newcenter = transformPoint(
|
|
|
|
box.x + box.width / 2,
|
|
|
|
box.y + box.height / 2,
|
|
|
|
transformListToTransform(tlist).matrix
|
|
|
|
);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
const a = angle * Math.PI / 180;
|
|
|
|
const s = (Math.abs(a) > (1.0e-10))
|
|
|
|
? Math.sin(a) / (1 - Math.cos(a))
|
2018-05-18 04:02:30 +00:00
|
|
|
// FIXME: This blows up if the angle is exactly 0!
|
2018-05-18 03:25:45 +00:00
|
|
|
: 2 / a;
|
|
|
|
|
|
|
|
for (let i = 0; i < tlist.numberOfItems; ++i) {
|
|
|
|
const xform = tlist.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (xform.type === 4) {
|
|
|
|
// extract old center through mystical arts
|
2018-05-18 03:25:45 +00:00
|
|
|
const rm = xform.matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
oldcenter.y = (s * rm.e + rm.f) / 2;
|
|
|
|
oldcenter.x = (rm.e - s * rm.f) / 2;
|
|
|
|
tlist.removeItem(i);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2 = translate, 3 = scale, 4 = rotate, 1 = matrix imposition
|
2018-05-18 03:25:45 +00:00
|
|
|
let operation = 0;
|
|
|
|
const N = tlist.numberOfItems;
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
// Check if it has a gradient with userSpaceOnUse, in which case
|
|
|
|
// adjust it by recalculating the matrix transform.
|
|
|
|
// TODO: Make this work in Webkit using svgedit.transformlist.SVGTransformList
|
2018-05-18 03:25:45 +00:00
|
|
|
if (!isWebkit()) {
|
|
|
|
const fill = selected.getAttribute('fill');
|
|
|
|
if (fill && fill.startsWith('url(')) {
|
|
|
|
const paint = getRefElem(fill);
|
|
|
|
let type = 'pattern';
|
2018-05-18 04:02:30 +00:00
|
|
|
if (paint.tagName !== type) type = 'gradient';
|
2018-05-18 03:25:45 +00:00
|
|
|
const attrVal = paint.getAttribute(type + 'Units');
|
2018-05-18 04:02:30 +00:00
|
|
|
if (attrVal === 'userSpaceOnUse') {
|
|
|
|
// Update the userSpaceOnUse element
|
2018-05-18 03:25:45 +00:00
|
|
|
m = transformListToTransform(tlist).matrix;
|
|
|
|
const gtlist = getTransformList(paint);
|
|
|
|
const gmatrix = transformListToTransform(gtlist).matrix;
|
|
|
|
m = matrixMultiply(m, gmatrix);
|
|
|
|
const mStr = 'matrix(' + [m.a, m.b, m.c, m.d, m.e, m.f].join(',') + ')';
|
2018-05-18 04:02:30 +00:00
|
|
|
paint.setAttribute(type + 'Transform', mStr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// first, if it was a scale of a non-skewed element, then the second-last
|
|
|
|
// transform will be the [S]
|
|
|
|
// if we had [M][T][S][T] we want to extract the matrix equivalent of
|
|
|
|
// [T][S][T] and push it down to the element
|
|
|
|
if (N >= 3 && tlist.getItem(N - 2).type === 3 &&
|
|
|
|
tlist.getItem(N - 3).type === 2 && tlist.getItem(N - 1).type === 2) {
|
|
|
|
// Removed this so a <use> with a given [T][S][T] would convert to a matrix.
|
|
|
|
// Is that bad?
|
2018-05-18 06:41:43 +00:00
|
|
|
// && selected.nodeName != 'use'
|
2018-05-18 04:02:30 +00:00
|
|
|
operation = 3; // scale
|
2018-05-18 03:25:45 +00:00
|
|
|
m = transformListToTransform(tlist, N - 3, N - 1).matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
tlist.removeItem(N - 1);
|
|
|
|
tlist.removeItem(N - 2);
|
|
|
|
tlist.removeItem(N - 3);
|
|
|
|
// if we had [T][S][-T][M], then this was a skewed element being resized
|
|
|
|
// Thus, we simply combine it all into one matrix
|
|
|
|
} else if (N === 4 && tlist.getItem(N - 1).type === 1) {
|
|
|
|
operation = 3; // scale
|
2018-05-18 03:25:45 +00:00
|
|
|
m = transformListToTransform(tlist).matrix;
|
|
|
|
const e2t = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
e2t.setMatrix(m);
|
|
|
|
tlist.clear();
|
|
|
|
tlist.appendItem(e2t);
|
|
|
|
// reset the matrix so that the element is not re-mapped
|
|
|
|
m = svgroot.createSVGMatrix();
|
|
|
|
// if we had [R][T][S][-T][M], then this was a rotated matrix-element
|
|
|
|
// if we had [T1][M] we want to transform this into [M][T2]
|
|
|
|
// therefore [ T2 ] = [ M_inv ] [ T1 ] [ M ] and we can push [T2]
|
|
|
|
// down to the element
|
|
|
|
} else if ((N === 1 || (N > 1 && tlist.getItem(1).type !== 3)) &&
|
|
|
|
tlist.getItem(0).type === 2) {
|
|
|
|
operation = 2; // translate
|
2018-05-18 03:25:45 +00:00
|
|
|
const oldxlate = tlist.getItem(0).matrix,
|
|
|
|
meq = transformListToTransform(tlist, 1).matrix,
|
2018-05-18 04:02:30 +00:00
|
|
|
meqInv = meq.inverse();
|
2018-05-18 03:25:45 +00:00
|
|
|
m = matrixMultiply(meqInv, oldxlate, meq);
|
2018-05-18 04:02:30 +00:00
|
|
|
tlist.removeItem(0);
|
|
|
|
// else if this child now has a matrix imposition (from a parent group)
|
|
|
|
// we might be able to simplify
|
|
|
|
} else if (N === 1 && tlist.getItem(0).type === 1 && !angle) {
|
|
|
|
// Remap all point-based elements
|
2018-05-18 03:25:45 +00:00
|
|
|
m = transformListToTransform(tlist).matrix;
|
2018-05-18 04:02:30 +00:00
|
|
|
switch (selected.tagName) {
|
|
|
|
case 'line':
|
|
|
|
changes = $(selected).attr(['x1', 'y1', 'x2', 'y2']);
|
|
|
|
// Fallthrough
|
|
|
|
case 'polyline':
|
|
|
|
case 'polygon':
|
|
|
|
changes.points = selected.getAttribute('points');
|
|
|
|
if (changes.points) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const list = selected.points;
|
|
|
|
const len = list.numberOfItems;
|
2018-05-18 04:02:30 +00:00
|
|
|
changes.points = new Array(len);
|
2018-05-18 03:25:45 +00:00
|
|
|
for (let i = 0; i < len; ++i) {
|
|
|
|
const pt = list.getItem(i);
|
2018-05-18 04:02:30 +00:00
|
|
|
changes.points[i] = {x: pt.x, y: pt.y};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Fallthrough
|
|
|
|
case 'path':
|
|
|
|
changes.d = selected.getAttribute('d');
|
|
|
|
operation = 1;
|
|
|
|
tlist.clear();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// if it was a rotation, put the rotate back and return without a command
|
|
|
|
// (this function has zero work to do for a rotate())
|
|
|
|
} else {
|
|
|
|
operation = 4; // rotation
|
|
|
|
if (angle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const newRot = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newRot.setRotate(angle, newcenter.x, newcenter.y);
|
|
|
|
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(newRot, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(newRot);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (tlist.numberOfItems === 0) {
|
|
|
|
selected.removeAttribute('transform');
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if it was a translate or resize, we need to remap the element and absorb the xform
|
|
|
|
if (operation === 1 || operation === 2 || operation === 3) {
|
2018-05-18 03:25:45 +00:00
|
|
|
remapElement(selected, changes, m);
|
2018-05-18 04:02:30 +00:00
|
|
|
} // if we are remapping
|
|
|
|
|
|
|
|
// if it was a translate, put back the rotate at the new center
|
|
|
|
if (operation === 2) {
|
|
|
|
if (angle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
if (!hasMatrixTransform(tlist)) {
|
2018-05-18 04:02:30 +00:00
|
|
|
newcenter = {
|
|
|
|
x: oldcenter.x + m.e,
|
|
|
|
y: oldcenter.y + m.f
|
|
|
|
};
|
|
|
|
}
|
2018-05-18 03:25:45 +00:00
|
|
|
const newRot = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
newRot.setRotate(angle, newcenter.x, newcenter.y);
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(newRot, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(newRot);
|
|
|
|
}
|
|
|
|
}
|
2018-05-18 06:41:43 +00:00
|
|
|
// We have special processing for tspans: Tspans are not transformable
|
|
|
|
// but they can have x,y coordinates (sigh). Thus, if this was a translate,
|
2018-05-18 04:02:30 +00:00
|
|
|
// on a text element, also translate any tspan children.
|
|
|
|
if (selected.tagName === 'text') {
|
2018-05-18 03:25:45 +00:00
|
|
|
const children = selected.childNodes;
|
|
|
|
let c = children.length;
|
2018-05-18 04:02:30 +00:00
|
|
|
while (c--) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const child = children.item(c);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (child.tagName === 'tspan') {
|
2018-05-18 03:25:45 +00:00
|
|
|
const tspanChanges = {
|
2018-05-18 04:02:30 +00:00
|
|
|
x: $(child).attr('x') || 0,
|
|
|
|
y: $(child).attr('y') || 0
|
|
|
|
};
|
2018-05-18 03:25:45 +00:00
|
|
|
remapElement(child, tspanChanges, m);
|
2018-05-18 04:02:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// [Rold][M][T][S][-T] became [Rold][M]
|
|
|
|
// we want it to be [Rnew][M][Tr] where Tr is the
|
|
|
|
// translation required to re-center it
|
|
|
|
// Therefore, [Tr] = [M_inv][Rnew_inv][Rold][M]
|
|
|
|
} else if (operation === 3 && angle) {
|
2018-05-18 03:25:45 +00:00
|
|
|
const m = transformListToTransform(tlist).matrix;
|
|
|
|
const roldt = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
roldt.setRotate(angle, oldcenter.x, oldcenter.y);
|
2018-05-18 03:25:45 +00:00
|
|
|
const rold = roldt.matrix;
|
|
|
|
const rnew = svgroot.createSVGTransform();
|
2018-05-18 04:02:30 +00:00
|
|
|
rnew.setRotate(angle, newcenter.x, newcenter.y);
|
2018-05-18 03:25:45 +00:00
|
|
|
const rnewInv = rnew.matrix.inverse();
|
|
|
|
const mInv = m.inverse();
|
|
|
|
const extrat = matrixMultiply(mInv, rnewInv, rold, m);
|
2018-05-18 04:02:30 +00:00
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
remapElement(selected, changes, extrat);
|
2018-05-18 04:02:30 +00:00
|
|
|
if (angle) {
|
|
|
|
if (tlist.numberOfItems) {
|
|
|
|
tlist.insertItemBefore(rnew, 0);
|
|
|
|
} else {
|
|
|
|
tlist.appendItem(rnew);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} // a non-group
|
|
|
|
|
|
|
|
// if the transform list has been emptied, remove it
|
|
|
|
if (tlist.numberOfItems === 0) {
|
|
|
|
selected.removeAttribute('transform');
|
|
|
|
}
|
|
|
|
|
2018-05-18 03:25:45 +00:00
|
|
|
batchCmd.addSubCommand(new ChangeElementCommand(selected, initial));
|
2018-05-18 04:02:30 +00:00
|
|
|
|
|
|
|
return batchCmd;
|
2013-02-20 06:29:25 +00:00
|
|
|
};
|